V1 Web PWA · Walk result

Journey result: Multi-device workspace handoff — a one-way packet, not a device round trip

A dated evidence record for V1 Web PWA: observed behavior, current source reconciliation, automation evidence, and explicit proof limits.

verdict: partial· 2026-05-29
8sections16 minread1table

On this page
  • Walked: 2026-05-29 at bf12b0f7d8. The retained result named an ephemeral /tmp/claude-oshun-lt-2-walkthrough/walk.mjs script and reported reachable workspace URLs plus /v1/devices; it preserved no script, trace, request log, account pair, packet, or second-device receipt.
  • Reconciled: 2026-07-17 against the workspace route group and alias, packet builder and browser storage, Iris handoff contract, authenticated BFF route and durable snapshot authority, native Home fetch/consume path, fixture Library card, mobile navigation and Maestro contracts, device-token inventory, independent profile sync, and all five named Playwright files. The runtime reconciliation includes a three-process exact-production BFF and Chromium API proof, but not a fresh application or native-device run.
  • Verdict: partial / deep for the shipped one-way seams — the current runtime has a real desktop PUT → per-user BFF record → native Home GET channel, plus a same-browser localStorage path and strong workspace/deep-link evidence. The BFF now acknowledges only durable writes, enforces the exact contract's expiry, and has restart proof. The browser still does not await or display that acknowledgment, select a target device, persist the mobile return packet, reconcile concurrent edits, or drive the joined desktop → native mobile → desktop lifecycle in automation.
  • Current authority: WALKTHROUGH/journeys/multi-device-workspace-handoff.md defines the same one-way result and separates packet transport, browser restoration, device inventory, and profile sync.

Result at a glance#

Interaction Proof · limit
Historical route walk Proof: the old walk recorded 200 responses for real /workspace/... and /app/<surface>/... URLs and a responding /v1/devices. Limit: its walker and raw evidence were ephemeral. A reachable route group and a registered-device list do not prove that workspace state moved between two devices.
Workspace frame Proof: ten workspace-multi-panel cases cover auth gating, all eight panels, tab and note state, same-browser handoff capture/resume, constrained widths, tablet switching, exports, and local write failure. Limit: only a subset concerns handoff, and the displayed workspace combines live Nisaba notebook reads with a routed Veritas saved-article fixture in its broad desktop case.
Six-item packet Proof: the shared contract deterministically normalizes reading, study, journal, voice, assistant, and notebook items, derives readiness, a token, oshun://handoff/... deep link, 48-hour expiresAt, and memory-write projections. Limit: the web builder embeds static continuity values, including userId: oshun-v1-workspace-user, fixed counts, consent, scopes, and active domain; it does not read the authenticated member's live Iris continuity state.
Desktop send Proof: clicking Send workspace to mobile writes the serialized packet under oshun.iris.mobile-handoff.v1; a failed local write produces no captured badge and no network attempt. Limit: after local success, the browser launches an authenticated BFF PUT without awaiting its response. The visible timestamp records local capture even when auth, validation, size, network, or storage persistence fails.
BFF packet record Proof: the BFF requires its durable sink, serializes candidate/save/publish mutations, awaits PUT/GET-touch/DELETE persistence, bounds canonical JSON at 256 KiB UTF-8, enforces exact-contract expiry plus 30-day retention, exports the exact subject, and joins signed continuity erasure with a digest fence. Five route cases, eleven store cases, a real-PostgreSQL integration case, and a three-process/two-restart Chromium API spec cover failure, isolation, recovery, consume, and adjacent preservation. Limit: semantic validation remains client-side, only one latest packet exists per user, and no device receipt is modeled.
Native Home receive Proof: Home refresh calls the real GET, parses the shared contract, renders the card only for a valid packet, routes the primary item, and launches best-effort DELETE. Limit: delete failure does not block navigation or clear the local card, and no native automation seeds a desktop BFF packet then observes this fetched Home card.
Mobile Library specimen Proof: component, navigation, contract, and Maestro evidence cover all six chips, resume routing, and the Send back to web control. Limit: the Library screen always calls buildMobileHomeIrisHandoffState() with authored Bhagavad Gita data; it is not the Home record fetched from the BFF. The Maestro flow explicitly expects Home's honest empty state, then exercises this Library fixture.
Links and narrow restoration Proof: browser tests restore tab, panel, side panel, width, and selected content at 390 px. Limit: two link types are easy to conflate: packet deepLink is an oshun://handoff/<token> URI, while the tested browser state link is a relative /workspace/...?... URL. Exact selected evidence is restored only after the test copies desktop tab localStorage into the second browser context.
Mobile return Proof: mobile can derive a correctly typed mobile_to_web packet, and web can apply such a packet when a test writes it into browser localStorage. Limit: both native Home and fixture Library Send back handlers open an assistant prompt. They do not PUT the return state, and web never fetches a return packet from the BFF.
Device and profile continuity Proof: /v1/devices lists real durable device-token registrations, and one two-context profile spec proves profile/preference refresh after focus. Limit: neither authority is joined to handoff: the packet route has no device id, /v1/devices has no active workspace surface, and profile sync is not a workspace delivery receipt.

