# Journey: Assistant handoff and context carry

User invokes the global assistant dock from any domain surface; the panel opens
with a typed context handoff (active route, current artifact, evidence state,
persona identity, memory scope); the user switches persona across a domain
boundary (e.g., Tara teacher → Veritas explainer); Iris recalls permitted prior
context; and the transcript labels it as `Remembered context`, with explicit
profile/notebook lines when those scopes apply. Catches broken handoff payloads,
persona switches that drop memory, and disclosure strips that de-sync from the
active grounding state without promising obsolete continuity copy.

## Personas

- **Signed-in customer, mid-Tara-sit** — primary; invokes from `/tara/sit/[id]`
  then navigates to Veritas with the assistant attached
- **Signed-in customer, in Veritas** — opens from `/veritas` or `/veritas/claim`
  and asks a contemplative question that routes to a Tara persona handoff
- **Memory-off customer** — chose `memoryScope='off'` in onboarding (see
  [`onboarding-ten-steps.md`](./onboarding-ten-steps.md)); disclosure strip must
  reflect "memory off" on every reply; recall must not happen
- **Anonymous user** — no assistant dock; invocation points hidden / inert

## Pre-conditions

- Signed-in session — see
  [`shell/04-auth-session.md`](../shell/04-auth-session.md)
- `memoryScope` is one of `off | session | profile`
- Assistant BFF routes healthy: `POST /v1/assistant/sessions`,
  `POST /v1/assistant/sessions/:sessionId/message`,
  `GET /v1/assistant/sessions`, `DELETE /v1/assistant/sessions/:sessionId`
- Viewport ≥ `ASSISTANT_DOCK_MIN_WIDTH` (1360 px) for the dock; below that, the
  panel uses overlay presentation

## Steps

### 1. Open the source surface and dock

- [ ] User on `/tara/sit/[id]` (or any Veritas / Nisaba / Metis surface)
- [ ] Collapsed `AssistantDock` visible top-right
      (`ASSISTANT_DOCK_COLLAPSED_WIDTH = 128`) when viewport ≥ 1360 px
- [ ] **Verify**: [`customer/03-tara/tara.md`](../customer/03-tara/tara.md)
      surface mounted

### 2. Invoke the assistant

- [ ] Click `data-shell-assistant-dock-expand` (or global shortcut)
- [ ] Dock transitions `collapsed` → `expanded`
      (`ASSISTANT_DOCK_EXPANDED_WIDTH = 360`), 200 ms unless reduced-motion
- [ ] `AssistantPanel` mounts with `presentation='dock'`
- [ ] Shell builds `AssistantContextHandoff` from
      `libs/oshun/shell-assistant/src/context-handoff.ts` carrying
      `activeRoute`, `activeDomain`, `artifact`, `personaIdentity`,
      `evidenceState`, `memoryScope`, `disclosureState`, `permittedToolGrants`
- [ ] Handoff runs through the sanitiser: caps selection / seed length, strips
      email / US phone / SSN patterns, rejects entities whose `platformShell`
      mismatches the launch intent's shell

### 3. Context handoff card + disclosure strip

- [ ] `AssistantPanel` paints `data-assistant-context-handoff` chips
      `Artifact: <label>`, `Evidence: <status>`, `Memory: <scope>`,
      `Persona: <displayName>`, `Tools: <count>`
- [ ] `ShellPersistentContextStrip` mirrors that set with secondary "Back to
      <surface>" and primary "Return to <surface>" (or thread label)
- [ ] `AssistantDisclosureStrip` renders memory / persona / grounding / voice /
      avatar indicators

### 4. Session creation + Iris recall

- [ ] First message creates a session via `POST /v1/assistant/sessions` with
      `userPreferences.memoryScope`, `memoryConsentGranted`,
      `enabledMemoryCategories`, `suppressionRules` attached
- [ ] If `memoryScope !== 'off'`, Iris recall hydrates profile (durable),
      session (rolling 30 d raw / 90 d summarized), and notebook-linked memory
      bound to the active artifact per V1/features.md § Iris Memory and Identity
- [ ] If `off`, no recall; disclosure strip reflects "Memory: off" on every
      reply

### 5. Reply with grounding + disclosure

- [x] When recall is available, the reply uses the current BFF contract:
      `Remembered context:` followed by permitted profile/notebook facts; it
      does not depend on the older aspirational "I noticed you were just
      sitting…" sentence
- [ ] Reply carries `AssistantTurnGrounding` (`level`, `supportCount`,
      `contradictionCount`, optional `primarySource` + `citations`); "Inspect
      evidence" opens `VeritasInspectionOverlay` with a model from
      `buildAssistantTurnInspectionModel`
