docs: add matrix per-chat context design

This commit is contained in:
Mikhail Putilovskij 2026-04-19 16:37:41 +03:00
parent 430c82dba1
commit 9bb93fbbda

View file

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