Story-2839 (RFD-AGT-013 T1d-a) closes the remaining broker gap between
agents.chat_session routing and the already-shipped shared memory
scope. The memory pack assembler in src/shared/memory/pack.py already
keys the fold by (agent_key, matter_id, user_id), but the WebSocket
edge still had a Mira-only default-session helper and no shared
validation path for explicit chat_session_id values. This story
extracts that rule into one importable resolver and makes every matter-chat
launch use it.
| Surface | Change |
|---|---|
src/shared/chat_session_resolver.py (new) |
New shared resolver resolve_matter_turn_session(). If the turn
carries an explicit chat_session_id, it verifies the row is owned by
the same (firm, matter, user, agent) identity and is not deleted.
If the turn carries no session id, it resolves or creates the single
default_agent_chat row for that same identity.
|
src/app_api/router_ws.py |
Removes the old _resolve_default_mira_session() helper and routes
both Mira and Atlas matter-chat launches through the shared resolver before the
command envelope is built. The nested app Mira envelope still translates into
mira_matter_chat, but now through the common path instead of a
one-off helper.
|
src/clerk_worker/runner.py |
Updates the persistence error text to name the shared resolver, so the operator breadcrumb matches the live routing seam. |
tests/unit/test_router_ws.py |
WS unit coverage now pins the shared resolver path: Mira envelope translation
still resolves a default session, explicit matter-chat launches still route to
the clerk rail, and a launch without chatSessionId resolves the
default thread for atlas_matter_chat.
|
tests/integration/test_clerk_converse_turn_persistence.py |
The integration proof now imports the shared resolver directly for the real matter-session keying step, so the system-boundary coverage points at the new canonical implementation. |
The product rule is "one shared memory per (agent, matter, counsel),
plus transcript turns routed to the selected thread." Named sessions are allowed,
but they do not mint separate memory scopes. Before this story,
that rule was split:
agent_key + matter_id + user_id.
Story-2839 makes the routing rule explicit and singular: a matter-chat turn either
lands on the chosen owned thread, or it lands on the one default thread for that
(agent, matter, counsel) identity.
| Path | Before | After |
|---|---|---|
| Mira matter-chat launch with no session id | Worked through a dedicated _resolve_default_mira_session() helper in router_ws.py. |
Works through the shared resolver in src/shared/chat_session_resolver.py. |
| Atlas matter-chat launch with no session id | No shared resolver path existed at the WS edge. | Resolves or creates the default thread via the same shared helper as Mira. |
Explicit chat_session_id on a matter-chat launch |
No single shared ownership/scope resolver enforced it at the edge. | One fail-closed helper validates the session belongs to the same (firm, matter, user, agent) identity and is not deleted. |
| Memory scope | Already shared per (agent, matter, counsel). |
Unchanged, but now aligned with the routing seam that selects the turn's thread. |
pramaan.chat_session.not_found.uv run pytest tests/unit/test_router_ws.py -q
# 44 passed
uv run ruff check src/shared/chat_session_resolver.py \
src/app_api/router_ws.py \
src/clerk_worker/runner.py \
tests/unit/test_router_ws.py \
tests/integration/test_clerk_converse_turn_persistence.py
# all checks passed
uv run pytest tests/integration/test_clerk_converse_turn_persistence.py -q
# could not run locally: DATABASE_URL / PRAMAAN_ADMIN_DATABASE_URL not set
# CI must verify this integration path
The local proof is therefore the unit boundary plus static lint on the touched files. The system-boundary integration test remains the authoritative guard for the real DB-backed thread persistence path, but in this environment it is CI-only because the required database URLs were absent.