diff --git a/docs/superpowers/specs/2026-04-19-matrix-per-chat-context-design.md b/docs/superpowers/specs/2026-04-19-matrix-per-chat-context-design.md new file mode 100644 index 0000000..9807bd6 --- /dev/null +++ b/docs/superpowers/specs/2026-04-19-matrix-per-chat-context-design.md @@ -0,0 +1,278 @@ +# Matrix Per-Chat Context Design + +## Goal + +Move the Matrix surface from the current shared-agent-context MVP to true per-chat agent contexts using the new platform `chat_id`, while preserving the existing Matrix UX model built around rooms, spaces, and local chat labels such as `C1`, `C2`, and `C3`. + +## Core Decision + +The Matrix surface remains the owner of user-facing chat organization. + +- Matrix rooms, spaces, chat names, and archive state remain surface concerns. +- The platform agent becomes the owner of actual conversation context. +- The integration layer stores an explicit mapping from each surface chat to one platform context. + +This is the selected "Variant A" architecture: + +`surface_chat -> platform_chat_id` + +## Why This Decision + +The current Matrix adapter already has a stable UX model: + +- a user has a space +- each working room has a local chat id like `C1` +- commands such as `!new`, `!chats`, `!rename`, and `!archive` operate on that model + +Replacing that entire model with platform-native chat ids would be a broader refactor than needed. The surface and the platform solve different problems: + +- the surface organizes rooms and commands for users +- the platform persists and branches real conversation context + +Keeping those responsibilities separate lets us add true per-chat context now without rebuilding the Matrix adapter around a new identity model. + +## Scope + +This design covers: + +- true per-chat context for Matrix rooms +- a new `!branch` command +- real context-aware semantics for `!new`, `!context`, `!save`, and `!load` +- lazy migration of legacy Matrix rooms created before platform `chat_id` support + +This design does not cover: + +- end-to-end Matrix encryption support +- Telegram changes +- platform UI for browsing contexts +- a future unified cross-surface chat browser + +## Data Model + +### Surface chat identity + +The Matrix surface keeps its existing identifiers: + +- Matrix room id, for example `!room:example.org` +- local chat id, for example `C2` +- room name +- archive status +- owning space id + +These remain the source of truth for Matrix UX. + +### Platform context identity + +Each working Matrix room gets a `platform_chat_id` stored in its room metadata. + +Example `room_meta` shape: + +```json +{ + "chat_id": "C2", + "space_id": "!space:example.org", + "name": "Research", + "platform_chat_id": "chat_8f2c..." +} +``` + +Rules: + +- one working Matrix room maps to exactly one current platform context +- two Matrix rooms must not share the same `platform_chat_id` unless we intentionally implement that later +- branching creates a new `platform_chat_id`, never reuses the old one + +## Runtime Semantics + +### Normal message flow + +1. A Matrix message arrives in a working room. +2. The Matrix adapter resolves the room to local `room_meta`. +3. The integration layer reads `platform_chat_id` from that metadata. +4. `RealPlatformClient.send_message(...)` sends the message to the platform using that `platform_chat_id`. +5. The platform appends the exchange to that specific context and returns the reply. +6. The Matrix adapter sends the reply back to the room. + +The key change is that the agent no longer treats all Matrix rooms as one shared context. + +### `!new` + +`!new` creates a new user-facing chat and a new empty platform context at the same time. + +Flow: + +1. Create a new Matrix room in the user space. +2. Ask the platform to create a new blank context and return its `platform_chat_id`. +3. Store that `platform_chat_id` in the new room metadata. +4. Invite the user into the room. + +Result: + +- the new room is immediately independent +- sending the first message does not share memory with the previous room + +### `!branch` + +`!branch` creates a new room whose starting point is a snapshot of the current room context. + +Flow: + +1. Resolve the current room's `platform_chat_id`. +2. Ask the platform to create a new context branched from that source. +3. Create a new Matrix room. +4. Store the new `platform_chat_id` in the new room metadata. +5. Invite the user into the new room. + +Result: + +- the new room starts with the current history and state +- later messages diverge independently + +### `!save` + +`!save [name]` saves a snapshot of the current room's platform context under the current user. + +Semantics: + +- saves are owned by the user, not by the room +- the saved snapshot originates from the current `platform_chat_id` + +### `!load` + +`!load` shows the user's saved contexts and loads the chosen snapshot into the current room's platform context. + +Semantics: + +- a saved context created in one room can be loaded into any other room owned by the same user +- loading does not replace the Matrix room identity +- loading affects only the current room's mapped `platform_chat_id` + +### `!context` + +`!context` reports the state of the current room context, not a global user session. + +Minimum expected output: + +- current room name or local chat id +- current `platform_chat_id` presence or status +- what saved context, if any, was last loaded here +- last token usage if the platform still returns it + +## Legacy Room Migration + +Existing Matrix rooms were created before real platform `chat_id` support and therefore may not have `platform_chat_id` in their metadata. + +We need a non-destructive migration. + +### Lazy migration strategy + +For a room without `platform_chat_id`: + +1. On the first operation that requires platform context, detect the missing mapping. +2. Create a new blank platform context for that room. +3. Persist the new `platform_chat_id` into room metadata. +4. Continue the requested operation normally. + +This applies to: + +- first normal message +- `!context` +- `!save` +- `!load` +- `!branch` + +This avoids forcing users to recreate their rooms manually. + +## Interface Changes + +### Matrix metadata + +Extend Matrix `room_meta` helpers to read and write `platform_chat_id`. + +### Real platform client + +`RealPlatformClient` must stop treating the current `chat_id` parameter as a purely local label when talking to the platform. The surface-facing call can still receive the local `chat_id`, but platform calls must use the resolved `platform_chat_id`. + +Recommended integration direction: + +- Matrix resolves the room mapping before calling the platform +- `RealPlatformClient` receives the platform context id it should use + +This keeps the platform client simple and avoids giving it Matrix-specific storage responsibilities. + +### Agent API wrapper + +The wrapper must support platform calls that are explicitly context-aware: + +- create new context +- branch context +- send message into a specific context +- save current context +- load saved context into a specific context + +If upstream naming differs, the adapter layer should normalize those operations into stable local methods. + +## Command Semantics in MVP + +The MVP command set should evolve to this: + +- `!new` creates a new room with a new empty platform context +- `!branch` creates a new room with a branched platform context +- `!context` reports the current room context +- `!save` saves the current room context for the user +- `!load` loads one of the user's saved contexts into the current room + +Commands that do not have reliable backend support should remain hidden or explicitly marked unavailable. + +## Error Handling + +### Missing mapping + +If `platform_chat_id` is missing: + +- try lazy migration first +- only return an error if migration fails + +### Platform create or branch failure + +If the platform cannot create or branch a context: + +- do not create partially-initialized room metadata +- return a user-facing error in the source room +- log enough detail to diagnose the backend failure + +### Save and load failure + +The surface must not claim success before the platform confirms success. + +For MVP quality: + +- user-facing text should say "request sent" only when confirmation is not available +- once platform confirmation exists, switch to real success or failure messages + +## Testing + +Add or update tests for: + +- a new room gets a new `platform_chat_id` +- two rooms created with `!new` do not share context ids +- `!branch` creates a new room with a different `platform_chat_id` derived from the current one +- sending messages from two rooms uses different platform context ids +- saved contexts remain user-visible across rooms +- loading the same saved context into two different rooms affects those rooms independently afterward +- a legacy room without `platform_chat_id` lazily receives one on first use +- failures during create, branch, save, and load do not leave broken metadata behind + +## Migration Path + +This design preserves a clean future direction: + +- Matrix continues to own its UX model +- Telegram can adopt the same `surface_chat -> platform_chat_id` mapping later +- when the platform matures further, more of the save/load/branch logic can move from prompts or local workarounds into real platform APIs + +The key long-term boundary stays stable: + +- surfaces own presentation and routing +- the platform owns context +- the integration layer owns the mapping