Skip to content

feat(daemon): cap terminals per execution host - #27488

Merged
OrcaWin merged 4 commits into
mainfrom
np/d1c-terminal-cap
Oct 11, 2026
Merged

OrcaWin merged 4 commits into
mainfrom
np/d1c-terminal-cap

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​247 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​246
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​382 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​376

ELI5

Orca now refuses to start a new terminal once a machine is already running 256 of them. The limit is shared by every Orca terminal host the user runs on that machine. It is the same machine whether the terminals come from the desktop app, orca serve, a dev build, or an SSH host's managed server. A refused terminal gets a clear, translated message. Terminals that are already running are never stopped.

What Changed

  • Before: SSH hosts had no terminal limit at all. The old SSH relay stopped at 50, but it was deleted. A leak or a reconnect loop could start terminals until the remote machine ran out of pty devices or processes. Local machines never had a limit.
  • After: each terminal daemon counts its live and starting terminals of every kind: plain shells, agents and restores. It writes that count to a small per-daemon file in a machine-local, per-user directory: /tmp/.orca-daemon-<uid>/terminals/ (the existing validated private socket base), or the user temp dir on Windows. Before a daemon starts a terminal process, it writes its reservation and then adds up every other daemon's file. If the total would go over the limit, it takes the reservation back and refuses. Because each daemon writes before it reads, two daemons racing for the last slot cannot both get it.
  • Stale files: a file whose owner process is gone is deleted. A file whose owner has not refreshed it in 5 minutes is ignored. Each daemon refreshes its file every minute while it runs and removes it when it stops. A file or directory that cannot be read never blocks a terminal. The daemon still applies the limit to its own terminals.
  • Who gets what at the limit:
    • A new shell, or any request from an older client, fails with: "The host is already running as many terminals as Orca allows. Close a terminal you no longer need, then try again." The renderer translates this message (en, es, fr, ja, ko, zh). Older clients show the English text as an ordinary terminal error. It does not match any "daemon gone" message, so no client replaces a healthy daemon because of it.
    • A client that declared a restore intent (feat(runtime): accept a declared spawn intent from paired clients #27452) gets the existing terminal_spawn_deferred:live_cap answer, so its pane stays asleep and keeps its resume handle. No new wire code is added, so a client released with intent support already understands it.
    • A restore that would only wait in the pacing queue to be refused is told right away.
  • Attach and adopting a live session never count as a new terminal.
  • Tuning: set ORCA_DAEMON_MAX_HOST_TERMINALS (1 to 4096) in the daemon's environment. This follows the existing ORCA_DAEMON_SESSION_SCROLLBACK_ROWS pattern.

Why

Why 256. macOS lets the whole machine open 511 ptys by default (kern.tty.ptmx_max). That pool is shared with every other terminal program, and with the ptys that agents open inside Orca terminals. Half of it leaves room for those. 256 is also five times the relay's old 50, which people with many worktrees did reach (#9819). Linux allows 4096 ptys by default, and Windows has no pty device pool, so 256 is the binding number on the most constrained platform.

Why a file per daemon, not a shared counter file. Each file has exactly one writer, so there is no lock to take, no lock to recover from a crashed holder, and no read-modify-write race. The owner's pid identifies a stale file directly.

Why /tmp. It is machine-local and the same for GUI and SSH sessions. A data root in the home directory could sit on a network share mounted by several machines, and then two machines would share one count. The directory is per user, and a planted directory is refused, as for the socket base.

Why enforce on desktop too. A pty or process limit is a property of the machine. The pacing gate stays in observe mode on desktop, but this cap is a hard ceiling, like the relay's was.

Not covered (stated limits): a daemon from an older Orca version kept alive across an update does not publish a count. It still counts its own terminals, but the others cannot see them. The in-process fallback provider, used only while the daemon cannot spawn, is not counted.

Linked Issue

Part of the headless-serve robustness work for #10493 (D1 step 4, decision Q1: a total terminal cap per execution host to replace the deleted relay's 50).

Visual Proof

N/A. The only visible change is the text of an existing terminal error toast. This environment cannot launch Electron. The translation swap is covered by a unit test.

Testing

  • Ledger: two daemons sharing one directory share one limit; a refused reservation leaves the published count unchanged; a dead owner's file is deleted; an unrefreshed file is ignored; a refreshing idle daemon stays counted and is dropped when it closes; an unreadable entry or an unusable directory never blocks; the env override is validated.

  • Gate, through a real TerminalHost: a shell and an old-client restore are refused with the message at the limit, and running sessions stay alive; declared automatic-restore and user-resume get live_cap; an exit frees a slot; attach is never charged; a failed fork frees its slot; a paced restore at the limit is refused without waiting.

  • Renderer: the cap message is translated.

  • pnpm tc, pnpm lint, oxlint and pnpm run check:code-quality:changed pass. In src/main/daemon, 225 files pass. The only failures are the daemon-init-*, entry-layout and --expose-gc suites. They fail on this machine before any of this code runs, because the node binary path is not found when they spawn.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

SSH: the limit is enforced by the daemon on the execution host, and the count lives on that host. A client never reads or writes it. Windows: per-user temp dir, with rename retries for antivirus locks. WSL panes are wsl.exe processes on the Windows host, so they count there. Performance: one small write and one directory read per new terminal, never per attach.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@OrcaWin
OrcaWin requested a review from brennanb2025 as a code owner October 11, 2026 13:55
@coderabbitai

coderabbitai Bot commented Oct 11, 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: c2dac424-b557-444a-8063-1b298c25b485


📥 Commits

Reviewing files that changed from the base of the PR and between bbac3af and cf03929.



📒 Files selected for processing (19)
  • src/main/daemon/daemon-automatic-work-guard.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-server-options.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/daemon-socket-endpoint-path.ts
  • src/main/daemon/host-terminal-ledger.test.ts
  • src/main/daemon/host-terminal-ledger.ts
  • src/main/daemon/terminal-spawn-gate.test.ts
  • src/main/daemon/terminal-spawn-gate.ts
  • src/renderer/src/components/terminal-pane/terminal-spawn-error-display.test.ts
  • src/renderer/src/components/terminal-pane/terminal-spawn-error-display.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/shared/terminal-spawn-error-copy.ts


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




📝 Walkthrough
📝 Walkthrough

Walkthrough

The daemon now tracks terminal counts across processes through a shared host ledger. The spawn gate reserves capacity, updates published counts, and refuses requests at the host cap. Daemon options select and pass the ledger directory, and the guard manages ledger tracking during startup and shutdown. The renderer translates the host-cap error into localized messages.



Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to cf039

This change adds a per-machine terminal cap with safe fallbacks when the shared ledger is unavailable. No merge-blocking risk was identified in the supplied context.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 12 files. (7 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check Passed The title clearly and concisely identifies the main change: adding a terminal cap per execution host.
Description check Passed The description is complete and focused. It explains the user impact, implementation, rationale, linked issue, testing, platform considerations, known test failures, and checklist status. The Linked I…

Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 12 files. (7 skipped: 7 unsupported.)


  • 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.

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