Evidence map#

The vertical relay keeps the real one-way transport distinct from the two local test seams. Solid edges are shipped calls or exercised local transitions; dashed edges name the missing acknowledgment and return joins.

flowchart TB A[Desktop workspace and static continuity projection] --> B[Six-item Iris packet] B --> C[Browser localStorage capture] C --> D[Browser does not await authenticated BFF PUT] D --> E[Durably acknowledged latest packet per authenticated user] E --> F[Native Home GET and client-side parse] F --> G[Best-effort DELETE and mobile route] G -. Send back opens assistant; no PUT .-> H[No server mobile-to-web packet] H -. web has no BFF return read .-> I[Web resume only from localStorage]

Proven observations#

The historical pass measured reachability, not handoff#

  • The route correction in the old result was valid: (workspace) is a Next route group, so /workspace is public URL syntax, while /app requires a [surface] segment. The current source still has (workspace)/workspace/[[...path]] and app/[surface]/[[...path]].
  • /v1/devices is also real today, but its meaning changed from the result's implied handoff prerequisite. It reads the durable device-token store and returns an honest empty array or registered platform/token rows. Its activeSurfacePath is always null because presence is not tracked.
  • The handoff route never calls that inventory. It keys one record by the authenticated user id and has no target device, platform, push token, presence, or acknowledgment field. The registry's former /v1/devices summary therefore described adjacency, not delivery.
  • The old generic pass is not retained. A route walk with zero page errors cannot establish the cross-device and round-trip outcome named by the result.

The workspace packet is typed but carries static identity context#

  • buildWorkspaceIrisMobileHandoffState derives six items from the active tab, live reading anchor when available, selected evidence when available, and the current workspace state link. Honest fallback ids say no reading/evidence is selected rather than inventing a live record.
  • The shared buildIrisMobileHandoffState sorts those items, resolves required memory scopes, derives ready/limited/blocked, computes a deterministic token, adds a nominal 48-hour expiry, and builds the custom oshun://handoff/<token>?direction=...&source=... URI.
  • Web does not supply live continuity. WORKSPACE_IRIS_CONTINUITY hard-codes the user id, consumer, domain, consent, available scopes, five memory counts, and a 2026 timestamp. The active tab timestamp changes lastUpdatedAt, but it does not make the projection account-owned.
  • buildIrisMobileHandoffMemoryWrites returns write-shaped values for every non-blocked item. Neither the web send handler nor BFF packet route submits those values to the Iris memory write API. The 6 memory writes chip is a projection count, not six committed memories.

Local capture happens before cross-device delivery#

  • sendWorkspaceToMobile first serializes to the one browser key. Only a successful local write sets mobileHandoffSentAt, clears a locally injected incoming packet, and launches the network request.
  • The subsequent fetch is a fire-and-forget authenticated PUT whose promise is discarded. Only network rejection is caught; a resolved 400, 401, 413, or 500 is equally invisible to the component.
  • The visible copy says Iris state captured at..., which is narrowly true for localStorage. It is not a BFF storedAt, remote-device receipt, snapshot commit, or mobile render timestamp.
  • The failed-write Playwright case is useful negative evidence: a forced quota failure produces neither the key nor the badge and prevents the BFF request. No named browser case performs the inverse assertion—local success plus BFF failure—and verifies that another device receives nothing.

The BFF is a latest-packet mailbox with client-side semantics#

  • PUT, GET, and DELETE /v1/iris/mobile-handoff share auth and abuse protection. The route binds the storage key to request.authContext.userId, returns cache-control: no-store on reads, and isolates an adjacent subject in the route test.
  • The validator accepts any plain, bounded canonical JSON object. It rejects cycles, sparse arrays, non-finite values, prototype keys, excessive depth, and payloads above 256 KiB measured as UTF-8. It remains intentionally opaque to direction, embedded userId, item identity, tenant, readiness, and route safety; Native Home performs shared-contract semantic parsing.
  • Production requires the shared snapshot sink and hydrates before routes open. PUT, retention-touching GET, consume DELETE, retention purge, and subject erasure all serialize a detached candidate, await its snapshot save, and only then publish or acknowledge. Failure returns a stable 503 without exposing the candidate, and the queue remains usable for retry.
  • Schema v1 strictly restores canonical entries and digest-only deletion fences, performs one validated legacy { entries } migration, rejects future, malformed, duplicate, noncanonical, or resurrecting snapshots, and bounds the authority at 10,000 entries, 100,000 fences, and 64 MiB.
  • For the exact oshun.memory.iris.mobile_handoff@1.0.0 shape, the BFF enforces canonical expiresAt during reads and the retention worker. All entries also use the continuity-state 30-day last-access window. The generic server does not otherwise claim full contract validation.
  • Customer continuity export includes only the authenticated subject's pending handoff. Signed continuity erasure removes that entry, preserves adjacent ids, and writes a SHA-256 digest fence that rejects stale recreation without retaining the raw subject id.

