ME-5 (Story-2099) built the three memory stores; ME-6 builds the
read. Two pieces (RFD docs/notes/rfd-memory-engine.md
§7.1, §6.2, §5.5, §10): a pure contracts package
(src/shared/memory_contracts/) the stateless brains import as the
single source of contract truth, and a pack assembly service
that folds the stores plus a People projection into one
memory_pack.v1 the broker will stamp onto the invoke envelope. No
new migration — the stores already exist. The interceptor, the
"remember:" grammar, and the broker stamping are later ME stories;
ME-6 is the seam they hang off.
| Surface | Change |
|---|---|
src/shared/memory_contracts/ (§7.1, §6.2) |
New PURE package, mirror of context_contracts: MemoryPack/MemoryPerson/MemoryItemView (memory_pack.v1) and MemoryProposal/MemoryProposalItem (memory_proposal.v1). extra="forbid" freezes the shape; every carrier pins a schema_version literal. No sqlalchemy/boto3/settings import — a store dependency here would drag the ORM into every brain. |
tests/unit/memory_contracts/test_purity.py |
The no-sqlalchemy CI gate (mirror of the context_contracts one): a fresh-interpreter import proves nothing transitive pulls a store module in, and an AST scan bans the direct import lines so a future edit fails loudly at review. |
src/shared/memory/pack.py — assemble_memory_pack() |
The pack assembly service. A fixed set of grouped queries (§10, DR-1): 5 statements for a matter scope, 3 for a form scope — never per-row. Reads the fold + memory_version, accepted preferences, accepted non-superseded strategy, and the People projection. |
src/shared/agent_contracts/invoke.py |
Additive optional field memory_pack: MemoryPack | None = None on agent_invoke.v1. A missing pack is a valid pack (the brain degrades to today's behaviour — fail-open); it rides the SAME pre-fetch assembly that stamps the Context Pack (RFD R-6 / CE-14 seam). No breaking change — the existing envelope is byte-unchanged when the field is absent. |
src/shared/observability/manual_span.py |
New closed-registry span functions.memory.pack_assemble (ADR-124), INTERNAL, required attr firm_id (root dimension, never baggage). It is where the pack build's db_query_count budget is observed. |
Each Lambda invocation talks to RDS over the network, so latency is round-trip accumulation. DR-1 forbids a query count that scales with rows. The assembly is bounded by the scope shape, not by how many parties / preferences / strategy items exist:
| # | Query | Scope |
|---|---|---|
| 1 | agents.session_memory → memory_version + fold_text | always |
| 2 | agents.counsel_preference WHERE status IN (accepted, auto_accepted), this counsel | always |
| 3 | agents.matter_strategy_item WHERE status='accepted' AND superseded_by IS NULL, ordered goal→focus→constraint→note, capped | matter only |
| 4 | identity.users → the requesting counsel's display name | always |
| 5 | business.matter_participants → the who's-who | matter only |
5 statements for a matter scope, 3 for a form scope — both ≤ 6.
The O(1)-query test (tests/unit/test_memory_pack_assembly.py) proves
it by running the assembly with 1 participant and with 200 and asserting the
execute() count is identical.
There is no people table. People is assembled at pack build from
what already exists: the requesting counsel (from identity.users) as
"counsel (you)" on side us, plus the matter's
matter_participants mapped through their side axis
(ours→us, opposing→them, court/others→other)
and role_type. When the frame or participants change, People changes with them on
the next pack build — a people_memory table would drift from the
frame within a week. Pre-matter (form scope) degrades honestly to just the
requesting counsel. (Full firm-side co-counsel enumeration + intake-form
participants are the ME-14 "completeness" story.)
memory_version is the data version — the cache-key
component (R-2) that bumps on any accepted memory change; a fresh scope with no
session_memory row reads as the born-at-1 default.
schema_version ("memory_pack.v1") is the
shape version — additive-only within its vN, forks
to v2 only on a field-meaning change. They are never conflated.
A seeded fixture would hide the case that actually breaks first: a brand-new
matter with nothing remembered. tests/integration/test_memory_pack_assembly_rls.py
exercises both boundary states through the runtime pramaan_app role
under RLS — a populated matter (preference + strategy + versioned fold +
participant, with a superseded strategy item proven absent), and the
empty scope (fresh counsel, fresh matter, nothing seeded) which yields a valid
pack: memory_version = 1, session_fold = None, empty
lists, only "counsel (you)" in People. A wrong column name or an RLS
gap fails here, not in prod.
uv run ruff check
uv run pytest tests/unit/memory_contracts tests/unit/test_memory_pack_assembly.py
make openapi # no drift — agent_invoke.v1 is a broker→brain envelope,
# not a served route DTO
make test-db && make test # includes the RLS assembly integration proof
Recall: ME-6 turns the stores into one importable, fail-open,
O(1)-query memory_pack.v1 — the seam the interceptor and the broker
stamping hang off next.