- [ ] Disclosure indicators stay in sync with active grounding / memory /
      persona / voice state per turn

### 6. Cross-domain persona switch

User on `/tara/sit/[id]` with persona `tara-guide` (`teacher` role) asks a
fact-grounded question; panel offers a Veritas-leaning explainer handoff.

- [ ] `personaHandoffState` (from `buildAssistantPersonaSwitchingModel` +
      `buildAssistantPersonaHandoffState`) surfaces options with
      `status: 'active' | 'recommended' | 'available' | 'restricted'`
- [ ] User picks a recommended persona (e.g., `source-lineage-guide` or
      `stoic-scholar`); panel paints new `activePersona.id` / `role` /
      `memoryBoundary.kind`
- [ ] `AssistantPersonaHandoffAuditEvent` appended (visible via
      `data-assistant-persona-handoff-audit-count`)
- [ ] Memory boundary honoured: profile-scope memory carries across;
      sensitive-category memory not licensed for the new persona is suppressed
      per V1/features.md § Privacy-Aware Suppression

### 7. Navigate the domain boundary

- [ ] User navigates from `/tara/sit/[id]` to `/veritas` (or `/veritas/claim`)
      without dismissing the assistant
- [ ] Dock stays attached; new `AssistantContextHandoff` is computed; prior
      handoff recorded in persistent context for "return to" affordances
- [ ] Context handoff card refreshes with new `Artifact` / `Evidence` / possibly
      a different `Persona` recommendation
- [ ] Disclosure strip re-renders for the new domain's grounding state
- [ ] **Verify**:
      [`customer/05-veritas/veritas.md`](../customer/05-veritas/veritas.md)

### 8. Continuity disclosure + dismiss

- [ ] The first reply on the new surface may include prior context only when the
      memory scope permits it; recalled facts remain visibly prefixed as
      `Remembered context:`
- [ ] If memory is off, the assistant still gets the current-screen handoff but
      cannot reference the prior Tara sit
- [ ] Click `data-shell-assistant-dock-dismiss` (X) → dock returns to
      `collapsed`; thread state persists in sessionStorage under
      `ASSISTANT_THREAD_STORAGE_KEY` (`oshun-web-assistant-thread`)
- [ ] `updateOshunPersistentShellContext` updates so re-invocation can offer
      "Return to <thread>" / "Back to <surface>"

## Post-conditions

- Live session exists with turn count recorded
- Session's `userPreferences.memoryScope` matches the user's profile
- Transcript + active context handoff persisted in sessionStorage
- Disclosure indicators match actual subsystem states each turn
- `PersonaHandoffAuditEvent` recorded for any persona switch

## Failure modes

- [ ] **Sanitiser drops the artifact** — artifact's `platformShell` mismatched
      launch intent; panel receives `null` artifact, shows "no current artifact"
      copy instead of an empty chip
- [ ] **Sanitiser drops a mismatched entity** — a wrong-shell entity is omitted
      before session creation; the handoff carries `entity: null`, preserves the
      current artifact, and shows "Entity: removed (shell mismatch)" without
      exposing the dropped entity label
- [ ] **`POST /v1/assistant/sessions` returns 403 (no domain access)** — panel
      falls back to `AssistantSafeFallbackState` from
      `buildAssistantSafeFallbackState`; user sees "Assistant unavailable on
      this surface"
- [ ] **Memory scope mismatch** — `contextHandoff.memoryScope` disagrees with
      `userPreferences.memoryScope`; panel uses
      `effectiveMemoryScope = contextHandoff?.memoryScope ?? memoryScope`
      (most-restrictive of the two)
- [ ] **Restricted persona switch** — option `state === 'restricted'`; UI
      hard-blocks; audit records the attempt with `outcome: 'fallback'`; panel
      reverts to previous active persona
- [ ] **Cross-tenant entity leak** — sanitiser rejects with
      `403 forbidden_cross_tenant_scope`; INC fires in operator admin
- [ ] **Viewport shrinks below 1360 px mid-session** — dock transitions to
      overlay; thread state persists; persistent-context strip changes layout
- [ ] **Crisis-frame suppression activates** — Lilith intercepts (see
      [`crisis-aware-tone-policy.md`](./crisis-aware-tone-policy.md)); no new
      memory written, existing memory suppressed from recall except
      safety-critical; crisis surface mounts in-place

## E2E coverage

