Skip to Content

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

Tiertier valueScopeKinds admittedNever stores
1 individualindividualone person in one programepisodic (references to records you decided), preference, vocabulary, rubric_adjustment, lessonother people’s decisions; any retrieved source text
2 personpersonone person across every program of the organisationpreference, vocabulary, rubric_adjustment onlyanything with a record or passage reference; any program fact
3 role-sharedroleeveryone 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

ReaderSeesRoute
youyour own tier 1 in a program and your tier 2GET /me/memory?program_id= (authenticated self)
your twin’s main runyour 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 onlyinside the run
an owner, lead or quality reviewer, for oversightanother person’s tier 1 only, in that programGET /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 admincounts and dates onlynone 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 (tier individual only); it arrives as a write_approval interrupt rendered under the proposal it came from, with Keep and Not now inline. Nothing is saved without being shown. Keeping is audited memory.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}/promote with an empty body, by the record’s owner under program.agent_memory.manage, answers MemoryPromoteOut {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 in web/components/workbench/memory-panel.tsx:44-48, 101-111 and web/lib/content.ts:295-296). Only preference, vocabulary and rubric_adjustment are promotable; episodic and lesson answer 422 kind_not_promotable and 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. Audited memory.promote.
  • Suggest for the role (toward tier 3, R5). The tool memory.propose_promotion is designed in R4 (contract 12.4 and 12.6, recorded 2026-09-16): its tool specification exists, it answers not_implemented for every target tier, and it sits in no harness toolset and not in ALWAYS_ALLOWED_TOOLS. Fact (verified in api/app/tools/allowlists.py:37 and api/app/tools/catalogue.py:92): the set holds memory.recall, memory.propose_note and lens.list_rubrics only, and MEMORY_NOTE_TIERS is ("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 as promotion_requested. Curation is two-party: a holder of org.role_memory.manage accepts 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 without program.agent_memory.manage use POST /me/memory/{id}/propose-share (R5). Fixed phrase: “Shared memory holds methods, never program facts.”
  • Forget. DELETE /me/memory/{record_id} (own record) audits memory.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 by source_ref prevents 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

TierRetention
1soft-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
2deleted with the user; exported with the person’s data
3superseded, 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.

RouteRuleReturns
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}/promoteprogram.agent_memory.manage + own recordMemoryPromote {} -> 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.readMemoryRecordOut[]; 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.

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