Context. surface customer · domain account · route /profile/memory · auth signed-in · source apps/oshun/web/src/app/profile/memory/page.tsx
Last walked. 2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data (2xx), console/page-errors, expected content, screenshot verified; live screen-reader, touch, offline, and telemetry-delivery checks pending a manual AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content re-verified 2026-06-03 against current source; real Iris empty + populated BFF hydration re-verified 2026-06-25 by memory-edit-pause-forget.spec.ts; persisted pause + assistant dock memory-off handoff re-verified 2026-06-27; anonymous redirect and offline cached/read write-block path re-verified 2026-06-27; transient BFF review failure + retry re-verified 2026-06-30; real Iris single-fact hard-delete through /v1/iris/adapter/memory/:memoryId plus review read-back absence re-verified 2026-06-30; cross-tab edit-conflict keep-remote and overwrite resolution re-verified 2026-06-30; synced Iris export receipt through /v1/iris/adapter/export re-verified 2026-06-30
Purpose#
OSHUN assistant memory controls — review, edit, export, pause, and delete what
the assistant remembers. Thin server component that mounts the
ProfileMemoryControls client composition.
Entry points#
/profilesub-section deep link (highlightPath = memory)- Profile settings panel "Memory" row →
/profile/memory - Privacy-related links from
/legal/privacy(assistant memory section) — verify - Direct URL / bookmark — yes (auth required)
- Onboarding "Memory" step (
/onboarding?step=memory) hand-off — verify
Layout regions#
page.tsx is <ProfileMemoryControls /> with no other chrome from the route.
The component composition is in
apps/oshun/web/src/app/profile/memory/ProfileMemoryControls.tsx — a
'use client' view that starts from the honest empty local state, adopts any
cached local control state after hydration, then replaces it with the member's
real Iris review from the BFF when /v1/iris/adapter/review returns records.
The pause state is also hydrated from /v1/iris/adapter/pause?role=settings and
profile pause/resume writes are persisted back through that BFF endpoint.
Regions:
- Header —
<h1>"What OSHUN remembers", the off-the-record pause switch, and Export JSON - Alert banners — off-the-record banner, full-clear 7-day grace banner, and transient Iris BFF read-unavailable retry banner (conditional)
- Status grid — State / Active facts / Audit events / Tombstones metrics
- Workspace — left: the paginated Memory facts table with category / scope filters and scope tabs; right: the per-fact inspector (detail, provenance, recent recalls, edit, scope-change, fact controls, step-up, hard delete, full clear)
- Controls grid — Category controls, Scope controls, Export bundle
- Audit chain + footer toast / reset
States#
- Initial hydration / loading — there is no skeleton; the route renders the deterministic empty state first, then shows the sync notice once real Iris records replace it
- Empty — no stored memories; ledger row
data-profile-memory-empty-statesays "No remembered facts yet." and the inspector showsdata-profile-memory-empty-inspector - Populated — list of real Iris or cached memory entries (scope, category, origin, content, source, timestamp)
- Paused — memory capture toggle off; existing entries still visible
- Editing — selected entry exposes label/content edit controls
- Exporting / export ready — synced Iris exports first request a
server-side export receipt from
/v1/iris/adapter/export?role=settings, then show a non-secret receipt plus the inline signed JSON preview and download link. Local-only cached exports show the same signed bundle with a local-only receipt note. - Deleting (single) — destructive confirmation + step-up phrase
- Deleting (all) — full-clear confirmation + step-up + 7-day grace banner
- Error (recoverable) — a failed Iris review/consent hydration keeps
cached/local memory rows visible, exposes
data-profile-memory-bff-statuswith retry copy, and the Retry Iris read action re-runs the real BFF hydration path - Anonymous user — middleware redirects direct
/profile/memoryvisits to/welcome?redirect=/profile/memory; the memory surface does not mount - Offline — cached memory entries remain readable; signed-in BFF-backed pause writes fail closed with "Off-the-record mode could not sync. Try again.", the switch remains off, and no pause banner is shown
Interactions#
All interactions live inside ProfileMemoryControls (a single 'use client'
view). Heading: <h1> "What OSHUN remembers". The real affordances (with their
data-profile-memory-* test ids):
Header#
- Off the record / Resume memory (
role="switch",data-profile-memory-pause) — toggles capture-pause; raises an "Off the record"role="alert"banner (data-profile-memory-pause-banner) with its own Resume button. For signed-in real Iris users this writesPATCH /v1/iris/adapter/pause?role=settings, exposesdata-profile-memory-pause-sync, and notifies the shell assistant dock so its memory disclosure switches tooff. - Export JSON (
data-profile-memory-export) — see Export panel below
Status grid#
- Status metrics (non-interactive) — State, Active facts, Audit events, Tombstones
Memory facts table#
- Category filter / Scope filter (
<select>s) — filter the fact rows by category / scope - Scope tabs (
role="tablist",data-profile-memory-scope-tabs) — "All scopes" + one tab per scope present, each with a live count - Fact rows (
data-profile-memory-fact-row) — paginated table (Fact / Category / Scope / Origin / Last used / Status / Actions); selecting a row drives the inspector; per-row Edit / Clear / Opt out icon buttons - Empty ledger (
data-profile-memory-empty-state) — rendered whenpagedFacts.length === 0; no fabricated member names or demo facts - Pagination (
data-profile-memory-pagination) — Previous / Next overFORGET_PAGE_SIZE(10) rows per page
Inspector (selected fact)#
- Empty inspector (
data-profile-memory-empty-inspector) — rendered when no selected fact exists - Detail — body, scope / category / origin / suppression / source / downstream-use metadata, and a "View audit chain →" jump link
- Provenance chain (
data-profile-memory-provenance-chain) — created + each revision + "Edited from" predecessor - Recent recalls (
data-profile-memory-recent-recalls) — last surfaces this fact was recalled on - Edit fact (
data-profile-memory-save-edit) — label + content inputs, optimistic-concurrency check; on conflict an edit-conflict panel (data-profile-memory-edit-conflict) offers Keep remote version / Save my edit as the next revision - Move to a different scope (
data-profile-memory-scope-change-panel) — target-scope<select>+ Move scope; rejects tenant→consumer promotion - Fact controls — Opt out this fact / Clear this fact. When the
selected row came from a synced Iris review, Clear this fact first
calls
DELETE /v1/iris/adapter/memory/:memoryId?role=settings&mode=soft; local cached rows still use the local control-state reducer. - Step-up authentication (
data-profile-memory-step-up-panel) — challenge-phrase input gating destructive flows; shows active/missing token status - Hard delete (
data-profile-memory-hard-delete) — irreversible delete, requires the hard-delete confirmation phrase + a valid step-up token. For synced Iris rows, the page first callsDELETE /v1/iris/adapter/memory/:memoryId?role=settings&mode=hard; the real BFF review read-back no longer returns that memory. - Forget everything (7-day grace)
(
data-profile-memory-full-clear-panel) — full-clear confirmation phrase + step-up; schedules a clear with a 7-day grace banner (data-profile-memory-grace-banner) carrying a Cancel clear action
Category / scope controls#
- Category controls (
data-profile-memory-category-controls) — per category: Opt out / Clear (clear requires step-up) - Scope controls (
data-profile-memory-scope-controls) — per scope: Opt out / Clear (clear requires step-up)
Export panel#
- Export bundle (
data-profile-memory-export-panel) — optional DSAR request ID input flags the bundle as DSAR fulfilment; Build export / Sign DSAR bundle produces a signed JSON (facts, consents, opt-outs, tombstones, downstream uses, audit chain) with a download link + preview. When the page is synced from real Iris, the same action first posts to/v1/iris/adapter/export?role=settingsand rendersdata-profile-memory-export-receiptwith the export id, memory count, and checksum. - Audit chain (
#audit-chain,data-profile-memory-audit-panel) — last memory mutations, appended and signed in the export bundle - Reset local memory controls (footer) — re-seeds the local state
Data & contracts#
- Reads: hybrid client cache + real BFF hydration.
ProfileMemoryControlsstarts withbuildInitialProfileMemoryState()from@oshun/memory-iris, adoptslocalStorage[PROFILE_MEMORY_STORAGE_KEY]after hydration, then callsfetchRealProfileMemoryStateResult(realUserId)onceuseOshunProfileStore()has a user id. That helper reads/v1/iris/adapter/review?role=settings&pageSize=100and/v1/iris/adapter/consents?role=settingsthrough the sharedapiclient. Non-empty BFF reviews replace the local state and show "Memory controls synced with your Iris record"; empty BFF reviews return{ status: 'empty' }so a member's cached local facts are not erased by an empty read. Unavailable BFF reads return{ status: 'unavailable' }, leave cached controls visible, and render the retry banner instead of silently falling back. - Pause read/write:
fetchRealProfileMemoryPauseState('settings')hydrates the persisted pause record even when review is empty. The signed-in pause toggle and banner Resume action callPATCH /v1/iris/adapter/pause?role=settings; the response is mirrored into local control state and dispatchesoshun:iris-memory-pause-changedfor the shell assistant dock. - Other writes: single-fact clear and hard-delete now call the real Iris
delete endpoint first when the page is synced from BFF review, then mirror the
existing local control-state mutation for the current view. Synced exports
call the real Iris export endpoint and render the returned receipt before the
local signed bundle is exposed. Edit / opt-out / scope change / category clear
/ scope clear / full-clear still return a new
IrisUserMemoryControlStatefrom a@oshun/memory-irispure function and persist tolocalStorage; those mutations do not yet POST back to Iris. - Realtime: same-shell pause changes update the assistant dock via the
pause-changed event; focus/visibility refreshes read the persisted BFF pause.
Other server-persisted memory edits do not stream in real time, but open
/profile/memorytabs listen for same-originlocalStoragechanges so a local edit in one tab can refresh another tab and trigger the optimistic edit-conflict panel instead of silently resetting a dirty draft. - Caching:
localStorage(PROFILE_MEMORY_STORAGE_KEY) for the control state; no SWR, no server cache. The offline route-level E2E keeps seeded cached rows visible whilenavigator.onLine === falseand asserts that the signed-in pause mutation is blocked rather than queued locally when the BFF cannot be reached. - Auth/role check: signed-in; middleware enforces
- Metadata:
title: 'Memory Controls',description: 'Review, edit, export, pause, and delete OSHUN assistant memory.',robots: { index: false, follow: false },alternates.canonical: '/profile/memory'
Cross-references#
- Parent:
profile.md - Sibling:
profile-safety.md - Onboarding memory step:
../01-onboarding/onboarding.md - Legal:
../00-public/legal-privacy.md - Component sources:
apps/oshun/web/src/app/profile/memory/ProfileMemoryControls.tsxapps/oshun/web/src/app/profile/memory/memory-state.ts
- E2E sources:
apps/oshun/web/e2e/memory-edit-pause-forget.spec.tsapps/oshun/web/e2e/profile-memory-controls.spec.ts
Open questions / known gaps#
- Single-fact clear / hard-delete are wired to the real Iris delete endpoint
when the page is synced from BFF review;
memory-edit-pause-forget.spec.tshard-deletes a remembered profile fact from/profile/memoryand proves/v1/iris/adapter/reviewno longer returns the deleted content. - Writes remain local-only for edit, opt-out, scope change, category/scope clear, and full-clear; pause/resume, single-fact delete, and synced export receipt creation are the controls currently wired to BFF Iris mutation endpoints.
- Failed BFF hydration keeps cached/local state, exposes a visible
retry/status banner, and retries through the real BFF hydration path in
memory-edit-pause-forget.spec.ts. - Confirm whether memory pause affects ongoing assistant sessions in real
time: same-shell pause/resume dispatches update the assistant dock to
memory scope
off, and focus/visibility refreshes re-read the persisted BFF pause endpoint. - Confirm the DSAR-backed export/delete path for this exact UI; synced
export now records an Iris export receipt, but the optional DSAR request
id is still only signed into the local JSON bundle and is not yet
persisted as an
IrisDataRightsRequest. - Anonymous redirect and offline cached/write-blocked behavior are covered
at route level in
memory-edit-pause-forget.spec.ts.