- Backed by
  [`apps/oshun/web/e2e/assistant-continuity-journey.spec.ts`](../../apps/oshun/web/e2e/assistant-continuity-journey.spec.ts),
  [`apps/oshun/web/e2e/shell-utility-dock.spec.ts`](../../apps/oshun/web/e2e/shell-utility-dock.spec.ts),
  [`apps/oshun/web/e2e/shell-boundary.spec.ts`](../../apps/oshun/web/e2e/shell-boundary.spec.ts),
  and
  [`apps/oshun/web/e2e/assistant-handoff-deepening.spec.ts`](../../apps/oshun/web/e2e/assistant-handoff-deepening.spec.ts)
  — the continuity spec drives assistant open from library and explore CTAs and
  from a domain surface, asserting persona switching, voice-identity strip,
  disclosure strip, memory-scope chip, and the `return-thread` / `return-anchor`
  persistent-context links; the utility-dock spec confirms the dock persists
  across nav and surfaces correctly at desktop and mobile viewports; the
  boundary spec asserts the admin-shell context handoff carries
  `memory-scope=off` and the `studio-operator-copilot` persona with
  safe-fallback state attributes; the handoff-deepening spec opens the assistant
  from `/library`, expands the persona disclosure, switches to the stoic-scholar
  persona, and asserts the active-persona label updates to "Stoic Research
  Guide" and that the handoff-state div carries
  `data-assistant-persona-handoff-audit-count="1"` with an audit-event row whose
  `data-assistant-persona-handoff-audit-outcome="applied"`. The
  handoff-deepening spec also drives the **restricted-persona failure mode**: an
  admin-only persona (`allowedShells:['admin']`) renders
  `data-assistant-persona-state="restricted"` with
  `data-assistant-persona-disabled="true"` and `disabled` in the customer shell,
  and force-activating it is a no-op (active-persona label unchanged, no new
  handoff audit row). It also opens the assistant from `/profile`, `/library`,
  and `/domains/tara?origin=home` to prove customer `permittedToolGrants` are
  scoped by active domain: profile exposes only `navigation.open`, library adds
  `library.save`, and Tara swaps that for `domain.action.execute`. The
  continuity spec also drives the **customer memory-off branch** over real BFF
  preferences: it patches `/v1/preferences` to `assistant.memory.scope='off'`,
  waits for the browser preference store to hydrate that remote value, opens the
  customer assistant from `/library`, and asserts the real
  `/v1/assistant/sessions` create payload, BFF continuity response, context
  handoff chip, disclosure indicator, transcript surface, and first turn all
  carry `memoryScope='off'`. It also selects real browser text containing email
  / phone / SSN-shaped content before opening the assistant from `/explore`,
  then asserts the live session-create payload's `contextHandoff.selection`
  contains `[redacted-email]`, `[redacted-phone]`, and `[redacted-ssn]` without
  the raw PII. It also opens the browser-visible `/explore` null-artifact launch
  path and asserts the handoff carries `data-assistant-context-artifact="none"`
  plus the exact "Artifact: no current artifact" chip copy. It now also opens
  `/search` with a fixed wrong-shell entity preview, asserts the live
  session-create payload carries `entity: null` plus
  `artifact.metadata.entitySanitization='entity-shell-mismatch'`, and verifies
  the browser panel exposes `data-assistant-context-entity="none"`,
  `data-assistant-context-entity-state="shell-mismatch"`, and the exact "Entity:
  removed (shell mismatch)" chip without exposing the dropped admin entity
  label. It also covers the **cross-domain navigation-depth branch** by starting
  on `/domains/tara?origin=explore`, clicking the browser-visible Veritas domain
  shortcut, then opening the assistant from the Veritas context strip and
  asserting the live session-create payload, assistant persistent-context strip,
  continuity rail copy, and return-thread / return-anchor links preserve
  `origin=explore&stack=tara`. It now also covers the browser-visible
  **profile + notebook Iris recall continuity variant**: the test grants real
  Iris consents, enables profile memory through `/v1/preferences`, writes
  profile and notebook memories through `/v1/iris/adapter/remember`, opens the
  assistant from a live Nisaba notebook route, verifies the session-create
  handoff carries `artifact.kind='domain.notebook'` and
  `artifact.metadata.notebookId`, sends a live assistant message, then asserts
  both the real `response.memoryRecall` envelope and the mounted transcript
  render "Remembered context:", "Profile memory:", "Notebook memory:", and the
  seeded Tara / Dhammapada recall copy with `Memory: profile` visible. It now
  also drives a live Veritas assistant turn through the real
  `/v1/assistant/sessions/:sessionId/message` route, asserts the BFF returns a
  grounded `response.grounding` projection sourced from Veritas article cards,
  then clicks the browser-visible `Inspect evidence` action and verifies the
  shared Veritas inspection overlay names the International Energy Agency
  source, support count, evidence citations, and source-directory action. The
  `shell-utility-dock` spec now also proves the dock/thread storage boundary in
  the real customer shell: expanding the desktop dock writes
  `oshun-web-assistant-dock-mode='expanded'`, collapsing writes `collapsed`,
  same-tab reload restores that collapsed mode and the typed transcript from
  `sessionStorage['oshun-web-assistant-thread']`, a second tab inherits only the
  localStorage dock mode while seeding an independent sessionStorage-backed
  assistant session, and the original tab's prompt is absent from that second
  tab's transcript.
