Story-1720 Sessions (a/2) — container: per-user+agent, resume-revalidate, scope-drop
RFD-AGT-002 D-12. A session is the container that holds one counsel's running conversation with one agent. This Story builds the brain-side rules of that container — the typed value plus the three guards that keep it honest and confidential. It is the agents (a/2) half of D-12; the app (b/2) sibling owns the new/resume/switch dropdown UI. The durable store (AgentCore Memory, the 4 sealed ledgers — D-13) is a separate Story blocked on the seal-proof Spike and is deliberately not wired here: a session is a stateless-brain value the runtime hands in, the guards accept or reject it, nothing is persisted from this module.
The container
SessionContainer session_id derived from + bound to (user_id, agent_key) user_id the single owning user agent_key the single owning agent title / source scope-safe: generic | user (auto is forbidden) saved the minimal-save resume requires sealed set the moment matter context is touched page_card PageCardStamp re-checked on resume context_slices each tagged firm | matter (matter slice carries matter_id)
AC1 — session_id is per-user + per-agent
derive_session_id(user_id, agent_key, nonce=…)folds the owning user AND agent into a deterministic SHA digest, so an id minted for one pair can never be reproduced for another. Thenoncelets one user hold many sessions per agent (new/resume/switch) while each id still binds its single owning pair. Deterministic — no randomness, no timestamp — so a resumed turn recomputes an identical handle.assert_session_owner(session, user_id=…, agent_key=…)re-asserts the bound pair on every use (not merely trusted from the id). A wrong user OR a wrong agent raisesAtlasSessionOwnerError(codeatlas_session_owner) — the brain never serves one user's conversation to another, nor one agent's session to a different agent (the per-USER seal of RFD-AGT-001 + the per-AGENT bound D-4 adds).
AC2 — resume re-validates the Page Card (no blind replay)
- A resumed session carries the
PageCardStampit last ran under: the surface plus all three version axes (contract_version·catalog_version·universe_version, §7.4). revalidate_page_card_on_resume(session, expected_…=…)re-checks that stamp against the LIVE expected versions before Atlas acts. Any drifted axis — a capability renamed or removed since the session was saved — raisesAtlasStaleSessionError(codeatlas_stale_session) so the turn re-warms instead of replaying a capability that no longer exists. A missing stamp is treated as stale, never silent.- Resume requires a minimal save (D-12):
assert_resumable()rejects an unsaved session withAtlasSessionNotSavedError(codeatlas_session_not_saved) — you cannot resume what was never saved. The revalidate gate runs the save check first.
AC3 — switching INTO a session drops the prior matter-scoped context
switch_into(prior, target)is directional (D-12): the session being LEFT sheds its matter-scoped slices before it can be touched again; the target is returned untouched (it revalidates its own Page Card on resume). Returns(cleaned_prior, target);priorisNoneon the first switch.drop_matter_scoped_context(session)keepsFIRMslices and removes everyMATTERslice (a pure copy, never a mutation), clearingsealedonce no matter context remains. This is how a matter-privileged slice from session A can never bleed into the work the user does next (§"Cross-matter confidentiality").
Seal & scope-safe titles
- Every
ContextSliceis taggedfirmormatter; a matter slice MUST carry itsmatter_id(a model validator rejects one without). Touching a matter viarecord_matter_touch()setssealed=True— a matter-touched session is the sealed unit the drop-on-switch and the per-matter boundary act on. assert_scope_safe_title()enforces the D-12 "Never": a title may only beGENERICorUSER-set; anAUTOtitle pulled from a client/matter name is rejected, so privileged names never leak into the session-dropdown metadata.- Surface changes re-inject context into the same container; the module
deliberately exposes no
fork_session— there is none (D-12 "don't fork").
Files
agents/atlas/sessions.py— the container + guards:derive_session_id(),new_session(),assert_session_owner(),assert_resumable(),revalidate_page_card_on_resume(),drop_matter_scoped_context(),switch_into(),record_matter_touch(),assert_scope_safe_title().agents/atlas/errors.py—AtlasSessionOwnerError,AtlasSessionNotSavedError,AtlasStaleSessionErroradded to the typed taxonomy (frozen wire codes).agents/atlas/tests/test_sessions.py— 24 deterministic, model-free tests.
Verification
ruff check agents/atlas→ all checks passedpytest tests/test_sessions.py -q→ 24 passedpytest tests -q→ 370 passed (no regression from the new error types)