Native Home is the real consumer; Library is a specimen#

  • Native Home requests the per-user record whenever lastRefreshedAt changes, parses JSON.stringify(entry.state), and stores only a valid shared-contract value in component state. Fetch or parse failure results in the honest No cross-device handoff waiting branch.
  • Resume on mobile remembers a recognized domain, fires haptics, launches a best-effort delete, and routes using the packet token plus the item's mobile or web path. Navigation deliberately continues when delete fails, so replay remains possible on refresh.
  • The shared mobile HTTP helper now omits content-type: application/json when a request has no body. That lets the bodyless handoff DELETE reach Fastify's consume route instead of failing during empty-JSON parsing.
  • The Home handler does not mark the packet consumed in local state before routing, await delete, or confirm that embedded user data matches the authenticated subject. Exact-contract expiry is enforced by the BFF before a record reaches Home, although the client parser itself does not compare time.
  • The Library tab is different. It always constructs a six-item mobile fixture with Bhagavad Gita reading, Metis study, Arete journal, Tara voice, assistant, and Nisaba notebook values. Its card proves presentation and route helpers, not a received desktop packet.
  • The authored Maestro flow documents this distinction in comments: it asserts Home's empty live state, switches to Library, inspects all six fixture chips, and taps send-back into the assistant sheet. It never performs desktop PUT → native Home GET.
  • The packet-level deepLink is always an oshun://handoff URI. The multi-device-deepening case explicitly parses that protocol, host, direction, source, and token after a local send.
  • Workspace also renders a distinct relative state URL. It carries pathname, active tab, panel, side-panel kind, and width. Item routePath values can carry that URL, while the packet's top-level deepLink does not encode the layout fields.
  • The 390 px two-context case captures the relative state URL, then explicitly copies oshun.workspace.tabs.v1 from desktop into the second browser before navigation. URL state restores layout and panel; copied storage restores the selected evidence id.
  • The second context uses a browser with a mobile viewport, not the native Expo app. Its result proves responsive web restoration under shared browser state, not a platform deep link or account-synced content selection.

The return lane ends at an assistant prompt#

  • buildMobileReturnIrisHandoffState correctly reverses the direction, updates timestamps, and rebuilds a typed token. Mobile navigation and contract tests exercise that pure transformation.
  • Native Home and fixture Library then calculate projected write count and open the assistant with Prepare Iris hand-off... copy. The handler does not PUT that packet, write Iris memory, mutate the received record, or publish an event for web.
  • Web has no GET /v1/iris/mobile-handoff consumer. It reads a mobile_to_web packet only from oshun.iris.mobile-handoff.v1 during hydration or a same-origin storage event. Component and Playwright tests inject that serialized state directly.
  • There is no revision, vector clock, merge policy, note conflict UI, replay fence, or acknowledgment linking mobile edits to the desktop tab. The shipped result is therefore one-way even though both directions exist in the type system.

The named automation proves several separate seams#

  • The five named Playwright files contain 19 cases: ten broad workspace cases, two deep-link/Home-status cases, three tab/write-count/restore cases, one independent two-context profile-preference case, and three phased BFF API cases. Only portions of the broad workspace file touch handoff.
  • The route has five focused cases; the authority store has eleven unit cases and one real-PostgreSQL restart/export/erase case. The phased browser proof used signed production auth against the exact built BFF across three clean processes: seed, recover-and-consume, then consumed-absence recovery. These establish real API durability, not native Home presentation.
  • The shared Iris contract has four unit cases; mobile navigation has three, its coverage contract has two, and the card has one. Those tests establish packet mechanics and presentation, not a joined deployed lifecycle.
  • The independent profile case is genuine two-context evidence for profile, theme, language, calendar, timezone, and document direction refresh. The Home continuity panel likewise proves profile refresh states. Neither reads the handoff mailbox or acknowledges workspace delivery.
  • No named case starts with a real desktop workspace click, observes BFF storedAt, opens native Home under the same account, resumes and consumes the exact token, changes state, sends it back through the BFF, and observes the desktop workspace merge it.

