Memory
Phase 4 gives your twin a memory with three tiers and one rule above them all: nothing is saved unless you say
so, everything saved is visible to you and deletable by you, and none of it is ever used to evaluate people or
to train a model. Names are fixed in PRODUCT-CONTRACT.md section 12.6 and the design is
ADR 0033. The statement the docs and the validation
package quote, verbatim:
Human decisions, inbox feedback, lens scorecards and memory are never used to train or tune any model.
“Evaluation data” means the per-twin reporting of the twin_evaluate job; no endpoint aggregates memory or
feedback per person.
Tiers 1 and 2 (read, forget, keep across programs) ship designed and move to
preview with the R4 gates. Tier 3 (role-shared memory), its curation routes and its permission keys are
reserved for R5 and stay designed; nothing here presents them as live.
The three tiers
| Tier | tier value | Scope | Kinds admitted | Never stores |
|---|---|---|---|---|
| 1 individual | individual | one person in one program | episodic (references to records you decided), preference, vocabulary, rubric_adjustment, lesson | other people’s decisions; any retrieved source text |
| 2 person | person | one person across every program of the organisation | preference, vocabulary, rubric_adjustment only | anything with a record or passage reference; any program fact |
| 3 role-shared | role | everyone wearing a workbench role in the organisation (R5) | rubric_version, vocabulary, lesson (curated) | program-identifying facts; personal data; user ids |
All three live in one table, memory_records, isolated by organisation at the application level and searched by
text index (no vector search in R4): every query filters organization_id and the namespace’s organisation
prefix org/{org}/ is compared with the token on every call (_check_namespace, verified in
api/app/services/memory.py:235-238; the two-organisation test pins it). A PostgreSQL row-level-security policy
keyed on a session setting is R5 (ADR 0033,
amended 2026-09-16): no such policy exists anywhere in the schema and the application role sets no session
variable, so the docs and the validation package say “organisation column plus namespace check”, never
“row-level security”, until then. A row carries tier, kind, a summary of at most 600 characters that
has passed the sanitiser and the tone lint, a body, origin (human or agent), status (proposed,
kept, promotion_requested, promoted, forgotten), source_ref, version, expires_at and, when it
was proposed by a delegate, on_behalf_of. A tier-3 row’s source_ref points to the promotion audit event,
never to the run that produced the thought; curated_by is the curator, never the author; deleting a user nulls
both. The organisation prefix of every record is compared with the caller’s organisation on every call, and a
two-organisation isolation test pins it.
Who reads what
| Reader | Sees | Route |
|---|---|---|
| you | your own tier 1 in a program and your tier 2 | GET /me/memory?program_id= (authenticated self) |
| your twin’s main run | your tiers 1 and 2 and your role’s tier 3 (about twenty, ten and twenty records by recency and text match) as a brief marked as data with the fixed prefix <<<memory:...>>> | inside the run |
| a lens node (R5) | tier 3 only | inside the run |
| an owner, lead or quality reviewer, for oversight | another person’s tier 1 only, in that program | GET /programs/{program_id}/agent-memory?user_id= under program.twins.read; audited memory.oversight_read; shown to the person as “Viewed by <Name> on <date>.” |
| an organisation admin | counts and dates only | none in R4 |
Purpose limitation: oversight reads are for incident and audit review only, never for people evaluation.
Tier 2 is excluded from oversight entirely. A program run’s brief drops any tier-2 record that carries a
reference, so a fact from program A cannot reach a brief for program B (the two-program test). Agent-origin
episodic records are never citable as evidence; only the passage references they carry are. Under delegation the
delegate’s own tiers are read and written; the principal’s memory is never touched, and a note proposed while
deciding for someone lands in the delegate’s tier 1 tagged on_behalf_of.
Writes: keep, promote, curate, forget
- Keep (“Remember this?”). A twin proposes a note with
memory.propose_note(tierindividualonly); it arrives as awrite_approvalinterrupt rendered under the proposal it came from, with Keep and Not now inline. Nothing is saved without being shown. Keeping is auditedmemory.keep. Automatic keeping after an accepted decision is an R5 opt-in per program (USER_SETTING_MEMORY_AUTOKEEP, default off) and does not exist in R4. Fixed phrase: “Your twin saves nothing unless you say so.” - Keep across programs (tier 1 to tier 2) is the person’s own action and the only tier 1 to tier 2 path
in R4; the twin never proposes it.
POST /programs/{program_id}/agent-memory/{record_id}/promotewith an empty body, by the record’s owner underprogram.agent_memory.manage, answersMemoryPromoteOut {record, scrubbed_terms_count}. When the control cannot act it stays focusable and the line under it states why: “Your persona cannot keep notes across programs in this release.” when you lack the key, or “Only preferences, vocabulary and rubric adjustments can be kept across programs.” when the record’s kind does not travel (verified inweb/components/workbench/memory-panel.tsx:44-48, 101-111andweb/lib/content.ts:295-296). Onlypreference,vocabularyandrubric_adjustmentare promotable;episodicandlessonanswer422 kind_not_promotableand the error sits beside the Keep control. The scrubber runs on the summary regardless and removes the program slug and name, compound and batch identifiers, target names from the program’s hypotheses, milestone names, partner names from connection metadata and member names; one test per item. Auditedmemory.promote. - Suggest for the role (toward tier 3, R5). The tool
memory.propose_promotionisdesignedin R4 (contract 12.4 and 12.6, recorded 2026-09-16): its tool specification exists, it answersnot_implementedfor every target tier, and it sits in no harness toolset and not inALWAYS_ALLOWED_TOOLS. Fact (verified inapi/app/tools/allowlists.py:37andapi/app/tools/catalogue.py:92): the set holdsmemory.recall,memory.propose_noteandlens.list_rubricsonly, andMEMORY_NOTE_TIERSis("individual",)| Consequence: the code matches the contract. In R5 the tool rides the same “Remember this?” interrupt; acceptance files a copy stripped of user ids aspromotion_requested. Curation is two-party: a holder oforg.role_memory.manageaccepts and a current holder of that workbench role confirms; both names and the version are shown; a rubric version with no role-holder confirmation is refused. Personas withoutprogram.agent_memory.manageusePOST /me/memory/{id}/propose-share(R5). Fixed phrase: “Shared memory holds methods, never program facts.” - Forget.
DELETE /me/memory/{record_id}(own record) auditsmemory.forget; an owner or lead may delete a tier-1 record with a reason, audited and shown to the person. Forget is a two-step control (“Confirm forget”) followed by the fixed line in place: “Forgotten. Nothing from this record is used again.” A tombstone bysource_refprevents the same note from being derived again.
Interrupt kinds stay 5; the memory interrupt is the existing write_approval.
Injection safety
A memory summary passes the sanitiser, the injection-pattern list and the markup allow-list twice: when it is
proposed and again when it is injected into a brief. The brief is data with the fixed prefix, and every harness
prompt states that memory never changes rubric, refusal or citation behaviour. The agent container’s contract
tests push the injection fixture through memory.propose_note and require it refused or neutralised.
Retention
| Tier | Retention |
|---|---|
| 1 | soft-forgotten 90 days after membership in the program ends; deletable any time by the person, or by an owner or lead with a reason; episodic records expire after 180 days unless pinned |
| 2 | deleted with the user; exported with the person’s data |
| 3 | superseded, never edited; deleted with a reason by a curator pair |
The worker job memory_expire applies the windows; tombstones by source_ref stop re-derivation.
How memory is shown
The “What your twin remembers” panel is opened from below the fold of the role home and docks beside the
regions (?remembers=1); opening it beside a thread is designed and not in this build. Its regions are the fixed phrases “In this program”, “About you, everywhere” and “Shared by <role plural>” (for example “Shared by medicinal chemists”). Each record shows its kind, its date, “Your twin
remembers this because you decided it.” for episodic records, and “Viewed by <Name> on <date>.” when an
oversight read happened. Controls: Forget and Remove are two-step controls; Keep across programs and
Suggest for the role expand a one-line inline form. See
How to read what your twin remembers.
Permissions, routes and DTOs
Tier 1 uses the existing program.agent_memory.read and program.agent_memory.manage, with
program.twins.read for oversight; tier 2 uses self routes; tier 3 will use org.role_memory.read (four
organisation personas) and org.role_memory.manage (owner, admin) at PERSONA_VERSION 4 in R5, which also
adds program.agent_memory.manage to the quality reviewer and the manufacturing specialist. R4 adds no
permission key; PERSONA_VERSION stays 3; the catalog stays at 87.
| Route | Rule | Returns |
|---|---|---|
GET /me/memory (?program_id=) | authenticated (self) | MemoryRecordOut[] |
DELETE /me/memory/{record_id} | authenticated (own record) | 204; audit memory.forget |
POST /programs/{program_id}/agent-memory/{record_id}/promote | program.agent_memory.manage + own record | MemoryPromote {} -> MemoryPromoteOut; 422 kind_not_promotable |
GET /programs/{program_id}/agent-memory (?user_id=, ?tier=) | program.agent_memory.read; another person’s records need program.twins.read | MemoryRecordOut[]; audit memory.oversight_read |
DELETE /programs/{program_id}/agent-memory/{record_id} | program.agent_memory.manage (owner or lead, with a reason) | 204 |
MemoryRecordOut is the existing {id, namespace, summary, created_at} extended with tier, kind, origin, status, program_id?, workbench_role_key?, source_ref?, promoted_from_id?, on_behalf_of?, version, expires_at?, viewed_by?[{user_id, name, at}], updated_at. The two existing program routes leave designed for preview
with this section and gain the user_id and tier filters; no new row is added for them.
Related
Source: PRODUCT-CONTRACT.md sections 12.6, 12.7 and 12.8; PRODUCT-PLAN.md Part 9 section 6; api/app/auth/personas.py (program.twins.read held by owner, lead and quality reviewer at PERSONA_VERSION = 3); api/app/auth/permissions.py (program.agent_memory.read | manage, EXPECTED_PERMISSION_COUNT = 87); docs/lib/content.ts (noTrainingStatement, MEMORY_TIERS, MEMORY_PROMOTABLE_KINDS, consoleCopy); decisions/0033-memory-tiers-on-own-tables.md