diff --git a/claw/core/mcp_mapper.py b/claw/core/mcp_mapper.py index 3b1bb4a..cb9b741 100644 --- a/claw/core/mcp_mapper.py +++ b/claw/core/mcp_mapper.py @@ -8,7 +8,7 @@ from typing import Any, Protocol -from claude_agent_sdk.types import McpHttpServerConfig +from claude_agent_sdk.types import McpHttpServerConfig, McpServerConfig, McpStdioServerConfig from claw.core.auth import get_proxy_headers @@ -19,6 +19,7 @@ class _HasMcpConfig(Protocol): uc_mcp_connections: list[str] managed_mcps: list[str] custom_mcps: dict[str, str] + CLAW_MEMORY_VOLUME_PATH: str def uc_connection_to_mcp( @@ -116,13 +117,12 @@ def build_mcp_servers( workspace_host: str, sp_token: str, user_pat: str, -) -> dict[str, McpHttpServerConfig]: - """Build all MCP server configs: external + managed + custom. +) -> dict[str, McpServerConfig]: + """Build all MCP server configs: external + managed + custom + memory. - Returns an empty dict when nothing is configured, so the agent - runs with only its local tools. + Always includes the claw-memory stdio server for persistent memory. """ - result: dict[str, McpHttpServerConfig] = {} + result: dict[str, McpServerConfig] = {} # External (UC connections) for name in config.uc_mcp_connections: @@ -134,4 +134,17 @@ def build_mcp_servers( # Custom (arbitrary HTTP) result.update(build_custom_mcp_servers(config, workspace_host, sp_token, user_pat)) + # Memory — stdio MCP for read/write MEMORY.md on UC Volume + host = workspace_host.rstrip("/") + result["claw-memory"] = McpStdioServerConfig( + type="stdio", + command="python", + args=["-m", "claw.core.memory_mcp"], + env={ + "CLAW_MEMORY_VOLUME_PATH": config.CLAW_MEMORY_VOLUME_PATH, + "DATABRICKS_HOST": host, + "DATABRICKS_TOKEN": user_pat, + }, + ) + return result diff --git a/claw/core/memory_mcp.py b/claw/core/memory_mcp.py new file mode 100644 index 0000000..ae5abbb --- /dev/null +++ b/claw/core/memory_mcp.py @@ -0,0 +1,63 @@ +"""Stdio MCP server exposing read/write of MEMORY.md on UC Volume. + +Tools: +- read_memory — download MEMORY.md from the configured UC Volume path +- write_memory — upload content to MEMORY.md on the configured UC Volume path + +Config via env vars: CLAW_MEMORY_VOLUME_PATH, DATABRICKS_HOST, DATABRICKS_TOKEN +""" + +from __future__ import annotations + +import io +import os + +from databricks.sdk import WorkspaceClient +from databricks.sdk.errors import NotFound +from mcp.server import FastMCP + +mcp = FastMCP("claw-memory") + + +def _volume_path() -> str: + """Return the full UC Volume path to MEMORY.md.""" + base = os.environ["CLAW_MEMORY_VOLUME_PATH"].rstrip("/") + return f"{base}/MEMORY.md" + + +def _get_ws() -> WorkspaceClient: + """Create a WorkspaceClient from env vars.""" + return WorkspaceClient( + host=os.environ["DATABRICKS_HOST"], + token=os.environ["DATABRICKS_TOKEN"], + ) + + +@mcp.tool() +def read_memory_tool() -> str: + """Read the agent's persistent memory (MEMORY.md) from the UC Volume. + + Returns the full content as a string, or empty string if no memory exists yet. + """ + ws = _get_ws() + try: + response = ws.files.download(_volume_path()) + return response.contents.read().decode("utf-8") + except NotFound: + return "" + + +@mcp.tool() +def write_memory_tool(content: str) -> str: + """Write updated memory content to MEMORY.md on the UC Volume. + + Overwrites the entire file. Pass the complete desired content. + """ + ws = _get_ws() + buf = io.BytesIO(content.encode("utf-8")) + ws.files.upload(_volume_path(), contents=buf, overwrite=True) + return "Memory written successfully." + + +if __name__ == "__main__": + mcp.run(transport="stdio") diff --git a/claw/core/slack_poller.py b/claw/core/slack_poller.py index fee0511..8cb0e7c 100644 --- a/claw/core/slack_poller.py +++ b/claw/core/slack_poller.py @@ -12,7 +12,6 @@ from claw.core.agent import create_client, iter_response, send_message from claw.models import Session as SessionModel from claw.core.mcp_mapper import build_mcp_servers -from claw.core.memory import read_memory, write_memory from claw.core.sessions import close_session, get_active_sessions, get_recent_messages, get_session, get_sessions_to_evict, save_message, update_last_poll_ts, upsert_session from claw.core.slack_client import fetch_history, fetch_thread_replies, open_self_dm, reply_to_thread, resolve_identity @@ -226,10 +225,7 @@ async def _handle_message(self, message: dict) -> None: # Track this thread as active self._active_threads.add(thread_ts) - # Read memory - memory = read_memory(self._ws, self._config) - - # Build MCP servers + # Build MCP servers (includes claw-memory for persistent memory) mcp_servers = build_mcp_servers( self._config, self._ws.config.host, @@ -237,11 +233,7 @@ async def _handle_message(self, message: dict) -> None: user_pat=self._config.DATABRICKS_USER_PAT, ) - # Build prompt with memory context - if memory: - prompt = f"Memory:\n{memory}\n\nUser message:\n{text}" - else: - prompt = text + prompt = text # Get or create client (reuse from cache if available) client_is_new = thread_ts not in self._clients @@ -270,7 +262,7 @@ async def _handle_message(self, message: dict) -> None: # Send prompt and spawn background task for response collection await send_message(client, prompt) self._response_tasks[thread_ts] = asyncio.create_task( - self._run_response_loop(thread_ts, channel, memory, text) + self._run_response_loop(thread_ts, channel, text) ) except Exception: @@ -330,7 +322,7 @@ async def _evict_lra_client(self) -> None: except Exception: logger.exception("Failed to close session for evicted thread %s", lra_ts) - async def _run_response_loop(self, thread_ts: str, channel: str, memory: str, original_text: str) -> None: + async def _run_response_loop(self, thread_ts: str, channel: str, original_text: str) -> None: """Background task: collect iter_response chunks, reply, update DB.""" try: client = self._clients[thread_ts] @@ -391,9 +383,6 @@ async def _run_response_loop(self, thread_ts: str, channel: str, memory: str, or gen.is_error, ) - # Write memory - write_memory(self._ws, self._config, memory) - # Reply in thread result = await reply_to_thread( self._ws, diff --git a/openspec/changes/memory-mcp/proposal.md b/openspec/changes/memory-mcp/proposal.md new file mode 100644 index 0000000..cc319e4 --- /dev/null +++ b/openspec/changes/memory-mcp/proposal.md @@ -0,0 +1,31 @@ +# Memory MCP Server — stdio MCP for MEMORY.md on UC Volume + +## Problem +The current memory implementation injects read_memory into the prompt and calls write_memory with stale content at the end of every response loop. This is broken — write_memory writes back the *original* memory, not any updates the agent made. The agent has no way to actually persist memory changes. + +## Solution +Replace the manual read/write calls with a stdio MCP server that exposes `read_memory` and `write_memory` as tools the agent can call directly. + +### 1. `claw/core/memory_mcp.py` — stdio MCP server +- Uses `mcp.server.FastMCP` to create a stdio MCP server with two tools: + - `read_memory() -> str` — downloads MEMORY.md from UC Volume via `ws.files.download()` + - `write_memory(content: str) -> str` — uploads MEMORY.md via `ws.files.upload()` +- Config via env vars: `CLAW_MEMORY_VOLUME_PATH`, `DATABRICKS_HOST`, `DATABRICKS_TOKEN` +- Runnable as `python -m claw.core.memory_mcp` + +### 2. `claw/core/mcp_mapper.py` — add "claw-memory" entry +- Add `McpStdioServerConfig` entry to `build_mcp_servers()` return dict +- Update type annotations to return `dict[str, McpServerConfig]` (union of HTTP + stdio) + +### 3. `claw/core/slack_poller.py` — remove broken memory calls +- Remove `read_memory` call and prompt injection (lines ~229-243) +- Remove `write_memory` call at end of `_run_response_loop` (line ~395) +- Remove `memory` parameter from `_run_response_loop` +- Remove imports of `read_memory`/`write_memory` + +### 4. `pyproject.toml` — no changes needed +- `mcp` is already a transitive dependency of `claude-agent-sdk` + +## Tests +- `tests/test_memory_mcp.py` — unit tests for `read_memory_tool` and `write_memory_tool` (mock WorkspaceClient) +- `tests/test_mcp_mapper.py` — verify "claw-memory" key in `build_mcp_servers()` output diff --git a/openspec/changes/memory-mcp/specs/mcp_mapper.md b/openspec/changes/memory-mcp/specs/mcp_mapper.md new file mode 100644 index 0000000..64f4743 --- /dev/null +++ b/openspec/changes/memory-mcp/specs/mcp_mapper.md @@ -0,0 +1,28 @@ +# Spec: mcp_mapper.py changes + +## Changes to `build_mcp_servers()` + +### Return type +Change from `dict[str, McpHttpServerConfig]` to `dict[str, McpServerConfig]` (union type). + +### New entry +Add "claw-memory" stdio server entry after custom MCPs: + +```python +servers["claw-memory"] = McpStdioServerConfig( + type="stdio", + command="python", + args=["-m", "claw.core.memory_mcp"], + env={ + "CLAW_MEMORY_VOLUME_PATH": config.CLAW_MEMORY_VOLUME_PATH, + "DATABRICKS_HOST": workspace_host, + "DATABRICKS_TOKEN": user_pat, + }, +) +``` + +### Protocol update +`_HasMcpConfig` needs `CLAW_MEMORY_VOLUME_PATH: str` attribute. + +### Import +Add `McpStdioServerConfig, McpServerConfig` to imports from `claude_agent_sdk.types`. diff --git a/openspec/changes/memory-mcp/specs/memory_mcp.md b/openspec/changes/memory-mcp/specs/memory_mcp.md new file mode 100644 index 0000000..d2a244f --- /dev/null +++ b/openspec/changes/memory-mcp/specs/memory_mcp.md @@ -0,0 +1,27 @@ +# Spec: claw/core/memory_mcp.py + +## Module +`claw.core.memory_mcp` — stdio MCP server exposing read/write of MEMORY.md on UC Volume. + +## Public API (tool functions) + +### `read_memory_tool() -> str` +- Creates `WorkspaceClient(host=DATABRICKS_HOST, token=DATABRICKS_TOKEN)` +- Calls `ws.files.download("{CLAW_MEMORY_VOLUME_PATH}/MEMORY.md")` +- Returns content as UTF-8 string +- Returns empty string on `NotFound` + +### `write_memory_tool(content: str) -> str` +- Creates `WorkspaceClient(host=DATABRICKS_HOST, token=DATABRICKS_TOKEN)` +- Calls `ws.files.upload("{CLAW_MEMORY_VOLUME_PATH}/MEMORY.md", contents=BytesIO(content), overwrite=True)` +- Returns confirmation string + +## Server setup +- `mcp = FastMCP("claw-memory")` +- Register both tools with `@mcp.tool()` +- `__main__` block runs `mcp.run(transport="stdio")` + +## Config +- `CLAW_MEMORY_VOLUME_PATH` — env var, base path on UC Volume +- `DATABRICKS_HOST` — env var, workspace URL +- `DATABRICKS_TOKEN` — env var, PAT diff --git a/openspec/changes/memory-mcp/specs/slack_poller.md b/openspec/changes/memory-mcp/specs/slack_poller.md new file mode 100644 index 0000000..f759fc9 --- /dev/null +++ b/openspec/changes/memory-mcp/specs/slack_poller.md @@ -0,0 +1,12 @@ +# Spec: slack_poller.py changes + +## Removals + +1. Remove import: `from claw.core.memory import read_memory, write_memory` +2. In `_handle_message`: + - Remove `memory = read_memory(self._ws, self._config)` call + - Remove the `if memory: ... else: ...` prompt wrapping — just use `prompt = text` + - Remove `memory` arg from `_run_response_loop` call +3. In `_run_response_loop`: + - Remove `memory` parameter from signature + - Remove `write_memory(self._ws, self._config, memory)` call diff --git a/tests/test_e2e_smoke.py b/tests/test_e2e_smoke.py index 934f0ca..8e5c032 100644 --- a/tests/test_e2e_smoke.py +++ b/tests/test_e2e_smoke.py @@ -138,8 +138,6 @@ class TestFullPipeline: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="existing memory") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.update_last_poll_ts") @@ -158,8 +156,6 @@ async def test_poll_triggers_agent_and_reply( mock_update_poll_ts, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -212,12 +208,6 @@ async def test_poll_triggers_agent_and_reply( assert reply_args[3] == "200.001" # thread_ts assert reply_args[4] == "Hello from Claw" # concatenated response - # Verify memory was read - mock_read_mem.assert_called_once() - - # Verify memory was written - mock_write_mem.assert_called_once() - # Verify session was upserted (at least for initial + final update) assert mock_upsert_session.call_count >= 1 @@ -232,8 +222,6 @@ class TestThreadContinuity: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session") @@ -244,8 +232,6 @@ async def test_second_message_resumes_sdk_session( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, diff --git a/tests/test_mcp_mapper.py b/tests/test_mcp_mapper.py index 1c58e1c..dd1799d 100644 --- a/tests/test_mcp_mapper.py +++ b/tests/test_mcp_mapper.py @@ -53,14 +53,19 @@ def test_uc_connection_to_mcp_headers_use_two_token_proxy(): # build_mcp_servers — 0 connections # --------------------------------------------------------------------------- -def test_build_mcp_servers_empty_connections(): +def test_build_mcp_servers_empty_connections_still_has_memory(): from claw.core.mcp_mapper import build_mcp_servers config = MagicMock() config.uc_mcp_connections = [] + config.managed_mcps = [] + config.custom_mcps = {} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/cat/sch/vol" result = build_mcp_servers(config, "https://host.com", sp_token="sp-tok", user_pat="user-pat") - assert result == {} + # Only claw-memory should be present when no other MCPs configured + assert len(result) == 1 + assert "claw-memory" in result # --------------------------------------------------------------------------- @@ -72,6 +77,9 @@ def test_build_mcp_servers_single_connection(): config = MagicMock() config.uc_mcp_connections = ["github"] + config.managed_mcps = [] + config.custom_mcps = {} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/cat/sch/vol" result = build_mcp_servers(config, "https://host.com", sp_token="sp-tok", user_pat="user-pat") assert "ext-github" in result @@ -88,12 +96,15 @@ def test_build_mcp_servers_multiple_connections(): config = MagicMock() config.uc_mcp_connections = ["github", "jira"] + config.managed_mcps = [] + config.custom_mcps = {} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/cat/sch/vol" result = build_mcp_servers(config, "https://host.com/", sp_token="sp-tok", user_pat="user-pat") - assert len(result) == 2 assert "ext-github" in result assert "ext-jira" in result assert result["ext-jira"]["url"] == "https://host.com/api/2.0/mcp/external/jira" + assert len(result) == 3 # 2 external + claw-memory # --------------------------------------------------------------------------- @@ -284,9 +295,62 @@ def test_build_mcp_servers_merges_all_three(): config.uc_mcp_connections = ["github"] config.managed_mcps = ["genie:abc123"] config.custom_mcps = {"my-srv": "https://example.com/mcp"} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/cat/sch/vol" result = build_mcp_servers(config, "https://host.com", sp_token="sp-tok", user_pat="user-pat") assert "ext-github" in result assert "managed-genie-abc123" in result assert "custom-my-srv" in result - assert len(result) == 3 + assert "claw-memory" in result + assert len(result) == 4 + + +# --------------------------------------------------------------------------- +# claw-memory stdio server in build_mcp_servers +# --------------------------------------------------------------------------- + + +def test_build_mcp_servers_includes_claw_memory(): + from claw.core.mcp_mapper import build_mcp_servers + + config = MagicMock() + config.uc_mcp_connections = [] + config.managed_mcps = [] + config.custom_mcps = {} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/catalog/schema/vol" + + result = build_mcp_servers(config, "https://host.com", sp_token="sp-tok", user_pat="user-pat") + assert "claw-memory" in result + + +def test_claw_memory_is_stdio_type(): + from claw.core.mcp_mapper import build_mcp_servers + + config = MagicMock() + config.uc_mcp_connections = [] + config.managed_mcps = [] + config.custom_mcps = {} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/catalog/schema/vol" + + result = build_mcp_servers(config, "https://host.com", sp_token="sp-tok", user_pat="user-pat") + server = result["claw-memory"] + assert server["type"] == "stdio" + assert server["command"] == "python" + assert "-m" in server["args"] + assert "claw.core.memory_mcp" in server["args"] + + +def test_claw_memory_env_vars(): + from claw.core.mcp_mapper import build_mcp_servers + + config = MagicMock() + config.uc_mcp_connections = [] + config.managed_mcps = [] + config.custom_mcps = {} + config.CLAW_MEMORY_VOLUME_PATH = "/Volumes/catalog/schema/vol" + + result = build_mcp_servers(config, "https://test.cloud.databricks.com", sp_token="sp-tok", user_pat="user-pat") + env = result["claw-memory"]["env"] + assert env["CLAW_MEMORY_VOLUME_PATH"] == "/Volumes/catalog/schema/vol" + assert env["DATABRICKS_HOST"] == "https://test.cloud.databricks.com" + assert env["DATABRICKS_TOKEN"] == "user-pat" diff --git a/tests/test_memory_mcp.py b/tests/test_memory_mcp.py new file mode 100644 index 0000000..a520a8e --- /dev/null +++ b/tests/test_memory_mcp.py @@ -0,0 +1,111 @@ +"""Tests for claw.core.memory_mcp — stdio MCP server tools.""" + +from __future__ import annotations + +import io +from unittest.mock import MagicMock, patch + +import pytest +from databricks.sdk.errors import NotFound + + +class TestReadMemoryTool: + """Unit tests for the read_memory_tool function.""" + + @patch.dict("os.environ", { + "CLAW_MEMORY_VOLUME_PATH": "/Volumes/catalog/schema/vol", + "DATABRICKS_HOST": "https://test.cloud.databricks.com", + "DATABRICKS_TOKEN": "dapi-test-token", + }) + @patch("claw.core.memory_mcp.WorkspaceClient") + def test_returns_file_content(self, mock_ws_cls: MagicMock) -> None: + from claw.core.memory_mcp import read_memory_tool + + mock_ws = mock_ws_cls.return_value + mock_response = MagicMock() + mock_response.contents = io.BytesIO(b"# My Memory\n- fact 1") + mock_ws.files.download.return_value = mock_response + + result = read_memory_tool() + + assert result == "# My Memory\n- fact 1" + mock_ws.files.download.assert_called_once_with( + "/Volumes/catalog/schema/vol/MEMORY.md" + ) + + @patch.dict("os.environ", { + "CLAW_MEMORY_VOLUME_PATH": "/Volumes/catalog/schema/vol", + "DATABRICKS_HOST": "https://test.cloud.databricks.com", + "DATABRICKS_TOKEN": "dapi-test-token", + }) + @patch("claw.core.memory_mcp.WorkspaceClient") + def test_returns_empty_on_not_found(self, mock_ws_cls: MagicMock) -> None: + from claw.core.memory_mcp import read_memory_tool + + mock_ws = mock_ws_cls.return_value + mock_ws.files.download.side_effect = NotFound("not found") + + result = read_memory_tool() + + assert result == "" + + @patch.dict("os.environ", { + "CLAW_MEMORY_VOLUME_PATH": "/Volumes/catalog/schema/vol/", + "DATABRICKS_HOST": "https://test.cloud.databricks.com", + "DATABRICKS_TOKEN": "dapi-test-token", + }) + @patch("claw.core.memory_mcp.WorkspaceClient") + def test_strips_trailing_slash_from_path(self, mock_ws_cls: MagicMock) -> None: + from claw.core.memory_mcp import read_memory_tool + + mock_ws = mock_ws_cls.return_value + mock_response = MagicMock() + mock_response.contents = io.BytesIO(b"content") + mock_ws.files.download.return_value = mock_response + + read_memory_tool() + + mock_ws.files.download.assert_called_once_with( + "/Volumes/catalog/schema/vol/MEMORY.md" + ) + + +class TestWriteMemoryTool: + """Unit tests for the write_memory_tool function.""" + + @patch.dict("os.environ", { + "CLAW_MEMORY_VOLUME_PATH": "/Volumes/catalog/schema/vol", + "DATABRICKS_HOST": "https://test.cloud.databricks.com", + "DATABRICKS_TOKEN": "dapi-test-token", + }) + @patch("claw.core.memory_mcp.WorkspaceClient") + def test_uploads_content(self, mock_ws_cls: MagicMock) -> None: + from claw.core.memory_mcp import write_memory_tool + + mock_ws = mock_ws_cls.return_value + + result = write_memory_tool("# Updated Memory") + + assert "ok" in result.lower() or "success" in result.lower() or "written" in result.lower() + mock_ws.files.upload.assert_called_once() + call_args = mock_ws.files.upload.call_args + assert call_args[0][0] == "/Volumes/catalog/schema/vol/MEMORY.md" + # Verify the content bytes + buf = call_args[1].get("contents") or call_args[0][1] + assert buf.read() == b"# Updated Memory" + + @patch.dict("os.environ", { + "CLAW_MEMORY_VOLUME_PATH": "/Volumes/catalog/schema/vol", + "DATABRICKS_HOST": "https://test.cloud.databricks.com", + "DATABRICKS_TOKEN": "dapi-test-token", + }) + @patch("claw.core.memory_mcp.WorkspaceClient") + def test_upload_called_with_overwrite(self, mock_ws_cls: MagicMock) -> None: + from claw.core.memory_mcp import write_memory_tool + + mock_ws = mock_ws_cls.return_value + + write_memory_tool("content") + + call_kwargs = mock_ws.files.upload.call_args[1] + assert call_kwargs.get("overwrite") is True diff --git a/tests/test_slack_poller.py b/tests/test_slack_poller.py index dd6d907..f397ac8 100644 --- a/tests/test_slack_poller.py +++ b/tests/test_slack_poller.py @@ -310,8 +310,6 @@ class TestClientRegistry: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -326,8 +324,6 @@ async def test_client_created_on_first_message( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -373,8 +369,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -389,8 +383,6 @@ async def test_client_reused_on_second_message( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -505,8 +497,6 @@ class TestHandleMessage: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="memory content") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -521,8 +511,6 @@ async def test_handle_message_runs_agent_and_replies( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -577,8 +565,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session") @@ -593,8 +579,6 @@ async def test_handle_message_resumes_existing_session( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -641,8 +625,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -657,8 +639,6 @@ async def test_handle_message_tracks_active_thread( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -847,8 +827,6 @@ class TestGracefulReconnect: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session") @@ -863,8 +841,6 @@ async def test_reconnect_uses_existing_sdk_session_id( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -921,8 +897,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -937,8 +911,6 @@ async def test_fresh_session_when_no_sdk_session_id( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -990,8 +962,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1006,8 +976,6 @@ async def test_client_reused_on_second_message( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1072,8 +1040,6 @@ class TestBackgroundResponseLoop: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1088,8 +1054,6 @@ async def test_response_loop_runs_as_background_task( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1152,8 +1116,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1168,8 +1130,6 @@ async def test_background_task_sends_reply_when_done( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1281,8 +1241,6 @@ class TestMessageHistorySave: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1297,8 +1255,6 @@ async def test_response_loop_saves_user_and_assistant_messages( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1364,8 +1320,6 @@ class TestMessageHistoryReconnect: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session") @@ -1380,8 +1334,6 @@ async def test_reconnect_injects_history_into_prompt( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1447,8 +1399,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1463,8 +1413,6 @@ async def test_no_history_injection_when_no_prior_messages( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1513,8 +1461,6 @@ async def _aiter(): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1529,8 +1475,6 @@ async def test_no_history_injection_when_client_already_cached( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1590,8 +1534,6 @@ class TestCountBasedEviction: @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1608,8 +1550,6 @@ async def test_evicts_lra_when_at_capacity( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1684,8 +1624,6 @@ def _mock_exec(stmt): @patch("claw.core.slack_poller.iter_response") @patch("claw.core.slack_poller.send_message", new_callable=AsyncMock) @patch("claw.core.slack_poller.create_client", new_callable=AsyncMock) - @patch("claw.core.slack_poller.write_memory") - @patch("claw.core.slack_poller.read_memory", return_value="") @patch("claw.core.slack_poller.build_mcp_servers", return_value={}) @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.get_session", return_value=None) @@ -1700,8 +1638,6 @@ async def test_no_eviction_below_capacity( mock_get_session, mock_upsert_session, mock_build_mcp, - mock_read_mem, - mock_write_mem, mock_create_client, mock_send_message, mock_iter_response, @@ -1852,7 +1788,6 @@ class TestResultMetadataLogging: @pytest.mark.asyncio @patch("claw.core.slack_poller.reply_to_thread", new_callable=AsyncMock) @patch("claw.core.slack_poller.iter_response") - @patch("claw.core.slack_poller.write_memory") @patch("claw.core.slack_poller.save_message") @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.DbSession") @@ -1861,7 +1796,6 @@ async def test_response_loop_logs_result_metadata( mock_db_session_cls, mock_upsert_session, mock_save_msg, - mock_write_mem, mock_iter_response, mock_reply, ): @@ -1895,7 +1829,7 @@ async def _aiter(): poller._clients["300.001"] = mock_client with patch("claw.core.slack_poller.logger") as mock_logger: - await poller._run_response_loop("300.001", "C123", "", "hello") + await poller._run_response_loop("300.001", "C123", "hello") # Find the info call that contains "Agent turn complete" info_calls = [c for c in mock_logger.info.call_args_list if "Agent turn complete" in str(c)] @@ -1909,7 +1843,6 @@ async def _aiter(): @pytest.mark.asyncio @patch("claw.core.slack_poller.reply_to_thread", new_callable=AsyncMock) @patch("claw.core.slack_poller.iter_response") - @patch("claw.core.slack_poller.write_memory") @patch("claw.core.slack_poller.save_message") @patch("claw.core.slack_poller.upsert_session") @patch("claw.core.slack_poller.DbSession") @@ -1918,7 +1851,6 @@ async def test_response_loop_logs_cost_none_as_zero( mock_db_session_cls, mock_upsert_session, mock_save_msg, - mock_write_mem, mock_iter_response, mock_reply, ): @@ -1952,7 +1884,7 @@ async def _aiter(): poller._clients["400.001"] = mock_client with patch("claw.core.slack_poller.logger") as mock_logger: - await poller._run_response_loop("400.001", "C123", "", "test") + await poller._run_response_loop("400.001", "C123", "test") info_calls = [c for c in mock_logger.info.call_args_list if "Agent turn complete" in str(c)] assert len(info_calls) == 1