surfaces/docs/superpowers/plans/2026-04-19-matrix-per-chat-context.md

18 KiB

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

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
# 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
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

@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
# 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
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

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
# 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
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

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
# 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
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

@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
# 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
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

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
# 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
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

@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
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.