# Iris Memory and Identity

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](./assistant-experience.md), [Tara](./domain-tara.md),
[Arete](./domain-arete.md), [Veritas](./domain-veritas.md),
[Nyx](./domain-nyx.md), [Nisaba](./domain-nisaba.md), and
[Metis](./domain-metis.md) — plus the governed admin path described in
[Review, Compliance, and Trust & Safety](./review-trust-safety.md) and
[Privacy, Consent, Data Portability, and User Controls](./privacy-consent-portability.md).
It sits between the [Psyche Real-Time Runtime](./psyche-realtime-runtime.md),
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`](../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
> `MemoryEntry` contract, the deterministic `RecallPipeline`, 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, the `body` field, 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 carries `scene`, `pose`,
> and `crisis`. 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. `pose` requires `data_processing`, `data_storage`, **and**
  `sensitive_data`).
- `optOutCategories` — the opt-out categories that suppress the scope.
- `canonicalTiers` — which memory tiers it spans (`core` / `working` /
  `archival` / `episodic` / `semantic`).
- `adminReviewable` and `requiresGovernanceReview` — 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=14` with 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**:

```ts
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 `ProvenanceLink`s (`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):**
>
> - **`body` is plain text.** features.md–1976 described `body` as a
>   discriminated union of
>   `Fact | Preference | Goal | Boundary | Relationship | Schedule | LineageDeclaration | Sensitivity`.
>   In the real schema `body` is `z.string().min(1).max(4000)` (entry.ts:170).
>   The taxonomy that _is_ modeled is the separate `category` field.
> - **`origin` enum.** features.md listed `promoted-from-session`. The real
>   `MemoryOriginSchema` (entry.ts:81–88) is
>   `user-stated | user-confirmed | model-inferred | operator-copilot | summarized-from-session | imported`.
>   The doc's `promoted-from-session` is spelled `summarized-from-session`, and
>   the doc omitted the real `operator-copilot` origin.
> - **`lifecycle` enum.** features.md listed `summarized`. The real
>   `MemoryLifecycleSchema` (entry.ts:91–97) is
>   `draft | active | paused | superseded | tombstoned` — there is **no**
>   `summarized` lifecycle, and the doc omitted `draft`.
> - **`suppression` is a single nullable object.** features.md–1988 described
>   `suppression[]` as an array of four reasons
>   (`crisis-frame / user-mute / tenant-policy-mute / category-revoked`). The
>   real `MemorySuppressionSchema` (entry.ts:124–129) is a single nullable
>   object `{ reason, startedAt, endsAt, notes }` whose reason enum is
>   `crisis-frame | user-pause | sensitive-category | tenant-quarantine` (note
>   the renamings).
> - **`multiActor` is a single nullable object.** features.md–1991 described
>   `multiActor[]` as an array; the real `MultiActorSchema` (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 a `ConsentRecord` with
> "prior state, new state, reason code." That shape is stale. The entry-level
> primitive is `ConsentEntry` (above), and the _durable, tamper-evident_ consent
> history is the separate append-only `IrisConsentLedger`/`IrisConsentEvent`
> described 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):

1. **Tenant boundary** (`matchesTenantBoundary`). An entry's tenant id
   (`entry.tenantId`, or `scope.tenantId` for tenant scopes) must equal the
   request's tenant; mismatches are dropped and counted under `tenant-mismatch`.
   Enforced _before any scoring_.
2. **Scope reachability** (`isReachableScope`). Only entries reachable from the
   request scope survive. Reachability is hierarchical: `session` recall reaches
   `session + profile`; `scene` reaches `scene + session + profile`; `pose`
   reaches `pose + scene + session + profile`; `notebook` reaches
   `notebook + profile` (and, under tenant policy, same-tenant memory); a
   `tenant`-scoped request reaches only its own tenant+user rows. Drops are
   counted under `scope-mismatch`.
3. **Crisis gate.** If `request.crisisFrame === true`, the candidate set
   collapses to **safety-critical entries only** — an entry survives only when
   `request.safetyCriticalContext === true` _and_ `isSafetyCriticalEntry`
   (`category === 'safety'`). Everything else is dropped and counted under
   `crisis-frame`. This matches the documented behavior (features.md–2010):
   during a Lilith crisis frame, no normal recall.
4. **Suppression + lifecycle gate.** Entries with an active `suppression` (its
   `endsAt` is null or in the future) are dropped and counted under their
   `reason`; entries whose `lifecycle !== 'active'` are dropped under
   `lifecycle-<state>`.
5. **Sensitive-category gate.** Sensitive entries (sensitive category _or_
   `multiActor.sensitiveInteraction`) pass only when (a) the category is in the
   request's `consentedCategories` **and** (b) the surfacing context is in the
   per-category allow-list (`hasRelevantSensitiveContext`). The per-category
   context map is concrete: health/biometric/pose categories require
   `health`/`safety`/`crisis` contexts; `sexuality`/`gender_identity` require
   `identity`/`safety`/`crisis`; spiritual requires `spiritual`; financial
   requires `financial`/`safety`/`crisis`; legal/immigration require
   `legal`/`safety`/`crisis`; abuse/relationship-violence require
   `relationship`/`safety`/`crisis`. Drops are counted as either
   `sensitive-category-without-consent` or
   `sensitive-category-outside-relevant-context`. This is exactly the "health
   memory surfaces in Tara recovery but never in a Veritas briefing" guarantee
   from the spec.