- Backed by
  [`apps/oshun/web/e2e/assistant-session-bff.spec.ts`](../../apps/oshun/web/e2e/assistant-session-bff.spec.ts)
  — **step 4's post-condition over REAL HTTP**: create a real session
  (`POST /v1/assistant/sessions` → sessionId + authorizedDomains + continuity
  disclosure), send two messages and assert the engine's `turnCount` grows 1 →
  2, and that `GET /v1/assistant/sessions` mirrors the recorded turn count (the
  "live session exists with the turn count recorded" post-condition the client
  specs could not assert). It also proves the route-level **Iris bridge
  bootstrap + recall fold** over the shared real BFF store: real
  `/v1/preferences` enables assistant profile memory, real
  `/v1/iris/adapter/consents` grants the Iris side, prior `/remember` profile
  and notebook facts remain visible in `/review`, `POST /v1/assistant/sessions`
  returns `memory.scope='profile'`, the first assistant message returns a
  `response.memoryRecall` envelope containing both scoped facts, the answer text
  folds the remembered Tara reset and Nisaba notebook cues into the reply, and
  the first assistant turn is durably recorded back into the same
  `/v1/iris/adapter/review` feed under the live assistant session id. It now
  also proves the **persona recommendation BFF endpoint**:
  `GET /v1/assistant/persona-handoffs/recommendations` returns the canonical
  customer candidate set for `/library`, blocks a requested admin-only persona
  with a fallback handoff, enforces customer domain scope (`domain:tara` cannot
  request Veritas recommendations), and serves admin-shell candidates only to an
  admin-scoped bearer while preserving the admin session-only memory boundary.
  It also proves the **mismatched-shell artifact failure mode at the BFF
  boundary**: a valid customer Veritas handoff changes live session continuity
  (`grounding.mode='required'`, `sourceDomain='veritas'`,
  `recentDomains=['veritas']`), while the same customer launch carrying an
  `admin.review-package` artifact is sanitized out before continuity merge and
  falls back to the default assistant grounding context. Plus 401 (no bearer),
  400 (empty text), 404 (unknown session), 403 (a session does not belong to
  another user), and DELETE → ended (no longer listed). It also drives the
  **crisis-frame suppression branch**:
  `POST /v1/admin/safety/crisis-frame/activate` publishes a real operator frame
  for the member, `GET /v1/safety/crisis-frames/mine` confirms the assistant
  projection, and a benign assistant message returns
  `crisis.source='operator-frame'`, `shouldSpeak=false`, no cards/actions, and
  `turnCount=0` instead of normal synthesis.
- Backed by
  [`apps/oshun/web/e2e/assistant-not-entitled-fallback.spec.ts`](../../apps/oshun/web/e2e/assistant-not-entitled-fallback.spec.ts)
  — the **downstream not-entitled degradation branch**: a 403 (entitlement /
  authz denial) must degrade HONESTLY, never fabricate. Session-create 403 (real
  "No domain access" gate, driven by re-issuing to the BFF with a `profile:read`
  bearer) flips the `downstream` safe-fallback subsystem to `fallback` with a
  not-entitled reason (distinct from the generic outage copy); a message 403
  (real ownership gate) surfaces an honest "isn't available on your current
  access" notice instead of a fabricated `generateSimulatedResponse` answer.
  This corrects the earlier "403 → safe-fallback is NOT achievable" note — the
  `downstream` subsystem is session-error driven.
