---
status: reconciled-partial
coverage_depth: deep-for-shipped-one-way-seams
last_walked: 2026-05-29
last_reconciled: 2026-07-17
specs:
  - apps/oshun/web/e2e/workspace-multi-panel.spec.ts
  - apps/oshun/web/e2e/multi-device-handoff-deepening.spec.ts
  - apps/oshun/web/e2e/multi-device-deepening.spec.ts
  - apps/oshun/web/e2e/profile-preference-sync.spec.ts
  - apps/oshun/web/e2e/iris-mobile-handoff-authority-bff.spec.ts
---

# Journey: Multi-device workspace handoff — one-way packet to native Home

The shipped cross-device leg carries one latest desktop workspace packet through
an authenticated per-user BFF mailbox to native Home. A separate browser-local
path supports capture and test-injected web resume. This is not a round trip:
desktop success copy is local-only, mobile consume is best-effort, and **Send
back to web** opens an assistant prompt instead of persisting the derived
`mobile_to_web` packet.

## Personas

- **Desktop researcher** — packages the active workspace tab, reading, evidence,
  and panel routes for mobile.
- **Native mobile commuter** — refreshes Home under the same BFF account and
  resumes the primary packet item.
- **Same-browser tester** — captures or injects a packet through one
  localStorage key without proving another device received it.
- **Responsive web reader** — follows a web state URL at 390 px; this is a
  mobile-width browser path, not the native Expo consumer.

## Pre-conditions

- Web and native Home are authenticated as the same BFF user for a real
  cross-device receive.
- `/workspace/...` can load its active tab. Nisaba notebook anchors may be live;
  the broad browser evidence still routes a Veritas saved-article fixture
  because authenticated setup has no public writer for that record.
- `PUT`, `GET`, and `DELETE /v1/iris/mobile-handoff` are reachable. The
  production BFF requires the shared snapshot store and hydrates it before
  routes open.
- The native Home refresh uses a BFF client with the account bearer. The mobile
  Library card is a separate fixture and must not be used as receipt proof.

## Steps

### 1. Open the real workspace route

- Use `/workspace` or `/workspace/<path>`. `(workspace)` is a Next route group
  and is not part of the public URL.
- `/app/<surface>/<path>` is a canonical shell alias. Bare `/app` has no
  `[surface]` and is not a workspace handoff destination.
- Authentication gating preserves the requested workspace path as the Welcome
  redirect target.

### 2. Prepare the desktop frame

- Select an active tab, panel, reading anchor, evidence source, side-panel kind,
  and side-panel width.
- Tabs, selected reading/evidence ids, and notes persist under workspace browser
  storage. The separate web state URL carries path, tab, panel, side, and width.
- At `MAX_WORKSPACE_TABS`, adding another tab evicts the oldest, updates the
  active state URL, and recomputes item ids and routes for the surviving active
  tab.

### 3. Build the six-item packet

- `buildWorkspaceIrisMobileHandoffState` creates reading, study, journal entry,
  voice session, assistant transcript, and notebook sync items.
- Each item carries web/mobile route material, continuity markers, an update
  timestamp, and selected reading/evidence ids when those live values exist.
- The shared contract derives readiness, a deterministic token, a nominal
  48-hour `expiresAt`, an `oshun://handoff/<token>` URI, and memory-write-shaped
  projections.
- The continuity block is static: `oshun-v1-workspace-user`, scopes, consent,
  active domain, and memory counts are not read from the authenticated member.
- The displayed memory-write count is computed from the packet. No send handler
  commits those projections to Iris memory.

### 4. Capture locally, then launch delivery

- Click **Send workspace to mobile**.
- Web first serializes the packet to `oshun.iris.mobile-handoff.v1`. Only local
  success renders **Iris state captured at...** and permits the network call.
- Web then launches a fire-and-forget authenticated `PUT` to
  `/v1/iris/mobile-handoff` and does not inspect the HTTP response.
- Local quota/private-mode failure prevents both badge and request. Network,
  auth, contract, size, or server failure after local capture leaves the badge
  visible and produces no delivery error.

### 5. Store one opaque packet per user

- The BFF derives the mailbox key from `request.authContext.userId`, not the
  packet's embedded static `userId`.
- It rejects an absent/non-object/array state, non-canonical JSON hazards, and
  objects above 256 KiB measured as UTF-8, then serializes replacement through
  candidate/save/publish and returns `storedAt` only after the snapshot commits.
- It does not fully validate direction, embedded identity, tenant, item ids, or
  routes. Client parsing supplies those semantics later. It does recognize the
  exact shared contract/version solely to enforce canonical `expiresAt`.
- Production requires the durable sink. Failed persistence returns stable `503`
  without publishing the candidate; startup strictly restores schema v1 or a
  validated legacy document and fails loudly on unknown or malformed state.
- Exact-subject continuity export, signed account erasure, 30-day last-access
  retention, exact-contract expiry, and a digest-only post-erasure write fence
  are part of the same durable partition.
- The route has no device id. `/v1/devices` separately lists registered durable
  device tokens and is not consulted by this mailbox.

### 6. Fetch on native Home

- Home calls `GET /v1/iris/mobile-handoff` when its refresh timestamp changes.
- The mobile client stringifies the returned object and parses it through the
  shared Iris contract. Missing, failed, or invalid data renders **No
  cross-device handoff waiting**.
- The client parser requires `expiresAt` but does not compare it with current
  time. Before returning an exact shared-contract packet, the BFF compares the
  canonical timestamp, durably removes an expired record, and also applies the
  continuity-state 30-day access window through its retention worker.

### 7. Resume and consume best-effort

- **Resume on mobile** uses the primary non-blocked item, remembers recognized
  domain context, and routes with the handoff token and item path.
- Home launches `DELETE /v1/iris/mobile-handoff` without awaiting it.
- The shared mobile HTTP transport omits JSON content type for this bodyless
  DELETE, so Fastify does not reject it as an empty JSON request before routing.
- Navigation succeeds even when delete fails. The card is not cleared in local
  state first, so the same packet can reappear on a later Home refresh.
- Five route cases, eleven store cases, a real-PostgreSQL integration case, and
  a phased Chromium API proof cover adjacent-user isolation, failed-write
  nonpublication, process restart, durable consume, and consumed absence after a
  second restart. They do not make Native Home's unawaited request exactly once.

### 8. Keep the two deep links distinct

- Packet `deepLink` is the custom `oshun://handoff/...` token URI. The token
  test checks its scheme, host, direction, source, and path.
- The workspace also renders a relative `/workspace/...?...` state link. That is
  the link used by the 390 px browser-restoration test.
- URL parameters restore tab, panel, side panel, and width. Selected reading and
  evidence remain primarily in `oshun.workspace.tabs.v1`.
- The two-context browser test explicitly copies that desktop tab storage into
  the mobile-width context before asserting exact evidence equality. The web URL
  alone does not transfer selected content between device profiles.

### 9. Treat Library and profile sync as separate evidence

- Native Library always constructs an authored Bhagavad Gita packet with
  `buildMobileHomeIrisHandoffState()`. Its populated card is not fetched from
  the mailbox.
- The authored Maestro flow first asserts the real Home empty branch, then moves
  to Library to inspect all six fixture channels and the send-back control.
- `HomeMultiDeviceContinuityPanel` and the two-context `profile-preference-sync`
  case prove profile/preference refresh states. They never read the workspace
  mailbox and are not a packet receipt.

### 10. Attempt the return lane

- `buildMobileReturnIrisHandoffState` reverses direction to `mobile_to_web` and
  refreshes timestamps.
- Native Home and Library calculate its projected write count, but the current
  handler does not PUT that packet. It opens the assistant with a prompt naming
  the token and count.
- Web never fetches a return packet. It can render/apply `mobile_to_web` only
  when a component or Playwright setup writes serialized state into the same
  localStorage key.
- No revision, conflict, replay, or merge contract carries mobile edits back to
  the desktop tab.

## Post-conditions

- Local capture proves only that one browser stored a serialized packet.
- A successful BFF PUT means one latest opaque packet committed for the same
  authenticated user and is recoverable after process restart.
- A valid native Home GET can render and route the primary packet item.
- Successful delete clears the mailbox; failed delete can replay it.
- Mobile creates no server return record, and desktop receives no mobile edits.
- `/v1/devices`, responsive web restoration, fixture Library, and profile sync
  remain separate authorities.

## Failure modes

- **Local write failure** — no badge and no BFF PUT.
- **BFF failure after local success** — badge remains; native Home receives
  nothing and web exposes no retry or error.
- **Blank/expired auth token** — the request can fail while local capture still
  looks successful.
- **Malformed/oversized state** — BFF returns `400`/`413`; web ignores the
  response.
- **Backing snapshot failure** — BFF returns `503` and publishes no candidate;
  the web currently discards that response and keeps its local capture badge.
- **Different account** — per-user lookup returns no other subject's packet.
- **Static embedded user mismatch** — BFF stores the opaque body under the auth
  subject but does not reconcile its embedded id.
- **Expired packet** — an exact shared-contract packet is durably removed before
  BFF delivery; opaque or future contract versions receive only the generic
  30-day continuity retention rule.
- **Delete failure** — navigation proceeds and a later refresh can show the
  packet again.
- **State link without copied storage** — responsive layout restores, but exact
  selected reading/evidence does not.
- **Library card mistaken for receipt** — the visible populated card is an
  authored fixture.
- **Send back to web** — opens an assistant prompt instead of delivering a
  return packet.
