mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
bc2fc4ea2bfd106d96fc80bd783fd0dad1fabc19
31
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
07b437c9e9 |
fix(terminal): detect foreground from the physical PTY owner (#36342)
* fix(terminal): observe foreground at the physical PTY owner
* fix(terminal): keep existing inventory, inspection and display semantics around master-group foreground
Review round 1 fixes for the physical-owner foreground observation:
- Stop and liveness verification keep complete-inventory proof: the router's
scoped listProcesses({ptyIds}) is owner resolution again (absent = []).
Only the launch guard's old-owner fallback asks for an established
target's owner via an internal targetOwnerOnly option.
- Degraded daemon mode routes foreground observations and target reads to
the established owner, so an unreachable unrelated daemon cannot refuse a
known terminal there either.
- Display and name refreshes read the master's foreground group but take
member rows from the shared single-flight process-table snapshot; no
per-terminal ps per refresh. Recognized agent names skip the confirm scan
again, and builds without the native accessor keep the existing reader.
- Ordinary inspections keep their first-match routing; strict owner and
incarnation checks apply only to foreground decisions. The unused
foregroundOnly preload/IPC/RPC surface is removed.
- Local inventory forwards only the target scope, never caller deadlines.
- An interpreter running an agent script counts as that agent, as before;
a blank argv row no longer voids a terminal capture outside the
foreground group.
- Bump the packaged compatibility contract to daemon protocol 55 and move
the node-pty accessor above the process getter's doc comment.
* fix(terminal): keep the node-pty patch hunk well-formed and drop redundant foreground tests
Remove two stray context lines from the src/unixTerminal.ts hunk so its body
matches its header (strict patch parsers reject the mismatch), and update the
lockfile patch hash. Prune foreground tests whose rows repeat branches the
parity table already covers.
* fix(terminal): index the shared snapshot by TTY once and keep observation age nonnegative
Display refreshes filtered the whole shared process table per pane, so
work grew with panes times processes. Group each snapshot by normalized
TTY once, memoized by array identity, and look each pane up directly;
the scaling contract now counts row TTY reads at N and 10N panes.
Both foreground observation producers clamp capturedAgeMs at zero, so a
wall-clock step back during the read no longer yields an envelope the
validator rejects and the launch guard refuses.
* test: move foreground-observation compat tests into the protocol compatibility suite
Main removed daemon-pty-adapter-steady-state-compat.test.ts; the merge restored it with only this branch's block. Reuse the suite's existing inspection adapter helper so no new type assertion is added.
|
||
|
|
eb3379ff99 |
Fix macOS daemon PTY kqueue descriptor leak (#16870)
* fix(daemon): release node-pty kqueue descriptors * test: remove redundant wait from kqueue regression |
||
|
|
756d623955 |
perf(terminal): bound the in-process macOS process table's blocking calls and ps fallbacks (#31783) (#36282)
* fix(terminal): bound the in-process macOS process table's blocking calls and ps fallbacks (#31783) Follow-up to #35987 from its post-merge reviews. - cwd and executable path: node-pty's readProcessCwd/readProcessPath now run PROC_PIDVNODEPATHINFO and proc_pidpath on a libuv worker (the synchronous exports are gone, so an older pty.node falls back to ps/lsof). JS caps them at two outstanding lookups and 250ms; a wedged mount or a full cap answers from lsof -b, which never stats, so a dead NFS/SMB/FUSE mount cannot freeze the daemon's event loop or take the whole pool. - Unreadable argv prints "(comm)", as ps does, instead of a proc_pidpath lookup under Orca's credentials on the table worker. - The table exports each row's euid. Argv recovery runs setuid ps only for another user's processes, never for our own mid-exec or exiting ones (or SIDL/exiting rows), which ps cannot read either: a pane looping short commands forked ps on ~15% of full reads, now none. - Recovery is coalesced per process across concurrent readers and a failed recovery waits 5s before costing another ps. The cache keys on comm and start time. - Full-table and shell-foreground captures, which feed strict evidence with the capture's age, take only argv ps read after their request; display readers keep the 30s cache. - While the reader is down (stalled-pool breaker open, or a rejecting addon, which now backs off 1s..30s), runPsInProcess runs /bin/ps itself through one shared budget: identical invocations share a run, a burst of 4 forks then one a second, and a run spawned before a request never answers it. Descendant teardown and foreground-group signalling keep their own full-timeout ps (callerRunsPs). 13 pollers at 750ms: 1,040 ps/min before, 63 after. No daemon protocol bump: the wire format is unchanged, and each daemon loads JS and pty.node from the same runtime copy. * fix(terminal): make the libproc reader survive worker teardown, fail closed and share one fair fork budget (#31783) Review follow-ups to the previous commit. - Native: every libproc AsyncWorker derives from OrcaPromiseWorker, which skips JS completion once an env cleanup hook marks the env closing and swallows a Napi::Error thrown mid-teardown. Terminating a worker_thread with cwd or table reads in flight aborted the process; 200 terminates each now survive. - (comm) in a terminal's foreground group (argv neither reader recovered, e.g. a root `su`) judges 'unknown', never 'agent', so the launch line is not typed into it. - Fresh argv only for getFreshSnapshot captures; the 500ms TTL readers keep the 30s cache. Argv recovery runs through its own fork budget (2, then one per 2s). Root `su` pane, 13 pollers at 750ms: TTL 79 -> 2 ps/min, fresh 158 -> 31. - The fork budget serves invocations in FIFO turns, grouping each one's waiters onto one run. 13 streams: served 80,42,1,1,2,0x8 -> 10-12 each. - Forced subprocess captures (a held naming pass) go through the fallback budget: 5 readers, 400 direct ps/min -> 63, 395 of 400 captures served. - A failed recovery is retried 5s after ps gave up, not after it started. - Tests pin Orca's euid instead of reading the runner's and assuming root is 0. - process-cwd keeps one lsof per pid until it exits, so a dead mount no longer leaks one per poll; shell readiness reads the exe path through runProcess, which gives up on an lsof that ignores SIGTERM. - A #35987 pty.node (table reader, no async path reads) keeps the in-process table; cwd/path use lsof, with a one-time warning. - captureProcessTable moves to process-table-ps-capture.ts. * fix(terminal): make withheld argv unverifiable and reuse recovered argv for at most 2s (#31783) Round-2 review follow-ups. - Worker teardown: drop OrcaPromiseWorker and its env flag (Node drains the pool before cleanup hooks, so it never fired). node-pty's mac pty target now builds with NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS, which covers both the call-in and completion paths. The smoke test now calls the reads from a timer while terminating: the previous build aborted 2 of 3 runs, this one survives 3/3 x 100. pty.cc is back to the first follow-up's exact content. - A foreground process whose argv ps withheld (`(comm)`: another user's, exiting, or a recovery the budget skipped) makes the batch resolver, and so host-stamped evidence, `argv_unreadable`/unverifiable rather than live with no agent; agent-foreground resolution reports it unavailable. - judgeTerminalForeground fails closed only for an unreadable shell (`(bash)` from a root `su`), not our own exiting `(git)`. - Recovered argv is reused for at most 2s per identity (pid, start, comm) for every reader (the next commit ages evidence by it), and fresh reads no longer queue behind the recovery budget. Root `su` pane, 13 fresh pollers: p50 2,989ms -> 53ms, 31 -> 26 ps/min (TTL reader 1 -> 26). - The fork budgets serve interactive and fresh readers before display polls (cheap and TTL snapshot captures), FIFO within each. 20 display streams plus a 3s launch-paste read every 5s: 0/12 -> 12/12 served, polls 4-7 each. - On a selected terminal, the foreground group's argv is recovered even with no process of ours left (`exec su`); the root login is not. - Budgeted captures keep the 32 MiB output ceiling. * fix(terminal): age strict evidence by its oldest recovered argv (#31783) Round-3 review follow-ups. - Recovered argv carried no read time, so a capture reusing it (cached, or a recovery still in flight) stamped its own capture time over argv up to 2s older, and the remote write guard could take a same-comm re-exec for the agent. Rows now carry when ps read their recovered argv; runPsInProcess reports the oldest through onArgvReadAt, and the snapshot reader stamps capturedAtMs at that input, so capturedAgeMs is the age of the oldest input and consumers' own freshness checks (the write guard's 500ms) decide. A recovery begun more than 2s before a reader asked no longer answers it, and an older recovery finishing late cannot replace a newer one. - resolveSpawnFileForegroundFromRows reports a withheld `(node)` foreground unavailable. - One budget queue entry per invocation: an interactive request promotes a queued poll's, a poll joins an interactive one. - parseShellForegroundRows gets its JSDoc back. * fix(terminal): keep a cached name from authorizing a refused write; integer, under-2s argv ages (#31783) Round-4 review follow-ups. - When the relay's evidence declines a write (stale, unverifiable, another agent), its foreground name may still prove the shell but no longer proves the agent: that name can be the tracker's 1s display cache, e.g. `claude` cached just before Claude exited to a root shell under su. Relays that send no evidence keep the name as before. - Snapshot ages round up to whole milliseconds; the wire rejects fractions from the monotonic-to-wall conversion. - Recovered argv is reused for at most 1.5s, so reuse plus the 500ms snapshot cache stays under the 2s freshness gate; reuse ages from when that ps spawned, not from the wait for its fork token, and only an answer is remembered (a timed-out or withheld run proves nothing). Fork budget unchanged. Root su pane, 13 pollers: 30 ps/min; fresh p50 51ms; strict evidence age p50 752ms, max 1,508ms. * fix(terminal): keep the relay's agent name behind structurally unverifiable evidence (#31783) The previous commit refused the name fallback whenever the relay sent any evidence, so `unverifiable` evidence for structural reasons also blocked writes: npm Codex (node codex.js spawning native codex) and Gemini (relaunching itself) put two agent rows in the foreground group (ambiguous_foreground_group), and remote tmux reads multiplexer_boundary. On SSH POSIX hosts that dropped launch prompts and failed worker briefs, conversation send/cancel and automations with agent_not_in_foreground. The name now cannot authorize only when the evidence saw the group (`live`: stale, or no agent in it, as in the cached-`claude`-over-root-shell repro) or withheld a foreground argv (`argv_unreadable`, possibly a root `su` shell). Other unverifiable reasons keep the name fallback, as before this branch. * fix(terminal): let a relay name complete only fresh agent-positive evidence (#31783) The SSH name fallback still let a stale or cached name authorize a write under any unverifiable reason but argv_unreadable: ps EAGAIN (process_table_unreadable) after the tracker cached `claude` over a root bash, anchor_missing, tty_boundary (`sudo -i` with use_pty, a root shell on a new pty), or a 1.5s-old ambiguous capture. - The name now completes evidence only when it is fresh (within the guard's 500ms bound), its reason is one that saw an agent row (ambiguous_foreground_group, multiplexer_boundary, candidate_start_time_missing), and the name is the launched agent. Every other reason, including ones this build does not know, reads unknown. Relays that send no evidence keep the name, for mixed versions. - The relay's evidence names one agent that holds two rows of the group (npm Codex: node codex.js spawning native codex; Gemini relaunching itself), so a new relay proves them live even when its name is `node`. Two different agents stay ambiguous. No wire change. * fix(terminal): judge a relay name by the host's capture age; keep old relays' npm Codex (#31783) - Name completion compared the capture's start with the read's start, so a slow response made old evidence look fresh: a capture 1,500ms old arriving after 1,100ms passed. It now uses the host-reported capturedAgeMs against the 500ms bound, which latency cannot shrink. Live evidence keeps its capture-start rule. - Relays before same-agent dedupe report npm Codex (node wrapper plus native binary) as ambiguous_foreground_group and name it `node`. For that reason only, `node` now completes the evidence when the launched agent is npm-installed per the node entrypoint tables; other agents and reasons stay fail-closed. * fix(terminal): bound relay-name completion by twice the snapshot TTL (#31783) The 500ms bound refused ~9.5% of tmux name completions on a host with 13 pollers (cached snapshot up to one 500ms TTL old plus ~30ms capture, max 528ms), and a refusal drops the launch prompt or throws with no retry. The bound is now 2 x PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS (1s), still on the host-reported age: 0 of 767 reads exceed it, the delayed 1,500ms repro still refuses, and the cached-name hazard (the tracker's ~1s display cache) is no wider. * fix(terminal): complete relay evidence with a name read now, not the display cache (#31783) With the 1s bound, a capture at 0-528ms, `codex` cached at 100ms, Codex exiting at 101ms and the guard at 999ms still let the cached name authorize a write over the bash that replaced it. - Name completion (structural reasons, host age within 2x the snapshot TTL) now asks confirmForegroundProcess, which reads the foreground now (a fresh scan, or node-pty's raw process name) rather than the tracker's 1s display cache; a null answer reads unknown. No new wire surface: the local daemon answers confirmForegroundProcess, paired runtimes evaluate guards locally, and the production controller returns null (read as unknown, failing safe) when a provider lacks it. Controllers without the method, and relays sending no evidence, keep the cached name as the legacy path. - Live evidence also accepts a host-reported age within 2x the TTL, so a real agent behind a 500-528ms cached snapshot on a fast link is no longer refused; a capture begun after the read was asked still counts, and 5,000ms-old evidence still refuses. * fix(terminal): confirm live evidence older than one snapshot TTL with a fresh read (#31783) Accepting live evidence up to 2x the TTL reopened the window the previous commit closed for name completion: a capture at 0-528ms, the agent exiting at ~529ms, and at 999ms `live claude` still read as agent over a bash. Live evidence begun no more than one TTL before the read was asked (so any host age within one TTL, however long ps took) still answers alone. Between that and 2x the TTL, confirmForegroundProcess reads the foreground now: the launched agent proves it, a shell reads shell, anything else unknown. Controllers without that read fall through and refuse, as before. Reachability: the remote (host.remote) write-guard path has no production caller since the SSH provider's removal (#27499); the registry dispatches only local providers and paired runtimes evaluate guards locally. This is defence in depth for any future remote provider. |
||
|
|
525719789f |
perf(terminal): read the macOS process table in process instead of forking setuid ps (#35987)
* perf(terminal): read the macOS process table in process instead of forking setuid ps (#31783) /bin/ps is setuid root, so every exec costs an uncached syspolicyd assessment and an EDR inspection. The terminal host forked ~7-10 ps/lsof per second with 4 agent panes, and each stale daemon multiplied that. node-pty's patched pty.node now exports processTable (sysctl KERN_PROC_ALL/KERN_PROCARGS2 on a worker thread), processCwd (PROC_PIDVNODEPATHINFO) and processPath (proc_pidpath). runPsInProcess renders the same ps column text for the invocations Orca issues, so the existing parsers and callers are unchanged; anything it cannot reproduce, non-macOS hosts, and builds without the exports keep running ps/lsof. Measured with 4 anchored panes polled every 750 ms for 60 s: ~440 ps/lsof children before, 0 after. * fix(daemon): roll the in-process macOS process table into a fresh v54 daemon (#31783) * fix(terminal): bound, escape and complete the in-process macOS process table like ps (#31783) Review follow-ups for the libproc reader: - Timeouts: every caller passes its ps timeout; a read stalled behind a saturated libuv pool resolves timed out (code null, timedOut) at that budget, exactly as the killed ps would. Identical native reads share one worker, each caller racing its own deadline; past 8 distinct reads in flight callers run ps. execFile-shaped callers reject like a failed child. - tty naming shares one slot with captureDarwinProcessTable's guard, so a draining naming pass sends both paths to ps instead of naming again. - Commands: the addon renders argv with adv_cmds' getproclline and strnvis (VIS_TAB|VIS_NL|VIS_NOSLASH) under the caller's LC_CTYPE via a thread-scoped uselocale, so newlines print as \012 instead of splitting tokens; zombies print <defunct>. - Other users' processes: unreadable argv is marked commandIncomplete. Only -p selections and processes on a terminal under the user's own readable process there (sudo in a pane) get a targeted `ps -p ... -o pid=,command=`, cached by (locale, pid, start time); root daemons and a pane's root login never spawn ps. - -t selects with KERN_PROC_TTY after ps's addelem_tty path resolution, so per-terminal reads skip argv for every other process (~0.3 ms vs 4 ms ps). - -p deduplicates; rows sort by terminal then pid as pscomp(); stat adds ps's < N X V L modifiers. The export is renamed readProcessTable so an older pty.node falls back to ps rather than misreading the new query. * test(terminal): diff the real libproc addon against /bin/ps on macOS (#31783) Spawns a pty holder whose argv carries a newline, tab, invalid UTF-8 and é, then compares Orca's ps invocations column by column: pid/ppid/pgid/tpgid/ lstart/tdev/tty exactly, commands for the user's own processes, and stat modulo running/idle (thread state needs setuid ps). Rows are compared only when two in-process reads around the ps run agree, so churn cannot flake it. * fix(terminal): never serve a native process table read begun before the request (#31783) A timed-out read stayed shareable, so a later descendant capture could join an earlier poll's abandoned worker and take its rows as fresh; a descendant that exited meanwhile could have its reused pid SIGTERMed. The addon now reports when its worker actually began reading (startedAtMs), and runPsInProcess refuses a shared read that began before its own request and reads again, so every consumer that signals from rows (descendant teardown, foreground-group signalling, identity probes) only sees rows scanned after its stamp. Regression: pty-descendant-termination-libproc. Also from review: - argv recovered with setuid ps is keyed by pid, start time to the microsecond, comm and executable path (exec changes the latter two) with a 10 min TTL and 512-entry bound. - Recovery covers a sudo use_pty tree: the unreadable chain must start on the terminal of the user's own readable process, so the root shell and agent on sudo's own pty qualify while root daemons, GUI helpers and a pane's root login (own pty, under a terminal-less or tty-inheriting host) do not. - Concurrent tty= readers wait out the single naming pass, and readers past the 8-read cap queue behind one, within their deadlines, instead of bursting /bin/ps at startup restore (24 panes polled at once: 0 ps). - darwin-terminal-naming-slot.ts is now darwin-terminal-naming-single-flight.ts. * test(terminal): require /bin/ps row order in the libproc parity test (#31783) Parsers that take the first matching row depend on ps's terminal-then-pid order, so compare the order of the rows both captures share, not just their contents. * test(terminal): pin libproc read start times instead of reading the wall clock (#31783) * fix(terminal): match native process table reads to requests by sequence, not wall clock (#31783) Freshness compared the worker's gettimeofday with Date.now(), so a clock step back let an abandoned read's rows through (wrong-pid SIGTERM) and a step forward could reject fresh reads until the deadline. Each read now takes a JS-side sequence number when created and is submitted a microtask later; a caller joins only a read not yet submitted or created after it registered, so callers in one turn share a worker and nothing submitted before a request serves it. The addon drops startedAtMs again. Deadlines, timeouts and the argv cache use performance.now(). Callers past the 8-read cap wait on one slot notifier and each joined read has one settle notifier; both unsubscribe on timeout, so a stalled pool retains no per-caller reactions. If the pool is still at the cap when an overflow caller's deadline passes, the reader is marked stalled for 30s and callers run ps, as before this branch. Recovered argv is trusted for 30s: exec can keep pid, start time, comm and executable (node -> node with another script) and no cheap field shows it, so a sudo pane costs one targeted ps per process per 30s. * fix(terminal): keep ps fallbacks off a stuck terminal naming pass (#31783) - captureDarwinProcessTable's canonical fallback, taken while the naming slot is busy, now runs /bin/ps directly instead of re-entering the in-process reader and waiting on that same slot (a held lstat timed out every strict capture without one subprocess fallback). - In-process tty= readers wait out a short pass (startup restore) but hand a pass older than 500ms to ps, which names terminals out of process. - The naming single flight keeps one settle notifier per pass; waiters sit in a Set and drop off on timeout, so timed-out callers leave no reactions on a stuck pass (was one per call). The notifier moves to deadline-settle-notifier.ts, shared with the native read scheduler. - Once the native read breaker has tripped, a caller finding the cap still held after the cool-down returns to ps at once instead of waiting out its timeout. - Descendant teardown and foreground-group signalling give the native read half their budget and fall back to a fresh ps on a native timeout, so a partly stalled pool cannot cost a kill. * fix(terminal): give signalling ps fallbacks their full baseline timeout (#31783) Splitting the budget in half shrank ps's window behind a stalled native read: a 750ms ps lost descendant SIGTERMs and an 80ms foreground ps lost group SIGWINCH. Descendant teardown and foreground-group signalling now try the native reader for min(150ms, timeout/4) (inProcessSliceMs), then give ps the whole original timeout; readProcessTableBeforeDeadline extends the production reader's outer deadline by that slice, so ps succeeds exactly when it did on main. The stale-read regression test now also asserts the fresh rows' real descendant is signalled. * fix(terminal): carry the in-process deadline allowance through every descendant reader (#31783) readProcessTableBeforeDeadline granted the native slice only when the reader was readProcessTable by identity, so the daemon shutdown sweep's sharing wrapper kept a bare 250ms deadline: a stalled native read plus a 200ms ps missed it on every retry and descendants survived shutdown. ProcessTableReader now declares deadlineAllowanceMs; the fresh reader sets it (0 without the addon or with a stalled pool, so Linux and ps-only hosts keep their exact deadlines), createProcessTableSnapshotReader and the shutdown wrapper inherit it, and injected readers default to none. The fresh reader moves to pty-descendant-fresh-table.ts. The real-ps parity test gets a 30s per-case timeout, since whole-table /bin/ps runs slow under suite load. * chore(terminal): move the descendant reader's child_process baseline entry with it (#31783) * test(terminal): pin the libproc test locale so lstart doesn't follow the runner's LANG (#31783) |
||
|
|
917ef9c737 | feat(browser): install Chrome extensions from the Web Store into Orca's browser (#30940) | ||
|
|
fcf5e7b418 |
fix(native): pin node-pty cfset*speed with legacy baud codes for glibc 2.42+ (#33803)
* fix(native): pin node-pty cfset*speed with legacy baud codes for glibc 2.42+ glibc 2.42 versioned cfsetispeed/cfsetospeed (GLIBC_2.42) to take literal rates and redefined B38400 as 38400U, so node-pty built on 2.42+ fails the Ubuntu 20.04 floor gate. Pin both to the compat version alongside the existing openpty/forkpty/pthread_sigmask pins, and pass the legacy CBAUD code (__B38400 on 2.42+ headers, B38400 otherwise): the compat setters reject literal rates with EINVAL, which left every PTY at speed 0. Fixes #19420. Supersedes #19421. Co-authored-by: MK <mkjung.github@gmail.com> * fix(native): fail the build if pinned cfset*speed would get a non-legacy baud code A 2.42+ header set without __B38400 would otherwise silently pass 38400U to the compat setters and open every terminal at B0. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: MK <mkjung.github@gmail.com> |
||
|
|
f3104c80bf |
fix(daemon): a Windows terminal's output-relay write error no longer kills the PTY daemon (#33391)
* fix(daemon): contain ConPTY output-relay write errors to the one terminal instead of killing the daemon * fix(daemon): end a Windows terminal whose output relay dies before connecting * fix(daemon): kill a Windows terminal whose output relay fails before its first output |
||
|
|
df0c44571b |
fix(build): shared native packages get one immutable build instead of in-place rebuilds (#30226)
* refactor(build): extract the lock-free shared tree publish from the Electron dist cache Native builds in package dirs shared across worktrees need the same stage, validate, rename-into-place publish; one implementation keeps the never-replace-a-good-entry and quarantine-restore rules in one place. * fix(build): never rebuild a native addon in a package dir other worktrees load Under pnpm's global virtual store node_modules/node-pty is one store slot shared by every worktree, so an in-place node-gyp or @electron/rebuild run in one worktree replaced the pty.node another was running. A shared package dir now gets an immutable per-ABI build/orca-napi-<platform>-<arch> compiled in a scratch copy, load-checked in a fresh process, and published atomically; a usable entry is reused, never rebuilt. The patched node-pty and windows-process-tree loaders try that dir before build/Release, and packaging renames it to build/Release so packaged layouts are unchanged. Private (non-GVS, CI) installs keep the existing in-place path. * fix(build): find shared native packages through the project's own node_modules link require.resolve with paths fell back to another install under Bun, so a test fixture's private node-pty was judged shared. * ci: run native checks when the shared native build modules change * fix(build): pin the shared native slot and keep sibling publishes intact Resolve the package's physical dir once so a concurrent relink cannot publish one slot's build into another, verify the quarantined tree is the one judged unusable before replacing it, and keep in-flight staging/quarantine trees out of the packaged resource copy. * fix(build): never move a sibling's fresh native entry, and refuse off-platform builds Re-check the entry's identity right before quarantining it, and fail instead of publishing a host-OS binary under another platform's entry name. |
||
|
|
d327cc9418 |
feat(daemon): opt-in OS ceiling on terminal processes, reported per host (#27769)
* feat(daemon): opt-in OS ceiling on terminal processes, reported on or off per host * fix(daemon): verify the ceiling is enforced, reap its slice with the daemon, and keep terminals starting when systemd-run is unusable * fix(daemon): stop the ceiling slice before the daemon scope, and report a dropped slice as lost * test(daemon): expect the pid record's processCeiling field --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
da6d632a91 | [CI] Revert library patches introduced by test optimization (#26727) | ||
|
|
59b9bdeb90 |
Reduce repeated test work and fix Windows cleanup and IME confirmation (#26608)
* ci: share test transforms and remove six redundant checks * test: remove obsolete project copies and two build string checks * test: use the injected clock for exact launch readiness budgets * fix: retain IME confirmation and retire disconnected Windows PTYs * ci: include relay installer in existing Windows boundaries * perf: import the canonical SSH manager for filesystem operations * test: remove 42 source-spelling checks * test: advance deadlines after real entry and await actual PTY exit * Preserve native Windows exit acknowledgments and skip daemon-only smoke triggers |
||
|
|
4c38cabb4e |
fix(windows): synchronize native terminal table lifetime (#23257)
* fix(i18n): restore diff note draft catalog entries * fix(windows): synchronize native terminal table lifetime * fix(ci): preserve focused Playwright file selection * test(windows): retain PTY stress failure evidence * test: isolate hourly identity inputs from release version changes * test(ci): require focused commands on every platform Assert each golden platform step and its focused arguments before deduplicating Playwright discovery probes. --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
b87a6c0f23 |
fix(pty): pace the EAGAIN write retry so a stalled reader can't saturate the daemon thread (#15319)
node-pty's CustomWriteStream retries an EAGAIN write with setImmediate, which re-attempts within microseconds. A pty whose child has stopped draining stdin keeps that branch EAGAIN-ing, so the retry becomes a busy-loop on the thread that owns every pty on the runtime. Measured against this commit's parent on macOS arm64: 121,316 EAGAIN/s at 101.6% CPU, versus 805/s at 4.1% with the retry paced to 1ms. The delay is 1ms rather than longer because the cost lands on readers that drain in bursts -- what an agent does between event-loop ticks. Delivering 2MB to a reader that drains 20ms out of every 100ms: 689ms unpaced, 907ms at 1ms, 1414ms at 5ms. 1ms keeps essentially all of the CPU saving without the delivery regression. clearImmediate -> clearTimeout in dispose() is required, not cosmetic: once the handle is a Timeout, clearImmediate does not cancel it and a pending retry can fire after dispose. The disposal guards that make that harmless (_fd = -1, queue drop) are already on main; this mirrors them into src/unixTerminal.ts so the TypeScript twin no longer drifts from the compiled lib. Scope: this fixes the CPU saturation. It does not stop other terminals from being serviced -- a second live pty kept answering echo round-trips throughout the storm in every configuration tested (1 and 8 stalled writers, macOS and Linux, 8 CPUs and 1), with throughput down ~20-50% rather than hung. The "every terminal froze" symptom in #11178 has another cause and that issue stays open. Upstream chose setImmediate deliberately (microsoft/node-pty#831, #833) to fix large-paste latency, and rejected polling POLLOUT because it reports writable rather than flushed. That reasoning targets a per-write delay in an interactive terminal; this delays only the EAGAIN branch in a long-lived daemon. Pastes to a draining reader are unaffected (0-3 EAGAINs per MB in every arm). Verified: patch applies to a pristine node-pty@1.1.0 tarball, the patched src/unixTerminal.ts compiles byte-identical to the patched lib/unixTerminal.js, patch_hash matches the file, and on Windows the changed code never executes (WindowsTerminal, 0 EAGAINs on a 300KB conpty write). |
||
|
|
314506003a |
fix: retain MSYS shell descendants in their terminal job (#19068)
* fix: retain MSYS shell descendants in their terminal job * test: complete MSYS regression CI registration and teardown contract * fix(windows): deny job breakaway for the whole Cygwin/MSYS shell family The per-PTY job probed only msys-2.0.dll, and only for bash.exe/sh.exe. Cygwin ships the same spawn.cc breakaway logic under cygwin1.dll, and an MSYS2 zsh escapes exactly like its bash does, so both kept the orphan bug. Probe the runtime DLL on the shell's own search path instead of matching shell names: that is the property that decides whether the runtime will ask for CREATE_BREAKAWAY_FROM_JOB, and it drops the name special-casing. * chore(patch): restore the conpty.cc index line The earlier hand-edit dropped it while every sibling section kept one. Recomputed against the real blobs: applying this patch to 7b286d3d yields exactly 4b06d185, so git apply -3 has its fallback back. |
||
|
|
f7e3af254a |
fix(pty): close the pseudoconsole and dispose the conout worker on Windows self-exit (F24) (#18635)
* fix(pty): close the pseudoconsole when a Windows shell exits by itself
`ClosePseudoConsole` is the only thing that reaps a ConPTY's console host.
node-pty calls it from one place, `PtyKill`, which starts by looking the baton
up by id -- and the exit watcher in `SetupExitCallback` erased that baton the
moment the shell died. So on the self-exit path (typing `exit`, how panes
usually close) the lookup missed, `PtyKill` did nothing at all, and the
pseudoconsole was never closed.
The baton now survives until BOTH the shell has exited and `kill()` has run;
whichever arrives second frees it. `PtyKill` copies `hpc` out under the lock and
closes it afterwards, guards `TerminateProcess` on a shell handle the watcher
may already have closed, and duplicates that handle rather than reordering, so
upstream's close-then-terminate sequence is unchanged.
Measured on Windows 11, 20 self-exit cycles driven exactly as Orca drives them
(`onExit -> destroy()`), handles bucketed by NT object type:
relay spawn (no useConptyDll) 225 -> 285 (+1 Process +2 File/term)
after 219 -> 219 FLAT
desktop spawn (useConptyDll) 239 -> 439 (+1 Process +2 Thread +5 File/term)
after 235 -> 395 (+2 Thread +4 File/term)
The desktop residue is a separate defect in the `useConptyDll` branch of
`WindowsPtyAgent.kill()`, which disposes the conout worker only from an
`_outSocket.on('data')` handler -- and no data arrives after the shell has gone.
Fixing that line as well takes the desktop to 222 -> 222 FLAT, but it lives in
the `kill()` hunk owned by F23, so it is left to that change.
Refs F24.
* fix(pty): dispose the conout worker when a Windows shell exits by itself
Second, independent defect on the same self-exit path, and the larger half of
the desktop's leak. The `useConptyDll` branch of `WindowsPtyAgent.kill()`
disposed the conout worker only from an `_outSocket.on('data')` handler -- and
once the shell has gone no more data ever arrives, so the worker was never
disposed. The non-DLL branch three lines above already disposed unconditionally,
which is why only the desktop (the only spawner that sets `useConptyDll`) hit it.
Measured on Windows 11, 20 cycles, handles bucketed by NT object type, totals:
self-exit, relay spawn 225 -> 285 now 219 -> 219 FLAT
self-exit, desktop spawn 239 -> 439 now 222 -> 222 FLAT
explicit kill, relay spawn 225 -> 285 now 219 -> 219 FLAT
explicit kill, desktop spawn 235 -> 395 now 219 -> 219 FLAT
Neither fix alone is enough on the desktop: the pseudoconsole close is worth
+1 Process +1 File per terminal, this dispose +2 Thread +4 File.
The relay asset (config/relay-assets/node-pty-1.1.0-windows-pty-teardown-patch.cjs)
deliberately gets no counterpart: the relay takes the non-DLL branch, where the
dispose is already unconditional. Its reconstruction table needs the new hunk
though, or un-applying the desktop hunks no longer yields published node-pty.
Taken over from F23 at win-relay-qa's request after they verified that the
desktop never executes the non-DLL branch F23 was scoped around.
Refs F24.
* fix(pty): harden PtyKill against a failed handle duplication and a missing DLL
Both from review of #18635.
DuplicateHandle's result was dropped. On the live explicit-kill path a failed
duplication left hShellDup null, which the guard below could not tell apart from
the self-exit case, so TerminateProcess was skipped and the shell kept running
after its pane closed -- a worse outcome than the handle leak this patch exists
to fix. The failure now terminates through handle->hShell under the lock, where
it is valid and where TerminateProcess does not block. The only cost is that the
rare path kills before the console closes instead of after.
LoadConptyDll is now resolved BEFORE any baton state is touched, matching what
PtyConnect already does for the same reason. It throws when conpty.dll is
missing, and a throw after consoleClosed was set would strand the pseudoconsole
permanently: the retry finds the work claimed and does nothing.
Also corrects three comments the earlier commits made stale:
- the ptyJobMutex note still said PtyKill reads the table unlocked
- PtyListJobProcessIds said the baton is gone once the shell exits; it now
outlives the shell, and the nulled hJob is what makes the answer null
- windows-pty-job.ts said node-pty drops its handle record on exit
Re-measured on Windows 11 with the rebuilt binary, 20 cycles, all four paths
still flat: self-exit relay 219->219, self-exit desktop 222->222, explicit-kill
relay 219->219, explicit-kill desktop 219->219. Both explicit-kill runs report
22/22 shells exited, so the kill still lands.
Refs F24.
|
||
|
|
39330c5aca |
fix(relay): retire PTYs the host proves are gone, and stop two per-poll scan storms (#17832)
* fix(relay): stop three CPU growth terms in a long-running remote session pty.resize gated only on `managed.disposed`, which is bookkeeping rather than liveness. A shell that exits without node-pty's `onExit` leaves an undisposed entry holding a closed master fd, and UnixTerminal.resize has no fd guard, so the ioctl threw `ioctl(2) failed, EBADF` into the dispatcher's generic parse-error catch. Nothing retired the entry, so it stayed advertised and kept activePtyCount above zero -- which is what stops a relay with an unlimited grace from reaching its idle-no-ptys exit (#12423). Probe liveness with the same helper attach/listProcesses use, retire a provably dead pid, and contain an ioctl failure over a live-or-unverifiable process. processHasChildren forked `pgrep -P` per pane per inspection poll, uncached. procps-ng opens six procfs files per process to resolve one ppid, so each call cost O(host process count). Answer from the TTL-cached `ps` table the same RPC already captured for the foreground lookup (#13537). The remote AI Vault scanner had no parse cache at all, so every forced rescan re-read and re-parsed the whole transcript corpus, including files untouched for a month. Give it the mtime+size keyed memo the local scanner has (#13753). * fix(pty): invalidate the descriptor when node-pty gives up the handle (#17930) Carried forward from PR #17930, which merged into this branch. Rebased onto current main; main's newer node-pty-fd-leak test is kept as-is. * fix(ai-vault): refresh codex titles on the remote parse-cache reuse path The remote cache keys on the transcript's (mtime, size, host), but codex titles live in $CODEX_HOME/session_index.jsonl and are written after the rollout — so a cache hit froze the fallback title forever. Mirrors the local scanner's existing reuse-path refresh via a shared core. * fix(relay): publish the exit a reap performs, and rescan for close decisions Two review findings on the CPU work. reapExitedPty told only the relay-internal exit listener, so a retirement left the client's pane mounted against a session the relay had already forgotten -- the next attach answered `PTY "<id>" not found` with nothing before it to explain why. Pre-existing on three probe paths; resize made it user-triggered. Publish the same pending-exit the natural onExit path publishes, carrying -1 ("gone, status unrecoverable"), and skip it when onExit already reported the real code. processHasChildren now answers from a 500ms TTL-cached table. That is right for pty.inspectProcess, which every tracked pane polls, but pty.hasChildProcesses gates the window-close confirmation and workspace cleanup's idle evidence -- one destructive decision per answer, where a child started inside the window would be killed unasked. Give that RPC a fresh scan; pgrep used to. * fix(relay): publish a reap's exit only on proven-exited evidence The publication is a verdict the client acts on by retiring the pane, so it must not be reachable from the disposed-record sweep, which retires off our own bookkeeping rather than the host's process table. Only ESRCH earns it. * fix(i18n): restore the activity-options key the rebase dropped * fix(i18n): union en.json with main so the rebase cannot drop keys |
||
|
|
8197268956 |
fix(pty,remote): close the pty master fd leak, and two remote-terminal defects (#17914)
* fix(pty,remote): close the pty master fd leak and two remote-terminal defects so on Linux every later child of the process -- both later pty children and plain child_process spawns -- inherits it and keeps the /dev/pts device alive. Measured on Linux with stock node-pty 1.1.0: master fd flags 0404002 (cloexec=false), and 17 -> /dev/pts/ptmx present in both a later pty child's /proc/self/fd and a later child_process child's. Extend the existing node-pty patch with pty_cloexec() on both PtyFork spawn paths; after the patch the flags read 02404002 (cloexec=true) and neither child sees the master. This covers the app and terminal daemon only -- the SSH relay installs node-pty from npm on the remote host, so it stays exposed (see the report). rejecting inspection as a renderer-global unhandledrejection, which an unreachable runtime produced on every cadence tick. path cleared the close intent for it exactly like a dropped connection, so a host that keeps republishing the dead surface re-materialized the pane the user just closed. Keep that intent and drop its TTL. Also route the banner's "Remote terminal was closed." line through translate() so it stops mixing English into a localized banner. * test(pty,remote): make the fd-leak evidence positive and size the close intent to its RPC The Linux 'does not hand an earlier pty master to a later pty child' case only asserted that ptmx was absent from the captured listing, so any run that produced no listing passed without inspecting a single fd. Block the child on stdin, emit a sentinel, and assert both the sentinel and a real /dev/pts fd row before the negative assertion. Verified in node:24-bookworm: passes with the patch, and with pty_cloexec() reverted it fails on four inherited /dev/pts/ptmx rows. The close intent's TTL was a 10s literal while the close RPC that can still answer tab_not_found had its own 15s literal. A host that answered slowly while republishing the surface had its intent evicted by the republish path's own pending-check, so makeWebSessionCloseIntentDurable found nothing to flip and #9194 reproduced. Derive the TTL from the shared session.tabs RPC timeout so the two cannot cross, with an invariant test and a regression test for the slow answer. |
||
|
|
fbe94ceff6 |
fix: close readiness gaps found by merged-change audit (#17159)
* fix(ssh): fence stale kills and retired pane replay * fix(ssh): support cancellable interactive authentication * fix(ssh): await remote catalog before snapshot adoption * fix(pty): contain Windows ConPTY input failures * fix(power): avoid redundant macOS display blocking * perf(editor): narrow markdown override subscriptions * fix(quick-open): close directory handles after reads * refactor(linux): remove unused proc socket scanner * fix(usage): apply flat Sonnet 4.6 pricing * ci: prime Node next native test cache * docs(skills): resolve snapshot cleanup data path * fix(ssh): recover install locks after host reboot * test(ssh): recognize boot-aware install locks * test(ssh): prove previous-boot lock recovery live * test(wire): pin pre-metadata release coverage * fix(terminal): preserve remote tab ownership through recovery races * test(runtime): fence replaced terminal handles in agent guard * fix(ssh): preserve remote snapshot authority across polls * fix(pty): contain late ConPTY output EPIPE * test(pty): register Windows exit watcher before kill * fix: close SSH and tab readiness race gaps * fix(tabs): retain headless order and placeholder titles * fix(build): avoid parallel electron-vite config race * test(windows): avoid MSYS temp path rewriting * test(windows): avoid killing exited PTY * fix(pty): avoid late ConPTY input teardown race * fix(terminal): sync reconnect error ownership after commit * fix(runtime): use canonical worktree identity comparison * test(ssh): assert complete cold-hydration baseline * test(windows): invoke quoted retention fixture via PowerShell * test(windows): read ConPTY grid through mode con * fix(terminal): publish PTY replacements atomically * fix(terminal): infer stale identity on reattach * fix(terminal): fence stale pane PTY callbacks * fix(terminal): fence stale pane binds after rebind * fix(terminal): reject stale pane transport callbacks * fix(terminal): fence mirrored reattach spawn callbacks * fix(terminal): replace stale pane PTYs on remount * fix(ci): size the Windows launcher-compile test budget from measurement `native-smoke (windows-latest)` fails ~4.5% of runs on `preserves a multiline argument through the compiled remote launcher` with "Test timed out in 15000ms" — on unrelated PRs, for reasons that have nothing to do with them. Across 176 sampled attempts it is the only red that job produced, and it hit seven different PRs in two days: #16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085. The test is six process creations: powershell.exe forks csc.exe, then the freshly compiled orca.exe forks node.exe, twice. Hosted Windows runners periodically slow process creation down, and this test amplifies that far harder than anything else in the job. Comparing the 80 attempts where it ran under 3s against the 12 where it ran over 12s, its own median goes 2198ms -> 15917ms (7.2x) while the same file's powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash process tests in the neighbouring file move 1.4x, and the other 35 files put together move 1.5x. Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms, correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%) exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s testTimeout, so deleting the override and inheriting the config is not enough on its own. 60s clears all 176 with 1.7x headroom on the worst. This is slow, not hung. Every body here is synchronous spawnSync, so Vitest cannot interrupt one — the timer fires only after the body returns and the reported duration is real elapsed time. That is why a failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The work finished; the stopwatch was short. Seven reruns at one identical head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the last of those would have been red on code that had not changed. The 15s came from #8897, which raised this test off Vitest's built-in 5s default because the job then ran bare `pnpm vitest run`. #8909 landed 3h27m later and pointed the job at config/vitest.config.ts, which is the real fix for that. The constant stayed behind and has been the binding budget ever since. * fix(terminal): fence stale remount reattach ownership * fix(terminal): reconcile mounted pane identity after replacement * fix(terminal): fence stale reattach fallback ownership * fix(terminal): fence deferred SSH reattach ownership * fix(terminal): fence stale split pane ownership callbacks * fix(terminal): keep stale spawns from consuming startup --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
8dd7d6060c | fix(release): stabilize native builds across CI platforms (#16947) | ||
|
|
2d500278b4 |
build(windows): refuse unpatched node-pty prebuilds
Merged after clean CI, Windows packaging verification, and readiness review. |
||
|
|
2b1254d681 |
fix(windows): own PTY process trees with job objects (#15755)
* fix(windows): own PTY process trees with job objects Teardown used to answer 'is this tree mine, and how do I kill it?' by scraping the process table, walking parent pids back to Orca, and running taskkill /T /F only if the walk said yes. Every step is a guess, and the code said so itself: windows-pty-root-identity.ts:35 already named the fix -- 'an inherited handle / Job Object'. The guesses fail in the ways users report. A pid walk cannot survive pid reuse, so teardown refused whenever it could not prove ownership, and a refused kill is an orphaned agent tree holding the worktree directory open (#9045, #10475, #10087). A descendant that reparented is invisible to the walk. The scrape itself could be blocked by policy, which read as 'no evidence'. node-pty now creates a job object per ConPTY and assigns the shell under CREATE_SUSPENDED, before it can spawn anything -- assigning afterwards leaves a window in which a fast child escapes. Termination is one TerminateJobObject; liveness is QueryInformationJobObject. Verified on Windows 11 against a shell whose grandchild was spawned detached: job membership came back [shell, grandchild] and one call killed both. Neither a parent-pid walk nor GetConsoleProcessList sees that grandchild -- it leaves the console and reparents, which is exactly the claude.exe/node.exe/cmd.exe orphan in #9045. KILL_ON_JOB_CLOSE means a daemon that dies without unwinding no longer strands shells (#9195, #10415). The job is the daemon's, not the app's, so an app-main crash still leaves sessions alive -- the guarantee win-crash-survival-e2e asserts. Both entry points report unavailable rather than a false success when a pty has no job: an outer job without BREAKAWAY_OK can refuse the assignment, and a pty from an older build has none. Reading 'we could not tell' as 'already dead' is the original bug, so the old probe stays as the fallback. * test(windows): pin job ownership against a real detached grandchild The unit tests pin the contract; this pins what the contract is for. A grandchild spawned detached leaves the pane's console and reparents, so GetConsoleProcessList and a parent-pid walk both miss it -- that is the process that outlived its pane and held the worktree directory open. Includes a guard that this build actually has job support, so a node-pty rebuilt from unpatched sources fails loudly instead of letting every assertion pass vacuously. * fix(windows): correct the job liveness contract to what Windows actually does I claimed an emptied tree would report [] and that this was the evidence a stale registry entry lacks (#15549). Running it on Windows 11 showed otherwise: node-pty drops its handle record and closes the job when the shell exits, so a dead tree reports null. Null therefore means unverifiable in the sense of docs/reference/ssh-execution-boundary.md -- no job support, not a ConPTY, or no longer tracked -- and is never evidence that processes died. A caller reading it as proof of death would have been right by accident after a normal exit and wrong on a host that refused the assignment. What the API does add is descendant liveness for a tree that is still tracked, including children that detached from the console. * fix(windows): stop a clean shell exit from reaping backgrounded processes Measured on Windows 11: with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE on the per-PTY job, releasing the handle when the shell exits also killed whatever the user had backgrounded. Typing 'exit' in a pane reaped a detached server that survived before this patch. That is a behaviour change nobody asked for. The approved change was that killing the terminal daemon reaps its shells -- not that a clean exit reaps your background job. The job's purpose is to make an EXPLICIT teardown exact, which TerminateJobObject still does. Reaping a dead daemon's shells now needs the daemon-level job the design called for: the daemon assigns itself, children inherit membership, and its closure on daemon death reaps them without touching clean-exit semantics. Not in this PR; noted in the reference doc. * test(windows): pin that a clean exit leaves backgrounded work alone The counterpart to the tree-kill test. Without it, re-adding JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE would look like a tightening rather than the regression it is. * fix(windows): stop a winpty pty id from matching a ConPTY job winpty.cc and conpty.cc each mint their 'pty' id from an independent counter, and windowsPtyAgent stores both in the same _pty field. So a winpty-backed terminal's id can collide with a live ConPTY baton -- and closing that pane would have terminated an unrelated pane's entire process tree. Both job entry points now take the shell pid and the native side refuses unless GetProcessId(hShell) matches, which makes the id unforgeable. Two more from the same read-through: - ResumeThread's failure was ignored. A shell left suspended is a pane that never prints and never exits, which is far harder to diagnose than a failed spawn; it now cleans up and throws. - handle->hJob was assigned before LoadConptyDll, which can throw. A baton carrying a job but never reaching SetupExitCallback has nothing left to close it, so the assignment moved down beside hShell. * docs(windows): record the unsynchronised node-pty baton table Pre-existing upstream -- the exit thread erases while the main thread reads -- but terminatePtyJob adds an instance of it, so it belongs in writing rather than in someone's head. * fix(windows): close four gaps found in review BREAKAWAY. The per-PTY job set no limits, so a child asking for CREATE_BREAKAWAY_FROM_JOB was refused with ERROR_ACCESS_DENIED. Installers, msiexec and some updater and service-control paths spawn that way deliberately -- they worked before this patch and would have failed only inside an Orca terminal, which is the worst shape a bug report can take. JOB_OBJECT_LIMIT_BREAKAWAY_OK restores it; a child still has to ask, so ordinary descendants stay owned. EMPTY IS NOT UNAVAILABLE. The native reader returns an empty list -- not an error -- when CreateToolhelp32Snapshot fails, which is what an EDR hook or a restricted token produces. Callers read that as 'nothing is running' and teardown concludes a live PTY root is already gone. The snapshot must contain the querying process; nothing else is unfalsifiable, and one predicate catches empty, truncated and permission-filtered tables alike. NO DEADLINE. Replacing execFile dropped its 3s timeout. The vendored reader latches a module-global while a request is in flight and clears it only after draining its callbacks, with no try/catch -- so one wedge leaves every later call queued behind a promise that never settles, and the process table is dead for the life of the app. The bound is back. GUESSED IMAGE PATH. executablePath was derived from the first space-delimited token, which reads 'C:\Program' out of an unquoted 'C:\Program Files\nodejs\node.exe ...'. Wrong evidence is worse than none, and the only consumer already had the full path in , so the field is gone rather than repaired. Also: remove_pty_baton no longer sits inside assert(), which NDEBUG would compile away along with the call, and the job accessors hold a lock across lookup and use -- handle values are recycled, so an unguarded read could pass the shell-pid check against an unrelated process and terminate the wrong job. * fix(windows): apply the job lock once per accessor The patch script matched a string its own replacement still contained, so PtyTerminateJob got two lock_guards named guard and PtyListJobProcessIds got none. MSVC caught it: error C2374 redefinition. * test(windows): pin that a child can still break away from the job Verified on Windows 11: 'start /b' writes its marker and no access-denied appears. Without JOB_OBJECT_LIMIT_BREAKAWAY_OK this fails, and it fails only inside an Orca terminal -- so the failure would look like Orca corrupting unrelated software rather than like a job-object change. * fix(windows): stop the ownership guard from reading a closing handle The guard called GetProcessId(hShell) to prove identity, but the exit watcher closes hShell on another thread -- so the guard could read a closed handle, and under strict handle checks that is fatal rather than merely wrong. Worse, it widened the gap between validating hJob and using it from two instructions to a kernel round-trip, and handle values recycle: the likeliest occupant of a freshly recycled value in this process is another pane's job. The pid never needed a handle. It is captured at spawn and compared as a DWORD, so the guard touches no handle at all, and hShell is now closed inside the same lock as hJob. Also from review: - reject CR/LF in a cmd argument. cmd ends the command at a raw line break whatever the quote state, so there is no escape for it; encoding one anyway truncates the argument and can leave the remainder to run as a command. Agent prompts are this encoder's motivating input. - ask the process table only for the fields a caller needs. Memory and CommandLine each cost an OpenProcess per process, inline, for every process on the box -- and the 1024 bound is patched out. Ancestry reads now skip both. - corpus gains the degenerate quote-only and two-quote arguments. - PtyListJobProcessIds' docblock still taught the empty-list contract that was corrected on the TS side, and now records that the ConPTY console host is never a job member. - drop a write to NumberOfAssignedProcesses, which is output-only. - pty_baton::hShell is initialised; ownsShell was only safe because && short-circuited ahead of it. The backgrounded-child test is rescoped: 'start /b' uses CREATE_NEW_CONSOLE, not CREATE_BREAKAWAY_FROM_JOB, so it proves job membership does not block backgrounding -- not that BREAKAWAY_OK works. That flag rests on the Win32 contract, and I have said so rather than letting the test imply coverage it does not have. * fix(windows): bound retries after the process table wedges The 3s deadline stops a caller hanging, but the timed-out call leaves its callback in the vendored module's queue -- and that queue drains only when the latched request completes, which in this wedge never happens. Retrying at the caller's poll rate would add a closure per tick forever. A 30s cooldown bounds it to one probe, and a late callback clears the cooldown because it proves the reader recovered. Also pins the deadlock invariant in the patch: the exit thread's lock must close before tsfn.BlockingCall, because that waits on the JS thread and the JS thread can be waiting on the same mutex inside PtyTerminateJob. Correct today by scoping; a comment so a later refactor does not widen it. * revert(windows): drop the field-selection API, which cannot pay off I added it for a real perf finding -- Memory and CommandLine each cost an OpenProcess per process -- and then never wired a caller, so the claim that ancestry reads skip them was wrong. Wiring it would have been worse than leaving it dead. The only ancestry consumer is the teardown identity probe, which needs a snapshot that started AFTER it asked, for pid-recycle detection. Bypassing the shared reader to get narrow fields would let that request join a scan already in flight -- trading a correctness guarantee for milliseconds. Field selection only pays off if callers can ask for less, and they cannot: one shared snapshot serves every caller so a 32-wide teardown collapses into a single scan, which means it has to carry every field. The reasoning now lives next to the flags instead of in a dead export. * fix(process): three P1s from review — a crash vector and two wedge bugs STDIN EPIPE COULD TAKE DOWN THE MAIN PROCESS. A child that exits without reading makes the queued write fail with EPIPE, and an unhandled error on a stream is an uncaught exception. The child's own error listener does not cover its stdin stream, so runProcess({ input }) against a short-lived child was a crash, not a failed call. THE COOLDOWN LEAKED A BATCH PER CYCLE INSTEAD OF BOUNDING IT. At expiry every concurrent caller passed the check before any of them re-armed it, so each enqueued a callback into the still-latched native queue and each cycle leaked another batch. The cooldown is now re-armed BEFORE probing, so exactly one caller gets through. A SYNCHRONOUS THROW LEFT ITS DEADLINE RUNNING. The timer was declared inside the try, so catch could not clear it; it fired later and wedged a reader that had already recovered. Hoisted and cleared, and wedge state now carries a generation so a request that lost its deadline cannot mutate it on behalf of the one that replaced it. Found by review once the prompts were short enough for the reviewer to finish -- the previous two rounds died on prompt length. * fix(process): stop a stream error from crashing the main process Same class as the stdin EPIPE finding, two instances further on: stdout and stderr had data listeners and no error listeners, and an unhandled error on a stream is an uncaught exception. Scoped to runProcess, which owns the child outright. spawnProcess hands the streams to its caller, and a blanket handler there defeats callers that track and remove their own listeners -- the SSH ProxyCommand transport does exactly that, and its cleanup test caught the attempt. Documented on spawnProcess so the boundary is explicit rather than inferred. * fix(windows): validate the ConPTY DLL before creating the process LoadConptyDll throws when conpty.dll is missing -- a real state, and one this branch hit during development. It ran after CreateProcessW and ResumeThread but before the baton and the exit watcher were installed, so a throw leaked the job, process and thread handles and left an untracked shell tree running. Once per attempt, so a broken install accumulates orphan shells on every retry. Resolving the DLL first costs nothing and leaves exactly two throws after creation: the CreateProcessW failure, where nothing exists yet, and the resume failure, which already cleans up after itself. This also closes the same leak for hProcess and hThread, which predates the job work. * feat(windows): add the daemon-level job the design called for The plan specified two nested jobs and I built one. That gap is why dropping KILL_ON_JOB_CLOSE from the per-PTY job cost the approved guarantee that a dead daemon reaps its shells -- I had one job trying to answer two questions, and the two answers conflict. They are separate jobs. The per-PTY job answers 'kill exactly this pane's tree, now', and cannot be kill-on-close because its handle is released when the shell exits, which would reap whatever the user backgrounded. The daemon assigns itself to a second job that IS kill-on-close; its handle is released only when the daemon dies. Children inherit membership, so every pty is covered and the per-PTY jobs nest inside it. Daemon, never app: an app-main crash must still leave sessions alive, which win-crash-survival-e2e asserts. Both jobs carry BREAKAWAY_OK, or a child asking to break away is refused at whichever level lacks it. Restores #9195 and #10415, which I withdrew from this PR earlier. * docs(windows): record what the host job does not cover An app-hosted PTY gets a per-PTY job but no crash reaping, because the alternative is a kill-on-close job on the app -- which is precisely what the crash-survival guarantee forbids. * ci(windows): run the win32 suites in the PR windows job Both were skip-on-non-win32 and had only ever run on one machine I drive by hand -- which went unreachable at exactly the moment I needed to verify the percent-escaping fix. Verification that depends on one box is not verification. The job already builds node-pty from patched source and already runs a useConptyDll test, so the ConPTY runtime files are in place by this step. This also makes the encoder a gate: the corpus is the only thing standing between an agent prompt and a mangled argv, and it now runs against real cmd.exe on every PR. * fix(deps): refresh the lockfile for the current patch hashes pnpm records a hash per patched dependency, and I regenerated both patches repeatedly across the review rounds without refreshing the lockfile. Every local run used --frozen-lockfile's looser sibling, so nothing caught it until CI did: ERR_PNPM_LOCKFILE_CONFIG_MISMATCH Cannot proceed with the frozen installation. The current "patchedDependencies" configuration doesn't match the value found in the lockfile Verified with pnpm install --frozen-lockfile locally this time. * ci(windows): build node-pty from source before the win32 suites CI proved the encoder fix on real cmd.exe -- 26/26 -- and in the same run proved the job suite had been testing an unpatched binary. node-pty prefers its upstream prebuild, which does not contain this patch, so every job-object export was absent and isPtyJobOwnershipAvailable() was false. That guard is why the failure was loud rather than a vacuous pass, and it is the reason the assertion exists. Packaging was never affected: rebuild-native-deps.mjs already builds node-pty from source for Electron and restores the ConPTY runtime files. The gap was the node-runtime test environment only. Not changing requiresPatchedNodePtySourceBuild's win32 exemption here. Its premise -- that the patch is Unix-only -- is now false, but lifting it also needs pnpm rebuild to force a source build, and I cannot validate that on macOS and Linux from here. Recorded as a follow-up instead of changed blind. * test(windows): gate the host-job guarantee in CI The daemon-level job had one hand-run proof and no automated coverage -- the same shape of gap that let an unpatched node-pty go unnoticed until CI caught it. It needs a real second process, because the assertion is about what happens when that process is force-killed: a host in a kill-on-close job must strand neither its pty nor a grandchild spawned detached, which is the process a parent-pid walk cannot see. Runs in the Windows PR job alongside the per-pty and encoder suites, so both halves of the two-job design are now gated rather than asserted. * fix(windows): serialise host-job creation Two callers racing PtyAssignCurrentProcessToJob would each create a job, put the process in both, and leak the first handle -- and the handle is what keeps a kill-on-close job alive, so a leaked one is never released. 'Only JS calls it' is not a guarantee: a worker thread with its own N-API env shares these statics. Also records the ordering requirement it depends on. AssignProcessToJobObject adds only the named process; children inherit membership, but a pty that already exists does not join retroactively and would not be reaped. The daemon assigns at startup, before the ConPTY warmup and before any session, which is correct today and now stated rather than implied. * fix(daemon): keep the host job off the startup path Assigning the host job at daemon startup resolves the node-pty native module, which loads the ConPTY addon -- and paying that before the endpoint is published delayed readiness enough that daemon-boot-smoke failed on windows-latest, deterministically. windows-conpty-warmup already carries the comment for this exact hazard ('setImmediate keeps the ready/handshake path ahead of the warm-up') and I put an eager load in front of it anyway. Moved to the pty spawn path, which already pays ConPTY cost, and memoised. Children inherit job membership, so assigning immediately before the first spawn still covers every pty -- and nothing can spawn one before the endpoint exists. |
||
|
|
a0944cc129 |
fix(linux): restore Ubuntu 20.04 launch — pin node-pty glibc symbols + add glibc/libstdc++ packaging gate (#9902) (#10019)
* fix(linux): restore Ubuntu 20.04 launch by pinning node-pty glibc symbols (#9902) The bundled node-pty pty.node is compiled from source in release CI on ubuntu-latest (glibc 2.39). glibc's 2.32-2.34 libpthread/libutil merge relocated openpty/forkpty (GLIBC_2.34) and pthread_sigmask (GLIBC_2.32) into libc under new symbol versions, so the from-source build bound to versions absent on Ubuntu 20.04 (glibc 2.31). The main process imports node-pty at startup, so the app crashed on launch. pty.node is the sole blocker (Electron needs GLIBC_2.25; other native modules <= 2.17). - Patch node-pty: a .symver shim pins the 3 symbols to their pre-merge version (GLIBC_2.2.5 x64 / GLIBC_2.17 arm64), and Linux-only ldflags force libutil.so.1/libpthread.so.0 back into DT_NEEDED. Guarded to Linux; macOS/Windows untouched. - Add a packaging gate (verify-linux-glibc-floor.cjs, afterPack): reads each bundled native binary's objdump -p version needs and fails the Linux build if any strong GLIBC_/GLIBCXX_/CXXABI_ node exceeds stock Ubuntu 20.04 (glibc 2.31 / GLIBCXX_3.4.28 / CXXABI_1.3.12). Catches GLIBC_ABI_DT_RELR, rejects GLIBC_PRIVATE, skips weak needs, fail-closed. - Docs + tests; the lazy sherpa-onnx speech prebuilt (GLIBCXX_3.4.29, never loaded at launch) is a documented libstdc++-floor exemption. * fix(linux): assert DT_NEEDED provider deps in the glibc-floor gate Harden the packaging gate (flagged in adversarial re-eval): the version-floor check alone can false-pass if the patch's forced `-l:libutil.so.1` ever silently drops — the pinned openpty@GLIBC_2.2.5 still resolves from libc's compat alias at build time, but fails to load on Ubuntu 20.04 where openpty/forkpty live only in libutil. The gate now also asserts that any binary importing openpty/forkpty keeps libutil.so.1 in DT_NEEDED. Validated on a real symver-pinned .so with libutil dropped (now fails) vs. present (passes). Documents the recommended real-host smoke-test follow-up. |
||
|
|
6a4b89785c |
revert: back out the Windows terminal update-survival chain (#7421→#7499) (#7505)
* Revert "Preload the daemon windowsHide shim via --require; wrap promisify custom (#7499)" This reverts commit |
||
|
|
f0fdd3a716 |
Hide console windows for children of the node.exe-hosted daemon (#7486)
Since #7473 the terminal daemon runs under a standalone node.exe. Electron's bundled Node defaults windowsHide to true; plain node.exe defaults it to false, so every child_process call in the daemon that does not pass the flag - the periodic PowerShell CIM process probes, node-pty's kill-path conpty_console_list_agent fork - now allocates a visible console, which opens and closes a Windows Terminal window on the user's screen every few seconds. Fix: daemon-entry installs a child_process shim (first import, before any module captures bindings like promisify(execFile)) that defaults windowsHide: true across spawn/exec/execFile/fork and their sync variants, restoring the Electron default the daemon has always relied on. Explicit windowsHide from a caller still wins. Also adds windowsHide to node-pty's console-list agent fork in the existing patch as defense in depth. Verified on Windows: reproduced the flash with the rc.5 production daemon (WindowsTerminal windows, ~3s cadence matching the CIM probe interval, conhost spawned visible-capable "0x4"); with the shim, a node.exe-hosted daemon's children (OpenConsole, powershell, node helpers) all run without a visible-capable console and session kill still works end to end. |
||
|
|
509c41e2bf | Relocate node-pty ConPTY runtime outside the Windows install dir (fixes update-time terminal loss) (#7421) | ||
|
|
ff687a37b4 | Fix Windows ConPTY process-list fallback (#3009) | ||
|
|
3cf4f7e93d | Close node-pty spawn failure fds (#1597) | ||
|
|
bcbb7528bd | Close node-pty macOS slave fd (#1588) | ||
|
|
1fbe7d9dbf |
Improve node-pty spawn diagnostics (#1587)
* Improve node-pty spawn diagnostics * Preserve original error stack when adding node-pty recovery hint Mutate the existing Error's message instead of replacing the object so the original stack trace and custom fields survive into telemetry/logs. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
a638215729 |
fix(pty): release ptmx fd on natural exit + defuse SIGHUP-to-recycled-pid (#1327)
* fix(pty): release ptmx fd on natural exit + defuse SIGHUP-to-recycled-pid Daemons accumulated ptmx fds over time because node-pty's UnixTerminal only releases the master fd when destroy() runs. On the natural-exit path (the common case — user closes a tab, shell runs `exit`) nothing ever calls destroy(), so the fd leaks until GC. On macOS this eventually hits kern.tty.ptmx_max=511 and all new terminals fail to spawn. Fix: release the fd synchronously on every teardown path (natural exit, explicit kill, stale SSH spawn, daemon shutdown) and close the concurrent SIGHUP-to-recycled-pid hazard inside node-pty's UnixTerminal.destroy(). - src/main/daemon/pty-subprocess.ts: synchronous POSIX proc.kill neutralization inside proc.onExit; dead guards on forceKill/signal so they never target a reaped-and-possibly-recycled pid - src/main/daemon/session.ts: new disposeSubprocess() for already- exited sessions (fd release only, no SIGKILL) — avoids sending SIGKILL to a recycled pid during daemon shutdown - src/main/daemon/terminal-host.ts: dispose loop routes on isAlive — live sessions get forceKillAndDisposeSubprocess (SIGKILL + fd release), exited sessions get disposeSubprocess (fd release only) - src/main/providers/local-pty-provider.ts: same POSIX kill neutralization at top of onExit for the legacy local path - src/relay/pty-handler.ts: same neutralization in wireAndStore; disposed flag guards all public entry points; dispose() uses SIGKILL (not SIGTERM) before destroy since the relay is exiting; killTimer fallback + immediate-shutdown + stale-spawn cleanup all call disposeManagedPty + ptys.delete so wedged children (D-state, bad NFS) can't leak map entries against the 50-PTY cap Windows is exempt everywhere — WindowsTerminal.destroy IS a kill() call internally (closes the ConPTY agent), so neutralizing would turn destroy into a no-op and leak the agent. See docs/fix-pty-fd-leak.md for the full design. Co-authored-by: Orca <help@stably.ai> * fix(pty): patch node-pty native off-by-one leaking /dev/ptmx per spawn node-pty 1.1.0's pty_posix_spawn on macOS walks low_fds[0..2] in an allocation loop that breaks at the first fd >= STDERR_FILENO, then cleans up via `for (; count > 0; count--) close(low_fds[count])`. In the typical case (break at count=0) the cleanup body never runs and low_fds[0] — a /dev/ptmx handle — leaks per spawn. Fixed upstream in microsoft/node-pty af053f2 (PR #882), not in any 1.1.0 release. Backport the 3-line cleanup-loop fix as a pnpm patch. E2E validated against a dev daemon: 200 spawn/kill cycles kept the daemon's ptmx fd count flat at baseline; prior runs reproduced linear 1-per-spawn growth. Also documents the native root cause as a status addendum in docs/fix-pty-fd-leak.md — the JS-side destroy() discipline previously landed is still load-bearing for the SIGHUP-to-recycled-pid hazard and for synchronous fd release on daemon shutdown. Co-authored-by: Orca <help@stably.ai> * fix(pty): capture stable kill spy ref in pty.test.ts destroyPtyProcess reassigns proc.kill = () => {} on POSIX to defuse the SIGHUP-to-recycled-pid hazard (see docs/fix-pty-fd-leak.md). After that reassignment, proc.kill.mock is undefined and the assertions crashed in CI. Capture a stable reference to the vi.fn() before it gets reassigned. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
4318f3bfa7 |
chore: reduce root-directory clutter (#1275)
Co-authored-by: Orca <help@stably.ai> |