Boundaries and gaps#

  • A local capture timestamp is not delivery acknowledgment. The UI never awaits the BFF response or renders its storedAt.
  • A projected memory write is not persisted Iris memory. The builder emits write-shaped data, but the handoff send path stores only one opaque packet.
  • A static packet user id is not authenticated continuity. The embedded oshun-v1-workspace-user is not reconciled with the BFF subject.
  • An opaque object cap is not contract validation. Direction, tenant, expiry, ids, routes, and embedded identity are client-trusted.
  • Durable server acknowledgment is not visible delivery acknowledgment. The BFF now saves before 200, but the browser discards that response and no target device confirms receipt.
  • A latest-user mailbox is not device delivery. There is no device target, queue, receipt, push wake-up, or per-device consume status.
  • /v1/devices is not the handoff transport. It lists durable device-token registrations and is not joined to packet send or receive.
  • An oshun://handoff token is not a workspace state link. Responsive web restoration uses a separate URL and copied browser tab storage.
  • A mobile viewport is not the native app. The cross-context web test proves responsive hydration, not Expo routing or the BFF-fed Home card.
  • A fixture card is not the fetched Home record. Library's populated card is authored locally; the Maestro flow's live Home branch is empty.
  • A durable consume endpoint is not exactly-once device delivery. Native Home does not await it; network failure permits replay, and no local state transition confirms server consumption.
  • A typed return packet is not a return channel. Send back opens an assistant prompt; it does not persist state for web.
  • Profile refresh is not workspace acknowledgment. The independent multi-context sync and Home status panel use profile/preference authorities.
  • A bounded digest fence is not a distributed identity lifecycle. The local partition fails closed at fence capacity and has no archive or multi-region compare-and-swap policy.
  • Nineteen web/API cases are not a device round trip. No automation joins the exact token across desktop, native Home, consume, mobile edits, server return, and desktop merge.

Re-run evidence#

The reconciliation now includes focused runtime authority verification. It did not run a native Expo/Maestro device round trip. Retain the repository reports rather than another ephemeral walker:

bash
pnpm --dir apps/oshun/web exec playwright test -c playwright.config.ts e2e/workspace-multi-panel.spec.ts e2e/multi-device-handoff-deepening.spec.ts e2e/multi-device-deepening.spec.ts e2e/profile-preference-sync.spec.ts --project=chromium --workers=1
pnpm --dir apps/oshun/bff exec vitest run src/routes/iris-mobile-handoff.test.ts
pnpm --dir apps/oshun/bff exec vitest run src/routes/iris-mobile-handoff-store.spec.ts
OSHUN_ADMIN_DATABASE_URL=<postgres> pnpm --dir apps/oshun/bff exec vitest run -c vitest.integration.config.ts src/routes/iris-mobile-handoff-durability.integration.spec.ts
IRIS_MOBILE_HANDOFF_E2E_PHASE=<seed|recover|consumed> E2E_BFF_URL=<exact-bff> pnpm --dir apps/oshun/web exec playwright test e2e/iris-mobile-handoff-authority-bff.spec.ts --config playwright.fast.config.ts --project=chromium --workers=1
pnpm exec vitest run libs/oshun/memory-iris/src/mobile-handoff.test.ts
pnpm --dir apps/oshun/mobile test -- --runInBand src/navigation/mobileIrisHandoff.test.ts src/navigation/mobileIrisHandoff.contract.test.ts src/components/MobileIrisHandoffCard.test.tsx
pnpm --dir apps/oshun/mobile test -- --runInBand src/network/http-client.test.ts src/network/oshun-bff-client.test.ts

Retain the commit and environment, authenticated web/mobile subject ids, the desktop packet and top-level token, local capture timestamp, BFF response and storedAt, snapshot tier and restart read-back, native fetch/parse/card state, consume result, exact route, and any return packet. A round-trip-grade run must also retain a deliberate mobile edit, server-side return receipt, desktop read-back, conflict decision, and the final state on both devices. The BFF's separate restart, export, erasure, retention, and digest-fence evidence should remain attached to the same release proof.

Source trail#

Cross-references#

Open questions#

  • Which live Iris continuity read will replace the static packet subject, consent, scope, domain, and memory counts?
  • Should the BFF validate the shared contract and bind embedded subject/tenant to auth before storing routes and summaries?
  • Will Send workspace to mobile expose captured locally, stored, and received as distinct states with retry and accessible failure copy?
  • How will a target device be selected from the real device-token inventory, and what delivery/read receipt will it publish?
  • How should the web surface the BFF's durable 200, stable failure reasons, and retry state instead of discarding the response?
  • What replay/receipt protocol should sit above the serialized latest-write and durable consume operations to provide device-level exactly-once semantics?
  • Will native Home clear local card state only after an awaited consume, and how will it recover from delete failure without accidental replay?
  • What route will accept mobile_to_web, and how will web fetch, consume, and merge it with concurrent tab and note updates?
  • What archive or identity-reuse policy should govern the bounded digest fences after the local partition has erased the customer record?
  • What native automation will prove the exact desktop packet on Home, a mobile edit, server return, desktop merge, and final equality after process restart?