Repository navigation
feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) - #24519
Conversation
…16741 T6-8) Read-only export of a direct-SSH target's catalog and dormant state into the signed T6-7 manifest: repositories, folder workspaces and their project groups, worktree metadata and lineage, sparse presets, retired worktree names, the workspace session with bounded scrollback snapshots, automations, and client routing. Reads go through the profile-state Store via a read-only OrcadSourceExportPersistence domain; nothing retires the source. Adds the export-aware migration preflight on top of T6-5's dependents census, a resumable snapshot transfer driver with injected destination operations, and destination-side chunk staging keyed to a caller-supplied staged manifest. Lands the P7/P9 holds: session-owner projection hooks, syncDirectoryDurablySync and the durable-write mode, scrollback path and stored-bytes exports, retained refs, and dormant-tab buffer preservation. Inert until T6-10.
|
| return false | ||
| } | ||
| const rawValue = isWorktreeHostIdentity(value) ? getWorktreeIdFromHostIdentity(value) : value | ||
| if (scope.folderWorkspaceKeys.has(rawValue)) { |
There was a problem hiding this comment.
Foreign host state enters export
If another host has the same repository and worktree ID as the SSH target, this check removes the host qualifier before deciding ownership. Export scans every host’s session, so it can copy that other host’s dormant tab into the target’s manifest and attribute it to the destination. Check an explicit host qualifier before treating the worktree as belonging to this target.
How this was verified: Host-qualified keys distinguish identical worktree IDs on different hosts, but export matches the unqualified ID while scanning all host sessions.
Knowledge Base Used: Workspace and worktree lifecycle
| const group = byId.get(nextId) | ||
| if (!group?.parentGroupId || includedIds.has(group.parentGroupId)) { | ||
| continue | ||
| } | ||
| includedIds.add(group.parentGroupId) |
There was a problem hiding this comment.
If a group belonging to this SSH target has a parent whose connectionId names another target, this walk includes the parent without checking ownership. Group creation and manifest validation allow that relationship, so the signed export carries the other target’s group metadata. Cross-target ancestry needs to be rejected or handled separately.
How this was verified: The ancestry walk does not check the parent’s connection, and neither group creation nor manifest validation requires parent and child to share a target.
Knowledge Base Used: Workspace and worktree lifecycle
| const blockers: OrcadMigrationBlocker[] = [...collectTargetCatalogBlockers(store, target)] | ||
| const terminalLeases = store | ||
| .getSshRemotePtyLeases(targetId) | ||
| .filter((lease) => lease.state !== 'terminated' && lease.state !== 'expired') |
There was a problem hiding this comment.
If a target’s only remaining PTY lease is expired, this filter omits it and preflight can report the target as claimable. Expiration means the client lost its route, not that the remote process stopped. The dependency census still counts the lease, but preflight excludes lease counts from that check, so migration could proceed while remote work remains live.
Knowledge Base Used:
| manifest.destinationEnvironmentId | ||
| ) | ||
| const dormantMatches = | ||
| ignoreTransferredDormantState || | ||
| serializeOrcadMigrationValue(dormant.payload) === | ||
| serializeOrcadMigrationValue(manifest.payload.dormantState ?? emptyDormantPayload()) | ||
| if (dormantMatches) { | ||
| for (const kind of ORCAD_MIGRATION_TRANSFERRED_DORMANT_KINDS) { | ||
| counts[kind] = dormant.blockedCounts[kind] |
There was a problem hiding this comment.
Stale manifests miss new state
If a transferable dormant tab or automation appears after a manifest was created, the untransferred census skips comparing current state with that manifest. It then replaces the dependency count with the current projection’s blocked count, which can be zero even though the new row is absent from the manifest. A caller checking whether that manifest covers the source can therefore receive a false zero.
Knowledge Base Used: Desktop persistence and lifecycle
| releaseOrcadMigrationScrollback(migrationId: string): void { | ||
| this[orcadSourceExportContext].retainedScrollbackRefsByMigrationId.delete(migrationId) | ||
| } |
There was a problem hiding this comment.
Retained snapshots remain orphaned
If a tab closes during transfer, retention keeps its scrollback file while the session drops the ref. Release only removes the retention entry; later cleanup looks at refs in the previous session, where that ref no longer exists. The unreferenced file is therefore left on disk indefinitely, adding storage use with each affected transfer.
Knowledge Base Used: Desktop persistence and lifecycle
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)📝 WalkthroughWalkthroughThe pull request adds Orcad migration manifest export, source-state projection, dependency preflight, and terminal scrollback snapshot transfer. It adds migration-scoped retention so cleanup preserves snapshots used by in-flight exports. It also adds destination upload staging and validation, plus tests for migration projections, dependency counts, snapshot retention, and transfer behavior. Priority: ➖ Normal Merge Risk: 🟡 Moderate · up to The new migration export can wrongly block or allow migrations, and it can export large local-only scrollback outside the verified transfer path. Nothing calls it in production yet. Fix these issues before a caller is wired in. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change prepares saved terminal output and other workspace data for transfer. Selection and integrity checks limit the risk, and no newly exposed external transfer path was established. Recovery ownership still needs clarification before transfers are enabled. Retained concerns
Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 80a89497-a2aa-475c-9642-30763acb99b3
📒 Files selected for processing (46)
src/main/durable-file-write.tssrc/main/orca-profiles/profile-session-owner-transfer.tssrc/main/orcad-migration-export-holds.test.tssrc/main/orcad/orcad-migration-manifest-digest.tssrc/main/persistence/loading-store/session-snapshot-operations.tssrc/main/persistence/loading-store/store-domain-composition.tssrc/main/persistence/loading-store/store-runtime-state.tssrc/main/persistence/loading-store/terminal-session-cleanup.tssrc/main/persistence/loading-store/workspace-session-snapshot-publication.tssrc/main/persistence/migrating-orcad-catalog/orcad-destination-catalog-projection.tssrc/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.tssrc/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-automation-state.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-catalog.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-client-browser-intents.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-client-state.test.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-client-state.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.test.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-dormant-state.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-export.test.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-export.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-retired-worktree-names.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-scope.test.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-scope.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.test.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-session-dependencies.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-eligibility.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-fragments.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-layout.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session.tssrc/main/persistence/migrating-orcad-catalog/orcad-source-worktree-metadata.tssrc/main/ssh/orcad-migration-manifest-export.test.tssrc/main/ssh/orcad-migration-manifest-export.tssrc/main/ssh/orcad-migration-snapshot-coordinator.test.tssrc/main/ssh/orcad-migration-snapshot-coordinator.tssrc/main/ssh/ssh-target-orcad-claims.tssrc/main/ssh/ssh-target-orcad-dependents.tssrc/main/ssh/ssh-target-orcad-preflight.test.tssrc/main/ssh/ssh-target-orcad-preflight.tssrc/main/terminal-scrollback-snapshot-async-migration.tssrc/main/terminal-scrollback-snapshots.tssrc/main/worktree-retirement-namespace.tssrc/shared/workspace-session-terminal-buffers.test.tssrc/shared/workspace-session-terminal-buffers.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| const acknowledgements = Object.entries(ui.acknowledgedAgentsByPaneKey ?? {}).filter( | ||
| ([paneKey]) => { | ||
| const allowed = eligible ? orcadMigrationPaneBelongsToTabs(paneKey, eligible.tabIds) : false | ||
| if (!allowed) { | ||
| blockedCounts['ui-routing'] += 1 | ||
| } | ||
| return allowed | ||
| } | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Count only source-scoped agent acknowledgements as ui-routing blockers.
This filter runs over every entry in the profile-wide ui.acknowledgedAgentsByPaneKey. Each pane key outside eligible.tabIds increments blockedCounts['ui-routing']. That includes acknowledgements for local tabs and for tabs on other SSH targets. If eligible is null, every acknowledgement in the profile counts as a blocker.
collectOrcadMigrationUntransferredDependencyCensus copies dormant.blockedCounts['ui-routing'] into the census, and collectUntransferredDependentBlockers turns a nonzero count into orcad_migration_dependent_state. As a result, one acknowledged agent anywhere in the profile makes preflightOrcadMigrationExport return claimable: false for every target.
Skip pane keys that do not belong to a source-scoped tab. Count a blocker only when a source-scoped pane is not representable in the eligible session.
🐛 Proposed fix
const eligible = session ? collectEligibleSessionIdentity(session, scope) : null
+ const sourceTabIds = inspectOrcadMigrationSourceSessions(state, {
+ targetId: scope.targetId,
+ hostId: scope.hostId,
+ ownerMatches: (ownerKey) => orcadMigrationOwnerMatchesScope(ownerKey, scope)
+ }).tabIds
const acknowledgements = Object.entries(ui.acknowledgedAgentsByPaneKey ?? {}).filter(
([paneKey]) => {
+ if (!orcadMigrationPaneBelongsToTabs(paneKey, sourceTabIds)) {
+ return false
+ }
const allowed = eligible ? orcadMigrationPaneBelongsToTabs(paneKey, eligible.tabIds) : false
if (!allowed) {
blockedCounts['ui-routing'] += 1
}
return allowed
}
)Import inspectOrcadMigrationSourceSessions from ./orcad-source-session-dependencies.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const acknowledgements = Object.entries(ui.acknowledgedAgentsByPaneKey ?? {}).filter( | |
| ([paneKey]) => { | |
| const allowed = eligible ? orcadMigrationPaneBelongsToTabs(paneKey, eligible.tabIds) : false | |
| if (!allowed) { | |
| blockedCounts['ui-routing'] += 1 | |
| } | |
| return allowed | |
| } | |
| ) | |
| const sourceTabIds = inspectOrcadMigrationSourceSessions(state, { | |
| targetId: scope.targetId, | |
| hostId: scope.hostId, | |
| ownerMatches: (ownerKey) => orcadMigrationOwnerMatchesScope(ownerKey, scope) | |
| }).tabIds | |
| const acknowledgements = Object.entries(ui.acknowledgedAgentsByPaneKey ?? {}).filter( | |
| ([paneKey]) => { | |
| if (!orcadMigrationPaneBelongsToTabs(paneKey, sourceTabIds)) { | |
| return false | |
| } | |
| const allowed = eligible ? orcadMigrationPaneBelongsToTabs(paneKey, eligible.tabIds) : false | |
| if (!allowed) { | |
| blockedCounts['ui-routing'] += 1 | |
| } | |
| return allowed | |
| } | |
| ) |
| const leafIds = new Set([ | ||
| ...Object.keys(sourceLayout.buffersByLeafId ?? {}), | ||
| ...Object.keys(sourceLayout.scrollbackRefsByLeafId ?? {}) | ||
| ]) | ||
| for (const leafId of [...leafIds].sort(compareKeys)) { | ||
| const buffer = sourceLayout.buffersByLeafId?.[leafId] | ||
| const sourceRef = sourceLayout.scrollbackRefsByLeafId?.[leafId] | ||
| const ref = buffer ? makeTerminalScrollbackSnapshotRef(tabId, leafId) : sourceRef |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Handle localOnlyScrollbackByTabId in the scrollback projection.
extractSessionOwnersForTransfer copies localOnlyScrollbackByTabId for the transferred tabs (profile-session-owner-transfer.ts, lines 112-118). This projection reads only buffersByLeafId and scrollbackRefsByLeafId, and it never removes the local-only field. This causes two failures:
- The raw local-only buffers stay inline in
dormantState.workspaceSession. They bypassMAX_ORCAD_MIGRATION_SCROLLBACK_TOTAL_BYTESand the snapshot descriptors. TheWorkspaceSessionStatedoc comment states that this field can be tens of MiB and "never leaves this client". - A leaf whose only copy is local-only gets no snapshot descriptor. Its scrollback therefore does not go through the verified chunk-transfer path.
Resolve each leaf through resolveLeafScrollbackBuffers, as the type documentation requires. Then delete localOnlyScrollbackByTabId from projected. Apply the same resolution in findSnapshotBytes (lines 132-138), so that chunk reads match the exported descriptors.
🐛 Sketch
- const leafIds = new Set([
- ...Object.keys(sourceLayout.buffersByLeafId ?? {}),
- ...Object.keys(sourceLayout.scrollbackRefsByLeafId ?? {})
- ])
+ const resolvedBuffers = resolveLeafScrollbackBuffers(session, tabId) // per its actual signature
+ const leafIds = new Set([
+ ...Object.keys(resolvedBuffers ?? {}),
+ ...Object.keys(sourceLayout.scrollbackRefsByLeafId ?? {})
+ ])
for (const leafId of [...leafIds].sort(compareKeys)) {
- const buffer = sourceLayout.buffersByLeafId?.[leafId]
+ const buffer = resolvedBuffers?.[leafId]+ delete projected.localOnlyScrollbackByTabId
if (
snapshots.length > MAX_ORCAD_MIGRATION_SCROLLBACK_SNAPSHOTS ||| // Closing a tab mid-transfer must not delete the bytes this manifest promised. | ||
| args.source.retainOrcadMigrationScrollback(args.manifest) | ||
| try { | ||
| await sendSnapshots(args, snapshots) | ||
| } finally { | ||
| args.source.releaseOrcadMigrationScrollback(args.manifest.migrationId) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
set -eu
printf '%s\n' '--- changed file ---'
cat -n src/main/ssh/orcad-migration-snapshot-coordinator.ts
printf '%s\n' '--- retain/release definitions and usages ---'
rg -n -C 5 'retainOrcadMigrationScrollback|releaseOrcadMigrationScrollback' .
printf '%s\n' '--- relevant diff ---'
git diff --unified=80 444f1952c771e08134d490145ca6c756487ad163 77375bb61e7fa834324b280b16e9a0071c871aac -- src/main/ssh/orcad-migration-snapshot-coordinator.tsRepository: stablyai/orca
Length of output: 18974
🤖 get_repo_knowledge executed:
get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings
Length of output: 17478
🏁 Script executed:
set -eu
cat -n src/main/ssh/orcad-migration-snapshot-coordinator.ts
printf '%s\n' '--- methods ---'
rg -n -C 8 'retainOrcadMigrationScrollback|releaseOrcadMigrationScrollback' .
printf '%s\n' '--- diff ---'
git diff --unified=60 444f1952c771e08134d490145ca6c756487ad163 77375bb61e7fa834324b280b16e9a0071c871aac -- src/main/ssh/orcad-migration-snapshot-coordinator.tsRepository: stablyai/orca
Length of output: 22531
🏁 Script executed:
set -eu
git diff --unified=20 444f1952c771e08134d490145ca6c756487ad163 77375bb61e7fa834324b280b16e9a0071c871aac -- src/main/ssh/orcad-migration-snapshot-coordinator.ts
rg -n -C 4 'retainOrcadMigrationScrollback|releaseOrcadMigrationScrollback' .Repository: stablyai/orca
Length of output: 12025
🏁 Script executed:
set -eu
printf '%s\n' '--- retention map consumers ---'
rg -n -C 10 'retainedScrollbackRefsByMigrationId|terminalScrollbackSnapshotStorage|delete.*scrollback|unlink.*scrollback' src/main
printf '%s\n' '--- coordinator callers ---'
rg -n -C 8 'transferOrcadMigrationSnapshots' src
printf '%s\n' '--- source export context ---'
sed -n '1,190p' src/main/persistence/migrating-orcad-catalog/orcad-source-export.tsRepository: stablyai/orca
Length of output: 41811
🏁 Script executed:
set -eu
printf '%s\n' '--- exact retention consumers ---'
rg -n -C 14 'retainedScrollbackRefsByMigrationId' src/main
printf '%s\n' '--- cleanup APIs and callers ---'
rg -n -C 10 'deleteRemovedTerminalScrollbackSnapshots|removedSnapshot|retainedScrollback' src/main/persistence src/main/terminal-scrollback-snapshot-async-migration.tsRepository: stablyai/orca
Length of output: 44939
Serialize or reference-count transfers per migrationId.
When two transferOrcadMigrationSnapshots calls for the same migrationId overlap, the first call deletes the shared retention entry while the second call is still reading snapshots. Cleanup can then delete bytes that the second transfer still needs.
Serialize transfers for each migrationId, or release the entry only after the final overlapping transfer completes.
| const blockers: OrcadMigrationBlocker[] = [...collectTargetCatalogBlockers(store, target)] | ||
| const terminalLeases = store | ||
| .getSshRemotePtyLeases(targetId) | ||
| .filter((lease) => lease.state !== 'terminated' && lease.state !== 'expired') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Block on expired relay leases or prove that they cannot own remote work.
This filter excludes expired leases from the orcad_migration_direct_ssh_terminal_leases blocker. The same PR treats those leases as live authority in two other places:
countTerminalRecoveryStateinorcad-source-dependency-census.tsstates "Expired routes may still own remote work; only terminated leases resolve recovery authority" and filters only!== 'terminated'.shutdownMarkerHasTerminalAuthoritytreats any non-terminatedlease as PTY authority.
With an expired lease and no consumer-recovery record, preflight reports claimable: true. The relay PTY can still be running. The migration can then proceed while the source still holds remote work that cannot move to orcad. Align the predicate with the census so that only terminated leases do not block.
🐛 Proposed fix
- .filter((lease) => lease.state !== 'terminated' && lease.state !== 'expired')
+ // Expired routes may still own remote work; only terminated leases are final.
+ .filter((lease) => lease.state !== 'terminated')📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .filter((lease) => lease.state !== 'terminated' && lease.state !== 'expired') | |
| // Expired routes may still own remote work; only terminated leases are final. | |
| .filter((lease) => lease.state !== 'terminated') |
There was a problem hiding this comment.
Important
Two real correctness gaps in the export/preflight path, plus two smaller ones in the snapshot lifecycle. The PR is inert (no production caller yet), so nothing is broken today — but these will bite when T6-10 wires the export up. The expired-lease gap is the one I'd fix before this becomes live.
Reviewed changes — reviewed the single commit 77375bb in full (46 files, +4855/−30).
- Signed manifest export —
createOrcadMigrationManifestcollects the target's catalog + dormant state, validates through T6-7's parser, and signs the digest. - Read-only store domain —
OrcadSourceExportPersistenceexposes the dependency census, dormant collection, digest-checked chunk reads, and retained-ref tracking; registered in the store composition. - Dormant projection — workspace-session eligibility/fragments/layout/chunk merging, worktree metadata/lineage, sparse presets, retired names, automations, and client state, each filtered to the target's owner scope.
- Chunked snapshot transfer — destination staging/verify/hard-link commit plus a source coordinator with resume and lost-acknowledgement reconciliation.
- Export preflight —
preflightOrcadMigrationExportdrains catalog rows, blocks live leases and untransferred dependents, and treats an unreadable census asunverifiable. - Held changes — session-owner projection hooks, durable-write
mode+syncDirectoryDurablySync, scrollback reader/path exports, retained-ref plumbing, and dormant-tab buffer retention.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| const blockers: OrcadMigrationBlocker[] = [...collectTargetCatalogBlockers(store, target)] | ||
| const terminalLeases = store | ||
| .getSshRemotePtyLeases(targetId) | ||
| .filter((lease) => lease.state !== 'terminated' && lease.state !== 'expired') |
There was a problem hiding this comment.
Filtering out expired leases leaves a target with a client-side expiry (an expired lease carrying no supersededBy/relayIdRecycled mark) with no lease blocker at all: the census deliberately omits terminal-lease from CENSUS_DEPENDENCY_KINDS, and the manifest carries no lease field. sshRemotePtyLeaseAllowsReattach documents that such an orphan is not provably dead, so preflightOrcadMigrationExport would report claimable: true and cutover would strand the remote shell. Gate the lease list on sshRemotePtyLeaseAllowsReattach(lease) instead of the hand-rolled state check.
Technical details
# Expired orphan lease drains silently
## Affected sites
- `src/main/ssh/ssh-target-orcad-preflight.ts:48` — excludes `expired` unconditionally.
- `src/main/ssh/ssh-target-orcad-dependents.ts:82-84` — `terminal-lease` excluded from `CENSUS_DEPENDENCY_KINDS`, so the census can never surface it.
- `src/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.ts:158-164` — `countTerminalRecoveryState` counts a lease as active when `state !== 'terminated'` (i.e. includes `expired`), the opposite predicate to the preflight, producing the false-positive block noted below.
## Required outcome
- An `expired` lease that cannot be shown dead (no `supersededBy`, no `relayIdRecycled`) blocks export, matching `src/shared/ssh-types.ts:275-293` and `docs/reference/ssh-execution-boundary.md`.
- An `expired` lease that *is* provably dead (either mark present) drains.
- The recovery predicate in `countTerminalRecoveryState` should use the same `sshRemotePtyLeaseAllowsReattach` gate so the two sites agree; today an expired orphan lease plus a `sshPtyConsumerRecovery` row produces `orcad_migration_dependent_state` and strands a drainable target.
## Open questions for the human
- Is the deliberate `expired` exclusion here intended to mean "client lost its route, so release the host", or was it meant to mirror the old `collectDependentStateBlockers`, which counts every lease status and blocks? The two paths disagree.| const ref = layout.scrollbackRefsByLeafId?.[descriptor.leafId] | ||
| const bytes = buffer | ||
| ? Buffer.from(buffer, 'utf8') | ||
| : ref === descriptor.ref |
There was a problem hiding this comment.
findSnapshotBytes resolves bytes only through the live session's scrollbackRefsByLeafId/buffersByLeafId. retainOrcadMigrationScrollback keeps a snapshot file alive on disk, but once a tab closes mid-transfer its ref disappears from every session partition, so the retained file is never consulted and the next chunk throws orcad_migration_source_snapshot_changed. Retention exists precisely to survive that close, so a retained ref should be readable straight from storage by descriptor.ref before falling back to the session lookup.
Technical details
# Retained snapshot bytes are not readable after a tab close
## Affected sites
- `src/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.ts:122-148` — `findSnapshotBytes` requires `layout.scrollbackRefsByLeafId[leafId] === descriptor.ref` (or an in-memory buffer); a removed tab's layout is gone.
- `src/main/persistence/migrating-orcad-catalog/orcad-source-export.ts:81-93` — `retainOrcadMigrationScrollback` records refs, but `readOrcadMigrationSourceSnapshotChunk` never reads those files directly.
- `src/main/persistence/loading-store/workspace-session-snapshot-publication.ts:104-109` — retention is what suppresses the deletion of those files.
## Required outcome
A chunk read for a ref the manifest names must still succeed after its tab is closed while retained. Either resolve the bytes by `descriptor.ref` from the (retained) store before the session lookup, or persist buffer-sourced snapshots to a ref-addressed file at export time so every descriptor has durable bytes independent of the live session.
## Open questions for the human
- `projectOrcadMigrationSessionScrollback` computes `sha256`/`byteLength` for buffer-sourced (dormant-tab) snapshots without writing them to disk; should it call `writeTerminalScrollbackSnapshotSync` so every snapshot has a file to retain?| } | ||
|
|
||
| releaseOrcadMigrationScrollback(migrationId: string): void { | ||
| this[orcadSourceExportContext].retainedScrollbackRefsByMigrationId.delete(migrationId) |
There was a problem hiding this comment.
releaseOrcadMigrationScrollback only drops the retained set; it never re-runs the deletion that was skipped while the ref was retained. If a tab closed during the transfer, the ref is already absent from every future prior session by the time release happens, so deleteRemovedTerminalScrollbackSnapshots[Async] can never see it again and the ${ref}.bin file leaks permanently — nothing in the repo enumerates the snapshot root to reclaim it (pruneOrcadMigrationSnapshotStaging only sweeps staging directories). Re-run the deletion for the just-released refs, or add an orphan sweep, before forgetting them.
Technical details
# Released retained refs are never reclaimed
## Affected sites
- `src/main/persistence/migrating-orcad-catalog/orcad-source-export.ts:91-93` — release deletes the map entry and nothing else.
- `src/main/ssh/orcad-migration-snapshot-coordinator.ts:46-51` — retain at transfer start, release in `finally`.
- `src/main/persistence/loading-store/terminal-session-cleanup.ts:31-40` and `src/main/terminal-scrollback-snapshot-async-migration.ts:44-53` — deletion is ref-addressed `prior`/`next` only.
- `src/main/terminal-scrollback-snapshots.ts:216-236` — delete helpers take a ref; no directory enumeration exists under the snapshot root.
## Required outcome
When retention ends, any retained ref no longer referenced by the current session is deleted. This can be done by invoking the existing deletion pass with the released refs as `prior` refs against the current session, or by a bounded orphan sweep of `${ref}.bin` files at release time.
## Suggested approach (optional)
Have `releaseOrcadMigrationScrollback` return the released refs; the coordinator (or the Store) then calls `deleteRemovedTerminalScrollbackSnapshots` with a synthetic prior session carrying those refs.| throw error | ||
| } | ||
| } | ||
| rmSync(stagedPath, { force: true }) |
There was a problem hiding this comment.
The staged name is unlinked before the final hard link is made durable: syncDirectoryDurablySync(root) runs only after the whole loop (line 146). A crash between linkSync (line 134) and that fsync can replay the staged unlink without the final link, leaving the inode with zero links — and the source already released retention when chunk transfer finished, so the bytes cannot be re-fetched if the tab has since closed. Fsync root immediately after linking, then remove the staged name.
Technical details
# Link not durable before staged unlink
## Affected sites
- `src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts:134` — `linkSync(stagedPath, finalPath)`.
- `src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts:144` — `rmSync(stagedPath)` runs before the fsync.
- `src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts:146` — `syncDirectoryDurablySync(root)` after the loop.
- `src/main/ssh/orcad-migration-snapshot-coordinator.ts:46-51` — source retention released before commit runs.
## Required outcome
After linking a descriptor, fsync the directory containing `finalPath` before unlinking `stagedPath`, so a crash cannot persist the unlink without the link. (`syncDirectoryDurablySync` is already exported by this PR.)* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)" This reverts commit 783101b. * Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)" This reverts commit 38c2d1d. * Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)" This reverts commit 8b76683. * Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)" This reverts commit d3f8c50. * Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)" This reverts commit 43d9b43. * Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)" This reverts commit b093d3a. * Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)" This reverts commit 1a9ac0e. * Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)" This reverts commit 99db2bf. * Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)" This reverts commit 34a582b. * Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)" This reverts commit d53063d. * Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)" This reverts commit ff212db. * Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)" This reverts commit 92cb717. * Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)" This reverts commit 6b36e4f. * Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)" This reverts commit d23ecef. * Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)" This reverts commit dd87ae5. * Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)" This reverts commit ece9e4d. * Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)" This reverts commit 3fbdaba. * Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)" This reverts commit 4e8edc8. * Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)" This reverts commit 60c9326. * Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)" This reverts commit 99e0303. * Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)" This reverts commit 817af76. * Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)" This reverts commit c991893. * Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)" This reverts commit dc08ffe. * Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)" This reverts commit a789233. * Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)" This reverts commit 0b812bd. * Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)" This reverts commit 3aa2d3a. --------- Co-authored-by: m4air <m4air@Mac.localdomain>

