Iris is the subsystem that decides what the assistant remembers, for how long,
under what conditions, and who else can ever see it. It owns assistant
identity, the memory scope hierarchy, retention and decay, conflict resolution,
privacy-aware suppression, cross-device continuity, and the operator-inspection
regime. Iris serves every customer-facing surface in V1 — the
Assistant Experience, Tara,
Arete, Veritas,
Nyx, Nisaba, and
Metis — plus the governed admin path described in
Review, Compliance, and Trust & Safety and
Privacy, Consent, Data Portability, and User Controls.
It sits between the Psyche Real-Time Runtime,
which produces the conversational turns, and the durable governance substrate,
which audits every recall, write, and inspection. Backlog and acceptance
criteria live under §10 of V1/TODOS.md; the contracts
referenced here are shipping Zod schemas in libs/contracts/src/iris/, and the
runtime lives in the @oshun/memory-iris package (libs/oshun/memory-iris/).
Reality note. This area is overwhelmingly shipped, not aspirational: the
MemoryEntrycontract, the deterministicRecallPipeline, the append-only consent ledger, the data-rights lifecycle, the conflict resolver, multi-actor masking, and the admin-inspection state machine all exist as real, tested modules. Where the prose of the original spec diverges from the code (scope names, thebodyfield, lifecycle values, FSRS), this page follows the code and calls the divergence out honestly. Iris memory decay is usage-weighted, not FSRS — FSRS v4 is a separate Mnemosyne concern (libs/mnemosyne/core/src/memory-science.ts) and is not an Iris feature.
Memory Scope Hierarchy#
A scope is the boundary that determines which contexts may read or write a given memory. Iris models scope at two layers, and the two layers do not have identical vocabularies — a fact worth stating plainly because the V1 spec text undercounts both.
The canonical contract: eight scope kinds#
The source of truth is MemoryScopeKeySchema in
libs/contracts/src/iris/entry.ts (lines 15–45), a Zod discriminated union on
kind with eight members. Each carries exactly the identifiers needed to
address the scope:
kind |
Identifiers carried | Meaning |
|---|---|---|
profile |
userId |
Durable, user-controlled facts about the user. |
session |
userId, sessionId |
In-flight conversation state and a short rolling window of prior sessions. |
scene |
userId, sessionId, sceneId, roomId |
Spatial room/venue/scene context for embodied (V3) sessions, bounded to one rendered scene. |
pose |
userId, sessionId, sceneId, poseId, avatarId |
Aggregate body-pose / avatar-alignment memory for opted-in embodied coaching. |
notebook |
userId, notebookId |
Memory bound to a specific notebook, collection, or course context. |
crisis |
userId |
The frame entered during a Lilith crisis cascade — see suppression below. |
operator-copilot |
userId, operatorId |
A separate scope for admin copilots with narrower retention and tighter audit. |
tenant |
tenantId, userId |
Institutional Metis tenant memory of learner study state; never blends into consumer profile memory. |
Accuracy fix. Earlier prose (features.md and
ARCHITECTURE.md:752–753) said "MemoryScope has exactly five values:profile | session | notebook | operator-copilot | tenant." That undercounts. The shipping contract is eight kinds — it also carriesscene,pose, andcrisis. The five-item list also contradicted the doc's own recall algorithm, which already relied on scene/crisis scopes. This page reconciles to the code.
The scene and pose scopes exist for embodied/V3 memory and were entirely
absent from the V1 five-scope list. They are real (with their own retention
envelopes and export payloads, below) and are simply not yet surfaced in the
consumer V1 UX.
The adapter layer: eleven scopes#
The runtime adapter (libs/oshun/memory-iris/src/memory-model.ts,
IrisMemoryScope) operates on a wider eleven-value vocabulary:
assistant_profile, session, scene, pose, conversation, domain,
cross_domain, notebook, operator_copilot, tenant, admin_review. The
names differ from the canonical contract on purpose — assistant_profile is the
adapter spelling of canonical profile; the adapter additionally distinguishes
conversation (persistent threaded history) from session (ephemeral live
context), splits domain from cross_domain, and adds the governed
admin_review surface. Treat the canonical contract as the wire/storage shape
and the adapter scopes as the policy-and-retention layer that maps onto it.
Per-scope governance metadata#
Each adapter scope carries a rich governance blueprint (IRIS_SCOPE_BLUEPRINTS
in memory-model.ts) that the original spec never surfaced. Every scope
declares:
requiredConsents— which consent types must be granted for the scope to be enabled (e.g.poserequiresdata_processing,data_storage, andsensitive_data).optOutCategories— the opt-out categories that suppress the scope.canonicalTiers— which memory tiers it spans (core/working/archival/episodic/semantic).adminReviewableandrequiresGovernanceReview— whether operators may inspect it and whether it is gated behind governance workflow.allowedConsumers— the explicit allow-list of which subsystems may read it (assistant,tara,arete,veritas,nyx,nisaba,metis,studio,admin,support).
buildIrisScopePolicies() evaluates these blueprints against the user's live
consents and opt-outs and returns, per scope, an enabled flag, an effective
mode (durable / ephemeral / suppressed), and human-readable notes
explaining any suppression ("Session memory requires data processing consent.",
"Consumer support is not allowed to use this scope."). The operator_copilot,
tenant, cross_domain, pose, and admin_review scopes all carry
requiresGovernanceReview: true.
Retention, Decay, and Compaction#
Retention windows#
Retention is data-first: the constants live in
libs/oshun/memory-iris/src/scope-hierarchy.ts, and
getIrisScopeRetentionEnvelope() maps each scope to an envelope describing how
long raw and summarized memory survive and what happens at expiry.
| Scope | Raw retention | Summary retention | Expiry action | Constant |
|---|---|---|---|---|
assistant_profile (profile) |
durable (no max) | — | review | retained until user/account deletion |
session / conversation |
30 days | 90 days | summarize | IRIS_SESSION_RAW_RETENTION_DAYS=30, IRIS_SESSION_SUMMARY_RETENTION_DAYS=90 |
scene |
14 days | 90 days | summarize | IRIS_SCENE_RAW_RETENTION_DAYS=14, IRIS_SCENE_SUMMARY_RETENTION_DAYS=90 |
pose |
7 days | 30 days | summarize | IRIS_POSE_RAW_RETENTION_DAYS=7, IRIS_POSE_SUMMARY_RETENTION_DAYS=30 |
notebook |
bound to notebook lifetime | — | delete (on notebook delete) | — |
operator_copilot |
14 days | — | review | IRIS_OPERATOR_COPILOT_RETENTION_DAYS=14 |
tenant |
365 days (per contract) | — | review | IRIS_TENANT_MEMORY_RETENTION_DAYS=365 |
Accuracy note. The V1 spec said admin-copilot memory was "7 days raw, 30 days summarized." The shipping constant is
IRIS_OPERATOR_COPILOT_RETENTION_DAYS=14with a single review action. This page follows the code.
Session-to-profile promotion#
Session memory is never silently promoted to durable profile memory.
evaluateIrisSessionProfilePromotion() enforces a two-path rule: a promotion is
allowed only when the user explicitly asks ("explicit-promotion") or when a
fact recurs at or above IRIS_SESSION_PROFILE_PROMOTION_THRESHOLD = 3
occurrences ("repeated-occurrence-threshold-met"). Anything else returns
allowed: false with a precise reason (not-requested,
implicit-promotion-blocked, repeated-occurrence-threshold-not-met). This is
the implementation of the inference-vs-confirmation policy below: an inference
must earn durability.
Decay function (usage-weighted, not FSRS)#
Decay is usage-weighted, computed inside the recall ranker rather than as a
spaced-repetition schedule. The recencyScore in RecallPipeline
(recall/pipeline.ts) is an exponential decay on the time since
lastReferencedAt (falling back to updatedAt) with a 30-day half-life:
const halfLifeMs = 30 * 24 * 60 * 60 * 1000;
return Math.exp(-ageMs / halfLifeMs);
Unreferenced facts therefore decay in rank; referenceCount and the origin
weight push frequently used and user-stated facts back up. There is no FSRS in
Iris. FSRS v4 — FSRSParameters, FSRSReviewResult,
FSRS_DEFAULT_PARAMETERS, calculateRetention(stability, elapsed) — lives in
libs/mnemosyne/core/src/memory-science.ts and serves study-scheduling, a
different domain. The only stability/fsrs strings inside
@oshun/memory-iris are unrelated (sort stability, a status literal). Do not
attribute spaced repetition to Iris.
Compaction and tombstones#
Compaction is rolling summarization with traceable provenance: every entry
carries a provenanceChain of ProvenanceLinks (kind ∈
session-turn | session-summary | profile-promotion | manual-import | inference-batch | notebook-attachment | scene-event | pose-sample,
each with a ref and capturedAt), so a summary can always be expanded back to
the turns that produced it. Deletions are tombstoned — tombstoned is the
only terminal lifecycle — and propagated; no silent re-creation from old
conversations, and every forget event is appended to the audit chain.
The MemoryEntry Contract#
Every fact Iris stores is a MemoryEntry. The canonical schema is
MemoryEntrySchema at libs/contracts/src/iris/entry.ts:162, with
type MemoryEntry = z.infer<typeof MemoryEntrySchema>. Its lifecycle is
immutable-revision-then-tombstone: every mutation writes a new revision that
supersedes its predecessor, and the only terminal lifecycle is tombstoned.
Fields#
| Field | Type | Notes |
|---|---|---|
id |
UUID | Stable across revisions. |
revisionId |
UUID | Unique per revision. |
previousRevisionId |
UUID | null | Links the immutable revision chain. |
userId |
UUID | Owner. |
tenantId |
UUID | null | Set for tenant-scoped memory; enforced before scoring. |
scope |
MemoryScopeKey |
The eight-kind discriminated union above. |
category |
MemoryCategory |
~28-value enum (see taxonomy). |
body |
string, min(1).max(4000) |
Plain text, not a typed union. |
confidence |
number [0,1] |
Derived from origin. |
origin |
MemoryOrigin |
Provenance of the assertion. |
lifecycle |
MemoryLifecycle |
draft | active | paused | superseded | tombstoned. |
consent |
ConsentEntry[] |
Mandatory for sensitive categories. |
provenanceChain |
ProvenanceLink[] |
Survives summarization. |
expiresAt |
timestamp | null | Soft-expiry. |
lastReferencedAt |
timestamp | null | Drives recency decay. |
referenceCount |
int ≥ 0 | Monotonic; resets only on user reset. |
suppression |
MemorySuppression | null |
A single nullable object, not an array. |
multiActor |
MultiActor | null |
A single nullable object, not an array. |
auditChain |
AuditEntry[] |
Append-only. |
createdAt / updatedAt |
timestamp | — |
Accuracy fixes (spec text → code):
bodyis plain text. features.md–1976 describedbodyas a discriminated union ofFact | Preference | Goal | Boundary | Relationship | Schedule | LineageDeclaration | Sensitivity. In the real schemabodyisz.string().min(1).max(4000)(entry.ts:170). The taxonomy that is modeled is the separatecategoryfield.originenum. features.md listedpromoted-from-session. The realMemoryOriginSchema(entry.ts:81–88) isuser-stated | user-confirmed | model-inferred | operator-copilot | summarized-from-session | imported. The doc'spromoted-from-sessionis spelledsummarized-from-session, and the doc omitted the realoperator-copilotorigin.lifecycleenum. features.md listedsummarized. The realMemoryLifecycleSchema(entry.ts:91–97) isdraft | active | paused | superseded | tombstoned— there is nosummarizedlifecycle, and the doc omitteddraft.suppressionis a single nullable object. features.md–1988 describedsuppression[]as an array of four reasons (crisis-frame / user-mute / tenant-policy-mute / category-revoked). The realMemorySuppressionSchema(entry.ts:124–129) is a single nullable object{ reason, startedAt, endsAt, notes }whose reason enum iscrisis-frame | user-pause | sensitive-category | tenant-quarantine(note the renamings).multiActoris a single nullable object. features.md–1991 describedmultiActor[]as an array; the realMultiActorSchema(entry.ts:180) is one nullable{ actorHandle, relationshipNote (max 500), sensitiveInteraction }.
Category taxonomy and the sensitive set#
MemoryCategorySchema (entry.ts:48–78) is a ~28-value enum: fact,
preference, goal, relationship, commitment, identity, spatial,
pose_alignment, biometric, spiritual, medical, sexual, financial,
legal, work, family, safety, physical_health, mental_health,
substance_use, sexuality, gender_identity, religion_user_redacted,
abuse_history, immigration_status, financial_distress,
relationship_violence, legal_jeopardy, other.
Of these, a precise 19-member SENSITIVE_CATEGORIES ReadonlySet
(entry.ts:187–211), exposed via isSensitiveCategory(), gates the sensitive
path: pose_alignment, biometric, spiritual, medical, sexual,
financial, legal, safety, physical_health, mental_health,
substance_use, sexuality, gender_identity, religion_user_redacted,
abuse_history, immigration_status, financial_distress,
relationship_violence, legal_jeopardy. This is far more concrete than the
original prose list at features.md–1902 — note it includes the V3 embodied
categories pose_alignment and biometric alongside the human-disclosure
categories.
Consent records#
The shipping consent primitive on the entry is ConsentEntrySchema
(entry.ts:100–105):
{ category, grantedAt, revokedAt (nullable), source: inline-prompt | settings-toggle | dsar-import }.
Staleness fix.
ARCHITECTURE.md:754 described aConsentRecordwith "prior state, new state, reason code." That shape is stale. The entry-level primitive isConsentEntry(above), and the durable, tamper-evident consent history is the separate append-onlyIrisConsentLedger/IrisConsentEventdescribed below — richer and differently shaped than "prior/new state."
Persistence and the schema test#
Every mutation writes a new immutable revision and flips the predecessor to
superseded; tombstoned is the only terminal lifecycle. The contract
round-trip fixture covers every origin, every lifecycle, every sensitive
category, and the suppression marker.
Recall-Resolution Algorithm#
When the assistant or a domain adapter asks Iris for memory relevant to a
context, Iris runs a deterministic pipeline: same context + same memory
state ⇒ same ranked output. The implementation is the RecallPipeline class at
libs/oshun/memory-iris/src/recall/pipeline.ts:128. Recall is never silent —
each surfaced entry carries a surfaceRationale and its provenanceChain, and
the whole resolution emits a RecallAuditEnvelope.
resolve({ candidates, request }) applies the gates in this order (matching
the documented sequence):
- Tenant boundary (
matchesTenantBoundary). An entry's tenant id (entry.tenantId, orscope.tenantIdfor tenant scopes) must equal the request's tenant; mismatches are dropped and counted undertenant-mismatch. Enforced before any scoring. - Scope reachability (
isReachableScope). Only entries reachable from the request scope survive. Reachability is hierarchical:sessionrecall reachessession + profile;scenereachesscene + session + profile;posereachespose + scene + session + profile;notebookreachesnotebook + profile(and, under tenant policy, same-tenant memory); atenant-scoped request reaches only its own tenant+user rows. Drops are counted underscope-mismatch. - Crisis gate. If
request.crisisFrame === true, the candidate set collapses to safety-critical entries only — an entry survives only whenrequest.safetyCriticalContext === trueandisSafetyCriticalEntry(category === 'safety'). Everything else is dropped and counted undercrisis-frame. This matches the documented behavior (features.md–2010): during a Lilith crisis frame, no normal recall. - Suppression + lifecycle gate. Entries with an active
suppression(itsendsAtis null or in the future) are dropped and counted under theirreason; entries whoselifecycle !== 'active'are dropped underlifecycle-<state>. - Sensitive-category gate. Sensitive entries (sensitive category or
multiActor.sensitiveInteraction) pass only when (a) the category is in the request'sconsentedCategoriesand (b) the surfacing context is in the per-category allow-list (hasRelevantSensitiveContext). The per-category context map is concrete: health/biometric/pose categories requirehealth/safety/crisiscontexts;sexuality/gender_identityrequireidentity/safety/crisis; spiritual requiresspiritual; financial requiresfinancial/safety/crisis; legal/immigration requirelegal/safety/crisis; abuse/relationship-violence requirerelationship/safety/crisis. Drops are counted as eithersensitive-category-without-consentorsensitive-category-outside-relevant-context. This is exactly the "health memory surfaces in Tara recovery but never in a Veritas briefing" guarantee from the spec. - Relevance scoring. Survivors are scored by the four-factor blend in
DEFAULT_WEIGHTS(pipeline.ts:113–118):recency 0.4 + semantic 0.35 + referenceCount 0.15 + origin 0.1.referenceCountis normalized asmin(1, referenceCount/10);originweight ranksuser-stated (1) > user-confirmed (0.9) > summarized-from-session (0.75) > imported (0.7) > model-inferred (0.5) > operator-copilot (0.4). - Conflict / freshness dedupe.
dedupeByConflictcollapses candidates that share a conflict key (default key =category :: actorHandle :: subject), keeping the row with the more recentupdatedAt— most-recent-wins within a conflict cluster. - Per-surface budget then surface. Results are capped by
DEFAULT_BUDGETS, then returned with rationale and provenance. On theadminsurface (outside a DSAR workflow),multiActoris redacted before the entry leaves the pipeline (redactMultiActorForRecallSurface).
Honest approximation. Both
ARCHITECTURE.md:792 and features.md–2021 describe "semantic-similarity" as a ranking factor. In the code,semanticis Jaccard token overlap (jaccardSemantic) — set-intersection over tokens longer than two characters — not an embedding similarity. Neither prior doc flagged this. The pipeline accepts an injectablesemanticfunction inRecallPipelineOptions, so a real embedding ranker can be supplied at the call site, but the shipping default is Jaccard. Treat embedding-grade semantic recall as a forward-looking seam, not a shipped capability.
Per-surface budgets#
DEFAULT_BUDGETS (pipeline.ts:45–50) caps result counts and decides whether the
rationale is visible:
| Surface | Max entries | Rationale visible |
|---|---|---|
assistant |
12 | no (held for explainability) |
shell |
3 | no |
notebook |
999 | yes |
admin |
100 | yes |
These match the documented budgets at features.md–2031 (assistant ≤12, shell ≤3, notebook up to a working cap).
The recall audit envelope#
Every resolve() returns a RecallResult whose auditEnvelope is a concrete
shape — far more than the spec's "suppressed entries (count only)":
interface RecallAuditEnvelope {
scopeKey: string; // e.g. "scene:user:session:scene:room"
returnedIds: readonly string[];
suppressionCounts: Record<string, number>; // keyed by drop reason
evaluatedCount: number;
}
suppressionCounts is keyed by the exact reason an entry was dropped
(tenant-mismatch, scope-mismatch, crisis-frame, the suppression reason,
lifecycle-<state>, sensitive-category-without-consent,
sensitive-category-outside-relevant-context) — counts only, never content. If
a RecallAuditSink is wired, the pipeline also ingests a canonical
memory.recall.resolved audit event (policyId: 'iris-memory-recall-v1')
carrying the scope, returned ids, suppression counts, and evaluatedCount, with
severity warning whenever anything was suppressed.
Inference vs Confirmation Policy#
Iris distinguishes user-stated facts from model-inferred ones and surfaces inferences for confirmation rather than persisting them silently.
- Inference threshold. An inference becomes a persisted
MemoryEntryonly when the same inference recurs across N distinct sessions/turns above the tuned threshold (IRIS_SESSION_PROFILE_PROMOTION_THRESHOLD = 3) or the user explicitly confirms an inline prompt. Both paths start atorigin = model-inferred(orsummarized-from-sessionfor compaction-derived facts). - Promotion. A confirmed inference promotes toward
user-confirmed; uncontested inferences decay faster because theiroriginweight (0.5) is lower than user-stated (1.0). - Sensitive inferences are never auto-persisted. They always require explicit confirmation and per-category consent — enforced jointly by the sensitive-category storage gate and the recall gate.
Conflict Resolution#
When a new statement conflicts with stored memory,
resolveIrisMemoryWriteConflict() (conflict-resolution.ts) chooses a rule by
comparing the source kind of the prior and new assertions. The rule ladder
(chooseRule) is precise:
admin-copilot-never-trumps-consumer-stated— anadmin_copilot_inferredwrite can never overwrite anexplicit_useroruser_confirmedfact; the existing entry is kept.explicit-user-trumps-inferred— an explicit user statement beats any inferred source.user-confirmed-trumps-inferred— a user-confirmed fact beats inferred sources, and a prior user-confirmed fact is kept against a new inferred write (forcedOutcomeForPolicyreturnskeep_existing).most-recent-wins— the default when both sides carry equal authority.
Meaningful conflicts surface to the user as a question ("you mentioned X earlier
— should I update?") rather than a silent overwrite, and
appendIrisMemoryConflictAuditEvent() logs the prior state, new state, rule
applied, and user response.
Privacy-Aware Suppression#
Sensitive-category memory is the most tightly governed path in Iris.
- Sensitive categories are the 19-member
SENSITIVE_CATEGORIESset above;privacy-suppression.tscarries the parallelIRIS_PRIVACY_SENSITIVE_CATEGORIES,IRIS_PRIVACY_RECALL_CONTEXTS, and anIRIS_PRIVACY_SENSITIVE_CATEGORY_POLICIEStable mapping each category to its allowed recall contexts. - Storage gate.
evaluateIrisPrivacyStorageGate()blocks any sensitive write that lacks explicit per-category consent — sensitive facts are stored only with opt-in. - Recall gate.
filterIrisPrivacyAwareRecall()enforces the per-category context allow-list (the same logic the recall pipeline's sensitive gate applies), so a sensitive fact never surfaces outside a relevant context and never reaches operators except through an authorized DSAR or a safety-critical exception. - Consent-revocation cascade.
cascadeIrisSensitiveConsentRevocation()deletes the associated memory and downstream summaries when a sensitive opt-in is revoked, while retaining the audit trail. - PII handling.
redactIrisPrivacySummary()andredactIrisPiiFromText()apply per-category redaction with tokenization (IrisPiiToken) for any cross-system reference.
Consent Ledger#
Beyond the per-entry ConsentEntry, Iris keeps an append-only, fingerprinted
consent ledger (consent-ledger.ts) — a real, previously undocumented
governance primitive. Each IrisConsentEvent is versioned
(IRIS_CONSENT_EVENT_RECORD_VERSION = 1) and carries a tamper-evident
fingerprint computed by computeIrisConsentEventFingerprint() over the event
body (terms hash, witness, type, status, legal basis, timestamps).
validateIrisConsentEvent() recomputes and compares the fingerprint, so a
rewritten event fails validation. IrisConsentLedger holds the ordered event
list; appendIrisConsentEvent() is the only mutation;
computeIrisActiveConsents() derives the current granted set by taking the
latest event per consent type, and computeIrisConsentAuditTrail() replays the
full history. Consent terms themselves are hashed with
computeIrisConsentTermsHash(). The supported consent types
(IRIS_CONSENT_TYPES), statuses (IRIS_CONSENT_STATUSES), legal bases
(IRIS_LEGAL_BASES), and capture methods (IRIS_CONSENT_CAPTURE_METHODS) are
all enumerated.
Data Rights (DSAR)#
The durable record for every member-initiated data-rights request is
IrisDataRightsRequest (data-rights.ts). The three request kinds are
IRIS_DATA_RIGHTS_REQUEST_KINDS = ['delete', 'export', 'access'] — concrete
DSAR mechanics beyond the spec's "export / delete" bullets.
- Delete (
IrisDataRightsDeletePayload) supportsmode: 'soft' | 'hard', a list of scopes/categories, apurgeConsentLedgerflag (which also purges consent events for the deleted categories), and aretentionGraceMswindow within which a soft delete can be re-activated. Validation enforces that aharddelete hasretentionGraceMs === 0and that at least one scope or category is named. - Export (
IrisDataRightsExportPayload) names scopes, aformat, include flags (includeConsents,includeOptOuts,includeNotebooks,includeSessionMemory, plus opt-inincludeSceneMemory/includePoseMemory), adeliveryChannel, and anencryptedflag.buildIrisDataRightsMemoryExportBundle()assembles the scene/pose payloads, marking pose memorybiometricAggregateOnly. - Access (
IrisDataRightsAccessPayload) is a read-only "show me what you have on me" request that never mutates data.
The lifecycle is a validated state machine —
submitted → verified → in-progress → completed | failed | cancelled | expired | appealed
(ALLOWED_TRANSITIONS, isIrisDataRightsTransitionAllowed) — backed by an
append-only audit trail that must begin with a submitted event and stay
chronologically non-decreasing. Statutory windows are first-class:
IRIS_DATA_RIGHTS_DEFAULT_DEADLINE_MS is 30 days,
IRIS_DATA_RIGHTS_DEFAULT_VERIFICATION_MS is 7 days, and
expireUnverifiedIrisDataRightsRequest() auto-expires a request whose
verifyBy has passed. Completed delete/export requests must carry a
resultFingerprint binding the artifact (export-file hash or deleted-id-set
hash) to the request, so an operator can later prove the right data was returned
or deleted. Verification methods (session-token, email-link, phone-otp,
operator-signoff, court-order) and delivery channels are enumerated.
Customer Memory UX Flows#
The customer-facing surface lives at apps/oshun/web/src/app/profile/memory/
(page.tsx, ProfileMemoryControls.tsx, memory-state.ts,
memory-controls.module.css) and in the equivalent mobile flow. Every screen is
keyboard-only navigable and AA-contrast compliant. The intended flows are:
- What we remember (index): a paginated list of
MemoryEntryrows grouped by scope, with category badges, last-referenced time, an origin badge, and per-row actions (edit, forget, pause, change scope). - Detail view: full body, expandable provenance chain, recent recalls (where + when + why this entry surfaced), suppression state, and an audit-chain link.
- Edit flow: per-field edit with conflict-detection preview; each edit creates a new revision and the predecessor stays visible in a history tab.
- Forget flow: per-entry forget (tombstone), per-category clear, full-memory clear — all step-up-authed; full clear surfaces a grace window before final purge.
- Pause flow: session-scoped "off-the-record" toggle that writes no memory,
persists across reconnect, and shows a banner while active
(
suppression.reason = 'user-pause'). - Scope-change flow: promote session → profile or → notebook, demote profile → notebook, demote tenant → read-only copy, restricted by per-scope rules (tenant memory cannot promote to consumer profile).
- Export flow: the signed bundle from the DSAR path above.
Completeness note (honest). Per the V1 completeness audit, the customer memory UX is partial — the edit/pause/forget surface is not yet at full parity with the contract. The contracts, recall pipeline, consent ledger, data-rights lifecycle, and admin state machine are shipped and tested; the consumer edit-pause-forget UI is the part still in flight. See
§10ofV1/TODOS.mdand Privacy, Consent, Data Portability, and User Controls.
Multi-Actor Memory#
Conversations frequently reference other people. Iris treats each mention as a
masked actor ref, never as a memory about that other person. The shape is
the single nullable MultiActorSchema on the entry:
{ actorHandle, relationshipNote (max 500 chars), sensitiveInteraction }.
- Masking. A mentioned person is referenced by an opaque
actorHandlebound to the user's namespace, plus a relationship-only note ("my partner," "my professor"). The handle is hashed via@noble/hashes/sha2(chosen overnode:cryptoso the module bundles for the browser — the/profile/memorysurface used to 500 on the Node crypto import). - No third-party memory. Iris never builds a profile of the mentioned person; only the relationship context is retained.
- Surfacing rules. Actor handles surface only inside the user's own context.
On the
adminrecall surface and in non-DSAR inspection snapshots,multiActoris redacted tonull(redactMultiActorForRecallSurface,redactMultiActorForInspection); the only exception is a DSAR where the user is the subject. - Right to be forgotten by association. A "forget all mentions of" request tombstones every entry tagged with the actor handle and removes the handle from the namespace.
- Sensitive interactions. When
sensitiveInteractionis true, the entry inherits the sensitive-category consent and recall gating (the recall pipeline treats it as sensitive and requires amulti-actor-sensitiveconsent plus arelationship/safety/crisiscontext).
Cross-Device Continuity Protocol#
Iris is the source of truth for hand-off between devices across reading, study,
voice, ritual, journaling, and assistant transcripts. Continuity lives in two
real places: the canonical ContinuationTokenSchema at
libs/contracts/src/iris/continuation.ts:52 (with type ContinuationToken at
line 111) and the runtime under memory-iris/src/continuity/ plus
mobile-handoff.ts.
The real ContinuationToken shape#
The original spec sketched
{ userId, scopeKey, surfaceContext, anchorRef, posture, lastUpdatedAt }. A
field-by-field check shows the real strict Zod schema is richer:
| Field | Type | Notes |
|---|---|---|
tokenId |
UUID | (not in the sketch) |
userId |
UUID | matches |
scopeKey |
string (≤160, no whitespace) | matches |
deviceId |
string (≤96) | (not in the sketch) — needed for cross-device conflict |
surfaceContext |
{ surface, sessionId?, tenantId? } strict |
surface ∈ tara | psyche | living-scene | nisaba | metis | shell |
anchorRef |
{ surface, anchorId, cursor } strict |
cursor is the locator (seconds into audio, fold index in a scene, page number) |
posture |
enum | reading | listening | co-watching | practicing | studying | reviewing | paused (richer than the sketch's free-text "paused at minute 4") |
sensitiveCategories |
string[] (≤16) | drives the consent guard |
crossDeviceConsentId |
UUID | null | required when sensitiveCategories is non-empty (enforced by superRefine) |
idempotencyKey |
string | idempotent write path through the BFF |
lastUpdatedAt |
timestamp | matches |
A cross-field superRefine enforces two invariants: anchorRef.surface must
equal surfaceContext.surface, and a non-empty sensitiveCategories requires a
crossDeviceConsentId. This is the contract realization of the spec's
"sensitive-category interactions emit a token only with cross-device consent"
rule.
Write, read, conflict, reconnect#
Surfaces emit token updates at natural checkpoints (segment boundary, paragraph,
lesson step, pause) through the BFF with idempotency-key semantics. On launch or
hand-off, the shell asks Iris for the most recent N tokens and renders
continuation cards. When two devices write within a tight window, the most
recent write wins by server timestamp, and the loser surfaces a "this was also
playing on your other device" notice instead of silently overwriting. On
reconnect, the token and the durable anchorRef resume mid-segment for any
surface that supports it (Tara audio, Psyche voice, Living Scenes). Tokens carry
no content — only references and posture.
Operator Inspection Regime#
Every operator read of consumer memory is gated by a strict state machine,
InspectionStateMachine in
libs/oshun/memory-iris/src/admin-inspection/state-machine.ts. There are
exactly three legitimate entry paths and two terminal states.
States and transitions#
InspectionState =
requested | dsar-fulfillment | incident-escalation | routine-review | granted | denied | closed.
The transition map (TRANSITIONS, lines 43–51) is an exact match to
features.md–2153:
requested → dsar-fulfillment | incident-escalation | routine-review
dsar-fulfillment → granted | denied
incident-escalation → granted | denied
routine-review → granted | denied
granted → closed
denied → (terminal)
closed → (terminal)
transition() throws on any illegal transition. No back-transitions exist.
The policy gate#
At the transition to granted, defaultPolicyGate() checks role,
scope-of-request, the target's tenant, target sensitivity, current consent
state, and (for incidents) a signed incident reference. Concretely:
- Cross-tenant scopes are denied (
cross-tenant); a profile scope whose id ≠ the target user isscope-out-of-bounds. incident-escalationwithout asignedIncidentRefisno-legitimate-interest.- For sensitive targets: a missing
currentConsentSnapshotIdisconsent-not-current;routine-reviewof sensitive memory is refused;supportandadminroles are refused sensitive access entirely;legalmay read sensitive memory only indsar-fulfillment;t&smay read sensitive memory only inincident-escalationwith a signed incident ref. These encode the spec's authorization model: admin-copilots can never read consumer sensitive memory, the privacy/legal path is DSAR-only, and the safety path is incident-only.
User notice, audit, and replay#
When a granted → closed transition closes a sensitive read with no
investigation carveout, a user notice is queued with a default 72-hour
(72 * 60 * 60 * 1000 ms) compliance window (pendingUserNotices()). Every
transition appends a canonical audit event to @oshun/audit-platform
(action: 'iris.admin_inspection.transition',
policyId: 'iris.admin_inspection.state_machine.v1') carrying actor, role,
scope, target user/tenant, reason code, and — on granted — the exact set of
MemoryEntry ids the operator saw. replayGrantedSnapshot() reconstructs
precisely that snapshot, so any inspection is reproducible as the operator saw
it (with non-DSAR multi-actor redaction applied at snapshot time). Tenant
operators see only their own tenant's memory; there is no cross-tenant access.
The Assistant Shell Bridge#
Iris does not talk to the conversation runtime directly; the
@oshun/shell-assistant package mediates. The shell handles intent
classification and action routing across Tara,
Veritas, Nyx,
Arete, Nisaba, and
Metis, and bridges to Iris through iris-memory-bridge.ts
and to the realtime runtime through psyche-session-bridge.ts.
IrisMemoryBridge (with createIrisMemoryBridge() /
createAdminIrisMemoryBridge()) maintains a redaction-ready turn log that feeds
export and delete, exposes recallRelevantMemory() as the assistant's recall
entry point, and emits IrisMemoryBridgeEvents (including recall.completed)
so exporters, audit shippers, and UI surfaces can observe what was recalled. The
IrisMemoryBridgeShellMode is 'customer' | 'admin', keeping the consumer and
operator recall paths distinct at the bridge layer.
Evaluation, Safety, and Tests#
Iris ships an evaluation regime that gates release:
- Memory-leakage suite — sensitive facts in non-relevant contexts, cross-scope, cross-tenant, and cross-user leakage prompts.
- Forget-completeness suite — deletion verified across storage, summaries,
downstream uses, retrievals, and audit references (the DSAR
resultFingerprintis the durable proof). - Conflict-resolution evals — conflict prompts with the expected resolution path (one per rule in the ladder above).
- Decay-accuracy evals — synthetic timelines verifying the usage-weighted decay (30-day half-life), not FSRS.
- Crisis-suppression evals — verify no normal recall during a crisis frame.
- Determinism — same context + same memory state ⇒ same ranked output.
- Cross-tenant leakage — every cross-tenant query rejected at the tenant-boundary gate.
- Sensitive-category recall gating — the per-category surface allow-list enforced in every surface (assistant, shell, notebooks, ritual resume, briefing).
- Inference threshold / promotion — synthetic timelines verifying the N-occurrence threshold (3) and the user-confirmation promotion.
- Multi-actor — "forget all mentions of" removes every tagged entry and the actor handle.
- Continuation token — reconnect mid-segment resumes correctly across Tara / Psyche / Living Scenes / Nisaba / Metis.
- Admin inspection state machine — every illegitimate transition rejected; granted reads stream to audit; user notice fires on close for sensitive reads.
- Drift detection — memory-recall accuracy, false-recall rate, sensitive-category leakage rate.
Related#
- Assistant Experience — the primary consumer of
Iris recall, via the
@oshun/shell-assistantbridge. - Privacy, Consent, Data Portability, and User Controls — the consent ledger, DSAR, and customer memory controls in product context.
- Review, Compliance, and Trust & Safety — the operator side of the inspection regime.
- Psyche Real-Time Runtime — produces the turns Iris remembers; consumes continuation tokens.
- Lilith Persona Policy — owns the crisis frame that triggers Iris crisis-suppression.
- Customer Curation, Notebooks, Collections, and Sharing — the notebook scope's product home.
- Tenant, Institution, and Operator Toolkit — tenant-scoped learner memory and operator copilots.
- Backlog:
§10inV1/TODOS.md. Dependency registry:V1/DEPENDENCIES.md. Feature hub: ../features.md.