refactor(renderer): delete the direct-SSH authority and reconnect coordinator (#27587)

No ssh: git provider is registered in production, so the reconnect
coordinator's catalog and lineage reads always came back non-authoritative
and its worktree refresh never merged. Its only live effect was clearing an
unconverted target's stale PTY bindings on each connected push and on wake;
the state bridge now does that directly.

Deletes the coordinator, preparation, worktree refresh scheduler, host
hydration, rollout flag and their product telemetry event, the scoped
direct-SSH reconnect/hydration options, and the now caller-less
repos:listForExecutionHost and worktrees:listLineageForHost IPC handlers.

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-11 14:41:17 -07:00
committed by GitHub
co-authored by m4air
parent fee083fa47
commit bd54fc7fa3
83 changed files with 137 additions and 7020 deletions
+15 -68
View File
@@ -4263,28 +4263,25 @@
"terminal input availability",
"worktree discovery",
"Source Control status refresh",
"direct SSH detected-worktree scheduling",
"direct SSH reconnect telemetry",
"direct SSH host catalog authority",
"direct SSH generation-scope rollover"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "ssh"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "ssh"],
"coverageNotes": "Local deterministic evidence covers git-common classification, desktop watcher debounce counts, non-overlapping poller semantics, macOS native-watch fallback, preload cleanup, Source Control active-visible repo filtering, paired-runtime publication to two independent clients with reconnect and stale-cache checks, the direct SSH five-slot fair scheduler, timeout barrier, aggregate privacy schema, coordinator-to-renderer telemetry wiring, host-catalog provenance rejection, and process generation-scope rollover across sibling targets. A headed macOS Electron host and separate paired Electron client both rendered a worktree created through the real Git binary and watcher entry path without reconnect. A macOS Electron client also completed a direct SSH disconnect/reconnect against an ephemeral Linux Docker target with exact host/authority hydration and remote proof-file verification. Linux/Windows desktop clients, multi-target live fanout, and WSL runs remain gaps.",
"coverageNotes": "Local deterministic evidence covers git-common classification, desktop watcher debounce counts, non-overlapping poller semantics, macOS native-watch fallback, preload cleanup, Source Control active-visible repo filtering, paired-runtime publication to two independent clients with reconnect and stale-cache checks, and process generation-scope rollover across sibling targets. A headed macOS Electron host and separate paired Electron client both rendered a worktree created through the real Git binary and watcher entry path without reconnect. Linux/Windows desktop clients, multi-target live fanout, and WSL runs remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/7086",
"https://github.com/stablyai/orca/issues/16477"
],
"invariant": "Index-only Git activity below the common Git directory must not emit worktrees:changed, invalidate worktree caches, or trigger fetchWorktrees fanout; structural add/remove/HEAD/gitdir/locked/config.worktree changes must still refresh worktrees and nudge Source Control. When the authoritative paired host accepts an external structural change for a uniquely identified server-local repo, each live client for that host must receive one existing host-scoped worktreesChanged invalidation and converge to the same repo/worktree identity without reconnect; duplicate watcher events must coalesce, stale cached or in-flight catalogs must not survive invalidation, and event-ignoring clients must remain compatible. Nested SSH watchers and colliding local/SSH repo IDs must not publish the host-blind event until existing semantics can carry owner identity. External head moves (commit, amend, reset) must reach background worktree rows through spawn-free metadata reads, never through structural fanout. Direct SSH reconnect discovery must stay host- and authority-qualified, reject contradictory main-catalog provenance without returning rows, admit at most five locally unsettled provider calls, retain a retrying timeout barrier, and emit one identifier-free aggregate product event per target operation. A process generation-scope rollover revokes every direct SSH target and old-scope provider request, not only the target whose counter exhausted.",
"oracle": "Classify exact git-common paths as structural, status-only, or ignored; count notifications from debounced watcher events; force the Linux/Windows poll path to emit allowlisted leaf events, detect linked HEAD rewrites independent of entry-directory mtime, and surface in-place index rewrites via the backstop re-stat; diff head identities from metadata-file reads and notify only real head moves; assert Source Control subscribes to both structural and status-only signals with active-repo and visibility filters. Join the real runtime RPC server to two independently authenticated E2EE clients plus an event-ignoring client, advance the host Git inventory, trigger duplicate structural watcher callbacks, and require one host renderer notification plus one runtime event and a refreshed catalog per subscribed client; require the event-ignoring client to refresh safely on explicit read. Hold a scan after it captures old inventory, join it from a second paired client, publish the watcher invalidation, advance Git independently, and require both overlapping RPCs to retry or join one fresh scan before returning. Disconnect one client, advance inventory again, and require reconnect to read current state without stale event replay. Give a watcher a nested SSH connection id or a local repo ID colliding with an SSH owner and require the host renderer refresh while the unqualified runtime event stays absent. Force the real path in isolated Electron apps with git worktree add, a host Git inventory assertion, store-subscription barriers, matching host/client worktree identity, and visible rows in both renderers. For direct SSH, reject catalog rows whose explicit and legacy host provenance contradict, roll one exhausted target into a fresh process generation scope while invalidating sibling target tokens, count locally unsettled attempts and round-robin admissions, keep lineage blocked through the first timeout retry, distinguish timeout/rejection/cancel/stale results, and reject telemetry properties carrying target, repo, host, path, label, user, request, lease, terminal, or raw-error data.",
"invariant": "Index-only Git activity below the common Git directory must not emit worktrees:changed, invalidate worktree caches, or trigger fetchWorktrees fanout; structural add/remove/HEAD/gitdir/locked/config.worktree changes must still refresh worktrees and nudge Source Control. When the authoritative paired host accepts an external structural change for a uniquely identified server-local repo, each live client for that host must receive one existing host-scoped worktreesChanged invalidation and converge to the same repo/worktree identity without reconnect; duplicate watcher events must coalesce, stale cached or in-flight catalogs must not survive invalidation, and event-ignoring clients must remain compatible. Nested SSH watchers and colliding local/SSH repo IDs must not publish the host-blind event until existing semantics can carry owner identity. External head moves (commit, amend, reset) must reach background worktree rows through spawn-free metadata reads, never through structural fanout. A process generation-scope rollover revokes every direct SSH target and old-scope provider request, not only the target whose counter exhausted.",
"oracle": "Classify exact git-common paths as structural, status-only, or ignored; count notifications from debounced watcher events; force the Linux/Windows poll path to emit allowlisted leaf events, detect linked HEAD rewrites independent of entry-directory mtime, and surface in-place index rewrites via the backstop re-stat; diff head identities from metadata-file reads and notify only real head moves; assert Source Control subscribes to both structural and status-only signals with active-repo and visibility filters. Join the real runtime RPC server to two independently authenticated E2EE clients plus an event-ignoring client, advance the host Git inventory, trigger duplicate structural watcher callbacks, and require one host renderer notification plus one runtime event and a refreshed catalog per subscribed client; require the event-ignoring client to refresh safely on explicit read. Hold a scan after it captures old inventory, join it from a second paired client, publish the watcher invalidation, advance Git independently, and require both overlapping RPCs to retry or join one fresh scan before returning. Disconnect one client, advance inventory again, and require reconnect to read current state without stale event replay. Give a watcher a nested SSH connection id or a local repo ID colliding with an SSH owner and require the host renderer refresh while the unqualified runtime event stays absent. Force the real path in isolated Electron apps with git worktree add, a host Git inventory assertion, store-subscription barriers, matching host/client worktree identity, and visible rows in both renderers. For direct SSH, roll one exhausted target into a fresh process generation scope while invalidating sibling target tokens, and fill a partial pushed authority before clearing that target's PTY bindings once.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/worktree-base-directory-event-filter.test.ts src/main/ipc/worktree-base-directory-watcher.test.ts src/main/ipc/worktree-base-directory-poller.test.ts src/main/ipc/worktree-head-identity-reader.test.ts src/renderer/src/hooks/worktree-head-identity-apply.test.ts src/renderer/src/components/right-sidebar/git-status-push-signal-refresh.test.ts src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/watched-worktree-catalog-notification.test.ts src/main/runtime/external-worktree-paired-client-discovery.integration.test.ts src/main/window/attach-main-window-services.test.ts",
"ORCA_E2E_FORCE_HEADFUL=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-external-worktree-discovery.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/repos-remote.test.ts src/main/ssh/ssh-connection-generation.test.ts src/main/ssh/ssh-provider-authority.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/shared/direct-ssh-reconnect-telemetry-schema.test.ts src/renderer/src/lib/direct-ssh-reconnect-product-telemetry.test.ts src/renderer/src/hooks/direct-ssh-worktree-refresh-scheduler.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts --reporter=dot"
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts --reporter=dot"
],
"testFiles": [
"src/main/ipc/worktree-base-directory-event-filter.test.ts",
@@ -4301,11 +4298,7 @@
"src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts",
"src/main/ipc/repos-remote.test.ts",
"src/main/ssh/ssh-connection-generation.test.ts",
"src/main/ssh/ssh-provider-authority.test.ts",
"src/shared/direct-ssh-reconnect-telemetry-schema.test.ts",
"src/renderer/src/lib/direct-ssh-reconnect-product-telemetry.test.ts",
"src/renderer/src/hooks/direct-ssh-worktree-refresh-scheduler.test.ts",
"src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts"
"src/main/ssh/ssh-provider-authority.test.ts"
],
"assertionRefs": [
{
@@ -4367,7 +4360,7 @@
{
"file": "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts",
"assertions": [
"direct SSH coordinator telemetry is wired through the fail-soft product adapter"
"a filled partial SSH authority clears the target's PTY bindings once and a conflicting reply is rejected"
]
},
{
@@ -4423,36 +4416,6 @@
"generation-scope rollover invalidates every target authority before abort callbacks run",
"every registered old-scope provider request aborts exactly once"
]
},
{
"file": "src/renderer/src/hooks/direct-ssh-worktree-refresh-scheduler.test.ts",
"assertions": [
"coordinator-owned locally unsettled provider work never exceeds five and target lanes round-robin",
"the first timeout remains retrying and reports queue wait separately from provider execution",
"cancel debt admits at most two replacements and terminally distinguishes budget exhaustion"
]
},
{
"file": "src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts",
"assertions": [
"lineage and token creation remain blocked until a timed-out repo retry settles",
"exact overlapping preparation emits one aggregate with a join count",
"telemetry callback failure cannot affect reconnect completion"
]
},
{
"file": "src/shared/direct-ssh-reconnect-telemetry-schema.test.ts",
"assertions": [
"timeout, rejection, cancellation, and stale outcomes have independent fields",
"target, repo, host, path, label, user, request, lease, terminal, and raw-error fields are rejected"
]
},
{
"file": "src/renderer/src/lib/direct-ssh-reconnect-product-telemetry.test.ts",
"assertions": [
"one coordinator aggregate maps to one typed product event with queue and provider percentiles",
"adapter failure is swallowed before it can reach recovery"
]
}
],
"evidenceRuns": [
@@ -4475,13 +4438,13 @@
"summary": "Three main-process catalog and authority files passed with 118 tests, including contradictory catalog provenance rejection and all-target generation-scope revocation."
},
{
"date": "2026-07-27",
"date": "2026-10-11",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/direct-ssh-reconnect-telemetry-schema.test.ts src/renderer/src/lib/direct-ssh-reconnect-product-telemetry.test.ts src/renderer/src/hooks/direct-ssh-worktree-refresh-scheduler.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts --reporter=dot",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 3.45,
"summary": "Five focused direct SSH scheduler, coordinator, telemetry, and hook-wiring files passed with 130 tests."
"durationSeconds": 1.21,
"summary": "The direct SSH state-bridge wiring file passed with 6 tests after the reconnect coordinator, scheduler and telemetry were deleted."
},
{
"date": "2026-08-26",
@@ -4512,11 +4475,11 @@
},
"redGreenEvidence": {
"status": "partial",
"evidence": "The watcher count assertions fail against the old single-signal classifier because linked index events call notifyWorktreesChanged. The direct SSH tests encode failures for unbounded admission, early lineage release, merged timeout/rejection/cancel/stale results, identifier-bearing telemetry, duplicate joined events, and telemetry exceptions, but no intentional-break artifact was run or claimed; saved red/green artifacts are still needed before blocking promotion."
"evidence": "The watcher count assertions fail against the old single-signal classifier because linked index events call notifyWorktreesChanged. No intentional-break artifact was run or claimed; saved red/green artifacts are still needed before blocking promotion."
},
"performanceBudget": {
"required": true,
"evidence": "Index-only bursts produce zero structural notifications, so renderer fetchWorktrees and detected-worktree cache invalidation are not reached. The non-darwin poller stays bounded and non-overlapping. Direct SSH coordinator-owned detected-worktree work is capped at five locally unsettled calls with a two-call late-work allowance; terminal finalization precedes provider discovery, and queue wait and provider execution are reported separately. A 30-second live Electron run with 2,000 external linked-status calls delivered 50 ordered input chunks and recorded zero Orca-owned git worktree spawns across six diagnostic windows; no equivalent live direct SSH fanout benchmark is claimed."
"evidence": "Index-only bursts produce zero structural notifications, so renderer fetchWorktrees and detected-worktree cache invalidation are not reached. The non-darwin poller stays bounded and non-overlapping. A 30-second live Electron run with 2,000 external linked-status calls delivered 50 ordered input chunks and recorded zero Orca-owned git worktree spawns across six diagnostic windows."
},
"promotionCriteria": [
"Run in soak for at least 100 consecutive passes or 14 days across required CI platforms.",
@@ -12958,7 +12921,7 @@
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-outcome-recovery.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-recovery-state.test.ts src/renderer/src/components/terminal-pane/TerminalRemoteRuntimeReconnectBanner.test.tsx src/renderer/src/components/terminal-pane/terminal-remote-runtime-recovery-ui-state.test.ts src/shared/remote-runtime-socket-liveness.test.ts src/shared/remote-runtime-shared-control-connection.test.ts src/shared/remote-runtime-shared-control-socket-generation.test.ts src/shared/remote-runtime-client-error-classification.test.ts src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts src/main/runtime/rpc/methods/terminal-create-idempotency.test.ts src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/repos-remote.test.ts src/main/ipc/ssh.test.ts src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts src/main/ipc/worktrees-lineage-hydration.test.ts src/main/runtime/public-ssh-state.test.ts src/main/ssh/ssh-connection-manager.test.ts src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-provider-authority.test.ts src/preload/ssh-authority-forwarding.test.ts src/renderer/src/runtime/runtime-client-events.test.ts src/renderer/src/runtime/runtime-environment-ssh-state.test.ts src/shared/ssh-retained-payload-admission.test.ts --reporter=dot",
"pnpm exec electron-vite build --mode e2e",
"pnpm run build:web-from-renderer",
@@ -13001,8 +12964,6 @@
"src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts",
"src/renderer/src/store/slices/terminals-hydration.test.ts",
"src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts",
"src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts",
"src/renderer/src/hooks/direct-ssh-host-hydration.test.ts",
"src/renderer/src/hooks/direct-ssh-state-routing.test.ts",
"src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts",
"src/main/ipc/repos-remote.test.ts",
@@ -13270,20 +13231,6 @@
"a provider result becomes stale when same-ID repo ownership turns malformed or contradictory during the await"
]
},
{
"file": "src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts",
"assertions": [
"terminal invalidation and retry run synchronously before provider preparation",
"hydrated terminal finalization and same-authority correction are current-authority fenced",
"rapid authority rotation keeps immediate terminal checks while damping full preparation"
]
},
{
"file": "src/renderer/src/hooks/direct-ssh-host-hydration.test.ts",
"assertions": [
"exact-host catalog and lineage hydration preserves sibling SSH, local, runtime, ambiguous, and contradictory rows"
]
},
{
"file": "src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts",
"assertions": [
@@ -13317,7 +13264,7 @@
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 10.7,
"summary": "Fourteen direct-SSH files and 155 tests passed, including 32-arrival placement and readiness bursts with one listener/timer maximum, immediate supersession and stop cleanup, latest-only hydration, stale-push fencing, pending-capture and in-flight-write upload exclusion, and existing retry, authority, hydration, split-pane, and detach contracts."
@@ -13353,7 +13300,7 @@
"date": "2026-07-28",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts src/renderer/src/hooks/direct-ssh-host-hydration.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts src/renderer/src/store/slices/terminals-hydration.test.ts src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts src/renderer/src/hooks/direct-ssh-state-routing.test.ts src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts --reporter=dot",
"result": "passed",
"durationSeconds": 15.8,
"summary": "Twelve direct SSH files and 647 tests passed, including exact lease revalidation after asynchronous SSH preparation, primary-exit continuation gaps, pending-only and live null-PTY two-sided split-detach authority, delayed post-success sibling admission, stale-authority provider retirement, late ownership-provenance rejection, and deleted-tab ledger pruning."
-9
View File
@@ -2841,7 +2841,6 @@
"src/main/ipc/worktrees/listing/detected-worktree-scan-listing-distro.test.ts": 559,
"src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts": 68,
"src/main/ipc/worktrees/listing/register-sparse-checkout-cache-invalidation.test.ts": 197,
"src/main/ipc/worktrees/metadata/workspace-lineage-candidate-scan.test.ts": 80,
"src/main/ipc/zcode-plan-credentials.test.ts": 58,
"src/main/issue-command-ignore.test.ts": 620,
"src/main/jcode/daemon-prewarm.test.ts": 54,
@@ -9019,14 +9018,8 @@
"src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts": 1678,
"src/renderer/src/hooks/composer-state/provider-runtime-sync.test.ts": 1661,
"src/renderer/src/hooks/composer-state/quick-creation-request.test.ts": 110,
"src/renderer/src/hooks/direct-ssh-host-catalog-noop.test.ts": 1364,
"src/renderer/src/hooks/direct-ssh-host-hydration.test.ts": 79,
"src/renderer/src/hooks/direct-ssh-reconnect-coordinator.test.ts": 130,
"src/renderer/src/hooks/direct-ssh-reconnect-rollout.test.ts": 49,
"src/renderer/src/hooks/direct-ssh-reconnect-tokens.test.ts": 41,
"src/renderer/src/hooks/direct-ssh-runtime-wake-isolation.test.tsx": 1605,
"src/renderer/src/hooks/direct-ssh-state-routing.test.ts": 183,
"src/renderer/src/hooks/direct-ssh-worktree-refresh-scheduler.test.ts": 72,
"src/renderer/src/hooks/editor-external-watch-fallback-explorer.test.ts": 355,
"src/renderer/src/hooks/editor-external-watch-selected-host.test.ts": 324,
"src/renderer/src/hooks/editor-runtime-file-watch.test.ts": 55,
@@ -9298,7 +9291,6 @@
"src/renderer/src/lib/desktop-window-chrome.test.ts": 56,
"src/renderer/src/lib/diff-comment-compat.test.ts": 50,
"src/renderer/src/lib/diff-comments-format.test.ts": 47,
"src/renderer/src/lib/direct-ssh-reconnect-product-telemetry.test.ts": 56,
"src/renderer/src/lib/direct-ssh-target-scope.test.ts": 70,
"src/renderer/src/lib/doc-preview-grants.test.ts": 370,
"src/renderer/src/lib/document-theme.test.ts": 55,
@@ -10835,7 +10827,6 @@
"src/shared/cross-platform-path.test.ts": 55,
"src/shared/currency-format.test.ts": 47,
"src/shared/daemon-adoption-telemetry.test.ts": 349,
"src/shared/direct-ssh-reconnect-telemetry-schema.test.ts": 279,
"src/shared/doc-preview-file-access.test.ts": 93,
"src/shared/doc-preview-scheme.test.ts": 44,
"src/shared/draft-paste-ready-scanner-grok-trace-replay.test.ts": 75,
@@ -30,13 +30,8 @@ vi.mock('../ssh/ssh-target-registry', () => moduleMocks.sshModuleMock(reposMocks
import { registerRepoHandlers } from './repos'
import { clearGitCapabilityStateForTests } from '../git/git-capability-state'
import {
getSshProviderAuthority,
resetSshProviderAuthorities,
rotateSshProviderAuthority
} from '../ssh/ssh-provider-authority'
import { resetSshProviderAuthorities } from '../ssh/ssh-provider-authority'
import { getGitRepoRoot, isGitRepo } from '../git/repo'
import { toSshExecutionHostId } from '../../shared/execution-host'
import { createRepoHandlerHarness, resetProjectGroupMocks } from './repos-remote-test-harness'
const { handleMock, mockStore } = reposMocks
@@ -65,128 +60,6 @@ describe('projectGroups IPC validation', () => {
expect(mockStore.createProjectGroup).not.toHaveBeenCalled()
})
it('returns an immutable repo catalog for exactly one execution host', async () => {
const localRepo = {
id: 'duplicate',
path: '/local/repo',
displayName: 'local',
badgeColor: '#000',
addedAt: 0
}
const sshRepo = {
id: 'duplicate',
path: '/remote/repo',
displayName: 'remote',
badgeColor: '#000',
addedAt: 0,
connectionId: 'conn-1'
}
const runtimeRepo = {
id: 'duplicate',
path: '/runtime/repo',
displayName: 'runtime',
badgeColor: '#000',
addedAt: 0,
executionHostId: 'runtime:environment-a'
}
mockStore.getRepos.mockReturnValue([localRepo, sshRepo, runtimeRepo])
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: toSshExecutionHostId('conn-1'),
expectedAuthority: getSshProviderAuthority('conn-1')
})
).resolves.toMatchObject({
authoritative: true,
authority: {
kind: 'direct-ssh',
executionHostId: 'ssh:conn-1',
targetId: 'conn-1'
},
repos: [sshRepo]
})
const local = await handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: 'local'
})
expect(local).toMatchObject({ authoritative: true, repos: [localRepo] })
expect((local as { repos: object[] }).repos[0]).not.toBe(localRepo)
})
it('rejects repo catalogs whose execution host contradicts their SSH connection', async () => {
const baseRepo = {
id: 'repo-1',
path: '/repo',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0
}
mockStore.getRepos.mockReturnValue([
{
...baseRepo,
connectionId: 'conn-1',
executionHostId: 'local'
}
])
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: 'local'
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: toSshExecutionHostId('conn-1'),
expectedAuthority: getSshProviderAuthority('conn-1')
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
mockStore.getRepos.mockReturnValue([
{
...baseRepo,
connectionId: 'conn-1',
executionHostId: toSshExecutionHostId('conn-2')
}
])
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: toSshExecutionHostId('conn-1'),
expectedAuthority: getSshProviderAuthority('conn-1')
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
})
it('rejects runtime, partial, mismatched, and stale catalog authority', async () => {
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: 'runtime:environment-a'
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: toSshExecutionHostId('conn-1')
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
await expect(
handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: toSshExecutionHostId('conn-1'),
expectedAuthority: {
...getSshProviderAuthority('other-target'),
targetId: 'other-target'
}
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
const expectedAuthority = getSshProviderAuthority('conn-1')
const pending = handlers.get('repos:listForExecutionHost')!(null, {
executionHostId: toSshExecutionHostId('conn-1'),
expectedAuthority
})
rotateSshProviderAuthority('conn-1')
await expect(pending).resolves.toMatchObject({ authoritative: false, reason: 'stale' })
})
it('rejects malformed local project group update arguments before persistence', () => {
expect(() =>
handlers.get('projectGroups:update')!(null, {
-1
View File
@@ -22,7 +22,6 @@ export function registerRepoHandlers(
): void {
// Remove previously registered handlers so we can re-register on macOS app re-activation (new window).
ipcMain.removeHandler('repos:list')
ipcMain.removeHandler('repos:listForExecutionHost')
ipcMain.removeHandler('repos:add')
ipcMain.removeHandler('repos:removeForHost')
ipcMain.removeHandler('repos:reorder')
@@ -1,111 +0,0 @@
import type { Store } from '../../persistence'
import type { Repo } from '../../../shared/repo-types'
import type {
HostRepoCatalogSnapshot,
ListReposForExecutionHostArgs
} from '../../../shared/host-repo-catalog-contract'
import {
getRepoExecutionHostId,
LOCAL_EXECUTION_HOST_ID,
parseExecutionHostId
} from '../../../shared/execution-host'
import { isAdmissibleDirectSshAuthority } from '../../../shared/ssh-retained-payload-admission'
import { isCurrentSshProviderAuthority } from '../../ssh/ssh-provider-authority'
import { getSshGitProvider } from '../../providers/ssh-git-dispatch'
import { visibleRepos } from '../../ssh/orcad-retained-source'
function hasValidCatalogSshAuthority(
args: ListReposForExecutionHostArgs
): args is Extract<ListReposForExecutionHostArgs, { expectedAuthority: unknown }> {
if (!('expectedAuthority' in args)) {
return false
}
return isAdmissibleDirectSshAuthority(args.expectedAuthority)
}
function repoHostContradictsConnection(repo: Repo): boolean {
if (!repo.executionHostId || !repo.connectionId) {
return false
}
const explicitHost = parseExecutionHostId(repo.executionHostId)
return explicitHost?.kind !== 'ssh' || explicitHost.targetId !== repo.connectionId
}
function getConsistentRepoCatalogForHost(
repos: readonly Repo[],
host: NonNullable<ReturnType<typeof parseExecutionHostId>>
): Repo[] | null {
const hasContradiction = repos.some(
(repo) =>
repoHostContradictsConnection(repo) &&
(getRepoExecutionHostId(repo) === host.id ||
(host.kind === 'ssh' && repo.connectionId === host.targetId))
)
return hasContradiction ? null : repos.filter((repo) => getRepoExecutionHostId(repo) === host.id)
}
export async function listReposForExecutionHost(
store: Store,
args: ListReposForExecutionHostArgs
): Promise<HostRepoCatalogSnapshot> {
const parsedHost = parseExecutionHostId(args?.executionHostId)
const rejected = (
reason: Extract<HostRepoCatalogSnapshot, { authoritative: false }>['reason']
): HostRepoCatalogSnapshot => ({
authoritative: false,
executionHostId: args.executionHostId,
reason
})
if (!parsedHost || parsedHost.kind === 'runtime') {
return rejected('rejected')
}
if (parsedHost.kind === 'local') {
if ('expectedAuthority' in args) {
return rejected('rejected')
}
const repos = getConsistentRepoCatalogForHost(visibleRepos(store), parsedHost)
if (!repos) {
return rejected('rejected')
}
return {
authoritative: true,
authority: { kind: 'local', executionHostId: LOCAL_EXECUTION_HOST_ID },
repos: structuredClone(repos)
}
}
if (
!hasValidCatalogSshAuthority(args) ||
args.expectedAuthority.targetId !== parsedHost.targetId
) {
return rejected('rejected')
}
const authority = { ...args.expectedAuthority }
if (!isCurrentSshProviderAuthority(authority)) {
return rejected('stale')
}
const provider = getSshGitProvider(parsedHost.targetId)
if (!provider) {
return rejected('unavailable')
}
const matchingRepos = getConsistentRepoCatalogForHost(visibleRepos(store), parsedHost)
if (!matchingRepos) {
return rejected('rejected')
}
const repos = structuredClone(matchingRepos)
await Promise.resolve()
if (
getSshGitProvider(parsedHost.targetId) !== provider ||
!isCurrentSshProviderAuthority(authority)
) {
return rejected('stale')
}
return {
authoritative: true,
authority: {
kind: 'direct-ssh',
executionHostId: parsedHost.id,
...authority
},
repos
}
}
@@ -2,17 +2,12 @@ import type { BrowserWindow } from 'electron'
import { ipcMain } from 'electron'
import type { Store } from '../../persistence'
import type { Project, ProjectUpdateArgs } from '../../../shared/project-types'
import type {
HostRepoCatalogSnapshot,
ListReposForExecutionHostArgs
} from '../../../shared/host-repo-catalog-contract'
import { normalizeExecutionHostId } from '../../../shared/execution-host'
import { enrichRepoGitUsernames } from '../../repo-git-username-enrichment'
import { enrichMissingRepoGitRemoteIdentities } from '../../repo-git-remote-identity-enrichment'
import { invalidateAuthorizedRootsCache } from '../registered-worktree-roots-cache'
import { notifyReposChanged } from './repos-changed-notification'
import { ProjectUpdateIpcArgs, parseProjectGroupIpcArgs } from './repo-ipc-arg-schemas'
import { listReposForExecutionHost } from './host-repo-catalog-snapshot'
import { visibleRepos } from '../../ssh/orcad-retained-source'
export function registerRepoCatalogHandlers(mainWindow: BrowserWindow, store: Store): void {
@@ -27,12 +22,6 @@ export function registerRepoCatalogHandlers(mainWindow: BrowserWindow, store: St
return visibleRepos(store)
})
ipcMain.handle(
'repos:listForExecutionHost',
(_event, args: ListReposForExecutionHostArgs): Promise<HostRepoCatalogSnapshot> =>
listReposForExecutionHost(store, args)
)
ipcMain.handle('projects:list', () => {
enrichMissingRepoGitRemoteIdentities(store, { onChanged: broadcastReposChanged })
return store.getProjects()
@@ -150,7 +150,6 @@ describe('recordWorkspaceLineageForCreatedWorktree', () => {
expect(store.setWorktreeLineage).not.toHaveBeenCalled()
expect(result.lineage).toBeNull()
// A persisted cross-host row makes filterLineageForHost return null for the entire host.
expect(store.setWorkspaceLineage).not.toHaveBeenCalled()
expect(result.workspaceLineage).toBeNull()
}
+1 -3
View File
@@ -285,9 +285,7 @@ export function recordWorkspaceLineageForCreatedWorktree(
}
}
// Why: persisting a workspace row for an out-of-boundary worktree parent poisons the whole host —
// `filterLineageForHost` returns null for any owned row whose child and parent hosts differ, so
// every nesting on that host stops hydrating, and the row survives restarts.
// Why: a workspace row for an out-of-boundary worktree parent is a cross-host link that survives restarts.
if (parentOutsideLineageBoundary) {
return NO_CREATED_WORKTREE_LINEAGE
}
@@ -36,7 +36,6 @@ const WORKTREE_HANDLER_CHANNELS = [
'worktrees:forceDeletePreservedBranch',
'worktrees:updateMeta',
'worktrees:listLineage',
'worktrees:listLineageForHost',
'worktrees:updateLineage',
'worktrees:persistSortOrder',
'worktrees:getBranchRenameFailureOutput',
@@ -1,13 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import type { Worktree } from '../../shared/worktree/types'
import { toSshExecutionHostId } from '../../shared/execution-host'
import { LINEAGE_HYDRATION_TIMEOUT_MS } from './worktrees/metadata/host-lineage-listing'
import { getSshProviderAuthority, rotateSshProviderAuthority } from '../ssh/ssh-provider-authority'
import { listWorktreesMock, getSshGitProviderMock } from './worktrees-test-module-mocks'
import { handlers, ipcEvent, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
import { makeWorktreeMeta } from './worktrees-test-fixtures'
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
vi.mock('electron', async () =>
(await import('./worktrees-test-module-mocks')).electronModuleMock()
@@ -92,156 +88,8 @@ vi.mock('../runtime/worktree-teardown', async () =>
vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock())
describe('registerWorktreeHandlers', () => {
let runtimeStub: WorktreeRuntimeStub
beforeEach(() => {
runtimeStub = setupWorktreeHandlers()
})
it('answers SSH lineage as unavailable without reading this machine', async () => {
const result = await handlers['worktrees:listLineageForHost'](ipcEvent, {
executionHostId: toSshExecutionHostId('target-a'),
expectedAuthority: getSshProviderAuthority('target-a')
})
expect(result).toEqual({
authoritative: false,
executionHostId: 'ssh:target-a',
reason: 'unavailable'
})
expect(runtimeStub.hydrateInferredWorktreeLineage).not.toHaveBeenCalled()
expect(store.removeWorktreeLineage).not.toHaveBeenCalled()
})
it('snapshots lineage catalogs once and memoizes repeated owner resolution', async () => {
const worktreeIds = Array.from(
{ length: 101 },
(_, index) => `repo-1::/workspace/repo-${index}`
)
const lineage = Object.fromEntries(
worktreeIds.slice(1).map((worktreeId, index) => [
worktreeId,
{
worktreeId,
worktreeInstanceId: `child-${index}`,
parentWorktreeId: worktreeIds[index],
parentWorktreeInstanceId: `parent-${index}`,
origin: 'cli',
capture: { source: 'cwd-context', confidence: 'inferred' },
createdAt: index
}
])
)
store.getAllWorktreeLineage.mockReturnValue(lineage)
store.getRepos.mockClear()
store.getFolderWorkspaces.mockClear()
store.getProjectGroups.mockClear()
store.getWorktreeMeta.mockClear()
const result = await handlers['worktrees:listLineageForHost'](ipcEvent, {
executionHostId: 'local'
})
expect(result).toMatchObject({ authoritative: true })
expect(
Object.keys((result as { worktreeLineageById: Record<string, unknown> }).worktreeLineageById)
).toHaveLength(100)
expect(store.getRepos).toHaveBeenCalledOnce()
expect(store.getFolderWorkspaces).toHaveBeenCalledOnce()
expect(store.getProjectGroups).toHaveBeenCalledOnce()
expect(store.getWorktreeMeta).toHaveBeenCalledTimes(101)
})
it('preserves ambiguous legacy lineage instead of guessing among duplicate repo owners', async () => {
const child = 'duplicate::/child'
const parent = 'duplicate::/parent'
store.getRepos.mockReturnValue([
{
id: 'duplicate',
path: '/local',
displayName: 'local',
badgeColor: '#000',
addedAt: 0
},
{
id: 'duplicate',
path: '/remote',
displayName: 'remote',
badgeColor: '#000',
addedAt: 0,
connectionId: 'target-a'
}
])
store.getAllWorktreeLineage.mockReturnValue({
[child]: {
worktreeId: child,
worktreeInstanceId: 'child',
parentWorktreeId: parent,
parentWorktreeInstanceId: 'parent',
origin: 'cli',
capture: { source: 'cwd-context', confidence: 'inferred' },
createdAt: 1
}
})
await expect(
handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: 'local' })
).resolves.toEqual({
authoritative: false,
executionHostId: 'local',
reason: 'ambiguous-owner'
})
expect(store.removeWorktreeLineage).not.toHaveBeenCalled()
})
it('rejects runtime lineage reads and stale SSH authority', async () => {
await expect(
handlers['worktrees:listLineageForHost'](ipcEvent, {
executionHostId: 'runtime:environment-a'
})
).resolves.toMatchObject({ authoritative: false, reason: 'rejected' })
expect(runtimeStub.hydrateInferredWorktreeLineage).not.toHaveBeenCalled()
const authority = getSshProviderAuthority('target-a')
rotateSshProviderAuthority('target-a')
await expect(
handlers['worktrees:listLineageForHost'](ipcEvent, {
executionHostId: toSshExecutionHostId('target-a'),
expectedAuthority: authority
})
).resolves.toMatchObject({ authoritative: false, reason: 'stale' })
expect(store.removeWorktreeLineage).not.toHaveBeenCalled()
})
it('bounds noncooperative lineage hydration and permits a later read', async () => {
vi.useFakeTimers()
try {
runtimeStub.hydrateInferredWorktreeLineage.mockReturnValue(new Promise<void>(() => {}))
const pending = handlers['worktrees:listLineageForHost'](ipcEvent, {
executionHostId: 'local'
})
await vi.advanceTimersByTimeAsync(LINEAGE_HYDRATION_TIMEOUT_MS - 1)
let settled = false
void Promise.resolve(pending).finally(() => {
settled = true
})
await Promise.resolve()
expect(settled).toBe(false)
await vi.advanceTimersByTimeAsync(1)
await expect(pending).resolves.toMatchObject({
authoritative: false,
reason: 'unavailable'
})
expect(vi.getTimerCount()).toBe(0)
runtimeStub.hydrateInferredWorktreeLineage.mockResolvedValue(undefined)
await expect(
handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: 'local' })
).resolves.toMatchObject({ authoritative: true })
} finally {
vi.useRealTimers()
}
setupWorktreeHandlers()
})
it('hydrates detected worktrees with instance-validated legacy lineage after an update', async () => {
-1
View File
@@ -47,7 +47,6 @@ const WORKTREE_HANDLER_CHANNELS = [
'worktrees:forceDeletePreservedBranch',
'worktrees:updateMeta',
'worktrees:listLineage',
'worktrees:listLineageForHost',
'worktrees:updateLineage',
'worktrees:persistSortOrder',
'worktrees:getBranchRenameFailureOutput',
@@ -14,7 +14,6 @@ import { isFolderRepo } from '../../../../shared/repo-kind'
import { projectResolvedWorktreeLineage } from '../../../../shared/resolved-worktree-lineage'
import type { DirectSshDetectedWorktreeRequest } from '../../../../shared/detected-worktree-provider-contract'
import { isAdmissibleDirectSshAuthority } from '../../../../shared/ssh-retained-payload-admission'
import type { ListDesktopLineageForHostArgs } from '../../../../shared/host-lineage-contract'
import {
buildDetectedGitWorktrees,
createSshWorktreeMetaIndex,
@@ -206,12 +205,3 @@ export function hasValidDirectSshAuthority(
): args is DirectSshDetectedWorktreeRequest {
return isAdmissibleDirectSshAuthority(args.expectedAuthority)
}
export function hasValidLineageSshAuthority(
args: ListDesktopLineageForHostArgs
): args is Extract<ListDesktopLineageForHostArgs, { expectedAuthority: unknown }> {
if (!('expectedAuthority' in args)) {
return false
}
return isAdmissibleDirectSshAuthority(args.expectedAuthority)
}
@@ -1,77 +0,0 @@
import type { OrcaRuntimeService } from '../../../runtime/orca-runtime'
import type { Store } from '../../../persistence/loading-store/store'
import type {
ListDesktopLineageForHostArgs,
HostLineageSnapshot
} from '../../../../shared/host-lineage-contract'
import { parseExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { isCurrentSshProviderAuthority } from '../../../ssh/ssh-provider-authority'
import { hasValidLineageSshAuthority } from '../listing/detected-provider-listing'
import { filterLineageForHost } from './workspace-lineage-filtering'
export const LINEAGE_HYDRATION_TIMEOUT_MS = 5_000
export async function hydrateLineageWithinDeadline(runtime: OrcaRuntimeService): Promise<boolean> {
let timeout: ReturnType<typeof setTimeout> | undefined
const hydration = Promise.resolve()
.then(() => runtime.hydrateInferredWorktreeLineage())
.then(
() => true,
() => false
)
const deadline = new Promise<false>((resolve) => {
timeout = setTimeout(() => resolve(false), LINEAGE_HYDRATION_TIMEOUT_MS)
})
try {
return await Promise.race([hydration, deadline])
} finally {
if (timeout) {
clearTimeout(timeout)
}
}
}
export async function listDesktopLineageForHost(
store: Store,
runtime: OrcaRuntimeService,
args: ListDesktopLineageForHostArgs
): Promise<HostLineageSnapshot> {
const parsedHost = parseExecutionHostId(args?.executionHostId)
const rejected = (
reason: Extract<HostLineageSnapshot, { authoritative: false }>['reason']
): HostLineageSnapshot => ({
authoritative: false,
executionHostId: args.executionHostId,
reason
})
if (!parsedHost || parsedHost.kind === 'runtime') {
return rejected('rejected')
}
if (parsedHost.kind === 'ssh') {
if (
!hasValidLineageSshAuthority(args) ||
args.expectedAuthority.targetId !== parsedHost.targetId
) {
return rejected('rejected')
}
// An `ssh:` host is not served here, so its lineage is never answered from this machine.
return rejected(
isCurrentSshProviderAuthority({ ...args.expectedAuthority }) ? 'unavailable' : 'stale'
)
}
if ('expectedAuthority' in args) {
return rejected('rejected')
}
if (!(await hydrateLineageWithinDeadline(runtime))) {
return rejected('unavailable')
}
const lineage = filterLineageForHost(store, parsedHost.id)
if (!lineage) {
return rejected('ambiguous-owner')
}
return {
authoritative: true,
authority: { kind: 'local', executionHostId: LOCAL_EXECUTION_HOST_ID },
...lineage
}
}
@@ -1,123 +0,0 @@
import { parseExecutionHostId } from '../../../../shared/execution-host'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { Store } from '../../../persistence/loading-store/store'
import type { Repo } from '../../../../shared/repo-types'
import { parseWorktreeId } from '../../worktree-logic'
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../../../shared/project-group-types'
import { resolveRepoOwnershipEvidence } from '../listing/worktree-host-ownership'
export type LineageOwner =
| { status: 'owned'; hostId: ExecutionHostId }
| { status: 'ambiguous' | 'contradictory' | 'runtime' }
export type LineageFolder = FolderWorkspace
export type LineageGroup = ProjectGroup
export type LineageResolutionContext = {
store: Store
repos: Repo[]
groups: LineageGroup[]
reposById: Map<string, Repo[]>
foldersById: Map<string, LineageFolder[]>
groupsById: Map<string, LineageGroup[]>
groupSubtreeIdsByRoot: Map<string, Set<string>>
worktreeOwners: Map<string, LineageOwner>
folderOwners: Map<string, LineageOwner>
workspaceOwners: Map<string, LineageOwner>
}
export function indexLineageEntriesById<T extends { id: string }>(
entries: readonly T[]
): Map<string, T[]> {
const index = new Map<string, T[]>()
for (const entry of entries) {
const matching = index.get(entry.id) ?? []
matching.push(entry)
index.set(entry.id, matching)
}
return index
}
export function createLineageResolutionContext(store: Store): LineageResolutionContext {
const repos = store.getRepos()
const folders = store.getFolderWorkspaces()
const groups = store.getProjectGroups()
return {
store,
repos,
groups,
reposById: indexLineageEntriesById(repos),
foldersById: indexLineageEntriesById(folders),
groupsById: indexLineageEntriesById(groups),
groupSubtreeIdsByRoot: new Map(),
worktreeOwners: new Map(),
folderOwners: new Map(),
workspaceOwners: new Map()
}
}
export function resolveRepoLineageOwner(repo: Repo): LineageOwner {
const owner = resolveRepoOwnershipEvidence(repo)
if (owner.status === 'malformed') {
return { status: 'ambiguous' }
}
if (owner.status === 'contradictory') {
return { status: 'contradictory' }
}
return parseExecutionHostId(owner.hostId)?.kind === 'runtime' ? { status: 'runtime' } : owner
}
export function resolveWorktreeLineageOwner(
context: LineageResolutionContext,
worktreeId: string
): LineageOwner {
const cached = context.worktreeOwners.get(worktreeId)
if (cached) {
return cached
}
const remember = (owner: LineageOwner): LineageOwner => {
context.worktreeOwners.set(worktreeId, owner)
return owner
}
let repoId: string
try {
repoId = parseWorktreeId(worktreeId).repoId
} catch {
return remember({ status: 'ambiguous' })
}
const repos = context.reposById.get(repoId) ?? []
const meta = context.store.getWorktreeMeta(worktreeId)
const runtimeOwnerEnvironmentId = (
meta as (WorktreeMeta & { runtimeOwnerEnvironmentId?: string }) | undefined
)?.runtimeOwnerEnvironmentId?.trim()
if (runtimeOwnerEnvironmentId) {
return remember({ status: 'runtime' })
}
if (meta?.hostId) {
const explicitHost = parseExecutionHostId(meta.hostId)
if (!explicitHost) {
return remember({ status: 'ambiguous' })
}
if (explicitHost.kind === 'runtime') {
return remember({ status: 'runtime' })
}
const matchingRepos = repos.filter((repo) => {
const owner = resolveRepoLineageOwner(repo)
return owner.status === 'owned' && owner.hostId === explicitHost.id
})
if (matchingRepos.length === 1) {
return remember({ status: 'owned', hostId: explicitHost.id })
}
return remember(
matchingRepos.length > 1
? { status: 'ambiguous' }
: { status: repos.length > 0 ? 'contradictory' : 'ambiguous' }
)
}
if (repos.length !== 1) {
return remember({ status: 'ambiguous' })
}
return remember(resolveRepoLineageOwner(repos[0]))
}
@@ -4,17 +4,12 @@ import { displayNameUpdatePinsLabel } from '../../../../shared/worktree/display-
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { stripOrcaProvenanceMetaUpdates } from '../../../worktree-removal-safety'
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id'
import type {
ListDesktopLineageForHostArgs,
HostLineageSnapshot
} from '../../../../shared/host-lineage-contract'
import { notifyWorktreesChanged } from '../../worktree-remote'
import { parseWorktreeId } from '../../worktree-logic'
import { planWorktreeSortOrderUpdates } from '../../../../shared/worktree/sort-order-update'
import { readBranchRenameFailureOutputForDisplay } from '../../../agent-hooks/branch-rename-failure-output'
import { normalizeLinkedWorkItemFields } from '../ipc-context-schemas'
import { isRemovedFolderWorkspaceInstance } from '../../../folder-workspace-meta-presence'
import { listDesktopLineageForHost } from './host-lineage-listing'
import type { WorktreeIpcContext } from '../worktree-ipc-context'
export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): void {
@@ -72,12 +67,6 @@ export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): v
}
})
ipcMain.handle(
'worktrees:listLineageForHost',
(_event, args: ListDesktopLineageForHostArgs): Promise<HostLineageSnapshot> =>
listDesktopLineageForHost(store, runtime, args)
)
ipcMain.handle(
'worktrees:updateLineage',
async (_event, args: { worktreeId: string; parentWorktreeId?: string; noParent?: boolean }) => {
@@ -1,158 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../../shared/repo-types'
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../../../shared/project-group-types'
import { createLineageResolutionContext } from './lineage-owner-resolution'
import { getFolderLineageCandidateRepos } from './workspace-lineage-filtering'
vi.mock('../../worktree-logic', () => ({ parseWorktreeId: vi.fn() }))
function repo(id: string, fields: Partial<Repo> = {}): Repo {
return { id, path: `/root/${id}`, displayName: id, badgeColor: '#000', addedAt: 0, ...fields }
}
function group(id = 'group', fields: Partial<ProjectGroup> = {}): ProjectGroup {
return {
id,
name: id,
parentPath: null,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 0,
updatedAt: 0,
...fields
}
}
function folder(fields: Partial<FolderWorkspace> = {}): FolderWorkspace {
return {
id: 'folder',
projectGroupId: 'group',
name: 'folder',
folderPath: '/root',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0,
...fields
}
}
function context(repos: Repo[], groups = [group()]) {
const store = {
getRepos: () => repos,
getFolderWorkspaces: () => [],
getProjectGroups: () => groups
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Context construction only reads these three store catalogs.
return createLineageResolutionContext(store as never)
}
describe('folder lineage candidate lookup', () => {
it('reads grouped target membership once across a large path-candidate list', () => {
let targetReads = 0
const grouped = Array.from({ length: 300 }, (_, index): Repo => ({
...repo(`grouped-${index}`, { projectGroupId: 'group', path: `/elsewhere/${index}` }),
get connectionId() {
targetReads += 1
return null
}
}))
const candidates = Array.from({ length: 1_000 }, (_, index) => repo(`path-${index}`))
const result = getFolderLineageCandidateRepos(context([...grouped, ...candidates]), folder())
expect(result).toEqual([...grouped, ...candidates])
expect(targetReads).toBe(300)
})
it('preserves descendant-group priority, path boundaries and local/SSH membership', () => {
const entries = [
repo('path-a', { connectionId: 'a' }),
repo('group-b', { projectGroupId: 'nested', path: '/elsewhere', connectionId: 'b' }),
repo('local-null', { connectionId: null }),
repo('group-local', { projectGroupId: 'group' }),
repo('path-b', { connectionId: 'b' }),
repo('local-absent'),
repo('sibling', { path: '/root-other/repo', connectionId: 'b' }),
repo('root', { path: '/root', connectionId: 'b' })
]
const snapshot = structuredClone(entries)
const result = getFolderLineageCandidateRepos(
context(entries, [group(), group('nested', { parentGroupId: 'group' })]),
folder()
)
expect(result.map((entry) => entry.id)).toEqual([
'group-b',
'group-local',
'local-null',
'path-b',
'local-absent',
'root'
])
expect(result[0]).toBe(entries[1])
expect(entries).toEqual(snapshot)
})
it.each([
{ folderTarget: 'a', groupTarget: 'b', expected: ['grouped', 'a'] },
{ folderTarget: null, groupTarget: 'b', expected: ['grouped', 'b'] },
{ folderTarget: undefined, groupTarget: undefined, expected: ['grouped', 'local'] }
])(
'keeps explicit target precedence: $folderTarget / $groupTarget',
({ folderTarget, groupTarget, expected }) => {
const entries = [
repo('a', { connectionId: 'a' }),
repo('b', { connectionId: 'b' }),
repo('local'),
repo('grouped', { projectGroupId: 'group', path: '/outside' })
]
expect(
getFolderLineageCandidateRepos(
context(entries, [group('group', { connectionId: groupTarget })]),
folder({ connectionId: folderTarget })
).map((entry) => entry.id)
).toEqual(expected)
}
)
it('keeps every contained target when the folder has no grouped repositories', () => {
const entries = [
repo('local'),
repo('remote', { connectionId: 'a' }),
repo('outside', { path: '/elsewhere' })
]
expect(getFolderLineageCandidateRepos(context(entries), folder())).toEqual(entries.slice(0, 2))
})
it('does not read grouped targets when there are no path candidates', () => {
let targetReads = 0
const grouped: Repo = {
...repo('grouped', { projectGroupId: 'group' }),
get connectionId() {
targetReads += 1
return 'a'
}
}
expect(getFolderLineageCandidateRepos(context([grouped]), folder())).toEqual([grouped])
expect(targetReads).toBe(0)
})
it('rebuilds target membership after a later change in the same context', () => {
const grouped = repo('grouped', { projectGroupId: 'group', connectionId: 'a' })
const a = repo('a', { connectionId: 'a' })
const b = repo('b', { connectionId: 'b' })
const current = context([grouped, a, b])
expect(getFolderLineageCandidateRepos(current, folder())).toEqual([grouped, a])
grouped.connectionId = 'b'
expect(getFolderLineageCandidateRepos(current, folder())).toEqual([grouped, b])
})
})
@@ -1,186 +0,0 @@
import type { Repo } from '../../../../shared/repo-types'
import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups'
import { isPathInsideOrEqual } from '../../../../shared/cross-platform-path'
import { parseExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
import type { Store } from '../../../persistence/loading-store/store'
import type { WorktreeLineage, WorkspaceLineage } from '../../../../shared/worktree/lineage-types'
import {
createLineageResolutionContext,
resolveRepoLineageOwner,
resolveWorktreeLineageOwner
} from './lineage-owner-resolution'
import type {
LineageFolder,
LineageOwner,
LineageResolutionContext
} from './lineage-owner-resolution'
export function getFolderLineageCandidateRepos(
context: LineageResolutionContext,
folder: LineageFolder
): Repo[] {
let groupIds = context.groupSubtreeIdsByRoot.get(folder.projectGroupId)
if (!groupIds) {
groupIds = getProjectGroupSubtreeIds(context.groups, folder.projectGroupId)
context.groupSubtreeIdsByRoot.set(folder.projectGroupId, groupIds)
}
const grouped = context.repos.filter(
(repo) => typeof repo.projectGroupId === 'string' && groupIds.has(repo.projectGroupId)
)
const pathRepos = context.repos.filter(
(repo) =>
!(typeof repo.projectGroupId === 'string' && groupIds.has(repo.projectGroupId)) &&
isPathInsideOrEqual(folder.folderPath, repo.path)
)
const group = context.groupsById.get(folder.projectGroupId)?.[0]
const connectionId = folder.connectionId ?? group?.connectionId ?? null
if (connectionId) {
return [...grouped, ...pathRepos.filter((repo) => (repo.connectionId ?? null) === connectionId)]
}
if (grouped.length === 0) {
return pathRepos
}
if (pathRepos.length === 0) {
return grouped
}
const groupedConnectionIds = new Set(grouped.map((repo) => repo.connectionId ?? null))
return [
...grouped,
...pathRepos.filter((repo) => groupedConnectionIds.has(repo.connectionId ?? null))
]
}
export function resolveFolderLineageOwner(
context: LineageResolutionContext,
folderWorkspaceId: string
): LineageOwner {
const cached = context.folderOwners.get(folderWorkspaceId)
if (cached) {
return cached
}
const remember = (owner: LineageOwner): LineageOwner => {
context.folderOwners.set(folderWorkspaceId, owner)
return owner
}
const folders = context.foldersById.get(folderWorkspaceId) ?? []
if (folders.length !== 1) {
return remember({ status: 'ambiguous' })
}
const folder = folders[0]
const groups = context.groupsById.get(folder.projectGroupId) ?? []
if (groups.length !== 1) {
return remember({ status: 'ambiguous' })
}
const group = groups[0]
const hosts = new Set<ExecutionHostId>()
if (folder.connectionId) {
hosts.add(`ssh:${encodeURIComponent(folder.connectionId)}`)
}
if (group.connectionId) {
hosts.add(`ssh:${encodeURIComponent(group.connectionId)}`)
}
if (group.executionHostId) {
const parsed = parseExecutionHostId(group.executionHostId)
if (!parsed) {
return remember({ status: 'ambiguous' })
}
hosts.add(parsed.id)
}
for (const repo of getFolderLineageCandidateRepos(context, folder)) {
const owner = resolveRepoLineageOwner(repo)
if (owner.status !== 'owned') {
return remember(owner)
}
hosts.add(owner.hostId)
}
if (hosts.size > 1) {
return remember({ status: 'contradictory' })
}
const hostId = [...hosts][0] ?? LOCAL_EXECUTION_HOST_ID
return remember(
parseExecutionHostId(hostId)?.kind === 'runtime'
? { status: 'runtime' }
: { status: 'owned', hostId }
)
}
export function resolveWorkspaceLineageOwner(
context: LineageResolutionContext,
workspaceKey: string
): LineageOwner {
const cached = context.workspaceOwners.get(workspaceKey)
if (cached) {
return cached
}
const workspace = parseWorkspaceKey(workspaceKey)
const owner = !workspace
? { status: 'ambiguous' as const }
: workspace.type === 'worktree'
? resolveWorktreeLineageOwner(context, workspace.worktreeId)
: resolveFolderLineageOwner(context, workspace.folderWorkspaceId)
context.workspaceOwners.set(workspaceKey, owner)
return owner
}
export function filterLineageForHost(
store: Store,
executionHostId: ExecutionHostId
): {
worktreeLineageById: Record<string, WorktreeLineage>
workspaceLineageByChildKey: Record<string, WorkspaceLineage>
} | null {
const context = createLineageResolutionContext(store)
const worktreeLineageById: Record<string, WorktreeLineage> = {}
const workspaceLineageByChildKey: Record<string, WorkspaceLineage> = {}
for (const [worktreeId, lineage] of Object.entries(store.getAllWorktreeLineage())) {
const child = resolveWorktreeLineageOwner(context, worktreeId)
const parent = resolveWorktreeLineageOwner(context, lineage.parentWorktreeId)
if (child.status === 'ambiguous' || child.status === 'contradictory') {
return null
}
if (parent.status === 'ambiguous' || parent.status === 'contradictory') {
return null
}
if (
child.status === 'owned' &&
parent.status === 'owned' &&
child.hostId === executionHostId &&
parent.hostId === executionHostId
) {
worktreeLineageById[worktreeId] = structuredClone(lineage)
} else if (
child.status === 'owned' &&
parent.status === 'owned' &&
child.hostId !== parent.hostId
) {
return null
}
}
for (const [childKey, lineage] of Object.entries(store.getAllWorkspaceLineage())) {
const child = resolveWorkspaceLineageOwner(context, childKey)
const parent = resolveWorkspaceLineageOwner(context, lineage.parentWorkspaceKey)
if (child.status === 'ambiguous' || child.status === 'contradictory') {
return null
}
if (parent.status === 'ambiguous' || parent.status === 'contradictory') {
return null
}
if (
child.status === 'owned' &&
parent.status === 'owned' &&
child.hostId === executionHostId &&
parent.hostId === executionHostId
) {
workspaceLineageByChildKey[childKey] = structuredClone(lineage)
} else if (
child.status === 'owned' &&
parent.status === 'owned' &&
child.hostId !== parent.hostId
) {
return null
}
}
return { worktreeLineageById, workspaceLineageByChildKey }
}
@@ -6,7 +6,6 @@ import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../shared/project-group-types'
import type { Repo } from '../../shared/repo-types'
import type { SshTarget } from '../../shared/ssh-types'
import { isAdmissibleDirectSshAuthority } from '../../shared/ssh-retained-payload-admission'
import type { Store } from '../persistence'
import { orcadMigrationCutoverFixture } from './orcad-migration-cutover-fixture'
import { writeOrcadMigrationSourceCutover } from './orcad-migration-cutover-journal'
@@ -15,13 +14,9 @@ const userData = vi.hoisted(() => ({ dir: '' }))
vi.mock('../../shared/app-environment', () => ({
getAppEnvironment: () => ({ getPath: () => userData.dir })
}))
vi.mock('./ssh-provider-authority', () => ({ isCurrentSshProviderAuthority: () => true }))
const provider = {}
vi.mock('../providers/ssh-git-dispatch', () => ({ getSshGitProvider: () => provider }))
const { isFrozenOrcadSourceSessionPartition, visibleProjectGroups } =
await import('./orcad-retained-source')
const { listReposForExecutionHost } = await import('../ipc/repos/host-repo-catalog-snapshot')
const FENCED: SshTarget = {
id: 'ssh-box',
@@ -125,16 +120,4 @@ describe('lists while a converted host keeps its source rows', () => {
rmSync(userDataPath, { recursive: true, force: true })
}
})
it('leaves the source repos out of the host catalog the SSH bridge hydrates from', async () => {
const authority: unknown = { targetId: FENCED.id, providerEpoch: 'e1', connectionGeneration: 1 }
if (!isAdmissibleDirectSshAuthority(authority)) {
throw new Error('fixture authority rejected')
}
const snapshot = await listReposForExecutionHost(catalog([FENCED]), {
executionHostId: `ssh:${FENCED.id}`,
expectedAuthority: authority
})
expect(snapshot).toMatchObject({ authoritative: true, repos: [] })
})
})
-7
View File
@@ -1,8 +1,4 @@
import { ipcRenderer } from 'electron'
import type {
HostRepoCatalogSnapshot,
ListReposForExecutionHostArgs
} from '../../shared/host-repo-catalog-contract'
import type { BaseRefDefaultResult, BaseRefSearchResult } from '../../shared/repo-types'
import type { ExecutionHostId } from '../../shared/execution-host'
import type { PreloadApi } from '../api-types'
@@ -10,9 +6,6 @@ import type { PreloadApi } from '../api-types'
export const reposApi = {
list: () => ipcRenderer.invoke('repos:list'),
listForExecutionHost: (args: ListReposForExecutionHostArgs): Promise<HostRepoCatalogSnapshot> =>
ipcRenderer.invoke('repos:listForExecutionHost', args),
add: (args) => ipcRenderer.invoke('repos:add', args),
addRemote: (args) => ipcRenderer.invoke('repos:addRemote', args),
-5
View File
@@ -1,9 +1,5 @@
import type { ExecutionHostId } from '../../shared/execution-host'
import type { GhAccountBinding } from '../../shared/github/account-binding'
import type {
HostRepoCatalogSnapshot,
ListReposForExecutionHostArgs
} from '../../shared/host-repo-catalog-contract'
import type {
NestedRepoScanResult,
ProjectGroup,
@@ -27,7 +23,6 @@ import type { BaseRefDefaultResult, BaseRefSearchResult, Repo } from '../../shar
export type RepositoryApi = {
list: () => Promise<Repo[]>
listForExecutionHost?: (args: ListReposForExecutionHostArgs) => Promise<HostRepoCatalogSnapshot>
// Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`.
add: (args: {
path: string
-5
View File
@@ -15,10 +15,6 @@ import type {
FolderWorkspacePathStatus,
FolderWorkspacePathStatusRequest
} from '../../shared/folder-workspace-path-status'
import type {
HostLineageSnapshot,
ListDesktopLineageForHostArgs
} from '../../shared/host-lineage-contract'
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import type {
WorktreeBaseStatusEvent,
@@ -131,7 +127,6 @@ export type WorktreeApi = {
lineage: Record<string, WorktreeLineage>
workspaceLineage?: Record<string, WorkspaceLineage>
}>
listLineageForHost?: (args: ListDesktopLineageForHostArgs) => Promise<HostLineageSnapshot>
updateLineage: (args: {
worktreeId: string
parentWorktreeId?: string
-7
View File
@@ -1,8 +1,4 @@
import { ipcRenderer } from 'electron'
import type {
HostLineageSnapshot,
ListDesktopLineageForHostArgs
} from '../../shared/host-lineage-contract'
import type {
WorktreeBaseStatusEvent,
WorktreeRemoteBranchConflictEvent
@@ -60,9 +56,6 @@ export const worktreesApi = {
listLineage: () => ipcRenderer.invoke('worktrees:listLineage'),
listLineageForHost: (args: ListDesktopLineageForHostArgs): Promise<HostLineageSnapshot> =>
ipcRenderer.invoke('worktrees:listLineageForHost', args),
updateLineage: (args) => ipcRenderer.invoke('worktrees:updateLineage', args),
persistSortOrder: (args) => ipcRenderer.invoke('worktrees:persistSortOrder', args),
@@ -1,162 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../shared/repo-types'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import { createTestStore } from '../store/slices/store-test-helpers'
import { createDirectSshHostHydration } from './direct-ssh-host-hydration'
const authority: DirectSshAuthority = {
targetId: 'target-a',
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture uses one opaque epoch identity throughout.
providerEpoch: 'catalog-epoch' as SshProviderEpoch,
connectionGeneration: 1
}
const remoteRepo: Repo = {
id: 'shared',
path: '/srv/repo',
displayName: 'Remote repo',
badgeColor: '',
addedAt: 1,
connectionId: authority.targetId,
executionHostId: 'ssh:target-a',
hookSettings: { mode: 'auto', scripts: { setup: 'echo ready', archive: '' } },
symlinkPaths: ['.env'],
repoIcon: { type: 'lucide', name: 'Box' }
}
function fixture() {
const localRepo: Repo = {
...remoteRepo,
path: '/local/repo',
connectionId: null,
executionHostId: 'local'
}
const folderRepo: Repo = { ...remoteRepo, id: 'folder', kind: 'folder', path: '/srv/folder' }
const initialRepos = [remoteRepo, localRepo, folderRepo]
const store = createTestStore()
store.setState({
repos: initialRepos,
manualRepoOrder: [
{ hostId: 'ssh:target-a', repoId: 'shared' },
{ hostId: 'local', repoId: 'shared' },
{ hostId: 'ssh:target-a', repoId: 'folder' }
]
})
let fetched: readonly Repo[] = [remoteRepo, folderRepo]
const hydration = createDirectSshHostHydration({
store,
listRepos: async () => ({
authoritative: true,
authority: { kind: 'direct-ssh', executionHostId: 'ssh:target-a', ...authority },
repos: structuredClone(fetched)
}),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
const listener = vi.fn()
store.subscribe(listener)
return {
store,
hydration,
listener,
initialRepos,
localRepo,
folderRepo,
setFetched: (repos: readonly Repo[]) => {
fetched = repos
}
}
}
describe('direct SSH host catalog publication', () => {
it('does not publish cloned equal repos across twenty refreshes', async () => {
const { store, hydration, listener, initialRepos } = fixture()
const initialState = store.getState()
for (let index = 0; index < 20; index++) {
await hydration.capturePreparationInput(authority, 'wake-refresh')
}
expect(store.getState().repos).toBe(initialRepos)
expect(store.getState()).toBe(initialState)
expect(listener).not.toHaveBeenCalled()
hydration.stop()
})
it('publishes a nested change once and retains equal sibling identities', async () => {
const { store, hydration, listener, localRepo, folderRepo, setFetched } = fixture()
const changed = { ...remoteRepo, symlinkPaths: ['.env', '.env.local'] }
setFetched([changed, folderRepo])
await hydration.capturePreparationInput(authority, 'wake-refresh')
expect(store.getState().repos).toEqual([changed, localRepo, folderRepo])
expect(store.getState().repos[0]).not.toBe(remoteRepo)
expect(store.getState().repos[1]).toBe(localRepo)
expect(store.getState().repos[2]).toBe(folderRepo)
expect(listener).toHaveBeenCalledOnce()
await hydration.capturePreparationInput(authority, 'wake-refresh')
expect(listener).toHaveBeenCalledOnce()
hydration.stop()
})
it('removes only target rows and retains manual order when adding a repo', async () => {
const { store, hydration, listener, localRepo, folderRepo, setFetched } = fixture()
const added = { ...remoteRepo, id: 'added', path: '/srv/added' }
setFetched([added, folderRepo])
await hydration.capturePreparationInput(authority, 'wake-refresh')
expect(store.getState().repos).toEqual([localRepo, folderRepo, added])
expect(store.getState().repos[0]).toBe(localRepo)
expect(store.getState().repos[1]).toBe(folderRepo)
expect(listener).toHaveBeenCalledOnce()
hydration.stop()
})
it('still advances catalog observation revisions when rows stay equal', async () => {
const { hydration, listener } = fixture()
const first = await hydration.capturePreparationInput(authority, 'reconnect')
const second = await hydration.capturePreparationInput(authority, 'wake-refresh')
expect(first).not.toBeNull()
expect(second?.catalogRevision).toBe(first!.catalogRevision + 1)
const repoFingerprint = JSON.stringify([
['ssh:target-a', 'folder'],
['ssh:target-a', 'shared']
])
expect(
hydration.isPreparationTokenCurrent({
authority,
catalogRevision: first!.catalogRevision,
repoFingerprint
})
).toBe(false)
expect(
hydration.isPreparationTokenCurrent({
authority,
catalogRevision: second!.catalogRevision,
repoFingerprint
})
).toBe(true)
expect(listener).not.toHaveBeenCalled()
hydration.stop()
})
// Why these two shapes: the gate must fail closed on a field no previous row carried, so a
// `Repo` field added after this gate landed still publishes without anyone revisiting the
// comparison. `'auto'` is documented as semantically identical to an absent
// `issueSourcePreference`, which makes it the sharpest case for the strict own-key policy.
it.each([
{
label: 'an optional key no previous row carried',
changed: { ...remoteRepo, issueSourcePreference: 'auto' } satisfies Repo
},
{
label: 'a nested record no previous row carried',
changed: { ...remoteRepo, upstream: { owner: 'octo', repo: 'remote' } } satisfies Repo
}
])('publishes when a fetched row gains $label', async ({ changed }) => {
const { store, hydration, listener, folderRepo, setFetched } = fixture()
setFetched([changed, folderRepo])
await hydration.capturePreparationInput(authority, 'wake-refresh')
expect(listener).toHaveBeenCalledOnce()
expect(store.getState().repos).toContainEqual(changed)
await hydration.capturePreparationInput(authority, 'wake-refresh')
expect(listener).toHaveBeenCalledOnce()
hydration.stop()
})
})
@@ -1,20 +0,0 @@
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import { resolveDirectSshTargetScope } from '../lib/direct-ssh-target-scope'
import type { AppState } from '../store/types'
export function directSshHostHydrationScope(
state: AppState,
authority: DirectSshAuthority,
catalogRevision: number
) {
return resolveDirectSshTargetScope({
targetId: authority.targetId,
catalogRevision,
repos: state.repos,
worktreesByRepo: state.worktreesByRepo,
detectedWorktreesByRepo: state.detectedWorktreesByRepo,
folderWorkspaces: state.folderWorkspaces,
projectGroups: state.projectGroups,
restoredRuntimeHostIdByWorkspaceSessionKey: state.restoredRuntimeHostIdByWorkspaceSessionKey
})
}
@@ -1,17 +0,0 @@
import type { DirectSshTargetScope } from '../lib/direct-ssh-target-scope'
import type { DirectSshPreparationInputTelemetry } from './direct-ssh-reconnect-coordinator'
export function directSshHostHydrationTelemetry(
scope: DirectSshTargetScope,
catalogOutcome: DirectSshPreparationInputTelemetry['catalogOutcome'],
catalogDurationMs: number
): DirectSshPreparationInputTelemetry {
return {
catalogOutcome,
catalogDurationMs,
gitWorktreeCount: scope.gitWorktreeIds.size,
folderWorkspaceCount: Math.max(0, scope.terminalWorkspaceKeys.size - scope.gitWorktreeIds.size),
ambiguousOwnerCount: scope.ambiguousOwnerCount,
contradictoryOwnerCount: scope.contradictoryOwnerCount
}
}
@@ -1,545 +0,0 @@
import { createStore } from 'zustand/vanilla'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { HostLineageSnapshot } from '../../../shared/host-lineage-contract'
import type { HostRepoCatalogSnapshot } from '../../../shared/host-repo-catalog-contract'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import type { Repo } from '../../../shared/repo-types'
import type { WorkspaceLineage, WorktreeLineage } from '../../../shared/worktree/lineage-types'
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../shared/workspace-scope'
import type { AppState } from '../store/types'
import { createDirectSshHostHydration } from './direct-ssh-host-hydration'
function authority(targetId = 'target-a', epoch = 'epoch-a'): DirectSshAuthority {
return {
targetId,
providerEpoch: epoch as SshProviderEpoch,
connectionGeneration: 1
}
}
function repo(id: string, targetId: string | null): Repo {
return {
id,
path: targetId ? `/${targetId}/${id}` : `/local/${id}`,
projectGroupId: null,
connectionId: targetId,
executionHostId: targetId ? (`ssh:${targetId}` as const) : ('local' as const)
} as Repo
}
function state(overrides: Record<string, unknown> = {}): AppState {
return {
repos: [],
worktreesByRepo: {},
detectedWorktreesByRepo: {},
folderWorkspaces: [],
projectGroups: [],
restoredRuntimeHostIdByWorkspaceSessionKey: {},
worktreeLineageById: {},
workspaceLineageByChildKey: {},
...overrides
} as unknown as AppState
}
function productionLineage(worktreeId: string, parentWorktreeId: string): WorktreeLineage {
return {
worktreeId,
worktreeInstanceId: `${worktreeId}-instance`,
parentWorktreeId,
parentWorktreeInstanceId: `${parentWorktreeId}-instance`,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-1',
coordinatorHandle: 'coord-1',
createdAt: 1
}
}
function productionWorkspaceLineage(childId: string, parentId: string): WorkspaceLineage {
return {
childWorkspaceKey: worktreeWorkspaceKey(childId),
childInstanceId: `${childId}-instance`,
parentWorkspaceKey: worktreeWorkspaceKey(parentId),
parentInstanceId: `${parentId}-instance`,
origin: 'orchestration',
capture: { source: 'orchestration-context', confidence: 'explicit' },
taskId: 'task-1',
coordinatorHandle: 'coord-1',
createdAt: 1
}
}
function hostSnapshot(
owner: DirectSshAuthority,
repos: ReturnType<typeof repo>[]
): HostRepoCatalogSnapshot {
return {
authoritative: true as const,
authority: {
kind: 'direct-ssh' as const,
executionHostId: `ssh:${owner.targetId}` as const,
...owner
},
repos
}
}
afterEach(() => {
vi.useRealTimers()
})
describe('createDirectSshHostHydration', () => {
it('replaces only the exact SSH host catalog when repo IDs collide', async () => {
const owner = authority()
const store = createStore<AppState>(() =>
state({
repos: [repo('shared', null), repo('shared', 'target-a'), repo('shared', 'target-b')]
})
)
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(async () => hostSnapshot(owner, [repo('shared', 'target-a')])),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
const input = await hydration.capturePreparationInput(owner, 'reconnect')
expect(input?.repoRefs).toEqual([{ repoId: 'shared', executionHostId: 'ssh:target-a' }])
expect(store.getState().repos).toEqual([
repo('shared', null),
repo('shared', 'target-b'),
repo('shared', 'target-a')
])
})
it('keeps the manual cross-host order when the host catalog is republished', async () => {
const owner = authority('box')
const store = createStore<AppState>(() =>
state({
repos: [repo('bravo', 'box'), repo('alpha', null), repo('delta', 'box')],
manualRepoOrder: [
{ hostId: 'ssh:box', repoId: 'bravo' },
{ hostId: 'local', repoId: 'alpha' },
{ hostId: 'ssh:box', repoId: 'delta' }
]
})
)
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(async () =>
hostSnapshot(owner, [repo('bravo', 'box'), repo('delta', 'box')])
),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
await hydration.capturePreparationInput(owner, 'reconnect')
expect(store.getState().repos.map((entry) => entry.id)).toEqual(['bravo', 'alpha', 'delta'])
})
// A repo added after the last drag has no overlay entry. Ranked rows must keep their order and
// the newcomer sinks to the tail, rather than the overlay being discarded for being incomplete.
it('keeps ranked rows in order and appends unranked ones when the overlay is partial', async () => {
const owner = authority('box')
const store = createStore<AppState>(() =>
state({
repos: [repo('bravo', 'box'), repo('alpha', null), repo('delta', 'box')],
manualRepoOrder: [
{ hostId: 'ssh:box', repoId: 'delta' },
{ hostId: 'local', repoId: 'alpha' }
]
})
)
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(async () =>
hostSnapshot(owner, [repo('bravo', 'box'), repo('delta', 'box')])
),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
await hydration.capturePreparationInput(owner, 'reconnect')
expect(store.getState().repos.map((entry) => entry.id)).toEqual(['delta', 'alpha', 'bravo'])
})
it('rejects a mismatched host response without publishing', async () => {
const owner = authority()
const store = createStore<AppState>(() => state({ repos: [repo('cached', 'target-a')] }))
const before = store.getState().repos
let publications = 0
store.subscribe(() => {
publications += 1
})
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(async () =>
hostSnapshot(authority('target-b', 'epoch-b'), [repo('wrong', 'target-b')])
),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
await expect(hydration.capturePreparationInput(owner, 'wake-refresh')).resolves.toBeNull()
expect(store.getState().repos).toBe(before)
expect(publications).toBe(0)
})
it('settles a non-cooperative catalog read at five seconds and clears its timer', async () => {
vi.useFakeTimers()
const owner = authority()
const store = createStore<AppState>(() => state({ repos: [repo('cached', 'target-a')] }))
let resolveLate!: (value: HostRepoCatalogSnapshot) => void
const lateSnapshot = new Promise<HostRepoCatalogSnapshot>((resolve) => {
resolveLate = resolve
})
let publications = 0
store.subscribe(() => {
publications += 1
})
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(() => lateSnapshot),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
const pending = hydration.capturePreparationInput(owner, 'wake-refresh')
let settled = false
void pending.then(() => {
settled = true
})
await vi.advanceTimersByTimeAsync(4_999)
expect(settled).toBe(false)
await vi.advanceTimersByTimeAsync(1)
await expect(pending).resolves.toMatchObject({
catalogRevision: 0,
repoRefs: [{ repoId: 'cached', executionHostId: 'ssh:target-a' }]
})
expect(vi.getTimerCount()).toBe(0)
resolveLate(hostSnapshot(owner, [repo('late', 'target-a')]))
await Promise.resolve()
await Promise.resolve()
expect(store.getState().repos).toEqual([repo('cached', 'target-a')])
expect(publications).toBe(0)
})
it('discards a catalog reply after authority changes with zero mutation', async () => {
const owner = authority()
const store = createStore<AppState>(() => state({ repos: [repo('cached', 'target-a')] }))
let current = true
let resolve!: (value: ReturnType<typeof hostSnapshot>) => void
const pendingSnapshot = new Promise<HostRepoCatalogSnapshot>((settle) => {
resolve = settle
})
let publications = 0
store.subscribe(() => {
publications += 1
})
const hydration = createDirectSshHostHydration({
store,
listRepos: () => pendingSnapshot,
listLineage: vi.fn(),
isCurrentAuthority: () => current
})
const pending = hydration.capturePreparationInput(owner, 'reconnect')
current = false
resolve(hostSnapshot(owner, [repo('new', 'target-a')]))
await expect(pending).resolves.toBeNull()
expect(store.getState().repos).toEqual([repo('cached', 'target-a')])
expect(publications).toBe(0)
})
it('settles pending host reads and clears their timers on stop', async () => {
vi.useFakeTimers()
const owner = authority()
const store = createStore<AppState>(() => state())
const hydration = createDirectSshHostHydration({
store,
listRepos: () => new Promise<HostRepoCatalogSnapshot>(() => {}),
listLineage: vi.fn(),
isCurrentAuthority: () => true
})
const pending = hydration.capturePreparationInput(owner, 'wake-refresh')
await Promise.resolve()
expect(vi.getTimerCount()).toBe(1)
hydration.stop()
await expect(pending).resolves.toBeNull()
expect(vi.getTimerCount()).toBe(0)
})
it('replaces only exact-host Git and folder lineage across both key namespaces', async () => {
const owner = authority()
const store = createStore<AppState>(() =>
state({
repos: [
repo('a', 'target-a'),
repo('b', 'target-b'),
repo('ambiguous', 'target-a'),
repo('ambiguous', 'target-a'),
repo('contradictory', 'target-a')
],
worktreesByRepo: {
a: [{ id: 'a::/work', repoId: 'a', hostId: 'ssh:target-a' }],
b: [{ id: 'b::/work', repoId: 'b', hostId: 'ssh:target-b' }],
ambiguous: [
{
id: 'ambiguous::/work',
repoId: 'ambiguous',
hostId: 'ssh:target-a'
}
],
contradictory: [
{
id: 'contradictory::/work',
repoId: 'contradictory',
hostId: 'ssh:target-b'
}
]
},
projectGroups: [
{
id: 'group-a',
parentGroupId: null,
connectionId: 'target-a',
executionHostId: 'ssh:target-a'
},
{
id: 'group-b',
parentGroupId: null,
connectionId: 'target-b',
executionHostId: 'ssh:target-b'
}
],
folderWorkspaces: [
{
id: 'folder-a',
projectGroupId: 'group-a',
folderPath: '/target-a/folder',
connectionId: 'target-a'
},
{
id: 'folder-b',
projectGroupId: 'group-b',
folderPath: '/target-b/folder',
connectionId: 'target-b'
}
],
worktreeLineageById: {
'a::/work': { parentWorktreeId: 'stale-a' },
'b::/work': { parentWorktreeId: 'keep-b' },
'ambiguous::/work': { parentWorktreeId: 'keep-ambiguous' },
'contradictory::/work': { parentWorktreeId: 'keep-contradictory' }
},
workspaceLineageByChildKey: {
[worktreeWorkspaceKey('a::/work')]: { parentWorkspaceKey: 'stale-a-workspace' },
[worktreeWorkspaceKey('b::/work')]: { parentWorkspaceKey: 'keep-b-workspace' },
[worktreeWorkspaceKey('ambiguous::/work')]: {
parentWorkspaceKey: 'keep-ambiguous-workspace'
},
[worktreeWorkspaceKey('contradictory::/work')]: {
parentWorkspaceKey: 'keep-contradictory-workspace'
},
[folderWorkspaceKey('folder-a')]: { parentWorkspaceKey: 'stale-folder' },
[folderWorkspaceKey('folder-b')]: { parentWorkspaceKey: 'keep-folder-b' }
}
})
)
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(),
listLineage: vi.fn(
async () =>
({
authoritative: true,
authority: {
kind: 'direct-ssh',
executionHostId: 'ssh:target-a',
...owner
},
worktreeLineageById: {
'a::/work': { parentWorktreeId: 'fresh-a' },
'b::/work': { parentWorktreeId: 'foreign-overwrite' },
'ambiguous::/work': { parentWorktreeId: 'ambiguous-overwrite' },
'contradictory::/work': { parentWorktreeId: 'contradictory-overwrite' }
},
workspaceLineageByChildKey: {
'a::/work': { parentWorkspaceKey: 'raw-terminal-key-must-not-enter-lineage' },
[worktreeWorkspaceKey('a::/work')]: {
parentWorkspaceKey: 'fresh-a-workspace'
},
[worktreeWorkspaceKey('b::/work')]: {
parentWorkspaceKey: 'foreign-workspace-overwrite'
},
[worktreeWorkspaceKey('ambiguous::/work')]: {
parentWorkspaceKey: 'ambiguous-workspace-overwrite'
},
[worktreeWorkspaceKey('contradictory::/work')]: {
parentWorkspaceKey: 'contradictory-workspace-overwrite'
},
[folderWorkspaceKey('folder-a')]: { parentWorkspaceKey: 'fresh-folder' },
[folderWorkspaceKey('folder-b')]: {
parentWorkspaceKey: 'foreign-folder-overwrite'
}
}
}) as unknown as HostLineageSnapshot
),
isCurrentAuthority: () => true
})
await expect(
hydration.readHostScopedLineage({
...owner,
catalogRevision: 0,
repoRefs: [{ repoId: 'a', executionHostId: 'ssh:target-a' }],
authorityRequirement: 'required',
reason: 'reconnect'
})
).resolves.toBe('complete')
expect(store.getState().worktreeLineageById).toEqual({
'a::/work': { parentWorktreeId: 'fresh-a' },
'b::/work': { parentWorktreeId: 'keep-b' },
'ambiguous::/work': { parentWorktreeId: 'keep-ambiguous' },
'contradictory::/work': { parentWorktreeId: 'keep-contradictory' }
})
expect(store.getState().workspaceLineageByChildKey).toEqual({
[worktreeWorkspaceKey('b::/work')]: { parentWorkspaceKey: 'keep-b-workspace' },
[worktreeWorkspaceKey('ambiguous::/work')]: {
parentWorkspaceKey: 'keep-ambiguous-workspace'
},
[worktreeWorkspaceKey('contradictory::/work')]: {
parentWorkspaceKey: 'keep-contradictory-workspace'
},
[folderWorkspaceKey('folder-b')]: { parentWorkspaceKey: 'keep-folder-b' },
[worktreeWorkspaceKey('a::/work')]: { parentWorkspaceKey: 'fresh-a-workspace' },
[folderWorkspaceKey('folder-a')]: { parentWorkspaceKey: 'fresh-folder' }
})
})
it('keeps lineage map identity for a cloned no-op host snapshot', async () => {
const owner = authority()
const hostLineage = productionLineage('a::/work', 'a::/parent')
const foreignLineage = productionLineage('b::/work', 'b::/parent')
const hostWorkspace = productionWorkspaceLineage('a::/work', 'a::/parent')
const foreignWorkspace = productionWorkspaceLineage('b::/work', 'b::/parent')
const store = createStore<AppState>(() =>
state({
repos: [repo('a', 'target-a'), repo('b', 'target-b')],
worktreesByRepo: {
a: [{ id: 'a::/work', repoId: 'a', hostId: 'ssh:target-a' }],
b: [{ id: 'b::/work', repoId: 'b', hostId: 'ssh:target-b' }]
},
worktreeLineageById: {
'a::/work': hostLineage,
'b::/work': foreignLineage
},
workspaceLineageByChildKey: {
[worktreeWorkspaceKey('a::/work')]: hostWorkspace,
[worktreeWorkspaceKey('b::/work')]: foreignWorkspace
}
})
)
const snapshot: HostLineageSnapshot = {
authoritative: true,
authority: {
kind: 'direct-ssh',
executionHostId: 'ssh:target-a',
...owner
},
worktreeLineageById: {
'a::/work': hostLineage
},
workspaceLineageByChildKey: {
[worktreeWorkspaceKey('a::/work')]: hostWorkspace
}
}
let publications = 0
store.subscribe(() => {
publications += 1
})
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(),
listLineage: vi.fn(async () => structuredClone(snapshot)),
isCurrentAuthority: () => true
})
const beforeLineage = store.getState().worktreeLineageById
const beforeWorkspace = store.getState().workspaceLineageByChildKey
await expect(
hydration.readHostScopedLineage({
...owner,
catalogRevision: 0,
repoRefs: [{ repoId: 'a', executionHostId: 'ssh:target-a' }],
authorityRequirement: 'required',
reason: 'reconnect'
})
).resolves.toBe('complete')
expect(store.getState().worktreeLineageById).toBe(beforeLineage)
expect(store.getState().workspaceLineageByChildKey).toBe(beforeWorkspace)
expect(store.getState().worktreeLineageById['a::/work']).toBe(hostLineage)
expect(store.getState().worktreeLineageById['b::/work']).toBe(foreignLineage)
expect(store.getState().workspaceLineageByChildKey[worktreeWorkspaceKey('a::/work')]).toBe(
hostWorkspace
)
expect(store.getState().workspaceLineageByChildKey[worktreeWorkspaceKey('b::/work')]).toBe(
foreignWorkspace
)
expect(publications).toBe(0)
})
it('rejects lineage captured before a newer same-authority catalog revision', async () => {
const owner = authority()
const store = createStore<AppState>(() =>
state({
repos: [repo('a', 'target-a')],
worktreesByRepo: {
a: [{ id: 'a::/work', repoId: 'a', hostId: 'ssh:target-a' }]
},
worktreeLineageById: {
'a::/work': { parentWorktreeId: 'keep-current' }
}
})
)
let resolveLineage!: (value: HostLineageSnapshot) => void
const pendingLineage = new Promise<HostLineageSnapshot>((resolve) => {
resolveLineage = resolve
})
const hydration = createDirectSshHostHydration({
store,
listRepos: vi.fn(async () => hostSnapshot(owner, [repo('a', 'target-a')])),
listLineage: () => pendingLineage,
isCurrentAuthority: () => true
})
const firstInput = await hydration.capturePreparationInput(owner, 'reconnect')
if (!firstInput) {
throw new Error('Expected first preparation input')
}
const pending = hydration.readHostScopedLineage(firstInput)
await hydration.capturePreparationInput(owner, 'wake-refresh')
const beforeLateLineage = store.getState().worktreeLineageById
resolveLineage({
authoritative: true,
authority: {
kind: 'direct-ssh',
executionHostId: 'ssh:target-a',
...owner
},
worktreeLineageById: {
'a::/work': { parentWorktreeId: 'stale-overwrite' }
},
workspaceLineageByChildKey: {}
} as unknown as HostLineageSnapshot)
await expect(pending).resolves.toBe('stale')
expect(store.getState().worktreeLineageById).toBe(beforeLateLineage)
})
})
@@ -1,315 +0,0 @@
import type { StoreApi } from 'zustand'
import { getRepoExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host'
import type { HostLineageSnapshot } from '../../../shared/host-lineage-contract'
import type { HostRepoCatalogSnapshot } from '../../../shared/host-repo-catalog-contract'
import { applyManualRepoOrder } from '../../../shared/manual-repo-order'
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import { isWorkspaceKey } from '../../../shared/workspace-scope'
import type { AppState } from '../store/types'
import { reconcileFetchedRepos, reuseEqualRecordMap } from '../store/slices/repo-identity-reconcile'
import type {
DirectSshLineageOutcome,
DirectSshPreparationInput,
DirectSshPreparationReason
} from './direct-ssh-reconnect-coordinator'
import { directSshHostHydrationTelemetry } from './direct-ssh-host-hydration-telemetry'
import { directSshHostHydrationScope } from './direct-ssh-host-hydration-scope'
import { directSshAuthoritiesEqual } from './direct-ssh-reconnect-tokens'
export const DIRECT_SSH_HOST_READ_TIMEOUT_MS = 5_000
type HostReadTimer = ReturnType<typeof setTimeout>
export type DirectSshHostHydrationDeps = {
store: Pick<StoreApi<AppState>, 'getState' | 'setState'>
listRepos: (authority: DirectSshAuthority) => Promise<HostRepoCatalogSnapshot>
listLineage: (authority: DirectSshAuthority) => Promise<HostLineageSnapshot>
isCurrentAuthority: (authority: DirectSshAuthority) => boolean
setTimer?: (callback: () => void, delayMs: number) => HostReadTimer
clearTimer?: (timer: HostReadTimer) => void
}
export type DirectSshHostHydration = {
capturePreparationInput: (
authority: DirectSshAuthority,
reason: DirectSshPreparationReason,
snapshotRevision?: number
) => Promise<DirectSshPreparationInput | null>
readHostScopedLineage: (input: DirectSshPreparationInput) => Promise<DirectSshLineageOutcome>
isPreparationTokenCurrent: (token: {
authority: DirectSshAuthority
catalogRevision: number
repoFingerprint: string
}) => boolean
stop: () => void
}
type BoundedResult<T> = { status: 'complete'; value: T } | { status: 'timed-out' | 'unavailable' }
function isAuthoritativeHost(
authority: HostRepoCatalogSnapshot | HostLineageSnapshot,
expected: DirectSshAuthority
): authority is Extract<typeof authority, { authoritative: true }> {
if (!authority.authoritative || authority.authority.kind !== 'direct-ssh') {
return false
}
return (
authority.authority.executionHostId === toSshExecutionHostId(expected.targetId) &&
directSshAuthoritiesEqual(authority.authority, expected)
)
}
function mergeExactHostCatalog(state: AppState, snapshot: HostRepoCatalogSnapshot): AppState {
if (!snapshot.authoritative || snapshot.authority.kind !== 'direct-ssh') {
return state
}
const hostId = snapshot.authority.executionHostId
// Why re-apply the overlay: re-appending the host's rows puts them at the tail, which would
// undo the user's manual cross-host order on every connect.
const repos = reconcileFetchedRepos(
state.repos,
applyManualRepoOrder(
[...state.repos.filter((repo) => getRepoExecutionHostId(repo) !== hostId), ...snapshot.repos],
state.manualRepoOrder
)
)
return repos === state.repos ? state : { ...state, repos }
}
// Why: the host owns every in-scope key, so drop the stale in-scope rows, overlay the snapshot's,
// and leave out-of-scope hosts alone. Reusing equal rows keeps a cloned no-op read from republishing.
function overlayHostScopedLineage<T>(
previous: Readonly<Record<string, T>>,
incoming: Readonly<Record<string, T>>,
isHostScoped: (key: string) => boolean
): Readonly<Record<string, T>> {
const next: Record<string, T> = Object.fromEntries([
...Object.entries(previous).filter(([key]) => !isHostScoped(key)),
...Object.entries(incoming).filter(([key]) => isHostScoped(key))
])
return reuseEqualRecordMap(previous, next)
}
function mergeExactHostLineage(
state: AppState,
snapshot: Extract<HostLineageSnapshot, { authoritative: true }>,
authority: DirectSshAuthority,
catalogRevision: number
): AppState {
const scope = directSshHostHydrationScope(state, authority, catalogRevision)
const worktreeLineageById = overlayHostScopedLineage(
state.worktreeLineageById,
snapshot.worktreeLineageById,
(worktreeId) => scope.gitWorktreeIds.has(worktreeId)
)
const workspaceLineageByChildKey = overlayHostScopedLineage(
state.workspaceLineageByChildKey,
snapshot.workspaceLineageByChildKey,
(childKey) => isWorkspaceKey(childKey) && scope.lineageWorkspaceKeys.has(childKey)
)
if (
worktreeLineageById === state.worktreeLineageById &&
workspaceLineageByChildKey === state.workspaceLineageByChildKey
) {
return state
}
return { ...state, worktreeLineageById, workspaceLineageByChildKey }
}
export function createDirectSshHostHydration(
deps: DirectSshHostHydrationDeps
): DirectSshHostHydration {
const setTimer: NonNullable<DirectSshHostHydrationDeps['setTimer']> =
deps.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs))
const clearTimer: NonNullable<DirectSshHostHydrationDeps['clearTimer']> =
deps.clearTimer ?? ((timer) => clearTimeout(timer))
const catalogRevisionByTarget = new Map<string, number>()
const catalogInFlight = new Map<string, Promise<'complete' | 'degraded' | 'stale'>>()
const pendingDeadlines = new Set<{ timer: HostReadTimer; settle: () => void }>()
let stopped = false
const bounded = async <T>(operation: Promise<T>): Promise<BoundedResult<T>> => {
let pendingDeadline: { timer: HostReadTimer; settle: () => void } | undefined
const deadline = new Promise<BoundedResult<T>>((resolve) => {
const settle = (): void => resolve({ status: 'timed-out' })
const timer = setTimer(settle, DIRECT_SSH_HOST_READ_TIMEOUT_MS)
pendingDeadline = { timer, settle }
pendingDeadlines.add(pendingDeadline)
})
try {
return await Promise.race([
operation.then<BoundedResult<T>, BoundedResult<T>>(
(value) => ({ status: 'complete', value }),
() => ({ status: 'unavailable' })
),
deadline
])
} finally {
if (pendingDeadline) {
clearTimer(pendingDeadline.timer)
pendingDeadlines.delete(pendingDeadline)
}
}
}
const refreshCatalog = (
authority: DirectSshAuthority
): Promise<'complete' | 'degraded' | 'stale'> => {
const key = JSON.stringify([
authority.targetId,
authority.providerEpoch,
authority.connectionGeneration
])
const existing = catalogInFlight.get(key)
if (existing) {
return existing
}
const operation = (async (): Promise<'complete' | 'degraded' | 'stale'> => {
const boundedResult = await bounded(Promise.resolve().then(() => deps.listRepos(authority)))
if (stopped || !deps.isCurrentAuthority(authority)) {
return 'stale'
}
if (boundedResult.status !== 'complete') {
return 'degraded'
}
const snapshot = boundedResult.value
if (!snapshot.authoritative) {
return snapshot.reason === 'stale' || snapshot.reason === 'rejected' ? 'stale' : 'degraded'
}
if (
!isAuthoritativeHost(snapshot, authority) ||
snapshot.repos.some(
(repo) =>
getRepoExecutionHostId(repo) !== toSshExecutionHostId(authority.targetId) ||
repo.connectionId !== authority.targetId
)
) {
return 'stale'
}
let admitted = false
deps.store.setState((state) => {
if (stopped || !deps.isCurrentAuthority(authority)) {
return state
}
admitted = true
return mergeExactHostCatalog(state, snapshot)
})
if (!admitted) {
return 'stale'
}
catalogRevisionByTarget.set(
authority.targetId,
(catalogRevisionByTarget.get(authority.targetId) ?? 0) + 1
)
return 'complete'
})().finally(() => {
if (catalogInFlight.get(key) === operation) {
catalogInFlight.delete(key)
}
})
catalogInFlight.set(key, operation)
return operation
}
const capturePreparationInput = async (
authority: DirectSshAuthority,
reason: DirectSshPreparationReason,
snapshotRevision?: number
): Promise<DirectSshPreparationInput | null> => {
const catalogStartedAt = Date.now()
const catalogOutcome = await refreshCatalog(authority)
const catalogDurationMs = Math.max(0, Date.now() - catalogStartedAt)
if (catalogOutcome === 'stale' || stopped || !deps.isCurrentAuthority(authority)) {
return null
}
const catalogRevision = catalogRevisionByTarget.get(authority.targetId) ?? 0
const scope = directSshHostHydrationScope(deps.store.getState(), authority, catalogRevision)
return {
...authority,
catalogRevision,
repoRefs: scope.gitRepos,
authorityRequirement: 'required',
...(snapshotRevision === undefined ? {} : { snapshotRevision }),
reason,
telemetry: directSshHostHydrationTelemetry(scope, catalogOutcome, catalogDurationMs)
}
}
const readHostScopedLineage = async (
input: DirectSshPreparationInput
): Promise<DirectSshLineageOutcome> => {
const authority = {
targetId: input.targetId,
providerEpoch: input.providerEpoch,
connectionGeneration: input.connectionGeneration
}
const boundedResult = await bounded(Promise.resolve().then(() => deps.listLineage(authority)))
if (
stopped ||
!deps.isCurrentAuthority(authority) ||
(catalogRevisionByTarget.get(authority.targetId) ?? 0) !== input.catalogRevision
) {
return 'stale'
}
if (boundedResult.status !== 'complete') {
return 'degraded'
}
const snapshot = boundedResult.value
if (!snapshot.authoritative) {
return snapshot.reason === 'stale' || snapshot.reason === 'rejected' ? 'stale' : 'degraded'
}
if (!isAuthoritativeHost(snapshot, authority)) {
return 'stale'
}
let admitted = false
deps.store.setState((state) => {
if (
stopped ||
!deps.isCurrentAuthority(authority) ||
(catalogRevisionByTarget.get(authority.targetId) ?? 0) !== input.catalogRevision
) {
return state
}
admitted = true
return mergeExactHostLineage(state, snapshot, authority, input.catalogRevision)
})
return admitted ? 'complete' : 'stale'
}
return {
capturePreparationInput,
readHostScopedLineage,
isPreparationTokenCurrent: (token) => {
if (
stopped ||
!deps.isCurrentAuthority(token.authority) ||
(catalogRevisionByTarget.get(token.authority.targetId) ?? 0) !== token.catalogRevision
) {
return false
}
const scope = directSshHostHydrationScope(
deps.store.getState(),
token.authority,
token.catalogRevision
)
const sortedRepos = [...scope.gitRepos].sort((left, right) => {
const leftKey = `${left.executionHostId}\0${left.repoId}`
const rightKey = `${right.executionHostId}\0${right.repoId}`
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0
})
return (
JSON.stringify(sortedRepos.map((repo) => [repo.executionHostId, repo.repoId])) ===
token.repoFingerprint
)
},
stop: () => {
stopped = true
for (const deadline of pendingDeadlines) {
clearTimer(deadline.timer)
deadline.settle()
}
pendingDeadlines.clear()
catalogInFlight.clear()
}
}
}
@@ -1,66 +0,0 @@
import type {
DirectSshPreparationMetrics,
DirectSshPreparationOutcome,
DirectSshReconnectOutcome,
DirectSshRepoOutcomeCounts
} from './direct-ssh-reconnect-coordinator-types'
export function createEmptyDirectSshRepoOutcomeCounts(): DirectSshRepoOutcomeCounts {
return {
complete: 0,
'non-authoritative': 0,
'timed-out': 0,
'cancel-budget-exhausted': 0,
canceled: 0,
stale: 0,
rejected: 0
}
}
export function createEmptyDirectSshPreparationMetrics(): DirectSshPreparationMetrics {
return {
queueWaitDurationsMs: [],
providerExecutionDurationsMs: [],
timeoutRetryCount: 0,
locallySettledWaiterCount: 0,
cancelDebtCount: 0,
replacementAdmissionDelayedCount: 0,
schedulerOverlappingJoinCount: 0,
peakLocallyUnsettled: 0,
estimatedLateWorkAllowanceCount: 0,
lineageDurationMs: 0
}
}
export function combineDirectSshReconnectOutcome(
prepared: DirectSshPreparationOutcome,
staleBindingsCleared: number,
retriedTerminals: number,
correctedTerminals: number
): DirectSshReconnectOutcome {
return {
...prepared,
staleBindingsCleared,
retriedTerminals,
correctedTerminals,
stabilizing: false
}
}
export function createTerminalOnlyDirectSshReconnectOutcome(
status: DirectSshReconnectOutcome['status'],
staleBindingsCleared = 0,
retriedTerminals = 0
): DirectSshReconnectOutcome {
return {
status,
token: null,
repoOutcomes: createEmptyDirectSshRepoOutcomeCounts(),
lineageOutcome: 'not-started',
metrics: createEmptyDirectSshPreparationMetrics(),
staleBindingsCleared,
retriedTerminals,
correctedTerminals: 0,
stabilizing: status === 'stabilizing'
}
}
@@ -1,25 +0,0 @@
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import type { DirectSshReconnectTimer } from './direct-ssh-reconnect-coordinator-types'
export type DirectSshReconnectTargetState = {
authority: DirectSshAuthority
installedAt: number
dampUntil: number | null
timer: DirectSshReconnectTimer | null
}
export function createDirectSshReconnectTargetState(
authority: DirectSshAuthority,
previous: DirectSshReconnectTargetState | undefined,
installedAt: number,
stabilizationMs: number
): DirectSshReconnectTargetState {
const rotatedRapidly =
previous !== undefined && installedAt - previous.installedAt < stabilizationMs
return {
authority,
installedAt,
dampUntil: rotatedRapidly ? installedAt + stabilizationMs : null,
timer: null
}
}
@@ -1,118 +0,0 @@
import type {
DirectSshCoordinatorTelemetry,
DirectSshPreparationInput,
DirectSshPreparationOutcome,
DirectSshPreparationReason,
DirectSshReconnectOutcome
} from './direct-ssh-reconnect-coordinator-types'
import { createTerminalOnlyDirectSshReconnectOutcome } from './direct-ssh-reconnect-coordinator-outcomes'
type TelemetryOutcome = DirectSshPreparationOutcome | DirectSshReconnectOutcome
type TelemetryOptions = {
terminalFinalizationDurationMs?: number
staleBindingsCleared?: number
retriedTerminals?: number
correctedTerminals?: number
damped?: boolean
authorityRotationCount?: number
catalogOutcome?: DirectSshCoordinatorTelemetry['catalogOutcome']
catalogDurationMs?: number
}
type DirectSshCoordinatorTelemetryReporter = {
report: (
mode: DirectSshCoordinatorTelemetry['mode'],
input: DirectSshPreparationInput,
outcome: TelemetryOutcome,
startedAt: number,
options?: TelemetryOptions
) => void
reportWithoutInput: (
mode: DirectSshCoordinatorTelemetry['mode'],
reason: DirectSshPreparationReason,
outcome: TelemetryOutcome,
startedAt: number,
options?: TelemetryOptions
) => void
}
export function createDirectSshCoordinatorTelemetryReporter(args: {
onTelemetry?: (event: DirectSshCoordinatorTelemetry) => void
now: () => number
}): DirectSshCoordinatorTelemetryReporter {
const emit = (event: DirectSshCoordinatorTelemetry): void => {
try {
args.onTelemetry?.(event)
} catch {
// Recovery cannot depend on diagnostics.
}
}
const report = (
mode: DirectSshCoordinatorTelemetry['mode'],
input: DirectSshPreparationInput,
outcome: TelemetryOutcome,
startedAt: number,
options: TelemetryOptions = {}
): void => {
const inputMetrics = input.telemetry
const outcomeMetrics =
outcome.metrics ?? createTerminalOnlyDirectSshReconnectOutcome('stale').metrics!
emit({
mode,
reason: input.reason,
outcome: outcome.status,
durationMs: Math.max(0, args.now() - startedAt),
staleBindingsCleared: options.staleBindingsCleared ?? 0,
retriedTerminals: options.retriedTerminals ?? 0,
correctedTerminals: options.correctedTerminals ?? 0,
terminalFinalizationDurationMs: options.terminalFinalizationDurationMs ?? 0,
catalogOutcome: options.catalogOutcome ?? inputMetrics?.catalogOutcome ?? 'complete',
catalogDurationMs: options.catalogDurationMs ?? inputMetrics?.catalogDurationMs ?? 0,
gitWorktreeCount: inputMetrics?.gitWorktreeCount ?? 0,
folderWorkspaceCount: inputMetrics?.folderWorkspaceCount ?? 0,
ambiguousOwnerCount: inputMetrics?.ambiguousOwnerCount ?? 0,
contradictoryOwnerCount: inputMetrics?.contradictoryOwnerCount ?? 0,
repoOutcomes: { ...outcome.repoOutcomes },
lineageOutcome: outcome.lineageOutcome,
queueWaitDurationsMs: [...outcomeMetrics.queueWaitDurationsMs],
providerExecutionDurationsMs: [...outcomeMetrics.providerExecutionDurationsMs],
timeoutRetryCount: outcomeMetrics.timeoutRetryCount,
locallySettledWaiterCount: outcomeMetrics.locallySettledWaiterCount,
cancelDebtCount: outcomeMetrics.cancelDebtCount,
replacementAdmissionDelayedCount: outcomeMetrics.replacementAdmissionDelayedCount,
overlappingJoinCount: outcomeMetrics.schedulerOverlappingJoinCount,
peakLocallyUnsettled: outcomeMetrics.peakLocallyUnsettled,
estimatedLateWorkAllowanceCount: outcomeMetrics.estimatedLateWorkAllowanceCount,
authorityRotationCount: options.authorityRotationCount ?? 0,
damped: options.damped ?? false
})
}
const reportWithoutInput: DirectSshCoordinatorTelemetryReporter['reportWithoutInput'] = (
mode,
reason,
outcome,
startedAt,
options = {}
) => {
report(
mode,
{
targetId: '',
providerEpoch: '' as DirectSshPreparationInput['providerEpoch'],
connectionGeneration: 0,
catalogRevision: 0,
repoRefs: [],
authorityRequirement: 'required',
reason
},
outcome,
startedAt,
options
)
}
return { report, reportWithoutInput }
}
@@ -1,145 +0,0 @@
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import type { DirectSshGitRepoRef } from '../lib/direct-ssh-target-scope-types'
import type {
DirectSshWorktreeRefreshScheduler,
DirectSshWorktreeRefreshTerminalStatus
} from './direct-ssh-worktree-refresh-scheduler'
export type DirectSshPreparationReason =
| 'reconnect'
| 'initial-hydration'
| 'workspace-snapshot'
| 'wake-refresh'
export type DirectSshAuthorityRequirement = 'required' | 'allow-metadata-fallback'
export type DirectSshPreparationInput = DirectSshAuthority & {
catalogRevision: number
repoRefs: readonly DirectSshGitRepoRef[]
authorityRequirement: DirectSshAuthorityRequirement
snapshotRevision?: number
reason: DirectSshPreparationReason
telemetry?: DirectSshPreparationInputTelemetry
}
export type DirectSshPreparationInputTelemetry = {
catalogOutcome: 'complete' | 'degraded'
catalogDurationMs: number
gitWorktreeCount: number
folderWorkspaceCount: number
ambiguousOwnerCount: number
contradictoryOwnerCount: number
}
export type DirectSshPreparationToken = {
authority: DirectSshAuthority
catalogRevision: number
repoFingerprint: string
authorityRequirement: DirectSshAuthorityRequirement
snapshotRevision: number | null
outcome: 'complete' | 'degraded'
}
export type DirectSshLineageOutcome = 'complete' | 'degraded' | 'canceled' | 'stale'
export type DirectSshRepoOutcomeCounts = Record<DirectSshWorktreeRefreshTerminalStatus, number>
export type DirectSshPreparationOutcome = {
status: 'complete' | 'degraded' | 'canceled' | 'stale' | 'stopped'
token: DirectSshPreparationToken | null
repoOutcomes: DirectSshRepoOutcomeCounts
lineageOutcome: DirectSshLineageOutcome | 'not-started'
metrics?: DirectSshPreparationMetrics
}
export type DirectSshPreparationMetrics = {
queueWaitDurationsMs: readonly number[]
providerExecutionDurationsMs: readonly number[]
timeoutRetryCount: number
locallySettledWaiterCount: number
cancelDebtCount: number
replacementAdmissionDelayedCount: number
schedulerOverlappingJoinCount: number
peakLocallyUnsettled: number
estimatedLateWorkAllowanceCount: number
lineageDurationMs: number
}
export type DirectSshReconnectOutcome = Omit<DirectSshPreparationOutcome, 'status'> & {
status: DirectSshPreparationOutcome['status'] | 'stabilizing'
staleBindingsCleared: number
retriedTerminals: number
correctedTerminals: number
stabilizing: boolean
}
export type DirectSshCorrectionReason =
| 'preparation-complete'
| 'wake-refresh'
| 'workspace-hydrated'
| 'snapshot-applied'
export type DirectSshCoordinatorTelemetry = {
mode: 'reconnect' | 'prepare-only'
reason: DirectSshPreparationReason
outcome: DirectSshReconnectOutcome['status']
durationMs: number
staleBindingsCleared: number
retriedTerminals: number
correctedTerminals: number
terminalFinalizationDurationMs: number
catalogOutcome: 'complete' | 'degraded' | 'stale'
catalogDurationMs: number
gitWorktreeCount: number
folderWorkspaceCount: number
ambiguousOwnerCount: number
contradictoryOwnerCount: number
repoOutcomes: DirectSshRepoOutcomeCounts
lineageOutcome: DirectSshPreparationOutcome['lineageOutcome']
queueWaitDurationsMs: readonly number[]
providerExecutionDurationsMs: readonly number[]
timeoutRetryCount: number
locallySettledWaiterCount: number
cancelDebtCount: number
replacementAdmissionDelayedCount: number
overlappingJoinCount: number
peakLocallyUnsettled: number
estimatedLateWorkAllowanceCount: number
authorityRotationCount: number
damped: boolean
}
export type DirectSshReconnectTimer = ReturnType<typeof setTimeout>
export type DirectSshReconnectCoordinatorDeps = {
scheduler: DirectSshWorktreeRefreshScheduler
isCurrentConnectedAuthority: (authority: DirectSshAuthority) => boolean
capturePreparationInput: (
authority: DirectSshAuthority,
reason: DirectSshPreparationReason
) => Promise<DirectSshPreparationInput | null>
readHostScopedLineage: (input: DirectSshPreparationInput) => Promise<DirectSshLineageOutcome>
invalidateStaleTerminalBindings: (authority: DirectSshAuthority) => number
retryTargetPanes: (authority: DirectSshAuthority) => number
correctUnboundTerminalPanes: (
authority: DirectSshAuthority,
reason: DirectSshCorrectionReason
) => number
onTelemetry?: (event: DirectSshCoordinatorTelemetry) => void
now?: () => number
setTimer?: (callback: () => void, delayMs: number) => DirectSshReconnectTimer
clearTimer?: (timer: DirectSshReconnectTimer) => void
stabilizationMs?: number
}
export type DirectSshReconnectCoordinator = {
requestReconnect: (authority: DirectSshAuthority) => Promise<DirectSshReconnectOutcome>
prepareOnly: (input: DirectSshPreparationInput) => Promise<DirectSshPreparationOutcome>
correctUnboundTerminals: (
authority: DirectSshAuthority,
reason: DirectSshCorrectionReason
) => number
replaceAuthority: (authority: DirectSshAuthority) => void
invalidate: (targetId: string) => void
stop: () => void
}
@@ -1,542 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type {
HostQualifiedDetectedWorktreeResult,
ProviderRequestId
} from '../../../shared/detected-worktree-provider-contract'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import {
createDirectSshWorktreeRefreshScheduler,
type DirectSshWorktreeRefreshKey,
type DirectSshWorktreeRefreshLease,
type DirectSshWorktreeRefreshOutcome,
type DirectSshWorktreeRefreshScheduler,
type WaiterLeaseId
} from './direct-ssh-worktree-refresh-scheduler'
import {
createDirectSshReconnectCoordinator,
type DirectSshLineageOutcome,
type DirectSshPreparationInput,
type DirectSshReconnectCoordinatorDeps
} from './direct-ssh-reconnect-coordinator'
type Deferred<T> = {
promise: Promise<T>
resolve: (value: T) => void
reject: (error: unknown) => void
}
function deferred<T>(): Deferred<T> {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise
reject = rejectPromise
})
return { promise, resolve, reject }
}
function epoch(value: string): SshProviderEpoch {
return value as SshProviderEpoch
}
function authority(targetId: string, epochId = `epoch-${targetId}`): DirectSshAuthority {
return {
targetId,
providerEpoch: epoch(epochId),
connectionGeneration: 1
}
}
function preparationInput(
owner: DirectSshAuthority,
repoIds: readonly string[] = ['repo'],
overrides: Partial<DirectSshPreparationInput> = {}
): DirectSshPreparationInput {
return {
...owner,
catalogRevision: 1,
repoRefs: repoIds.map((repoId) => ({
repoId,
executionHostId: `ssh:${owner.targetId}`
})),
authorityRequirement: 'required',
reason: 'workspace-snapshot',
...overrides
}
}
type FakeLease = {
key: DirectSshWorktreeRefreshKey
deferred: Deferred<DirectSshWorktreeRefreshOutcome>
lease: DirectSshWorktreeRefreshLease
}
function createFakeScheduler(): {
scheduler: DirectSshWorktreeRefreshScheduler
leases: FakeLease[]
complete: (index: number, status?: DirectSshWorktreeRefreshOutcome['status']) => void
} {
const leases: FakeLease[] = []
const request = vi.fn((key: DirectSshWorktreeRefreshKey) => {
const leaseDeferred = deferred<DirectSshWorktreeRefreshOutcome>()
let released = false
const lease: DirectSshWorktreeRefreshLease = {
waiterLeaseId: `lease-${leases.length + 1}` as WaiterLeaseId,
result: leaseDeferred.promise,
release: vi.fn(() => {
if (!released) {
released = true
leaseDeferred.resolve({ status: 'canceled' })
}
})
}
leases.push({ key, deferred: leaseDeferred, lease })
return lease
})
const scheduler: DirectSshWorktreeRefreshScheduler = {
request,
invalidateAuthority: vi.fn(),
invalidateTarget: vi.fn(),
disposeProvider: vi.fn(),
getSnapshot: vi.fn(() => ({
locallyUnsettled: 0,
queued: 0,
retrying: 0,
logicalTasks: leases.length,
waiters: leases.length,
cancelDebtByAuthority: new Map()
})),
stop: vi.fn()
}
return {
scheduler,
leases,
complete: (index, status = 'complete') => leases[index].deferred.resolve({ status })
}
}
function createCoordinatorHarness(fakeScheduler = createFakeScheduler()) {
const current = new Map<string, DirectSshAuthority>()
const events: string[] = []
const capturePreparationInput = vi.fn(
async (owner: DirectSshAuthority, reason: DirectSshPreparationInput['reason']) => {
events.push(`capture:${owner.targetId}`)
return preparationInput(owner, ['repo'], { reason })
}
)
const deps: DirectSshReconnectCoordinatorDeps = {
scheduler: fakeScheduler.scheduler,
isCurrentConnectedAuthority: (owner) => {
const active = current.get(owner.targetId)
return (
active?.providerEpoch === owner.providerEpoch &&
active.connectionGeneration === owner.connectionGeneration
)
},
capturePreparationInput,
readHostScopedLineage: vi.fn(async (): Promise<DirectSshLineageOutcome> => 'complete'),
invalidateStaleTerminalBindings: vi.fn((owner) => {
events.push(`invalidate:${owner.targetId}`)
return 1
}),
retryTargetPanes: vi.fn((owner) => {
events.push(`retry:${owner.targetId}`)
return 2
}),
correctUnboundTerminalPanes: vi.fn(() => 1),
onTelemetry: vi.fn()
}
const coordinator = createDirectSshReconnectCoordinator(deps)
return { coordinator, deps, current, events, ...fakeScheduler }
}
async function flush(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
}
afterEach(() => {
vi.useRealTimers()
})
describe('createDirectSshReconnectCoordinator', () => {
it('runs terminal invalidation and retry synchronously before preparation awaits', () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
void harness.coordinator.requestReconnect(owner)
expect(harness.events).toEqual(['invalidate:target-a', 'retry:target-a', 'capture:target-a'])
expect(harness.leases).toHaveLength(0)
})
it('prepares the first observed authority immediately', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
const pending = harness.coordinator.requestReconnect(owner)
await flush()
expect(harness.deps.capturePreparationInput).toHaveBeenCalledWith(owner, 'reconnect')
expect(harness.leases).toHaveLength(1)
harness.complete(0)
await expect(pending).resolves.toMatchObject({ status: 'complete', stabilizing: false })
})
it('keeps target B independent while target A preparation is blocked', async () => {
const harness = createCoordinatorHarness()
const targetA = authority('target-a')
const targetB = authority('target-b')
harness.current.set(targetA.targetId, targetA)
harness.current.set(targetB.targetId, targetB)
const pendingA = harness.coordinator.requestReconnect(targetA)
await flush()
const pendingB = harness.coordinator.requestReconnect(targetB)
await flush()
expect(harness.leases).toHaveLength(2)
harness.complete(1)
await expect(pendingB).resolves.toMatchObject({ status: 'complete' })
expect(harness.deps.correctUnboundTerminalPanes).toHaveBeenCalledTimes(1)
let targetASettled = false
void pendingA.then(() => {
targetASettled = true
})
await flush()
expect(targetASettled).toBe(false)
harness.coordinator.stop()
})
it('treats exact-equal authority replacement as a true timing no-op', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.coordinator.replaceAuthority(owner)
vi.setSystemTime(4_000)
harness.coordinator.replaceAuthority({ ...owner })
expect(harness.scheduler.disposeProvider).not.toHaveBeenCalled()
expect(harness.scheduler.invalidateTarget).not.toHaveBeenCalled()
vi.setSystemTime(5_001)
const next = authority('target-a', 'next')
harness.current.set(next.targetId, next)
const pending = harness.coordinator.requestReconnect(next)
await flush()
expect(harness.deps.capturePreparationInput).toHaveBeenCalledWith(next, 'reconnect')
harness.complete(0)
await expect(pending).resolves.toMatchObject({ status: 'complete', stabilizing: false })
})
it('cancels obsolete work on changed authority without ordering epochs', async () => {
const harness = createCoordinatorHarness()
const oldOwner = authority('target-a', 'z-old')
const newOwner = authority('target-a', 'a-new')
harness.current.set(oldOwner.targetId, oldOwner)
harness.coordinator.replaceAuthority(oldOwner)
const pending = harness.coordinator.prepareOnly(preparationInput(oldOwner))
await flush()
harness.current.set(newOwner.targetId, newOwner)
harness.coordinator.replaceAuthority(newOwner)
await expect(pending).resolves.toMatchObject({ status: 'stale', token: null })
expect(harness.leases[0].lease.release).toHaveBeenCalledWith('invalidated')
expect(harness.scheduler.disposeProvider).toHaveBeenCalledWith(oldOwner)
})
it('settles invalidation locally while a late lineage read remains pending', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
const lineage = deferred<'complete'>()
vi.mocked(harness.deps.readHostScopedLineage).mockReturnValue(lineage.promise)
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
const pending = harness.coordinator.prepareOnly(preparationInput(owner))
harness.complete(0)
await flush()
expect(harness.deps.readHostScopedLineage).toHaveBeenCalledOnce()
harness.coordinator.invalidate(owner.targetId)
await expect(pending).resolves.toMatchObject({ status: 'stale', token: null })
lineage.resolve('complete')
await flush()
expect(harness.deps.correctUnboundTerminalPanes).not.toHaveBeenCalled()
})
it('joins exact overlapping preparation and reruns after completion', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
const input = preparationInput(owner)
const first = harness.coordinator.prepareOnly(input)
const joined = harness.coordinator.prepareOnly({
...input,
repoRefs: input.repoRefs.toReversed()
})
expect(joined).toBe(first)
expect(harness.leases).toHaveLength(1)
harness.complete(0)
await expect(first).resolves.toMatchObject({ status: 'complete' })
const later = harness.coordinator.prepareOnly(input)
expect(later).not.toBe(first)
expect(harness.leases).toHaveLength(2)
harness.coordinator.stop()
})
it('keeps prepare-only isolated from terminal retry and correction', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
const pending = harness.coordinator.prepareOnly(preparationInput(owner))
harness.complete(0, 'non-authoritative')
await expect(pending).resolves.toMatchObject({
status: 'degraded',
token: { outcome: 'degraded' }
})
expect(harness.deps.invalidateStaleTerminalBindings).not.toHaveBeenCalled()
expect(harness.deps.retryTargetPanes).not.toHaveBeenCalled()
expect(harness.deps.correctUnboundTerminalPanes).not.toHaveBeenCalled()
})
it('rejects repo refs for another SSH host before scheduler admission', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
await expect(
harness.coordinator.prepareOnly(
preparationInput(owner, ['repo'], {
repoRefs: [{ repoId: 'repo', executionHostId: 'ssh:target-b' }]
})
)
).resolves.toMatchObject({ status: 'stale', token: null })
expect(harness.leases).toHaveLength(0)
expect(harness.deps.readHostScopedLineage).not.toHaveBeenCalled()
})
it('holds lineage and token creation through the scheduler timeout retry barrier', async () => {
const attempts: {
id: ProviderRequestId
deferred: Deferred<HostQualifiedDetectedWorktreeResult>
key: DirectSshWorktreeRefreshKey
}[] = []
const scheduler = createDirectSshWorktreeRefreshScheduler({
startAttempt: (attemptKey) => {
const result = deferred<HostQualifiedDetectedWorktreeResult>()
const id = `provider-${attempts.length + 1}` as ProviderRequestId
attempts.push({ id, deferred: result, key: attemptKey })
return { providerRequestId: id, result: result.promise, cancel: vi.fn() }
}
})
const harness = createCoordinatorHarness({
scheduler,
leases: [],
complete: () => {}
})
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
const pending = harness.coordinator.prepareOnly(preparationInput(owner))
attempts[0].deferred.resolve({
status: 'timed-out',
providerRequestId: attempts[0].id,
executionHostId: attempts[0].key.executionHostId
})
await flush()
expect(attempts).toHaveLength(2)
expect(harness.deps.readHostScopedLineage).not.toHaveBeenCalled()
attempts[1].deferred.resolve({
status: 'complete',
providerRequestId: attempts[1].id,
repoId: 'repo',
authority: {
kind: 'direct-ssh',
executionHostId: attempts[1].key.executionHostId,
...owner
},
result: { repoId: 'repo', authoritative: true, source: 'git', worktrees: [] }
})
await expect(pending).resolves.toMatchObject({ status: 'complete', token: {} })
expect(harness.deps.readHostScopedLineage).toHaveBeenCalledOnce()
})
it('starts lineage only after every repo is terminal and returns a fenced token', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
const pending = harness.coordinator.prepareOnly(
preparationInput(owner, ['repo-b', 'repo-a'], {
catalogRevision: 7,
snapshotRevision: 11
})
)
expect(harness.leases.map((lease) => lease.key.repoId)).toEqual(['repo-a', 'repo-b'])
harness.complete(0)
await flush()
expect(harness.deps.readHostScopedLineage).not.toHaveBeenCalled()
harness.complete(1)
await expect(pending).resolves.toMatchObject({
status: 'complete',
token: {
authority: owner,
catalogRevision: 7,
snapshotRevision: 11,
outcome: 'complete'
}
})
expect(harness.deps.readHostScopedLineage).toHaveBeenCalledOnce()
})
it('damps rapid rotations to the latest authority while terminal checks stay immediate', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const harness = createCoordinatorHarness()
const initial = authority('target-a', 'initial')
const middle = authority('target-a', 'middle')
const flapping = authority('target-a', 'flapping')
const latest = authority('target-a', 'latest')
harness.current.set(initial.targetId, initial)
harness.coordinator.replaceAuthority(initial)
vi.setSystemTime(5_001)
harness.current.set(middle.targetId, middle)
const stableReplacement = harness.coordinator.requestReconnect(middle)
await flush()
harness.complete(0)
await expect(stableReplacement).resolves.toMatchObject({
status: 'complete',
stabilizing: false
})
vi.setSystemTime(6_000)
harness.current.set(flapping.targetId, flapping)
await expect(harness.coordinator.requestReconnect(flapping)).resolves.toMatchObject({
status: 'stabilizing'
})
vi.setSystemTime(7_000)
harness.current.set(latest.targetId, latest)
await expect(harness.coordinator.requestReconnect(latest)).resolves.toMatchObject({
status: 'stabilizing'
})
expect(harness.deps.retryTargetPanes).toHaveBeenCalledTimes(3)
expect(harness.deps.capturePreparationInput).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTimeAsync(4_999)
expect(harness.deps.capturePreparationInput).toHaveBeenCalledTimes(1)
await vi.advanceTimersByTimeAsync(1)
expect(harness.deps.capturePreparationInput).toHaveBeenCalledTimes(2)
expect(harness.deps.capturePreparationInput).toHaveBeenCalledWith(latest, 'reconnect')
expect(harness.deps.capturePreparationInput).not.toHaveBeenCalledWith(flapping, 'reconnect')
harness.complete(1)
})
it('corrects only current authority and cleans pending work and timers on stop', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const harness = createCoordinatorHarness()
const initial = authority('target-a', 'initial')
const latest = authority('target-a', 'latest')
harness.current.set(initial.targetId, initial)
harness.coordinator.replaceAuthority(initial)
harness.current.set(latest.targetId, latest)
void harness.coordinator.requestReconnect(latest)
expect(harness.coordinator.correctUnboundTerminals(latest, 'wake-refresh')).toBe(1)
harness.coordinator.stop()
await vi.advanceTimersByTimeAsync(5_000)
expect(harness.deps.capturePreparationInput).not.toHaveBeenCalled()
expect(harness.scheduler.stop).toHaveBeenCalledOnce()
expect(harness.coordinator.correctUnboundTerminals(latest, 'wake-refresh')).toBe(0)
})
it('emits one aggregate for joined work with scheduler and scope metrics', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
harness.coordinator.replaceAuthority(owner)
const input = preparationInput(owner, ['repo'], {
telemetry: {
catalogOutcome: 'degraded',
catalogDurationMs: 12,
gitWorktreeCount: 3,
folderWorkspaceCount: 2,
ambiguousOwnerCount: 1,
contradictoryOwnerCount: 1
}
})
const first = harness.coordinator.prepareOnly(input)
const joined = harness.coordinator.prepareOnly(input)
harness.leases[0].deferred.resolve({
status: 'complete',
metrics: {
queueWaitDurationsMs: [8],
providerExecutionDurationsMs: [13],
timeoutRetryCount: 1,
locallySettledWaiterCount: 1,
cancelDebtCount: 1,
replacementAdmissionDelayedCount: 1,
overlappingJoinCount: 2,
peakLocallyUnsettled: 4,
estimatedLateWorkAllowanceCount: 1
}
})
await Promise.all([first, joined])
expect(harness.deps.onTelemetry).toHaveBeenCalledOnce()
expect(harness.deps.onTelemetry).toHaveBeenCalledWith(
expect.objectContaining({
mode: 'prepare-only',
catalogOutcome: 'degraded',
catalogDurationMs: 12,
gitWorktreeCount: 3,
folderWorkspaceCount: 2,
queueWaitDurationsMs: [8],
providerExecutionDurationsMs: [13],
timeoutRetryCount: 1,
cancelDebtCount: 1,
replacementAdmissionDelayedCount: 1,
overlappingJoinCount: 3,
peakLocallyUnsettled: 4
})
)
})
it('does not let a telemetry callback failure affect recovery', async () => {
const harness = createCoordinatorHarness()
const owner = authority('target-a')
harness.current.set(owner.targetId, owner)
vi.mocked(harness.deps.onTelemetry!).mockImplementation(() => {
throw new Error('telemetry unavailable')
})
const pending = harness.coordinator.requestReconnect(owner)
await flush()
harness.complete(0)
await expect(pending).resolves.toMatchObject({ status: 'complete' })
})
})
@@ -1,291 +0,0 @@
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import {
createDirectSshPreparationCoordinator,
type DirectSshPreparationCoordinator
} from './direct-ssh-reconnect-preparation'
import {
combineDirectSshReconnectOutcome,
createEmptyDirectSshRepoOutcomeCounts,
createTerminalOnlyDirectSshReconnectOutcome
} from './direct-ssh-reconnect-coordinator-outcomes'
import {
createDirectSshReconnectTargetState,
type DirectSshReconnectTargetState
} from './direct-ssh-reconnect-coordinator-stabilization'
import { createDirectSshCoordinatorTelemetryReporter } from './direct-ssh-reconnect-coordinator-telemetry'
import type {
DirectSshCorrectionReason,
DirectSshPreparationInput,
DirectSshPreparationOutcome,
DirectSshPreparationReason,
DirectSshReconnectCoordinator,
DirectSshReconnectCoordinatorDeps,
DirectSshReconnectOutcome,
DirectSshReconnectTimer
} from './direct-ssh-reconnect-coordinator-types'
import {
directSshAuthoritiesEqual,
isDirectSshPreparationInputHostConsistent,
normalizeDirectSshPreparationInput
} from './direct-ssh-reconnect-tokens'
export type * from './direct-ssh-reconnect-coordinator-types'
export const DIRECT_SSH_RELAY_STABILIZATION_MS = 5_000
export function createDirectSshReconnectCoordinator(
deps: DirectSshReconnectCoordinatorDeps
): DirectSshReconnectCoordinator {
const now = deps.now ?? Date.now
const setTimer =
deps.setTimer ?? ((callback: () => void, delayMs: number) => setTimeout(callback, delayMs))
const clearTimer = deps.clearTimer ?? ((timer: DirectSshReconnectTimer) => clearTimeout(timer))
const stabilizationMs = deps.stabilizationMs ?? DIRECT_SSH_RELAY_STABILIZATION_MS
const targets = new Map<string, DirectSshReconnectTargetState>()
let stopped = false
const isCurrent = (authority: DirectSshAuthority): boolean => {
const state = targets.get(authority.targetId)
return (
!stopped &&
directSshAuthoritiesEqual(state?.authority, authority) &&
deps.isCurrentConnectedAuthority(authority)
)
}
const preparation: DirectSshPreparationCoordinator = createDirectSshPreparationCoordinator({
scheduler: deps.scheduler,
isCurrentAuthority: isCurrent,
readLineage: deps.readHostScopedLineage,
now
})
const telemetry = createDirectSshCoordinatorTelemetryReporter({
onTelemetry: deps.onTelemetry,
now
})
const replaceAuthority = (authority: DirectSshAuthority): boolean => {
if (stopped) {
return false
}
const previous = targets.get(authority.targetId)
if (directSshAuthoritiesEqual(previous?.authority, authority)) {
return false
}
if (previous) {
if (previous.timer) {
clearTimer(previous.timer)
}
preparation.invalidateAuthority(previous.authority)
deps.scheduler.disposeProvider(previous.authority)
}
const installedAt = now()
targets.set(
authority.targetId,
createDirectSshReconnectTargetState(authority, previous, installedAt, stabilizationMs)
)
return previous !== undefined
}
const captureInput = async (
authority: DirectSshAuthority,
reason: DirectSshPreparationReason
): Promise<DirectSshPreparationInput | null> => {
const captured = await deps.capturePreparationInput(authority, reason)
if (
!captured ||
!directSshAuthoritiesEqual(captured, authority) ||
!isDirectSshPreparationInputHostConsistent(captured) ||
!isCurrent(authority)
) {
return null
}
return normalizeDirectSshPreparationInput({ ...captured, reason })
}
const runPreparedReconnect = async (
authority: DirectSshAuthority,
staleBindingsCleared: number,
retriedTerminals: number,
damped: boolean,
operationStartedAt = now(),
terminalFinalizationDurationMs = 0,
authorityRotationCount = 0
): Promise<DirectSshReconnectOutcome> => {
const input = await captureInput(authority, 'reconnect')
if (!input) {
const outcome = createTerminalOnlyDirectSshReconnectOutcome(
'stale',
staleBindingsCleared,
retriedTerminals
)
telemetry.reportWithoutInput('reconnect', 'reconnect', outcome, operationStartedAt, {
staleBindingsCleared,
retriedTerminals,
terminalFinalizationDurationMs,
catalogOutcome: 'stale',
catalogDurationMs: Math.max(0, now() - operationStartedAt),
authorityRotationCount,
damped
})
return outcome
}
const acquired = preparation.acquire(input)
const prepared = await acquired.promise
let correctedTerminals = 0
if (prepared.token && isCurrent(authority)) {
correctedTerminals = deps.correctUnboundTerminalPanes(authority, 'preparation-complete')
}
const outcome = combineDirectSshReconnectOutcome(
prepared,
staleBindingsCleared,
retriedTerminals,
correctedTerminals
)
if (!acquired.joined) {
telemetry.report('reconnect', input, prepared, operationStartedAt, {
terminalFinalizationDurationMs,
staleBindingsCleared,
retriedTerminals,
correctedTerminals,
damped,
authorityRotationCount
})
}
return outcome
}
const runDelayedPreparation = async (authority: DirectSshAuthority): Promise<void> => {
if (!isCurrent(authority)) {
return
}
await runPreparedReconnect(authority, 0, 0, true, now(), 0, 1)
}
const scheduleLatestPreparation = (state: DirectSshReconnectTargetState): void => {
if (state.timer || state.dampUntil === null) {
return
}
const delayMs = Math.max(0, state.dampUntil - now())
state.timer = setTimer(() => {
state.timer = null
state.dampUntil = null
void runDelayedPreparation(state.authority)
}, delayMs)
}
const requestReconnect = async (
authority: DirectSshAuthority
): Promise<DirectSshReconnectOutcome> => {
const startedAt = now()
if (stopped || !deps.isCurrentConnectedAuthority(authority)) {
const outcome = createTerminalOnlyDirectSshReconnectOutcome(stopped ? 'stopped' : 'stale')
telemetry.reportWithoutInput('reconnect', 'reconnect', outcome, startedAt, {
catalogOutcome: 'degraded'
})
return outcome
}
const rotated = replaceAuthority(authority)
if (!isCurrent(authority)) {
const outcome = createTerminalOnlyDirectSshReconnectOutcome('stale')
telemetry.reportWithoutInput('reconnect', 'reconnect', outcome, startedAt, {
authorityRotationCount: rotated ? 1 : 0
})
return outcome
}
const terminalStartedAt = now()
const staleBindingsCleared = deps.invalidateStaleTerminalBindings(authority)
const retriedTerminals = deps.retryTargetPanes(authority)
const terminalFinalizationDurationMs = Math.max(0, now() - terminalStartedAt)
const state = targets.get(authority.targetId)!
if (state.dampUntil !== null && now() < state.dampUntil) {
scheduleLatestPreparation(state)
const outcome = createTerminalOnlyDirectSshReconnectOutcome(
'stabilizing',
staleBindingsCleared,
retriedTerminals
)
telemetry.reportWithoutInput('reconnect', 'reconnect', outcome, startedAt, {
staleBindingsCleared,
retriedTerminals,
terminalFinalizationDurationMs,
catalogOutcome: 'degraded',
authorityRotationCount: rotated ? 1 : 0,
damped: true
})
return outcome
}
return runPreparedReconnect(
authority,
staleBindingsCleared,
retriedTerminals,
false,
startedAt,
terminalFinalizationDurationMs,
rotated ? 1 : 0
)
}
const prepareOnly = (
rawInput: DirectSshPreparationInput
): Promise<DirectSshPreparationOutcome> => {
const input = normalizeDirectSshPreparationInput(rawInput)
if (stopped || !isCurrent(input) || !isDirectSshPreparationInputHostConsistent(input)) {
const outcome: DirectSshPreparationOutcome = {
status: stopped ? 'stopped' : 'stale',
token: null,
repoOutcomes: createEmptyDirectSshRepoOutcomeCounts(),
lineageOutcome: 'not-started',
metrics: createTerminalOnlyDirectSshReconnectOutcome('stale').metrics
}
telemetry.report('prepare-only', input, outcome, now())
return Promise.resolve(outcome)
}
const startedAt = now()
const acquired = preparation.acquire(input)
if (!acquired.joined) {
void acquired.promise.then((outcome) => {
telemetry.report('prepare-only', input, outcome, startedAt)
})
}
return acquired.promise
}
const correctUnboundTerminals = (
authority: DirectSshAuthority,
reason: DirectSshCorrectionReason
): number => (isCurrent(authority) ? deps.correctUnboundTerminalPanes(authority, reason) : 0)
const invalidate = (targetId: string): void => {
const state = targets.get(targetId)
if (state?.timer) {
clearTimer(state.timer)
}
preparation.invalidateTarget(targetId)
deps.scheduler.invalidateTarget(targetId)
targets.delete(targetId)
}
const stop = (): void => {
if (stopped) {
return
}
stopped = true
for (const state of targets.values()) {
if (state.timer) {
clearTimer(state.timer)
}
}
preparation.stop()
deps.scheduler.stop()
targets.clear()
}
return {
requestReconnect,
prepareOnly,
correctUnboundTerminals,
replaceAuthority,
invalidate,
stop
}
}
@@ -1,39 +0,0 @@
import type { DirectSshWorktreeRefreshOutcome } from './direct-ssh-worktree-refresh-scheduler'
import type { DirectSshPreparationMetrics } from './direct-ssh-reconnect-coordinator-types'
import { createEmptyDirectSshPreparationMetrics } from './direct-ssh-reconnect-coordinator-outcomes'
export function aggregateDirectSshPreparationMetrics(
outcomes: readonly DirectSshWorktreeRefreshOutcome[],
joinCount: number
): DirectSshPreparationMetrics {
const aggregate = createEmptyDirectSshPreparationMetrics()
for (const outcome of outcomes) {
const metrics = outcome.metrics
if (!metrics) {
continue
}
aggregate.queueWaitDurationsMs = [
...aggregate.queueWaitDurationsMs,
...metrics.queueWaitDurationsMs
]
aggregate.providerExecutionDurationsMs = [
...aggregate.providerExecutionDurationsMs,
...metrics.providerExecutionDurationsMs
]
aggregate.timeoutRetryCount += metrics.timeoutRetryCount
aggregate.locallySettledWaiterCount += metrics.locallySettledWaiterCount
aggregate.cancelDebtCount += metrics.cancelDebtCount
aggregate.replacementAdmissionDelayedCount += metrics.replacementAdmissionDelayedCount
aggregate.schedulerOverlappingJoinCount += metrics.overlappingJoinCount
aggregate.peakLocallyUnsettled = Math.max(
aggregate.peakLocallyUnsettled,
metrics.peakLocallyUnsettled
)
aggregate.estimatedLateWorkAllowanceCount = Math.max(
aggregate.estimatedLateWorkAllowanceCount,
metrics.estimatedLateWorkAllowanceCount
)
}
aggregate.schedulerOverlappingJoinCount += joinCount
return aggregate
}
@@ -1,291 +0,0 @@
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import type {
DirectSshWorktreeRefreshLease,
DirectSshWorktreeRefreshOutcome,
DirectSshWorktreeRefreshScheduler,
DirectSshWorktreeRefreshTerminalStatus
} from './direct-ssh-worktree-refresh-scheduler'
import type {
DirectSshLineageOutcome,
DirectSshPreparationInput,
DirectSshPreparationOutcome,
DirectSshPreparationToken,
DirectSshRepoOutcomeCounts
} from './direct-ssh-reconnect-coordinator-types'
import {
createEmptyDirectSshPreparationMetrics,
createEmptyDirectSshRepoOutcomeCounts
} from './direct-ssh-reconnect-coordinator-outcomes'
import { aggregateDirectSshPreparationMetrics } from './direct-ssh-reconnect-preparation-metrics'
import {
directSshAuthoritiesEqual,
directSshPreparationOperationKey,
directSshRepoFingerprint,
normalizeDirectSshPreparationInput
} from './direct-ssh-reconnect-tokens'
type PreparationOperation = {
key: string
input: DirectSshPreparationInput
leases: DirectSshWorktreeRefreshLease[]
joinCount: number
invalidatedAs: 'stale' | 'stopped' | null
settleInvalidation: (reason: 'stale' | 'stopped') => void
invalidation: Promise<'stale' | 'stopped'>
promise: Promise<DirectSshPreparationOutcome>
}
export type DirectSshPreparationCoordinator = {
acquire: (input: DirectSshPreparationInput) => {
promise: Promise<DirectSshPreparationOutcome>
joined: boolean
}
invalidateAuthority: (authority: DirectSshAuthority) => void
invalidateTarget: (targetId: string) => void
stop: () => void
}
type PreparationCoordinatorDeps = {
scheduler: DirectSshWorktreeRefreshScheduler
isCurrentAuthority: (authority: DirectSshAuthority) => boolean
readLineage: (input: DirectSshPreparationInput) => Promise<DirectSshLineageOutcome>
now?: () => number
}
const TERMINAL_STATUSES: readonly DirectSshWorktreeRefreshTerminalStatus[] = [
'complete',
'non-authoritative',
'timed-out',
'cancel-budget-exhausted',
'canceled',
'stale',
'rejected'
]
function countRepoOutcomes(
outcomes: readonly DirectSshWorktreeRefreshOutcome[]
): DirectSshRepoOutcomeCounts {
const counts = createEmptyDirectSshRepoOutcomeCounts()
for (const outcome of outcomes) {
counts[outcome.status]++
}
return counts
}
function awaitRepoLeases(
leases: readonly DirectSshWorktreeRefreshLease[]
): Promise<DirectSshWorktreeRefreshOutcome[]> {
if (leases.length === 0) {
return Promise.resolve([])
}
return new Promise((resolve) => {
const outcomes: DirectSshWorktreeRefreshOutcome[] = []
let remaining = leases.length
leases.forEach((lease, index) => {
void lease.result.then(
(outcome) => {
outcomes[index] = outcome
remaining--
if (remaining === 0) {
resolve(outcomes)
}
},
() => {
outcomes[index] = { status: 'rejected' }
remaining--
if (remaining === 0) {
resolve(outcomes)
}
}
)
})
})
}
function terminalPreparationOutcome(
status: 'canceled' | 'stale' | 'stopped',
repoOutcomes: DirectSshRepoOutcomeCounts,
metrics = createEmptyDirectSshPreparationMetrics()
): DirectSshPreparationOutcome {
return {
status,
token: null,
repoOutcomes,
lineageOutcome: 'not-started',
metrics
}
}
function buildPreparationToken(
input: DirectSshPreparationInput,
outcome: 'complete' | 'degraded'
): DirectSshPreparationToken {
return {
authority: {
targetId: input.targetId,
providerEpoch: input.providerEpoch,
connectionGeneration: input.connectionGeneration
},
catalogRevision: input.catalogRevision,
repoFingerprint: directSshRepoFingerprint(input),
authorityRequirement: input.authorityRequirement,
snapshotRevision: input.snapshotRevision ?? null,
outcome
}
}
function hasOutcome(
counts: DirectSshRepoOutcomeCounts,
statuses: readonly DirectSshWorktreeRefreshTerminalStatus[]
): boolean {
return statuses.some((status) => counts[status] > 0)
}
export function createDirectSshPreparationCoordinator(
deps: PreparationCoordinatorDeps
): DirectSshPreparationCoordinator {
const now = deps.now ?? Date.now
const operations = new Map<string, PreparationOperation>()
let stopped = false
const run = async (operation: PreparationOperation): Promise<DirectSshPreparationOutcome> => {
const { input } = operation
operation.leases = input.repoRefs.map((repo) =>
deps.scheduler.request({
targetId: input.targetId,
providerEpoch: input.providerEpoch,
connectionGeneration: input.connectionGeneration,
repoId: repo.repoId,
executionHostId: repo.executionHostId,
catalogRevision: input.catalogRevision,
authorityRequirement: input.authorityRequirement
})
)
const outcomes = await awaitRepoLeases(operation.leases)
const repoOutcomes = countRepoOutcomes(outcomes)
const metrics = aggregateDirectSshPreparationMetrics(outcomes, operation.joinCount)
if (operation.invalidatedAs) {
return terminalPreparationOutcome(operation.invalidatedAs, repoOutcomes, metrics)
}
if (!deps.isCurrentAuthority(input)) {
return terminalPreparationOutcome('stale', repoOutcomes, metrics)
}
if (hasOutcome(repoOutcomes, ['canceled', 'stale'])) {
const status = repoOutcomes.stale > 0 ? 'stale' : 'canceled'
return terminalPreparationOutcome(status, repoOutcomes, metrics)
}
let lineageOutcome: DirectSshLineageOutcome
const lineageStartedAt = now()
try {
lineageOutcome = await deps.readLineage(input)
} catch {
lineageOutcome = 'degraded'
}
metrics.lineageDurationMs = Math.max(0, now() - lineageStartedAt)
if (operation.invalidatedAs) {
return terminalPreparationOutcome(operation.invalidatedAs, repoOutcomes, metrics)
}
if (!deps.isCurrentAuthority(input) || lineageOutcome === 'stale') {
return terminalPreparationOutcome('stale', repoOutcomes, metrics)
}
if (lineageOutcome === 'canceled') {
return terminalPreparationOutcome('canceled', repoOutcomes, metrics)
}
const degraded =
lineageOutcome === 'degraded' ||
TERMINAL_STATUSES.some((status) => status !== 'complete' && repoOutcomes[status] > 0)
const status = degraded ? 'degraded' : 'complete'
return {
status,
token: buildPreparationToken(input, status),
repoOutcomes,
lineageOutcome,
metrics
}
}
const acquire = (
rawInput: DirectSshPreparationInput
): { promise: Promise<DirectSshPreparationOutcome>; joined: boolean } => {
const input = normalizeDirectSshPreparationInput(rawInput)
const key = directSshPreparationOperationKey(input)
const current = operations.get(key)
if (current) {
current.joinCount++
return { promise: current.promise, joined: true }
}
if (stopped) {
return {
promise: Promise.resolve(
terminalPreparationOutcome('stopped', createEmptyDirectSshRepoOutcomeCounts())
),
joined: false
}
}
const operation: PreparationOperation = {
key,
input,
leases: [],
joinCount: 0,
invalidatedAs: null,
settleInvalidation: () => {},
invalidation: Promise.resolve('stopped'),
promise: Promise.resolve(
terminalPreparationOutcome('stopped', createEmptyDirectSshRepoOutcomeCounts())
)
}
operation.invalidation = new Promise((resolve) => {
operation.settleInvalidation = resolve
})
operations.set(key, operation)
operation.promise = Promise.race([
run(operation),
operation.invalidation.then((reason) =>
terminalPreparationOutcome(reason, createEmptyDirectSshRepoOutcomeCounts())
)
]).finally(() => {
if (operations.get(key) === operation) {
operations.delete(key)
}
})
return { promise: operation.promise, joined: false }
}
const invalidateMatching = (
predicate: (operation: PreparationOperation) => boolean,
reason: 'stale' | 'stopped'
): void => {
for (const operation of operations.values()) {
if (!predicate(operation)) {
continue
}
operation.invalidatedAs = reason
operation.settleInvalidation(reason)
for (const lease of operation.leases) {
lease.release(reason === 'stopped' ? 'stopped' : 'invalidated')
}
}
}
const invalidateAuthority = (authority: DirectSshAuthority): void => {
invalidateMatching(
(operation) => directSshAuthoritiesEqual(operation.input, authority),
'stale'
)
}
const invalidateTarget = (targetId: string): void => {
invalidateMatching((operation) => operation.input.targetId === targetId, 'stale')
}
const stop = (): void => {
if (stopped) {
return
}
stopped = true
invalidateMatching(() => true, 'stopped')
}
return { acquire, invalidateAuthority, invalidateTarget, stop }
}
@@ -1,42 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
DIRECT_SSH_RECONNECT_SESSION_ROUTE_KEY,
isDirectSshReconnectCoordinatorRoutingEnabled,
resolveDirectSshReconnectCoordinatorRouting
} from './direct-ssh-reconnect-rollout'
describe('direct SSH reconnect rollout', () => {
it('defaults coordinator routing on', () => {
expect(resolveDirectSshReconnectCoordinatorRouting({})).toBe(true)
expect(resolveDirectSshReconnectCoordinatorRouting({ buildValue: 'true' })).toBe(true)
})
it('supports build and session disable routes without session re-enablement', () => {
expect(
resolveDirectSshReconnectCoordinatorRouting({
buildValue: 'false',
sessionValue: 'true'
})
).toBe(false)
expect(
resolveDirectSshReconnectCoordinatorRouting({
buildValue: 'true',
sessionValue: 'false'
})
).toBe(false)
})
it('reads the current session route and fails open when storage is unavailable', () => {
const getItem = vi.fn(() => 'false')
vi.stubGlobal('sessionStorage', { getItem })
expect(isDirectSshReconnectCoordinatorRoutingEnabled()).toBe(false)
expect(getItem).toHaveBeenCalledWith(DIRECT_SSH_RECONNECT_SESSION_ROUTE_KEY)
getItem.mockImplementation(() => {
throw new Error('blocked')
})
expect(isDirectSshReconnectCoordinatorRoutingEnabled()).toBe(true)
vi.unstubAllGlobals()
})
})
@@ -1,25 +0,0 @@
export const DIRECT_SSH_RECONNECT_SESSION_ROUTE_KEY = 'orca.directSshReconnectCoordinator.enabled'
export function resolveDirectSshReconnectCoordinatorRouting(args: {
buildValue?: string
sessionValue?: string | null
}): boolean {
if (args.buildValue?.trim().toLowerCase() === 'false') {
return false
}
return args.sessionValue?.trim().toLowerCase() !== 'false'
}
export function isDirectSshReconnectCoordinatorRoutingEnabled(): boolean {
let sessionValue: string | null = null
try {
sessionValue =
globalThis.sessionStorage?.getItem(DIRECT_SSH_RECONNECT_SESSION_ROUTE_KEY) ?? null
} catch {
// Storage can be unavailable in privacy-restricted renderer sessions.
}
return resolveDirectSshReconnectCoordinatorRouting({
buildValue: import.meta.env.VITE_DIRECT_SSH_RECONNECT_COORDINATOR,
sessionValue
})
}
@@ -1,27 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import { directSshAuthoritiesEqual } from './direct-ssh-reconnect-tokens'
const authority: DirectSshAuthority = {
targetId: 'target-a',
providerEpoch: 'epoch-a' as SshProviderEpoch,
connectionGeneration: 1
}
describe('directSshAuthoritiesEqual', () => {
it('fails closed when either authority is missing', () => {
expect(directSshAuthoritiesEqual(null, null)).toBe(false)
expect(directSshAuthoritiesEqual(authority, null)).toBe(false)
expect(directSshAuthoritiesEqual(undefined, authority)).toBe(false)
})
it('requires the complete authority tuple', () => {
expect(directSshAuthoritiesEqual(authority, { ...authority })).toBe(true)
expect(
directSshAuthoritiesEqual(authority, {
...authority,
connectionGeneration: authority.connectionGeneration + 1
})
).toBe(false)
})
})
@@ -1,58 +0,0 @@
import { toSshExecutionHostId } from '../../../shared/execution-host'
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import type { DirectSshPreparationInput } from './direct-ssh-reconnect-coordinator-types'
export function directSshAuthoritiesEqual(
left: DirectSshAuthority | null | undefined,
right: DirectSshAuthority | null | undefined
): boolean {
if (!left || !right) {
return false
}
return (
left.targetId === right.targetId &&
left.providerEpoch === right.providerEpoch &&
left.connectionGeneration === right.connectionGeneration
)
}
export function normalizeDirectSshPreparationInput(
input: DirectSshPreparationInput
): DirectSshPreparationInput {
return {
...input,
repoRefs: [...input.repoRefs].sort(
(left, right) =>
compareText(left.executionHostId, right.executionHostId) ||
compareText(left.repoId, right.repoId)
)
}
}
function compareText(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0
}
export function isDirectSshPreparationInputHostConsistent(
input: DirectSshPreparationInput
): boolean {
const expectedHost = toSshExecutionHostId(input.targetId)
return input.repoRefs.every((repo) => repo.executionHostId === expectedHost)
}
export function directSshRepoFingerprint(input: DirectSshPreparationInput): string {
return JSON.stringify(input.repoRefs.map((repo) => [repo.executionHostId, repo.repoId]))
}
export function directSshPreparationOperationKey(input: DirectSshPreparationInput): string {
return JSON.stringify([
input.targetId,
input.providerEpoch,
input.connectionGeneration,
input.catalogRevision,
directSshRepoFingerprint(input),
input.authorityRequirement,
input.snapshotRevision ?? null,
input.reason
])
}
@@ -142,16 +142,13 @@ describe('runtime and direct SSH wake isolation', () => {
]
])
})
const staleAuthority = { ...currentAuthority, connectionGeneration: 6 }
expect(store.getState().invalidateStaleDirectSshTargetPtyBindings(staleAuthority)).toBe(0)
expect(store.getState().tabsByWorktree[directWorktreeId][0].ptyId).toBe(directPtyId)
const wakePreparation = vi.fn()
const correctedCounts: number[] = []
const unregisterDirectWake = registerDirectSshWakeRouting({
getConnectionStates: () => store.getState().sshConnectionStates,
wakeAuthority: (nextAuthority) => {
correctedCounts.push(
store.getState().invalidateStaleDirectSshTargetPtyBindings(nextAuthority)
store.getState().clearDirectSshTargetPtyBindings(nextAuthority.targetId)
)
wakePreparation(nextAuthority)
},
@@ -3,8 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import {
directSshAuthorityFromConnectionState,
registerDirectSshWakeRouting,
routeDirectSshConnectedState
registerDirectSshWakeRouting
} from './direct-ssh-state-routing'
function authority(epoch = 'epoch-a'): DirectSshAuthority {
@@ -15,109 +14,6 @@ function authority(epoch = 'epoch-a'): DirectSshAuthority {
}
}
function harness() {
return {
coordinatorRoutingEnabled: true,
coordinator: {
requestReconnect: vi.fn(async () => ({ status: 'complete' })),
correctUnboundTerminals: vi.fn(() => 0),
replaceAuthority: vi.fn()
},
invalidateStaleTerminalBindings: vi.fn(() => 0),
retryTargetPanes: vi.fn(() => 0),
prepareHost: vi.fn(),
rememberReconnectAuthority: vi.fn()
}
}
describe('routeDirectSshConnectedState', () => {
it('keeps initial hydration preparation-only', () => {
const deps = harness()
const current = authority()
expect(
routeDirectSshConnectedState(deps, {
authority: current,
previousAuthority: null,
origin: 'initial-hydration'
})
).toBe('initial-hydration')
expect(deps.prepareHost).toHaveBeenCalledWith(current, 'initial-hydration')
expect(deps.coordinator.requestReconnect).not.toHaveBeenCalled()
expect(deps.coordinator.correctUnboundTerminals).not.toHaveBeenCalled()
})
it('requests reconnect only when the complete pushed authority changes', () => {
const deps = harness()
const previous = authority('epoch-old')
const current = authority('epoch-new')
expect(
routeDirectSshConnectedState(deps, {
authority: current,
previousAuthority: previous,
origin: 'push'
})
).toBe('changed-authority')
expect(deps.rememberReconnectAuthority).toHaveBeenCalledWith(current)
expect(deps.coordinator.requestReconnect).toHaveBeenCalledWith(current)
expect(deps.coordinator.replaceAuthority).not.toHaveBeenCalled()
expect(deps.prepareHost).not.toHaveBeenCalled()
})
it('runs the disabled fallback terminal actions synchronously before bounded preparation', () => {
const order: string[] = []
const deps = harness()
deps.coordinatorRoutingEnabled = false
deps.coordinator.replaceAuthority.mockImplementation(() => order.push('replace'))
deps.invalidateStaleTerminalBindings.mockImplementation(() => {
order.push('invalidate')
return 1
})
deps.retryTargetPanes.mockImplementation(() => {
order.push('retry')
return 1
})
deps.prepareHost.mockImplementation(() => {
order.push('prepare')
return new Promise(() => {})
})
const previous = authority('epoch-old')
const current = authority('epoch-new')
expect(
routeDirectSshConnectedState(deps, {
authority: current,
previousAuthority: previous,
origin: 'push'
})
).toBe('changed-authority-fallback')
expect(order).toEqual(['replace', 'invalidate', 'retry', 'prepare'])
expect(deps.coordinator.requestReconnect).not.toHaveBeenCalled()
expect(deps.prepareHost).toHaveBeenCalledWith(current, 'reconnect', {
authorityAlreadyReplaced: true
})
expect(deps.rememberReconnectAuthority).toHaveBeenCalledWith(current)
})
it('routes an exact-equal push through correction and fresh preparation', () => {
const deps = harness()
const current = authority()
expect(
routeDirectSshConnectedState(deps, {
authority: current,
previousAuthority: { ...current },
origin: 'push'
})
).toBe('same-authority-wake')
expect(deps.coordinator.correctUnboundTerminals).toHaveBeenCalledWith(current, 'wake-refresh')
expect(deps.prepareHost).toHaveBeenCalledWith(current, 'wake-refresh')
expect(deps.coordinator.requestReconnect).not.toHaveBeenCalled()
})
})
describe('registerDirectSshWakeRouting', () => {
it('wakes only complete connected direct authorities and removes both listeners', () => {
const wakeAuthority = vi.fn()
@@ -1,62 +1,7 @@
import type { DirectSshAuthority, SshConnectionState } from '../../../shared/ssh-types'
import { directSshAuthoritiesEqual } from './direct-ssh-reconnect-tokens'
export type DirectSshConnectedStateOrigin = 'push' | 'initial-hydration'
export type DirectSshConnectedStateRoutingDeps = {
coordinator: {
requestReconnect: (authority: DirectSshAuthority) => Promise<unknown>
correctUnboundTerminals: (authority: DirectSshAuthority, reason: 'wake-refresh') => number
replaceAuthority: (authority: DirectSshAuthority) => void
}
coordinatorRoutingEnabled: boolean
invalidateStaleTerminalBindings: (authority: DirectSshAuthority) => number
retryTargetPanes: (authority: DirectSshAuthority) => number
prepareHost: (
authority: DirectSshAuthority,
reason: 'reconnect' | 'initial-hydration' | 'wake-refresh',
options?: { authorityAlreadyReplaced?: boolean }
) => void | Promise<void>
rememberReconnectAuthority: (authority: DirectSshAuthority | null) => void
}
export type DirectSshConnectedStateRoute =
| 'initial-hydration'
| 'changed-authority'
| 'changed-authority-fallback'
| 'same-authority-wake'
export function routeDirectSshConnectedState(
deps: DirectSshConnectedStateRoutingDeps,
input: {
authority: DirectSshAuthority
previousAuthority: DirectSshAuthority | null
origin: DirectSshConnectedStateOrigin
}
): DirectSshConnectedStateRoute {
const { authority } = input
if (input.origin === 'initial-hydration') {
deps.rememberReconnectAuthority(null)
void deps.prepareHost(authority, 'initial-hydration')
return 'initial-hydration'
}
if (!directSshAuthoritiesEqual(input.previousAuthority, authority)) {
deps.rememberReconnectAuthority(authority)
if (deps.coordinatorRoutingEnabled) {
void deps.coordinator.requestReconnect(authority)
return 'changed-authority'
}
deps.coordinator.replaceAuthority(authority)
deps.invalidateStaleTerminalBindings(authority)
deps.retryTargetPanes(authority)
void deps.prepareHost(authority, 'reconnect', { authorityAlreadyReplaced: true })
return 'changed-authority-fallback'
}
deps.coordinator.correctUnboundTerminals(authority, 'wake-refresh')
void deps.prepareHost(authority, 'wake-refresh')
return 'same-authority-wake'
}
export function directSshAuthorityFromConnectionState(
targetId: string,
state: SshConnectionState
@@ -1,16 +0,0 @@
import type {
DirectSshWorktreeRefreshAttempt,
DirectSshWorktreeRefreshReleaseReason
} from './direct-ssh-worktree-refresh-scheduler-types'
export function cancelDirectSshWorktreeRefreshAttempt(
attempt: DirectSshWorktreeRefreshAttempt,
reason: DirectSshWorktreeRefreshReleaseReason
): boolean {
try {
const outcome = attempt.cancel(reason)
return outcome !== false && outcome !== 'retained' && outcome !== 'already-settled'
} catch {
return true
}
}
@@ -1,95 +0,0 @@
import type {
DirectSshWorktreeRefreshLogicalTask,
DirectSshWorktreeRefreshMetrics,
DirectSshWorktreeRefreshSchedulerSnapshot
} from './direct-ssh-worktree-refresh-scheduler-types'
export function createDirectSshWorktreeRefreshMetrics(): DirectSshWorktreeRefreshMetrics {
return {
queueWaitDurationsMs: [],
providerExecutionDurationsMs: [],
timeoutRetryCount: 0,
locallySettledWaiterCount: 0,
cancelDebtCount: 0,
replacementAdmissionDelayedCount: 0,
overlappingJoinCount: 0,
peakLocallyUnsettled: 0,
estimatedLateWorkAllowanceCount: 0
}
}
export function adjustDirectSshAuthorityUnsettled(
unsettledByAuthority: Map<string, number>,
authorityId: string,
delta: number
): void {
const next = (unsettledByAuthority.get(authorityId) ?? 0) + delta
if (next > 0) {
unsettledByAuthority.set(authorityId, next)
} else {
unsettledByAuthority.delete(authorityId)
}
}
export function copyDirectSshWorktreeRefreshMetrics(
metrics: DirectSshWorktreeRefreshMetrics
): DirectSshWorktreeRefreshMetrics {
return {
...metrics,
queueWaitDurationsMs: [...metrics.queueWaitDurationsMs],
providerExecutionDurationsMs: [...metrics.providerExecutionDurationsMs]
}
}
export function recordDirectSshCancelDebt(
metrics: DirectSshWorktreeRefreshMetrics,
lateWorkLimit: number
): void {
metrics.cancelDebtCount++
metrics.estimatedLateWorkAllowanceCount = Math.min(
lateWorkLimit,
metrics.estimatedLateWorkAllowanceCount + 1
)
}
export function recordDirectSshQueueAdmission(
metrics: DirectSshWorktreeRefreshMetrics,
queueWaitDurationMs: number,
locallyUnsettled: number
): void {
metrics.queueWaitDurationsMs = [...metrics.queueWaitDurationsMs, queueWaitDurationMs]
metrics.peakLocallyUnsettled = Math.max(metrics.peakLocallyUnsettled, locallyUnsettled)
}
export function recordDirectSshProviderExecution(
metrics: DirectSshWorktreeRefreshMetrics,
providerExecutionDurationMs: number
): void {
metrics.providerExecutionDurationsMs = [
...metrics.providerExecutionDurationsMs,
providerExecutionDurationMs
]
}
export function directSshWorktreeRefreshSchedulerSnapshot(
tasks: ReadonlyMap<string, DirectSshWorktreeRefreshLogicalTask>,
locallyUnsettled: number,
cancelDebtByAuthority: ReadonlyMap<string, number>
): DirectSshWorktreeRefreshSchedulerSnapshot {
let queued = 0
let retrying = 0
let waiters = 0
for (const task of tasks.values()) {
queued += task.state === 'queued' ? 1 : 0
retrying += task.state === 'retrying' ? 1 : 0
waiters += task.waiters.size
}
return {
locallyUnsettled,
queued,
retrying,
logicalTasks: tasks.size,
waiters,
cancelDebtByAuthority: new Map(cancelDebtByAuthority)
}
}
@@ -1,128 +0,0 @@
import type {
HostQualifiedDetectedWorktreeResult,
ProviderRequestId,
SshExecutionHostId
} from '../../../shared/detected-worktree-provider-contract'
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import type {
DetectedWorktreeRefreshReleaseOutcome,
WaiterLeaseId
} from '../store/slices/detected-worktree-refresh-leases'
export type { WaiterLeaseId } from '../store/slices/detected-worktree-refresh-leases'
export type DirectSshWorktreeRefreshAuthorityRequirement = 'required' | 'allow-metadata-fallback'
export type DirectSshWorktreeRefreshKey = DirectSshAuthority & {
repoId: string
executionHostId: SshExecutionHostId
catalogRevision: number
authorityRequirement: DirectSshWorktreeRefreshAuthorityRequirement
}
export type DirectSshWorktreeRefreshTerminalStatus =
| 'complete'
| 'non-authoritative'
| 'timed-out'
| 'cancel-budget-exhausted'
| 'canceled'
| 'stale'
| 'rejected'
export type DirectSshWorktreeRefreshOutcome = {
status: DirectSshWorktreeRefreshTerminalStatus
providerRequestId?: ProviderRequestId
providerResult?: HostQualifiedDetectedWorktreeResult
metrics?: DirectSshWorktreeRefreshMetrics
}
export type DirectSshWorktreeRefreshMetrics = {
queueWaitDurationsMs: readonly number[]
providerExecutionDurationsMs: readonly number[]
timeoutRetryCount: number
locallySettledWaiterCount: number
cancelDebtCount: number
replacementAdmissionDelayedCount: number
overlappingJoinCount: number
peakLocallyUnsettled: number
estimatedLateWorkAllowanceCount: number
}
export type DirectSshWorktreeRefreshReleaseReason = 'superseded' | 'invalidated' | 'stopped'
export type DirectSshWorktreeRefreshLease = {
waiterLeaseId: WaiterLeaseId
result: Promise<DirectSshWorktreeRefreshOutcome>
release: (reason: DirectSshWorktreeRefreshReleaseReason) => void
}
export type DirectSshWorktreeRefreshAttempt = {
providerRequestId: ProviderRequestId
result: Promise<HostQualifiedDetectedWorktreeResult>
cancel: (
reason: DirectSshWorktreeRefreshReleaseReason
) => boolean | void | DetectedWorktreeRefreshReleaseOutcome
}
export type DirectSshWorktreeRefreshSchedulerDeps = {
startAttempt: (key: DirectSshWorktreeRefreshKey) => DirectSshWorktreeRefreshAttempt
createWaiterLeaseId?: () => WaiterLeaseId
onUnexpectedError?: (error: unknown) => void
now?: () => number
}
export type DirectSshWorktreeRefreshSchedulerSnapshot = {
locallyUnsettled: number
queued: number
retrying: number
logicalTasks: number
waiters: number
cancelDebtByAuthority: ReadonlyMap<string, number>
}
export type DirectSshWorktreeRefreshScheduler = {
request: (key: DirectSshWorktreeRefreshKey) => DirectSshWorktreeRefreshLease
invalidateAuthority: (authority: DirectSshAuthority) => void
invalidateTarget: (targetId: string) => void
disposeProvider: (authority: DirectSshAuthority) => void
getSnapshot: () => DirectSshWorktreeRefreshSchedulerSnapshot
stop: () => void
}
export type DirectSshWorktreeRefreshWaiter = {
resolve: (outcome: DirectSshWorktreeRefreshOutcome) => void
}
export type DirectSshWorktreeRefreshLogicalTask = {
key: DirectSshWorktreeRefreshKey
keyId: string
authorityId: string
state: 'queued' | 'running' | 'retrying' | 'terminal'
attemptCount: number
attempt: DirectSshWorktreeRefreshAttempt | null
attemptCanceled: boolean
attemptStartedAt: number | null
queuedAt: number
metrics: DirectSshWorktreeRefreshMetrics
waiters: Map<WaiterLeaseId, DirectSshWorktreeRefreshWaiter>
}
export function directSshProviderAuthorityKey(authority: DirectSshAuthority): string {
return JSON.stringify([
authority.targetId,
authority.providerEpoch,
authority.connectionGeneration
])
}
export function directSshWorktreeRefreshKey(key: DirectSshWorktreeRefreshKey): string {
return JSON.stringify([
key.targetId,
key.repoId,
key.executionHostId,
key.providerEpoch,
key.connectionGeneration,
key.catalogRevision,
key.authorityRequirement
])
}
@@ -1,435 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type {
HostQualifiedDetectedWorktreeResult,
ProviderRequestId
} from '../../../shared/detected-worktree-provider-contract'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types'
import {
createDirectSshWorktreeRefreshScheduler,
DIRECT_SSH_WORKTREE_SCAN_CONCURRENCY,
type DirectSshWorktreeRefreshAttempt,
type DirectSshWorktreeRefreshKey,
type DirectSshWorktreeRefreshReleaseReason
} from './direct-ssh-worktree-refresh-scheduler'
type Deferred<T> = {
promise: Promise<T>
resolve: (value: T) => void
reject: (error: unknown) => void
}
type ControlledAttempt = DirectSshWorktreeRefreshAttempt & {
key: DirectSshWorktreeRefreshKey
deferred: Deferred<HostQualifiedDetectedWorktreeResult>
cancel: ReturnType<typeof vi.fn<DirectSshWorktreeRefreshAttempt['cancel']>>
}
function deferred<T>(): Deferred<T> {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise
reject = rejectPromise
})
return { promise, resolve, reject }
}
function epoch(value: string): SshProviderEpoch {
return value as SshProviderEpoch
}
function requestId(value: string): ProviderRequestId {
return value as ProviderRequestId
}
function key(
targetId: string,
repoId: string,
overrides: Partial<DirectSshWorktreeRefreshKey> = {}
): DirectSshWorktreeRefreshKey {
return {
targetId,
repoId,
executionHostId: `ssh:${targetId}`,
providerEpoch: epoch(`epoch-${targetId}`),
connectionGeneration: 1,
catalogRevision: 1,
authorityRequirement: 'required',
...overrides
}
}
function terminalResult(
attempt: ControlledAttempt,
status: 'timed-out' | 'canceled' | 'stale' | 'rejected'
): HostQualifiedDetectedWorktreeResult {
return {
status,
providerRequestId: attempt.providerRequestId,
executionHostId: attempt.key.executionHostId
}
}
function completeResult(
attempt: ControlledAttempt,
status: 'complete' | 'non-authoritative' = 'complete'
): HostQualifiedDetectedWorktreeResult {
return {
status,
providerRequestId: attempt.providerRequestId,
repoId: attempt.key.repoId,
authority: {
kind: 'direct-ssh',
executionHostId: attempt.key.executionHostId,
targetId: attempt.key.targetId,
providerEpoch: attempt.key.providerEpoch,
connectionGeneration: attempt.key.connectionGeneration
},
result: {
repoId: attempt.key.repoId,
authoritative: status === 'complete',
source: status === 'complete' ? 'git' : 'metadata-fallback',
worktrees: []
}
}
}
function createHarness(cancelOutcome?: 'cancel-failed', now?: () => number) {
const attempts: ControlledAttempt[] = []
const onUnexpectedError = vi.fn()
const startAttempt = vi.fn((attemptKey: DirectSshWorktreeRefreshKey) => {
const providerDeferred = deferred<HostQualifiedDetectedWorktreeResult>()
const waiterDeferred = deferred<HostQualifiedDetectedWorktreeResult>()
const providerRequestId = requestId(`provider-${attempts.length + 1}`)
const attempt: ControlledAttempt = {
key: attemptKey,
providerRequestId,
result: waiterDeferred.promise,
deferred: providerDeferred,
cancel: vi.fn((_reason: DirectSshWorktreeRefreshReleaseReason) => {
waiterDeferred.resolve({
providerRequestId,
executionHostId: attemptKey.executionHostId,
status: 'canceled'
})
return cancelOutcome
})
}
void providerDeferred.promise.then(waiterDeferred.resolve, waiterDeferred.reject)
attempts.push(attempt)
return attempt
})
const scheduler = createDirectSshWorktreeRefreshScheduler({
startAttempt,
onUnexpectedError,
now
})
return { scheduler, attempts, startAttempt, onUnexpectedError }
}
async function flushAttempt(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
}
describe('createDirectSshWorktreeRefreshScheduler', () => {
it('admits an idle singleton immediately and cleans it up on completion', async () => {
const { scheduler, attempts, startAttempt } = createHarness()
const lease = scheduler.request(key('target-a', 'repo-a'))
expect(startAttempt).toHaveBeenCalledTimes(1)
expect(scheduler.getSnapshot().locallyUnsettled).toBe(1)
attempts[0].deferred.resolve(completeResult(attempts[0]))
await expect(lease.result).resolves.toMatchObject({ status: 'complete' })
await flushAttempt()
expect(scheduler.getSnapshot()).toMatchObject({
locallyUnsettled: 0,
logicalTasks: 0,
waiters: 0
})
})
it('never admits more than five locally unsettled attempts', async () => {
const { scheduler, attempts } = createHarness()
const leases = Array.from({ length: 6 }, (_, index) =>
scheduler.request(key('target-a', `repo-${index}`))
)
expect(attempts).toHaveLength(DIRECT_SSH_WORKTREE_SCAN_CONCURRENCY)
expect(scheduler.getSnapshot().queued).toBe(1)
attempts[0].deferred.resolve(completeResult(attempts[0]))
await leases[0].result
await flushAttempt()
expect(attempts).toHaveLength(6)
expect(scheduler.getSnapshot().locallyUnsettled).toBe(5)
scheduler.stop()
})
it('round-robins target lanes after at most one earlier lane admission', async () => {
const { scheduler, attempts } = createHarness()
const leases = Array.from({ length: 7 }, (_, index) =>
scheduler.request(key('target-a', `repo-a-${index}`))
)
const targetB = scheduler.request(key('target-b', 'repo-b'))
attempts[0].deferred.resolve(completeResult(attempts[0]))
await leases[0].result
await flushAttempt()
expect(attempts[5].key.targetId).toBe('target-a')
attempts[1].deferred.resolve(completeResult(attempts[1]))
await leases[1].result
await flushAttempt()
expect(attempts[6].key.targetId).toBe('target-b')
scheduler.stop()
await expect(targetB.result).resolves.toMatchObject({ status: 'canceled' })
})
it('joins only exact keys and separates authority, revision, and requirement', () => {
const { scheduler, attempts } = createHarness()
const base = key('target-a', 'repo')
const first = scheduler.request(base)
const joined = scheduler.request({ ...base })
scheduler.request({ ...base, providerEpoch: epoch('epoch-next') })
scheduler.request({ ...base, connectionGeneration: 2 })
scheduler.request({ ...base, catalogRevision: 2 })
scheduler.request({ ...base, authorityRequirement: 'allow-metadata-fallback' })
expect(first.waiterLeaseId).not.toBe(joined.waiterLeaseId)
expect(attempts).toHaveLength(5)
expect(scheduler.getSnapshot()).toMatchObject({ logicalTasks: 5, waiters: 6 })
scheduler.stop()
})
it('keeps the logical barrier pending through one timeout and uses a fresh request ID', async () => {
const { scheduler, attempts } = createHarness()
const lease = scheduler.request(key('target-a', 'repo'))
let settled = false
void lease.result.then(() => {
settled = true
})
attempts[0].deferred.resolve(terminalResult(attempts[0], 'timed-out'))
await flushAttempt()
expect(settled).toBe(false)
expect(attempts).toHaveLength(2)
expect(attempts[1].providerRequestId).not.toBe(attempts[0].providerRequestId)
attempts[1].deferred.resolve(completeResult(attempts[1]))
await expect(lease.result).resolves.toMatchObject({
status: 'complete',
providerRequestId: attempts[1].providerRequestId
})
})
it('requeues a timed-out retry at its target lane tail', async () => {
const { scheduler, attempts } = createHarness()
const retrying = scheduler.request(key('target-a', 'retrying'))
const blockers = Array.from({ length: 4 }, (_, index) =>
scheduler.request(key('target-a', `blocker-${index}`))
)
const targetB = scheduler.request(key('target-b', 'target-b'))
attempts[0].deferred.resolve(terminalResult(attempts[0], 'timed-out'))
await flushAttempt()
expect(attempts[5].key.targetId).toBe('target-b')
attempts[1].deferred.resolve(completeResult(attempts[1]))
await blockers[0].result
await flushAttempt()
expect(attempts[6].key.repoId).toBe('retrying')
attempts[6].deferred.resolve(completeResult(attempts[6]))
await expect(retrying.result).resolves.toMatchObject({ status: 'complete' })
scheduler.stop()
await expect(targetB.result).resolves.toMatchObject({ status: 'canceled' })
})
it('makes a second timeout terminal', async () => {
const { scheduler, attempts } = createHarness()
const lease = scheduler.request(key('target-a', 'repo'))
attempts[0].deferred.resolve(terminalResult(attempts[0], 'timed-out'))
await flushAttempt()
attempts[1].deferred.resolve(terminalResult(attempts[1], 'timed-out'))
await expect(lease.result).resolves.toMatchObject({
status: 'timed-out',
providerRequestId: attempts[1].providerRequestId
})
})
it('invalidates queued and current work without reporting cancellation errors', async () => {
const { scheduler, attempts, onUnexpectedError } = createHarness()
const leases = Array.from({ length: 6 }, (_, index) =>
scheduler.request(key('target-a', `repo-${index}`))
)
scheduler.invalidateTarget('target-a')
await expect(Promise.all(leases.map((lease) => lease.result))).resolves.toEqual(
Array.from({ length: 6 }, () => expect.objectContaining({ status: 'stale' }))
)
expect(attempts.map((attempt) => attempt.cancel.mock.calls.length)).toEqual([1, 1, 1, 1, 1])
expect(scheduler.getSnapshot()).toMatchObject({ logicalTasks: 0, waiters: 0 })
for (const attempt of attempts) {
attempt.deferred.reject(new Error('expected provider cancellation'))
}
await flushAttempt()
expect(onUnexpectedError).not.toHaveBeenCalled()
expect(scheduler.getSnapshot().locallyUnsettled).toBe(0)
})
it('bounds five failed cancellations to two replacements while old calls remain pending', async () => {
const { scheduler, attempts } = createHarness('cancel-failed')
const authority: DirectSshAuthority = {
targetId: 'target-a',
providerEpoch: epoch('epoch-target-a'),
connectionGeneration: 1
}
const initial = Array.from({ length: 5 }, (_, index) =>
scheduler.request(key('target-a', `old-${index}`))
)
scheduler.invalidateAuthority(authority)
await Promise.all(initial.map((lease) => lease.result))
await flushAttempt()
expect(scheduler.getSnapshot()).toMatchObject({
locallyUnsettled: 0,
queued: 0,
logicalTasks: 0
})
const replacements = Array.from({ length: 3 }, (_, index) =>
scheduler.request(key('target-a', `replacement-${index}`))
)
expect(attempts).toHaveLength(7)
expect(scheduler.getSnapshot().locallyUnsettled).toBe(2)
await expect(replacements[2].result).resolves.toMatchObject({
status: 'cancel-budget-exhausted'
})
attempts[5].deferred.resolve(completeResult(attempts[5]))
attempts[6].deferred.resolve(completeResult(attempts[6]))
await expect(
Promise.all(replacements.slice(0, 2).map((lease) => lease.result))
).resolves.toEqual(
Array.from({ length: 2 }, () => expect.objectContaining({ status: 'complete' }))
)
await flushAttempt()
expect(scheduler.getSnapshot().locallyUnsettled).toBe(0)
scheduler.disposeProvider(authority)
for (const attempt of attempts.slice(0, 5)) {
attempt.deferred.reject(new Error('late canceled provider'))
}
await flushAttempt()
expect([...scheduler.getSnapshot().cancelDebtByAuthority.values()]).toEqual([])
const afterDisposal = scheduler.request(key('target-a', 'after-disposal'))
expect(attempts).toHaveLength(8)
attempts[7].deferred.resolve(completeResult(attempts[7]))
await expect(afterDisposal.result).resolves.toMatchObject({ status: 'complete' })
})
it('settles one released waiter without canceling a joined provider', async () => {
const { scheduler, attempts } = createHarness()
const refreshKey = key('target-a', 'repo')
const released = scheduler.request(refreshKey)
const retained = scheduler.request(refreshKey)
released.release('superseded')
await expect(released.result).resolves.toMatchObject({ status: 'canceled' })
expect(attempts[0].cancel).not.toHaveBeenCalled()
expect(scheduler.getSnapshot().waiters).toBe(1)
attempts[0].deferred.resolve(completeResult(attempts[0]))
await expect(retained.result).resolves.toMatchObject({ status: 'complete' })
released.release('stopped')
await flushAttempt()
expect(scheduler.getSnapshot()).toMatchObject({ logicalTasks: 0, waiters: 0 })
})
it('does not charge cancel debt when a shared provider invocation stays active', async () => {
const { scheduler, attempts } = createHarness()
const lease = scheduler.request(key('target-a', 'repo'))
attempts[0].cancel.mockReturnValue(false)
lease.release('superseded')
await expect(lease.result).resolves.toMatchObject({ status: 'canceled' })
expect(attempts[0].cancel).toHaveBeenCalledOnce()
expect([...scheduler.getSnapshot().cancelDebtByAuthority.values()]).toEqual([])
attempts[0].deferred.resolve(completeResult(attempts[0]))
await flushAttempt()
})
it('stops current and queued waiters and rejects later requests without starting', async () => {
const { scheduler, attempts, startAttempt } = createHarness()
const current = scheduler.request(key('target-a', 'current'))
scheduler.stop()
await expect(current.result).resolves.toMatchObject({ status: 'canceled' })
expect(attempts[0].cancel).toHaveBeenCalledOnce()
const afterStop = scheduler.request(key('target-a', 'after-stop'))
await expect(afterStop.result).resolves.toMatchObject({ status: 'canceled' })
expect(startAttempt).toHaveBeenCalledOnce()
expect(scheduler.getSnapshot()).toMatchObject({ logicalTasks: 0, waiters: 0 })
attempts[0].deferred.reject(new Error('stopped'))
await flushAttempt()
})
it('surfaces non-authoritative and rejected terminal outcomes', async () => {
const { scheduler, attempts } = createHarness()
const metadata = scheduler.request(key('target-a', 'metadata'))
const rejected = scheduler.request(key('target-a', 'rejected'))
attempts[0].deferred.resolve(completeResult(attempts[0], 'non-authoritative'))
attempts[1].deferred.resolve(terminalResult(attempts[1], 'rejected'))
await expect(metadata.result).resolves.toMatchObject({ status: 'non-authoritative' })
await expect(rejected.result).resolves.toMatchObject({ status: 'rejected' })
})
it('reports queue wait, provider execution, retry, joins, and scoped peak metrics', async () => {
let now = 0
const { scheduler, attempts } = createHarness(undefined, () => now)
const leases = Array.from({ length: 6 }, (_, index) =>
scheduler.request(key('target-a', `repo-${index}`))
)
scheduler.request(key('target-a', 'repo-5'))
now = 25
attempts[0].deferred.resolve(completeResult(attempts[0]))
await leases[0].result
await flushAttempt()
expect(attempts).toHaveLength(6)
now = 100
attempts[5].deferred.resolve(terminalResult(attempts[5], 'timed-out'))
await flushAttempt()
now = 140
attempts[6].deferred.resolve(completeResult(attempts[6]))
await expect(leases[5].result).resolves.toMatchObject({
status: 'complete',
metrics: {
queueWaitDurationsMs: [25, 0],
providerExecutionDurationsMs: [75, 40],
timeoutRetryCount: 1,
cancelDebtCount: 1,
overlappingJoinCount: 1,
peakLocallyUnsettled: 5
}
})
scheduler.stop()
})
})
@@ -1,316 +0,0 @@
import type {
HostQualifiedDetectedWorktreeResult,
ProviderRequestId
} from '../../../shared/detected-worktree-provider-contract'
import type { DirectSshAuthority } from '../../../shared/ssh-types'
import type {
DirectSshWorktreeRefreshAttempt,
DirectSshWorktreeRefreshKey,
DirectSshWorktreeRefreshLease,
DirectSshWorktreeRefreshLogicalTask as LogicalTask,
DirectSshWorktreeRefreshOutcome,
DirectSshWorktreeRefreshReleaseReason,
DirectSshWorktreeRefreshScheduler,
DirectSshWorktreeRefreshSchedulerDeps,
WaiterLeaseId
} from './direct-ssh-worktree-refresh-scheduler-types'
import { cancelDirectSshWorktreeRefreshAttempt } from './direct-ssh-worktree-refresh-scheduler-cancellation'
import {
adjustDirectSshAuthorityUnsettled,
copyDirectSshWorktreeRefreshMetrics,
createDirectSshWorktreeRefreshMetrics,
directSshWorktreeRefreshSchedulerSnapshot,
recordDirectSshCancelDebt,
recordDirectSshProviderExecution,
recordDirectSshQueueAdmission
} from './direct-ssh-worktree-refresh-scheduler-metrics'
import { DirectSshWorktreeRefreshTargetQueue } from './direct-ssh-worktree-refresh-target-queue'
import {
directSshProviderAuthorityKey,
directSshWorktreeRefreshKey
} from './direct-ssh-worktree-refresh-scheduler-types'
export type * from './direct-ssh-worktree-refresh-scheduler-types'
export const DIRECT_SSH_WORKTREE_SCAN_CONCURRENCY = 5
export const DIRECT_SSH_PROVIDER_START_BUDGET = 7
export function createDirectSshWorktreeRefreshScheduler(
deps: DirectSshWorktreeRefreshSchedulerDeps
): DirectSshWorktreeRefreshScheduler {
const now = deps.now ?? Date.now
const tasksByKey = new Map<string, LogicalTask>()
const targetQueue = new DirectSshWorktreeRefreshTargetQueue()
const unsettledByAuthority = new Map<string, number>()
const cancelDebtByAuthority = new Map<string, number>()
let locallyUnsettled = 0
let leaseSequence = 0
let stopped = false
const createLeaseId = (): WaiterLeaseId =>
deps.createWaiterLeaseId?.() ?? (`direct-ssh-waiter-${++leaseSequence}` as WaiterLeaseId)
const metricsFor = (
task: LogicalTask,
locallySettledWaiterCount = task.metrics.locallySettledWaiterCount
): DirectSshWorktreeRefreshOutcome['metrics'] => {
const metrics = copyDirectSshWorktreeRefreshMetrics(task.metrics)
metrics.locallySettledWaiterCount = locallySettledWaiterCount
return metrics
}
const finishTask = (task: LogicalTask, outcome: DirectSshWorktreeRefreshOutcome): void => {
if (task.state === 'terminal') {
return
}
task.state = 'terminal'
task.attempt = null
tasksByKey.delete(task.keyId)
task.metrics.locallySettledWaiterCount += task.waiters.size
const settledOutcome = { ...outcome, metrics: metricsFor(task) }
for (const waiter of task.waiters.values()) {
waiter.resolve(settledOutcome)
}
task.waiters.clear()
}
const addCancelDebt = (task: LogicalTask): void => {
recordDirectSshCancelDebt(
task.metrics,
DIRECT_SSH_PROVIDER_START_BUDGET - DIRECT_SSH_WORKTREE_SCAN_CONCURRENCY
)
cancelDebtByAuthority.set(
task.authorityId,
(cancelDebtByAuthority.get(task.authorityId) ?? 0) + 1
)
}
const cancelAttempt = (
task: LogicalTask,
reason: DirectSshWorktreeRefreshReleaseReason
): void => {
if (!task.attempt || task.attemptCanceled) {
return
}
task.attemptCanceled = true
if (cancelDirectSshWorktreeRefreshAttempt(task.attempt, reason)) {
addCancelDebt(task)
}
}
const canStart = (task: LogicalTask): boolean => {
const unsettled = unsettledByAuthority.get(task.authorityId) ?? 0
const debt = cancelDebtByAuthority.get(task.authorityId) ?? 0
return unsettled + debt + 1 <= DIRECT_SSH_PROVIDER_START_BUDGET
}
const handleAttemptResult = (
task: LogicalTask,
requestId: ProviderRequestId,
result: HostQualifiedDetectedWorktreeResult
): void => {
if (task.state === 'terminal' || task.attempt?.providerRequestId !== requestId) {
return
}
if (result.providerRequestId !== requestId) {
finishTask(task, { status: 'rejected', providerRequestId: requestId })
return
}
if (result.status === 'timed-out') {
addCancelDebt(task)
if (task.attemptCount === 1) {
task.metrics.timeoutRetryCount++
task.attempt = null
task.attemptCanceled = true
targetQueue.enqueue(task, true, now())
return
}
}
const status =
result.status === 'authority-unknown' || result.status === 'ambiguous-owner'
? 'non-authoritative'
: result.status
finishTask(task, {
status,
providerRequestId: result.providerRequestId,
providerResult: result
})
}
function drain(): void {
while (!stopped && locallyUnsettled < DIRECT_SSH_WORKTREE_SCAN_CONCURRENCY) {
const task = targetQueue.takeNext()
if (!task) {
return
}
if (!canStart(task)) {
task.metrics.replacementAdmissionDelayedCount++
finishTask(task, { status: 'cancel-budget-exhausted' })
continue
}
task.state = 'running'
task.attemptCount++
locallyUnsettled++
recordDirectSshQueueAdmission(
task.metrics,
Math.max(0, now() - task.queuedAt),
locallyUnsettled
)
adjustDirectSshAuthorityUnsettled(unsettledByAuthority, task.authorityId, 1)
let attempt: DirectSshWorktreeRefreshAttempt
try {
attempt = deps.startAttempt(task.key)
task.attempt = attempt
task.attemptCanceled = false
task.attemptStartedAt = now()
} catch (error) {
locallyUnsettled--
adjustDirectSshAuthorityUnsettled(unsettledByAuthority, task.authorityId, -1)
deps.onUnexpectedError?.(error)
finishTask(task, { status: 'rejected' })
continue
}
void attempt.result
.then((result) => {
if (task.attemptStartedAt !== null) {
recordDirectSshProviderExecution(
task.metrics,
Math.max(0, now() - task.attemptStartedAt)
)
task.attemptStartedAt = null
}
handleAttemptResult(task, attempt.providerRequestId, result)
})
.catch((error) => {
if (task.attemptStartedAt !== null) {
recordDirectSshProviderExecution(
task.metrics,
Math.max(0, now() - task.attemptStartedAt)
)
task.attemptStartedAt = null
}
if (task.state !== 'terminal' && !task.attemptCanceled) {
deps.onUnexpectedError?.(error)
finishTask(task, {
status: 'rejected',
providerRequestId: attempt.providerRequestId
})
}
})
.finally(() => {
locallyUnsettled--
adjustDirectSshAuthorityUnsettled(unsettledByAuthority, task.authorityId, -1)
drain()
})
}
}
const releaseWaiter = (
task: LogicalTask,
waiterLeaseId: WaiterLeaseId,
reason: DirectSshWorktreeRefreshReleaseReason
): void => {
const waiter = task.waiters.get(waiterLeaseId)
if (!waiter) {
return
}
task.waiters.delete(waiterLeaseId)
task.metrics.locallySettledWaiterCount++
waiter.resolve({ status: 'canceled', metrics: metricsFor(task) })
if (task.waiters.size > 0) {
return
}
cancelAttempt(task, reason)
finishTask(task, { status: 'canceled' })
}
const request = (key: DirectSshWorktreeRefreshKey): DirectSshWorktreeRefreshLease => {
const keyId = directSshWorktreeRefreshKey(key)
let task = tasksByKey.get(keyId)
if (!task) {
const requestedAt = now()
task = {
key,
keyId,
authorityId: directSshProviderAuthorityKey(key),
state: 'queued',
attemptCount: 0,
attempt: null,
attemptCanceled: false,
attemptStartedAt: null,
queuedAt: requestedAt,
metrics: createDirectSshWorktreeRefreshMetrics(),
waiters: new Map()
}
tasksByKey.set(keyId, task)
targetQueue.enqueue(task, false, now())
} else {
task.metrics.overlappingJoinCount++
}
const waiterLeaseId = createLeaseId()
let resolve!: (outcome: DirectSshWorktreeRefreshOutcome) => void
const result = new Promise<DirectSshWorktreeRefreshOutcome>((settle) => {
resolve = settle
})
task.waiters.set(waiterLeaseId, { resolve })
if (stopped) {
releaseWaiter(task, waiterLeaseId, 'stopped')
targetQueue.clear()
} else {
drain()
}
return {
waiterLeaseId,
result,
release: (reason) => releaseWaiter(task!, waiterLeaseId, reason)
}
}
const invalidateMatching = (predicate: (task: LogicalTask) => boolean): void => {
for (const task of tasksByKey.values()) {
if (!predicate(task)) {
continue
}
cancelAttempt(task, 'invalidated')
finishTask(task, { status: 'stale', providerRequestId: task.attempt?.providerRequestId })
}
drain()
}
const invalidateAuthority = (authority: DirectSshAuthority): void => {
const id = directSshProviderAuthorityKey(authority)
invalidateMatching((task) => task.authorityId === id)
}
const invalidateTarget = (targetId: string): void => {
invalidateMatching((task) => task.key.targetId === targetId)
}
const disposeProvider = (authority: DirectSshAuthority): void => {
invalidateAuthority(authority)
cancelDebtByAuthority.delete(directSshProviderAuthorityKey(authority))
}
const getSnapshot = () =>
directSshWorktreeRefreshSchedulerSnapshot(tasksByKey, locallyUnsettled, cancelDebtByAuthority)
const stop = (): void => {
if (stopped) {
return
}
stopped = true
for (const task of tasksByKey.values()) {
cancelAttempt(task, 'stopped')
finishTask(task, { status: 'canceled', providerRequestId: task.attempt?.providerRequestId })
}
targetQueue.clear()
}
return {
request,
invalidateAuthority,
invalidateTarget,
disposeProvider,
getSnapshot,
stop
}
}
@@ -1,44 +0,0 @@
import type { DirectSshWorktreeRefreshLogicalTask } from './direct-ssh-worktree-refresh-scheduler-types'
export class DirectSshWorktreeRefreshTargetQueue {
private readonly queuedByTarget = new Map<string, DirectSshWorktreeRefreshLogicalTask[]>()
private readonly targetOrder: string[] = []
enqueue(task: DirectSshWorktreeRefreshLogicalTask, retrying: boolean, now: number): void {
task.state = retrying ? 'retrying' : 'queued'
task.queuedAt = now
const lane = this.queuedByTarget.get(task.key.targetId)
if (lane) {
lane.push(task)
return
}
this.queuedByTarget.set(task.key.targetId, [task])
this.targetOrder.push(task.key.targetId)
}
takeNext(): DirectSshWorktreeRefreshLogicalTask | null {
while (this.targetOrder.length > 0) {
const targetId = this.targetOrder.shift()!
const lane = this.queuedByTarget.get(targetId)
const task = lane?.shift()
if (!lane || !task) {
this.queuedByTarget.delete(targetId)
continue
}
if (lane.length > 0) {
this.targetOrder.push(targetId)
} else {
this.queuedByTarget.delete(targetId)
}
if (task.state !== 'terminal' && task.waiters.size > 0) {
return task
}
}
return null
}
clear(): void {
this.queuedByTarget.clear()
this.targetOrder.length = 0
}
}
@@ -3,24 +3,14 @@ import { buildStoreState } from './ipc-events-agent-status-store-test-fixtures'
import type { StoreLike } from './ipc-events-agent-status-store-test-fixtures'
import { buildWindowApi } from './ipc-events-agent-status-window-test-fixtures'
export type DirectSshReconnectCoordinatorDouble = {
requestReconnect: ReturnType<typeof vi.fn>
replaceAuthority: ReturnType<typeof vi.fn>
prepareOnly: ReturnType<typeof vi.fn>
correctUnboundTerminals: ReturnType<typeof vi.fn>
invalidate: ReturnType<typeof vi.fn>
stop: ReturnType<typeof vi.fn>
}
/** Store/coordinator doubles for the partial-authority reconciliation path; the spec wires them. */
/** Store doubles for the partial-authority reconciliation path; the spec wires them. */
export function buildSshAuthorityReconciliationHarness(args: {
partialAuthority: { providerEpoch?: string; connectionGeneration?: number }
latestAuthority: { providerEpoch: string; connectionGeneration: number }
}): {
coordinator: DirectSshReconnectCoordinatorDouble
clearBindings: ReturnType<typeof vi.fn>
emitPartialState: () => void
getState: ReturnType<typeof vi.fn>
requestReconnect: ReturnType<typeof vi.fn>
setSshConnectionState: ReturnType<typeof vi.fn>
storeState: StoreLike
storedState: () => Record<string, unknown> | undefined
@@ -37,7 +27,7 @@ export function buildSshAuthorityReconciliationHarness(args: {
const sshConnectionStates = new Map<string, Record<string, unknown>>()
let sshStateListener: ((data: { targetId: string; state: unknown }) => void) | undefined
const getState = vi.fn(() => Promise.resolve(latestState))
const requestReconnect = vi.fn(async () => ({ status: 'complete' }))
const clearBindings = vi.fn()
const setSshConnectionState = vi.fn((nextTargetId: string, state: Record<string, unknown>) => {
sshConnectionStates.set(nextTargetId, state)
})
@@ -45,22 +35,12 @@ export function buildSshAuthorityReconciliationHarness(args: {
sshTargetLabels: new Map([[targetId, 'Reconciliation Target']]),
sshConnectionStates,
setSshConnectionState,
invalidateStaleDirectSshTargetPtyBindings: vi.fn(() => 0),
setSshTargetsMetadata: vi.fn(),
setRemovedSshTargetLabels: vi.fn(),
clearRemoteDetectedAgents: vi.fn(),
clearDirectSshTargetPtyBindings: vi.fn(),
clearDirectSshTargetPtyBindings: clearBindings,
clearRemovedSshTargetState: vi.fn()
})
const coordinator = {
requestReconnect,
replaceAuthority: vi.fn(),
prepareOnly: vi.fn(),
correctUnboundTerminals: vi.fn(() => 0),
invalidate: vi.fn(),
stop: vi.fn()
}
vi.stubGlobal(
'window',
buildWindowApi({
@@ -76,7 +56,7 @@ export function buildSshAuthorityReconciliationHarness(args: {
)
return {
coordinator,
clearBindings,
emitPartialState: () => {
if (!sshStateListener) {
throw new Error('Expected SSH state listener')
@@ -84,7 +64,6 @@ export function buildSshAuthorityReconciliationHarness(args: {
sshStateListener({ targetId, state: partialState })
},
getState,
requestReconnect,
setSshConnectionState,
storeState,
storedState: () => sshConnectionStates.get(targetId)
@@ -11,7 +11,6 @@ import { registerBackgroundWorktreeRemovalBridge } from './background-worktree-r
import { registerBrowserRequestIpcBridge } from './browser-request-ipc-bridge'
import { registerBrowserStateIpcBridge } from './browser-state-ipc-bridge'
import { registerContentCreationIpcBridge } from './content-creation-ipc-bridge'
import { createDirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
import { registerDirectSshStateIpcBridge } from './direct-ssh-state-ipc-bridge'
import { registerMobileAndTerminalCloseIpcBridge } from './mobile-terminal-close-ipc-bridge'
import { registerMobileDriverIpcBridge } from './mobile-driver-ipc-bridge'
@@ -57,7 +56,6 @@ export function installAppLifetimeIpcEvents(
onCleanupPhase?: (phase: IpcEventsCleanupPhase) => void
): () => void {
const unsubs: (() => void)[] = []
const directSshRuntime = createDirectSshBridgeRuntime()
const backgroundWakeDispatcher = createBackgroundSleepingAgentWakeDispatcher()
unsubs.push(backgroundWakeDispatcher.dispose)
unsubs.push(attachMobileMarkdownBridge())
@@ -128,7 +126,7 @@ export function installAppLifetimeIpcEvents(
registerBrowserRequestIpcBridge(unsubs)
registerTabLifecycleIpcBridge(unsubs)
registerRateLimitIpcBridge(unsubs)
registerDirectSshStateIpcBridge(unsubs, directSshRuntime)
const stopDirectSshStateBridge = registerDirectSshStateIpcBridge(unsubs)
registerZoomIpcBridge(unsubs)
const agentStatusBridge = registerAgentStatusIpcBridge(unsubs)
const disposeMobileDriverHydration = registerMobileDriverIpcBridge(
@@ -147,7 +145,7 @@ export function installAppLifetimeIpcEvents(
onCleanupPhase?.('agentStore.unsubscribe')
unsubs.forEach((unsubscribe) => unsubscribe())
onCleanupPhase?.('ipc.dispose')
directSshRuntime.stop()
stopDirectSshStateBridge()
onCleanupPhase?.('directSsh.stop')
resetAgentHookCompletionNotificationCoordinators()
onCleanupPhase?.('notifications.reset')
@@ -1,160 +0,0 @@
import { createDirectSshReconnectProductTelemetryAdapter } from '@/lib/direct-ssh-reconnect-product-telemetry'
import { acquireDirectSshDetectedWorktreeRefresh } from '@/store/slices/worktrees'
import { toSshExecutionHostId } from '../../../../shared/execution-host'
import type { DirectSshAuthority } from '../../../../shared/ssh-types'
import { useAppStore } from '../../store'
import {
createDirectSshHostHydration,
type DirectSshHostHydration
} from '../direct-ssh-host-hydration'
import {
createDirectSshReconnectCoordinator,
type DirectSshPreparationInput,
type DirectSshPreparationReason,
type DirectSshReconnectCoordinator
} from '../direct-ssh-reconnect-coordinator'
import { directSshAuthoritiesEqual } from '../direct-ssh-reconnect-tokens'
import { createDirectSshWorktreeRefreshScheduler } from '../direct-ssh-worktree-refresh-scheduler'
import type { AppState } from '../../store/types'
type DirectSshTerminalActions = Partial<Pick<AppState, 'invalidateStaleDirectSshTargetPtyBindings'>>
type AuthorityDeadline = { timer: ReturnType<typeof setTimeout>; settle: () => void }
export type DirectSshBridgeRuntime = {
reconnectAuthorityByTarget: Map<string, DirectSshAuthority>
reconnectCoordinator: DirectSshReconnectCoordinator
hostHydration: DirectSshHostHydration
currentAuthority: (targetId: string) => DirectSshAuthority | null
terminalActions: () => DirectSshTerminalActions
prepareHost: (
authority: DirectSshAuthority,
reason: DirectSshPreparationReason,
options?: { authorityAlreadyReplaced?: boolean }
) => Promise<void>
isStopped: () => boolean
addDeadline: (deadline: AuthorityDeadline) => void
removeDeadline: (deadline: AuthorityDeadline) => void
stop: () => void
}
export function createDirectSshBridgeRuntime(): DirectSshBridgeRuntime {
const reconnectAuthorityByTarget = new Map<string, DirectSshAuthority>()
const deadlines = new Set<AuthorityDeadline>()
let stopped = false
const currentAuthority = (targetId: string): DirectSshAuthority | null => {
const state = useAppStore.getState().sshConnectionStates?.get(targetId)
if (
state?.status !== 'connected' ||
state.targetId !== targetId ||
!state.providerEpoch ||
state.connectionGeneration === undefined
) {
return null
}
return {
targetId,
providerEpoch: state.providerEpoch,
connectionGeneration: state.connectionGeneration
}
}
const scheduler = createDirectSshWorktreeRefreshScheduler({
startAttempt: (key) => {
const acquired = acquireDirectSshDetectedWorktreeRefresh(useAppStore, {
repoId: key.repoId,
executionHostId: key.executionHostId,
authority: {
targetId: key.targetId,
providerEpoch: key.providerEpoch,
connectionGeneration: key.connectionGeneration
},
requireAuthoritative: key.authorityRequirement === 'required'
})
return {
providerRequestId: acquired.providerRequestId,
result: acquired.result.then((result) => acquired.merge(result)),
cancel: acquired.release
}
}
})
const hostHydration = createDirectSshHostHydration({
store: useAppStore,
isCurrentAuthority: (authority) =>
directSshAuthoritiesEqual(currentAuthority(authority.targetId), authority),
listRepos: (authority) => {
const executionHostId = toSshExecutionHostId(authority.targetId)
return (
window.api.repos.listForExecutionHost?.({
executionHostId,
expectedAuthority: authority
}) ??
Promise.resolve({ authoritative: false, executionHostId, reason: 'unavailable' as const })
)
},
listLineage: (authority) => {
const executionHostId = toSshExecutionHostId(authority.targetId)
return (
window.api.worktrees.listLineageForHost?.({
executionHostId,
expectedAuthority: authority
}) ??
Promise.resolve({ authoritative: false, executionHostId, reason: 'unavailable' as const })
)
}
})
const terminalActions = (): DirectSshTerminalActions =>
useAppStore.getState() as DirectSshTerminalActions
const reconnectCoordinator = createDirectSshReconnectCoordinator({
scheduler,
isCurrentConnectedAuthority: (authority) =>
directSshAuthoritiesEqual(currentAuthority(authority.targetId), authority),
capturePreparationInput: hostHydration.capturePreparationInput,
readHostScopedLineage: hostHydration.readHostScopedLineage,
invalidateStaleTerminalBindings: (authority) =>
terminalActions().invalidateStaleDirectSshTargetPtyBindings?.(authority) ?? 0,
// Why 0: a pane on an `ssh:` row never spawns, so there is no pane to retry.
retryTargetPanes: () => 0,
correctUnboundTerminalPanes: (authority) => {
terminalActions().invalidateStaleDirectSshTargetPtyBindings?.(authority)
return 0
},
onTelemetry: createDirectSshReconnectProductTelemetryAdapter()
})
const prepareHost: DirectSshBridgeRuntime['prepareHost'] = async (authority, reason, options) => {
try {
if (!options?.authorityAlreadyReplaced) {
reconnectCoordinator.replaceAuthority(authority)
}
const input: DirectSshPreparationInput | null = await hostHydration.capturePreparationInput(
authority,
reason
)
if (input) {
await reconnectCoordinator.prepareOnly(input)
}
} catch {
// A failed preparation leaves the previous host picture; the next connect or wake retries.
}
}
return {
reconnectAuthorityByTarget,
reconnectCoordinator,
hostHydration,
currentAuthority,
terminalActions,
prepareHost,
isStopped: () => stopped,
addDeadline: (deadline) => deadlines.add(deadline),
removeDeadline: (deadline) => deadlines.delete(deadline),
stop: () => {
stopped = true
for (const deadline of deadlines) {
clearTimeout(deadline.timer)
deadline.settle()
}
deadlines.clear()
hostHydration.stop()
reconnectCoordinator.stop()
reconnectAuthorityByTarget.clear()
}
}
}
@@ -5,7 +5,6 @@ import type {
SshConnectionState,
SshProviderEpoch
} from '../../../../shared/ssh-types'
import { createDirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
import { registerDirectSshStateIpcBridge } from './direct-ssh-state-ipc-bridge'
function connectedState(
@@ -81,27 +80,19 @@ async function createHarness(
}
})
const store = useAppStore.getState()
const invalidate = vi.spyOn(store, 'invalidateStaleDirectSshTargetPtyBindings').mockReturnValue(1)
const clearBindings = vi.spyOn(store, 'clearDirectSshTargetPtyBindings').mockReturnValue(1)
const runtime = createDirectSshBridgeRuntime()
const requestReconnect = vi.spyOn(runtime.reconnectCoordinator, 'requestReconnect')
const prepareHost = vi.spyOn(runtime, 'prepareHost')
const unsubs: (() => void)[] = []
const stop = registerDirectSshStateIpcBridge(unsubs)
cleanups.push(() => {
for (const unsubscribe of unsubs) {
unsubscribe()
}
runtime.stop()
stop()
})
registerDirectSshStateIpcBridge(unsubs, runtime)
await settle()
return {
runtime,
getState,
invalidate,
clearBindings,
requestReconnect,
prepareHost,
emit: (state: SshConnectionState) => {
if (!stateListener) {
throw new Error('SSH state listener was not registered')
@@ -128,9 +119,9 @@ afterEach(async () => {
vi.unstubAllGlobals()
})
describe('direct SSH connect reply routing', () => {
describe('direct SSH connected-state routing', () => {
it.each(['reply-first', 'push-first'] as const)(
'clears stale bindings through the real coordinator once when connection ordering is %s',
'clears the target bindings once per connected push when ordering is %s',
async (ordering) => {
const harness = await createHarness()
const state = connectedState()
@@ -142,50 +133,33 @@ describe('direct SSH connect reply routing', () => {
harness.applyConnectReply(state)
}
expect(harness.requestReconnect).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.runtime.reconnectAuthorityByTarget.get(state.targetId)).toEqual(
authority(state)
)
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(state.targetId)
await settle()
harness.invalidate.mockClear()
harness.clearBindings.mockClear()
harness.emit({ ...state })
expect(harness.requestReconnect).toHaveBeenCalledOnce()
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.prepareHost).toHaveBeenLastCalledWith(authority(state), 'wake-refresh')
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(state.targetId)
}
)
it('keeps hydration preparation-only and corrects its duplicate without requesting reconnect', async () => {
it('leaves bindings alone on initial hydration and clears on the next push', async () => {
const state = connectedState()
const harness = await createHarness([state])
expect(harness.prepareHost).toHaveBeenCalledExactlyOnceWith(
authority(state),
'initial-hydration'
)
expect(harness.requestReconnect).not.toHaveBeenCalled()
expect(harness.invalidate).not.toHaveBeenCalled()
expect(harness.clearBindings).not.toHaveBeenCalled()
harness.emit({ ...state })
expect(harness.requestReconnect).not.toHaveBeenCalled()
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.runtime.reconnectAuthorityByTarget.size).toBe(0)
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(state.targetId)
const next = connectedState(state.targetId, 2)
harness.applyConnectReply(next)
harness.invalidate.mockClear()
harness.clearBindings.mockClear()
harness.emit(next)
expect(harness.requestReconnect).toHaveBeenCalledExactlyOnceWith(authority(next))
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(next))
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(next.targetId)
})
it.each(['disconnected', 'error'] as const)(
'forgets a routed authority after %s even when the next connect reply reaches the store first',
'clears bindings on %s and again on the next connected push',
async (status) => {
const state = connectedState()
const harness = await createHarness([state])
@@ -194,17 +168,12 @@ describe('direct SSH connect reply routing', () => {
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(state.targetId)
harness.applyConnectReply(state)
harness.emit(state)
expect(harness.requestReconnect).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.runtime.reconnectAuthorityByTarget.get(state.targetId)).toEqual(
authority(state)
)
expect(harness.clearBindings).toHaveBeenCalledTimes(2)
}
)
it.each(['providerEpoch', 'connectionGeneration'] as const)(
'preserves initial hydration routing when %s must be reconciled',
'reconciles a partial %s on initial hydration without clearing',
async (missingField) => {
const state = connectedState()
const partial: SshConnectionState = { ...state }
@@ -212,18 +181,10 @@ describe('direct SSH connect reply routing', () => {
const harness = await createHarness([partial], [state])
expect(useAppStore.getState().sshConnectionStates.get(state.targetId)).toEqual(state)
expect(harness.prepareHost).toHaveBeenCalledExactlyOnceWith(
authority(state),
'initial-hydration'
)
expect(harness.requestReconnect).not.toHaveBeenCalled()
expect(harness.invalidate).not.toHaveBeenCalled()
expect(harness.clearBindings).not.toHaveBeenCalled()
harness.emit(state)
expect(harness.requestReconnect).not.toHaveBeenCalled()
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.runtime.reconnectAuthorityByTarget.size).toBe(0)
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(state.targetId)
}
)
@@ -244,19 +205,18 @@ describe('direct SSH connect reply routing', () => {
delete partial[missingField]
harness.emit(partial)
expect(harness.requestReconnect).not.toHaveBeenCalled()
expect(harness.invalidate).not.toHaveBeenCalled()
expect(harness.clearBindings).not.toHaveBeenCalled()
resolveState(state)
await settle()
expect(harness.requestReconnect).toHaveBeenCalledExactlyOnceWith(authority(state))
expect(harness.invalidate).toHaveBeenCalledWith(authority(state))
harness.emit(state)
expect(harness.requestReconnect).toHaveBeenCalledOnce()
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(state.targetId)
expect(useAppStore.getState().sshConnectionStates.get(state.targetId)).toMatchObject(
authority(state)
)
}
)
it('keeps routed authorities and disconnect cleanup separate for each target', async () => {
it('keeps disconnect cleanup separate for each target', async () => {
const harness = await createHarness()
const first = connectedState('target-a')
const second = connectedState('target-b')
@@ -265,21 +225,10 @@ describe('direct SSH connect reply routing', () => {
harness.emit(state)
}
await settle()
expect(harness.requestReconnect.mock.calls).toEqual([[authority(first)], [authority(second)]])
expect(harness.clearBindings.mock.calls).toEqual([[first.targetId], [second.targetId]])
harness.clearBindings.mockClear()
harness.emit({ ...first, status: 'disconnected' })
harness.invalidate.mockClear()
harness.emit(second)
expect(harness.requestReconnect).toHaveBeenCalledTimes(2)
expect(harness.invalidate).toHaveBeenCalledExactlyOnceWith(authority(second))
expect(harness.runtime.reconnectAuthorityByTarget.get(second.targetId)).toEqual(
authority(second)
)
expect(harness.runtime.reconnectAuthorityByTarget.has(first.targetId)).toBe(false)
harness.applyConnectReply(first)
harness.emit(first)
expect(harness.requestReconnect).toHaveBeenNthCalledWith(3, authority(first))
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith(first.targetId)
})
})
@@ -1,33 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { SshConnectionState } from '../../../../shared/ssh-types'
import type { DirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
import { registerDirectSshStateIpcBridge } from './direct-ssh-state-ipc-bridge'
function connectingState(targetId: string): SshConnectionState {
return { targetId, status: 'connecting', error: null, reconnectAttempt: 0 }
}
function bridgeRuntime(): DirectSshBridgeRuntime {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the routing under test reads only these members; the rest of the runtime is never touched.
return {
reconnectAuthorityByTarget: new Map(),
reconnectCoordinator: {
requestReconnect: vi.fn(async () => ({ status: 'complete' })),
replaceAuthority: vi.fn(),
correctUnboundTerminals: vi.fn(() => 0),
invalidate: vi.fn()
},
currentAuthority: () => null,
terminalActions: () => ({}),
prepareHost: vi.fn(async () => {}),
isStopped: () => false,
addDeadline: vi.fn(),
removeDeadline: vi.fn(),
stop: vi.fn()
} as unknown as DirectSshBridgeRuntime
}
async function settle(): Promise<void> {
for (let index = 0; index < 40; index += 1) {
await Promise.resolve()
@@ -85,7 +64,7 @@ describe('direct SSH hydration fanout', () => {
}
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
const states = useAppStore.getState().sshConnectionStates
@@ -6,7 +6,6 @@ import {
} from '@/store/slices/runtime-environment-ssh'
import type { SshConnectionState } from '../../../../shared/ssh-types'
import { registerDirectSshStateIpcBridge } from './direct-ssh-state-ipc-bridge'
import type { DirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
type SshApiStub = {
listTargets: () => Promise<{ id: string; label: string }[]>
@@ -43,26 +42,6 @@ function stubWindow(ssh: SshApiStub): void {
})
}
function bridgeRuntime(): DirectSshBridgeRuntime {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the routing under test reads only these members; the rest of the runtime is never touched.
return {
reconnectAuthorityByTarget: new Map(),
reconnectCoordinator: {
requestReconnect: vi.fn(async () => ({ status: 'complete' })),
replaceAuthority: vi.fn(),
correctUnboundTerminals: vi.fn(() => 0),
invalidate: vi.fn()
},
currentAuthority: () => null,
terminalActions: () => ({}),
prepareHost: vi.fn(async () => {}),
isStopped: () => false,
addDeadline: vi.fn(),
removeDeadline: vi.fn(),
stop: vi.fn()
} as unknown as DirectSshBridgeRuntime
}
/** Lets pending IPC promises settle without leaning on a wall-clock delay. */
async function settle(): Promise<void> {
for (let index = 0; index < 12; index += 1) {
@@ -102,7 +81,7 @@ describe('direct SSH hydration target metadata', () => {
getState: () => Promise.resolve(null)
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
resolveRemovedLabels({ 'ssh-ghost': 'old devbox' })
@@ -126,7 +105,7 @@ describe('direct SSH hydration target metadata', () => {
getState: () => Promise.resolve(connectingState('ssh-live'))
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
expect({
@@ -153,7 +132,7 @@ describe('direct SSH hydration target metadata', () => {
getState: () => Promise.resolve(null)
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
// Older paired hosts lack the RPC; the target list is the evidence that matters and must survive.
@@ -177,7 +156,7 @@ describe('direct SSH hydration target metadata', () => {
: Promise.resolve(connectingState(targetId))
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
const state = useAppStore.getState()
@@ -195,7 +174,7 @@ describe('direct SSH hydration target metadata', () => {
getState: () => Promise.resolve(null)
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
expect({
@@ -226,7 +205,7 @@ describe('direct SSH hydration target metadata', () => {
getState: () => Promise.resolve(null)
})
registerDirectSshStateIpcBridge(unsubs, bridgeRuntime())
unsubs.push(registerDirectSshStateIpcBridge(unsubs))
await settle()
// A target added during the tombstone round trip pushes state and re-queries the list.
@@ -1,6 +1,5 @@
import type { SshConnectionState } from '../../../../shared/ssh-types'
import { useAppStore } from '../../store'
import type { DirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
/** Why: an unanswered ssh.getState burns the full 30s runtime-RPC timeout, so a serial walk
* makes the last target wait (N-1) timeouts; a small pool keeps a wedged target from blocking
@@ -8,13 +7,13 @@ import type { DirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
const HYDRATION_FANOUT = 4
export async function hydrateDirectSshInitialState(
runtime: DirectSshBridgeRuntime,
isStopped: () => boolean,
watermarkByTargetId: ReadonlyMap<string, number>,
applySshConnectionStateChange: (targetId: string, state: SshConnectionState) => void
): Promise<void> {
try {
const targets = await window.api.ssh.listTargets()
if (runtime.isStopped()) {
if (isStopped()) {
return
}
// Why: the loaded list is the hydration evidence (#9911) — never gate it behind the
@@ -23,7 +22,7 @@ export async function hydrateDirectSshInitialState(
const hydrateRemovedLabels = async (): Promise<void> => {
try {
const removedLabels = await window.api.ssh.listRemovedTargetLabels()
if (!runtime.isStopped()) {
if (!isStopped()) {
useAppStore.getState().setRemovedSshTargetLabels(removedLabels)
}
} catch (error) {
@@ -34,14 +33,14 @@ export async function hydrateDirectSshInitialState(
void hydrateRemovedLabels()
let nextIndex = 0
const drainTargets = async (): Promise<void> => {
while (nextIndex < targets.length && !runtime.isStopped()) {
while (nextIndex < targets.length && !isStopped()) {
const target = targets[nextIndex]
nextIndex += 1
const hydrationWatermark = watermarkByTargetId.get(target.id) ?? 0
try {
const state = await window.api.ssh.getState({ targetId: target.id })
if (
!runtime.isStopped() &&
!isStopped() &&
state &&
(watermarkByTargetId.get(target.id) ?? 0) === hydrationWatermark
) {
@@ -1,30 +1,27 @@
import { canConnectSshStatus } from '@/ssh/ssh-connection-recoverability'
import type { DirectSshAuthority, SshConnectionState } from '../../../../shared/ssh-types'
import { useAppStore } from '../../store'
import { isDirectSshReconnectCoordinatorRoutingEnabled } from '../direct-ssh-reconnect-rollout'
import { directSshAuthoritiesEqual } from '../direct-ssh-reconnect-tokens'
import { directSshAuthoritiesEqual } from '../../store/slices/worktrees/listing/direct-ssh-authority'
import {
directSshAuthorityFromConnectionState,
registerDirectSshWakeRouting,
routeDirectSshConnectedState,
type DirectSshConnectedStateOrigin
} from '../direct-ssh-state-routing'
import type { DirectSshBridgeRuntime } from './direct-ssh-bridge-runtime'
import { hydrateDirectSshInitialState } from './direct-ssh-initial-state-hydration'
import { applySshManagedServerTransition } from './ssh-managed-server-state-effects'
export function registerDirectSshStateIpcBridge(
unsubs: (() => void)[],
runtime: DirectSshBridgeRuntime
): void {
const {
reconnectAuthorityByTarget,
reconnectCoordinator,
currentAuthority,
terminalActions,
prepareHost
} = runtime
type AuthorityDeadline = { timer: ReturnType<typeof setTimeout>; settle: () => void }
/** Returns the stop that settles pending authority reads; listeners are released through unsubs. */
export function registerDirectSshStateIpcBridge(unsubs: (() => void)[]): () => void {
let stopped = false
const isStopped = (): boolean => stopped
const deadlines = new Set<AuthorityDeadline>()
const currentAuthority = (targetId: string): DirectSshAuthority | null => {
const state = useAppStore.getState().sshConnectionStates?.get(targetId)
return state ? directSshAuthorityFromConnectionState(targetId, state) : null
}
const sshStateWatermarkByTargetId = new Map<string, number>()
// Connect replies can update the store before this bridge routes the matching push.
const routedAuthorityByTarget = new Map<string, DirectSshAuthority>()
const pendingPortHydrationByTargetId = new Map<
string,
{ receivedForwardPush: boolean; receivedDetectedPush: boolean }
@@ -36,7 +33,7 @@ export function registerDirectSshStateIpcBridge(
}
pendingPortHydrationByTargetId.set(targetId, pendingPortHydration)
const isHydrationAuthorityCurrent = (): boolean =>
!runtime.isStopped() && directSshAuthoritiesEqual(currentAuthority(targetId), authority)
!isStopped() && directSshAuthoritiesEqual(currentAuthority(targetId), authority)
const forwardHydration = window.api.ssh.listPortForwards({ targetId }).then((forwards) => {
if (isHydrationAuthorityCurrent() && !pendingPortHydration.receivedForwardPush) {
useAppStore.getState().setPortForwards(targetId, forwards)
@@ -58,7 +55,7 @@ export function registerDirectSshStateIpcBridge(
state: SshConnectionState,
origin: DirectSshConnectedStateOrigin
) => void
void hydrateDirectSshInitialState(runtime, sshStateWatermarkByTargetId, (targetId, state) =>
void hydrateDirectSshInitialState(isStopped, sshStateWatermarkByTargetId, (targetId, state) =>
applySshConnectionStateChange(targetId, state, 'initial-hydration')
)
unsubs.push(
@@ -98,17 +95,17 @@ export function registerDirectSshStateIpcBridge(
origin: DirectSshConnectedStateOrigin,
watermark: number
): void => {
let pendingDeadline: { timer: ReturnType<typeof setTimeout>; settle: () => void } | undefined
let pendingDeadline: AuthorityDeadline | undefined
const deadline = new Promise<null>((resolve) => {
const settle = (): void => resolve(null)
const timer = setTimeout(settle, 5_000)
pendingDeadline = { timer, settle }
runtime.addDeadline(pendingDeadline)
deadlines.add(pendingDeadline)
})
void Promise.race([window.api.ssh.getState({ targetId }).catch(() => null), deadline])
.then((latest) => {
if (
runtime.isStopped() ||
isStopped() ||
latest?.targetId !== targetId ||
!latest?.providerEpoch ||
latest.connectionGeneration === undefined ||
@@ -144,7 +141,7 @@ export function registerDirectSshStateIpcBridge(
.finally(() => {
if (pendingDeadline) {
clearTimeout(pendingDeadline.timer)
runtime.removeDeadline(pendingDeadline)
deadlines.delete(pendingDeadline)
}
})
}
@@ -160,9 +157,6 @@ export function registerDirectSshStateIpcBridge(
applySshManagedServerTransition(targetId, previousManagedServer, state.managedServer)
if (canConnectSshStatus(state.status)) {
routedAuthorityByTarget.delete(targetId)
reconnectAuthorityByTarget.delete(targetId)
reconnectCoordinator.invalidate(targetId)
store.clearRemoteDetectedAgents(targetId)
store.clearPortForwards(targetId)
@@ -180,26 +174,10 @@ export function registerDirectSshStateIpcBridge(
reconcileSshAuthority(targetId, state, origin, sshStateWatermarkByTargetId.get(targetId) ?? 0)
return
}
const previousAuthority = routedAuthorityByTarget.get(targetId) ?? null
routedAuthorityByTarget.set(targetId, authority)
routeDirectSshConnectedState(
{
coordinator: reconnectCoordinator,
coordinatorRoutingEnabled: isDirectSshReconnectCoordinatorRoutingEnabled(),
invalidateStaleTerminalBindings: (nextAuthority) =>
terminalActions().invalidateStaleDirectSshTargetPtyBindings?.(nextAuthority) ?? 0,
retryTargetPanes: () => 0,
prepareHost,
rememberReconnectAuthority: (nextAuthority) => {
if (nextAuthority) {
reconnectAuthorityByTarget.set(targetId, nextAuthority)
} else {
reconnectAuthorityByTarget.delete(targetId)
}
}
},
{ authority, previousAuthority, origin }
)
// Why: no pane binds an `ssh:` PTY, so every binding a newly pushed connection finds is stale.
if (origin === 'push') {
store.clearDirectSshTargetPtyBindings(targetId)
}
if (origin === 'initial-hydration') {
hydrateSshPorts(targetId, authority)
}
@@ -226,12 +204,11 @@ export function registerDirectSshStateIpcBridge(
return
}
latestSshTargetStateEventByTargetId.delete(data.targetId)
if (runtime.isStopped()) {
if (isStopped()) {
return
}
const latestStore = useAppStore.getState()
if (!targets.some((target) => target.id === data.targetId)) {
routedAuthorityByTarget.delete(data.targetId)
latestStore.clearRemovedSshTargetState(data.targetId)
return
}
@@ -240,7 +217,7 @@ export function registerDirectSshStateIpcBridge(
})
.catch(() => {
if (
!runtime.isStopped() &&
!isStopped() &&
latestSshTargetStateEventByTargetId.get(data.targetId) === stateEventId
) {
latestSshTargetStateEventByTargetId.delete(data.targetId)
@@ -259,12 +236,19 @@ export function registerDirectSshStateIpcBridge(
registerDirectSshWakeRouting({
getConnectionStates: () => useAppStore.getState().sshConnectionStates ?? [],
wakeAuthority: (authority) => {
reconnectCoordinator.correctUnboundTerminals(authority, 'wake-refresh')
void prepareHost(authority, 'wake-refresh')
useAppStore.getState().clearDirectSshTargetPtyBindings(authority.targetId)
},
...(typeof window.api.ui.onSystemResumed === 'function'
? { onSystemResumed: (callback: () => void) => window.api.ui.onSystemResumed(callback) }
: {})
})
)
return () => {
stopped = true
for (const deadline of deadlines) {
clearTimeout(deadline.timer)
deadline.settle()
}
deadlines.clear()
}
}
@@ -7,14 +7,8 @@ import {
stubAuxiliaryModules
} from './ipc-events-agent-status-window-test-fixtures'
import { buildSshAuthorityReconciliationHarness } from './ipc-events-ssh-authority-test-fixtures'
import type { DirectSshReconnectCoordinatorDouble } from './ipc-events-ssh-authority-test-fixtures'
function stubDirectSshModules(args: {
storeState: StoreLike
coordinator: DirectSshReconnectCoordinatorDouble
coordinatorRoutingEnabled?: boolean
capturePreparationInput?: ReturnType<typeof vi.fn>
}): void {
function stubDirectSshModules(args: { storeState: StoreLike }): void {
stubReactSyncEffect()
stubAuxiliaryModules()
vi.doMock('../store', () => ({
@@ -23,29 +17,6 @@ function stubDirectSshModules(args: {
getState: () => args.storeState
}
}))
vi.doMock('./direct-ssh-reconnect-rollout', () => ({
isDirectSshReconnectCoordinatorRoutingEnabled: () => args.coordinatorRoutingEnabled ?? true
}))
vi.doMock('./direct-ssh-worktree-refresh-scheduler', () => ({
createDirectSshWorktreeRefreshScheduler: () => ({
stop: vi.fn(),
disposeProvider: vi.fn()
})
}))
vi.doMock('./direct-ssh-host-hydration', () => ({
createDirectSshHostHydration: () => ({
capturePreparationInput: args.capturePreparationInput ?? vi.fn(),
readHostScopedLineage: vi.fn(),
isPreparationTokenCurrent: vi.fn(() => true),
stop: vi.fn()
})
}))
vi.doMock('./direct-ssh-reconnect-coordinator', () => ({
createDirectSshReconnectCoordinator: () => args.coordinator
}))
vi.doMock('@/lib/direct-ssh-reconnect-product-telemetry', () => ({
createDirectSshReconnectProductTelemetryAdapter: vi.fn()
}))
}
// Why: end-to-end exercise of startup agent-status restoration through
@@ -103,7 +74,7 @@ describe('useIpcEvents agent status snapshot integration', () => {
{ partialAuthority: { providerEpoch: 'epoch-current' } },
{ partialAuthority: { connectionGeneration: 7 } }
])(
'fills a same-watermark partial SSH authority and routes it once',
'fills a same-watermark partial SSH authority and clears its bindings once',
async ({ partialAuthority }) => {
const harness = buildSshAuthorityReconciliationHarness({
partialAuthority,
@@ -112,14 +83,14 @@ describe('useIpcEvents agent status snapshot integration', () => {
connectionGeneration: 7
}
})
stubDirectSshModules({ storeState: harness.storeState, coordinator: harness.coordinator })
stubDirectSshModules({ storeState: harness.storeState })
const { useIpcEvents } = await import('./useIpcEvents')
useIpcEvents()
harness.emitPartialState()
await vi.waitFor(() => {
expect(harness.requestReconnect).toHaveBeenCalledOnce()
expect(harness.clearBindings).toHaveBeenCalledExactlyOnceWith('target-reconciliation')
})
expect(harness.getState).toHaveBeenCalledOnce()
expect(harness.setSshConnectionState).toHaveBeenCalledTimes(2)
@@ -147,7 +118,7 @@ describe('useIpcEvents agent status snapshot integration', () => {
connectionGeneration: 7
}
})
stubDirectSshModules({ storeState: harness.storeState, coordinator: harness.coordinator })
stubDirectSshModules({ storeState: harness.storeState })
const { useIpcEvents } = await import('./useIpcEvents')
useIpcEvents()
@@ -157,7 +128,7 @@ describe('useIpcEvents agent status snapshot integration', () => {
expect(harness.getState).toHaveBeenCalledOnce()
})
await Promise.resolve()
expect(harness.requestReconnect).not.toHaveBeenCalled()
expect(harness.clearBindings).not.toHaveBeenCalled()
expect(harness.setSshConnectionState).toHaveBeenCalledOnce()
expect(harness.storedState()).toEqual(
expect.objectContaining({
@@ -168,110 +139,6 @@ describe('useIpcEvents agent status snapshot integration', () => {
}
)
it.each([
{ enabled: true, expectedRoute: ['request'] },
{
enabled: false,
expectedRoute: ['replace', 'invalidate', 'capture', 'prepare']
}
])(
'routes a changed direct SSH authority through the enabled=$enabled path',
async ({ enabled, expectedRoute }) => {
const order: string[] = []
let sshStateListener: ((data: { targetId: string; state: unknown }) => void) | undefined
const oldState = {
targetId: 'target-a',
status: 'connected' as const,
error: null,
reconnectAttempt: 0,
providerEpoch: 'epoch-old',
connectionGeneration: 1
}
const nextState = {
...oldState,
providerEpoch: 'epoch-new',
connectionGeneration: 2
}
const storeState = buildStoreState({
sshTargetLabels: new Map([['target-a', 'Target A']]),
sshConnectionStates: new Map([['target-a', oldState]]),
setSshConnectionState: (targetId: string, state: unknown) => {
;(storeState.sshConnectionStates as Map<string, unknown>).set(targetId, state)
},
invalidateStaleDirectSshTargetPtyBindings: () => {
order.push('invalidate')
return 1
},
setSshTargetsMetadata: vi.fn(),
setRemovedSshTargetLabels: vi.fn(),
clearRemoteDetectedAgents: vi.fn(),
clearDirectSshTargetPtyBindings: vi.fn(),
clearRemovedSshTargetState: vi.fn()
})
const coordinator = {
requestReconnect: vi.fn(async () => {
order.push('request')
return { status: 'complete' }
}),
replaceAuthority: vi.fn(() => {
order.push('replace')
}),
prepareOnly: vi.fn(async () => {
order.push('prepare')
return { token: null }
}),
correctUnboundTerminals: vi.fn(() => 0),
invalidate: vi.fn(),
stop: vi.fn()
}
const capturePreparationInput = vi.fn(async (authority, reason) => {
order.push('capture')
return {
...authority,
reason,
catalogRevision: 1,
repoRefs: [],
authorityRequirement: 'required'
}
})
stubDirectSshModules({
storeState,
coordinator,
coordinatorRoutingEnabled: enabled,
capturePreparationInput
})
vi.stubGlobal(
'window',
buildWindowApi({
onSet: () => () => {},
ssh: {
onStateChanged: (listener: (data: { targetId: string; state: unknown }) => void) => {
sshStateListener = listener
return () => {}
}
}
})
)
const { useIpcEvents } = await import('./useIpcEvents')
useIpcEvents()
sshStateListener?.({ targetId: 'target-a', state: nextState })
await Promise.resolve()
await Promise.resolve()
expect(order).toEqual(expectedRoute)
if (enabled) {
expect(coordinator.requestReconnect).toHaveBeenCalledOnce()
expect(coordinator.prepareOnly).not.toHaveBeenCalled()
} else {
expect(coordinator.requestReconnect).not.toHaveBeenCalled()
expect(coordinator.replaceAuthority).toHaveBeenCalledOnce()
expect(coordinator.prepareOnly).toHaveBeenCalledOnce()
}
}
)
it('does not let initial SSH port snapshots overwrite newer push events', async () => {
const targetId = 'target-ports'
const secondTargetId = 'target-ports-second'
@@ -425,20 +292,11 @@ describe('useIpcEvents agent status snapshot integration', () => {
fetchWorktreeLineage: vi.fn(async () => undefined),
clearRemoteDetectedAgents: vi.fn(),
clearDirectSshTargetPtyBindings: vi.fn(),
clearRemovedSshTargetState: vi.fn(),
invalidateStaleDirectSshTargetPtyBindings: vi.fn(() => 0)
clearRemovedSshTargetState: vi.fn()
})
const coordinator = {
requestReconnect: vi.fn(async () => ({ status: 'complete' })),
replaceAuthority: vi.fn(),
prepareOnly: vi.fn(async () => ({ token: null })),
correctUnboundTerminals: vi.fn(() => 0),
invalidate: vi.fn(),
stop: vi.fn()
}
let partialTargetStateCalls = 0
stubDirectSshModules({ storeState, coordinator })
stubDirectSshModules({ storeState })
vi.stubGlobal(
'window',
buildWindowApi({
@@ -1,8 +0,0 @@
import type { EventProps } from '../../../shared/telemetry-events'
export type DirectSshReconnectProductProps = EventProps<'direct_ssh_reconnect_operation'>
export type DirectSshReconnectProductSink = (
name: 'direct_ssh_reconnect_operation',
props: DirectSshReconnectProductProps
) => void
@@ -1,106 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { DirectSshCoordinatorTelemetry } from '../hooks/direct-ssh-reconnect-coordinator'
import {
createDirectSshReconnectProductTelemetryAdapter,
toDirectSshReconnectProductProps
} from './direct-ssh-reconnect-product-telemetry'
function coordinatorEvent(
overrides: Partial<DirectSshCoordinatorTelemetry> = {}
): DirectSshCoordinatorTelemetry {
return {
mode: 'prepare-only',
reason: 'workspace-snapshot',
outcome: 'degraded',
durationMs: 900,
staleBindingsCleared: 1,
retriedTerminals: 2,
correctedTerminals: 3,
terminalFinalizationDurationMs: 4,
catalogOutcome: 'complete',
catalogDurationMs: 20,
gitWorktreeCount: 5,
folderWorkspaceCount: 2,
ambiguousOwnerCount: 1,
contradictoryOwnerCount: 1,
repoOutcomes: {
complete: 2,
'non-authoritative': 1,
'timed-out': 1,
'cancel-budget-exhausted': 1,
canceled: 1,
stale: 1,
rejected: 1
},
lineageOutcome: 'degraded',
queueWaitDurationsMs: [30, 10, 20, 40],
providerExecutionDurationsMs: [400, 100, 300, 200],
timeoutRetryCount: 2,
locallySettledWaiterCount: 4,
cancelDebtCount: 2,
replacementAdmissionDelayedCount: 1,
overlappingJoinCount: 3,
peakLocallyUnsettled: 4,
estimatedLateWorkAllowanceCount: 2,
authorityRotationCount: 1,
damped: true,
...overrides
}
}
describe('direct SSH reconnect product telemetry', () => {
it('maps one coordinator aggregate to privacy-safe product fields', () => {
const props = toDirectSshReconnectProductProps(coordinatorEvent())
expect(props).toMatchObject({
mode: 'prepare_only',
reason: 'workspace_snapshot',
outcome: 'degraded',
repo_retrying_count: 2,
repo_timed_out_count: 1,
repo_rejected_count: 1,
repo_canceled_count: 1,
repo_stale_count: 1,
lineage_degraded_count: 1,
queue_wait_sample_count: 4,
queue_wait_duration_ms_p50: 20,
queue_wait_duration_ms_p95: 40,
provider_execution_sample_count: 4,
provider_execution_duration_ms_p50: 200,
provider_execution_duration_ms_p99: 400,
cancel_debt_count: 2,
replacement_admission_delayed_count: 1,
overlapping_join_count: 3,
timeout_retry_count: 2,
damped_preparation_count: 1
})
expect(Object.keys(props).join(' ')).not.toMatch(
/target_id|repo_id|host|path|label|user|request_id|lease_id|terminal_id|error/
)
expect(props).not.toHaveProperty('terminal_correction_failed_count')
expect(props).not.toHaveProperty('terminal_correction_rearmed_count')
expect(props).not.toHaveProperty('concurrent_non_coordinator_call_count')
expect(props).not.toHaveProperty('arrival_order_reply_discarded_count')
})
it('emits exactly one typed event per adapter call', () => {
const sink = vi.fn()
const emit = createDirectSshReconnectProductTelemetryAdapter(sink)
emit(coordinatorEvent())
expect(sink).toHaveBeenCalledOnce()
expect(sink).toHaveBeenCalledWith(
'direct_ssh_reconnect_operation',
expect.objectContaining({ total_duration_ms: 900 })
)
})
it('does not let telemetry failures escape into recovery', () => {
const emit = createDirectSshReconnectProductTelemetryAdapter(() => {
throw new Error('telemetry unavailable')
})
expect(() => emit(coordinatorEvent())).not.toThrow()
})
})
@@ -1,127 +0,0 @@
import type { DirectSshCoordinatorTelemetry } from '../hooks/direct-ssh-reconnect-coordinator'
import { track } from './telemetry'
import type {
DirectSshReconnectProductProps,
DirectSshReconnectProductSink
} from './direct-ssh-reconnect-product-telemetry-types'
const MAX_COUNT = 1_000_000
const MAX_DURATION_MS = 86_400_000
function boundedInteger(value: number, maximum = MAX_COUNT): number {
return Math.min(maximum, Math.max(0, Math.round(Number.isFinite(value) ? value : 0)))
}
function durationDistribution(values: readonly number[]): {
count: number
p50: number
p95: number
p99: number
max: number
} {
const sorted = values
.map((value) => boundedInteger(value, MAX_DURATION_MS))
.toSorted((a, b) => a - b)
const percentile = (quantile: number): number =>
sorted.length === 0 ? 0 : sorted[Math.max(0, Math.ceil(sorted.length * quantile) - 1)]
return {
count: boundedInteger(sorted.length),
p50: percentile(0.5),
p95: percentile(0.95),
p99: percentile(0.99),
max: sorted.at(-1) ?? 0
}
}
function lineageCounts(
outcome: DirectSshCoordinatorTelemetry['lineageOutcome']
): Pick<
DirectSshReconnectProductProps,
| 'lineage_complete_count'
| 'lineage_degraded_count'
| 'lineage_canceled_count'
| 'lineage_stale_count'
| 'lineage_not_started_count'
> {
return {
lineage_complete_count: outcome === 'complete' ? 1 : 0,
lineage_degraded_count: outcome === 'degraded' ? 1 : 0,
lineage_canceled_count: outcome === 'canceled' ? 1 : 0,
lineage_stale_count: outcome === 'stale' ? 1 : 0,
lineage_not_started_count: outcome === 'not-started' ? 1 : 0
}
}
export function toDirectSshReconnectProductProps(
event: DirectSshCoordinatorTelemetry
): DirectSshReconnectProductProps {
const queueWait = durationDistribution(event.queueWaitDurationsMs)
const providerExecution = durationDistribution(event.providerExecutionDurationsMs)
return {
mode: event.mode === 'prepare-only' ? 'prepare_only' : 'reconnect',
reason: event.reason.replaceAll('-', '_') as DirectSshReconnectProductProps['reason'],
outcome: event.outcome,
terminal_retried_count: boundedInteger(event.retriedTerminals),
terminal_stale_binding_cleared_count: boundedInteger(event.staleBindingsCleared),
terminal_correction_succeeded_count: boundedInteger(event.correctedTerminals),
catalog_complete_count: event.catalogOutcome === 'complete' ? 1 : 0,
catalog_degraded_count: event.catalogOutcome === 'degraded' ? 1 : 0,
catalog_stale_count: event.catalogOutcome === 'stale' ? 1 : 0,
repo_complete_count: boundedInteger(event.repoOutcomes.complete),
repo_non_authoritative_count: boundedInteger(event.repoOutcomes['non-authoritative']),
// One first-timeout transition per repo makes retrying repos equal timeout retries.
repo_retrying_count: boundedInteger(event.timeoutRetryCount),
repo_timed_out_count: boundedInteger(event.repoOutcomes['timed-out']),
repo_cancel_budget_exhausted_count: boundedInteger(
event.repoOutcomes['cancel-budget-exhausted']
),
repo_canceled_count: boundedInteger(event.repoOutcomes.canceled),
repo_stale_count: boundedInteger(event.repoOutcomes.stale),
repo_rejected_count: boundedInteger(event.repoOutcomes.rejected),
...lineageCounts(event.lineageOutcome),
git_worktree_count: boundedInteger(event.gitWorktreeCount),
folder_workspace_count: boundedInteger(event.folderWorkspaceCount),
ambiguous_owner_count: boundedInteger(event.ambiguousOwnerCount),
contradictory_owner_count: boundedInteger(event.contradictoryOwnerCount),
total_duration_ms: boundedInteger(event.durationMs, MAX_DURATION_MS),
terminal_finalization_duration_ms: boundedInteger(
event.terminalFinalizationDurationMs,
MAX_DURATION_MS
),
catalog_duration_ms: boundedInteger(event.catalogDurationMs, MAX_DURATION_MS),
queue_wait_sample_count: queueWait.count,
queue_wait_duration_ms_p50: queueWait.p50,
queue_wait_duration_ms_p95: queueWait.p95,
queue_wait_duration_ms_p99: queueWait.p99,
queue_wait_duration_ms_max: queueWait.max,
provider_execution_sample_count: providerExecution.count,
provider_execution_duration_ms_p50: providerExecution.p50,
provider_execution_duration_ms_p95: providerExecution.p95,
provider_execution_duration_ms_p99: providerExecution.p99,
provider_execution_duration_ms_max: providerExecution.max,
timeout_retry_count: boundedInteger(event.timeoutRetryCount),
locally_settled_waiter_count: boundedInteger(event.locallySettledWaiterCount),
cancel_debt_count: boundedInteger(event.cancelDebtCount),
replacement_admission_delayed_count: boundedInteger(event.replacementAdmissionDelayedCount),
overlapping_join_count: boundedInteger(event.overlappingJoinCount),
coordinator_owned_direct_ssh_detected_worktree_concurrency_peak: boundedInteger(
event.peakLocallyUnsettled,
5
),
estimated_late_work_allowance_count: boundedInteger(event.estimatedLateWorkAllowanceCount, 2),
authority_rotation_count: boundedInteger(event.authorityRotationCount),
damped_preparation_count: event.damped ? 1 : 0
}
}
export function createDirectSshReconnectProductTelemetryAdapter(
sink: DirectSshReconnectProductSink = track
): (event: DirectSshCoordinatorTelemetry) => void {
return (event) => {
try {
sink('direct_ssh_reconnect_operation', toDirectSshReconnectProductProps(event))
} catch {
// Recovery cannot depend on product telemetry.
}
}
}
@@ -5,7 +5,6 @@ import { createTestStore, makeWorktree, makeTab, makeLayout } from './store-test
import { computeVisibleWorktreeIds } from '@/components/sidebar/visible-worktrees'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import { createStoreSessionMockApi } from './store-session-test-harness'
import type { DirectSshAuthority, SshProviderEpoch } from '../../../../shared/ssh-types'
// Mock sonner (imported by repos.ts)
vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } }))
@@ -524,128 +523,4 @@ describe('reconnectPersistedTerminals', () => {
expect(after.deferredSshSessionIdsByTabId).toBe(before.deferredSshSessionIdsByTabId)
expect(after.workspaceSessionReady).toBe(false)
})
it('leaves scoped reconnect state untouched when direct SSH authority is stale at entry', async () => {
const store = createDaemonEnabledStore()
const targetId = 'ssh-1'
const worktreeId = 'repo1::/remote/wt1'
const staleAuthority: DirectSshAuthority = {
targetId,
providerEpoch: 'epoch-1' as SshProviderEpoch,
connectionGeneration: 1
}
store.setState({
tabsByWorktree: {
[worktreeId]: [makeTab({ id: 'tab1', worktreeId, ptyId: null })]
},
ptyIdsByTabId: { tab1: [] },
pendingReconnectWorktreeIds: [worktreeId],
pendingReconnectTabByWorktree: { [worktreeId]: ['tab1'] },
pendingReconnectPtyIdByTabId: { tab1: 'ssh:ssh-1@@relay-1' },
deferredSshSessionIdsByTabId: { tab1: 'ssh:ssh-1@@deferred-1' },
sshConnectionStates: new Map([
[
targetId,
{
targetId,
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: staleAuthority.providerEpoch,
connectionGeneration: 2
}
]
]),
workspaceSessionReady: false
})
const before = store.getState()
await store.getState().reconnectPersistedTerminals(undefined, {
directSshAuthority: staleAuthority,
workspaceKeys: [worktreeId]
})
const after = store.getState()
expect(after.tabsByWorktree).toBe(before.tabsByWorktree)
expect(after.ptyIdsByTabId).toBe(before.ptyIdsByTabId)
expect(after.pendingReconnectWorktreeIds).toBe(before.pendingReconnectWorktreeIds)
expect(after.pendingReconnectTabByWorktree).toBe(before.pendingReconnectTabByWorktree)
expect(after.pendingReconnectPtyIdByTabId).toBe(before.pendingReconnectPtyIdByTabId)
expect(after.deferredSshSessionIdsByTabId).toBe(before.deferredSshSessionIdsByTabId)
expect(after.workspaceSessionReady).toBe(before.workspaceSessionReady)
})
it('leaves scoped reconnect state untouched when direct SSH authority rotates before publication', async () => {
const store = createDaemonEnabledStore()
const targetId = 'ssh-1'
const worktreeId = 'repo1::/remote/wt1'
const authority: DirectSshAuthority = {
targetId,
providerEpoch: 'epoch-1' as SshProviderEpoch,
connectionGeneration: 1
}
let authorityReads = 0
const currentAuthorityState = {
targetId,
status: 'connected' as const,
error: null,
reconnectAttempt: 0,
providerEpoch: authority.providerEpoch,
connectionGeneration: authority.connectionGeneration
}
const rotatedAuthorityState = {
...currentAuthorityState,
connectionGeneration: 2
}
const sshConnectionStates = new Map([[targetId, currentAuthorityState]])
const originalGet = sshConnectionStates.get.bind(sshConnectionStates)
// Rotate from the first read after the entry authority check, so the
// pre-publication check sees the new generation whatever number of
// intermediate reads the reconnect pass happens to make.
sshConnectionStates.get = ((key: string) => {
authorityReads += 1
return authorityReads >= 2 ? rotatedAuthorityState : originalGet(key)
}) as typeof sshConnectionStates.get
store.setState({
repos: [
{
id: 'repo1',
path: '/repo1',
displayName: 'Repo 1',
badgeColor: '#000',
addedAt: 0,
connectionId: targetId
}
],
worktreesByRepo: {
repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1', path: '/remote/wt1' })]
},
tabsByWorktree: {
[worktreeId]: [makeTab({ id: 'tab1', worktreeId, ptyId: null })]
},
ptyIdsByTabId: { tab1: [] },
pendingReconnectWorktreeIds: [worktreeId],
pendingReconnectTabByWorktree: { [worktreeId]: ['tab1'] },
pendingReconnectPtyIdByTabId: { tab1: 'ssh:ssh-1@@relay-1' },
deferredSshSessionIdsByTabId: {},
sshConnectionStates,
workspaceSessionReady: false
})
const before = store.getState()
await store.getState().reconnectPersistedTerminals(undefined, {
directSshAuthority: authority,
workspaceKeys: [worktreeId]
})
const after = store.getState()
expect(authorityReads).toBeGreaterThanOrEqual(2)
expect(after.tabsByWorktree).toBe(before.tabsByWorktree)
expect(after.ptyIdsByTabId).toBe(before.ptyIdsByTabId)
expect(after.pendingReconnectWorktreeIds).toBe(before.pendingReconnectWorktreeIds)
expect(after.pendingReconnectTabByWorktree).toBe(before.pendingReconnectTabByWorktree)
expect(after.pendingReconnectPtyIdByTabId).toBe(before.pendingReconnectPtyIdByTabId)
expect(after.deferredSshSessionIdsByTabId).toBe(before.deferredSshSessionIdsByTabId)
expect(after.workspaceSessionReady).toBe(false)
})
})
@@ -1,7 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type * as AgentStatusModule from '@/lib/agent-status'
import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types'
import type { SshProviderEpoch } from '../../../../shared/ssh-types'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
import { createTestStore, makeWorktree, makeTab, makeLayout, TEST_REPO } from './store-test-helpers'
@@ -453,14 +452,8 @@ describe('restored folder workspace hydration', () => {
const worktreeId = 'repo-a::/target'
const retainedTab = makeTab({ id: 'tab-retained', worktreeId })
const retiredTab = makeTab({ id: 'tab-retired', worktreeId })
const authority = {
targetId: 'target-a',
providerEpoch: 'epoch-a' as SshProviderEpoch,
connectionGeneration: 1
}
store.setState({
repos: [{ ...TEST_REPO, id: 'repo-a', connectionId: authority.targetId }],
repos: [{ ...TEST_REPO, id: 'repo-a', connectionId: 'target-a' }],
worktreesByRepo: {
'repo-a': [makeWorktree({ id: worktreeId, repoId: 'repo-a', path: '/target' })]
},
@@ -481,7 +474,7 @@ describe('restored folder workspace hydration', () => {
tabsByWorktree: { [worktreeId]: [retainedTab] },
terminalLayoutsByTabId: {}
},
{ directSshAuthority: authority, replaceWorkspaceKeys: [worktreeId] }
{ replaceWorkspaceKeys: [worktreeId] }
)
expect(store.getState().unverifiedPtyLossTabIds).toEqual({
@@ -21,14 +21,6 @@ type CanonicalTerminals = {
aiVaultTitleByTabId: Map<string, AiVaultSessionTitle>
}
export type HydrateWorkspaceTerminalRowsOptions = {
/**
* Rows arrived from a remote snapshot. `unifiedTabs` is not on the remote wire, so the session's
* canonical list still describes the local client and cannot arbitrate ownership of these rows.
*/
rowsFromRemoteSnapshot?: boolean
}
export type WorkspaceTerminalRowHydration = {
rows: TerminalTab[]
/** PTYs a retained row must give up because a canonical row already owns them. */
@@ -50,8 +42,7 @@ export type WorkspaceTerminalRowHydration = {
export function hydrateWorkspaceTerminalRows(
session: WorkspaceSessionState,
worktreeId: string,
rows: readonly TerminalTab[],
options: HydrateWorkspaceTerminalRowsOptions = {}
rows: readonly TerminalTab[]
): WorkspaceTerminalRowHydration {
const canonical = readCanonicalTerminals(session, worktreeId, rows)
const releasedPtyIdsByTabId = new Map<string, Set<string>>()
@@ -66,9 +57,7 @@ export function hydrateWorkspaceTerminalRows(
invalidTabIds.push(row.id)
continue
}
const claim = options.rowsFromRemoteSnapshot
? RETAINED_UNCLAIMED
: resolveCanonicalPtyClaim(session, row, canonical)
const claim = resolveCanonicalPtyClaim(session, row, canonical)
if (claim.kind === 'subsumed') {
subsumedTabIds.push(row.id)
canonicalTabIdBySubsumedTabId.set(row.id, claim.canonicalTabId)
@@ -1,7 +1,6 @@
// Keep this bare import first: it installs the preload-API stub the store imports read at eval time.
import './terminal-hydration-store-test-bootstrap'
import { describe, expect, it, vi } from 'vitest'
import { hydrateWorkspaceTerminalRows } from './terminal-session-row-hydration'
import { getOrphanTerminalIds } from './terminal-orphan-helpers'
import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume'
import type { Tab } from '../../../../shared/tab-types'
@@ -337,31 +336,6 @@ describe('hydrateWorkspaceSession canonical PTY overlap', () => {
expect(Object.keys(state.sleepingAgentSessionsByPaneKey)).toEqual([`split-tab:${soloLeafId}`])
})
it('lets remote-snapshot rows keep every PTY, since unifiedTabs describes the local client', () => {
const sharedPtyId = 'daemon-shared'
const session = makeSession({
tabs: [
makeTab({ id: 'canonical-tab', worktreeId: WORKTREE_ID, ptyId: sharedPtyId }),
makeTab({ id: 'remote-tab', worktreeId: WORKTREE_ID, ptyId: sharedPtyId, sortOrder: 1 })
],
layouts: {
'canonical-tab': { ...makeLayout(), ptyIdsByLeafId: { 'canonical-leaf': sharedPtyId } },
'remote-tab': { ...makeLayout(), ptyIdsByLeafId: { 'remote-leaf': sharedPtyId } }
},
canonicalEntityIds: ['canonical-tab']
})
const rows = session.tabsByWorktree[WORKTREE_ID]!
expect(
hydrateWorkspaceTerminalRows(session, WORKTREE_ID, rows, { rowsFromRemoteSnapshot: true })
.subsumedTabIds
).toEqual([])
// Guards the gate: without the flag the same rows collapse, so a caller that forgets it drops live remote rows.
expect(hydrateWorkspaceTerminalRows(session, WORKTREE_ID, rows).subsumedTabIds).toEqual([
'remote-tab'
])
})
it('retains a non-canonical row that owns no PTY at all', () => {
const sharedPtyId = 'daemon-shared'
const session = makeSession({
@@ -92,7 +92,6 @@ const mockApi = {
globalThis.window = { api: mockApi }
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
import type { SshProviderEpoch } from '../../../../shared/ssh-types'
import {
FLOATING_TERMINAL_WORKTREE_ID,
getDefaultWorkspaceSession
@@ -711,127 +710,6 @@ describe('hydrateWorkspaceSession', () => {
expect(store.getState().worktreeNavHistoryIndex).toBe(1)
expect(canGoBackWorktreeHistory(store.getState())).toBe(true)
})
it('hydrates and reconnects one SSH target without mutating unrelated state', async () => {
const store = createTestStore()
const targetWorktreeId = 'repo-a::/target'
const siblingWorktreeId = 'repo-b::/sibling'
const localWorktreeId = 'repo-local::/local'
const runtimeWorktreeId = 'repo-runtime::/runtime'
const folderWorktreeId = folderWorkspaceKey('folder-1')
const targetTab = makeTab({ id: 'tab-target', worktreeId: targetWorktreeId, ptyId: null })
const deletedTargetTab = makeTab({ id: 'tab-target-deleted', worktreeId: targetWorktreeId })
const siblingTab = makeTab({
id: 'tab-sibling',
worktreeId: siblingWorktreeId,
ptyId: 'ssh:target-b@@pty-b'
})
const runtimeTab = makeTab({
id: 'tab-runtime',
worktreeId: runtimeWorktreeId,
ptyId: 'runtime:env@@pty-runtime'
})
const localTab = makeTab({ id: 'tab-local', worktreeId: localWorktreeId, ptyId: null })
const folderTab = makeTab({ id: 'tab-folder', worktreeId: folderWorktreeId, ptyId: null })
const siblingTabs = [siblingTab]
const runtimeTabs = [runtimeTab]
const [localTabs, folderTabs] = [[localTab], [folderTab]]
const runtimeOwners = { [runtimeWorktreeId]: 'runtime:env' as const }
const authority = {
targetId: 'target-a',
providerEpoch: 'epoch-a' as SshProviderEpoch,
connectionGeneration: 7
}
seedStore(store, {
workspaceSessionReady: true,
repos: [
{ ...TEST_REPO, id: 'repo-a', connectionId: 'target-a' },
{ ...TEST_REPO, id: 'repo-b', connectionId: 'target-b' }
],
worktreesByRepo: {
'repo-a': [makeWorktree({ id: targetWorktreeId, repoId: 'repo-a', path: '/target' })],
'repo-b': [makeWorktree({ id: siblingWorktreeId, repoId: 'repo-b', path: '/sibling' })]
},
tabsByWorktree: {
[targetWorktreeId]: [targetTab, deletedTargetTab],
[siblingWorktreeId]: siblingTabs,
[localWorktreeId]: localTabs,
[runtimeWorktreeId]: runtimeTabs,
[folderWorktreeId]: folderTabs
},
ptyIdsByTabId: {
[targetTab.id]: [],
[siblingTab.id]: [siblingTab.ptyId!],
[runtimeTab.id]: [runtimeTab.ptyId!]
},
activeRepoId: 'repo-b',
activeWorktreeId: siblingWorktreeId,
activeTabId: siblingTab.id,
restoredRuntimeHostIdByWorkspaceSessionKey: runtimeOwners,
sshConnectionStates: new Map([
[
authority.targetId,
{
targetId: authority.targetId,
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: authority.providerEpoch,
connectionGeneration: authority.connectionGeneration
}
]
])
})
const session: WorkspaceSessionState = {
...getDefaultWorkspaceSession(),
activeRepoId: 'repo-a',
activeWorktreeId: targetWorktreeId,
activeTabId: targetTab.id,
tabsByWorktree: {
[targetWorktreeId]: [
{ ...targetTab, ptyId: 'ssh:target-a@@pty-a' },
makeTab({
id: 'tab-wrong-host',
worktreeId: targetWorktreeId,
ptyId: 'ssh:target-b@@pty-wrong'
})
],
[siblingWorktreeId]: siblingTabs,
[runtimeWorktreeId]: runtimeTabs
},
activeWorktreeIdsOnShutdown: [targetWorktreeId],
terminalLayoutsByTabId: {}
}
store.getState().hydrateWorkspaceSession(session, {
directSshAuthority: authority,
replaceWorkspaceKeys: [targetWorktreeId]
})
expect(store.getState().tabsByWorktree[siblingWorktreeId]).toBe(siblingTabs)
expect(store.getState().tabsByWorktree[localWorktreeId]).toBe(localTabs)
expect(store.getState().tabsByWorktree[runtimeWorktreeId]).toBe(runtimeTabs)
expect(store.getState().tabsByWorktree[folderWorktreeId]).toBe(folderTabs)
expect(store.getState().ptyIdsByTabId[siblingTab.id]).toEqual([siblingTab.ptyId])
expect(store.getState().ptyIdsByTabId[runtimeTab.id]).toEqual([runtimeTab.ptyId])
expect(store.getState().restoredRuntimeHostIdByWorkspaceSessionKey).toBe(runtimeOwners)
expect(store.getState().activeWorktreeId).toBe(siblingWorktreeId)
expect(store.getState().pendingReconnectPtyIdByTabId).toEqual({
[targetTab.id]: 'ssh:target-a@@pty-a'
})
await store.getState().reconnectPersistedTerminals(undefined, {
directSshAuthority: authority,
workspaceKeys: [targetWorktreeId]
})
const targetTabs = store.getState().tabsByWorktree[targetWorktreeId]
expect(targetTabs.find((tab) => tab.id === targetTab.id)?.ptyId).toBe('ssh:target-a@@pty-a')
expect(targetTabs.find((tab) => tab.id === 'tab-wrong-host')?.ptyId).toBeNull()
expect(store.getState().tabsByWorktree[siblingWorktreeId]).toBe(siblingTabs)
expect(store.getState().tabsByWorktree[runtimeWorktreeId]).toBe(runtimeTabs)
expect(store.getState().workspaceSessionReady).toBe(true)
})
})
describe('hydrationSucceeded flag (issue #1158)', () => {
@@ -1,18 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '../types'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type {
HostQualifiedDetectedWorktreeResult,
ListDetectedWorktreesArgs
} from '../../../../shared/detected-worktree-provider-contract'
import type { SshProviderEpoch } from '../../../../shared/ssh-types'
import { clearHugeRepoWarningDismissalsForTests } from '@/lib/source-control-huge-repo-warning-dismissals'
import { acquireDirectSshDetectedWorktreeRefresh } from './worktrees'
import {
TEST_SSH_AUTHORITY,
makeDetectedResult,
qualifyDetectedResult
} from './worktrees-detected-listing-fixtures'
import { TEST_SSH_AUTHORITY, makeDetectedResult } from './worktrees-detected-listing-fixtures'
import { makeWorktree } from './worktrees-slice-test-fixtures'
import {
createTestStore,
@@ -513,219 +503,4 @@ describe('fetchWorktrees', () => {
expect(store.getState().worktreesByRepo).toBe(worktreesByRepo)
expect(store.getState().detectedWorktreesByRepo).toBe(detectedWorktreesByRepo)
})
it('keeps worktree maps byte-identical for stale and malformed direct results', async () => {
const store = createTestStore()
const existing = makeWorktree({
id: 'repo-ssh::/home/orca/existing',
repoId: 'repo-ssh',
path: '/home/orca/existing',
hostId: 'ssh:ssh-1'
})
store.setState({
repos: [
{
id: 'repo-ssh',
path: '/home/orca/repo',
displayName: 'SSH Repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-1'
}
],
worktreesByRepo: { 'repo-ssh': [existing] },
detectedWorktreesByRepo: {
'repo-ssh': makeDetectedResult('repo-ssh', [existing])
}
} as Partial<AppState>)
const beforeWorktrees = store.getState().worktreesByRepo
const beforeDetected = store.getState().detectedWorktreesByRepo
const beforeBytes = JSON.stringify([beforeWorktrees, beforeDetected])
let request!: ListDetectedWorktreesArgs
let resolveProvider!: (result: HostQualifiedDetectedWorktreeResult) => void
mockApi.worktrees.listDetected.mockImplementationOnce(
async (args: ListDetectedWorktreesArgs) => {
request = args
return new Promise<HostQualifiedDetectedWorktreeResult>((resolve) => {
resolveProvider = resolve
})
}
)
const lease = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: 'ssh:ssh-1',
authority: TEST_SSH_AUTHORITY
})
const nextAuthority = {
...TEST_SSH_AUTHORITY,
providerEpoch: 'provider-ssh-2' as SshProviderEpoch,
connectionGeneration: 2
}
store.setState({
sshConnectionStates: new Map([
[
'ssh-1',
{
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: nextAuthority.providerEpoch,
connectionGeneration: nextAuthority.connectionGeneration
}
]
])
} as Partial<AppState>)
const subscriber = vi.fn()
const unsubscribe = store.subscribe(subscriber)
resolveProvider(
qualifyDetectedResult(
request,
makeDetectedResult('repo-ssh', [
makeWorktree({
id: 'repo-ssh::/home/orca/stale',
repoId: 'repo-ssh',
path: '/home/orca/stale'
})
])
)
)
const staleResult = await lease.result
expect(lease.merge(staleResult)).toMatchObject({ status: 'stale' })
expect(store.getState().worktreesByRepo).toBe(beforeWorktrees)
expect(store.getState().detectedWorktreesByRepo).toBe(beforeDetected)
expect(JSON.stringify([beforeWorktrees, beforeDetected])).toBe(beforeBytes)
expect(subscriber).not.toHaveBeenCalled()
unsubscribe()
store.setState({
sshConnectionStates: new Map([
[
'ssh-1',
{
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: nextAuthority.providerEpoch,
connectionGeneration: nextAuthority.connectionGeneration
}
]
])
} as Partial<AppState>)
mockApi.worktrees.listDetected.mockImplementationOnce(
async (args: ListDetectedWorktreesArgs) => ({
...qualifyDetectedResult(args, makeDetectedResult(args.repoId, [])),
repoId: 'wrong-repo'
})
)
const malformed = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: 'ssh:ssh-1',
authority: nextAuthority
})
const malformedResult = await malformed.result
expect(malformed.merge(malformedResult)).toMatchObject({
status: 'rejected'
})
expect(store.getState().worktreesByRepo).toBe(beforeWorktrees)
expect(store.getState().detectedWorktreesByRepo).toBe(beforeDetected)
})
it('keeps duplicate repo IDs isolated across direct SSH hosts', async () => {
const store = createTestStore()
const authorityA = {
targetId: 'ssh-a',
providerEpoch: 'provider-a' as SshProviderEpoch,
connectionGeneration: 1
}
const authorityB = {
targetId: 'ssh-b',
providerEpoch: 'provider-b' as SshProviderEpoch,
connectionGeneration: 4
}
const worktreeA = makeWorktree({
id: 'same-repo::/srv/a',
repoId: 'same-repo',
path: '/srv/a'
})
const worktreeB = makeWorktree({
id: 'same-repo::/srv/b',
repoId: 'same-repo',
path: '/srv/b'
})
store.setState({
repos: [
{
id: 'same-repo',
path: '/srv/repo-a',
displayName: 'Repo A',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-a'
},
{
id: 'same-repo',
path: '/srv/repo-b',
displayName: 'Repo B',
badgeColor: '#111',
addedAt: 1,
connectionId: 'ssh-b'
}
],
sshConnectionStates: new Map([
[
'ssh-a',
{
targetId: 'ssh-a',
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: authorityA.providerEpoch,
connectionGeneration: authorityA.connectionGeneration
}
],
[
'ssh-b',
{
targetId: 'ssh-b',
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: authorityB.providerEpoch,
connectionGeneration: authorityB.connectionGeneration
}
]
])
} as Partial<AppState>)
mockApi.worktrees.listDetected
.mockImplementationOnce(async (args: ListDetectedWorktreesArgs) =>
qualifyDetectedResult(args, makeDetectedResult(args.repoId, [worktreeA]))
)
.mockImplementationOnce(async (args: ListDetectedWorktreesArgs) =>
qualifyDetectedResult(args, makeDetectedResult(args.repoId, [worktreeB]))
)
const refreshA = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'same-repo',
executionHostId: 'ssh:ssh-a',
authority: authorityA
})
const refreshB = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'same-repo',
executionHostId: 'ssh:ssh-b',
authority: authorityB
})
refreshA.merge(await refreshA.result)
refreshB.merge(await refreshB.result)
expect(mockApi.worktrees.listDetected).toHaveBeenCalledTimes(2)
expect(refreshA.providerRequestId).not.toBe(refreshB.providerRequestId)
expect(store.getState().worktreesByRepo['same-repo']).toEqual([
{ ...worktreeA, hostId: 'ssh:ssh-a' },
{ ...worktreeB, hostId: 'ssh:ssh-b' }
])
})
})
@@ -1,11 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '../types'
import type {
HostQualifiedDetectedWorktreeResult,
ListDetectedWorktreesArgs
} from '../../../../shared/detected-worktree-provider-contract'
import type { ListDetectedWorktreesArgs } from '../../../../shared/detected-worktree-provider-contract'
import { clearHugeRepoWarningDismissalsForTests } from '@/lib/source-control-huge-repo-warning-dismissals'
import { acquireDirectSshDetectedWorktreeRefresh } from './worktrees'
import {
TEST_SSH_AUTHORITY,
makeDetectedResult,
@@ -466,164 +462,6 @@ describe('fetchWorktrees', () => {
expect(mockApi.worktrees.listDetected).toHaveBeenCalledTimes(1)
})
it('exposes shared direct leases and reports cancellation only for the last waiter', async () => {
const store = createTestStore()
let request!: ListDetectedWorktreesArgs
let resolveProvider!: (result: HostQualifiedDetectedWorktreeResult) => void
const provider = new Promise<HostQualifiedDetectedWorktreeResult>((resolve) => {
resolveProvider = resolve
})
mockApi.worktrees.listDetected.mockImplementationOnce(
async (args: ListDetectedWorktreesArgs) => {
request = args
return provider
}
)
const input = {
repoId: 'repo-ssh',
executionHostId: 'ssh:ssh-1' as const,
authority: TEST_SSH_AUTHORITY
}
const first = acquireDirectSshDetectedWorktreeRefresh(store, input)
const second = acquireDirectSshDetectedWorktreeRefresh(store, input)
expect(mockApi.worktrees.listDetected).toHaveBeenCalledTimes(1)
expect(first.providerRequestId).toBe(second.providerRequestId)
expect(first.waiterLeaseId).not.toBe(second.waiterLeaseId)
expect(first.release('superseded')).toBe('retained')
expect(mockApi.worktrees.cancelListDetected).not.toHaveBeenCalled()
expect(second.release('invalidated')).toBe('cancel-started')
expect(second.release('stopped')).toBe('already-settled')
expect(mockApi.worktrees.cancelListDetected).toHaveBeenCalledWith({
providerRequestId: first.providerRequestId
})
resolveProvider(qualifyDetectedResult(request, makeDetectedResult('repo-ssh', [])))
await provider
await Promise.resolve()
})
it('merges one exact direct provider result once without a second scan', async () => {
const store = createTestStore()
const worktree = makeWorktree({
id: 'repo-ssh::/home/orca/feature',
repoId: 'repo-ssh',
path: '/home/orca/feature'
})
store.setState({
repos: [
{
id: 'repo-ssh',
path: '/home/orca/repo',
displayName: 'SSH Repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-1'
}
]
} as Partial<AppState>)
mockApi.worktrees.listDetected.mockImplementationOnce(async (args: ListDetectedWorktreesArgs) =>
qualifyDetectedResult(args, makeDetectedResult(args.repoId, [worktree]))
)
const subscriber = vi.fn()
const unsubscribe = store.subscribe(subscriber)
const lease = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: 'ssh:ssh-1',
authority: TEST_SSH_AUTHORITY
})
const providerResult = await lease.result
const firstMerge = lease.merge(providerResult)
const secondMerge = lease.merge(providerResult)
expect(firstMerge).toBe(providerResult)
expect(secondMerge).toBe(providerResult)
expect(mockApi.worktrees.listDetected).toHaveBeenCalledTimes(1)
expect(subscriber).toHaveBeenCalledTimes(1)
expect(store.getState().worktreesByRepo['repo-ssh']).toEqual([
{ ...worktree, hostId: 'ssh:ssh-1' }
])
unsubscribe()
})
it('rejects a late duplicate exact-host owner with zero mutation publications', async () => {
const store = createTestStore()
const existing = makeWorktree({
id: 'repo-ssh::/home/orca/existing',
repoId: 'repo-ssh',
path: '/home/orca/existing',
branch: 'refs/heads/old',
hostId: 'ssh:ssh-1'
})
store.setState({
repos: [
{
id: 'repo-ssh',
path: '/home/orca/repo',
displayName: 'SSH Repo',
badgeColor: '#000',
addedAt: 0,
connectionId: 'ssh-1'
}
],
worktreesByRepo: { 'repo-ssh': [existing] },
detectedWorktreesByRepo: {
'repo-ssh': makeDetectedResult('repo-ssh', [existing])
}
} as Partial<AppState>)
let request!: ListDetectedWorktreesArgs
let resolveProvider!: (result: HostQualifiedDetectedWorktreeResult) => void
mockApi.worktrees.listDetected.mockImplementationOnce(
(args: ListDetectedWorktreesArgs) =>
new Promise<HostQualifiedDetectedWorktreeResult>((resolve) => {
request = args
resolveProvider = resolve
})
)
const lease = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: 'ssh:ssh-1',
authority: TEST_SSH_AUTHORITY
})
store.setState((state) => ({
repos: [
...state.repos,
{
id: 'repo-ssh',
path: '/home/orca/duplicate',
displayName: 'Duplicate SSH Repo',
badgeColor: '#111',
addedAt: 1,
connectionId: 'ssh-1'
}
]
}))
const beforeWorktrees = store.getState().worktreesByRepo
const beforeDetected = store.getState().detectedWorktreesByRepo
const subscriber = vi.fn()
const unsubscribe = store.subscribe(subscriber)
resolveProvider(
qualifyDetectedResult(
request,
makeDetectedResult('repo-ssh', [
{
...existing,
branch: 'refs/heads/new'
}
])
)
)
expect(lease.merge(await lease.result)).toMatchObject({ status: 'stale' })
expect(store.getState().worktreesByRepo).toBe(beforeWorktrees)
expect(store.getState().detectedWorktreesByRepo).toBe(beforeDetected)
expect(subscriber).not.toHaveBeenCalled()
unsubscribe()
})
it('fails closed before provider acquisition when direct authority is partial', async () => {
const store = createTestStore()
const worktreesByRepo = store.getState().worktreesByRepo
@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '../types'
import type { WorktreeCatalogVersion } from '../../../../shared/worktree/catalog-version'
import { acquireDirectSshDetectedWorktreeRefresh } from './worktrees'
import {
TEST_SSH_AUTHORITY,
makeDetectedResult,
@@ -459,8 +458,7 @@ describe('the startup hydration purge behind a listing that is no longer current
})
})
// Why this suite exists: the SSH reconnect preparation ends on any repo reporting 'stale' and
// skips its post-connect workspace sync, which nothing retries while the connection holds.
// An older listing must neither roll back the create's catalog nor report the repo stale.
describe('a direct SSH listing older than an applied create', () => {
const sshHost = 'ssh:ssh-1'
@@ -502,20 +500,6 @@ describe('a direct SSH listing older than an applied create', () => {
return created
}
it('is not applied, and reports the repo current rather than stale', async () => {
const store = createTestStore()
const created = seedSsh(store)
const lease = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: sshHost,
authority: TEST_SSH_AUTHORITY
})
const providerResult = await lease.result
expect(lease.merge(providerResult)).toBe(providerResult)
expect(store.getState().worktreesByRepo['repo-ssh']?.map((w) => w.id)).toEqual([created.id])
})
it('a direct-authority fetchWorktrees caller gets the same report, without a relist', async () => {
const store = createTestStore()
const created = seedSsh(store)
@@ -530,46 +514,4 @@ describe('a direct SSH listing older than an applied create', () => {
expect(mockApi.worktrees.listDetected).toHaveBeenCalledOnce()
expect(store.getState().worktreesByRepo['repo-ssh']?.map((w) => w.id)).toEqual([created.id])
})
it('still reports stale when the repo owner went away during the listing, though it is also older', async () => {
const store = createTestStore()
seedSsh(store)
const lease = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: sshHost,
authority: TEST_SSH_AUTHORITY
})
const providerResult = await lease.result
store.setState({ repos: [] })
expect(lease.merge(providerResult)).toMatchObject({ status: 'stale' })
})
it('control: still reports stale when the connection moved during the listing', async () => {
const store = createTestStore()
seedSsh(store)
const lease = acquireDirectSshDetectedWorktreeRefresh(store, {
repoId: 'repo-ssh',
executionHostId: sshHost,
authority: TEST_SSH_AUTHORITY
})
const providerResult = await lease.result
store.setState({
sshConnectionStates: new Map([
[
TEST_SSH_AUTHORITY.targetId,
{
targetId: TEST_SSH_AUTHORITY.targetId,
status: 'connected',
error: null,
reconnectAttempt: 0,
providerEpoch: TEST_SSH_AUTHORITY.providerEpoch,
connectionGeneration: TEST_SSH_AUTHORITY.connectionGeneration + 1
}
]
])
})
expect(lease.merge(providerResult)).toMatchObject({ status: 'stale' })
})
})
@@ -71,8 +71,6 @@ export {
setDetachedHeadAutoDerivedDisplayNameForTests
} from './worktrees/metadata/detached-head-display-name'
export { resetAuthoritativelyRemovedWorktreeMemoryForTests } from './worktrees/listing/authoritative-worktree-removal-memory'
export type { DirectSshDetectedWorktreeRefresh } from './worktrees/listing/known-ssh-worktree-fetch'
export { acquireDirectSshDetectedWorktreeRefresh } from './worktrees/listing/known-ssh-worktree-fetch'
export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice> = (set, get) => ({
...worktreeSliceInitialState,
@@ -1,15 +1,11 @@
import type { StateCreator, StoreApi } from 'zustand'
import type { StateCreator } from 'zustand'
import type { AppState } from '../../../types'
import type { WorktreeSlice } from '../../worktree-helpers'
import type { DetectedWorktreeListResult } from '../../../../../../shared/worktree/types'
import type {
HostQualifiedDetectedWorktreeResult,
HostQualifiedKnownWorktreeResult,
ProviderRequestId,
SshExecutionHostId
} from '../../../../../../shared/detected-worktree-provider-contract'
import type { DirectSshAuthority } from '../../../../../../shared/ssh-types'
import type { DetectedWorktreeRefreshLease } from '../../detected-worktree-refresh-leases'
import { areDetectedWorktreeResultsEqual, areWorktreesEqual } from './worktree-catalog-visibility'
import {
getProjectHostSetupForRepoHost,
@@ -20,20 +16,9 @@ import {
worktreeMatchesHost
} from './worktree-host-ownership'
import { getCurrentDirectSshAuthority } from './direct-ssh-authority'
import {
acquireDetectedWorktreeRefreshLeaseForRepo,
normalizeNotAdmittedProviderResult,
qualifiedProviderResultIsAdmitted
} from './detected-worktree-refresh'
import { isDetectedWorktreeListResult } from './detected-worktree-provider-request'
import { staleDetectedWorktreeProviderResult } from './detected-worktree-refresh-admission'
import { mergeFetchedWorktrees } from './fetched-worktree-merge'
import { getAuthoritativelyRemovedWorktreeIds } from './authoritative-worktree-removal-memory'
import type {
AdmittedDetectedWorktreeRefresh,
DetectedWorktreeRefreshOptions,
WorktreeHostMatchOptions
} from './worktree-slice-types'
import type { WorktreeHostMatchOptions } from './worktree-slice-types'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
export function appendMissingWorktreesForHost<
@@ -181,100 +166,3 @@ export async function runKnownSshWorktreeFetch(
})
return admitted ? known : null
}
export type DirectSshDetectedWorktreeRefresh = {
waiterLeaseId: DetectedWorktreeRefreshLease['waiterLeaseId']
providerRequestId: ProviderRequestId
result: Promise<HostQualifiedDetectedWorktreeResult>
release: DetectedWorktreeRefreshLease['release']
merge(result: HostQualifiedDetectedWorktreeResult): HostQualifiedDetectedWorktreeResult
}
export function acquireDirectSshDetectedWorktreeRefresh(
store: Pick<StoreApi<AppState>, 'getState' | 'setState'>,
request: {
repoId: string
executionHostId: SshExecutionHostId
authority: DirectSshAuthority
requireAuthoritative?: boolean
}
): DirectSshDetectedWorktreeRefresh {
const requestStartedState = store.getState()
const requestStartedWorktrees = requestStartedState.worktreesByRepo[request.repoId]
const ownerWasMissingAtStart = !requestStartedState.repos.some(
(repo) => repo.id === request.repoId
)
const setup = getProjectHostSetupForRepoHost(
requestStartedState,
request.repoId,
request.executionHostId
)
const options: DetectedWorktreeRefreshOptions = {
executionHostId: request.executionHostId,
directSshAuthority: request.authority,
requireAuthoritative: request.requireAuthoritative
}
const lease = acquireDetectedWorktreeRefreshLeaseForRepo(request.repoId, options)
let mergedResult: HostQualifiedDetectedWorktreeResult | undefined
return {
waiterLeaseId: lease.waiterLeaseId,
providerRequestId: lease.providerRequestId,
result: lease.result,
release: lease.release,
merge: (providerResult) => {
if (mergedResult) {
return mergedResult
}
if (
!qualifiedProviderResultIsAdmitted(
providerResult,
lease.providerRequestId,
request.repoId,
options
)
) {
mergedResult = normalizeNotAdmittedProviderResult(
providerResult,
lease.providerRequestId,
request.executionHostId
)
return mergedResult
}
if (request.requireAuthoritative && providerResult.status !== 'complete') {
mergedResult = providerResult
return mergedResult
}
const refresh: AdmittedDetectedWorktreeRefresh = {
status: 'admitted',
result: providerResult.result,
providerResult,
executionHostId: request.executionHostId,
directSshAuthority: request.authority
}
const outcome = mergeFetchedWorktrees(
store.setState as Parameters<StateCreator<AppState, [], [], WorktreeSlice>>[0],
{
repoId: request.repoId,
hostId: request.executionHostId,
ownerWasMissingAtStart,
requestStartedWorktrees,
setup,
refresh
}
)
// Why 'superseded' is current: a newer catalog is already applied for this host. 'stale' would
// end a reconnect preparation, and nothing retries it while the connection holds.
switch (outcome) {
case 'applied':
case 'superseded':
mergedResult = providerResult
break
case 'not-current':
mergedResult = staleDetectedWorktreeProviderResult(refresh) ?? providerResult
break
}
return mergedResult
}
}
}
@@ -1,17 +1,11 @@
import { clearDirectSshTerminalBindings } from '../slices/direct-ssh-terminal-recovery'
import type { TerminalSlice, TerminalStoreGet, TerminalStoreSet } from './terminal-state'
import {
isCurrentDirectSshAuthority,
resolveDirectSshTerminalKeys
} from './terminal-pty-identities'
import { resolveDirectSshTerminalKeys } from './terminal-pty-identities'
export function createDirectSshTerminalBindingActions(
set: TerminalStoreSet,
_get: TerminalStoreGet
): Pick<
TerminalSlice,
'clearDirectSshTargetPtyBindings' | 'invalidateStaleDirectSshTargetPtyBindings'
> {
): Pick<TerminalSlice, 'clearDirectSshTargetPtyBindings'> {
return {
clearDirectSshTargetPtyBindings: (targetId) => {
let clearedCount = 0
@@ -21,22 +15,6 @@ export function createDirectSshTerminalBindingActions(
return result.patch ?? s
})
return clearedCount
},
// Why: no pane binds an `ssh:` PTY any more, so every binding a new authority finds is stale.
invalidateStaleDirectSshTargetPtyBindings: (authority) => {
let clearedCount = 0
set((s) => {
if (!isCurrentDirectSshAuthority(s, authority)) {
return s
}
const result = clearDirectSshTerminalBindings(
s,
resolveDirectSshTerminalKeys(s, authority.targetId)
)
clearedCount = result.clearedCount
return result.patch ?? s
})
return clearedCount
}
}
}
@@ -9,7 +9,6 @@ import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../../../shared/agent-session-resume'
import type { DirectSshAuthority } from '../../../../shared/ssh-types'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import type { SessionOptionValue } from '../../../../shared/native-chat-session-options'
import type { AgentStartedTelemetry } from '../../lib/worktree-startup-payload'
@@ -27,8 +26,7 @@ import type {
import type {
AutomaticAgentResumeClaim,
CodexRestartNotice,
HydrateWorkspaceSessionOptions,
ReconnectPersistedTerminalsOptions
HydrateWorkspaceSessionOptions
} from './terminal-contracts'
export type TerminalActions = {
@@ -145,7 +143,6 @@ export type TerminalActions = {
/** Records the relay's own answer that a PTY id is gone; the one `exited` a respawn may act on. */
markPtySourceDisowned: (ptyId: string) => void
clearDirectSshTargetPtyBindings: (targetId: string) => number
invalidateStaleDirectSshTargetPtyBindings: (authority: DirectSshAuthority) => number
/** Awaited verified shutdown; unlike closeTab, physical teardown gates the state commit. */
shutdownWorktreeTerminals: (
worktreeId: string,
@@ -279,8 +276,5 @@ export type TerminalActions = {
options?: HydrateWorkspaceSessionOptions
) => void
/** Publishes wake hints without spawning; global completion alone opens the readiness gate. */
reconnectPersistedTerminals: (
signal?: AbortSignal,
options?: ReconnectPersistedTerminalsOptions
) => Promise<void>
reconnectPersistedTerminals: (signal?: AbortSignal) => Promise<void>
}
@@ -1,6 +1,5 @@
import type { TuiAgent } from '../../../../shared/tui-agent'
import type { AgentProviderSessionMetadata } from '../../../../shared/agent-session-resume'
import type { DirectSshAuthority } from '../../../../shared/ssh-types'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import type { WorkspaceSessionHydrationOptions } from '@/lib/workspace-session-hydration-keys'
import type { HostSessionSlices } from '@/lib/workspace-session-host-split'
@@ -24,18 +23,10 @@ export type CodexRestartNotice = {
dismissed?: true
}
/** Scoped direct-SSH hydration replaces only named workspace keys and authority. */
export type HydrateWorkspaceSessionOptions = {
directSshAuthority?: DirectSshAuthority
runtimeHostIdByWorkspaceSessionKey?: Record<string, ExecutionHostId>
/** Rows parked for hosts that lost a contested workspace id; omitted leaves the store's copy. */
contestedHostWorkspaceSessions?: HostSessionSlices
/** Partition each restored session key was read from; omitted leaves the store's copy. */
contestedPrimaryHostBySessionKey?: Record<string, ExecutionHostId>
} & WorkspaceSessionHydrationOptions
/** Scoped reconnect must still match this exact provider epoch and connection generation. */
export type ReconnectPersistedTerminalsOptions = {
directSshAuthority: DirectSshAuthority
workspaceKeys: readonly string[]
}
@@ -1,21 +1,8 @@
import type { AppState } from '../types'
import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types'
import type { DirectSshAuthority } from '../../../../shared/ssh-types'
import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
import { resolveDirectSshTerminalWorkspaceKeys } from '../slices/direct-ssh-terminal-workspace-scope'
export function isCurrentDirectSshAuthority(
state: AppState,
authority: DirectSshAuthority
): boolean {
const current = state.sshConnectionStates.get(authority.targetId)
return Boolean(
current?.status === 'connected' &&
current.providerEpoch === authority.providerEpoch &&
current.connectionGeneration === authority.connectionGeneration
)
}
export function resolveDirectSshTerminalKeys(state: AppState, targetId: string): Set<string> {
return resolveDirectSshTerminalWorkspaceKeys(
{
@@ -1,6 +1,5 @@
import type { AppState } from '../types'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
import { resolveAgentPaneAuthorityKey } from '../slices/agent-pane-authority'
import type { HydrateWorkspaceSessionOptions } from './terminal-contracts'
@@ -127,24 +126,6 @@ export function targetScopedWorkspaceHydrationPatch(
{},
targetTabIds
)
const authority = options.directSshAuthority
if (authority) {
for (const workspaceKey of hydrated.pendingReconnectWorktreeIds) {
if (!workspaceKeys.has(workspaceKey)) {
continue
}
for (const tab of session.tabsByWorktree[workspaceKey] ?? []) {
// Why: rows hydration dropped (invalid id, canonical duplicate) would leak reconnect keys nothing owns.
if (!retainedTargetTabIds.has(tab.id)) {
continue
}
const ptyId = session.remoteSessionIdsByTabId?.[tab.id] ?? tab.ptyId
if (ptyId && parseAppSshPtyId(ptyId)?.connectionId === authority.targetId) {
pendingReconnectPtyIdByTabId[tab.id] = ptyId
}
}
}
}
const activeOutsideScope =
state.activeWorktreeId != null && !workspaceKeys.has(state.activeWorktreeId)
const sleepingAgentSessionsByPaneKey = Object.fromEntries([
@@ -67,7 +67,7 @@ export function createWorkspaceTerminalHydrationActions(
sleepingAgentSessionsByPaneKey,
tabsByWorktree,
validTabIds
} = buildWorkspaceTerminalRowPlan(session, validWorktreeIds, options)
} = buildWorkspaceTerminalRowPlan(session, validWorktreeIds)
const fallbackActiveWorktreeId =
!session.activeWorktreeId &&
session.activeRepoId &&
@@ -1,20 +1,15 @@
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
import { buildByIdIndex, buildWorktreeByIdIndex } from '../slices/worktree-by-id-index'
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id'
import type { TerminalSlice, TerminalStoreGet, TerminalStoreSet } from './terminal-state'
import { isCurrentDirectSshAuthority } from './terminal-pty-identities'
export function createWorkspaceTerminalReconnectActions(
set: TerminalStoreSet,
get: TerminalStoreGet
): Pick<TerminalSlice, 'reconnectPersistedTerminals'> {
return {
reconnectPersistedTerminals: async (signal, options) => {
if (
signal?.aborted ||
(options && !isCurrentDirectSshAuthority(get(), options.directSshAuthority))
) {
reconnectPersistedTerminals: async (signal) => {
if (signal?.aborted) {
return
}
const {
@@ -25,14 +20,8 @@ export function createWorkspaceTerminalReconnectActions(
tabsByWorktree,
ptyIdsByTabId
} = get()
const scopedWorkspaceKeys = options ? new Set(options.workspaceKeys) : null
const ids = (pendingReconnectWorktreeIds ?? []).filter(
(id) => !scopedWorkspaceKeys || scopedWorkspaceKeys.has(id)
)
const ids = pendingReconnectWorktreeIds ?? []
if (ids.length === 0) {
if (options) {
return
}
set({
workspaceSessionReady: true,
pendingReconnectWorktreeIds: [],
@@ -65,13 +54,7 @@ export function createWorkspaceTerminalReconnectActions(
const tabId = tab.id
const layout = terminalLayoutsByTabId[tabId]
const leafPtyMap = layout?.ptyIdsByLeafId ?? {}
const pendingPtyId = pendingReconnectPtyIdByTabId[tabId]
const tabLevelPtyId =
options &&
parseAppSshPtyId(pendingPtyId ?? '')?.connectionId !==
options.directSshAuthority.targetId
? undefined
: pendingPtyId
const tabLevelPtyId = pendingReconnectPtyIdByTabId[tabId]
const hasLeafMappings = Object.keys(leafPtyMap).length > 0
// Why: publish live PTY hints before mount (pty-connection reattaches later) so the
// sessions status segment maps daemon IDs to tabs; otherwise all sessions look like orphans until the pane mounts.
@@ -99,24 +82,13 @@ export function createWorkspaceTerminalReconnectActions(
}
}
// Why: keep deferred SSH session IDs for post-cleanup reconnect.
const scopedTabIds = new Set(
[...(scopedWorkspaceKeys ?? ids)].flatMap((workspaceKey) =>
(tabsByWorktree[workspaceKey] ?? []).map((tab) => tab.id)
)
)
const deferredSshSessionIdsByTabId: Record<string, string> = options
? Object.fromEntries(
Object.entries(get().deferredSshSessionIdsByTabId).filter(
([tabId]) => !scopedTabIds.has(tabId)
)
)
: {}
const deferredSshSessionIdsByTabId: Record<string, string> = {}
for (const worktreeId of ids) {
const worktree = worktreeById.get(worktreeId)
// Why: SSH worktrees aren't in worktreesByRepo at cold start; fall back to the repo id in the composite worktree id so sessions still reach the deferred map.
const repoId = worktree?.repoId ?? getRepoIdFromWorktreeId(worktreeId)
const repo = repoId ? (repoById.get(repoId) ?? null) : null
const connectionId = options?.directSshAuthority.targetId ?? repo?.connectionId
const connectionId = repo?.connectionId
if (!connectionId) {
continue
}
@@ -136,44 +108,21 @@ export function createWorkspaceTerminalReconnectActions(
const tabs = tabsByWorktree[worktreeId] ?? []
for (const tab of tabs) {
const sessionId = pendingReconnectPtyIdByTabId[tab.id]
if (
sessionId &&
(!options || parseAppSshPtyId(sessionId)?.connectionId === connectionId)
) {
if (sessionId) {
deferredSshSessionIdsByTabId[tab.id] = sessionId
}
}
}
if (
signal?.aborted ||
(options && !isCurrentDirectSshAuthority(get(), options.directSshAuthority))
) {
if (signal?.aborted) {
return
}
const remainingReconnectWorktreeIds = options
? pendingReconnectWorktreeIds.filter((id) => !scopedWorkspaceKeys?.has(id))
: []
const remainingReconnectTabByWorktree = options
? Object.fromEntries(
Object.entries(pendingReconnectTabByWorktree).filter(
([workspaceKey]) => !scopedWorkspaceKeys?.has(workspaceKey)
)
)
: {}
const remainingReconnectPtyIdByTabId = options
? Object.fromEntries(
Object.entries(pendingReconnectPtyIdByTabId).filter(
([tabId]) => !scopedTabIds.has(tabId)
)
)
: {}
set({
...(reconnectedTabsByWorktree ? { tabsByWorktree: reconnectedTabsByWorktree } : {}),
...(reconnectedPtyIdsByTabId ? { ptyIdsByTabId: reconnectedPtyIdsByTabId } : {}),
...(options ? {} : { workspaceSessionReady: true }),
pendingReconnectWorktreeIds: remainingReconnectWorktreeIds,
pendingReconnectTabByWorktree: remainingReconnectTabByWorktree,
pendingReconnectPtyIdByTabId: remainingReconnectPtyIdByTabId,
workspaceSessionReady: true,
pendingReconnectWorktreeIds: [],
pendingReconnectTabByWorktree: {},
pendingReconnectPtyIdByTabId: {},
deferredSshSessionIdsByTabId
})
}
@@ -7,7 +7,6 @@ import {
hydrateWorkspaceTerminalRows
} from '../slices/terminal-session-row-hydration'
import { removeSleepingAgentSessionsForTab } from '../slices/terminal-tab-retirement'
import type { HydrateWorkspaceSessionOptions } from './terminal-contracts'
export type WorkspaceTerminalRowPlan = {
canonicalTabIdBySubsumedTabId: Map<string, string>
@@ -20,23 +19,13 @@ export type WorkspaceTerminalRowPlan = {
export function buildWorkspaceTerminalRowPlan(
session: WorkspaceSessionState,
validWorktreeIds: ReadonlySet<string>,
options?: HydrateWorkspaceSessionOptions
validWorktreeIds: ReadonlySet<string>
): WorkspaceTerminalRowPlan {
// Remote wire rows are authoritative independently of locally persisted unified tabs.
const remoteSnapshotWorkspaceKeys = new Set(
options?.directSshAuthority ? (options.replaceWorkspaceKeys ?? []) : []
)
const rowHydrationByWorktree = Object.entries(session.tabsByWorktree)
.filter(([worktreeId]) => validWorktreeIds.has(worktreeId))
.map(
([worktreeId, tabs]) =>
[
worktreeId,
hydrateWorkspaceTerminalRows(session, worktreeId, tabs, {
rowsFromRemoteSnapshot: remoteSnapshotWorkspaceKeys.has(worktreeId)
})
] as const
[worktreeId, hydrateWorkspaceTerminalRows(session, worktreeId, tabs)] as const
)
const tabsByWorktree: Record<string, TerminalTab[]> = Object.fromEntries(
rowHydrationByWorktree
@@ -1,112 +0,0 @@
import { describe, expect, it } from 'vitest'
import { eventSchemas, type EventProps } from './telemetry-events'
const validEvent = {
mode: 'reconnect',
reason: 'wake_refresh',
outcome: 'degraded',
terminal_retried_count: 3,
terminal_stale_binding_cleared_count: 1,
terminal_correction_succeeded_count: 2,
catalog_complete_count: 1,
catalog_degraded_count: 0,
catalog_stale_count: 0,
repo_complete_count: 2,
repo_non_authoritative_count: 1,
repo_retrying_count: 1,
repo_timed_out_count: 1,
repo_cancel_budget_exhausted_count: 0,
repo_canceled_count: 0,
repo_stale_count: 0,
repo_rejected_count: 1,
lineage_complete_count: 0,
lineage_degraded_count: 1,
lineage_canceled_count: 0,
lineage_stale_count: 0,
lineage_not_started_count: 0,
git_worktree_count: 4,
folder_workspace_count: 2,
ambiguous_owner_count: 1,
contradictory_owner_count: 1,
total_duration_ms: 750,
terminal_finalization_duration_ms: 5,
catalog_duration_ms: 25,
queue_wait_sample_count: 3,
queue_wait_duration_ms_p50: 10,
queue_wait_duration_ms_p95: 30,
queue_wait_duration_ms_p99: 30,
queue_wait_duration_ms_max: 30,
provider_execution_sample_count: 3,
provider_execution_duration_ms_p50: 100,
provider_execution_duration_ms_p95: 300,
provider_execution_duration_ms_p99: 300,
provider_execution_duration_ms_max: 300,
timeout_retry_count: 1,
locally_settled_waiter_count: 3,
cancel_debt_count: 1,
replacement_admission_delayed_count: 0,
overlapping_join_count: 1,
coordinator_owned_direct_ssh_detected_worktree_concurrency_peak: 3,
estimated_late_work_allowance_count: 1,
authority_rotation_count: 1,
damped_preparation_count: 0
} satisfies EventProps<'direct_ssh_reconnect_operation'>
describe('direct SSH reconnect telemetry schema', () => {
it('accepts the aggregate count and duration distribution', () => {
expect(eventSchemas.direct_ssh_reconnect_operation.safeParse(validEvent).success).toBe(true)
})
it.each([
'target_id',
'repo_id',
'host',
'path',
'label',
'username',
'request_id',
'lease_id',
'terminal_id',
'error',
'raw_error'
])('rejects identifier or raw failure field %s', (field) => {
expect(
eventSchemas.direct_ssh_reconnect_operation.safeParse({
...validEvent,
[field]: 'sensitive-value'
}).success
).toBe(false)
})
it('keeps timeout, rejection, cancellation, and stale counts independent', () => {
const parsed = eventSchemas.direct_ssh_reconnect_operation.parse({
...validEvent,
repo_timed_out_count: 2,
repo_rejected_count: 3,
repo_canceled_count: 4,
repo_stale_count: 5
})
expect([
parsed.repo_timed_out_count,
parsed.repo_rejected_count,
parsed.repo_canceled_count,
parsed.repo_stale_count
]).toEqual([2, 3, 4, 5])
})
it('rejects concurrency and late-work values beyond scheduler policy', () => {
expect(
eventSchemas.direct_ssh_reconnect_operation.safeParse({
...validEvent,
coordinator_owned_direct_ssh_detected_worktree_concurrency_peak: 6
}).success
).toBe(false)
expect(
eventSchemas.direct_ssh_reconnect_operation.safeParse({
...validEvent,
estimated_late_work_allowance_count: 3
}).success
).toBe(false)
})
})
-29
View File
@@ -1,29 +0,0 @@
import type { ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host'
import type { SshExecutionHostId } from './detected-worktree-provider-contract'
import type { DirectSshAuthority } from './ssh-types'
import type { WorkspaceLineage, WorktreeLineage } from './worktree/lineage-types'
export type ListDesktopLineageForHostArgs =
| { executionHostId: typeof LOCAL_EXECUTION_HOST_ID }
| {
executionHostId: SshExecutionHostId
expectedAuthority: DirectSshAuthority
}
export type HostLineageSnapshot =
| {
authoritative: true
authority:
| { kind: 'local'; executionHostId: typeof LOCAL_EXECUTION_HOST_ID }
| ({
kind: 'direct-ssh'
executionHostId: SshExecutionHostId
} & DirectSshAuthority)
worktreeLineageById: Record<string, WorktreeLineage>
workspaceLineageByChildKey: Record<string, WorkspaceLineage>
}
| {
authoritative: false
executionHostId: ExecutionHostId
reason: 'ambiguous-owner' | 'authority-unknown' | 'stale' | 'unavailable' | 'rejected'
}
-28
View File
@@ -1,28 +0,0 @@
import type { ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host'
import type { SshExecutionHostId } from './detected-worktree-provider-contract'
import type { DirectSshAuthority } from './ssh-types'
import type { Repo } from './repo-types'
export type ListReposForExecutionHostArgs =
| { executionHostId: typeof LOCAL_EXECUTION_HOST_ID }
| {
executionHostId: SshExecutionHostId
expectedAuthority: DirectSshAuthority
}
export type HostRepoCatalogSnapshot =
| {
authoritative: true
authority:
| { kind: 'local'; executionHostId: typeof LOCAL_EXECUTION_HOST_ID }
| ({
kind: 'direct-ssh'
executionHostId: SshExecutionHostId
} & DirectSshAuthority)
repos: readonly Repo[]
}
| {
authoritative: false
executionHostId: ExecutionHostId
reason: 'authority-unknown' | 'stale' | 'unavailable' | 'rejected'
}
-2
View File
@@ -30,7 +30,6 @@ import {
import {
contextualTourOutcomeSchema,
contextualTourShownSchema,
directSshReconnectOperationSchema,
editorExternalChangeConflictActionSchema,
editorExternalChangeConflictShownSchema,
setupGuideClosedSchema,
@@ -203,7 +202,6 @@ export const eventSchemas = {
editor_external_change_conflict_shown: editorExternalChangeConflictShownSchema,
editor_external_change_conflict_action: editorExternalChangeConflictActionSchema,
direct_ssh_reconnect_operation: directSshReconnectOperationSchema,
ssh_remote_runtime_resolved: sshRemoteRuntimeResolvedSchema,
ssh_host_server_decided: sshHostServerDecidedSchema,
ssh_host_server_conversion: sshHostServerConversionSchema,
@@ -119,59 +119,3 @@ export const editorExternalChangeConflictActionSchema = z
transport: z.enum(['local', 'ssh', 'runtime'])
})
.strict()
export const directSshReconnectCountSchema = z.number().int().min(0).max(1_000_000)
export const directSshReconnectDurationSchema = z.number().int().min(0).max(86_400_000)
export const directSshReconnectOperationSchema = z
.object({
mode: z.enum(['reconnect', 'prepare_only']),
reason: z.enum(['reconnect', 'initial_hydration', 'workspace_snapshot', 'wake_refresh']),
outcome: z.enum(['complete', 'degraded', 'canceled', 'stale', 'stopped', 'stabilizing']),
terminal_retried_count: directSshReconnectCountSchema,
terminal_stale_binding_cleared_count: directSshReconnectCountSchema,
terminal_correction_succeeded_count: directSshReconnectCountSchema,
catalog_complete_count: directSshReconnectCountSchema,
catalog_degraded_count: directSshReconnectCountSchema,
catalog_stale_count: directSshReconnectCountSchema,
repo_complete_count: directSshReconnectCountSchema,
repo_non_authoritative_count: directSshReconnectCountSchema,
repo_retrying_count: directSshReconnectCountSchema,
repo_timed_out_count: directSshReconnectCountSchema,
repo_cancel_budget_exhausted_count: directSshReconnectCountSchema,
repo_canceled_count: directSshReconnectCountSchema,
repo_stale_count: directSshReconnectCountSchema,
repo_rejected_count: directSshReconnectCountSchema,
lineage_complete_count: directSshReconnectCountSchema,
lineage_degraded_count: directSshReconnectCountSchema,
lineage_canceled_count: directSshReconnectCountSchema,
lineage_stale_count: directSshReconnectCountSchema,
lineage_not_started_count: directSshReconnectCountSchema,
git_worktree_count: directSshReconnectCountSchema,
folder_workspace_count: directSshReconnectCountSchema,
ambiguous_owner_count: directSshReconnectCountSchema,
contradictory_owner_count: directSshReconnectCountSchema,
total_duration_ms: directSshReconnectDurationSchema,
terminal_finalization_duration_ms: directSshReconnectDurationSchema,
catalog_duration_ms: directSshReconnectDurationSchema,
queue_wait_sample_count: directSshReconnectCountSchema,
queue_wait_duration_ms_p50: directSshReconnectDurationSchema,
queue_wait_duration_ms_p95: directSshReconnectDurationSchema,
queue_wait_duration_ms_p99: directSshReconnectDurationSchema,
queue_wait_duration_ms_max: directSshReconnectDurationSchema,
provider_execution_sample_count: directSshReconnectCountSchema,
provider_execution_duration_ms_p50: directSshReconnectDurationSchema,
provider_execution_duration_ms_p95: directSshReconnectDurationSchema,
provider_execution_duration_ms_p99: directSshReconnectDurationSchema,
provider_execution_duration_ms_max: directSshReconnectDurationSchema,
timeout_retry_count: directSshReconnectCountSchema,
locally_settled_waiter_count: directSshReconnectCountSchema,
cancel_debt_count: directSshReconnectCountSchema,
replacement_admission_delayed_count: directSshReconnectCountSchema,
overlapping_join_count: directSshReconnectCountSchema,
coordinator_owned_direct_ssh_detected_worktree_concurrency_peak:
directSshReconnectCountSchema.max(5),
estimated_late_work_allowance_count: directSshReconnectCountSchema.max(2),
authority_rotation_count: directSshReconnectCountSchema,
damped_preparation_count: directSshReconnectCountSchema
})
.strict()