ELI5
To move a user off the SSH relay and onto a managed
orcadserver, Orca first has to package up everything the user's Orca keeps for that SSH host:This PR adds that packaging step. It only reads: it builds a signed bundle (the "manifest") and leaves every row on the source side untouched. Nothing calls it yet, so users see no change.
What Changed
Export
New files under
src/main/persistence/migrating-orcad-catalog/orcad-source-*andsrc/main/ssh/orcad-migration-manifest-export.ts.What the manifest contains. For one SSH target,
createOrcadMigrationManifestcollects:Validation. The result goes through T6-7's
parseOrcadMigrationManifest(feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) #24422) and is signed with the same digest. Folder workspaces and git worktrees both export.What doesn't fit. Anything the manifest can't represent is counted as blocked rather than guessed.
Reads go through the profile store. They go through a new read-only domain on the profile-state
Store(refactor(persistence): retire ordinary JSON profile writes #23202),OrcadSourceExportPersistence. It offers four calls:collectOrcadMigrationSourceDormantStateinspectOrcadMigrationSourceDependencies/inspectOrcadMigrationUntransferredDependenciesreadOrcadMigrationSourceSnapshotChunk, which checks the manifest's digest, then the snapshot's length and sha256, before returning a bounded chunkretainOrcadMigrationScrollback/releaseOrcadMigrationScrollbackIt holds the runtime state and nothing that writes.
Snapshot transfer
orcad-migration-snapshot-coordinator.tssends a manifest's snapshots in bounded, resumable chunks. The destination's catalog operations are passed in, so it has no transport or cutover of its own. A chunk whose acknowledgement was lost is confirmed by reading the destination's recorded offset, never assumed. The bytes stay retained for the transfer's duration.orcad-scrollback-snapshot-transfer.tsstages, verifies and commits chunks on the destination. It now receives the staged manifest from its caller instead of reading destination catalog state, so T6-9's import can plug in.Preflight
ssh-target-orcad-preflight.tsaddspreflightOrcadMigrationExport. It reuses T6-5's claim structure (the catalog blockers were split out ascollectTargetCatalogBlockers).ssh-target-orcad-dependents.tswithcollectUntransferredDependentBlockersinstead of building a second census.unverifiable, never empty.Held changes from P7/P9
extractSessionOwnersForTransfergains optionalprojectSleepingAgentSessionandprojectSessionFocusprojection hooks, and re-exports the markdown id helpers.syncDirectoryDurablySyncis exported, andwriteFileDurableSyncgains a creationmode.getTerminalScrollbackSnapshotRoot,getTerminalScrollbackSnapshotPathandreadTerminalScrollbackStoredBytesSyncare now exported.retainedRefs. Session publication passes the refs that in-flight exports retain, so closing a tab mid-transfer can't delete bytes the manifest promised.ptyId === null) keeps its saved buffer, since there's nothing live to replay from.Not ported
These are T8 retirement/cutover pieces:
retire*/assert*Retired).orcad-source-session-mirror. Its only consumer is the live projection.orcad-managed-migration-status, which only lists source cutovers.orcad-migration-source-catalog, which only builds patches that retire the source catalog after a cutover.Also, from the destination side, only
toOrcadDestinationRepositoryis taken. It moved toorcad-destination-catalog-projection.ts.Why
Linked Issue
Part of the Node runtime plan (Phase 3), T6 track (#16741): T6-8, source-side dormant export. Inert until T6-10.
Visual Proof
N/A. There's no UI change and no production caller.
Testing
Ported and adapted from feat(orcad): consolidate Bun runtime and SSH/Remote migration #16741: dependency census; client state, now with typed fixtures instead of casts; snapshot staging, which now takes the staged manifest; scrollback state; manifest export; snapshot coordinator, which now covers retention as well.
New tests:
orcad-source-export.test.ts, against a real profile-stateStore:ssh-target-orcad-preflight.test.ts:orcad-source-scope.test.ts: folder workspaces and worktrees are both in scope.orcad-migration-export-holds.test.ts: the stored-bytes reader, sync and async retained refs, the durable-write mode, and the projection hooks.workspace-session-terminal-buffers.test.ts.Regression: every test under
src/main/persistence, the orcad andssh-targettests insrc/main/ssh,src/main/orca-profiles, the scrollback and durable-write tests, the shared migration contracts, and the import boundary.profile-state-backup-rotation.test.ts. That file fails identically on unmodifiedorigin/mainon this machine.Static checks:
node config/scripts/check-changed-code-quality.mjs origin/mainpasses;tsc --noEmitover the changed files is clean (the full typecheck was not run);oxlintis clean;pnpm-lock.yamlis unchanged;child_processimport.I manually tested these changes locally
Automated tests added/updated, or explained why not below
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes