ME-1 is the Memory Engine spine root. It extends the existing
agents.chat_session identity table (RFD
docs/notes/rfd-memory-engine.md §4.1, §4.3, §5.1) so a session's
identity is (matter_id | form_id) × counsel × session_id [× agent_key],
and so a session carries the ACP lifecycle state. No parallel table is created —
chat_session already owns identity, launch wiring, and RLS, so the
ladder says extend it. agents.chat_turn is untouched.
| Surface | Change |
|---|---|
Migration 0149_chat_session_lifecycle |
The one additive + one-nullability-flip migration. See the DDL rows below. |
chat_session.matter_id |
Flipped to NULLABLE. A pre-matter intake/settings session has no matter yet. |
chat_session.form_id (new) |
varchar(128), the pre-matter anchor and the form→matter re-key origin provenance (retained after promotion, §4.2). |
CHECK chat_session_scope_present |
matter_id IS NOT NULL OR form_id IS NOT NULL — the at-least-one scope guard (0092 agent_run precedent). |
CHECK chat_session_kind_valid |
Widened to admit named_agent_chat beside default_agent_chat/legacy_agent_chat (drop/recreate by hand — autogenerate cannot see CHECK predicates). |
| Lifecycle columns (new) | availability varchar(16) NOT NULL DEFAULT 'live' (CHECK live/archived/deleted), deleted_at timestamptz, cleared_until_turn_index int (CHECK ≥ 0). The DEFAULT 'live' is persistent so every existing INSERT path — which never names availability — keeps writing live sessions unchanged. |
| Indexes (new) | ix_chat_session_dropdown (firm_id, matter_id, user_id, agent_key, last_active_at DESC) — the session-list query — and ix_chat_session_form (firm_id, form_id) WHERE form_id IS NOT NULL — the re-key + pre-matter list. |
uq_chat_session_default_agent_chat |
Left intact. Today's single auto-resolved default thread keeps its exactly-one guarantee while counsel opens any number of named sessions beside it. |
src/shared/agents_db_models.py |
The Core table mirror follows the DB: matter_id nullable, plus the four new columns. The Python ChatSessionKind Literal is intentionally left at two values — no ME-1 code constructs or reads a named_agent_chat session yet; it extends with the session-creation verb in a later ME story. The DB CHECK is already ahead so that future consumer is not blocked-by-construction (the 0126 precedent). |
identity = (matter_id OR form_id) x counsel_user_id x session_id [x agent_key]
^ the matter, once one exists
^ form_id pre-matter (intake / settings)
^ session_id MANDATORY (the PK anchor; every turn hangs off it)
Every existing default-session path is unchanged: matter_id is only
relaxed (never a new required column), availability defaults to
'live' at the DB so inserts that omit it still succeed, the default
unique index is untouched, and the Python vocabulary is unchanged. The existing
test_chat_session_identity.py suite passes verbatim.
downgrade() re-narrows the shape. Because a form-scoped row
(matter_id IS NULL) or a named_agent_chat row cannot
survive the pre-ME-1 NOT NULL / two-value CHECK, the downgrade DELETEs those rows
first, then re-asserts the tighter shape (the 0060 narrowing-downgrade precedent).
Destructive on down-migration by construction; it exists to keep
alembic downgrade base runnable.
make test-db-reset && make test-db # fresh volume, migrate to 0150 set -a; . ./.env.test-db; set +a # alembic up/down/up + check (a1c9f0d2b3e4 = 0150's parent, main's 0149) alembic downgrade a1c9f0d2b3e4 && alembic upgrade head && alembic check # "No new upgrade operations detected" alembic downgrade base && alembic upgrade head # full CI-parity roundtrip # existing default-session tests green UNCHANGED pytest tests/unit/test_chat_session_identity.py -q # 6 passed # cross-firm RLS zero-rows + zero-state list [] via a REAL request (RLS runtime role) pytest tests/integration/test_chat_session_lifecycle_rls.py -q # 1 passed # migration shape unit coverage pytest tests/unit/test_chat_session_lifecycle_migration.py -q # 2 passed
The zero-state proof runs the dropdown-index query against a freshly provisioned
(empty) firm through the RLS-enforced pramaan_app role and asserts
[] — the real first-run state, not a seeded fixture. The same test
proves firm B's runtime sees zero of firm A's rows, and that an
archived named_agent_chat session (a real insert of the
widened kind against Postgres) is excluded from the live dropdown while the
live one is returned. No HTTP list endpoint exists in ME-1 scope; it
arrives with the session-list story that consumes this index.