Skip to content

perf(startup): stop an unreachable SSH host from gating local terminal restore - #18164

Merged
nwparker merged 2 commits into
mainfrom
worktree-agent-a50910dc142c3d8fd
Sep 2, 2026
Merged

nwparker merged 2 commits into
mainfrom
worktree-agent-a50910dc142c3d8fd

Conversation

@nwparker

@nwparker nwparker commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​618 $\color{#cf222e}{\Huge{\mathbf{−}}}$​175 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​443
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​165 $\color{#cf222e}{\Huge{\mathbf{−}}}$​18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​147

ELI5

If one of your remote machines was asleep, Orca waited up to 15 seconds for it before bringing back any of your terminals — including the local ones that had nothing to do with that machine. Now it only waits for the remote machine whose terminals you're actually looking at. The rest connect quietly in the background and reattach to the exact same running sessions when you click their tabs.

What was wrong

use-app-startup-hydration.ts gates terminal restoration behind setTerminalStartupRestorationReady(true). Three things sat inside that gate that did not need to.

1. SSH reconnect awaited every target, not just the one in view. restoreSshConnectionsForStartup awaited ssh.connect for every non-passphrase target that was live at shutdown (SSH_RECONNECT_TIMEOUT_MS = 15_000), then serially polled ssh.getState per target. Only then did recover-legacy-worker-terminals, snapshot-capabilities, and reconnect-terminals run, and only then did the gate open. One asleep host cost 15 s before any local terminal restored.

2. Git worktree hydration was fenced on the PTY daemon spawn. awaitFirstWindowStartupServices() before fetch-hydration-worktrees resolves Promise.all([firstWindowStartupServicesReady, managedWslCliStartupBarrierReady]), and firstWindowStartupServicesReady is the daemon-PTY-provider start and the agent-hook-server bind. The git scan needs neither.

3. fetch-browser-session-profiles was awaited on the gate. On a remote runtime that action is callRuntimeRpc('browser.profileList', …, { timeoutMs: 15_000 }) — a second 15-second timeout on the terminal-restoration path.

What changed

  • restoreSshConnectionsForStartup takes blockingConnectionIds. Only those are awaited; every other eager target is registered as deferred up front and its ssh.connect is fired without a timer. collectActiveWorkspaceSshTargetIds (new, unit-tested) derives the blocking set from the active workspace's repo connectionId plus the SSH target embedded in its restored PTY ids — the PTY id is used because SSH worktrees are absent from worktreesByRepo at cold start.
  • A background target that connects is removed from deferredSshReconnectTargets, because a still-deferred connected target sends fresh panes down startFreshColdRestoreAgentResume instead of the normal runDeferredSessionReattachChoice. Without this, a reachable host would behave differently than it does today.
  • New app:awaitGitEnvironmentStartupBarrier IPC = Promise.all([shellPathReady, managedWslCliStartupBarrierReady]). state.shellPathReady is now published from initializeMainProcessRuntimeLaunch (the field existed but was never assigned). Worktree hydration uses this; app:prepareTerminalStartupRestoration still awaits the first-window services before anything is restored.
  • fetch-browser-session-profiles starts at the same point in the chain but is no longer awaited.

Corrections to the reported findings

The shell-PATH claim needed checking on Windows, and the conclusion is stronger than reported. On non-Windows, main-process-runtime-launch.ts:302 awaits shellPathReady before the window opens, so it is already resolved — as reported. But on packaged Windows the window opens before shellPathReady resolves (startWindowsDesktopBeforeShellPathReady), and firstWindowStartupServicesReady is shellPathReady.then(startServices).then(v => v.firstWindowReady) — so there the old await genuinely was the shell-PATH fence, and removing it outright would have been the "broken git environment" trade the task warned about. Rather than keep a platform-conditional await, this PR keeps an explicit shellPathReady fence on the git path.

Separately, configureWindowsHostGitEnvironmentReadiness already makes every host-Git spawn await the same fence internally (prepareWindowsHostGitEnvironment, called from git-exec-file.ts, git-spawn.ts, git-stream-stdout.ts), so this is belt-and-braces. It is kept because relying on a fence three modules away is not something a future reader should have to rediscover.

The duplicate recoverLegacyWorkerTerminals is load-bearing — left in place. The pre/post pair was added together in #11271. Under materializeRenderer: true, the pre-reconnect run calls ports.reveal(candidate), which materializes a legacy worker's surface in the renderer store. reconnectPersistedTerminals then reads tabsByWorktree — so the pre run can change what reconnect sees. Removing it is not a no-op and it is not removed here. (startup-degraded-recovery.ts:89-95 keeps the same pre/post pair, confirming the intent.)

The onboardingPromise await is not a round trip. window.api.onboarding.get() is started at the top of the chain (line 98) and is overlapped by ~13 awaited stages before it is joined. Measured cost at the join point is ~0 ms, and moving the onboarding callback past the gate would delay the onboarding modal on a first run for no gain. Left alone.

Measurements

tests/tools/benchmarks/startup-time-bench.mjs, 382 restored tabs, 28k-file synthetic profile, --wait-for-event renderer-startup-hydration-done, medians of 3 iterations, macOS arm64. Baselines were re-run back-to-back with the "after" runs because this machine's load varies; both baseline sets are shown.

Unreachable SSH host (--ssh-unreachable-targets 1, target on 203.0.113.1 — RFC 5737 TEST-NET-3, so the TCP handshake never completes and never resets, the wire behaviour of a sleeping host):

phase before after
renderer-ssh-reconnect 15.00 s 0 ms
did-finish-load → hydration-done 15.85 s 343 ms
total → hydration-done 17.27 s 1.34 s
renderer startup barrier 499 ms 2 ms

All local, no SSH:

phase before (run 1) before (run 2) after
did-finish-load → hydration-done 1.02 s 529 ms 382 ms
total → hydration-done 3.32 s 1.98 s 1.33 s
renderer startup barrier 213 ms 168 ms 3 ms
reconnect-terminals 15 ms 14 ms 16 ms

The barrier line is finding 2 in isolation: 168–213 ms → 2–3 ms, because the daemon spawn now overlaps the worktree scan instead of preceding it. On this fixture the git scan is one small repo; at 10 repos / 423 worktree entries the overlap is larger, not smaller.

Raw results: tests/tools/benchmarks/results/startup-{base2-local,base2-unreachable-ssh,after-local,after-unreachable-ssh}-*.json.

Benchmark harness fixes needed to take these numbers

  • repoRoot was resolve(scriptDir, '..', '..'), correct when the script lived at tools/benchmarks/ but stale since chore: declutter top-level repo layout #11890 moved it to tests/tools/benchmarks/. It resolved to <root>/tests, so the harness always failed its out/main/index.js precondition. daemon-coldstart-bench.mjs and main-thread-jank-bench.mjs carry the identical stale constant and are presumably also unrunnable; not touched here.
  • killProcessTree SIGKILLed only the direct child on POSIX. Electron's helper processes inherit the stderr pipe, so the harness never saw EOF and hung forever after its last iteration — the results JSON was written but nothing printed. Now spawns detached and kills the process group.
  • Added --ssh-unreachable-targets N to seed the unreachable-host case, and rendererSshReconnectMs / rendererStartupBarrierMs to the phase table.
  • Fixture builders moved to startup-bench-state-fixture.mjs to keep the harness under max-lines without a disable.

Negative user-facing trade-offs

Non-active SSH targets now connect on tab focus rather than during boot. This is a real behaviour change, stated plainly:

  • If the host is reachable and fast, the connect usually lands before reconnect-terminals (it is fired earlier than the old awaited group, and two IPC stages run in between), so sshConnectionStates reads connected and restoration is byte-identical to today. The target is also removed from deferredSshReconnectTargets on connect, so panes with no restored session take the same non-deferred code path they take today.
  • If the connect loses that race, the pane takes the deferred flow on mount: it paints main's parked snapshot, then waitForSshConnection joins the same in-flight ssh.connect (sshConnectPromises dedupes; ssh-connect-flow.ts:59 serialises concurrent connects for one target in main) and reattaches pendingSessionId — the same PTY id, the same relay, the same replay buffer. The user sees the parked snapshot for the extra few hundred ms instead of a blank pane. This is not a new path: it is exactly what a timed-out eager target has always done, and what every passphrase target has always done.
  • If the host is unreachable, the user reaches a workspace they could not have used anyway, and now reaches it 15 s sooner for every other workspace.

What cannot regress:

  • No terminal is dropped. When SSH is not connected at reconnect time, reconnectPersistedTerminals writes the session id into deferredSshSessionIdsByTabId and leaves tab.ptyId and ptyIdsByTabId set. Nothing clears a PTY id. A deferred id reads as liveness to the orphan sweep, so deferring is the safe direction (see the [Bug] Remote - Terminal window auto-closing. Unable to recover #9911 comment in workspace-terminal-reconnect.ts).
  • No remote PTY is reported dead. Nothing in this diff produces a verdict. refreshTerminalProviderSnapshotCapabilities returns authoritative: null (unknown) for a PTY on an unconnected target and keeps the pane mounted and eviction-exempt; main's legacy-worker recovery classifies an unreachable adoption as unverifiable and defers it. Loss of contact stays unverifiable, never exited.
  • Focusing a remote tab before hydration finishes. Panes cannot mount before the gate (use-terminal-watcher-effects.ts:142, worktree-agent-activation-gate.ts:53), so an early click is only an active-workspace change. After the gate opens, the pane runs the deferred flow and joins the in-flight connect. The gate opens ~15 s sooner, so this window is now shorter, not longer.
  • Passphrase targets are unchanged — still never auto-dialled, still deferred to a user-initiated connect.
  • Git environment — see the Windows correction above; the shell-PATH fence is preserved explicitly, and golden-worktree-create-switch (which creates a worktree via host Git) passes.

One deliberate asymmetry: the ssh.getState compatibility poll ("older/wrapped providers may return no state from connect") still runs only for awaited targets. A background target on such a provider gets its state from the ordinary ssh:stateChanged push, and the pane's waitForSshConnection resolves on ssh.connect returning without throwing regardless of the state value, so it cannot strand.

Reliability contract

  • Invariant: terminal-restore.pty-identity — every terminal that would have been restored is restored with the same PTY id, the same replay/scrollback, and the same live/parked state, regardless of which SSH targets were reachable at boot. ssh.verdict-vocabulary — a deferred reconnect never yields exited.
  • Failure source: at 10 repos / 423 worktree entries / 382 tabs / 1 SSH target, a sleeping host delayed all local terminal restoration by the full 15 s reconnect timeout.
  • Oracle: golden-quit-relaunch-session.spec.ts (the exact file and the live extra terminal survive quit → relaunch) passes on this branch. Deterministic layer: startup-ssh-connection-restore.test.ts proves an unreachable non-active target does not extend the awaited window while an active one still does; 3 of its 5 cases fail on main and the 2 that pass are the await-everything controls.
  • Gate: no existing config/reliability-gates.jsonc entry covers renderer startup SSH partitioning. node config/scripts/check-reliability-gates.mjs passes (100 gates). Recorded as an accepted gap rather than a new gate.
  • Provider/platform coverage: local PTY — covered (golden restart e2e, all-local bench). Daemon PTY — covered (same; the daemon barrier moved off the git path but still fences restoration). SSH/remote — covered at the unit layer and by the unreachable-host bench; no live SSH host was exercised (accepted gap). WSL — unaffected: managedWslCliStartupBarrierReady stays on the git path. Mobile/relay — unaffected; web-app-api.ts resolves the new barrier immediately, as it does the existing one. macOS — measured. Linux/Windows — reasoned, not run; the Windows-specific risk is the shell-PATH fence, addressed above and additionally covered by a structural test.
  • Performance budget: no new polling, timers, or subprocesses. One timer is removed per background target (timeoutMs is now optional and omitted for connects nobody awaits). One IPC round trip added on the git path, one removed from the gate.
  • Diagnostics: renderer-ssh-reconnect-done now reports backgroundTargets alongside eagerTargets/deferredTargets; renderer-git-environment-barrier-await-done replaces renderer-first-window-services-await-done; the bench surfaces both.
  • Residual gaps: (a) no live-SSH e2e; (b) Linux/Windows measured only by reasoning and unit/structural tests; (c) the reachable-but-slow race described above is argued from the code path, not from an instrumented run.

Verify

  • pnpm tc — clean.
  • npx oxlint <changed files> — clean; oxfmt --check clean.
  • npx vitest run --config config/vitest.config.ts src/renderer/src/app-shell src/renderer/src/startup src/main/startup — 435 passed, 1 skipped file.
  • Deferred-SSH reattach neighbours: ssh-pane-connect-gate, pty-connection-deferred-ssh-passphrase, pty-connection-parked-ssh-snapshot, ssh-reattach-model-restore, pty-connection-direct-ssh-reattach-retry, ssh-reconnect-model-paint-gate, store-session-terminal-reconnect, src/renderer/src/store/terminals — 88 passed.
  • golden-quit-relaunch-session and golden-worktree-create-switch e2e pass.
  • golden-terminal-file-link fails 2 tests ("Monaco opened a different file identity") — verified pre-existing: the identical two failures reproduce on a baseline build of main with no changes applied.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The startup flow adds a dedicated Git-environment barrier for shell-PATH and managed WSL readiness. Renderer hydration uses this barrier before worktree refresh and restores only active-workspace SSH targets synchronously. Other SSH targets reconnect in the background and update deferred state. Browser-session profile fetching no longer blocks terminal restoration. Benchmark tooling adds persisted Git and unreachable SSH fixtures, SSH timing metrics, fixture validation, and process-tree cleanup.

Merge Risk: 🔵 Low · up to c8e55

The PR makes local terminal restoration independent of unreachable inactive SSH hosts and narrows Git startup waiting, substantially reducing startup delays. It is mergeable with explicit owner awareness that overlapping startup attempts could leave stale deferred SSH restoration state, and that the benchmark flag should reject invalid counts.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides detailed rationale, implementation changes, trade-offs, testing, benchmarks, and known failures. However, it omits required template content, including the Linked Issue, Visua… Add the missing template sections. Include a real linked issue after "Fixes #"; add visual proof or write "N/A" with a reason; complete AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist; and retain the existing test…
Docstring Coverage ⚠️ Warning Docstring coverage is 29.17% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the startup performance change: unreachable SSH hosts no longer block local terminal restoration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description provides detailed rationale, implementation changes, trade-offs, testing, benchmarks, and known failures. However, it omits required template content, including the Linked Issue, Visual Proof or an explicit N/A, AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist sections.

Resolution

Add the missing template sections. Include a real linked issue after "Fixes #"; add visual proof or write "N/A" with a reason; complete AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist; and retain the existing testing and verification details.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

One race in the new deferred-target bookkeeping can re-mark an already-connected background target as deferred, reintroducing the exact "fresh pane instead of reattach" behavior this PR removes.

Reviewed changes

This run reviewed the SSH reconnect partitioning, the new app:awaitGitEnvironmentStartupBarrier git-environment fence, the shellPathReady publication, and the benchmark harness fixture extraction.

  • SSH reconnect partitioning — restoreSshConnectionsForStartup now awaits only the active workspace's blockingConnectionIds and dials the rest in the background without a timeout, clearing the deferred flag on connect.
  • Active-workspace target derivation — collectActiveWorkspaceSshTargetIds names the blocking set from the active repo's connectionId plus SSH PTY ids embedded in restored tabs and split leaves.
  • Git-environment barrier — a new app:awaitGitEnvironmentStartupBarrier IPC fences worktree hydration on shellPathReady + managedWslCliStartupBarrierReady, and state.shellPathReady is now published from initializeMainProcessRuntimeLaunch.
  • Benchmark harness — fixture builders extracted to startup-bench-state-fixture.mjs, repoRoot fixed, and process-group teardown corrected.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

nwparker and others added 2 commits September 2, 2026 12:55
…l restore

An asleep or unreachable SSH target held the terminal-restoration gate for the
full 15s reconnect timeout, so no terminal restored — local ones included.
Startup now awaits only the target that owns the active workspace's tabs and
lets the rest connect in the background, folded into the existing deferred path
that reattaches their PTYs on tab focus.

Also splits the renderer's git-environment fence out of the first-window PTY
services barrier: worktree hydration needs shell-PATH generation and the managed
WSL CLI registration, not a daemon PTY spawn or a hook-server bind. Terminal
restoration still fences on the first-window services via
app:prepareTerminalStartupRestoration.

Measured with tests/tools/benchmarks/startup-time-bench.mjs (382 restored tabs,
28k-file profile, medians of 3):
  unreachable SSH host: 17.27s -> 1.34s to renderer-startup-hydration-done
  all-local:             1.98s -> 1.33s
…d background SSH target undeferred

app-startup-routing.test.ts pinned the old step names, so the two ordering cases
went vacuous-then-red when the barrier split. Repoint them at the steps that now
carry the same fences: 'git-environment-barrier-await' (shell PATH + managed WSL,
the fence host Git needs) before hydration worktrees, and
'prepare-terminal-startup-restoration' (which awaits firstWindowStartupServicesReady
in main) before terminal reconnect. Both still fail against main's hydration source.

Also: the timed-out-eager rewrite of the deferred list re-added background targets
that had already connected, undoing removeDeferredSshReconnectTarget and sending
fresh panes on a reachable host down the cold-restore path.
@nwparker
nwparker force-pushed the worktree-agent-a50910dc142c3d8fd branch from 09b88ba to c8e5519 Compare September 2, 2026 19:58

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the fix for the deferred-target bookkeeping race and the startup-ordering oracle updates.

  • Deferred re-add fix — restoreSshConnectionsForStartup now accumulates connected background targets in a connectedBackgroundTargetIds set and filters them out of the timed-out deferred rewrite, so a reachable background host no longer gets pushed back into the deferred list.
  • Race regression test — a new unit test proves a connected background target is not resurrected into the deferred list when a blocking target times out, and it fails against the prior replace-all behavior.
  • Ordering oracle updates — app-startup-routing.test.ts now pins the renamed git-environment-barrier-await and prepare-terminal-startup-restoration stages against the hydration chain, matching the source.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: aba3c3e7-9d7f-4016-893e-f171f53ff730

📥 Commits

Reviewing files that changed from the base of the PR and between 3d67f2b and c8e5519.

📒 Files selected for processing (17)
  • src/main/startup/desktop-startup-ordering.test.ts
  • src/main/startup/main-process-ipc-bootstrap.ts
  • src/main/startup/main-process-runtime-launch.ts
  • src/preload/api/app-api.ts
  • src/preload/api/app-bridge.ts
  • src/renderer/src/app-shell/startup-actions-selector.test.ts
  • src/renderer/src/app-shell/startup-actions-selector.ts
  • src/renderer/src/app-shell/use-app-startup-hydration.ts
  • src/renderer/src/app-startup-routing.test.ts
  • src/renderer/src/startup/active-workspace-ssh-targets.test.ts
  • src/renderer/src/startup/active-workspace-ssh-targets.ts
  • src/renderer/src/startup/ssh-startup-reconnect.ts
  • src/renderer/src/startup/startup-ssh-connection-restore.test.ts
  • src/renderer/src/startup/startup-ssh-connection-restore.ts
  • src/renderer/src/web/preload-api/web-app-api.ts
  • tests/tools/benchmarks/startup-bench-state-fixture.mjs
  • tests/tools/benchmarks/startup-time-bench.mjs

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Comment on lines +91 to +92
case '--ssh-unreachable-targets':
args.sshUnreachableTargets = Number(next())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate --ssh-unreachable-targets as a bounded integer.

Number() accepts Infinity, fractions, and negative values. Infinity makes buildUnreachableSshTargets() loop until the process fails. Fractional values make the recorded count differ from the target count. Values above 254 exceed the fixture address range.

Reject values that are not safe integers in the range 0..254, or generate valid addresses across additional ranges.

Proposed fix
 case '--ssh-unreachable-targets':
-  args.sshUnreachableTargets = Number(next())
+  args.sshUnreachableTargets = Number(next())
+  if (
+    !Number.isSafeInteger(args.sshUnreachableTargets) ||
+    args.sshUnreachableTargets < 0 ||
+    args.sshUnreachableTargets > 254
+  ) {
+    throw new Error('--ssh-unreachable-targets must be an integer from 0 to 254')
+  }
   break
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case '--ssh-unreachable-targets':
args.sshUnreachableTargets = Number(next())
case '--ssh-unreachable-targets':
args.sshUnreachableTargets = Number(next())
if (
!Number.isSafeInteger(args.sshUnreachableTargets) ||
args.sshUnreachableTargets < 0 ||
args.sshUnreachableTargets > 254
) {
throw new Error('--ssh-unreachable-targets must be an integer from 0 to 254')
}
break

@nwparker

nwparker commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Readiness review (batch G). CI was red on this branch before the rebase — test / tests node 24 2/8 failed deterministically, not a flake, and it reproduces locally on the pre-rebase head:

FAIL  src/renderer/src/app-startup-routing.test.ts > hydrates persisted UI before local catalog and worktree hydration
  AssertionError: expected 7997 to be less than -1
FAIL  src/renderer/src/app-startup-routing.test.ts > waits for first-window startup services before terminal reconnect
  AssertionError: expected -1 to be greater than or equal to 0

Both cases pinned timeRendererStartupStep('first-window-services-await', which this PR removes from use-app-startup-hydration.ts. The Verify section's vitest paths (src/renderer/src/app-shell src/renderer/src/startup src/main/startup) do not include src/renderer/src/app-startup-routing.test.ts, so it was never run.

What I changed (2 commits squashed into one on top of yours, then rebased onto main):

  1. Repointed the two ordering cases at the steps that now carry the same fences — not weakened:

    • git-environment-barrier-await (= shellPathReady + managedWslCliStartupBarrierReady, the fence host Git actually needs) must precede fetch-hydration-worktrees, plus an added assertion that the awaited call is window.api.app.awaitGitEnvironmentStartupBarrier().
    • prepare-terminal-startup-restoration must precede pre-reconnect recovery → capability refresh → reconnect → post-reconnect. That handler awaits state.firstWindowStartupServicesReady in main before anything else, and desktop-startup-ordering.test.ts already pins that await, so the original invariant is still covered end to end.

    Verified non-vacuous: reverting use-app-startup-hydration.ts to main fails case 1; reverting main-process-ipc-bootstrap.ts/main-process-runtime-launch.ts fails keeps the git-environment barrier off the PTY startup services.

  2. Fixed a defect in startup-ssh-connection-restore.ts: the timed-out-eager branch rebuilt the deferred list from deferredTargetIds, which re-added background targets that had already connected and been removed by removeDeferredSshReconnectTarget. That undoes the exact guard the PR body promises ("the target is also removed from deferredSshReconnectTargets on connect, so panes with no restored session take the same non-deferred code path they take today") whenever the active workspace's own host times out. Now filtered by a connectedBackgroundTargetIds set, with a regression case that fails without the fix.

Independently verified from your claims:

  • 3 of 5 startup-ssh-connection-restore.test.ts cases fail against main's startup-ssh-connection-restore.ts/ssh-startup-reconnect.ts, and the unreachable-target case takes 30 s there — the await-everything behaviour is real.
  • The Windows PATH fence is genuinely preserved: state.shellPathReady = shellPathReady at main-process-runtime-launch.ts:294 is assigned before startWindowsDesktopBeforeShellPathReady and launchDesktopMode, main-process-state.ts:124 defaults it to Promise.resolve() so the handler can never read undefined, and the new main-side test pins both the assignment order and the barrier contents.
  • Benchmark harness fixes are test-only and cross-platform-guarded (killProcessTree keeps the taskkill branch for win32).

Local after rebase: 526 tests pass across app-startup-routing, src/renderer/src/startup, src/renderer/src/app-shell, src/main/startup, ssh-pane-connect-gate; typecheck and oxlint/oxfmt clean.

One residual, not blocking: a background target on an older/wrapped provider whose ssh.connect resolves null never publishes state, so it stays in deferredSshReconnectTargets and its fresh panes take the deferred path. Your body calls this asymmetry out for the ssh.getState poll; the deferred-flag consequence is the same shape. It self-heals on pane mount via deferred-session-attach.ts:119.

@nwparker
nwparker merged commit b00ec20 into main Sep 2, 2026
31 of 32 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…l restore (stablyai#18164)

* perf(startup): stop an unreachable SSH host from gating local terminal restore

An asleep or unreachable SSH target held the terminal-restoration gate for the
full 15s reconnect timeout, so no terminal restored — local ones included.
Startup now awaits only the target that owns the active workspace's tabs and
lets the rest connect in the background, folded into the existing deferred path
that reattaches their PTYs on tab focus.

Also splits the renderer's git-environment fence out of the first-window PTY
services barrier: worktree hydration needs shell-PATH generation and the managed
WSL CLI registration, not a daemon PTY spawn or a hook-server bind. Terminal
restoration still fences on the first-window services via
app:prepareTerminalStartupRestoration.

Measured with tests/tools/benchmarks/startup-time-bench.mjs (382 restored tabs,
28k-file profile, medians of 3):
  unreachable SSH host: 17.27s -> 1.34s to renderer-startup-hydration-done
  all-local:             1.98s -> 1.33s

* fix(startup): restore the startup-ordering oracle and keep a connected background SSH target undeferred

app-startup-routing.test.ts pinned the old step names, so the two ordering cases
went vacuous-then-red when the barrier split. Repoint them at the steps that now
carry the same fences: 'git-environment-barrier-await' (shell PATH + managed WSL,
the fence host Git needs) before hydration worktrees, and
'prepare-terminal-startup-restoration' (which awaits firstWindowStartupServicesReady
in main) before terminal reconnect. Both still fail against main's hydration source.

Also: the timed-out-eager rewrite of the deferred list re-added background targets
that had already connected, undoing removeDeferredSshReconnectTarget and sending
fresh panes on a reachable host down the cold-restore path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant