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.
(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
| Request | Meaning |
|---|---|
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.
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
-
The clean Alembic roundtrip upgrades to head, downgrades to base, upgrades
to head again, and finishes with
alembic check - The RLS integration test inserts a 3,000-character block, then uses a different firm’s runtime role to query that exact row id and receives no row.
-
The API integration test saves multiline text, closes the client/session,
opens a later session, and reads the exact text back for
mira. The pack test also plants another agent’s row and proves it does not enter Vani’s functions-side pack. - The over-budget test first saves a valid 3,000-character block, attempts 3,001 characters, receives the actionable validation error, and proves the prior document was not changed.