- **Coverage depth**: deep — see [`coverage.md`](./coverage.md).
- **Coverage closure notes**:
  - Step 4's profile-scope and notebook-linked Iris recall fold is now asserted
    at both the real BFF layer and the browser layer: the mounted assistant
    transcript now renders the exact profile + notebook "Remembered context"
    continuity copy after a live Nisaba notebook handoff.
  - Mismatched-shell artifact rejection is now real-HTTP covered at the BFF
    boundary, and the browser-visible null-artifact copy is covered from
    `/explore`; the browser-visible entity mismatch variant is covered from
    `/search` with a sanitized `entity: null` payload and safe mismatch chip;
    cross-domain navigation depth is covered by the Tara → Veritas browser route
    with assistant return links preserving `origin=explore&stack=tara`. The
    downstream `POST /v1/assistant/sessions` 403 safe-fallback branch is covered
    by `assistant-not-entitled-fallback`; operator crisis-frame suppression is
    covered by `assistant-session-bff`.
  - Persistent thread storage scope and dock mode lifecycle are now
    browser-covered: sessionStorage restores the same-tab transcript after
    reload, does not leak it into a second tab, and localStorage mirrors the
    dock `expanded` / `collapsed` / `closed` lifecycle.
  - Per-domain customer `permittedToolGrants` policy enforcement is now covered
    in `ShellLayout` unit tests and the real browser assistant-handoff spec.
  - Persona handoff candidate recommendations are now real-HTTP covered at the
    BFF boundary for customer, admin, fallback, and authorization-scope
    branches.

## Per-view files touched by this journey

Assistant is global; entry can be any signed-in surface. Examples:

- [`customer/03-tara/tara.md`](../customer/03-tara/tara.md) — Tara hub
- [`customer/03-tara/tara-sit-id.md`](../customer/03-tara/tara-sit-id.md) —
  mid-sit invocation
- [`customer/05-veritas/veritas.md`](../customer/05-veritas/veritas.md) —
  cross-boundary destination
- [`shell/04-auth-session.md`](../shell/04-auth-session.md) — session +
  authorization scoping
- [`shell/05-notifications.md`](../shell/05-notifications.md) — proactive
  follow-ups land here

## Cross-references

- Sibling journeys: [`onboarding-ten-steps.md`](./onboarding-ten-steps.md) sets
  `memoryScope` and the persona/tone preferences read here;
  [`tara-to-nisaba-handoff.md`](./tara-to-nisaba-handoff.md) covers an adjacent
  cross-domain bridge that does not use the dock; and
  [`crisis-aware-tone-policy.md`](./crisis-aware-tone-policy.md) covers Lilith
  interception inside the panel
- Feature spec: [`V1/features.md`](../../V1/features.md) § Assistant Experience;
  § Iris Memory and Identity (recall, scope hierarchy, suppression, conflict
  resolution)
- Component sources:
  `apps/oshun/web/src/components/assistant-dock/AssistantDock.tsx`;
  `apps/oshun/web/src/components/assistant/AssistantPanel.tsx`;
  `apps/oshun/web/src/components/ShellPersistentContextStrip.tsx`;
  `libs/oshun/shell-assistant/src/context-handoff.ts`;
  `libs/oshun/shell-assistant/src/persona-handoffs.ts`;
  `libs/oshun/shell-assistant/src/safe-fallbacks.ts`
- BFF routes: `apps/oshun/bff/src/routes/assistant.ts`;
  `apps/oshun/bff/src/routes/assistant-metis-handoff.ts`

## Open questions / known gaps

- [x] Exact profile + notebook recall/disclosure copy variant — browser now
      proves the mounted transcript renders the real `Remembered context:`
      profile/notebook copy after a Nisaba notebook handoff. The older
      aspirational "I noticed you were just sitting in <domain>…" copy is not
      the current BFF contract.
- [x] Iris recall pipeline endpoint — recall currently runs during the live
      message route through `IrisMemoryBridge.recallRelevantMemory`; there is no
      separate browser-visible recall endpoint in the current implementation.
- [x] Persistent thread storage scope — sessionStorage under
      `oshun-web-assistant-thread`; verify multi-tab behaviour
- [x] Dock mode lifecycle (closed/collapsed/expanded) mirror to `localStorage`
      under `ASSISTANT_DOCK_MODE_KEY`
- [x] `permittedToolGrants` per customer domain — `ShellLayout` now grants
      `navigation.open` everywhere, adds `library.save` only on save-capable
      domains, and adds `domain.action.execute` only on action-capable domains;
      unit and browser tests cover profile, library, and Tara.
- [x] Recommendation BFF endpoint for persona handoff candidates — implemented
      as `GET /v1/assistant/persona-handoffs/recommendations` and covered by the
      real-dev-infra `assistant-session-bff` Playwright API spec for customer,
      admin, fallback, and authz-scope branches.
