ME-5 lands the three greenfield persistence stores the Memory Engine reads into
the Memory Pack (RFD docs/notes/rfd-memory-engine.md §5.2, §5.3,
§5.4) and the one code path that bumps
memory_version — the cache-key component every accepted memory
change invalidates. This is substrate only: the interceptor,
summarizer, proposal-confirm flow, and pack assembly land in later ME stories.
All three tables live under the agents schema (broker runtime the
stateless brain never reads directly), each firm-RLS'd, each self-contained on
provenance so an item survives the deletion of the session that produced it.
| Surface | Change |
|---|---|
Migration 0156_memory_stores |
Creates the three tables off head d4c1b8e5a207 (0155). Hand-written RLS, partial indexes, CHECKs, grants, and updated_at triggers (autogenerate sees none of these). |
agents.counsel_preference (§5.2) |
Cross-matter, per-counsel style items ("lead with exposure"). Item rows, not a JSON array — each is independently proposed/confirmed/edited/deleted with its own provenance. status ∈ proposed/accepted/auto_accepted/rejected/deleted; source ∈ interceptor/explicit/settings. Exact-dup guard: partial unique uq_counsel_preference_accepted_text (firm_id, user_id, text) WHERE status IN ('accepted','auto_accepted'). No matter_id by design (it is about a person). |
agents.matter_strategy_item (§5.3) |
Matter-scoped, team-shared working plan ("goal = settlement"). kind ∈ goal/focus/constraint/note; status adds superseded; proposed_by_user_id is attribution, not scope (the plan belongs to the matter, provenance records who said each item). superseded_by is a self-ref edge — a new focus writes a new accepted row and marks the old one superseded, not an overwrite. |
agents.session_memory (§5.4) |
The rolling per-(matter|form × counsel × agent) fold and the home of memory_version int NOT NULL DEFAULT 1. Scope guard session_memory_scope_present (matter_id IS NOT NULL OR form_id IS NOT NULL) mirrors 0092/0150; two partial uniques (matter-scoped and form-scoped) give one fold row per scope; agent_key carries the same CHECK vocabulary as agent_run/chat_session (§5.4). |
| Provenance FK stance (§6.2) | origin_session_id / origin_turn_id / source_excerpt are denormalized and carry no FK to chat_session/chat_turn — the item must outlive session deletion and turn-content purge. The load-bearing ids (firm_id, matter_id, owning user_id) DO FK with RESTRICT, the 0044 agents-foundation precedent. |
src/shared/agents_db_models.py |
The Core-table mirror (used by the sibling pramaan-agents repo) plus the closed-vocabulary Literals/tuples. The alembic roundtrip's check ("No new upgrade operations detected") proves the mirror matches the DDL. |
src/shared/memory/version.py |
The ONE bump_memory_version() code path — see below. |
src/shared/audit/registry.py |
Two new subjects (§9.3): counsel_preference (area settings) and matter_strategy (area matter). Both exclude text/source_excerpt from any snapshot (memory text is content class, §9.1 R10/R15). Verbs reuse the existing vocabulary — accept→approved, undo→rejected. The chat_session subject already exists (ME-2). |
memory_version bump — one grouped UPDATE, three scopes (§5.4)
Every accepting/undoing write path (fold persist, strategy accept/supersede/delete,
preference accept/undo/delete, explicit remember) ends by bumping
memory_version so the next turn's prompt sandwich re-reads a fresh
memory block instead of a stale cached one. There is exactly one statement,
parameterized by scope:
bump_memory_version(session, firm_id, user_id=U) # preference change: cross-matter
-> UPDATE ... WHERE firm_id=:f AND user_id=:u # every fold row of counsel U, ONE grouped UPDATE (DR-2)
bump_memory_version(session, firm_id, matter_id=M) # strategy change: matter-shared
-> UPDATE ... WHERE firm_id=:f AND matter_id=:m # every counsel's fold for matter M
bump_memory_version(session, firm_id, matter_id=M, user_id=U, agent_key=K) # fold persist / explicit remember
-> UPDATE ... WHERE firm_id=:f AND matter_id=:m AND user_id=:u AND agent_key=:k # the single scope row
A bare firm-only call raises ValueError (a firm-wide bump is a bug,
never a use case). The helper only bumps rows that already exist — a scope with
no fold row has no cached block to invalidate, and its row is born at version 1
when the first fold persists (the ME-9 writer's job), which is the correct
starting version. It runs in the caller's transaction, so the bump commits or
aborts with the memory change it announces.
RLS is firm isolation + staff-with-grant SELECT (the 0153/0154 clone); the
preference service additionally filters user_id = actor (a colleague's
packs never carry them, §5.2 — service-layer, not RLS). The app role keeps
DELETE: counsel delete/clear is a real app-role write (§9.2), unlike
the retention-locked inbox table. Memory items use soft states for audit
retention; hard erasure rides the firm-level flows.
make test-db-reset && make test-db # fresh volume, migrate to 0156 set -a; . ./.env.test-db; set +a # alembic full CI-parity roundtrip + drift check make test-db-alembic-roundtrip # down->base, up->head, "No new upgrade operations detected" # roundtrip + firm-isolation RLS + the bump code path, through a REAL request as pramaan_app pytest tests/integration/test_memory_stores_rls.py -q # 1 passed # migration shape (RLS/indexes/uniques/CHECKs/grants) + vocab-drift + bump-helper unit shape pytest tests/unit/test_memory_stores_migration.py tests/unit/test_memory_version_bump.py -q # 9 passed # the global firm-scope RLS-completeness gate now accounts for the three new tables pytest tests/integration/test_rls_isolation.py -q # 25 passed
The system-boundary proof (test_memory_stores_rls.py) inserts a
preference, a strategy item, and four fold rows through the RLS-enforced
pramaan_app role against a freshly provisioned (empty) firm, reads
them back, then proves firm B's runtime sees zero of firm A's
memory rows and that a cross-firm bump touches nothing. It then exercises all four
bump scopes on real rows: the user-scoped preference bump moves every one of that
counsel's folds while a colleague's stays put; the matter-scoped strategy bump
moves every counsel's fold for that matter; the single-scope and form-scope bumps
each move exactly one. No HTTP endpoint exists in ME-5 scope; the routes that
consume these stores arrive with later ME stories.