- **Concurrent edits** — no merge or conflict model exists.
- **Deletion-fence capacity** — account erasure writes a bounded digest fence;
  once its 100,000-fence capacity is reached the partition fails closed pending
  an explicit archive/migration policy.

## E2E coverage

- [`workspace-multi-panel.spec.ts`](../../apps/oshun/web/e2e/workspace-multi-panel.spec.ts)
  contains ten broad workspace cases, including local packet capture, injected
  mobile-to-web resume, local write failure, tab/note state, and responsive
  panels.
- [`multi-device-handoff-deepening.spec.ts`](../../apps/oshun/web/e2e/multi-device-handoff-deepening.spec.ts)
  contains two cases for copied-storage 390 px web restoration and independent
  Home profile-sync states.
- [`multi-device-deepening.spec.ts`](../../apps/oshun/web/e2e/multi-device-deepening.spec.ts)
  contains three cases for tab eviction, token/item recomputation, projected
  write count, and URL-plus-storage reload.
- [`profile-preference-sync.spec.ts`](../../apps/oshun/web/e2e/profile-preference-sync.spec.ts)
  contains one genuine two-context account preference refresh case; it is not
  handoff transport.
- [`iris-mobile-handoff-authority-bff.spec.ts`](../../apps/oshun/web/e2e/iris-mobile-handoff-authority-bff.spec.ts)
  contains three phase-selectable signed-auth API cases. Against the exact built
  BFF they prove durable seed, recovery and consume after one clean restart, and
  consumed absence plus adjacent preservation after a second restart.
- [`iris-mobile-handoff.test.ts`](../../apps/oshun/bff/src/routes/iris-mobile-handoff.test.ts)
  contains five same-process route cases for store/fetch/consume, isolation,
  auth, malformed input, UTF-8/prototype bounds, failed durability, and the
  deletion fence. The store adds eleven unit cases and one real-PostgreSQL
  restart/export/erase case.
- Shared contract and mobile unit tests cover typed packet mechanics, all six
  route channels, return derivation, and card callbacks.
- [`home-daypart-and-return-journeys.yaml`](../../apps/oshun/mobile/e2e/flows/shell/home-daypart-and-return-journeys.yaml)
  asserts live Home empty state and then exercises the fixture Library card.
- No automation drives the joined desktop PUT → native Home GET → consume →
  mobile edit → server return → desktop merge lifecycle.

## Per-view files touched

- [`workspace/workspace-catchall.md`](../workspace/workspace-catchall.md) — web
  workspace state, local capture, and local resume.
- [`customer/02-home-discovery/home.md`](../customer/02-home-discovery/home.md)
  — independent profile/preference sync status.
- [`workspace/app-surface-catchall.md`](../workspace/app-surface-catchall.md) —
  canonical shell alias behavior.
- Native Home and Library are outside the web walkthrough view tree but are
  different consumers: real mailbox versus fixture packet.

## Cross-references

- [`MultiPanelWorkspace.tsx`](../../apps/oshun/web/src/components/workspace/MultiPanelWorkspace.tsx)
  — local-first send and local-only return read.
- [`irisMobileWorkspaceHandoff.ts`](../../apps/oshun/web/src/components/workspace/irisMobileWorkspaceHandoff.ts)
  — static workspace packet projection.
- [`iris-mobile-handoff.ts`](../../apps/oshun/bff/src/routes/iris-mobile-handoff.ts)
  — per-user latest-packet mailbox.
- [`iris-mobile-handoff-store.ts`](../../apps/oshun/bff/src/routes/iris-mobile-handoff-store.ts)
  — versioned durable authority, expiry/retention, export, and deletion fence.
- [`mobile-handoff.ts`](../../libs/oshun/memory-iris/src/mobile-handoff.ts) —
  shared contract, token URI, expiry metadata, and write projections.
- [`index.tsx`](<../../apps/oshun/mobile/app/(tabs)/index.tsx>) — real native
  Home fetch, consume, and assistant-only return action.
- [`library.tsx`](<../../apps/oshun/mobile/app/(tabs)/library.tsx>) — fixture
  packet card.

## Open questions

- [ ] Replace static continuity fields with live account-scoped Iris state.
- [ ] Validate and subject-bind the shared packet contract at the BFF boundary.
- [ ] Await the PUT and distinguish local capture, durable store, and device
      receipt in UI.
- [ ] Join the real device inventory for target selection and per-device status.
- [ ] Add device receipt/replay semantics above the durable serialized
      replacement and consume operations.
- [ ] Surface durable acknowledgment and stable failure reasons in the web UI
      instead of discarding the BFF response.
- [ ] Persist `mobile_to_web`, let web fetch/consume it, and define concurrent
      tab/note merge rules.
- [ ] Define an archive or identity-reuse policy for the bounded digest fences.
- [ ] Add native automation for the exact desktop → Home → desktop token and
      resulting state.
