Repository navigation
feat(daemon): opt-in OS ceiling on terminal processes, reported per host - #27769
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🔵 Low · up to The opt-in process ceiling can be reported as active if its cgroup limit changes while the control file remains. This is a bounded operational risk; be aware that the reported status does not verify the configured value. Pre-merge checks |
|
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
2af748d0-7f27-4fb9-9d51-0951ae77dfff
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (15)
config/patches/node-pty@1.1.0.patchsrc/cli/specs/serve.tssrc/main/daemon/daemon-cgroup-scope.tssrc/main/daemon/daemon-entry.tssrc/main/daemon/daemon-pid-file-parse.tssrc/main/daemon/daemon-scope-death-watch.tssrc/main/daemon/daemon-spawner.tssrc/main/daemon/pty-process-ceiling-runtime.tssrc/main/daemon/pty-process-ceiling-state.tssrc/main/daemon/pty-process-ceiling.test.tssrc/main/daemon/pty-process-ceiling.tssrc/main/daemon/pty-subprocess/native-pty-spawn.tssrc/main/orcad/orcad-health.tssrc/main/server/serve-readiness.tssrc/main/windows/windows-pty-job.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.
| + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobLimits{}; | ||
| + jobLimits.BasicLimitInformation.LimitFlags = | ||
| + handle->allowJobBreakaway ? JOB_OBJECT_LIMIT_BREAKAWAY_OK : 0; | ||
| + // Orca: an opt-in ceiling; forks past it fail inside this terminal's tree only. | ||
| + const uint32_t activeProcessLimit = ptyActiveProcessLimit.load(); | ||
| + if (activeProcessLimit > 0) { | ||
| + jobLimits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS; | ||
| + jobLimits.BasicLimitInformation.ActiveProcessLimit = activeProcessLimit; | ||
| + } | ||
| + if (!SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobLimits, sizeof(jobLimits)) || | ||
| + !AssignProcessToJobObject(hJob, piClient.hProcess)) { | ||
| + // Why tolerate failure: an outer job without JOB_OBJECT_LIMIT_BREAKAWAY_OK |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '875,935p' config/patches/node-pty@1.1.0.patch
sed -n '1210,1245p' config/patches/node-pty@1.1.0.patch
sed -n '93,125p' src/main/daemon/pty-process-ceiling.tsRepository: stablyai/orca
Length of output: 6466
🏁 Script executed:
set -eu
printf '%s\n' '--- relevant symbols ---'
rg -n -F --glob '*.{cc,js,ts}' -- 'setActiveProcessLimit' . || true
rg -n -F --glob '*.{cc,js,ts}' -- 'setWindowsLimit' . || true
rg -n -F --glob '*.{cc,js,ts}' -- 'PtyConnect' . || true
rg -n -F --glob '*.{cc,js,ts}' -- 'hJob' . || true
printf '%s\n' '--- Windows native patch around job setup and setter ---'
sed -n '820,960p' config/patches/node-pty@1.1.0.patch
printf '%s\n' '--- TypeScript resolver and Windows setup ---'
sed -n '1,240p' src/main/daemon/pty-process-ceiling.ts
printf '%s\n' '--- all resolver call sites and status consumers ---'
rg -n -F --glob '*.ts' -- 'resolvePtyProcessCeiling' src test tests . || true
rg -n -F --glob '*.ts' -- 'ptyProcessCeiling' src test tests . || trueRepository: stablyai/orca
Length of output: 29960
🏁 Script executed:
set -eu
printf '%s\n' '--- Windows job binding ---'
nl -ba src/main/windows/windows-pty-job.ts | sed -n '1,70p;210,255p'
printf '%s\n' '--- ceiling runtime initialization ---'
nl -ba src/main/daemon/pty-process-ceiling-runtime.ts | sed -n '1,90p'
printf '%s\n' '--- Windows ceiling tests ---'
nl -ba src/main/daemon/pty-process-ceiling.test.ts | sed -n '100,125p'Repository: stablyai/orca
Length of output: 10081
Do not report the Windows ceiling as on after job setup falls back
setPtyActiveProcessLimit returns the native setter result, but the native setter only stores the limit for future jobs. If SetInformationJobObject or AssignProcessToJobObject later fails, PtyConnect closes hJob and resumes the shell without a job. The intentional fallback keeps the terminal running, but that shell can spawn descendants without JOB_OBJECT_LIMIT_ACTIVE_PROCESS while the daemon reports the ceiling as on.
When a limit is requested, make job setup failure fail PtyConnect through its existing cleanup path instead of resuming an unassigned shell. The runtime contract states that the ceiling is applied to every PTY spawn.
| + | ||
| +/** | ||
| + * Orca: setActiveProcessLimit(limit) applies JOB_OBJECT_LIMIT_ACTIVE_PROCESS to | ||
| + * every ConPTY job created afterwards. 0 turns it off. Existing jobs keep theirs. | ||
| + */ | ||
| +static Napi::Value PtySetActiveProcessLimit(const Napi::CallbackInfo& info) { | ||
| + Napi::Env env(info.Env()); | ||
| + Napi::HandleScope scope(env); | ||
| + if (info.Length() != 1 || !info[0].IsNumber()) { | ||
| + throw Napi::Error::New(env, "Usage: pty.setActiveProcessLimit(limit)"); | ||
| + } | ||
| + ptyActiveProcessLimit.store(info[0].As<Napi::Number>().Uint32Value()); | ||
| + return Napi::Boolean::New(env, true); | ||
| +} | ||
| + | ||
| /** | ||
| * Init | ||
| */ | ||
| @@ -577,6 +887,9 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { | ||
| @@ -577,6 +910,10 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { | ||
| exports.Set("resize", Napi::Function::New(env, PtyResize)); | ||
| exports.Set("clear", Napi::Function::New(env, PtyClear)); | ||
| exports.Set("kill", Napi::Function::New(env, PtyKill)); | ||
| + exports.Set("terminateJob", Napi::Function::New(env, PtyTerminateJob)); | ||
| + exports.Set("listJobProcessIds", Napi::Function::New(env, PtyListJobProcessIds)); | ||
| + exports.Set("assignCurrentProcessToJob", Napi::Function::New(env, PtyAssignCurrentProcessToJob)); | ||
| + exports.Set("setActiveProcessLimit", Napi::Function::New(env, PtySetActiveProcessLimit)); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '40,60p' src/main/daemon/pty-process-ceiling.ts
sed -n '92,119p' src/main/daemon/pty-process-ceiling.ts
sed -n '1208,1240p' config/patches/node-pty@1.1.0.patch
sed -n '890,930p' config/patches/node-pty@1.1.0.patchRepository: stablyai/orca
Length of output: 5564
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- resolver and callers ---'
rg -n -F --glob '*.{ts,tsx,js,cjs,mjs}' -- 'resolvePtyProcessCeiling' src tests config || true
rg -n -F --glob '*.{ts,tsx,js,cjs,mjs}' -- 'setActiveProcessLimit' src tests config || true
sed -n '1,240p' src/main/daemon/pty-process-ceiling.ts
printf '%s\n' '--- native symbol/storage and job application ---'
rg -n -F --glob 'config/patches/node-pty@1.1.0.patch' -- 'ptyActiveProcessLimit' config/patches/node-pty@1.1.0.patch
rg -n -F --glob 'config/patches/node-pty@1.1.0.patch' -- 'setActiveProcessLimit' config/patches/node-pty@1.1.0.patch
rg -n -F --glob 'setWindowsLimit' -- 'setWindowsLimit' src tests config || true
sed -n '1,90p' config/patches/node-pty@1.1.0.patchRepository: stablyai/orca
Length of output: 11946
🏁 Script executed:
rg -n -F --glob '*.{ts,tsx,js,cjs,mjs}' -- 'resolvePtyProcessCeiling' src tests config || true
rg -n -F --glob '*.{ts,tsx,js,cjs,mjs}' -- 'setActiveProcessLimit' src tests config || true
sed -n '1,240p' src/main/daemon/pty-process-ceiling.ts
rg -n -F --glob 'config/patches/node-pty@1.1.0.patch' -- 'ptyActiveProcessLimit' config/patches/node-pty@1.1.0.patch
rg -n -F --glob 'config/patches/node-pty@1.1.0.patch' -- 'setActiveProcessLimit' config/patches/node-pty@1.1.0.patch
rg -n -F --glob 'setWindowsLimit' -- 'setWindowsLimit' src tests config || true
sed -n '1,90p' config/patches/node-pty@1.1.0.patchRepository: stablyai/orca
Length of output: 11867
🏁 Script executed:
printf '%s\n' 'CALLERS'
rg -n -F --glob '*.ts' -- 'resolvePtyProcessCeiling' src || true
rg -n -F --glob '*.ts' -- 'setActiveProcessLimit' src || true
printf '%s\n' 'RESOLVER'
nl -ba src/main/daemon/pty-process-ceiling.ts | sed -n '40,180p'
printf '%s\n' 'NATIVE'
rg -n -F -- 'ptyActiveProcessLimit' config/patches/node-pty@1.1.0.patch
nl -ba config/patches/node-pty@1.1.0.patch | sed -n '880,940p'
nl -ba config/patches/node-pty@1.1.0.patch | sed -n '1208,1240p'Repository: stablyai/orca
Length of output: 13367
🏁 Script executed:
rg -n -F --glob '*.ts' -- 'resolvePtyProcessCeiling' .
rg -n -F --glob '*.ts' -- 'setWindowsLimit' .
rg -n -F --glob '*.ts' -- 'setActiveProcessLimit' .Repository: stablyai/orca
Length of output: 2745
Reject Windows ceilings above UINT32_MAX before calling the native setter.
parsePtyProcessCeiling accepts safe integers above the Windows 32-bit range. On Windows, the daemon passes the value to setActiveProcessLimit, which stores it with Uint32Value(). Thus, 4294967296 becomes 0, while the setter returns true. The resolver reports the ceiling as on, but job creation skips JOB_OBJECT_LIMIT_ACTIVE_PROCESS because the stored value is zero.
Suggested fix
if (input.platform === 'win32') {
+ if (parsed.limit > 0xffffffff) {
+ return off('invalid_limit')
+ }
return input.setWindowsLimit?.(parsed.limit)| if (!pidsMaxExists(runtime.pidsMaxPath)) { | ||
| setPtyProcessCeilingForTest({ state: 'off', reason: 'slice_lost' }, null) | ||
| return { file, args, env } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
rg -n 'processCeiling|currentPtyProcessCeiling|write.*Pid|pidFile' src/main/daemon/daemon-entry.ts src/main/daemon/pty-process-ceiling-runtime.ts src/main/orcad/orcad-health.ts src/main/server/serve-readiness.ts src/main/daemon/daemon-spawner.ts
sed -n '40,110p' src/main/daemon/pty-process-ceiling-runtime.tsRepository: stablyai/orca
Length of output: 3910
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- daemon-entry relevant blocks ---'
nl -ba src/main/daemon/daemon-entry.ts | sed -n '115,165p;270,315p'
printf '%s\n' '--- daemon-spawner relevant definitions and callers ---'
nl -ba src/main/daemon/daemon-spawner.ts | sed -n '1,55p;135,220p'
printf '%s\n' '--- runtime state helpers ---'
nl -ba src/main/daemon/pty-process-ceiling-runtime.ts | sed -n '1,75p'
printf '%s\n' '--- health reader and fallback ---'
nl -ba src/main/orcad/orcad-health.ts | sed -n '35,75p;140,180p'
printf '%s\n' '--- readiness reader and fallback ---'
nl -ba src/main/server/serve-readiness.ts | sed -n '85,130p'
printf '%s\n' '--- all pid record publication references ---'
rg -n -F -- 'publishDaemonPidFile(' src/main
rg -n -F -- 'replaceDaemonPidFile(' src/mainRepository: stablyai/orca
Length of output: 21845
🏁 Script executed:
set -o pipefail
nl -ba src/main/daemon/daemon-entry.ts | sed -n '115,165p;270,315p'
nl -ba src/main/daemon/daemon-spawner.ts | sed -n '1,55p;135,220p'
nl -ba src/main/daemon/pty-process-ceiling-runtime.ts | sed -n '1,75p'
nl -ba src/main/orcad/orcad-health.ts | sed -n '35,75p;140,180p'
nl -ba src/main/server/serve-readiness.ts | sed -n '85,130p'
rg -n -F -- 'publishDaemonPidFile(' src/main || test "$?" -eq 1
rg -n -F -- 'replaceDaemonPidFile(' src/main || test "$?" -eq 1Repository: stablyai/orca
Length of output: 21603
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- publication callback definition and calls ---'
rg -n -F -- 'publishEndpointOwnership' src/main || test "$?" -eq 1
rg -n -F -- 'currentPtyProcessCeiling' src || test "$?" -eq 1
rg -n -F -- 'setPtyProcessCeilingForTest' src || test "$?" -eq 1
printf '%s\n' '--- daemon start implementation ---'
rg -n -F -- 'export async function startDaemon' src/main || test "$?" -eq 1
rg -n -F -- 'function startDaemon' src/main || test "$?" -eq 1
rg -n -F -- 'startDaemon(' src/main/daemon src/main | head -80Repository: stablyai/orca
Length of output: 8863
🏁 Script executed:
set -o pipefail
nl -ba src/main/daemon/daemon-endpoint-lifecycle.ts | sed -n '1,100p'
nl -ba src/main/daemon/daemon-main.ts | sed -n '1,65p;155,190p'Repository: stablyai/orca
Length of output: 7707
Republish the PID record when the ceiling is lost.
When wrapPtySpawnForCeiling detects a missing pids.max, it changes the runtime state to off (slice_lost) and starts the PTY without the ceiling. The PID record still contains the startup processCeiling value. orcad-health and orca serve can therefore continue to report on.
Republish the PID record after this transition. Use currentPtyProcessCeiling() and preserve the existing record fields and replacement semantics. The health reader has no current-runtime fallback.
| const tcc = wrapShellSpawnForMacosTccAttribution(shellPath, shellArgs, args.env) | ||
| // Linux with the opt-in ceiling: exec through systemd-run into the ceiling slice. | ||
| const wrapped = wrapPtySpawnForCeiling(tcc.file, tcc.args, args.env) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '30,100p' src/main/daemon/pty-subprocess/native-pty-spawn.ts
sed -n '40,105p' src/main/daemon/pty-process-ceiling-runtime.ts
rg -n 'systemd-run|spawnNativeDaemonPty|spawnAt' src/main/daemon/pty-subprocess src/main/daemon/pty-process-ceiling-runtime.tsRepository: stablyai/orca
Length of output: 6353
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- native PTY spawn ---'
nl -ba src/main/daemon/pty-subprocess/native-pty-spawn.ts | sed -n '1,125p'
printf '%s\n' '--- ceiling runtime definitions and callers ---'
rg -n -F --glob '*.ts' -- 'wrapPtySpawnForCeiling' src
rg -n -F --glob '*.ts' -- 'spawnNativeDaemonPty' src
rg -n -F --glob '*pty-process-ceiling*' -- 'terminals continue' src docs . 2>/dev/null || true
printf '%s\n' '--- relevant ceiling tests and documentation references ---'
rg -n -F --glob '*.{ts,md}' -- 'systemd-run' src docs . 2>/dev/null || true
rg -n -F --glob '*.{ts,md}' -- 'slice_lost' src docs . 2>/dev/null || true
rg -n -F --glob '*.{ts,md}' -- 'process ceiling' src docs . 2>/dev/null || trueRepository: stablyai/orca
Length of output: 17502
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- PTY subprocess caller ---'
nl -ba src/main/daemon/pty-subprocess.ts | sed -n '1,190p'
printf '%s\n' '--- PTY spawn consumers and lifecycle handling ---'
rg -n -F --glob '*.ts' -- 'spawned.process' src/main/daemon src/main | head -80
rg -n -F --glob '*.ts' -- '.onExit' src/main/daemon/pty-subprocess.ts src/main/daemon src/main | head -100
printf '%s\n' '--- ceiling tests ---'
nl -ba src/main/daemon/pty-process-ceiling.test.ts | sed -n '1,230p'
printf '%s\n' '--- node-pty dependency declarations ---'
rg -n -F --glob 'package.json' -- 'node-pty' .
rg -n -F --glob 'pnpm-lock.yaml' -- 'node-pty@' .
printf '%s\n' '--- changed diff ---'
git diff --no-ext-diff --unified=40 6c3ce444bc36b5826733a4f064faab3df6f495ef d74b886c3bf811892f1c131c2d850c99292bdc07 -- src/main/daemon/pty-subprocess/native-pty-spawn.ts src/main/daemon/pty-process-ceiling-runtime.ts src/main/daemon/pty-subprocess.tsRepository: stablyai/orca
Length of output: 32920
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- subprocess handle lifecycle ---'
nl -ba src/main/daemon/pty-subprocess/subprocess-handle.ts | sed -n '1,235p'
printf '%s\n' '--- node-pty 1.1.0 source or local package evidence ---'
find . -path '*/node-pty/*' -o -path '*/node_modules/node-pty*' | head -40
rg -n -F --glob '*.{ts,cpp,h,cc,md}' -- 'class UnixPtyProcess' . 2>/dev/null || true
rg -n -F --glob '*.{ts,cpp,h,cc,md}' -- 'spawn(file' . 2>/dev/null || true
printf '%s\n' '--- tests for native spawn failure and immediate exit ---'
rg -n -F --glob '*.test.ts' -- 'spawnNativeDaemonPty' src/main
rg -n -F --glob '*.test.ts' -- 'createPtySubprocess' src/main/daemon | head -80Repository: stablyai/orca
Length of output: 23788
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- node-pty 1.1.0 Unix spawn implementation ---'
nl -ba node_modules/.pnpm/node-pty@1.1.0_patch_hash=*/node_modules/node-pty/lib/unixTerminal.js | sed -n '1,230p'
printf '%s\n' '--- node-pty 1.1.0 API declarations ---'
rg -n -F --glob '*.d.ts' -- 'spawn(' node_modules/.pnpm/node-pty@1.1.0_patch_hash=*/node_modules/node-pty
rg -n -F --glob '*.js' -- 'pty.fork' node_modules/.pnpm/node-pty@1.1.0_patch_hash=*/node_modules/node-pty/libRepository: stablyai/orca
Length of output: 11003
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- systemd manager failure contract ---'
nl -ba src/main/daemon/daemon-cgroup-scope.ts | sed -n '130,160p'
printf '%s\n' '--- repository wording for continued terminal spawning ---'
rg -n -i --glob '*.{md,mdx,ts,tsx}' -- 'terminals? (continue|continues|still|keep).*(spawn|start)|spawn.*terminals?|terminal.*ceiling|process ceiling' . 2>/dev/null || trueRepository: stablyai/orca
Length of output: 41476
Retry when the Linux ceiling wrapper fails.
wrapPtySpawnForCeiling checks only pids.max. If the user manager is unreachable, node-pty still returns an IPty for systemd-run; the wrapper then exits asynchronously. The Linux path has no retry, so createDaemonPtySubprocessHandle marks the new terminal dead instead of launching the shell unwrapped.
Handle a wrapper startup failure in native-pty-spawn.ts. Disable the ceiling and retry the original tcc.file and tcc.args once. A try/catch around nodePty.spawn is not sufficient because node-pty can return before systemd-run exits.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
b5daa823-5ac5-4936-bb1d-08b3d723ec8a
📒 Files selected for processing (5)
src/main/daemon/daemon-scope-death-watch.tssrc/main/daemon/pty-process-ceiling-state.tssrc/main/daemon/pty-process-ceiling.test.tssrc/main/daemon/pty-process-ceiling.tssrc/main/orcad/orcad-health.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.
| return exists(join('/sys/fs/cgroup', state.controlGroup, 'pids.max')) | ||
| ? state | ||
| : { state: 'off', reason: 'slice_lost' } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Check the limit when confirming a slice.
If pids.max changes to max while the daemon runs, existsSync still returns true. Health then reports on although the slice no longer enforces state.limit. Read pids.max and compare its value with state.limit; report off when the values differ. The kernel defines max as an unlimited PID setting. (kernel.org)
…daemon, and keep terminals starting when systemd-run is unusable
…rt a dropped slice as lost
f40da9c to
b1a4249
Compare
…ary (#28054) #27756's script imported baseline helpers from check-focus-read-ratchet.mjs after #27788 had moved them into ratchet-baseline.mjs, so it crashed on start and failed every PR that merges main. Use the library's helpers, and record the two daemon-process launches from #27769 that the broken check never saw. Co-authored-by: Orca Worker <orca-worker@localhost>
…ary (#28054) #27756's script imported baseline helpers from check-focus-read-ratchet.mjs after #27788 had moved them into ratchet-baseline.mjs, so it crashed on start and failed every PR that merges main. Use the library's helpers, and record the two daemon-process launches from #27769 that the broken check never saw. Co-authored-by: Orca Worker <orca-worker@localhost>
…27875) * fix(browser): annotation card and outline follow the selected element The grab overlay keeps re-measuring the picked element after the click (scroll in any box, resize, element size changes), and the isolated viewport bridge reports that live box so the pending card follows it, including page zoom. The card's side pick uses its measured height. * fix(browser): bind annotation card to selected element * fix(browser): keep document annotation card on selection * fix(ci): point the main sync-spawn ratchet at the shared ratchet library (#28054) #27756's script imported baseline helpers from check-focus-read-ratchet.mjs after #27788 had moved them into ratchet-baseline.mjs, so it crashed on start and failed every PR that merges main. Use the library's helpers, and record the two daemon-process launches from #27769 that the broken check never saw. Co-authored-by: Orca Worker <orca-worker@localhost> * fix(browser): follow selected element outer-box size * refactor(chat): give the start-refusal resumable table its own module (#28085) #27946 and #26684 together put agent-session-failure-words.ts at 304 lines, over max-lines, which fails lint and the pre-commit hook on every merge of main. The refusal-code table is standalone with one reader, so it moves out. Co-authored-by: Orca Worker <orca-worker@localhost> --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai> Co-authored-by: Orca Worker <orca-worker@localhost>
Upstream stablyai#27769 changed the Windows conpty section of config/patches/node-pty@1.1.0.patch. This PR changes only the Unix section, so the patch file merges without conflict. The lockfile took upstream's version and pnpm 12.8.1 regenerated the node-pty patch hash from the merged patch. The lockfile differs from upstream main only in the three node-pty patch hash references. The PR's own changes are unchanged from the previous head. Validation: a frozen-lockfile install applied the merged patch and rebuilt node-pty; the addon still imports cfsetispeed and cfsetospeed at GLIBC_2.2.5. 309 tests passed across 15 focused files on the Node runner, including upstream's PTY process ceiling tests; 11 Windows- or macOS-only cases skipped. oxlint and oxfmt passed on the changed scripts. The Bun runner and native prebuild builds were not rerun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ELI5
A runaway build or agent inside a terminal can fork thousands of processes and take the whole machine down; #10493 was a kernel panic. Orca already limits how many agents it restores at once, but that is a software limit, and it can't stop what a running agent forks afterwards. This PR adds an optional kernel-enforced cap on the processes that terminals can run. It is off by default: it should be switched on by default only after saturation testing shows the terminal host stays responsive while the cap is being hit.
What Changed
ORCA_TERMINAL_PROCESS_CEILING=<n>(64 or more) turns it on when the terminal daemon starts. Unset or0means off; a smaller or non-numeric value is reported as off with the reasoninvalid limit.busctl) for one transient slice,orca_ptys_<launch>.slice, withTasksMax=<n>.systemd-run --user --scope --slice=…into its own scope under that slice.systemd-run --scopeexecs in place, so the pid, argv and exit status are unchanged. Once the slice holds<n>tasks, a fork inside it fails with EAGAIN.pids.maxcounts threads, and the daemon has to keep serving sockets, attach and cancel while a build in a terminal hits the cap. The name has no dashes, because systemd would read dashes as nesting.off (no user manager). It uses the same capability probe as the daemon's durable scope. If the slice can't be created, it reportsoff (slice unavailable). In both cases terminals spawn exactly as before.setActiveProcessLimit(n). When it is set, every later ConPTY job also getsJOB_OBJECT_LIMIT_ACTIVE_PROCESS, so the cap applies to each terminal's tree, not to all terminals together.off (native unavailable).off (unsupported platform). The design keepsRLIMIT_NPROCas a prototype for later.processCeiling(onwith limit and mechanism, oroffwith a reason) into its pid record, next tocgroupUnit.terminalDaemon.processCeiling.orca serve's human readiness gains a line such asTerminal process ceiling: off (disabled).orca serve --helpdocuments the switch.Why
The software caps already landed (live automatic-restore cap, total terminal cap, growth breaker), but only the kernel can guarantee a bound. The owner's decision (D1 Q4) was:
Rejected alternatives:
TasksMaxto the daemon's own scope (the original sketch). That would count the daemon's threads and let a fork bomb starve the daemon.Exec'ing through
systemd-runplaces the shell before it can fork.Linked Issue
Part of the headless-serve robustness work (C5 / Q4), toward #10493.
Visual Proof
N/A. Off by default, with no UI. The only visible change is one extra readiness line in
orca serveoutput.Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
pty-process-ceiling.test.tscovers:TasksMaxand a top-level name, and a failed request degrades;systemd-runwrap (argv kept, Chromium's disabled bus removed);No real systemd or Windows job was exercised here. That, and the saturation test (keep the daemon serving while a terminal hits the cap), are why it ships off.
The Windows native change compiles in CI's Windows build. When the switch is off it is inert: the static limit stays 0.
pnpm tc, oxlint andcheck:code-quality:changedpass locally;pnpm lintis covered by CI.Review
Agent skill upstream boundary
Notes
pids.events;Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)