Skip to content

fix(orcad): a client that reconnects during idle shutdown can still open terminals - #29958

Merged
nwparker merged 7 commits into
mainfrom
nwparker/idle-exit-retire-reconnect
Oct 17, 2026
Merged

nwparker merged 7 commits into
mainfrom
nwparker/idle-exit-retire-reconnect

Conversation

@nwparker

@nwparker nwparker commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​400 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​397
Prod 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​203 $\color{#cf222e}{\Huge{\mathbf{−}}}$​31 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​172

ELI5

A server that Orca starts on an SSH host shuts itself down once nobody has used it for a while. Before it exits, it also shuts down its terminal helper, but only if that helper has no terminals open. If you reconnected during those last few moments, the server cancelled its shutdown and stayed connected, but its terminal helper was already gone and could never be started again. Every new terminal then failed with "Terminal daemon is decommissioning" until the server was restarted by hand. Now the server starts a fresh terminal helper when that happens, so the reconnected client can open terminals as usual.

What Changed

  • Before: you reconnect to a managed SSH host just as its idle shutdown begins. The server stays up and the client shows as connected, but opening a terminal fails with Terminal daemon is decommissioning, and keeps failing.
  • After: the server notices the client came back after the terminal daemon agreed to retire, starts a fresh daemon, and keeps serving. New terminals open right away.

Mechanism (new orcad-idle-stop-daemon-recovery.ts, wired in orcad-managed-idle-exit-host.ts):

  • Retirement is treated as a point of no return whenever the daemon agreed to retire, whether the agreement arrived on time, arrived late after the 5 s timeout (lateRetirement), or might have arrived but its reply was lost. In the lost-reply case the server asks the daemon again. The daemon's own check-and-fence decides atomically, so a client that started a terminal in the meantime makes the daemon refuse, and that daemon is kept.
  • A replacement happens only after the daemon itself agreed to retire. It uses the existing restartDaemon(), targeted at the pid of the daemon that retired (expectedDaemonPid). The ownership check is skipped only when the endpoint is proven dead (refused or missing), so a different live daemon at that endpoint is never stopped.
  • If no replacement is possible, the server finishes the idle stop, and the client's reconnect relaunches a working server. The exception is work an exit would end (a chat session, a running agent, an enabled automation, a staged migration, or a held activation fence). In that case the server stays up rather than end that work.
  • isDaemonTerminalAdmissionClosed() checks the router's fence as well as the current daemon's.
  • daemon-provider-restart.ts no longer carries an older daemon generation that also retired into the new provider, because its dead endpoint would make every terminal inventory fail.
  • persistOrcadDaemonRetirementRecord now writes only the record's own fields, so the in-memory late-retirement promise can never break the strict on-disk stop receipt.

Why

The review suggested two options: finish the shutdown and let the client relaunch a server, or swap in a fresh terminal provider. Finishing the shutdown would also end any chat or automation that started during the window, and the reconnecting client would wait through a full relaunch. Swapping the daemon reuses the restart sequence that the Settings "restart daemon" action already runs, and it keeps the connection. It is only allowed once the daemon has atomically agreed to retire, so it cannot end a live terminal. Finishing the shutdown remains as the fallback if the swap fails.

Linked Issue

From the third external Codex review of the SSH consolidation (finding 1, P1; introduced by #26473).

Visual Proof

N/A: this is a server lifecycle fix with no UI change. The live before/after transcript is under Testing.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Unit tests

  • orcad-managed-idle-exit-retired-reconnect.test.ts uses the real DaemonPtyRouter, router retirement, retireOrcadDaemonIfIdle and provider registry. The client returns while retirement is pending. Before the fix, the next spawn rejects with Terminal daemon is decommissioning; after it, the spawn succeeds. The suite also covers:
    • a lost reply that the second ask settles as retired (replaced) or busy (kept, and still serving);
    • a failed replacement, which finishes the stop, or stays up when the client started an automation;
    • a busy daemon, which is never replaced.
  • orcad-idle-stop-daemon-recovery.test.ts: a late retirement after a timeout, a late refusal, and an unanswered second ask (never replaced).
  • orcad-daemon-retirement.test.ts: the late verdict is reported only after admission has reopened.
  • daemon-init-restart-sequence.test.ts: a targeted restart proceeds over a proven-dead endpoint, still refuses when a probe is inconclusive or a different pid owns the endpoint, and drops a retired legacy generation.
  • daemon-router-retirement.test.ts: a pending router census counts as closed admission.
  • orcad-managed-stop.test.ts: a timed-out outcome still writes a valid stop receipt.

Live on omarchy (Linux x64), fully isolated: linux-x64 orcad builds of main and of this branch ran from /tmp, with their own HOME, XDG dirs and ORCA_USER_DATA, a free loopback port, ORCA_ORCAD_MANAGED_ACTIVATION_ROOT set and ORCA_E2E_ORCAD_IDLE_TIMEOUT_MS=4000. To open the reconnect window on demand, the script paused the daemon with SIGSTOP during the final idle check, connected a WebSocket client (an E2EE hello, which counts as an open connection), then resumed the daemon. Retirement then committed with the client present. After that, it ran orca terminal create through the host's local CLI.

  • main: terminal daemon on idle stop: retired → the server stayed up → terminal create failed with "message": "Terminal daemon is decommissioning".
  • this branch: retired → a client returned during idle stop; started a fresh terminal daemon → terminal create returned ok: true, and reading the terminal showed its LIVE-OK output. Once the terminal was closed and the client had left, the server idled out again normally: the fresh daemon retired and orcad exited.

Not verified live, unit tests only: the lost-reply and late-reply paths (they cannot be timed deterministically on a real host), the legacy-generation drop, and macOS/Windows hosts.

@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c9b184e7-d7f5-4f76-b48f-410e97a108b4
📥 Commits

Reviewing files that changed from the base of the PR and between ad6b318 and e69abef.

📒 Files selected for processing: 18
  • src/main/daemon/daemon-init-dependency-mocks.ts
  • src/main/daemon/daemon-init-mock-types.ts
  • src/main/daemon/daemon-init-restart-sequence.test.ts
  • src/main/daemon/daemon-provider-restart.ts
  • src/main/daemon/daemon-provider-state.ts
  • src/main/daemon/daemon-pty-event-subscriptions.ts
  • src/main/daemon/daemon-pty-router-test-fixture.ts
  • src/main/daemon/daemon-pty-router.ts
  • src/main/daemon/daemon-router-retirement.test.ts
  • src/main/orcad/orcad-completed-stop-receipt.ts
  • src/main/orcad/orcad-daemon-retirement.test.ts
  • src/main/orcad/orcad-daemon-retirement.ts
  • src/main/orcad/orcad-idle-stop-daemon-recovery.test.ts
  • src/main/orcad/orcad-idle-stop-daemon-recovery.ts
  • src/main/orcad/orcad-managed-idle-exit-host.ts
  • src/main/orcad/orcad-managed-idle-exit-provider-race.test.ts
  • src/main/orcad/orcad-managed-idle-exit-retired-reconnect.test.ts
  • src/main/orcad/orcad-managed-stop.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

📝 Walkthrough

The changes add daemon admission and PID queries, targeted restart checks for endpoints that may be gone, and explicit results for late retirement attempts. A new recovery flow uses retirement outcomes and admission state to decide whether to retain or replace the daemon. Managed idle exit now invokes that flow when activity returns during retirement and uses the recovery result and remaining work to decide whether to shut down.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to e69ab

No identified issue blocks merging. Terminals remain available through the local fallback when the daemon provider is unavailable.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 18 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main fix: restoring terminal access when a client reconnects during idle shutdown.
Description check Passed The description is detailed and mostly complete. It explains the user-visible problem, mechanism, rationale, testing, live Linux verification, and known unverified platforms and paths. The Linked Issu…
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 is detailed and mostly complete. It explains the user-visible problem, mechanism, rationale, testing, live Linux verification, and known unverified platforms and paths. The Linked Issue section references #26473 but does not provide an actual issue link or Fixes # entry.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@nwparker
nwparker merged commit 6863096 into main Oct 17, 2026
44 of 45 checks passed
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