Commit Graph
298 Commits
Author SHA1 Message Date
Jinwoo Hong 90b8554fc9 fix(terminal): recover readiness after startup exec (#14027) 2026-08-12 14:31:42 -07:00
Brennan BensonandOrcaWin 4b9fb46646 fix(agent-hooks): stop rewriting typed Prime commands (#13906)
* fix(agent-hooks): preserve typed Prime commands

* test(wsl): enforce Prime bridge exclusion

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-08-11 22:34:54 -07:00
Jinjing 5bee7b5ce9 P1 STA 3887 design Preview Kitty IME (#13940)
* fix(terminal): carry kitty flags through Preview snapshots and pair rele

Preview was omitting the live kitty mirror from the IME bridge and dropping kitty flags from snapshots, so every commit was evaluated at flags 0. A TUI that negotiated bit-3 (report_all_keys_as_escape_codes) would receive the legacy raw text it declined.

Now the snapshot carries proven kitty flags beside their sequence boundary, the forwarder reads flags once per commit, and bit-1 (report_event_types) commits are paired with exactly one release regardless of keyup/insertText ordering. Snapshot authorities expose only the active screen's proven flags, so an old host's absent field stays unknown rather than downgraded to a manufactured zero.

* fix(terminal): sync kitty flags and IME releases across snapshots

* trim wordinesss

* fix(terminal): settle owed IME release before fresh same-key press

When a keyup is lost and the same key is pressed again, settle the stale
record's owed release instead of discarding it — this maintains correct
IME state during recovery. Also refine Kitty flag propagation to only
carry proven baselines across snapshots, and tighten related comments.

* fix(terminal): gate kitty flags on sequence boundaries

- Remote snapshots only include flags when seq is present
- Daemon uses parsed flags value when defined
- Ensures correct flag ordering in snapshot replay
2026-08-11 22:00:44 -07:00
NeilandOrca 5ea7df1a5b fix(terminal): make DECSET 2031 subscriptions silent (#13904)
fish arms `CSI ?2031h` before painting each prompt and withdraws it when it
hands the tty to a child — a ~1ms window. Orca answered that subscribe with
`CSI ?997;Nn` across a 1-3ms renderer hop, so the reply landed after the
withdrawal and was read as stdin by the next child, corrupting `brew`/`npx`
`[y/N]` prompts.

The reply is not stale by Orca's own view when written (measured
staleReplies: 0), so no suppress-the-stale-reply scheme can close this — the
information needed to suppress does not exist yet. Nothing asked for the reply
either. The Contour spec says a terminal "should only send out the DSR when the
palette has been updated"; Ghostty (Termio.zig:729 — force=true reachable only
from the ?996n DSR), iTerm2 (VT100Terminal.m:995 — flag only) and xterm.js
(InputHandler.ts:2035 — flag only) all emit nothing on the DECSET. So stop
entering the race: record the subscription, answer nothing.

Of 17 real programs measured under a pty, only fish, tmux, claude and opencode
subscribe; none block on a reply, and answering produces one redundant palette
re-query and zero rendering difference. tmux is the only one that sends `?996n`,
which Orca still answers.

- Subscribes are record-only at all four emitters (live scan, hidden-gate fact,
  parked byte watcher, parked responder — the last is deleted, it only replied).
- `?996n` answers, the subscription registry, and the theme-flip push are
  unchanged. `paneLastThemeMode` is still seeded at subscribe so the next
  appearance re-apply is not read as a flip.
- Replay grammar carries `?2031l` alongside `?2031h`, so a late-attaching remote
  client no longer registers a subscription the TUI already retired.

Also closes fish-integration gaps found alongside: `unset` (which fish lacks)
becomes `set -e` on paths parsed by the client's login shell, `config.fish` is
parsed for agent-home detection, and bracketed-paste startup delivery is made
consistent across local/daemon/relay.

Regression test drives real fish 4.7.1 under node-pty and asserts on what the
child process reads; it fails against pre-fix code with the exact payload from
the issue. CI installs fish 4 and fails loudly rather than skipping.

Closes #9993

Co-authored-by: Orca <help@stably.ai>
2026-08-11 21:16:36 -07:00
Neil 991a3fe963 chore(lint): update oxlint to 1.77 and enable no-op cleanup rules (#13901)
Enable eleven oxlint rules that simplify code without changing behavior, and fix
every existing violation. Each candidate was gated on measured cost rather than
assumption, so rules that regressed runtime performance or type checking were
dropped instead of suppressed.

typescript/no-redundant-type-constituents is the largest addition: 113 sites, no
autofix. Dead constituents are deleted. Where the redundant literal existed to
document intent (`string | 'all'`), it is preserved as `(string & {})`, which
keeps the autocomplete hint the original code was reaching for instead of
flattening it away. The rule also caught a broken import —
remote-shared-control-retirement-probe.ts pulled RuntimeStatus from
src/shared/types, which does not export it, so the type silently degraded to
`any`; no tsconfig covers that file, so tsc never saw it.

oxlint stays at 1.77.0 rather than 1.78.0 because .npmrc sets
minimum-release-age=4320 and 1.78.0 is younger than that window.

Rules evaluated and rejected, with what disqualified each:
- prefer-string-raw: String.raw is a runtime call, not a literal (184x slower)
- prefer-string-replace-all: 26% slower
- text-encoding-identifier-case: ~5% slower, reproducible
- prefer-spread: [...str] is 110% slower than split('') and differs on surrogates
- no-implicit-coercion: `!!x` narrows types and `Boolean(x)` does not (22 tsc errors)
- prefer-arrow-callback: arrows are not constructible, breaking `new` on mocks
- object-shorthand: rewrites source text asserted by a tracked reliability gate
- switch-case-braces: pushes ten files past max-lines, which cannot be suppressed
- no-useless-switch-case: drops `case undefined:` that switch-exhaustiveness-check needs
- arrow-body-style: 115 violations have no fix, and it breaks max-lines
- newline-after-import: false-positives on the leading-semicolon ASI idiom

electron-vite-output-contract asserted on the literal
Object.prototype.hasOwnProperty.call text; retarget it to Object.hasOwn, which
rejects inherited keys identically.
2026-08-11 18:19:43 -07:00
OrcaWin e6eec11b9f fix(wsl): preserve single-letter POSIX terminal cwd (#13859) 2026-08-11 15:29:33 -07:00
Brennan Benson 4e7421d9e1 fix(terminal): explain a dead terminal host instead of leaking a socket error (#13062)
* fix(terminal): explain a dead terminal host instead of leaking a socket error

When the daemon that owned a session has exited, resuming that terminal surfaced
the raw connect error for its endpoint — on Windows a named pipe path, since a pipe
disappears with its server process — in a red toast ending in "file an issue". The
session is unrecoverable and the text is unactionable.

Translate it at the daemon boundary into terminal_host_gone, following the existing
terminal_pane_owner_unverified path, and humanize it in the toast. Suppress the
issue link for a condition Orca can fully explain.

Covers the remote-host case: the conversion sits in attachStablePaneOwner, the first
catch a paired client's resume reaches before the error is serialized onto the wire.

* fix(terminal): preserve host-gone errors across versions

* fix(terminal): scope host-gone error matching

* refactor(terminal): derive the host-gone test and replace forms from one source

Two regex literals matched the same token with subtly different shapes
(capturing vs non-capturing, consuming vs lookahead), so an edit to one could
silently drift from the other. Build both from a single source string; the test
form stays non-global so it carries no lastIndex state between calls.

* test(terminal): cover host-gone regex boundaries

* test(terminal): isolate leading host-gone boundaries
2026-08-11 14:32:26 -07:00
Jinjing ef56ca8b81 fix(feedback): pre-include Orca version and OS in errors and feedback (#13851)
* fix(feedback): pre-include Orca version and OS in errors and feedback

Make terminal errors and Send Feedback carry easy-to-copy client
environment details so bug reports include build and platform context
without a follow-up ask.

* fix(feedback): satisfy exhaustive-deps in environment prefill hook

Destructure hook params so useEffect/useLayoutEffect dependency lists
are complete and the changed-code quality gate passes.

* fix(preload): stop importing node:os in sandboxed preload

Sandboxed Electron preloads cannot require node:os. That import crashed
the whole preload script, leaving window.api undefined and taking down
App chrome (dock badge, preflight). Keep platform.get on process APIs
only, and guard best-effort badge/preflight callers when api is missing.

* fix(feedback): count text typed below the env footer

Strip only the prefilled Orca/OS/Shell block for Send validation so
users who click past the footer and type can still submit.
2026-08-11 11:49:44 -07:00
Neil 34f46398d7 refactor(daemon): remove superseded PTY provider (#13477) 2026-08-10 20:25:43 -07:00
Neil 01102536c6 refactor(daemon): remove obsolete production launcher (#13504) 2026-08-10 20:25:24 -07:00
Neil fb39308dfe refactor(daemon): remove abandoned binary framing (#13530) 2026-08-10 20:24:56 -07:00
Brennan Benson f2984e2230 fix(terminal): skip a too-wide alt frame on snapshot replay (#13014)
* fix(terminal): skip a too-wide alt frame on snapshot replay

Reopening a parked worktree could paint a stale full-width TUI frame
through a narrower viewport, leaving clipped gutter fragments and
mid-word omissions until the live application repainted.

Replay pins xterm to the snapshot grid so soft-wrapped normal-buffer
history stays exact (#7279), then the post-replay fit returns the pane
to its container grid. Alternate buffers have no scrollback and do not
reflow; an absolutely positioned frame remains at its capture layout,
so narrowing exposes only clipped portions of those fixed-grid rows.

Skip only the visual frame when its capture is wider than the grid the
fit will land on. The alt buffer is still entered and cleared, so the
resize signal lands on a clean screen that the live application can
repaint. Equal-width and wider restores retain the frame, while normal
history always replays at its capture grid before fitting.

The target width comes from proposeDimensions, not terminal.cols: an
unfitted pane can still read xterm's default grid even when its actual
container matches the capture.

* fix(terminal): preserve offline SSH prepaint frame

* fix(terminal): drop a too-wide daemon alt frame on reattach

The renderer-only gate did not run on the user-visible remount path.
Instrumentation showed that daemon-connectResult-snapshot won before
the model-snapshot branches, so the composed snapshot painted in full
and the later narrower fit exposed its stale fixed-grid frame clipped
at the new viewport.

The daemon branch could not omit only the visual frame while main sent
one merged string. Publish the normal-buffer/mode prefix and visual alt
frame as additive optional metadata while retaining the merged snapshot
for mixed-version fallback. New renderers can keep history and restore
state without painting a frame captured for a wider grid.

Replay ordering remains capture-grid, write, then fit so normal-buffer
soft wrapping stays exact. The application owns the foreign-width alt
frame and repaints it after the resize signal rather than Orca trying to
transform an absolutely positioned screen.

* fix(terminal): preserve split daemon snapshot payload

* fix(terminal): preserve live state when dropping alt frame

* fix(terminal): restore DECOM cursor state exactly

* fix(terminal): preserve ordinary snapshot bytes

* test(terminal): cover fixed-grid alt replay resize

* fix(terminal): repaint after dropping mismatched frames

A hidden snapshot can omit an alternate-screen frame before the pane has a measurable target grid. If reveal later lands on the capture grid, a same-size PTY resize emits no SIGWINCH, so pulse the local PTY size whenever that frame was skipped.\n\nKeep performSafeFit's measurable-pane contract intact, publish daemon snapshot prefix and frame as explicit optional strings, and fall back to the merged payload when either field is absent. Cold owner-gone restores now omit a mismatched frame while retaining history and fresh-shell reset treatment; offline SSH preconnect remains unchanged.\n\nPin the vendored SerializeAddon out-of-range-row behavior used to capture live SGR state.
2026-08-10 18:24:04 -07:00
Brennan Benson d6362deb04 feat(agent-hooks): support Prime Agent status in WSL (#13430) 2026-08-09 22:55:32 -07:00
Brennan Benson 774bbc788f fix(terminal): preserve OSC 8 links across cold parking (#13382)
* fix(terminal): preserve OSC 8 links across cold parking

* test(terminal): make OSC 8 e2e cross-platform

* test(terminal): focus OSC restore e2e on activation
2026-08-09 16:36:12 -07:00
Brennan Benson 6858e072cf fix(terminal): agent pane auto-launch lost under fish + Starship (STA-3417) (#12840)
* fix(terminal): extend the shell-ready startup barrier to fish (STA-3417)

Fish never emitted the OSC 777 shell-ready marker, so agent launch
commands were written into the PTY while fish/Starship were still
initializing: the daemon path wrote them synchronously at session
create and the local path blind-wrote ~30ms after the first output
byte. The command was echoed by the kernel but never executed.

- shell-templates: shared fish --init-command that emits the marker
  once on the first fish_prompt event (the earliest point fish's own
  reader owns the PTY, mirroring zsh's zle-line-init marker)
- daemon shell-ready: fish joins the startup barrier so the launch
  command queues until the marker (timeout fallback unchanged)
- local-pty-shell-ready: fish launch config gains the marker wrapper
- codex-startup-delivery/tui-agent-startup: omp/pi/opencode plans now
  request shell-ready delivery (codex parity) so the SSH renderer path
  also waits for the prompt; plain payload-free codex stays on the
  markerless fast path

* fix(terminal): answer DA1 past the shell-ready barrier

The barrier queues all inbound input until the ready marker, including the
renderer's DA1 reply. A shell that withholds its first prompt until DA1 is
answered — fish waits 10s — therefore never emits the marker that would
release the reply it is waiting for. Measured: 10.37s to launch an agent,
versus 0.35s once the reply lands.

Answer DA1 from the daemon while the barrier holds, writing straight to the
subprocess so the reply bypasses the queue, and consume the query so the
renderer's xterm cannot also reply. Released on ready, timeout, or dispose,
handing DA1 back to the renderer for steady state.

Consolidates the identical DA1 handler the ConPTY override already used.

* fix(terminal): prevent duplicate startup DA1 replies
2026-08-09 14:29:14 -07:00
Brennan Benson 43bad61a93 fix(terminal): deliver SIGWINCH to the PTY foreground process group (#13164)
* fix(terminal): deliver SIGWINCH to the PTY foreground process group

A real window-size change is delivered by the kernel to the terminal's
foreground process group, but Orca signalled the pty's root pid. The root
is never in that group: the shell setpgid's away for job control, a
foreground TUI forms a third group again, and on macOS the root is
login(1), which neither handles nor forwards SIGWINCH.

So the explicit reattach repaint signal reached a running TUI on no
platform, and on macOS reached nothing at all. Resolve the tty's tpgid
and signal the negated group instead, pinned to the slave device captured
at spawn so a recycled pid cannot aim at a real terminal.

Scoped to SIGWINCH: destructive signals keep the narrower root-pid target
and the descendant-sweep identity machinery. POSIX-only, so Windows keeps
its current behavior where a negative pid is invalid and SIGTERM means
terminate. Applied on the relay too, so SSH hosts get it; that is
host-local with no wire change.

* fix(terminal): bound foreground group lookup latency

* fix(terminal): preserve SIGWINCH fallback on signal errors
2026-08-09 14:09:57 -07:00
Neilandbbingz 34f2a62cda fix(pty): stop color-scheme 997 replies from painting cooked prompts (#13309)
Route cooked-echo-risk terminal replies through bounded echo-safe delivery across local, daemon, and SSH relay PTYs. Preserve repeated valid replies, bypass the daemon startup input gate, and keep ordinary input plus latency-critical replies on their existing paths.

Closes #13137

Co-authored-by: bbingz <zzb@gxsmjx.com>
2026-08-09 01:45:15 -07:00
Jinwoo Hong fc8441194c fix(terminal): preserve remote pane output after host restart (#13158) 2026-08-08 01:47:05 -07:00
Neil f858c5a13a test(daemon): stop skipping killStaleDaemon checks on Windows (#13154)
Three guards in daemon-health.test.ts skipped tests that assert PID-record and
start-time logic. The file's daemonTestSocketPath already returns a real named
pipe on win32, so none of them needed a Unix socket — the skips cost coverage
for nothing.

Verified on a Windows host: the file goes from 29 passed / 4 skipped to
32 passed / 1 skipped, restoring Windows coverage of killStaleDaemon's
ownership decisions, which #12882 changed.

The fourth guard stays: that test spawns a real child and binds a filesystem
socket path, which fails with listen EACCES on Windows.
2026-08-08 01:01:30 -07:00
Neil bba32bd00c fix(daemon): let the publisher replace a dead endpoint, not a third party (#12882)
Terminals froze app-wide several times daily, needing a manual pkill. libuv
unlinks the pathname a server bound to when it closes, with no ownership check,
so a departing daemon deleted whichever socket then sat at the canonical path —
including a live replacement's. The replacement kept hosting PTYs no client
could reach.

#12709 fixed that mechanism; this replaces the shape around it. Two invariants:
only a daemon publishing itself onto the canonical endpoint may mutate that
entry, and only by replacing one it has itself just proven dead; and no actor
removes a name it did not create.

Publish binds a private name, takes the canonical one with an exclusive link,
and on EEXIST proves the incumbent dead by connecting before replacing it in a
single rename. Only 'connected' means occupied and only refused/missing prove
death — a timeout proves nothing and declines. Deletes the claim sweeper, the
reclaim tail of killStaleDaemon, and three unfenced unlinkSync(socketPath)
calls in the launcher.

Measured: rename exposed no gap across 6,525 darwin / 8,004 linux probes of a
live handover, where unlink-then-link gapped on 200 of 200.

Verified on all three platforms: full suite on macOS and Linux, and daemon
restart e2e on a real windows-2022 host. Contract in src/main/daemon/AGENTS.md.
2026-08-07 22:33:50 -07:00
Jinwoo HongandJinwoo-H 87d768058c fix(terminal): recover stale persisted owner bindings (#12976)
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
2026-08-06 21:18:41 -07:00
Brennan Benson a2d438db17 fix(daemon): detect severed macOS TCC attribution behind terminal automation denials (STA-3491) (#12848)
* fix(daemon): detect severed macOS TCC attribution and surface daemon-restart remedy (STA-3491)

macOS pins the detached PTY daemon's TCC responsible process to the app
binary that forked it. Once that binary is deleted (packaged updates
replace the bundle), Accessibility/Automation grants on Orca silently
stop covering every daemon-hosted terminal: osascript/System Events
fails with -25211 no matter what the user grants.

- record spawnerExecPath in the daemon pid file at fork
- adoption checks it: severed + 0 live sessions -> replace the daemon
  (reason severed_tcc_attribution); live sessions are preserved
- Settings (Developer Permissions + Manage Sessions) show a visible
  banner pointing at Manage Sessions -> Restart while severed

* fix(daemon): harden TCC attribution recovery
2026-08-05 23:54:30 -07:00
Neil 7da9368b78 fix(terminal): fence detached daemon endpoint ownership (#12709)
* fix(terminal): fence daemon endpoint ownership

* fix(terminal): clean failed daemon PID claims

* fix(terminal): close daemon ownership review gaps

* test(daemon): release startup IPC in boot smoke

* test(daemon): mirror production stdio in boot smoke

* fix(daemon): exit after rpc shutdown cleanup

* fix(terminal): make the socket name the daemon endpoint authority

The reported failure was a live daemon hosting PTYs that nothing could
reach: terminals acknowledged input and never ran it, listings diverged
from reality, and restarting the app never helped because the detached
helper survived. The ownership fence added for it could not fire in the
sequence that produces the split brain.

libuv unlinks the pathname a server bound to when that server closes,
with no ownership check. A daemon that lost its endpoint name therefore
deleted whichever socket then sat at that path — including a live
replacement's — stranding a daemon that still hosted every session.
Bind a private same-directory name and hard-link it into place instead:
libuv can only ever unlink our own bind name, the exclusive link is a
kernel-enforced endpoint claim, and the canonical name is removed only
under an inode ownership check. The bind name replaces the basename
rather than extending it, so it cannot overflow sun_path.

killStaleDaemon removed the PID record unconditionally immediately
before every fork, so the exclusive PID claim was always uncontested at
bind time. It also unlinked a live daemon's endpoint whenever a connect
probe merely timed out, and treated a `ps` timeout as proof of PID
recycling. Now only positive evidence of a dead endpoint authorizes
reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon
that cannot be proven stopped keeps its record and endpoint while the
launcher refuses to fork beside it.

A daemon whose endpoint was taken over now retires itself, draining
rather than killing, so an unreachable orphan stops being permanent.

A repaired PID record re-derives entryPath, appVersion and the Linux
incarnation markers from the authenticated owner instead of dropping
them; without appVersion a healthy daemon read as a permanently stale
bundle and, on Windows, went unpinned against daemon-host pruning.
Repair failure now fails open — abandoning a healthy daemon over a pid
file write cost every persistent terminal on the machine.

Also: treat only ENOENT as an unclaimed record so a Windows file lock is
not reported as an ownership conflict; settle start() before close() so
an accepted connection cannot defer it forever; sweep abandoned claim
and bind names; and type the endpoint-identity seam so a rename cannot
silently disable the fence.

Adds a real-process handover smoke that reproduces the failure with two
daemons racing one endpoint, and wires it into the native-smoke job.

* fix(daemon): retire only on proven endpoint ownership loss

The ownership watchdog read a null identity for any stat failure, so a
transient EACCES or EIO on the runtime directory would retire a daemon
that was still serving every terminal on the machine. Distinguish "the
entry is gone" from "the probe failed" and act only on the former.

Also require the loss to persist across two polls: a replacement
publishes by unlink-then-link, and a single observation can land in that
gap.

* fix(daemon): source repaired ownership metadata from the authenticated hello

Adversarial review found three defects in the previous two commits.

Re-deriving entryPath from the owner's command line truncated it at the
first space. A command line is a single space-joined string, so
`C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back
as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats
a present entryPath as authoritative, so a healthy daemon read as
`different_app_path` and was killed and re-forked — worse than the
missing-metadata case the derivation was added to fix. Carry entryPath
and appVersion as optional fields on the daemon hello identity instead:
the daemon already has both from its own argv, and per
docs/reference/remote-wire-compatibility.md a new optional field is safe
because every reader falls back when it is absent. This also removes a
synchronous `ps` spawn from the Electron main thread during startup.

`start()` rolled back the PID record even when it never published one.
Losing the endpoint link now runs that path, and the ownership-checked
unlink briefly renames the incumbent's record aside — enough to strand a
live daemon's ownership. Roll back only what we actually wrote.

publishDaemonSocketPath read its identity from the canonical name after
linking, so a concurrent unlink returned null: no ownership watchdog and
no endpoint cleanup on any shutdown path. Read it from the bound name
before linking, which shares the inode.

Refusing to fork beside an unconfirmed daemon left the user with no
daemon at all and no in-app recovery, since restart re-entered the same
fence. We have just proved something answers the endpoint, so adopt it
in degraded mode: live sessions keep working, fresh terminals run
locally. SIGTERM is also individually guarded now — an EPERM fell into
the blanket catch and reported "nothing alive", authorizing the very
duplicate this fence exists to prevent.

Also reset the ownership-loss streak on an inconclusive probe so the
confirmations are consecutive, and sweep scratch names before the launch
so a failed launch still reclaims them.
2026-08-05 21:12:32 -07:00
Brennan Benson 81e7a2ddf7 fix(terminal): recover live panes after renderer restart (STA-3536) (#12776)
* fix(terminal): stop transient probe blips from erroring restored panes (STA-3536)

terminal_pane_owner_unverified fired for every restored pane whenever one
liveness probe answer went missing: a cold-start daemon draining an attach
stampede misses the 2s getSize deadline, and a wedged superseded daemon
(protocol upgrades leave them running) turns every unmapped fan-out probe
null forever.

- probePtyOwners now skips legacy daemons whose startup inventory listing
  succeeded: fresh sessions never route to them, so they provably don't own
  an unmapped id and one wedged zombie can't poison every pane's verdict.
- attachStablePaneOwner retries the probe over a short backoff ladder before
  surfacing unverified, so a single missed deadline resolves to a verdict.
- The renderer replaces the raw error code with actionable copy.

* fix(terminal): stop retrying definitive owner probes

* fix(terminal): recover live panes after renderer restart

* refactor(terminal): share owner resolution abort guard
2026-08-05 20:52:22 -07:00
ShawnandBrennan Benson c4d5a535f2 fix(pty): restore macOS TCC attribution persistence (#12562)
* fix(pty): restore macOS login shell attribution

Replace the env intermediary with a clean bash trampoline that execs the configured shell as a login shell. Keep SHELL, custom arguments, fallback shells, and unsafe characters positional.

* chore(pty): log macOS TCC spawn strategy

Record wrapped versus direct daemon PTY strategy without private values so live TCC reports can distinguish launch behavior.

* fix(pty): harden macOS TCC login trampoline

* test(pty): update macOS TCC spawn expectation

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-05 00:36:22 -07:00
Jinwoo HongandOrcaWin 72245918a1 fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback (#12589)
* fix(terminal): attach never-activated daemon sessions on remote subscribe and provider-read fallback

A daemon-backed terminal whose tab was never activated in the host UI was
never attached, so the daemon emitted no bytes: paired clients rendered
blank/frozen panes and `terminal read` returned an empty tail while the PTY
was alive.

- Runtime: first remote view subscriber of a known-but-unattached local
  daemon session triggers an attach through the pty controller — attach-only,
  no resize, no renderer mount/focus, headless-safe, deduped across
  concurrent subscribers, and never detached on release. Excludes SSH-scoped
  ids and sessions a local spawn already published this generation.
- Read path: withVisibleSnapshotFallback now falls back to the provider tail
  for an empty-tail never-attached live local session; unprovable state stays
  empty, never an error.
- pty controller: expose attach with getProviderForPty-style routing,
  answering false on doubt; local daemon provider only.
- Daemon adapter: attach rides the session's applied size instead of a
  hardcoded 80x24, sends attachOnly, and retires a pre-v31 daemon's
  accidental spawn instead of publishing it.

Deterministic harness drives the real terminal.multiplex handler against a
real OrcaRuntimeService with an injected daemon-model controller whose data
events are gated on attach; covers snapshot-capable and snapshot-null
daemons, concurrency, release, replacement-spawn exclusion, and negative
safety. Red on base, green with the fix, red again with the fix reverted.

* fix(terminal): refuse degraded-provider attach fallback and surface failed legacy-spawn retire

Verifier follow-ups on subscriber-driven daemon attach:

- DegradedDaemonPtyProvider.attach routed unknown ids to the in-process
  fallback, whose no-op attach resolves — the runtime then pinned a
  subscriber-driven attach as succeeded while the stream stayed blank.
  Attach now refuses any route that resolves to the fallback (a fallback pty
  cannot own a daemon-surviving session), so the controller answers false,
  no sticky success is recorded, and a later subscriber attaches once a
  daemon adapter proves the id. Session-probe adoption moved to
  degraded-daemon-session-routing alongside the new refusal.
- The pre-v31 attach-only TOCTOU retire (accidental legacy spawn kill) now
  logs a warning with the sessionId on kill failure instead of swallowing
  it, so an orphaned replacement shell is diagnosable.

Regressions: degraded provider refuses unowned/fallback-owned attach and
routes to a daemon once it proves the id (red on previous commit); runtime
harness pins refused-attach retry for a later subscriber; adapter test pins
the surfaced kill failure.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:25:20 -07:00
Jinwoo HongandOrcaWin 51ca82d028 fix(runtime): seed terminal previews and titles from restore payloads (#12579)
* fix(terminal): seed list/read records from reattach restore payloads

After an app relaunch the PTY daemon survives and spawn silently
reattaches, but the restore payload (reattach snapshot, cold-restore
scrollback, relay replay, lastTitle) arrives as a spawn RPC result and
never passes through runtime.onPtyData — the only feeder of the terminal
records behind `terminal list`/`terminal read`. Every restart therefore
left connected terminals with empty title/preview/lastOutputAt and a
zero-line read tail, blinding orchestrators that poll terminals.

The spawn flow now calls runtime.seedTerminalRestoreTail with the restore
text and lastTitle, unconditionally of the renderer-authority emulator
gate (the records are main-side only). The seed reuses the live path's
normalize/tail/preview pipeline on a capped 256 KiB suffix (re-anchored
at a line boundary so a cut escape cannot leak), only fills records that
never saw output (a remount reattach cannot re-apply history), routes
titles through the applySeededAgentStatus precedent (state writes only —
no waiters, no side-effect facts), and never stamps lastOutputAt or
waitBlockedAt: restored bytes are historical, not fresh activity.

lastTitle is threaded from the daemon reattach snapshot and cold-restore
checkpoint into PtySpawnResult; relay replays seed preview only. SSH and
runtime-controller paths are unchanged — seeding is gated on the fields
existing.

* fix(terminal): seed restore records on the controller spawn path and prime the wait baseline

Follow-ups to the restore-record seed, from independent verification:

1. The runtime-controller spawn flow (createTerminal background creates —
   headless `orca serve`/CLI — and pane splits) never consumed restore
   payloads, so the exact orchestrator-blindness this fix targets survived
   on the topology that needs it most. The extraction now lives in one
   helper called from both spawn choke points (renderer pty:spawn and the
   controller flow); the runtime's empty-record guard makes overlapping
   seeds a no-op.

2. The throttled per-PTY wait scanner starts with a null baseline, so a
   permission prompt visible only in seeded HISTORY read as newly gained
   on the first benign live chunk and stamped waitBlockedAt "now".
   Seeding now primes the scanner baseline from the seeded tail without
   stamping; only a signal appearing in genuinely new output counts.

3. Cap re-anchoring accepts \r as well as \n (newline-free CR-redraw
   streams), consuming a full \r\n pair so the seed does not start with
   a phantom blank line.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:23:01 -07:00
Jinwoo HongandOrcaWin 27da04d50d fix(terminal): truthful handle liveness + no forked resume tabs for hidden restorable panes (#12574)
* fix(runtime): report terminal handles disconnected on controller-proven PTY absence

leaf.connected mirrors the renderer graph (ptyId !== null), so a restored
surface whose PTY died with a prior process was listed connected/writable
forever with empty title/lastOutputAt/preview — the exact signature automation
saw on run6 workspaces after a restart. listTerminals now threads the
controller inventory it already fetches into buildTerminalSummary and demotes
only on proven absence, only for locally-scoped ids; unknown liveness and
SSH/remote scopes never demote, and no session or pane is retired.

* fix(terminal): stop forking hidden restorable panes into replacement resume tabs

paneWillConnectOnActivation still assumed the pre-keep-alive mount model, but
every non-parked tab of the active worktree mounts and connects hidden at 0x0.
Activation therefore appended a replacement resume tab per non-group-active
agent pane and handed it the sleeping record, stranding the hidden pane as a
bare shell — or forking two live surfaces onto one provider session when the
old PTY survived in the daemon. The predicate now answers "will mount and
connect": any non-web-mirror tab of the active worktree qualifies; non-active
worktrees still answer false so background wake keeps its append-based resume.

Contract change: reverses the hidden-tab expectation from #6800, whose premise
(hidden panes never connect) no longer holds; that test is updated in place.

* test(terminal): pin the remote-scope exemption and the web-mirror ownership exception

CodeRabbit flagged both exclusions as untested: a remote-runtime-scoped leaf
absent from the local inventory must stay connected (its inventory lives on
the remote host), and a web-mirror tab must not own sleeping-session recovery
(it never mounts a local pane), so the appended replacement remains its
correct resume path.

* fix(terminal): rescue just-spawned ptys from absence demotion; unpark panes owning sleeping records

Review (GPT verifier) confirmed two gaps:
- listTerminals demoted a live just-spawned PTY when listProcesses snapshotted
  before session registration (the sweep's hasPty rescue is leaf-gated), and
  federation reads one connected:false as exited. The summary's proven-absence
  check now also consults the provider's sync hasPty.
- Ordinary per-tab cold parking (30s hidden) kept a non-group-active pane
  unmounted, so a sleeping record it owns under the new ownership predicate
  could not cold-restore until the user revealed the tab. Per-tab parks now
  exempt panes owning a sleeping-session record; worktree-level parks are
  untouched (they clear on activation).

* fix(terminal): reconcile the daemon session cache on inventory; scope the park exemption to consumable records

Round-2 review confirmed two holes in the round-1 fixes:
- DaemonPtyAdapter.hasPty is cached activeSessionIds membership, and a
  successful listSessions never removed ids the authoritative inventory
  omitted — an exit missed while the socket was down kept hasPty true
  forever, and the new spawn/list-race rescue would trust it, reopening
  connected-forever for that pty. listProcesses now drops pre-request cached
  ids the inventory does not list alive (ids spawned mid-flight are snapshot-
  protected).
- The park exemption covered records a pane can never consume
  (automaticResumeBlockedBy, passive-completed evidence), pinning hidden
  panes mounted indefinitely. The exemption now lives in
  sleeping-record-park-exemption.ts and requires a consumable record.

Also pins the web-mirror replacement's resume claim and startup command
(CodeRabbit round-2).

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 17:20:09 -07:00
OrcaWinandOrcaWin ac7f9a4fe1 fix(pty): require proven absence before retiring a stable pane owner (#12393)
* fix(pty): require proven absence before retiring a stable pane owner

"Session not found" only proves the provider we asked has no such PTY. A
degraded router answers unmapped session ids from the local fallback, which
never owned a daemon session, so a live agent's PTY produced the same error a
dead one does — and the pane was then given a synthetic exit, its durable
pane->PTY binding deleted, and a duplicate spawned. For a single-pane tab the
retirement also drops the tab from persisted state, which the renderer cannot
add back.

Gate the retirement on probePtyLiveness, which polls every possible owner and
answers false only when all of them agree the session is absent. `null` (nobody
could answer) is not absence. Providers without a probe are their own sole
owner, so their refusal stays authoritative and their behavior is unchanged.

Also move the retirement's durability barrier off writeToDiskSync: it fsyncs
the whole multi-MB state from the main thread, and a restore that retired N
dead panes paid that stall N times.

* fix(pty): make the unproven-absence veto transient and route attaches to the real owner

Review found the proven-absence gate correct in direction but terminal in
effect: "probe says alive" and "probe says unknown" both became a hard spawn
failure with no consumer, no retry, and a raw internal token in a toast.

- Graceful teardown (should-fix): a session being killed reports alive
  (getAppliedSize answers while isAlive, and isTerminating does not flip
  _state) while createOrAttach already throws SessionNotFoundError, so for up
  to KILL_TIMEOUT_MS the pane failed instead of retiring the doomed binding.
  The veto now re-proves on a bounded 6s budget: the moment any owner answers
  "absent" the pane retires and spawns fresh exactly as before.

- Unprovable owners (should-fix): DaemonPtyAdapter.probePtyLiveness never
  called ensureConnected, so a merely disconnected adapter answered null
  forever and one null poisons probePtyOwners; it also took no deadline, so a
  wedged daemon burned the client's 30s request timeout per attach. It now
  connects first (like listProcesses) and threads an absolute deadline through
  the fan-out. The thrown error is user-legible English instead of
  terminal_pane_owner_unverified.

- Durability barrier (should-fix): flushPendingOrThrowAsync is not a twin of
  flushOrThrow — it defaults to the drain-to-stable-generation loop the
  sibling best-effort caller deliberately avoids, adds active-view and GitHub
  sidecar writes, re-serializes state per iteration, and rejects when writes
  are frozen where the sync path no-ops. Reverted to flushOrThrow, matching
  the sibling retire paths in orca-runtime.

- Misrouting root cause (nit): DegradedDaemonFreshSpawnRouter.spawn sent an
  unmapped sessionId to the local fallback and DaemonPtyRouter.spawn sent it to
  the current daemon, both faking "Session not found" for a live session. Both
  now resolve an existing owner first (the same resolution every non-spawn path
  already uses); minted ids still route to the fallback/current daemon. The
  pane reattaches instead of only surviving.

Tests: retry-until-proven, teardown-window retire, misroute-then-reattach,
probe deadline threading, unconnected-adapter probe, and both routers' owner
resolution. Each fails with only the source reverted.

* fix(pty): drop the follow-up, keep the minimal proven-absence guard

Reverts aa27eb5274 and leaves only the original guard: retire a stable pane's
owner solely when a provider authoritatively answers absent
(probePtyLiveness === false). `true` and `null` still refuse to authorize
destruction, and providers without a probe keep their refusal authoritative, so
their behavior is unchanged.

The follow-up bought too much surface for a P0, and two of its pieces did not
hold up:

- The `hasPty` owner scan added to DaemonPtyRouter.spawn and the degraded
  fresh-spawn router is inert for the case it targeted. `hasPty` reads
  `activeSessionIds`, which only spawn/attach populate — `listProcesses`
  (daemon-pty-adapter.ts) never does. After a failed discovery the scan
  therefore cannot find the owner it was written to find.
- Its two routing tests could not fail: the mocks backed `hasPty` and
  `listProcesses` with one shared array, giving the fake adapter knowledge the
  real DaemonPtyAdapter provably lacks.

Also dropped: the bounded 6s retry loop and its constants, the sentinel symbol,
the exported user-facing message, the `opts.deadlineMs` widening of
probePtyLiveness and its threading through the probe/routers, and the
ensureConnected change in DaemonPtyAdapter.

Kept from the follow-up: the flushOrThrow revert. flushPendingOrThrowAsync is
not a like-for-like twin (drain-to-stable-generation loop, sidecar writes,
per-iteration serialize, rejects when writes are frozen), so
retirePersistedStablePaneOwner stays synchronous.

One gap the strict one-shot gate does open is closed here. `getSize` landed in
daemon protocol v18, and legacy adapters are created for every version in
PREVIOUS_DAEMON_PROTOCOL_VERSIONS (1..30). A pre-v18 daemon error-replies
"Unknown request type", probePtyLiveness catches it as `null`, and one `null`
makes probePtyOwners unprovable forever — so a genuinely dead pane could never
retire. probePtyLiveness now version-gates getSize and asks such a daemon for
`listSessions` instead, the same inventory legacy discovery already routes by,
requested directly rather than through listProcesses (which swallows errors into
an empty list) so an unreachable socket still answers `null`. Unreachable never
means absent; only an owner that answered does.

* fix(daemon): detect getSize support instead of inferring it from the protocol number

The version gate was not a sound boundary. `getSize` landed in 22b00a7cd2 while
PROTOCOL_VERSION was already 18 and that commit did not bump it, so version 18
means two different things: daemons built before it lack the request, daemons
built after it have it. Both report 18.

A daemon from that window therefore passed the `>= 18` gate, was sent `getSize`,
error-replied "Unknown request type", and answered `null` — and one `null` makes
the owner fan-out permanently unprovable, so a genuinely dead pane could never
retire. That is the same wedge the gate was added to close, just narrowed to one
build window.

Ask the daemon what it supports rather than trusting its number: attempt
`getSize`, and fall back to `listSessions` only when the daemon itself replies
that the request type is unknown. The predicate is deliberately narrow so a
transient failure stays unproven instead of being mistaken for a missing
capability, and the rejection is remembered so later probes skip a round trip
that cannot work. This mirrors the capability-probe rule AGENTS.md already
requires for Git, for the same reason: version numbers overstate capabilities.

Unreachable still never means absent.

* test(pty): pin that proven absence still retires, and correct a wrong rationale

Review found the guard had no positive-direction coverage: every existing retire
test uses a provider with no probePtyLiveness, so the guard is skipped there.
Mutating `!== false` into `!== undefined` — which turns the guard into a
permanent veto, so no daemon-backed pane could ever recover from a genuinely
dead owner — left the whole suite green. Pinned with a case that fails under
exactly that mutation.

Also corrects the fallback's Why-comment. It claimed listProcesses "swallows
errors into an empty list"; it does not — DaemonPtyAdapter.listProcesses
rethrows and the router fails closed. The error-swallowing lives in
discoverLegacySessions / discoverDegradedDaemonSessions. Requesting listSessions
directly is still right, but because a liveness probe should not publish
inventory audit observations as a side effect — not for the stated reason.

* fix(daemon): bound the liveness probe so a wedged daemon cannot stall a pane mount

probePtyLiveness passed no timeout, so each request fell back to the client's
30s REQUEST_TIMEOUT_MS. A wedged daemon holds its socket open rather than
refusing, so it answers neither quickly nor at all — and probePtyOwners awaits
every adapter before it can conclude anything. One hung daemon therefore stalled
each restoring pane for up to 30s before the pane reported that it could not
reopen.

Nearly all local terminals are daemon-backed, so this is the common
configuration, not an edge case.

Bound both probe requests to 2s. Answering "unknown" quickly is strictly better
here than answering slowly: unknown never authorizes retirement, it only defers
it, so the only thing a shorter budget can cost is an earlier retry. The tests
now pin the bound at the call rather than leaving it implicit.

* test(daemon): pin the getSize probe timeout, and reattach an orphaned comment

Review proved a coverage gap by mutation: removing LIVENESS_PROBE_TIMEOUT_MS
from the getSize request left the whole suite green, so nothing stopped that leg
regressing to the client's 30s default. Only the listSessions leg was pinned.
The assertion now fails under exactly that mutation.

Also moves isUnknownRequestTypeError below isDaemonGoneError. It had been
inserted between isDaemonGoneError and the comment describing it, so that
comment read as documenting the wrong function.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 23:48:18 -07:00
NeilandOrca a6b14eb04c fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112) (#12202)
* fix(terminal): reset stale mouse tracking on cold restore (#12101); stop OSC color-reply echo leak in POSIX agent panes (#12112)

#12101: a force-killed TUI never emits its DECRST reset, so its armed mouse
mode is latched into the on-disk checkpoint and re-derived into the
replacement process's emulator via the cold-restore history seed -- through
both rehydrateSequences and SerializeAddon's own mode trailer. The revived
bare shell then echoed SGR motion reports at the prompt. Seed a
RESET_MOUSE_REPORTING segment after the snapshot (before the torn escape
tail), only when there is real recovered content so the empty-array
"nothing to recover" sentinel survives.

#12112: agent panes arm a main-side PtyStartupIngress that answered opencode's
startup OSC 10/11 queries synchronously inside node-pty's onData, while the
POSIX tty still had ECHO on. The line discipline echoed Orca's own reply back
out as visible text. Echo suppression existed but was gated on windows-conpty.
Add PtyStartupReplyDelivery: POSIX defers the write off the query's turn and
recognizes its own echo anywhere in a span (bounded, non-destructive); ConPTY
keeps its synchronous write; windows-wsl is byte-identical to before.

Fixes #12101

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): read the slave's ECHO bit before answering a color query

The startup color reply was written into a PTY still in cooked mode, so the
line discipline echoed it back as visible junk (#12112). Whether that will
happen is readable state on the slave rather than something to infer from
returning bytes, so the reply now waits until the ECHO bit is observably
clear instead of guessing at echo shapes.

Two echo sources exist and only one is readable. A `quiet` verdict proves
the kernel will not echo, so it retires the caret projection; readline
echoes a master write in software with the tty already raw, so that
projection stays armed on every path. Scoping `quiet` narrowly is the whole
correctness argument here: reading it as "no suppression needed"
reintroduces the bug at a plain shell prompt.

Polling is bounded by a wall-clock budget rather than an attempt count,
because each probe is a subprocess and a multi-pane restore serializes them
on fork. Withholding measures flat at ~210ms from 1 to 100 panes.

Also resets a cold-restored pane's mouse reporting (#12101). The armed mode
is re-derived from the dead process's own persisted bytes through two
channels, so the daemon seeds a reset into recovered history and the
renderer stops trusting a persisted "live agent" signal after a cold
restore. The reset literals move to one shared profile module.

Fixes #12101
Fixes #12112

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the cold-restore reset on the spawn-adopted reattach path

A spawn can be answered with an adopted session, which reaches the reattach
handler by a door that skips the restored-session path. Pin that the cold-restore
signal survives it, so #12101's junk cannot come back through it.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): note why the adopted-reattach snapshot leaves the cursor visible

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): harden startup reply delivery

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-03 21:00:57 -07:00
50594c55a9 Stop the Windows Orca CLI from crashing when the environment carries both PATH and Path (#12218)
* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment

The packaged Windows `orca.exe` launcher read
`ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the
case-sensitive process block into a case-insensitive dictionary via `.Add`.
An inherited block carrying both `PATH` and `Path` threw
`ArgumentException: Item has already been added. Key in dictionary: 'PATH'`,
so every `orca` invocation exited 1 before Electron started
(native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67).

The launcher now mutates its own environment with
`Environment.SetEnvironmentVariable` and never touches either
`ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment
block and the child inherits the live one verbatim.

Orca was also minting the duplicate itself. `applyTerminalAttributionEnv`
read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows
PTY that inherited `Path` got a second spelling; which one the child resolved
was non-deterministic. `createLaunchEnv` did the same and, because its read
always missed on Windows, shipped Agent Teams terminals a `PATH` containing
only the tmux shim dir.

`resolvePathEnvKey` (extracted from the existing precedent in
windows-environment-path.ts) now drives every PATH read and write in the PTY
env pipeline, and attribution collapses Windows onto the single OS-resolved
spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior
is unchanged and a case-sensitive POSIX `Path` variable is never touched.

Closes #12046

* test(windows): track the launcher's own-environment marker

The #12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND
off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the
old dictionary writes and failed.

Co-authored-by: Orca <help@stably.ai>

* fix(windows): follow the host block's PATH spelling on sparse daemon env patches

Resolving a path-less Windows env to `Path` handed the daemon's own
`{...process.env, ...opts.env}` merge both spellings when the host block spelt
`PATH`. Fall back to the host block's own key, and collapse again inside the
daemon since that merge happens after attribution.

Co-authored-by: Orca <help@stably.ai>

* fix(windows): resolve the live PATH spelling by block order, not casing

Win32 resolves a duplicated variable by taking the first case-insensitive
match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first
preference targeted the shadowed spelling on the reporter's own
`["PATH","Path"]` block. Drop the attribution-side collapse with it: it
deleted the other spelling's value, and deleting the live key promotes
the shadowed one, so an env that stripped down to empty lost both.

* chore: drop unrelated merge formatting

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 20:45:23 -07:00
BingZ 79d3c847bd fix(runtime): attribute destructive close requests (#12238)
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions.
2026-08-03 20:35:40 -07:00
Brennan Benson 49dc113a0f Fix terminal corruption after restored snapshot replay (#12363)
* fix(terminal): preserve restored snapshot fidelity

* test(terminal): align legacy history handoff snapshot expectation

* fix(terminal): keep legacy snapshot panes mounted

* fix(terminal): refresh snapshot capability after startup

* fix(terminal): refresh snapshot capability in degraded startup

* fix(terminal): await snapshot provider authority
2026-08-03 20:00:28 -07:00
Brennan Benson 13f033f091 chore(daemon): disambiguate audit observations (#12343)
* chore(daemon): disambiguate audit observations

* fix(daemon): reject future audit protocol roles

* fix(telemetry): protect daemon audit observations
2026-08-03 16:28:49 -07:00
OrcaWinandOrcaWin 25213ec04d test(terminal): harden daemon upgrade adoption (#12353)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 15:03:37 -07:00
OrcaWinandOrcaWin e5174a452b fix(terminal): reattach panes across daemon protocol upgrade (#12351)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 14:19:44 -07:00
Brennan Benson 866bcda465 fix(terminal): recover degraded daemon spawn routing (#12277)
* fix(terminal): recover degraded daemon spawn routing

* fix(terminal): preserve fresh-session recovery semantics

* fix(terminal): avoid retaining exited recovery sessions
2026-08-03 12:47:41 -07:00
OrcaWin cd68a8b00c fix: preserve live agent PTYs through graph hydration (#11789) 2026-08-03 11:11:14 -07:00
Siddiqui QamarandOrcaWin f3c824bc28 fix(terminal): expand environment variables in Windows PATH (#11987)
* fix(terminal): expand variables in Windows PATH

* fix(terminal): preserve expanded Windows PATH at spawn

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 01:00:58 -07:00
NeilandOrca 73c5009b82 chore(dead-code): drop ~2k lines of unreachable exports and orphan modules (#12077)
* chore(dead-code): drop 2k lines of unreachable exports and orphan modules

Ran knip across every build entry (main, preload, renderer, popout, web,
cli, relay, workers, forked sidecars, config scripts) and removed what no
entry graph can reach.

- 11 orphan modules nothing imported, plus one test that only covered them
- 159 unused exports/types, with their now-dead helpers, imports and tests

Each candidate was verified against dynamic references before deletion.
42 knip hits were false positives and are kept: shared modules consumed by
the mobile/ workspace, the src/shared/plugins/** public API, vendored
shadcn primitives, and relay wire-protocol constants held for compatibility.

Adds knip.json + `pnpm audit:dead-code` so this stays measurable.

Verified: pnpm typecheck, pnpm lint, and 2081 tests across the 73 affected
test files all pass.

* chore(dead-code): move knip config under config/

Root-level additions are blocked by the root directory guard.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-02 00:33:57 -07:00
Neil 16c5526dfd fix(daemon): cover in-flight sleep in PAM watch (#11921)
* fix(daemon): rebaseline in-flight PAM suspension

* test(activity): await portal readiness commits
2026-08-01 03:35:34 -07:00
Neil 3a70078ab9 fix(daemon): prevent PAM rejection restart cascades (#11911)
* fix(daemon): back off transient PAM rejection retirement

* fix(daemon): rebaseline PAM evidence after sleep
2026-08-01 02:36:49 -07:00
Jinjing ad1e58d966 chore: declutter top-level repo layout (#11890)
Remove one-off incident docs and committed test-results noise, move
dev/repro/bench tools under tests/tools, and relocate i18next config
into config/ so the GitHub root scrolls to the description faster.
2026-08-01 00:25:35 -07:00
Brennan Benson 402e49203d fix(codex): keep persistent panes logged in after home routing (#11720) 2026-07-31 18:33:26 -07:00
NeilandOrca bc6a6e9ff0 [P2] fix(daemon): keep audit-only evidence off the main thread and out of false negatives (#11695)
* fix(daemon): keep audit-only evidence off the main thread and out of false negatives

Three audit-only daemon evidence defects:

- The Windows CIM probe emitted the "absent" marker whenever $p was empty, so a
  non-terminating Get-CimInstance failure (Winmgmt down, corrupt WMI repository,
  access denied) exited 0 with {"exists":false} and was recorded as proof the
  daemon process was gone. The script now sets $ErrorActionPreference and reports
  query failure explicitly; only a query that ran and found nothing yields
  'missing', everything else stays 'unavailable'.
- probeMacosProcess read the process start time through execFileSync('ps') on the
  Electron main thread. It now uses the async readMacosProcessStartedAtMs, matching
  the async command-line sibling in the same probe.
- daemon_audit_eligibility fired on every successful inventory, so routine
  listProcesses traffic could drain the shared 1,000-event per-session telemetry
  ceiling. Repeated identical observations now collapse to one heartbeat per
  5 minutes; a changed observation still emits immediately.

The evidence stays strictly audit-only — no lifecycle, routing or recovery path
consumes the observation.

Co-authored-by: Orca <help@stably.ai>

* fix(daemon): take the linux audit start-time read off the main thread

The linux branch of the audit-only evidence probe still bottomed out in
getProcessStartedAtMs, i.e. two readFileSync calls plus an
execFileSync('getconf', ['CLK_TCK']) spawn on the Electron main thread —
the same hazard the darwin half of this change removed. The `async`
wrapper hid it from the type signature and from the main-thread
regression test, which only covered darwin.

That path is reached whenever the pid record carries no start ticks or
boot id: legacy bare-integer pid files (the upgrade population this audit
exists to observe) and any host where /proc boot-id or start-tick reads
fail.

readLinuxProcessStartedAtMs now reads procfs asynchronously and caches
CLK_TCK for the process lifetime (it is fixed for the kernel's lifetime),
retrying only after a failed probe. The main-thread test now covers linux
with a ticks-less pid record.

Co-authored-by: Orca <help@stably.ai>

* fix(daemon): measure the audit heartbeat window on a monotonic clock

A backward wall-clock jump (NTP correction, VM resume) made elapsed time negative, which read as "still inside the window" and suppressed daemon_audit_eligibility heartbeats until wall time caught up. Rate-limit arithmetic now runs on performance.now() and treats any backward movement as eligible.

Co-authored-by: Orca <help@stably.ai>

* fix(daemon): guard the audit rate limiter and scope the CLK_TCK cache

The rate-limited tracker is the only production emitter, but its payload
serialization and clock read sat outside the guard that keeps audit
telemetry from affecting daemon availability — a throw there propagates
out of listProcesses' try and turns a successful inventory into a
reported inventory failure.

Also key the CLK_TCK cache by the runner that produced it: the value
belongs to the host executing getconf, not the module.

Co-authored-by: Orca <help@stably.ai>

* test(daemon): make audit CI synchronization deterministic

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-31 04:56:14 -07:00
Brennan Benson 90692ba40b fix(daemon): preserve audit evidence polarity (#11626) 2026-07-30 17:07:40 -07:00
Brennan Benson d4cfee76be Add audit-only daemon incarnation evidence (#11606)
* feat(daemon): add audit incarnation evidence

* fix(daemon): isolate audit evidence observers
2026-07-30 16:02:08 -07:00
NeilandOrca ab2b517cf9 perf(terminal): serialize checkpoints with one payload walk (#11422)
* perf(terminal): serialize checkpoints with one payload walk

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): bound checkpoint serialization

Co-authored-by: Orca <help@stably.ai>

* test(terminal): correct bounded serialization proof

Co-authored-by: Orca <help@stably.ai>

* test(terminal): cover over-limit multibyte checkpoints

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-30 01:14:12 -07:00
NeilandOrca 37af457752 fix(daemon): split router subscription fanout (#11490)
Co-authored-by: Orca <help@stably.ai>
2026-07-30 00:49:40 -07:00
Jinjing a60aa85592 fix: make remote server pairing failures actionable (#11510)
* fix: make remote server pairing failures actionable

* refactor: extract daemon router event types

* fix: address remote pairing review findings

* fix: address final remote pairing review feedback
2026-07-30 00:31:34 -07:00