# Matrix Per-Chat Context Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Move the Matrix surface from a shared agent context to true per-room platform contexts mapped through `platform_chat_id`, including `!new`, `!branch`, lazy migration, and per-room context commands. **Architecture:** Matrix keeps owning UX chats (`C1`, `C2`, rooms, spaces). Each working room stores a `platform_chat_id` in `room_meta`, and platform-facing operations use that mapping. Legacy rooms without a mapping lazily create one on first context-aware use. **Tech Stack:** Python 3.11, Matrix nio adapter, local state store, `lambda_agent_api`, pytest --- ### Task 1: Add `platform_chat_id` to Matrix metadata and tests **Files:** - Modify: `adapter/matrix/store.py` - Test: `tests/adapter/matrix/test_store.py` - [ ] **Step 1: Write the failing test** ```python async def test_room_meta_roundtrip_with_platform_chat_id(store: InMemoryStore): meta = { "chat_id": "C1", "matrix_user_id": "@alice:example.org", "platform_chat_id": "chat-platform-1", } await set_room_meta(store, "!r:m.org", meta) saved = await get_room_meta(store, "!r:m.org") assert saved is not None assert saved["platform_chat_id"] == "chat-platform-1" ``` - [ ] **Step 2: Run test to verify it fails or proves missing coverage** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_store.py -q` Expected: either FAIL on missing assertion coverage or PASS after adding the new test with current generic storage behavior - [ ] **Step 3: Write minimal implementation** ```python # adapter/matrix/store.py # No schema gate is required because room metadata is already stored as a dict. # Keep helpers unchanged, but add focused helper functions if they reduce repeated logic: async def get_platform_chat_id(store: StateStore, room_id: str) -> str | None: meta = await get_room_meta(store, room_id) return meta.get("platform_chat_id") if meta else None async def set_platform_chat_id(store: StateStore, room_id: str, platform_chat_id: str) -> None: meta = await get_room_meta(store, room_id) or {} meta["platform_chat_id"] = platform_chat_id await set_room_meta(store, room_id, meta) ``` - [ ] **Step 4: Run tests to verify they pass** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_store.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add adapter/matrix/store.py tests/adapter/matrix/test_store.py git commit -m "feat: add platform chat id room metadata helpers" ``` ### Task 2: Extend the platform wrapper to support context-aware API calls **Files:** - Modify: `sdk/agent_api_wrapper.py` - Modify: `sdk/real.py` - Test: `tests/platform/test_real.py` - [ ] **Step 1: Write the failing tests** ```python @pytest.mark.asyncio async def test_real_client_send_message_uses_platform_chat_id(): api = FakeAgentApi() client = RealPlatformClient(agent_api=api, prototype_state=PrototypeStateStore()) await client.send_message("@alice:example.org", "chat-platform-1", "hello") assert api.sent == [("chat-platform-1", "hello")] @pytest.mark.asyncio async def test_real_client_create_and_branch_context_delegate_to_agent_api(): api = FakeAgentApi(create_ids=["chat-new", "chat-branch"]) client = RealPlatformClient(agent_api=api, prototype_state=PrototypeStateStore()) created = await client.create_chat_context("@alice:example.org") branched = await client.branch_chat_context("@alice:example.org", "chat-source") assert created == "chat-new" assert branched == "chat-branch" assert api.branch_calls == ["chat-source"] ``` - [ ] **Step 2: Run tests to verify they fail** Run: `PYTHONPATH=. uv run pytest tests/platform/test_real.py -q` Expected: FAIL because `RealPlatformClient` does not yet expose context-aware methods or pass a platform context id through - [ ] **Step 3: Write minimal implementation** ```python # sdk/agent_api_wrapper.py class AgentApiWrapper(AgentApi): async def create_chat(self) -> str: ... async def branch_chat(self, chat_id: str) -> str: ... async def send_message(self, chat_id: str, text: str): ... async def save_context(self, chat_id: str, name: str) -> None: ... async def load_context(self, chat_id: str, name: str) -> None: ... # sdk/real.py class RealPlatformClient(PlatformClient): async def create_chat_context(self, user_id: str) -> str: return await self._agent_api.create_chat() async def branch_chat_context(self, user_id: str, from_chat_id: str) -> str: return await self._agent_api.branch_chat(from_chat_id) async def save_chat_context(self, user_id: str, chat_id: str, name: str) -> None: await self._agent_api.save_context(chat_id, name) async def load_chat_context(self, user_id: str, chat_id: str, name: str) -> None: await self._agent_api.load_context(chat_id, name) async def stream_message(...): async for event in self._agent_api.send_message(chat_id, text): ... ``` - [ ] **Step 4: Run tests to verify they pass** Run: `PYTHONPATH=. uv run pytest tests/platform/test_real.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add sdk/agent_api_wrapper.py sdk/real.py tests/platform/test_real.py git commit -m "feat: add context-aware real platform client methods" ``` ### Task 3: Create Matrix-side resolver for mapped or lazy-created platform contexts **Files:** - Modify: `adapter/matrix/bot.py` - Modify: `adapter/matrix/store.py` - Test: `tests/adapter/matrix/test_dispatcher.py` - [ ] **Step 1: Write the failing tests** ```python async def test_existing_room_without_platform_chat_id_gets_lazy_mapping_on_message(): runtime = build_runtime(platform=FakeRealPlatformClient(create_ids=["chat-platform-1"])) await set_room_meta(runtime.store, "!room:example.org", { "chat_id": "C1", "matrix_user_id": "@alice:example.org", }) client = SimpleNamespace(user_id="@bot:example.org", room_send=AsyncMock()) bot = MatrixBot(client, runtime) room = SimpleNamespace(room_id="!room:example.org") event = SimpleNamespace(sender="@alice:example.org", body="hello") await bot.on_room_message(room, event) meta = await get_room_meta(runtime.store, "!room:example.org") assert meta["platform_chat_id"] == "chat-platform-1" ``` - [ ] **Step 2: Run tests to verify they fail** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_dispatcher.py -q` Expected: FAIL because no lazy mapping exists - [ ] **Step 3: Write minimal implementation** ```python # adapter/matrix/bot.py async def _ensure_platform_chat_id(self, room_id: str, user_id: str) -> str: meta = await get_room_meta(self.runtime.store, room_id) if meta is None: raise ValueError("room metadata is required") platform_chat_id = meta.get("platform_chat_id") if platform_chat_id: return platform_chat_id if not hasattr(self.runtime.platform, "create_chat_context"): raise ValueError("real platform backend required") platform_chat_id = await self.runtime.platform.create_chat_context(user_id) meta["platform_chat_id"] = platform_chat_id await set_room_meta(self.runtime.store, room_id, meta) return platform_chat_id ``` - [ ] **Step 4: Run tests to verify they pass** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_dispatcher.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add adapter/matrix/bot.py adapter/matrix/store.py tests/adapter/matrix/test_dispatcher.py git commit -m "feat: lazily assign platform chat ids to matrix rooms" ``` ### Task 4: Make `!new` and workspace bootstrap create independent platform contexts **Files:** - Modify: `adapter/matrix/handlers/chat.py` - Modify: `adapter/matrix/handlers/auth.py` - Modify: `adapter/matrix/bot.py` - Test: `tests/adapter/matrix/test_chat_space.py` - Test: `tests/adapter/matrix/test_invite_space.py` - Test: `tests/adapter/matrix/test_dispatcher.py` - [ ] **Step 1: Write the failing tests** ```python async def test_new_chat_assigns_new_platform_chat_id(): client = SimpleNamespace( room_create=AsyncMock(return_value=SimpleNamespace(room_id="!r2:example")), room_put_state=AsyncMock(), room_invite=AsyncMock(), ) platform = FakeRealPlatformClient(create_ids=["chat-platform-7"]) runtime = build_runtime(platform=platform, client=client) await set_user_meta(runtime.store, "u1", {"space_id": "!space:example", "next_chat_index": 7}) result = await runtime.dispatcher.dispatch( IncomingCommand(user_id="u1", platform="matrix", chat_id="C3", command="new", args=["Research"]) ) meta = await get_room_meta(runtime.store, "!r2:example") assert meta["platform_chat_id"] == "chat-platform-7" ``` - [ ] **Step 2: Run tests to verify they fail** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_invite_space.py tests/adapter/matrix/test_dispatcher.py -q` Expected: FAIL because new chats do not yet store a platform context id - [ ] **Step 3: Write minimal implementation** ```python # adapter/matrix/handlers/chat.py # adapter/matrix/handlers/auth.py platform_chat_id = None if hasattr(platform, "create_chat_context"): platform_chat_id = await platform.create_chat_context(event.user_id) await set_room_meta(store, room_id, { "chat_id": chat_id, "matrix_user_id": event.user_id, "space_id": space_id, "platform_chat_id": platform_chat_id, }) ``` - [ ] **Step 4: Run tests to verify they pass** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_invite_space.py tests/adapter/matrix/test_dispatcher.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add adapter/matrix/handlers/chat.py adapter/matrix/handlers/auth.py adapter/matrix/bot.py tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_invite_space.py tests/adapter/matrix/test_dispatcher.py git commit -m "feat: assign platform contexts when creating matrix chats" ``` ### Task 5: Make per-room save, load, and context use the mapped platform context **Files:** - Modify: `adapter/matrix/handlers/context_commands.py` - Modify: `adapter/matrix/bot.py` - Modify: `sdk/prototype_state.py` - Test: `tests/adapter/matrix/test_context_commands.py` - [ ] **Step 1: Write the failing tests** ```python @pytest.mark.asyncio async def test_save_command_uses_room_platform_chat_id(): platform = MatrixCommandPlatform() runtime = build_runtime(platform=platform) await set_room_meta(runtime.store, "!room:example.org", { "chat_id": "C1", "matrix_user_id": "u1", "platform_chat_id": "chat-platform-1", }) event = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="save", args=["session-a"]) result = await make_handle_save(...)(event, runtime.auth_mgr, platform, runtime.chat_mgr, runtime.settings_mgr) assert platform.saved_calls == [("chat-platform-1", "session-a")] @pytest.mark.asyncio async def test_context_command_reports_current_room_platform_chat_id(): ... assert "chat-platform-1" in result[0].text ``` - [ ] **Step 2: Run tests to verify they fail** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_context_commands.py -q` Expected: FAIL because save/load/context do not currently use room-level platform mappings - [ ] **Step 3: Write minimal implementation** ```python # adapter/matrix/handlers/context_commands.py room_id = await _resolve_room_id(event, chat_mgr) meta = await get_room_meta(store, room_id) platform_chat_id = meta.get("platform_chat_id") await platform.save_chat_context(event.user_id, platform_chat_id, name) await platform.load_chat_context(event.user_id, platform_chat_id, name) # sdk/prototype_state.py # store current loaded session per user+platform_chat_id instead of only per user when needed for Matrix `!context` ``` - [ ] **Step 4: Run tests to verify they pass** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_context_commands.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add adapter/matrix/handlers/context_commands.py adapter/matrix/bot.py sdk/prototype_state.py tests/adapter/matrix/test_context_commands.py git commit -m "feat: bind matrix context commands to platform chat ids" ``` ### Task 6: Add `!branch` and help-text updates **Files:** - Modify: `adapter/matrix/handlers/chat.py` - Modify: `adapter/matrix/handlers/__init__.py` - Modify: `adapter/matrix/handlers/settings.py` - Modify: `adapter/matrix/handlers/auth.py` - Modify: `adapter/matrix/bot.py` - Test: `tests/adapter/matrix/test_chat_space.py` - Test: `tests/adapter/matrix/test_dispatcher.py` - [ ] **Step 1: Write the failing tests** ```python async def test_branch_creates_new_room_with_branched_platform_chat_id(): client = SimpleNamespace( room_create=AsyncMock(return_value=SimpleNamespace(room_id="!r3:example")), room_put_state=AsyncMock(), room_invite=AsyncMock(), ) platform = FakeRealPlatformClient(branch_ids=["chat-platform-branch"]) runtime = build_runtime(platform=platform, client=client) await set_room_meta(runtime.store, "!current:example", { "chat_id": "C2", "matrix_user_id": "u1", "space_id": "!space:example", "platform_chat_id": "chat-platform-source", }) result = await runtime.dispatcher.dispatch( IncomingCommand(user_id="u1", platform="matrix", chat_id="C2", command="branch", args=["Fork"]) ) meta = await get_room_meta(runtime.store, "!r3:example") assert meta["platform_chat_id"] == "chat-platform-branch" ``` - [ ] **Step 2: Run tests to verify they fail** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_dispatcher.py -q` Expected: FAIL because `branch` is not implemented - [ ] **Step 3: Write minimal implementation** ```python # adapter/matrix/handlers/chat.py def make_handle_branch(client, store): async def handle_branch(event, auth_mgr, platform, chat_mgr, settings_mgr): source_room_id = ... source_meta = await get_room_meta(store, source_room_id) platform_chat_id = await platform.branch_chat_context(event.user_id, source_meta["platform_chat_id"]) ... await set_room_meta(store, new_room_id, { "chat_id": new_chat_id, "matrix_user_id": event.user_id, "space_id": space_id, "platform_chat_id": platform_chat_id, }) ``` - [ ] **Step 4: Run tests to verify they pass** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_dispatcher.py -q` Expected: PASS - [ ] **Step 5: Commit** ```bash git add adapter/matrix/handlers/chat.py adapter/matrix/handlers/__init__.py adapter/matrix/handlers/settings.py adapter/matrix/handlers/auth.py adapter/matrix/bot.py tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_dispatcher.py git commit -m "feat: add matrix branch command for platform contexts" ``` ### Task 7: Verify the full Matrix flow and clean up legacy assumptions **Files:** - Modify: `tests/platform/test_real.py` - Modify: `tests/adapter/matrix/test_dispatcher.py` - Modify: `tests/adapter/matrix/test_context_commands.py` - Modify: `tests/core/test_integration.py` - [ ] **Step 1: Add integration coverage for independent room contexts** ```python @pytest.mark.asyncio async def test_two_rooms_send_messages_into_different_platform_contexts(): platform = FakeRealPlatformClient() runtime = build_runtime(platform=platform) await set_room_meta(runtime.store, "!r1:example", {"chat_id": "C1", "matrix_user_id": "u1", "platform_chat_id": "chat-1"}) await set_room_meta(runtime.store, "!r2:example", {"chat_id": "C2", "matrix_user_id": "u1", "platform_chat_id": "chat-2"}) ... assert platform.sent == [("chat-1", "hello"), ("chat-2", "world")] ``` - [ ] **Step 2: Run the focused verification suite** Run: `PYTHONPATH=. uv run pytest tests/platform/test_real.py tests/adapter/matrix/test_store.py tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_invite_space.py tests/adapter/matrix/test_dispatcher.py tests/adapter/matrix/test_context_commands.py tests/core/test_integration.py -q` Expected: PASS - [ ] **Step 3: Run the full Matrix suite** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix -q` Expected: PASS - [ ] **Step 4: Inspect help text and command visibility** Run: `PYTHONPATH=. uv run pytest tests/adapter/matrix/test_dispatcher.py -q` Expected: PASS with `!branch` present in help and hidden commands still absent - [ ] **Step 5: Commit** ```bash git add tests/platform/test_real.py tests/adapter/matrix/test_store.py tests/adapter/matrix/test_chat_space.py tests/adapter/matrix/test_invite_space.py tests/adapter/matrix/test_dispatcher.py tests/adapter/matrix/test_context_commands.py tests/core/test_integration.py git commit -m "test: verify matrix per-chat platform context flow" ``` ## Self-Review - Spec coverage: - `surface_chat -> platform_chat_id` mapping is covered by Tasks 1, 3, and 4. - `!new` independent contexts are covered by Task 4. - `!branch` snapshot flow is covered by Task 6. - per-room `!save`, `!load`, and `!context` are covered by Task 5. - lazy migration for legacy rooms is covered by Task 3. - verification across rooms is covered by Task 7. - Placeholder scan: - No `TODO` or `TBD` placeholders remain. - Commands and file paths are concrete. - Type consistency: - The plan consistently uses `platform_chat_id` for stored mapping and `create_chat_context` / `branch_chat_context` / `save_chat_context` / `load_chat_context` for platform-facing methods.