Story-3094 · Memory Engine

One counsel-owned personalisation document for each agent

Counsel writes one block saying how Vani should work with him. The broker stores that block exactly as typed and places it in Vani’s memory pack. It does not split, classify, infer, confirm, or silently shorten the text.

The mental model: this is a small configuration document, not a bag of detected facts. The row is an addressed envelope: (firm, counsel, agent) identifies who may open it and which agent receives it; text is the untouched letter inside.

The storage shape

agents.counsel_preference
  UNIQUE (firm_id, user_id, agent_key)
  agent_key VARCHAR(128) NOT NULL
  text      VARCHAR(3000) NOT NULL

Today:
  persona: Vani
  stable machine key: mira

This deliberately reuses the existing table. The migration removes the old item-row uniqueness/indexes, adds the agent address, widens the text, and installs the one-row invariant. Existing item rows are deleted because merging them into a document would require inventing ordering and prose. PRAMAAN is pre-production, so no compatibility reader or dual store remains.

The API is a document read and replace

RequestMeaning
GET /app/me/personalisation/mira Returns Vani’s block, or an empty document before the first save, plus remainingCharacters.
PUT /app/me/personalisation/mira Replaces the whole block. A 3,001-character request is rejected with an actionable 422; the previous block remains intact.

Only mira is accepted today. There is no invented surface key, firm-wide house style, auto-detection path, or brain writer. A save is a broker/API transaction; an identical save is a no-op so it does not needlessly invalidate the cached prompt prefix.

How the block reaches only its agent

settings PUT (firm + counsel + "mira")
  -> one-row upsert
  -> bump only mira_matter_chat memory versions
  -> later broker session
  -> assemble_memory_pack(agent_key="mira_matter_chat")
  -> read stored agent_key="mira", LIMIT 1
  -> memory_pack.text
  -> top-level agent_invoke.v1
  -> Story-3050: Vani ingress + CORE-first prompt placement

RLS remains the outer wall and every application query also includes firm and counsel. The pack maps Vani’s runtime key mira_matter_chat to the stable stored key mira. A different agent receives no block. The wire addition is the defaulted text projection allowed by memory_pack.v1’s additive versioning rule; the existing preferences list keeps its frozen independent-item meaning and remains empty for this document.

End-to-end boundary: this PR does not make a new Vani session honour the block. Story-3050 owns the agents half. Today pramaan-functions sends memory_pack at the top-level invoke envelope, but Vani does not copy it into MiraConverseInput, so she receives None. Vani also renders memory after the matter frame as SESSION_MEMORY, not as D8’s CORE-first personalisation block.

Why this departs from the item-row RFD

The RFD required independent rows for auto-detected preferences so each inference could carry provenance and be confirmed or rejected separately. Auto-detection is parked after reaching 88.9% precision against a 99% floor. Here counsel authors one textarea document and owns wholesale edits; there are no inferred items to confirm. Retaining item rows would preserve machinery whose reason no longer exists.

Evidence