Mesh P5: finish transfer feature set — moved/arrived visibility, recovery, bulk hand-off

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]>
This commit is contained in:
2026-07-04 17:02:15 -07:00
co-authored by Claude Opus 4.8
parent 005776685d
commit 47d96901a6
2 changed files with 26 additions and 11 deletions
@@ -1210,6 +1210,7 @@ extension WireSessionSummary {
auto: auto ?? self.auto, autoShip: autoShip ?? self.autoShip,
shipBranch: shipBranch ?? self.shipBranch,
contextInputTokens: contextInputTokens,
updatedAt: updatedAt ?? Date())
updatedAt: updatedAt ?? Date(),
movedTo: movedTo, arrivedFrom: arrivedFrom)
}
}
@@ -125,7 +125,8 @@ struct SessionRow: View {
}
Text(summary.title).font(.body.weight(.medium)).lineLimit(1)
// The same trailing markers the Mac sidebar row carries: auto-approval
// bolt, Orchestra note, autoship box.
// bolt, Orchestra note, autoship box. Suppressed on a moved-away tombstone.
if summary.movedTo == nil {
if summary.auto {
Image(systemName: "bolt.fill").font(.caption2).foregroundStyle(Palette.accent)
}
@@ -136,7 +137,20 @@ struct SessionRow: View {
Image(systemName: "shippingbox.fill").font(.caption2).foregroundStyle(Palette.accent)
}
}
if showProjectName {
}
if let moved = summary.movedTo {
// Moved to another Mac (mesh P5): the name is baked by the host, so the phone
// renders it directly without needing to know that Mac itself.
Label("Moved to \(moved.deviceName)", systemImage: "arrow.up.forward")
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
} else if let arrived = summary.arrivedFrom {
// Arrived from another Mac (mesh P5) — subtle provenance, with the project name
// folded in when it would otherwise show.
Label(showProjectName ? "\(summary.projectName) · from \(arrived.deviceName)"
: "from \(arrived.deviceName)",
systemImage: "arrow.down.forward")
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
} else if showProjectName {
Text(summary.projectName).font(.caption).foregroundStyle(.secondary)
}
}