Completes Phase 5 of the multi-device mesh / session-transfer program (docs/MESH_TRANSFER.md)
except the two-Mac memory-carry spike. All additive + capability-gated; SyncProtocol stays v1.
- Moved-session visibility: additive SessionSummary.movedTo (MovedDestination), decode-defaulted.
A moved session no longer silently vanishes — the source keeps a read-only "Moved to <Mac>"
tombstone under Archived (name resolved live from paired Macs), surfaced on relaunch without
rebuilding a runnable controller, and sent on the wire so phones see it too.
- Relaunch recovery driven from launch (+ on every peer reconnect, single-flight):
AppStore.recoverInterruptedTransfers clears abandoned pre-tombstone locks, discards orphaned
inbound staging, and re-drives a tombstoned commit via SessionTransferCoordinator.recoverTombstoned
(bounded, idempotent; a dest that lost staging leaves the lock, never revives the source).
- Bulk "Hand off active sessions…": transferableSessions + moveSessionsToPeer (sequential, rollup
error) behind a "Hand off…" button → HandoffSheet checklist in RemoteAccessView.
- Arrived-from provenance (mirror of moved-to): GRDB v24 arrived_from_device_id/arrived_at; the
importer stamps them at staging; additive SessionSummary.arrivedFrom (ArrivedFrom); a subtle
"Arrived from <Mac>" marker on the sidebar (live name) + iOS row (host-baked name).
- Stranded-arrival "Activate anyway": the importer persists the staged Session to the staging dir
at .ready, so a destination that relaunches before commit can recoverableInboundTransfers() and
activateRecoveredTransfer()/clearInboundStaging(). AppStore surfaces pendingArrivedTransfers with
activate/discard, shown in a new "Interrupted arrivals" section. (A .ready lock with no manifest
is now cleared as unrecoverable.)
Tests: +6 core, +2 protocol across WireMessageTests, SessionTransferTests, AppStoreTests,
AppStoreSyncBridgeTests. Full package builds; Swift suites green. iOS NucleicRemote edits reviewed
but not compiled here (separate Xcode target).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes Phase 4 of the multi-device mesh program (docs/MESH_TRANSFER.md):
a Mac now dials paired sibling Macs with the same platform-neutral SyncClient
the iPhone uses, so it is both a host to its phones and a .control-scope client
of its peers — the dependency session transfer (P5) was waiting on.
Wire (NucleicProtocol, version stays 1, additive + capability-gated):
- PeerAddresses {lanHint, tailnet, relayRoomID, updatedAt}; optional
Hello.addresses / Welcome.addresses; ClientMsg.addressUpdate gated on new
WireCapabilities.canUpdateAddresses; PairedDevice.addresses — all
decode-defaulted so shipped iPhones and pre-mesh stores load unchanged.
Core (NucleicCore):
- PeerClient: serial LAN→tailnet dial over an injectable PeerDialer, capped
backoff, live presence stream, listPeers only when advertised. MacPeerDialer
+ LANDialChannel add the outbound dial-side FrameChannel that didn't exist.
- Symmetric pairing into one PairedDeviceStore (one pasted link makes both Macs
dialable); accepting-Mac confirm (locked decision #4); SyncHost per-deviceID
connection dedup (keep-newest, 2s grace); AppStore lifecycle + meshPeers.
UI: "Paired Macs" section (presence/transport/revoke), paste-link pairing sheet,
QR sheet doubles as copy-link + confirm dialog.
Hardening (adversarial review, 11 defects fixed incl. two security holes):
- Mac-pairing confirm enforced on the reconnect promotion path (a phone can't
reconnect claiming deviceKind=mac to skip the confirm).
- Existing-device hello branch requires the authenticated static key to match
the pin (a pairing party can't claim another device's deviceID).
- startPeerClient guarded against a racing stopSyncServer; confirm timer stored
and cancelled on resolve; pair() treats pre-welcome wireError as terminal and
classifies decline vs unreachable; handshake deadlines; setPresence won't
resurrect an unpaired peer; meshPeersChanged no-ops once the server is down;
pairing UI cancels in-flight pair() on dismiss; LANDialChannel cancels on
.failed + TCP keepalive.
Tests: 871 green (742 core + 98 protocol + 31 new) — wire codec/backcompat,
dedup, address exchange, addressUpdate, mac-confirm decline/phone-skip,
key-mismatch + promotion rejects, PeerClient pair-via-link/reconnect/listPeers
gating/unpair loopback.
Co-Authored-By: Claude Fable 5 <[email protected]>
Four Mac surfaces the iOS remote couldn't render now ride the wire,
forward-compatibly (decodeIfPresent defaults; unknown HostMsg tags
already decode to .unknown):
- DashboardSnapshot gains `usage` (WireSubscriptionUsage — the Mac's
5-hour/weekly quota gauges) and `statusFeeds` (WireStatusFeed —
active provider incidents); the host re-pushes the dashboard when a
poll changes either.
- WireTodo gains `triage` (raw TriageLevel name); the phone renders
the Mac's TriageBadge honoring the encouraging/classic label
setting.
- ClientMsg.fetchDiff / HostMsg.sessionDiff deliver the full worktree
patch on demand (capped at 512 KB with a truncated flag), gated on
the new WireCapabilities.canFetchDiff so a new phone never sends it
to an old host. iOS renders a file list + colored unified patch,
falling back to the diffstat summary against older hosts.
Round-trip and legacy-decode tests cover the new fields; the full
suite passes apart from the pre-existing fake-backend fixture gaps
and the flaky nvrsion lock-domain test (same failure rate on the
base commit).
Co-Authored-By: Claude Fable 5 <[email protected]>
Bring NucleicRemote closer to desktop parity in two areas (the core
sync loop was already at parity — shared protocol, control scope).
Transcript fidelity (iOS): a client-side TranscriptProjection coalesces
streaming text by messageID and folds each tool call's lifecycle
(start/deltas/complete/result/fileChange) into one expandable card —
fixing the duplicate started+completed rows. Adds Markdown bubbles, the
gold Orchestra card for Task/Agent spawns, and the previously-dropped
usage/cost, rate-limit, file-change, turn-boundary and session-started
rows, plus a context-window % header badge.
Mid-session controls + model catalog (protocol/host/iOS): project the
host ModelCatalog over the wire as WireModelCatalog (in Welcome); add 5
control-scope setters (setSessionModel/Effort/Auto/AutoShip/ShipBranch)
backed by the existing AppStore.mutateSession + SessionController hooks;
enrich WireSessionSummary with model/effort/auto/autoShip/shipBranch/
contextInputTokens (all forward-compatible). The composer gains a model
picker and a catalog-driven effort menu (per-backend caps: Codex→xhigh,
Grok→auto), and the session header gains a model/effort/auto/autoship
control bar.
Tests: CBOR round-trips for the new messages, Welcome.modelCatalog, the
new summary fields, forward-compat decode of old bytes, and the setters
reaching the host. Verified in the Simulator (NUCLEIC_DEMO=1).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Protocol: SessionSummary gains projectID/disposition/pendingApprovalCount/favorite/
archived (fixes the archived-chats-in-Needs-You bug + done-vs-needs-you). New
DashboardSnapshot (counts + activity grid + projects + todos), WireProject/WireTodo,
StartChatRequest/CaptureTodoRequest. ClientMsg gains listDashboard + the control set
(startChat, captureTodo, dispatchTodo, setTodoStatus, deleteTodo, renameSession,
setFavorite, setArchived, deleteSession, integrate, discard); HostMsg gains dashboard.
Moved ProjectID/TodoID/TodoStatus/TurnDisposition into NucleicProtocol. SyncClient
claims control by default + surfaces a .dashboard event.
Host: phone is granted control scope (parity). AppStore implements dashboardSnapshot()
+ all control actions (mapping to startChat/addTodo/dispatchTodo/todo CRUD/deleteSession/
integrate/discard/favorite/archive/rename) and broadcasts dashboard refreshes on turn
end + after each action. ConnectionHandler dispatches the new cases, control-gated.
178 tests green (protocol round-trips for all new messages; host control actions reach
the bridge end-to-end + grant control scope).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>