You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(daemon): cap terminals per execution host - #27488
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)
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 | 4 | 1
❌ Failed checks (1 warning)
Check name
Status
Explanation
Resolution
Docstring Coverage
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
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Title check
The title clearly and concisely identifies the main change: adding a terminal cap per execution host.
Description check
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/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.terminal_spawn_deferred:live_capanswer, 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.ORCA_DAEMON_MAX_HOST_TERMINALS(1 to 4096) in the daemon's environment. This follows the existingORCA_DAEMON_SESSION_SCROLLBACK_ROWSpattern.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; declaredautomatic-restoreanduser-resumegetlive_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,oxlintandpnpm run check:code-quality:changedpass. Insrc/main/daemon, 225 files pass. The only failures are thedaemon-init-*, entry-layout and--expose-gcsuites. 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
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.exeprocesses on the Windows host, so they count there. Performance: one small write and one directory read per new terminal, never per attach.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)