Commit Graph
120 Commits
Author SHA1 Message Date
NeilandFlamingoPg 6a68dac421 fix(terminal): bound WebGL atlas pages and release removed page textures (#39715)
Cap merged atlas pages at FORCED_MAX_TEXTURE_SIZE instead of the device
MAX_TEXTURE_SIZE (16384 on Apple Silicon), stop the link render layer
from acquiring the shared atlas with a mismatched 2048 texture size, and
reset GL texture slots of removed atlas pages to the 1x1 placeholder.
Mirrors xtermjs/xterm.js#6187, #6156 and #6306 in the addon-webgl source
patch and regenerates the runtime patch and lock hash.

Co-authored-by: FlamingoPg <1106310035@qq.com>
2026-11-07 21:30:43 -08:00
NeilandCopilot 0b22e904e3 perf(terminal): avoid reparsing xterm inline color styles (#39482)
Set RGB and contrast-adjusted colors through CSS properties instead of serializing and reparsing the complete inline style. Regenerate the pinned xterm runtime patches and lock hash without changing terminal scheduling or recovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-07 12:53:11 -08:00
OrcaWinandOrcaWin e97b290cb8 test: remove 835 low-value tests (#39558)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 10:13:20 -08:00
Neil a59d47b1eb fix(terminal): let default-matching cell backgrounds follow background opacity (#39372)
Explicit SGR backgrounds equal to the translucent default background, and
style-only cells (dim, italic, extended underline) in WebGL, painted an
opaque fill over the theme wallpaper. Treat them as the default background
in both the WebGL and DOM renderers; the buffer keeps the original color.

Closes #37671
2026-11-07 00:41:53 -08:00
Neil 9bd6272fdf refactor(tooling): organize build, CI, and test owners (#38130)
* refactor(tooling): organize test runtime and maintenance owners

* test(tooling): preserve relocated runtime fixture dependency edges

* refactor(tooling): preserve xterm routing owner checks

* ci(tooling): include routing owner in sparse checkout

* test(tooling): preserve copied workflow dependency closures

* fix(tooling): normalize hashed JSON assets in release parity

* fix(tooling): distinguish asset aliases from literal references

* fix(tooling): frame normalized asset reference payloads

* fix(tooling): normalize decoded JSON asset references

* refactor(tooling): organize build, CI and release owners

* fix(tooling): align moved generator and workflow path contracts

* fix(tooling): update generated plugin bundle owner banners

* test(tooling): create relocated mobile verifier fixture directories

* test(tooling): copy headless detector dependency closure

* test(tooling): align native batch policy fixtures with owner path

* fix(tooling): link moved reliability command to historical evidence

* fix(tooling): preserve nested sandbox owners and canonical guide source

* fix(tooling): target release version owners in checkout fixture
2026-11-04 21:08:19 -08:00
OrcaWinandOrcaWin 74df8bf42e test: remove 997 low-value test declarations (#38058)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-03 23:59:28 -08:00
OrcaWinandOrcaWin e98c6a439a test: remove 2816 low-value test declarations (#38030)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-03 21:45:29 -08:00
Neil 9c7fe9793e fix(deps): backport minimatch, glob, and deepmerge security fixes (#37981)
Backport upstream matching and abort fixes plus safe deepmerge property handling across the independent workspaces. Includes installed-package regression tests and audit-tool fixture repair. Agent-reviewed at 83ca381d519e6247d7c1b3f0a52ce2907a9f9a19; CI aggregate passed. Version-based scanner alerts remain open pending documented disposition or compatible upstream releases.
2026-11-03 17:13:11 -08:00
Neil f3c65cd556 fix(deps): update Electron to 43.8.6 and resolve open security advisories (#37916)
- Electron 43.8.4 -> 43.8.6: upstream Chromium/V8/ANGLE/PDFium/Skia/WebRTC
  security backports. Drops the now-unneeded minimumReleaseAge exclusion.
- @hono/node-server 2.1.0 -> 2.1.5 (GHSA-rmxm-3fg6-px4f), lockfile only.
- mobile node-forge 1.4.0 -> 1.4.2: fixes 10 advisories (5 high) and the
  previously patched GHSA-86w9-cpqp-85rv, so the local patch and its audit
  exception are removed. The regression test still passes on 1.4.2.
2026-11-03 13:52:12 -08:00
Neil b69963356d chore(terminal): bump xterm to 6.1.0-beta.336 and regenerate patches (#37266)
Port the xterm, webgl, search, serialize and image source patches to upstream
08829c7. Where upstream fixed the same bug (#6204 mouse NaN guard, #6226 warm-up
cancel on dispose, #6235/#6237 serialize width clamp) take upstream and keep only
Orca's remaining delta (domContainer warm-up gate, clipped wide-cell blanking).
Bump mobile's webgl subset patch alongside. Drop serialize fuzz seed normal:5,
which now round-trips.
2026-11-01 18:05:58 -08:00
Brennan Benson 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.
2026-11-01 15:32:17 -08:00
Brennan Benson 9eb930173e Keep rich Markdown in place when focus returns from Find (#36781)
* Preserve rich Markdown viewport through bare focus returns

* Retire viewport ownership on editor remount

* Complete no-op navigation and body Tab coverage

* Keep patch metadata keys separate from dependency snapshots

* Retire the viewport before root detach callbacks

* Give the engine writer fixture its native selection

* Keep native focus navigation current in Markdown

* fix: leave smooth Markdown scrolling to native focus cancellation

* Cover focus wrapper removal and routed Markdown writers

* Type the viewport scroll spy and mark the focus-recovery wait as deliberate

* Scope Markdown viewport wheel and touch guards to the editor scroller
2026-11-01 11:46:13 -08:00
Neil 14d29b8d24 feat(themes): background images for app themes (#36589)
An app theme may declare one validated raster image (png/jpeg/webp, <=2 MB, >=640x360, inside the plugin) served through orca-plugin-image, painted as one static body layer under translucent terminals. Older Orca ignores the field; a bad image drops only the image.
2026-10-31 19:45:04 -07:00
Neil 82ac952ad7 feat(editor): one typography resolver for every Monaco surface + opt-in weight, line height, ligatures, size and macOS smoothing (#36577)
* feat(editor): route editor typography through one resolver and add opt-in weight, line height, ligatures, size and macOS smoothing

Monaco sites built font options ad hoc and never received fontWeight,
fontLigatures or lineHeight; the editor size was tied to the terminal.
editor-typography.ts now feeds every Monaco and DOM code surface. New
optional settings default to today's rendering. A macOS-only Font
Smoothing setting rebuilds WebGL terminal renderers so the change
reaches their shared glyph atlases.

* fix: satisfy changed-code quality gates

* fix(terminal): cancel WebGL atlas warm-up on dispose so rebuilt renderers stop leaking canvases

The warm-up queue kept drawing into a disposed atlas, which re-appended its
scratch canvas to the terminal element. Every WebGL renderer rebuild (GPU
toggle, context-loss retry, font smoothing toggle) leaked one canvas per pane.

* feat(editor): read pixel and zero line heights like code editors and let markdown code follow an explicit editor font
2026-10-31 19:20:48 -07:00
Neil 0cb87957e0 fix(terminal): stop terminal text turning bold after a restart (STA-4042) (#36463)
xterm's WebGL atlas warm-up pre-rasterized default-color ASCII without a DOM
container. When it ran before the first glyph was drawn (a blank restored
pane), the glyph canvas was still detached, so Chromium ignored the page's
-webkit-font-smoothing: antialiased and baked heavier glyphs into the cache
until something cleared the atlas (e.g. an image paste).

Warm up through the terminal element and skip it while that element is not
connected; skipped glyphs are drawn on demand.
2026-10-30 15:08:42 -07:00
Brennan Benson eb3379ff99 Fix macOS daemon PTY kqueue descriptor leak (#16870)
* fix(daemon): release node-pty kqueue descriptors

* test: remove redundant wait from kqueue regression
2026-10-30 11:42:31 -07:00
Neil 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.
2026-10-30 01:05:34 -07:00
Brennan Benson fd976a47be Keep zoomed PDF detail text sharp (#36173)
* Keep zoomed PDF detail text at device resolution

* Test PDF detail update dimension-read bounds

* Account for browser layout units in PDF geometry test
2026-10-29 21:27:45 -07:00
OrcaWinandOrcaWin 5951dbbf92 fix(editor): task-list parsing fixes (ordered parents, wrapped markup, tight continuations, bare markers) (#35187)
* fix(editor): preserve sibling tasks beneath ordered list items

* fix(editor): preserve wrapped inline markup in task items

* fix(editor): keep nested task continuations valid and editable

* fix(editor): keep literal task markers out of mixed checklists

* fix(editor): apply review fixes to task-list parsing stack

- Trim synthetic test matrices to the behavioral cases
- Regenerate pnpm-lock (drops a stale github-slugger importer entry)
- Assert the plain bullet's node type under an ordered parent

* fix(editor): keep one-column-deeper ordered tasks as sibling checkboxes

Dedent an ordered item's task block by the first task's column instead of
keeping its leading whitespace. A task one column deeper stays a sibling
checkbox, and text at or within the first task's column stays nested as
before. Strip leftover paragraph indent from task children.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-29 17:38:19 -07:00
Neil 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)
2026-10-29 09:51:53 -07:00
Neil f12e12f212 fix(test): report a failing test whose stack names a Bun builtin frame (#35384) 2026-10-28 15:15:29 -07:00
OrcaWinandOrcaWin a891e3612c chore: remove remaining Vitest patch (#34878)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 01:10:32 -07:00
OrcaWinandOrcaWin 7cdfebdc1b Revert iteration checks and custom Vitest selection (#34834)
* revert: remove iteration checks and custom Vitest selection

* chore: use upstream affected-test selection

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 00:11:04 -07:00
OrcaWinandOrcaWin 0866cb1b08 fix: bound local iteration checks and show live progress (#34700)
* fix: bound local iteration checks and show live progress

* fix: harden check lifecycles and cached test evidence

* fix: preserve process instrumentation and fixture lifecycles

* fix: give fixture unref spy a portable function type

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 23:04:23 -07:00
Kelvin Amoaba 917ef9c737 feat(browser): install Chrome extensions from the Web Store into Orca's browser (#30940) 2026-10-27 22:04:12 +00:00
Takuya AkiyaandNeil 176852ad5a fix(terminal): highlight the active IME conversion range (#27903)
Japanese IMEs convert one clause at a time, but the terminal's preedit overlay
painted the whole composition uniformly, so the clause the candidate window
targets was invisible. Read the IME's selection from xterm's textarea on the
insertCompositionText input event (Chromium only updates it there) and paint
that range of the preedit in the theme's selection colors. Cell-grid advance
corrections are unchanged and nothing reaches the PTY; a collapsed caret or a
range outside the preedit highlights nothing.

Fixes #27873

Co-authored-by: Neil <neil@stably.ai>
2026-10-27 04:51:06 -07:00
OrcaWinandOrcaWin becf899273 fix(test): stop Vitest failing green unit shards with EISDIR on directory stack frames (#34335)
Vitest's reporter checks existsSync(file) and then reads the file to find a
source map. When an error stack names a directory, readFileSync throws EISDIR
as an unhandled error, so the shard fails with every test passing. Read only
regular files.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 01:54:51 -07:00
MeapriandNeil 47567c6ac7 fix(terminal): refuse non-finite mouse coordinates so scroll never sends NaN to the PTY (#23330)
xterm's touch inertia gesture events carry no clientX/clientY, so with a
TUI requesting wheel reports a flick sent `ESC[<65;NaN;NaNM` to the PTY
(#20983). NaN slipped past `col < 0 || col >= cols`; the patched range
check in MouseService._triggerMouseEvent is negated so it fails closed.
Regenerate the beta.304 bundle and lock hash.

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-10-26 23:36:01 -07:00
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>
2026-10-26 00:29:36 -07:00
Neil e4bbbe045f perf(test): faster local test loop and cold-worktree changed runs (#33421)
- test:changed exits before starting Vitest when Git reports no changed files.
- Unrecorded tests beyond 100 in scope use Vitest's static import graph instead
  of all running, so a fresh worktree's first changed run no longer runs the
  whole suite (patched RecordedModulesResolver; CI does not record, unchanged).
- Bun's slow config load was the SQLite import scan statting every unit test;
  filtered runs now classify only the tests their filters can select, and the
  scan walks directories instead of fs.globSync.
- Silence the native config loader notice on every run.
- Zod's test bundle is content-addressed and reused, so a killed run leaks
  nothing; leaked per-run directories older than 6h are swept.
- build:packages skips compiling when package inputs, compiler, and dist are
  unchanged; test scripts no longer nest pnpm invocations.
2026-10-24 23:15:51 -07:00
Neil 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
2026-10-24 21:45:06 -07:00
Neil ec5cfbad37 fix(deps): patch vulnerable Zod collections in mobile and docs (#32941) 2026-10-23 15:29:15 -07:00
Kelvin Amoaba 4c6cfda0ac fix(editor): keep paragraphs starting with "Dr.", "A." or "iv." out of ordered lists (#32694) 2026-10-23 22:19:53 +00:00
Neil a8dff64cb8 chore: remove unused shadcn generator dependency (#31737) 2026-10-21 18:59:05 -07:00
Neil 5f50b95c9c fix: remediate dependency security advisories across workspaces (#31681)
* fix: remediate dependency security advisories across workspaces

* test: verify security patches without ad hoc dependency installs
2026-10-21 16:31:17 -07:00
Neil 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.
2026-10-18 20:21:50 -07:00
Neil 357aa41cb4 fix(terminal): keep Vim scrolling inside shrunken panes (#29960) 2026-10-16 20:45:13 -07:00
Neil 90849ed0c6 Enable Claude plugin images and bounded terminal file transport (#28200)
* fix: render Kitty Unicode placeholder images with bounded ownership

* Enable managed Claude plugin images and bounded host file transport

* Preserve isolated Claude evidence and paired restart diagnostics

* Resolve Windows workspace aliases before WSL image reads

* Handle split remote terminal identities and improve WSL file-read diagnostics

* Reuse native drive paths for WSL image files and preserve guest temporary roots

* Apply native Windows path restrictions to mapped drive image sources

* Preserve WSL participation evidence across image checks

* Keep WSL drive directory containment case-sensitive after canonicalization

* Negotiate managed Claude image support with the launching renderer

* Make image repaint checks independent of display profile and shell output

* Capture repaint diagnostics only after the pixel assertion fails

* Allow floating terminal image reads from host temporary folders

* Keep image stack independent of unrelated CI runner and mobile lint settings

* Reuse worktree argument builder for terminal viewer context

Remove duplicated mapping and keep the RPC method table within its existing line limit.
2026-10-15 21:43:48 -07:00
Neil 26278b32ce Render Kitty images at Unicode placeholder cells (#26031)
* fix: render Kitty Unicode placeholder images with bounded ownership

* Make image repaint checks independent of display profile and shell output

* Capture repaint diagnostics only after the pixel assertion fails
2026-10-15 21:29:26 -07:00
Neil 6607c54984 perf(ci): cut redundant tests and reuse expensive setup (#28669)
* perf(ci): reduce test overhead and reuse expensive build work

* test(ci): keep worker startup outside failure-case deadlines

* test: isolate push fixture advisory locks

* test(ci): preserve fixed drain retry allowance

* Align CI contracts with current triggers and guide content

* Stabilize consolidated reveal scenarios and retain behavioral CI contracts

* Fix upstream lint blockers while retaining behavior and assertions

* Classify global Jira preferences in the WSL migration inventory

* Align Jira test fixtures with startup and build safety checks

* Include package-test routing dependency in CI checkouts and fixtures

* Keep native stream classes intact in transcript dependency tests
2026-10-14 16:14:27 -07:00
Neil 58d0de6bb6 fix(terminal): keep the text of an IME key pressed before a commit finalizes (#28441)
* fix(terminal): keep the text of an IME key pressed before a commit finalizes

A Process (229) keydown that lands after compositionend but before xterm's deferred
finalizer runs took the composing early return and never recorded that it owed its
own commit. When the finalizer ran first, the key's insertText found no pending
transaction and no owed commit, so xterm dropped it: `你` then `,` reached the PTY
as `你`.

Record the debt for such a keydown when the ended session's commit is already in the
textarea, and clear it when the pending transaction settles the text itself. Sessions
still waiting for their post-composition echo keep their old path, so the echo cannot
be sent twice.

Fixes the intermittent terminal-linux-direct-ime-commit e2e failure.

* fix(terminal): keep a commit when Escape or a keypress follows compositionend

A plain Escape after compositionend cancelled the accepted commit, dropping both the
text and the Escape; it now settles the commit and then sends Escape. The Escape that
itself ends a session (still composing, reported after compositionend) still cancels.

A keypress for the Process key that owes a commit now clears that debt, so a later
input cannot send the key's text a second time.
2026-10-13 22:13:25 -07:00
Neil 83d096b5f5 perf(lint): avoid unnecessary anti-slop scope construction (#28470)
* perf(lint): construct anti-slop scopes only for relevant expressions

* fix(ci): resolve extensionless mobile census entries as files
2026-10-13 18:19:50 -07:00
Brennan Benson 7e0d6dc2f6 fix(diff): stop closed diff editors from leaking through Monaco's gutter menu (STA-3565) (#27793) 2026-10-12 20:01:48 -07:00
NeilandClaude Opus 5.5 125007b680 chore: upgrade terminal dependencies to matching payload betas (#27935)
* chore: upgrade terminal dependencies to the matching payload betas

* test: harden xterm payload contract, align mobile pins, fix serialize-at-ref across version bumps

- Cover foreign-payload replacement and the shared blank-cell reset in the
  installed image payload contract (both mutants previously survived).
- Bump mobile's xterm, webgl and unicode11 pins to the root's betas.
- Resolve the ref's own serialize version and patch so the differential
  fuzz baseline builds across a version bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:53:42 -07:00
OrcaWinandm4air 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>
2026-10-11 23:31:03 -07:00
Neil ac156c0302 perf(ci): overlap Bun typechecking and skip redundant alias scans (#27382)
* perf(ci): skip unnecessary alias scans and compare preflight compilers

* perf(ci): overlap measured Bun typechecking with static checks

* perf(ci): use measured Bun lint runtime and strengthen compiler parity checks
2026-10-10 22:48:25 -07:00
Neil da6d632a91 [CI] Revert library patches introduced by test optimization (#26727) 2026-10-08 20:29:42 -07:00
Neil 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
2026-10-08 16:08:49 -07:00
e3639ef8d6 Phase 3.5: fix the top SSH/remote issues at the root cause (#26483)
* fix(remote-runtime): stop arm64 Linux servers dropping clients every heartbeat (empty WebSocket write EFAULT) (#26200)

* fix(remote-runtime): never write an empty WebSocket payload on its own (arm64 Electron EFAULT)

Patch ws@8.22.0 Sender.sendFrame so a frame with an empty payload is written
as its header alone. Under Electron on arm64 kernels with 39-bit virtual
addresses, socket.write() of a zero-length buffer fails with EFAULT and kills
the connection, so every empty heartbeat ping, auto-pong and close dropped
the remote runtime with 1006. Wire bytes are unchanged.

* test(remote-runtime): run the ws empty-payload guard on real Node

Bun resolves 'ws' to its built-in client, so under the Bun test project the
guard exercised Bun's implementation instead of the patched package.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): ship the session scanner child so agent session history lists on orcad hosts (#26309)

orcad built every forked child except session-scanner-service-entry.js, so
aiVault.listSessions always failed on an orcad host. The resolver also skipped
the adjacent entry whenever isPackaged was true, so shipping the file alone
would not have been enough.

- build every ORCAD_CHILD_ENTRY_POINTS entry from one loop and add the scanner
- add it to ORCAD_ARTIFACTS so a deploy missing it fails integrity checks
- share one forked-child path resolver (asar layout vs flat app root) between
  the watcher and the scanner
- smoke-fork the built scanner during build-orcad under both runtimes

Fixes #24105

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(remote-runtime): one owner for a remote pane's binding and recovery (#26308)

A remote pane no longer latches in connecting on terminal_not_found, holds
keystrokes typed during same-terminal recovery instead of dropping them,
reports transport-owned retries so the pane is not remounted mid-retry, and
no longer infers absence from an inventory that predates the leaf.

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(e2e): move apt off the Azure mirror on runner images that use a mirror list (#26335)

Ubuntu 24.04 runner images resolve apt sources through /etc/apt/apt-mirrors.txt, so rewriting only the source lists left package downloads on azure.archive.ubuntu.com, which intermittently fails ('Ign:' on every package). Rewrite the mirror list too, and retry the install once with --fix-missing.

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): remote snapshot replay keeps scrollback it cannot replace (#26319)

Desktop subscribers of a remote terminal received screen-only snapshots,
and every pushed snapshot replay cleared the pane's scrollback with ESC[3J.
A reconnect, a remount, or a recovery after a dropped frame therefore wiped
history the host never resent.

- Host: desktop subscribe snapshots use the same scrollback-row ladder as
  mobile, and SnapshotStart now carries an optional scrollbackRows field.
- Client: a pushed snapshot clears scrollback only when the host says it
  carries history; screen-only images (scrollbackRows 0, or older hosts
  that omit the field) repaint the screen and keep the pane's history.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(daemon): clean launch env, validate client frames, fit the socket path (#26306)

* fix(daemon): clean launch env, validate client frames, fit the socket path

- Drop Chromium's DBUS_SESSION_BUS_ADDRESS=disabled: marker from the daemon,
  its systemd-run scope launch and every PTY env (#25580, #21119).
- Use the durable user-manager scope only when that manager outlives the
  caller: lingering on, or the caller already runs inside it (#24201).
- Shape-check hello and request frames, and catch request rejections, so a
  malformed frame closes one connection instead of the daemon (#17841).
- Move the daemon socket to a short per-uid private dir when the data root
  would overflow sun_path (#17840).

* fix(daemon): validate a relocated socket dir before any connect, not just before bind

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): keep crediting a PTY after its recovery delivery is canceled (#26341)

A canceled recovery delivery released its source spans while the renderer
and model could still settle them; the stale projection then threw on every
later ACK, so the next delivery was never credited and the relay paused the
PTY for good. Settling or transferring a span of a closed delivery is now a
no-op.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential (#26337)

* fix(rpc): deny-by-default caller permissions and a scoped SSH bridge credential

Every RPC method now declares a required permission, and RpcDispatcher checks it
against the caller's scope on every transport (owner, mobile, paired runtime,
SSH bridge). The SSH relay bridge no longer spawns the client CLI with owner
authority: each invocation gets a short-lived credential bound to its SSH target,
revoked when the child exits. Without the new per-host opt-in, that credential
reaches only its own host's terminals; with it, workspace methods but never
desktop control, accounts, settings writes, skill install/share, pairing or host
admin. Paired runtime clients need a pairing-time grant (orca serve
--grant-desktop-control) for computer.* actions.

Fixes #23835
Fixes #25259
Refs #18269

* fix(rpc): let a bridged SSH worker report to its own coordinator; allowlist paired-runtime permissions

Without the per-host opt-in, orchestration send/ask/reply/check/inbox now pass when the caller is a
terminal on the bridged host, binding pane key, payload Dispatch, run and recipient to that caller's
own orchestration (recipient off-host only as the creator of its active Dispatch).

Paired runtime clients get an explicit standard permission set; pairing-admin and desktop-control
are grant-only.

* fix(rpc): resolve a bridged SSH caller's own Run and Dispatch mailboxes before admission

Mail routing stores a worker's follow-ups under dispatch:<id> and worker
questions under run:<id>, so comparing the original recipient to the caller's
terminal handle refused an SSH worker replying to its coordinator, an SSH
coordinator answering its own Run's question, and explicit sends to the
caller's own run:/dispatch: address. The bridge now resolves the exact
mailboxes the caller's live pane and process read (as check does) and admits
replies and recipients against those.

* refactor(rpc): parse SSH bridge selectors with a typed schema instead of Reflect.get

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(projects): remove a project only on the host the user picked (#26340)

* fix(projects): remove a project only on the host the user picked

removeProject now requires a hostId and matches that host's row exactly.
The bare-id fallbacks (unique candidate, focused host) could resolve to
another host's live project and delete it. Orphaned SSH purge and group
deletion now pass each row's own host.

Fixes #13071

* fix(projects): group delete removes only the group's member rows

Same-id rows on other hosts outside the group were removed because membership
was rebuilt from bare ids after the cascade cleared it. Capture (id, host)
member pairs before deletion and remove only those.

* fix(projects): never let a stale catalog widen a project removal to every host

The renderer chose id-only `repos:remove` when its own catalog showed no
same-id row on another host. A stale catalog (another window, a paired
client, or a pending refresh) hid that row, and main then deleted every
host's registration and saved workspace metadata.

- The local catalog route now always calls `repos:removeForHost` with the
  owner row's host, for local, direct SSH, git and folder projects.
- `Store.removeProject` refuses an id that rows on more than one host share,
  so no id-only caller can delete another host's row.
- Setup rollbacks remove the just-created row by its host.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows (#26348)

* fix(serve): load SSH targets on a headless serve; a user Quit only closes serve-hosted windows

- launchServeMode registers the SSH handler layer before RPC binds, so a windowless
  'orca serve' lists and connects the profile's SSH targets (#25886, #8489).
- Under serve, the app-menu Quit/Exit, tray Quit and renderer-recovery Quit close the desktop
  windows instead of quitting the process (#15537). Signals, supervisor stops, relaunches and
  update installs still quit normally; on macOS a signal reaches before-quit the same way
  Cmd-Q does, so the guard lives on the user commands, not in before-quit.

* fix(serve): SSH profiles serve on Electron; native macOS quits and frozen windows no longer trap or stop serve

- orca serve: a profile with saved SSH targets stays on the Electron host by default, since
  orcad has no SSH stack yet (#25886, #8489). ORCA_SERVE_RUNTIME=orcad still forces orcad.
- macOS serve: Dock > Quit, app-switcher Cmd-Q and other native terminates only close the
  desktop windows (#15537). Signals, supervisor stops, relaunches, update installs and
  logout/shutdown still quit.
- The serve signal listeners now reclaim SIGINT/SIGTERM/SIGHUP from Electron's native handler.
  Any signal that had a Node listener before app ready was left with Electron's handler, so
  these listeners never ran.
- A serve user Quit closing a frozen renderer's window arms the same 10s ack deadline as a
  desktop quit, and skips minimize-to-tray.

* fix(serve): stay on Electron when the profile's SSH targets cannot be read yet

A valid older-schema profile is rejected by the read-only probe, and the
selector took that as "no SSH", so a default serve before the first
migration started orcad with an empty SSH registry. Fail closed instead.

* refactor(window): type user-Quit close marks as a named window slice, not object

* test(serve): pass the desktop-control grant default in the headless SSH launch fixture

* test(serve): run the serve-selection SQLite fixtures in the Node runtime project

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods (#26316)

* fix(terminal): keep the main-process terminal tail linear under TUI redraw floods

Full-width in-place row rewrites rebuilt the row string per character
(O(width^2)), a long chunk past the line cap spliced the row array once
per newline (O(lines x cap)), and multi-frame chunks summed every
cursor-up so they fell back to the whole-tail path. Rows now hold
mutable cells joined once, capped rows are dropped in batches, and the
redraw window uses the net cursor reach.

Fixes #11315

* fix(terminal): do not rebuild a redraw row the cursor only revisits

Rows stayed cell arrays once touched and re-joined on every newline, so a
cursor revisiting one wide row thousands of times cost O(width) per
newline. Runs now splice into the row string, a row switches to cells
only after 32 edits between snapshots, and cells are joined once and
dropped.

* fix(terminal): keep one cached row per chunk so unchanged redraw revisits cost O(1)

A row now keeps its cells, right-trimmed snapshot and content end across
newlines and only invalidates them on a real content change; identical
writes leave it clean. Adds a work-accounting matrix over restore, the
append path and live onPtyData, plus a per-character row oracle fuzz.

* fix(terminal): return redraw rows to flat strings with cached snapshots

Cells made every changed snapshot a full-width join, which was far slower
than the old flat-string splice. Rows are flat strings again, written one
run at a time; each keeps a cached right-trimmed snapshot and content end
that only a real change invalidates, so identical rewrites and cursor-only
revisits stay O(1). A wall-time test now times every review probe against
a vendored copy of the old row model on restore, append and onPtyData.

* test(terminal): read runtime leaf and PTY tail through typed members

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(ssh): relay fallback runs Orca's pinned Node by default, never a host npm install (#26303)

* fix(ssh): relay fallback runs the pinned Node ladder by default, never a host npm install

A host whose connect falls back to the relay route now gets the A/B/C ladder
(Orca's uploaded Node and prebuilt addons) unless it opts into Host Node. A
refused rung C steps to rung D with an actionable reason instead of compiling
native deps on the host.

* fix(ssh): mark the opt-in Host Node relay runtime as unsupported on the SSH host card

The relay fallback now defaults to Orca's pinned Node ladder; the host-npm path is
reachable only by an explicit per-host Runtime or ORCA_SSH_REMOTE_RUNTIME override.
When a relay actually launches on it, main publishes hostNodeRuntime on the
connection state (optional, admitted field-by-field for paired clients) and the
SSH host card shows an unsupported-configuration warning.

* fix(ssh): keep the relay runtime ladder honest about what it cannot verify

- NixOS's stub loader refusing Orca's generic Linux Node (exit 127, 'Could not
  start dynamically linked executable') is classified wrong_libc, so the ladder
  steps on to the host Node instead of failing the connect at rung A.
- In plain SSH mode the shell provider answers absent only for shells it saw
  exit; a PTY id it never minted (one an earlier relay may still run) is
  unverifiable, never exited.
- The rung D message offers Host Node only when the host's own Node refused the
  prebuilt addons, and calls it an unsupported configuration; a download,
  unresolved-host or Windows refusal gets no Host Node advice.

* fix(ssh): keep the host-Node relay where the ladder can't judge the host

A client without Orca's runtime artifacts (a dev, e2e or source build with no
orcad template, or an offline client) refused rungs A and C with
artifacts_unavailable, and the connect settled on rung D. That reason describes
the client, so it now lands on the host-Node relay it had before the ladder,
marked unsupported, and persists no host decision. A proved noexec still lands
on D.

Windows hosts have no rung B or C yet, so any rung A refusal there keeps the
host-Node relay too. The rung D message is worded from the host OS: Windows
gets no noexec-mount wording and is offered Host Node.

* fix(ssh): a host-Node fallback with no Node settles rung D, and mutable refusals expire

The host-Node relay the ladder chooses in place of D (a client artifact gap,
or a Windows rung A refusal) hard-failed the connect when the host had no
Node: the resolver threw a plain Error, which plain SSH mode does not admit.
The fallback now proves host Node with a strict probe first; an answered
"no Node" settles D with a classified, host-OS worded reason, and an
unanswered probe stays a retryable failure. A gap no longer rewrites D to
host Node once rung C proved host_node_missing.

Mutable pinned-runtime refusals (missing_lib, security_software, and noexec
on Windows) are no longer persisted and replay from memory for ten minutes
only, so a repaired host gets rung A back. The in-memory refusal cache moves
to ssh-relay-pinned-refusal-cache.ts.

* fix(ssh): no host does worse than the pre-ladder host-Node default

Restructure the relay runtime ladder around one invariant: A, B and C are tried
first, and any refusal past them falls back to exactly the pre-ladder default,
the host-Node relay marked unsupported. That covers an unidentified libc, a
client artifact gap, Windows, mutable refusals and a C N-API floor the npm build
would not have hit. Rung D is reached only when that fallback proves the host
has no usable Node, or a proved noexec home would defeat it too.

Strict host-Node resolution now treats only a command that ran and exited as an
answer, so a refused channel (MaxSessions), session limit or timeout stays a
retryable failure instead of a 'no Node' verdict. The launch reuses the Node the
fallback probe found.

Mutable refusals (missing_lib, security_software, Windows noexec) persist again
with their proof time and replay for 24 hours, so a host that still blocks
Orca's runtime is not re-uploaded to on every connect, while a repaired one gets
rung A back. A table-driven test asserts the invariant per host class.

* fix(ssh): every answered rung failure reaches the host-Node default, and D only past it

Two shortcuts skipped the pre-ladder default. An answered but unclassified
install failure (tar ENOSPC, an extract or hash marker, an unverified Windows
promote, a self-test that failed outside every refusal class) aborted the
connect on every attempt; it now steps down as a non-persisted install_failed.
Only unanswered failures (abort, timeout, channel-open, session limit, an
unconfirmed termination, SFTP connection loss, ssh exit 255) stay retryable,
and nothing steps down once a relay launch was issued.

A proved noexec no longer jumps to D: exec denial can be per binary (AppArmor
allowing /usr/bin/node), so it skips B and C, which load from the same tree,
and reaches the host-Node fallback. D now follows only that fallback answering
with a failure, so its outcome is a superset of the default's by construction.

The never-worse test is now the cross-product of every A, B, C and host-Node
answer on Linux and Windows, plus named review cases.

* fix(ssh): system-ssh upload failures and a broken cached runtime step down before launch

System-ssh directory upload and system sftp threw plain Errors that dropped the
host's exit status, so a remote tar ENOSPC or a Windows sftp permission refusal
aborted the connect instead of stepping down. The remote side now throws
SystemSshCommandExitError (ssh exit 255, signals and lost-connection
diagnostics stay retryable), and a local tar failure is still blamed on the
client rather than the remote tar that saw its truncated stream.

The warm runtime check only tested that the verified executable existed, so a
cached runtime that the host's exec policy or a removed library had since broken
was launched and hung on every reconnect. The same command now also runs it and
reports a refusal the way promotion does, so it steps down before any launch,
with no extra round trip.

* fix(ssh): keep the remote answer when local tar only failed writing to it

When the host's tar exits early (ENOSPC, a refusal), BSD tar on macOS exits 1
with 'Write error' and no signal, so the upload rethrew the local error and
dropped the typed remote exit, aborting the connect. The remote answer now wins
whenever local tar finished, was signalled, failed after ssh closed, or reported
a write or pipe error; a local tar that failed on its own input first is still
the client's fault.

* test(ssh): hostile-host cells allow read-only version probes, never installs; probe each Node once

The noexec-home cell failed on 'npm --version': under the never-worse ladder a
proved noexec reaches the strict host-Node fallback, whose probe asks npm its
version before settling D. The cells now judge install and compile calls (npm
install/ci, node-gyp, cc, make) and allow '<tool> --version' / '-v'.

The probe ran twice because the login shell named the Node the path probe had
already rejected. Candidate checks are now memoized per resolution, for the
host-Node fallback and rung C alike.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* chore(i18n): regenerate the runtime-required English catalog after the main merge

* fix(serve): serve on the profile the CLI selected, and refuse any other before the instance lock (#26424)

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(remote-runtime): open terminal tabs reattach after a managed server update re-pairs the host (#26433)

* fix(remote-runtime): a remote pane rebinds after a server update re-pairs its host

A pane captured the host's pairing revision once, when it mounted. A forced
managed-server update re-pairs the host under the same environment id, so every
later request from that pane was refused as stale, and the refusal counted as
fatal: the pane stopped retrying and Reconnect replayed the same stale revision.
Each request now carries the current revision, a pairing refusal re-reads the
catalog before the retry, and the pane treats it like lost contact.

* fix(remote-runtime): close a stream left on the old pairing and ignore a 0x0 grid hold

A stream that reopened before main re-paired stayed open while sendFrame refused
its stale revision, so typed keys vanished. A revision change now closes it as
recoverable so the pane rebinds on the new pairing. A just-restarted server also
published a 0x0 hold that shrank the visible pane to one cell; holds with no grid
are ignored.

* fix(remote-runtime): a pane follows only its own machine's re-pair

A re-pair under the same environment id to a different host key published its
revision before the catalog retired the old tabs, and the pane recovered at
the new revision, so its next call was authenticated as the replacement peer.
The catalog now classifies each re-pair before publishing revisions: a
replaced or removed machine's terminal streams end for good first, and a pane
only advances its pairing along a proven same-host rotation (same registration
and host key). An unresolved identity holds the pane until it is proven.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(rpc-permissions): Phase 3.5 elegance pass (#26471)

* refactor(rpc-permissions): drop never-read incarnation and invocation id from bridge credentials

* refactor(rpc-permissions): define runtime device grants once and reuse bridge scope fixtures

* refactor(rpc-permissions): decide terminal host membership in one predicate and share the opt-in hint

* refactor(rpc-permissions): parse bridge selectors once per call

* refactor(rpc-permissions): admit and bind unopted bridge methods from one table

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(remote-pane-binding): Phase 3.5 elegance pass (#26470)

* refactor(remote-pane-binding): keep the pane's pairing revision in one slot

* refactor(remote-pane-binding): share one hold decision across the input entry points

* refactor(remote-pane-binding): define the persisted-handle adopt once in attachPane

* refactor(remote-pane-binding): build the pushed-snapshot meta once

* refactor(remote-pane-binding): drop the test-only peerReplacedEnvironmentIds wrapper

* test(remote-pane-binding): share the input and subscribe frame readers via stream fixtures

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(relay-ladder): Phase 3.5 elegance pass (#26474)

* refactor(relay-ladder): put the host-Node fallback in the ladder before D

The Auto ladder now lists legacy as the rung before D, so the step function
is one rule plus the next rung. Deletes nextRelayRuntimeStep, the hostOs
step context and the mutable hostNodeFallback flag (now a constructor-time
laddered field). Windows still ends at legacy: B and C refuse there before
any host I/O.

* refactor(relay-ladder): one rung-D reason and message over reachable states

D is reached only from the host-Node fallback, so its refusal is
host_node_missing or install_failed. Merges the reason and message
functions, drops three message branches that could not run, and folds
noexecRemembered/noexecProved into one noexec field.

* test(relay-ladder): one never-worse cross-product with a remembered axis

Drops the named cases (each matches a cross-product outcome), the transport
case that only checked the model against itself, and the third case the rank
check already implies. Adds rung B's noexec answer and per-rung remembered
flags for A and B, the only rungs that replay a cached refusal.

* refactor(ssh): one answered-miss rethrow for the node resolution probes

Four catch blocks repeated the same session-limit, abort and strict checks.
They now call rethrowUnlessAnsweredMiss and keep their own fallthrough.
Also drops a stale 'Caches nothing' comment.

* refactor(ssh): share the host-answered system-ssh exit rule; flatten the upload throw cascade

isHostAnsweredSystemSshExit owns the 255/null transport rule for both the
upload pipeline and isAnsweredHostFailure. settleUploadPipeline drops a
branch that could not run: a failed tar is judged once, then ssh, then pipe.

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* refactor(serve-daemon-misc): Phase 3.5 elegance pass (#26478)

* refactor(ssh-pty): build obligation transitions behind one closed-delivery guard

* refactor(build-orcad): table-drive the shipped worker smokes

* refactor(daemon): pass scope probe seams as one deps object and share cgroup line parsing

* refactor(repos): route every project removal through removeProjectForHost

* refactor(build-orcad): type the worker smoke table for the type-aware gate

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(terminal-tail): Phase 3.5 elegance pass (#26465)

* refactor(terminal-tail): share mulberry32 and compare whole results in redraw equivalence tests

* refactor(terminal-tail): keep one splice ceiling at the onPtyData entry point

---------

Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>

* fix(terminal): bound the backing a redraw row can pin across chained erases (#26547)

Each erase that keeps at least half the row skipped the copy, so a chain
of them left nested slices pinning the original wide row. Rows now track
an upper bound on the string they slice and copy once the cumulative
shrink passes half.

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 05:36:25 -07:00
Neil b32462f246 Replace patched JSON parser with stream-json (#25202)
* Replace patched JSON parser with stream-json

* Isolate dependencies for historical server compatibility builds
2026-10-04 13:05:30 -07:00