6. **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`.
   `referenceCount` is normalized as `min(1, referenceCount/10)`; `origin`
   weight ranks
   `user-stated (1) > user-confirmed (0.9) > summarized-from-session (0.75) > imported (0.7) > model-inferred (0.5) > operator-copilot (0.4)`.
7. **Conflict / freshness dedupe.** `dedupeByConflict` collapses candidates that
   share a conflict key (default key = `category :: actorHandle :: subject`),
   keeping the row with the more recent `updatedAt` — most-recent-wins within a
   conflict cluster.
8. **Per-surface budget** then **surface**. Results are capped by
   `DEFAULT_BUDGETS`, then returned with rationale and provenance. On the
   `admin` surface (outside a DSAR workflow), `multiActor` is 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, `semantic` is
> **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 injectable `semantic` function in
> `RecallPipelineOptions`, 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)":

```ts
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 `MemoryEntry` only
  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 at
  `origin = model-inferred` (or `summarized-from-session` for compaction-derived
  facts).
- **Promotion.** A confirmed inference promotes toward `user-confirmed`;
  uncontested inferences decay faster because their `origin` weight (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:

1. `admin-copilot-never-trumps-consumer-stated` — an `admin_copilot_inferred`
   write can never overwrite an `explicit_user` or `user_confirmed` fact; the
   existing entry is kept.
2. `explicit-user-trumps-inferred` — an explicit user statement beats any
   inferred source.
3. `user-confirmed-trumps-inferred` — a user-confirmed fact beats inferred
   sources, and a prior user-confirmed fact is _kept_ against a new inferred
   write (`forcedOutcomeForPolicy` returns `keep_existing`).
4. `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_CATEGORIES` set above;
  `privacy-suppression.ts` carries the parallel
  `IRIS_PRIVACY_SENSITIVE_CATEGORIES`, `IRIS_PRIVACY_RECALL_CONTEXTS`, and an
  `IRIS_PRIVACY_SENSITIVE_CATEGORY_POLICIES` table 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()` and `redactIrisPiiFromText()`
  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`) supports `mode: 'soft' | 'hard'`, a
  list of scopes/categories, a `purgeConsentLedger` flag (which also purges
  consent events for the deleted categories), and a `retentionGraceMs` window
  within which a soft delete can be re-activated. Validation enforces that a
  `hard` delete has `retentionGraceMs === 0` and that at least one scope or
  category is named.
- **Export** (`IrisDataRightsExportPayload`) names scopes, a `format`, include
  flags (`includeConsents`, `includeOptOuts`, `includeNotebooks`,
  `includeSessionMemory`, plus opt-in `includeSceneMemory` /
  `includePoseMemory`), a `deliveryChannel`, and an `encrypted` flag.
  `buildIrisDataRightsMemoryExportBundle()` assembles the scene/pose payloads,
  marking pose memory `biometricAggregateOnly`.
- **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 `MemoryEntry` rows 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 `§10` of
> [`V1/TODOS.md`](../TODOS.md) and
> [Privacy, Consent, Data Portability, and User Controls](./privacy-consent-portability.md).

---

## 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 `actorHandle` bound
  to the user's namespace, plus a relationship-only note ("my partner," "my
  professor"). The handle is hashed via `@noble/hashes/sha2` (chosen over
  `node:crypto` so the module bundles for the browser — the `/profile/memory`
  surface 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 `admin` recall surface and in non-DSAR inspection snapshots,
  `multiActor` is redacted to `null` (`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 `sensitiveInteraction` is true, the entry
  inherits the sensitive-category consent and recall gating (the recall pipeline
  treats it as sensitive and requires a `multi-actor-sensitive` consent plus a
  `relationship`/`safety`/`crisis` context).

---

## 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 is `scope-out-of-bounds`.
- `incident-escalation` without a `signedIncidentRef` is
  `no-legitimate-interest`.
- For **sensitive** targets: a missing `currentConsentSnapshotId` is
  `consent-not-current`; `routine-review` of sensitive memory is refused;
  `support` and `admin` roles are refused sensitive access entirely; `legal` may
  read sensitive memory only in `dsar-fulfillment`; `t&s` may read sensitive
  memory only in `incident-escalation` with 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](./domain-tara.md),
[Veritas](./domain-veritas.md), [Nyx](./domain-nyx.md),
[Arete](./domain-arete.md), [Nisaba](./domain-nisaba.md), and
[Metis](./domain-metis.md), 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 `IrisMemoryBridgeEvent`s (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
  `resultFingerprint` is 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](./assistant-experience.md) — the primary consumer of
  Iris recall, via the `@oshun/shell-assistant` bridge.
- [Privacy, Consent, Data Portability, and User Controls](./privacy-consent-portability.md)
  — the consent ledger, DSAR, and customer memory controls in product context.
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md) — the
  operator side of the inspection regime.
- [Psyche Real-Time Runtime](./psyche-realtime-runtime.md) — produces the turns
  Iris remembers; consumes continuation tokens.
- [Lilith Persona Policy](./lilith-persona-policy.md) — owns the crisis frame
  that triggers Iris crisis-suppression.
- [Customer Curation, Notebooks, Collections, and Sharing](./customer-curation-notebooks.md)
  — the notebook scope's product home.
- [Tenant, Institution, and Operator Toolkit](./tenant-toolkit.md) —
  tenant-scoped learner memory and operator copilots.
- Backlog: `§10` in [`V1/TODOS.md`](../TODOS.md). Dependency registry:
  [`V1/DEPENDENCIES.md`](../DEPENDENCIES.md). Feature hub:
  [../features.md](../features.md).
