mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
fb52c0602a33bdb755d5da5f9a34bb77defffa03
457
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fb52c0602a |
fix(terminal): release xterm's DEC 2026 render hold instead of waiting out its 1s timeout (#23920)
* fix(terminal): release xterm's DEC 2026 render hold instead of waiting out its 1s timeout
xterm paints nothing while DEC mode 2026 (synchronized output) is open and only
force-flushes after 1000ms. Codex wraps every draw in mode 2026, so any byte gap
or chunk split that loses the closing \x1b[?2026l freezes the pane for a full
second and then repaints in one burst.
Orca never emitted \x1b[?2026l anywhere, and three paths could destroy a TUI's:
the per-PTY pending cap drops buffered output wholesale (mode 2031 was already
salvaged there, 2026 was not), main sliced pending data at a blind 16KB offset
that can land inside an open frame or sever the 8-byte marker, and the renderer's
backlog warnings replace a queued tail that may hold the close.
- salvage the 2026 latch across dropped output, mirroring the existing 2031
salvage, and append the release on both delivery sites
- ground 2026 in RESET_AFTER_BYTE_GAP and the replay baseline, and in both
backlog warnings, so every drop path is self-healing
- make main's 16KB flush split frame-aware instead of a blind byte offset
- lift the synchronized-output scanner into shared/ so main and the renderer
use one implementation
Closing a frame early costs one premature repaint; leaving it open costs a
second of blank screen, so the asymmetry favours always closing.
Also adds the reproduction this needed: the pre-existing typing bench observes
the xterm BUFFER, which the parser fills while rendering is held, so it scored
these freezes as fast echoes.
* fix(terminal): stop the renderer's queue drain cutting inside an open DEC 2026 frame
takeQueuedChunk sliced a queued chunk at a blind byte offset to fit the 16KB
coalescing budget, which can strand a frame's closing \x1b[?2026l in the residual
until a later drain. Same defect as main's flush split, same fix: reuse the
frame-aware split helper.
Usually masked because the drain coalesces adjacent chunks and reassembles what
main split, but not when the budget boundary falls inside a frame.
* fix(relay): keep the SSH path's bounded slice outside an open DEC 2026 frame
pty-handler split pending output at a byte offset with a surrogate-pair guard but
no synchronized-output awareness, so a frame straddling the 16KB wire slice had
its closing \x1b[?2026l stranded in the remainder — the same defect just fixed on
the local path, on the path AGENTS.md requires us to consider.
Placed before the surrogate guard so that guard keeps the final say, and floored
at 2 so frame alignment can never walk a healthy slice into the guard's
decrement and then into the chunkChars <= 0 pause-and-retry path.
Also drops a dead `splitAt === 0` branch in takeQueuedChunk: both callers pass a
positive limit and the helper never returns 0 for one.
The two new split tests were each confirmed to fail without their fix.
* test(terminal): sweep the DEC 2026 split helper over escape-sequence shapes and every limit
Covers OSC 52, DCS, repeated open/close markers and limits 1..len+3, asserting the
result never exceeds the limit, never reaches 0, and stays byte-exact. Also pins
that a buffer beginning inside an open frame degrades to the blind offset rather
than doing something worse, and documents that callers do not thread latch state.
* fix(terminal): ground DEC 2026 on the daemon slice, the recovery replays, and the process boundary
Four more sites could strand the latch, found by sweeping every path that drops,
splits, or replays terminal bytes.
- daemon-stream-data-batcher: the 64KB bulk-write slice used a surrogate-only
clamp, and its remainder is HELD until 'drain' — "seconds for multi-MB
backlogs" per the file's own note. A frame straddling that boundary parked its
\x1b[?2026l behind the hold, blanking the pane past xterm's 1s timeout once per
frame for as long as the backlog lasted. This is the default daemon-backed pane
path, so it is the one users actually hit. The new
clampToSafeBulkWriteSplitIndex frame-aligns first and surrogate-clamps last,
and lives in daemon-stream-data-split alongside the policy it belongs to.
- replay-data-drain and remote-runtime-terminal-binary-snapshots wrote a bare
\x1b[2J\x1b[3J\x1b[H, which does not clear mode 2026 — so on the SSH/remote
reconnect path, the very event most likely to sever a frame, the whole replay
could paint nothing.
- ipc-pty-attach: trimIncompleteTerminalControlTail can cut a half-written
\x1b[?2026l while its opening marker survives in the replayed prefix.
- PROCESS_BOUNDARY_GROUND: the "process that armed these modes is gone" ground
omitted 2026, the last unexplained gap in that file. A disable, so it still
satisfies the recovery barrier's ownership scan (only ?25h may be an enable).
Recovery-path expectations updated where they pin the emitted bytes. Deliberately
NOT touched: apply-reattach-payload and ssh-snapshot-prepaint already ground via
buildSnapshotReplayPrologue.
Still unfixed, deferred with reason: terminal-output-frame-chunks.ts splits the
remote wire on accumulated UTF-8 byte width and needs a different shape than the
char-index helper; desktop clients reassemble in main's pending buffer, so the
exposure is mobile/web only.
* fix(terminal): emit the DEC 2026 release before the mode-2031 tail, and stop claiming the drop path writes it
Two corrections from adversarial review of the earlier commits.
1. Ordering bug I introduced. getDroppedMode2031RendererData ends with
`state.tail`, which extractPrivateModeScanTail deliberately retains as an
INCOMPLETE private-mode sequence so the next chunk can resolve it. Appending the
2026 release after it put an ESC behind a dangling CSI, aborting it and silently
losing whatever mode spanned the drop boundary. The release now goes first.
2. The drop-path release does not reach xterm in the dominant case, and the comment
now says so instead of implying otherwise. live-data-callback's droppedOutput
branch discards `data` and salvages only queries
(salvageRendererQueriesFromDiscardedRestoreData handles CPR/DA1/OSC colour;
\x1b[?2026l is not a query), so for hidden panes and visible panes outside
foreground-restore backpressure the synthesized release was dropped. The grounded
snapshot replay releases the latch instead.
I tried writing it through writePtyOutputToXterm there and reverted: it consumes
the pending hidden-output snapshot and broke
pty-connection-hidden-snapshot-resize-signals ("re-restores a skipped alt frame"),
so the release rides the restore rather than perturbing that state machine.
Residual gap, documented: a cap-dropped pane whose restore never arrives.
The salvage is still load-bearing on the fall-through path, so it stays.
* fix(terminal): release DEC 2026 on the reattach clears, floor the split, and correct the freeze framing
Remaining findings from adversarial review.
- apply-reattach-payload's three bare-clear branches (:63 daemon snapshot, :229
relay replay, :269 cold restore) had no release anywhere in their sequence: I
checked all seven POST_REPLAY_* profiles reachable via chooseReattachReplayReset
and none contains \x1b[?2026l. Only the buildMainModelSnapshotReplayWrites branch
was grounded, so covering the streamed replay path and not the main reattach path
was inconsistent. Verified no production code matches these clear strings — the
three test updates are mock equality, and each was confirmed to fail without the
source change.
- clampToSafeBulkWriteSplitIndex could return 0 (('\u{1F600}aaaa', 1) — alignment
returns 1, the surrogate clamp decrements to 0), which would leave a zero-length
slice that never shifts the batcher's queue entry and spin its drain loop.
Unreachable from today's only caller, but it is exported with an unstated
precondition. Floored at 1.
- Frame alignment could halve per-PTY flush throughput: main re-queues the
remainder with eligibleRound = round + 1, so the shortfall cannot be refilled in
the same round, and aligned size is floor(W/F)*F — 50% worst case in the 8-16KB
band, which is exactly the full-screen redraw burst that reaches the pending cap.
Alignment is now rejected below half the window, preferring throughput and
letting the reset profiles release the latch.
Framing corrected throughout: bufferRows records a row range and clears nothing, so
the pane freezes on its last painted frame — it does not go blank. The real trade is
"stale but coherent for <=1s" versus "immediate partial frame", and
RESET_AFTER_BYTE_GAP (written alone, with no repaint behind it in the same write) is
the one site that can newly flash a partial frame. Said so at the constant instead
of implying the release is free.
* fix(terminal): rename the shape-flagged symbols the anti-slop audit rejects
CI's anti-slop gate rejects "shape" in symbol names as structural rather than
domain language: `shapes` -> `outputSamples`, and
`writeCodexShapedEchoProbeScript`/`codexShapedEchoProbeScript` ->
`writeCodexEchoProbeScript`/`codexEchoProbeScript`.
|
||
|
|
fb67d5d7c3 |
fix(runtime): stop a busy Codex 0.150-0.157 pane reading as tui-idle (#23805)
* fix(runtime): stop a busy Codex 0.150-0.157 pane reading as tui-idle The startup header box (OpenAI Codex / model: / directory:) stays on screen and in the tail for the whole session, so as tier-1 evidence it settled tui-idle mid-turn. For a codex pane it now counts only in the quiet lane, held to the same quiescence as the composer. * fix(runtime): keep a restored Codex pane's header as tier-1 readiness A restored or reattached pane has no lastOutputAt, so the quiet lane that now holds a Codex header can never fire and the wait sat pending until timeout, where main settled it. Gate the Codex tier-1 veto on the output clock rather than the agent name, and share one settled-prompt helper. * test(runtime): read no screen in the restored Codex pane test * test(runtime): pin the clock in the clockless Codex header cases * test(runtime): name the screen-readiness comparison for what it proves |
||
|
|
ad2e1b5efa |
fix(terminal): restore the mouse format with mouse tracking, so phone swipes don't type into Codex (#23946)
* fix(terminal): restore the mouse encoding with mouse tracking in every snapshot Swiping to scroll Codex from the phone on a Windows host typed legacy `ESC [ M` mouse reports into the Codex composer (#23818). SerializeAddon re-arms mouse tracking (?1000h/?1002h/?1003h) but never the SGR encoding (?1006h/?1016h). Any snapshot taken from a desktop pane's xterm (the runtime seeds its headless model from it after a reattach, and serves it to remote viewers when no model exists) therefore restored "tracking on, legacy encoding", and the phone encoded wheel events as X10 bytes, which ConPTY hands to Codex as keystrokes. serializeWithAbsoluteCursor, the one wrapper every Orca snapshot producer uses, now appends the encoding xterm itself parsed, read from xterm's mouse state service. The daemon/runtime headless model reads tracking and encoding from xterm too, so its regex mirror of the DECSET stream is deleted (one source of truth; one less regex pass per PTY chunk). Mixed versions: no wire field changes. A new host's snapshot carries an extra DECSET that old desktop and phone clients already parse; an old host's snapshot restores exactly as before. With tracking off the encoding alone sends no reports, so the wheel still scrolls scrollback. * test(terminal): pin the mouse-encoding read against the renderer xterm build * fix(terminal): type the xterm mouse-state read behind named shapes |
||
|
|
59ef74876f |
fix(terminal): the terminal's owner answers colour queries for the terminal's whole life (#23925)
* fix(terminal): the PTY owner answers OSC 10/11 for the terminal's whole life Codex and Claude's `theme: auto` ask the terminal for its foreground and background colours (OSC 10/11) and pick their colours from the reply. Orca answered in the process that owns the PTY only for agent launches and only for 5 s; after that the query was handed to whichever viewer was attached. On Windows ConPTY the owner kept swallowing the query but stopped answering it, so a Codex started from an older shell tab lost its message shading (#22332). On a headless `orca serve` host no viewer existed yet, so a Codex started before anyone attached got no reply at all (#22500). The owner (in-process provider, terminal daemon, SSH relay) now answers every OSC 10/11 query for the PTY's whole life and strips it, so no downstream view ever sees one to answer twice. It answers from, in order: the host-wide viewer theme pushed to that process, the creating viewer's colours sent at spawn (now for every PTY, not only agents), and Orca's default dark theme. The desktop pushes its renderer theme to every owner on change and on (re)connect: a daemon request gated on protocol v38, and an SSH relay notification that older relays ignore. The answer-once rule, the 5 s colour window and the colour-authority handoff are removed; Kitty keyboard queries keep their startup window. Viewer-side answerers (renderer xterm, main's hidden-pane model responder, the mobile webview) stay as the fallback for older owners, which still hand queries off; they are never reached for a new owner. * fix(terminal): answer OSC 10/11 with the colours the pane is really painted with Review follow-ups to the lifetime PTY-owner colour answerer. - The theme catalog moves to src/shared so the renderer and the PTY owners read one source; the owner's last-resort default is derived from it rather than copied. - Main seeds every owner from the host's saved theme settings (light or dark, custom themes, colour overrides) at startup, so a headless host and a desktop pane that queries before the renderer's first push are not told dark to a light-theme user. The renderer's push replaces it. - Colours an app sets with OSC 10/11, and clears with OSC 110/111, are tracked per terminal and reported back, as a viewer paints them; a theme change drops them, as a viewer's theme apply does. - A terminal a paired client created with its own colours answers with those, not the host's theme (`colorSource: 'remote-viewer'` on the spawn intent), so a light client on a dark host is told light. - After the 5 s startup window a reply's echo is watched for 512 bytes instead of 256 KB, and a torn query candidate is released after 500 ms rather than held indefinitely. * fix(terminal): keep the long echo watch for relayed replies; one theme lookup The 512-byte post-startup echo watch now applies only to replies the PTY owner produced itself. A viewer's reply relayed through answerLiveQueryReply keeps the 256 KB watch, because a cooked-mode app can keep printing after it queries and the echo then trails that output. The renderer's getTerminalTheme now calls the shared lookupTerminalTheme, so the custom-vs-built-in theme lookup exists once. * perf(terminal): scan colour overrides in one pass over each PTY chunk Two indexOf searches per OSC went quadratic on long runs of ST-terminated hyperlinks, and the tracker now sees every chunk of every terminal. * fix(terminal): one host viewer colour value, set by whichever viewer acted last A paired client's colours reached the host only as frozen spawn colours on terminal.create, tagged remote-viewer. UI-started agent sessions on a headless host answered OSC 10/11 with the host's saved theme, and a client's theme flip never reached panes it had created. The host now holds one viewer colour value that every PTY owner answers with. The desktop renderer's push, a window focus on the host, the new terminal.setViewerColors RPC, and terminal.create colours from older clients all set it; equal values do not re-notify daemons or relays. The remote-viewer tag (colorSource / terminalColorQuerySource / spawnFromRemoteViewer) is gone; it never shipped in a release. * fix(terminal): paired clients push their terminal colours on connect, change and focus The renderer publisher now hands each published fg/bg to subscribers. A new remote-runtime-terminal-color-push module calls terminal.setViewerColors on every host this client is connected to when it connects (or the host restarts), when the colours change, and when the window gains focus. A host that answers method_not_found or forbidden is not asked again until it reconnects. The app shell also republishes terminal view attributes on settings and system theme changes, so a theme change reaches main and paired hosts with no terminal pane open. * fix(terminal): a host with its own window answers OSC 10/11 with its own theme Round 1 kept one host-wide viewer colour value set by whichever viewer acted last, so a paired client's push (reconnect after sleep, a dusk theme flip) took over the host desktop's own panes until its window regained focus. The value is now derived: this host's renderer colours when a local window has pushed, otherwise the last paired client's push (terminal.setViewerColors or terminal.create colours), otherwise the saved theme. Only a headless host takes a client's theme. The window-focus reassert and the identical-re-push takeover are gone; owners are notified only when the derived value changes. * perf(terminal): scan only OSC starts for colour queries once the Kitty window closes The PTY owner answers OSC 10/11 for the terminal's whole life, and it tried every ESC as a query start: a 240 KB SGR-heavy read cost about 2 ms and 256 KB of bare ESC about 15 ms, long after startup. Once the Kitty query window closes only an OSC colour query can match, so the scan jumps between ESC ] starts, plus a trailing lone ESC so a query torn right after its ESC still resolves on the next read. Output and replies are unchanged. |
||
|
|
26bb7c23f1 |
fix(terminal): run Orca's cmd.exe, path-named and setup-gated Codex launches without the shared server (#23933)
* fix(terminal): give plain shells and cmd.exe Codex launches --no-daemon Plain bash, zsh and fish tabs were never wrapped, so a typed codex skipped the shell function that adds --no-daemon. Wrap them (bash keeps its prompt and DEBUG trap untouched unless Orca asked for command markers), add --no-daemon host-side where no function can run (cmd.exe, path-named binaries), and move new tabs to a v38 terminal daemon so they get the new wrappers. * fix(terminal): keep plain bash a login shell and give the setup gate the codex function Plain bash and Git Bash tabs launch exactly as before again: the rcfile wrapper would have made every one a non-login shell. Plain tabs on the user's configured shell args stay unwrapped on both transports. The wait-for-setup gate's bash -lc now defines the codex function, so a sequenced Codex launch gets --no-daemon from the binary it actually runs. * fix(terminal): define the setup gate's codex function after setup finishes Setup can be what puts codex on PATH, so defining the function before the marker wait found no binary and skipped --no-daemon. * refactor(terminal): fold the SSH/WSL guard into the Codex launch planner and bound the gate test * fix(terminal): honour the pane's env deletions in the Codex opt-out check Also pin the setup-gate test's fake codex ahead of path_helper's PATH. * revert(terminal): launch plain zsh and fish tabs exactly as on main Drops the always-wrap for plain zsh and fish, the configured-args guard that only served it, and the v38 daemon bump: the daemon's launch configs and generated wrappers are byte-identical to main again. Keeps the host-side --no-daemon for cmd.exe and path-named launches and the setup gate's codex function. |
||
|
|
7980ab9942 |
test: remove mock echoes and duplicate contracts that only reading finds (#23953)
* test: remove mock echoes and duplicate contracts that only reading finds
Two veins in one wave, both requiring the production path to be read rather than
pattern-matched.
Mock echoes (36 strongest candidates reviewed, 2 real): the flagged shape —
literals shared between a mock factory and an expect matcher — is almost always
a test feeding an input and asserting a transform. The two genuine echoes are in
`pty-management.test.ts`, where the handler returns
`getDaemonFolderAccessMismatch(identity)` verbatim, so asserting the mock's own
`evidence('allowed')` object and its `null` proved only the mock. One case's own
comment conceded the handler makes no decision. The branch-exercising cases in
that file stay.
Semantic sweep of 80 files no detector flagged, 27 junk cases removed. What it
found has no mechanical signature:
- a handler that is literally `() => getComputerUsePermissionStatus()`, so
`resolves.toBe(result)` guarded nothing;
- "does not mutate a stale registration off Linux" whose refusal came from a
DIFFERENT guard — `cli.ts` has no platform check, and the test's own mock made
`resolveAppImageRuntimeIdentity` return null, which a sibling case already owns;
- "keeps probing a host that is still retained" asserting a no-op, because
`retainScopes` only cancels queued probes and stores no state;
- two cases comparing against `referenceAllowedRoots`, a verbatim copy of the
pre-change algorithm kept in the test file — expected values produced by the
thing under test. The third such case stays: it calls the old algorithm to
COUNT its work (100_000 containment checks vs 100), a real bound on the
authorization hot path;
- a remote-folder refusal that came from the fake provider's own rejection
message rather than any Orca guard;
- "advertises each capability once", where a duplicate entry is inert in
production because membership is `includes`;
- an Antigravity `scaffold self-check` built on a hand-typed five-line screen —
the exact fixture shape docs/reference/antigravity-readiness-evidence.md blames
for five failed detector attempts — whose banner had already drifted to
`Antigravity CLI 1.0.3` against a real captured `1.2.0`. The raw-capture
provenance guard and the transcript checklist ratchet in that file stay.
No production file is touched and no test file is deleted.
* test: retire duplicate daemon and filesystem cases the sweep found
Continues the semantic sweep into src/main/ipc filesystem handlers and
src/main/daemon. 16 cases removed across 13 files; no production file touched.
The recurring shape is a case that reaches the same branch as its sibling by a
different-looking route:
- `parseArgs` is a flag-scanning loop, so "handles flags in any order" asserts the
identical result object as the in-order case, and "throws with no args" lands on
the same `Usage:` throw the two missing-flag cases already reach;
- an ENOENT case with "no code at all" and its sibling with a numeric transport
code both fall through to the same `ENOENT_MESSAGE.test` branch;
- "establishes connection with hello handshake" and "receives stream events" are
strict subsets of cases that require two matching authenticated sockets and a
frame split inside a multibyte character.
Two were vacuous rather than duplicated: a fixture self-comparison asserting
`String.normalize` gives different NFC and NFD spellings, and a
`not.toThrow` batch case whose 200k events are truncated by
`MAX_BATCHED_WATCHER_EVENTS = 5_000` long before the argument spread it was
written to exercise.
One whole-file deletion was reversed: `freebuff-detection.test.ts` looked like a
table restatement, but the sibling detection test covers only dependency ordering
and platform gating, not freebuff/codebuff independence — and those two names
share a suffix, so it is the only guard against one shadowing the other.
|
||
|
|
8f4a740b90 |
fix(daemon): roll Codex no-daemon shell launch into a fresh v37 daemon (#23907)
Terminal daemons survive app updates, so new tabs keep spawning from the old v36 daemon and never get #23900's Codex shell function. Bump to v37 so new tabs move to a fresh daemon; v36 owners stay attachable. |
||
|
|
9420d49bcb | fix(terminal): run Codex in Orca terminals without the shared background server (#23900) | ||
|
|
31012aeb09 |
test: remove assertion-free probes, copied inventories and export-shape checks (#23816)
Second audit wave, targeting three more junk patterns: - assertion-free cases that run code and assert nothing, so they pass no matter what the code does; - inventory literals re-typed from a production declaration, where the only way the assertion can fail is someone editing one of the two copies; - export key-set and export-shape loops (`typeof x === 'function'` over every export) that restate what TypeScript already enforces. Yield is much smaller than wave 1 on purpose: the assertion-free scanner has a high false-positive rate, because many flagged blocks assert through a shared helper or their oracle is "this must not throw". Those were kept. `mobileWebCheckArgs` in `config/scripts/run-mobile-web-app-checks.mjs` is de-exported — after the inventory comparison went away, nothing outside the module read it. |
||
|
|
36c473ea1a |
fix(runtime): wait out Codex 0.157's startup screen, and stop at Codex's startup dialogs, before typing a worker brief (#23745)
* fix(runtime): wait for Codex's live chat before typing a worker brief Codex 0.157 draws a provisional startup screen (header reads model: loading) and discards typed input while it starts its shared daemon behind it; a fresh Codex home makes that window seconds long, so worker-start pasted briefs that were truncated or never submitted. Codex 0.158 dropped the header labels Orca matched, so worker-start stopped seeing Codex as ready at all. Readiness now requires Codex's live chat on both layouts: the provisional header vetoes a text match unless the live status row is already painted, and 0.158's greeting layout counts once that status row appears. Codex 0.158's model announcement dialog is reported as a blocked prompt instead of receiving the brief. * fix(runtime): recognise Codex's provisional screen from the text copy and the screen probe Live worker-start on a fresh Codex 0.157 home still typed during the daemon start: Codex leaves its alternate screen for that window, so the live screen showed no header and the screen-based veto never fired. The text copy keeps the provisional header until the live chat paints its status row, so the veto now reads it there. The tui-idle visible-screen probe used the bare text rule on the rendered screen; it now goes through the same body rule. * test(daemon): register the new Codex captures' known serializer divergences The serialize round-trip replay picks up every fixture under runtime/__fixtures__, and the three new Codex 0.157/0.158 captures showed 48/9/8 "new-fail" checkpoints against an expected 0, turning CI red. They are the existing live-pen colour leak on restored cells, the same class as the other Codex and DSH entries; this branch changes no serializer code. * fix(runtime): keep Qoder off the Codex screen probe change; drop an unbacked row filter - The tui-idle visible-screen probe now classified Qoder panes with isQoderComposerReady, which skips the working veto evaluateTuiIdle applies first. Qoder paints its composer mid-turn, so an adopted Qoder pane whose hooks said "working" settled the wait immediately. Only Codex and unknown panes take the body rule there; every other agent keeps its old verdict. - The live status-row check skipped rows containing "waiting for startup", a string Codex 0.157/0.158's TUI never prints. The line-folded text copy keeps a whole screen on one line, so the filter could only ever veto the real status row. It is now a bounded includes() with no split. - Lowercase the wait text once in isKnownReadyPromptBody. - Restore the per-frame "screen never takes a settled header away" check, guarded on the provisional veto, instead of checking the final frame only. * fix(runtime): stop reporting Codex 0.158's model announcement once it is answered The announcement's choices stay in the text copy after the user answers it, and the existing dismissal check needed the model:/directory: labels that 0.158's header lacks, so tui-idle waits and the agent-status query kept reporting codex-model-migration-prompt over a live chat. Codex repaints its whole screen, header included, when a startup dialog closes, so the header after the dialog now marks it answered. Also corrects the live-chat marker comments: the middle dot also comes from the daemon session's agents hint row and the warnings notice, not only the status row. * docs(runtime): note that Codex startup dialogs also draw the live-footer dot * fix(runtime): recognise Codex 0.157/0.158 startup dialogs by the rows they really print Codex 0.157 and 0.158 no longer print `Press enter to continue/confirm` on their startup dialogs; they print key rows instead (`enter continue · esc skip`, `enter confirm · esc skip`, `enter/esc continue · ctrl+c quit`). The update, hooks-review and model-migration matchers still required the old wording, so none of these dialogs was reported as blocked. On 0.157 the dialog's `·` also satisfied the live-footer check, so a tui-idle wait read the update dialog as ready and worker-start would type the brief into it, where Enter picks "Update now" (npm install -g, Codex exits). On 0.158 the wait timed out instead of reporting blocked. The matchers now accept the old wording or the new row, tolerating the spaces the line-folded text copy drops around `·`. Each one matches from the dialog's first `·` (for the update dialog that is its title row, `Update available · 0.157.0 → …`), so the dialog is blocked from the same character that would otherwise make the provisional header read as live. The retired-model notice without choices has a catalog-supplied heading (`GPT-5.4 is no longer available`), so it is matched by its own key row. No new blocked-reason value. The startup-dialog matchers move to startup-dialog-blocked-signals.ts to keep terminal-wait-detection.ts under the line limit. Backed by six real captures (update available, hooks review, retired model without choices, each on 0.157.0 and 0.158.0), replayed frame by frame and through a tui-idle wait; the serializer round-trip replay registers their existing live-pen colour divergences. * fix(runtime): keep reporting Codex's retired-model notice after a relaunch in the same pane The retired-model notice is matched by its key row alone, and the matcher took the first `enter/esc continue ·` in the live window while every other startup-dialog matcher takes the last. Quitting Codex from the notice and relaunching it in the same pane leaves the old copy ahead of the new launch's header, so the header read as having dismissed the new notice: 0.157 then read ready and a worker brief would be typed into the dialog. Take the last key row, and replay each captured dialog quit-and-relaunched to pin all six. * fix(runtime): match Codex startup dialogs by the rows the text copy keeps intact Codex 0.157+ paints each startup dialog over its startup screen by cell diff, so Orca's line-folded text copy can drop letters and spaces from a heading: #23765's 0.157.1 capture reads `Updat available`. The update matcher needed `update available`, so on that capture tier 1 read the dialog's own `·` as the live chat's footer and a tui-idle wait settled ready on the update dialog, whose Enter picks "Update now". Match each dialog from its first `·` by rows Codex prints as fixed literals: `available · <version>` and `enter continue · esc skip` (update), `enter confirm ·` (hooks review), `enter/esc continue|confirm ·` (model notices, which also covers 0.158's new-model announcement, so its choice-text matcher goes). Legacy `Press enter to …` wording still matches. Add the new rows to the blocked-signal prefilter, and replay #23765's 0.157.1 update capture in the dialog suite. * fix(runtime): don't name Codex's mid-session pickers a hooks review Codex's rate-limit reset popup (and its other pickers) end their key row with `enter confirm · esc back`, which the hooks-review row matched now that it no longer needs the heading. Exclude `esc back` instead of requiring `esc skip`, so a half-painted hooks-review row still blocks. |
||
|
|
c8af48d8a4 |
fix(runtime): settle a quiet Codex composer as ready on every version (#23765)
* fix(runtime): settle a quiet Codex composer as tui-idle on every version Codex 0.158 dropped `model:`/`directory:` from its startup header, which both Codex readiness rules require, so `worker-start --agent codex` timed out; an idle Codex pane after a turn also had no readiness signal once the header left the screen. Generalize the Muse tier-1b lane into a quiet-ready-screen lane: a Codex (or agent-unknown) pane whose live screen shows the empty composer placeholder, no `to interrupt)` status row, no header `loading`, and no dialog wording in its live window, settles once the stream has been quiet for the tui-idle quiescence window. Additive only: the tier-1 rules and the Muse rule are unchanged. Fixtures: codex 0.150.1-0.158.0 captures at 120x40, including chunk-timed turns. * refactor(runtime): anchor the Codex quiet lane to the empty composer line Move the Codex screen rules into codex-terminal-readiness.ts and the quiet-screen body beside isKnownReadyPromptBody. The composer rule now matches only the `› Ask Codex to do anything` line and drops its dialog markers: every Codex dialog replaces the composer, and an answer ending "Would you like to…?" above a live composer must not hold the lane forever. The quiet lane checks quiescence before reading the screen. Trim the redundant startup and untimed turn fixtures. * fix(runtime): read Codex's busy row above the composer and scope the lane to codex panes * fix(runtime): read only Codex's live status row above the composer |
||
|
|
813aff8f8a |
fix(opencode): stop OpenCode 2 loading a stale plugin from the retired shared hooks dir (#23500)
* fix(opencode): stop OpenCode 2 loading a stale plugin from the retired shared hooks dir Before 1.4.209 Orca pointed OPENCODE_CONFIG_DIR at <userData>/opencode-hooks/shared and wrote a server()-only status plugin there. 1.4.209 moved the plugin to OpenCode's global config dir and 1.4.210 added the v2 setup() export, but nothing rewrote the old file. Shells, daemon-persisted panes and OpenCode 2 background services that still carry that OPENCODE_CONFIG_DIR load only that dir under OpenCode 2 (it replaces the global dir), so the v2 loader rejects the stale plugin with "Plugin must export a default definition with an id and an effect or setup function" and pane status dies. - Refresh the plugin in the retired shared dir (only when it already exists and its content differs) so OpenCode processes started later from old shells load the dual v1/v2 export. Runs on OpenCode pane spawns and on any spawn that inherits the retired dir, even with agent status hooks off. - Drop an inherited OPENCODE_CONFIG_DIR / ORCA_OPENCODE_* marker that points at the retired dir when building a new pane env, so new panes use global discovery. Limitation: an OpenCode 2 background service already running from an old pane keeps its cached copy of the stale module even after the file is rewritten (verified with opencode2 v2.0.18). It must be restarted (`opencode service restart`); a restart from a new Orca pane then picks up the global config because the env is stripped. * fix(opencode): harden legacy plugin repair and inherited config cleanup * fix(opencode): preserve daemon-owned user config during legacy cleanup * fix(opencode): sanitize inherited sources and repair unseen legacy copies * test(opencode): update shared PTY mocks for legacy repair * test(opencode): annotate shared repair mock signature --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
+7 |
1f6f8523ab |
feat(terminal): add Reset Terminal that clears leftover input modes on the host and pane (#23602)
* test(native-chat): await the async history and journal snapshot in three tests (#23560) #22835 made history() and journalSnapshot() async; tests from #23502 and #22944 still call them synchronously, so the typecheck job is red on every PR while main pushes do not run it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(usage): show ZCode Coding Plan quota on current main (#23520) Shows the ZCode Coding Plan quota in the status bar alongside the Claude and Codex usage readouts, reading the key from the user's own ZCode config. Credentials are scoped tightly: the host must be an exact match in the allowlist, HTTPS on port 443 only, `redirect: 'error'`, and the key is checked for CR/LF before it reaches a header. The key itself is never stored or logged — account identity is an HMAC. Both JSON inputs (a user-edited config file and the remote quota response) are narrowed at runtime rather than asserted, and the request cancels an unread response body on the error path so it cannot trip the undici parser crash (orca#8695). Co-authored-by: guanbear <guanbear@users.noreply.github.com> * fix(mobile): paired clients re-derive a kept terminal after a cold restore (#23109) * fix(mobile): paired clients re-derive a kept terminal after a cold restore A renderer frame published before a cold-restored terminal's PTY registered was fenced to an empty tab list and recorded as accepted, and the renderer never resends unchanged content. When registerPty binds a surface the accepted frame fenced out, re-merge that frame so the fence reads current state. * test(mobile): drive the live desktop window through the runtime's desktop seam * test(mobile): the re-derive path never flushes the store synchronously * test(mobile): a re-derived frame must not bring back a surface the host retired after accept * fix(mobile): a re-derived frame changes membership only for the registering surface The replay re-ran the whole accepted frame, so a surface the host retired after accept (a phone close whose remote PTY is still exiting, or a closed chat tab) came back. Every other surface now keeps the host's current decision; the removal repair is extracted from the terminal retirement helper so non-terminal tabs are removed the same way. * test(mobile): a re-derived frame must not drop or disown a phone-created terminal the desktop has not published * fix(mobile): a re-derived frame does not infer renderer retirements from its older frame * revert(mobile): drop the replay of a fenced renderer frame Reverts the production parts of |
||
|
|
3eac4d93d3 |
fix(terminal): stop guessing that apps died and wiping their keyboard modes (#23584)
* fix(terminal): stop guessing that apps died and wiping their keyboard modes The renderer wiped xterm's Kitty keyboard flags on every Ctrl+C, every live reattach, and every Windows agent turn end, though the app usually survives. xterm then encoded keys in legacy form while the pane mirror Orca's shortcut policy reads still held the negotiated flags, so Cmd+C, Shift+Enter, Option/Alt and IME commits disagreed with each other and with the app. - Delete the Ctrl+C wipe, the ConPTY agent-idle wipe, and the mirror reset on every PTY exit (it also ran on unverified host-loss exits). - Live reattach profiles no longer reset Kitty; every replay epilogue instead re-asserts the mirror's flags (pop-all, then the host-proven set; a bare pop while unproven), so a revealed xterm gets the live app's flags back. - Route every renderer-originated mode write through one scanning writer so xterm and the mirror always parse the same bytes: confirmed-shell reset, hibernate, cold restore, and a full process-boundary ground at fresh spawn. - Read Kitty flags as 0 where the protocol is withheld (ConPTY), since xterm ignores CSI u there but the mirror still scans it. - The dashboard popout restores snapshot flags as bytes so its xterm agrees. * style(terminal): separate the kitty restore builder from the pen reset * fix(terminal): let the kitty mirror own the withheld-protocol rule Review follow-ups for the stop-guessing change: - The mirror takes a `kittyKeyboard` option from the xterm's advertisement and ignores CSI u when withheld, as xterm does, replacing a per-reader helper that any new reader could skip. Daemon/headless users keep the default. - Replay epilogues are writers (`writeReplayEpilogue`, `writeReattachReplayReset`) that take the sync or async xterm writer, so nothing that looks like a builder mutates the mirror. - An abandoned hidden restore re-asserts the mirror's kitty flags after the byte-gap reset: its discarded chunks were already scanned. - Tests pin a non-zero host restore (epilogue ends `=31u`, mirror 31), a withheld pane staying at 0, and the restart-in-place ground landing after the mirror reset; the epilogue test helper is now an exact builder. * refactor(terminal): one epilogue writer and one scanned ground per boundary - Reattach callers write `chooseReattachReplayReset(...)` through `writeReplayEpilogue`, dropping the second writer from the session. - Fresh spawn and cold restore rely on their scanned ground alone: it leaves the mirror known at 0 with a proven baseline, so the extra reset() was dead. |
||
|
|
8b410b4893 |
feat: add first-class Qoder CLI support (#23581)
feat: add first-class Qoder CLI support Integrate Qoder launch, identity, canonical hook status, trust and resume. Verify with captured Qoder 1.1.64 transcripts and hidden Electron sidebar checks. Builds on and cross-reviews #7502, #8611, #9655, #12910, #13311 and #15291. Co-authored-by: dalveytech-vincent <vincent@dalveytech.com> Co-authored-by: Eridanus117 <45489268+Eridanus117@users.noreply.github.com> Co-authored-by: xingqingzzp-gif <xingqingzzp-gif@users.noreply.github.com> Co-authored-by: jyang2004 <jyang2004@users.noreply.github.com> Co-authored-by: yunqian <yunqian@alibaba-inc.com> Co-authored-by: huzhening.hzn <huzhening.hzn@alibaba-inc.com> |
||
|
|
400e4e7957 |
feat(agents): add Freebuff launch and sidebar status support (#23567)
Add Freebuff launch support and execution-host status reporting for the sidebar, including running, question, blocked, and settled states. Validate against captured CLI transcripts and real rendered sidebar evidence. Cross-referenced community implementations #17065, #20839, and the Freebuff portion of #18790. Preserve their agent/catalog/mobile/documentation coverage and add canonical status publication and regression tests. Co-authored-by: Harkaran Brar <18134082+harkaranbrar7@users.noreply.github.com> Co-authored-by: Prarambha369 <98906077+Prarambha369@users.noreply.github.com> Co-authored-by: Lesley Murfin <260182349+LesleyMurfin@users.noreply.github.com> |
||
|
|
45f3512a33 |
feat(agents): add first-class DeepSeek Harness (dsh) support (#22468)
* feat(agents): add first-class DeepSeek Harness (dsh) support Register DSH as a supervised Orca agent: catalog entry and detection for its dsh-tui profile, status/question hooks through DeepSeek's own Claude-Code hook bridge, composer-ready prompt delivery, session resume, headless Source Control AI, and title identity that no longer collides with Gemini's. * fix(dsh): reach Orca through DSH's credential scrub and stop reading its title as Gemini DSH runs command hooks through its own shell executor, which drops every env var whose name contains KEY, TOKEN, SECRET or PASSWORD — taking ORCA_PANE_KEY and ORCA_AGENT_LAUNCH_TOKEN with it, so every hook exited without posting. Mirror both onto scrub-safe aliases at spawn and restore them at the top of the DSH hook script. Its title collided too: DSH rests on the same glyph Gemini works on, so a resting DSH pane was relabelled Gemini CLI and reported working forever. Defer both the Gemini classifier and the title status detector on DSH's whale, in the base module both copies of that classifier read. * test(mobile): repin the session-route closure for the DSH agent icon * fix(dsh): address review — never splice user rows, cover remote panes, keep the diff off argv - findManagedDshPatchRegion paired an orphan start marker with a later block's end, so a truncated write made install/remove delete the user's own rows. Pair each end with the nearest preceding start; regression test fails without the fix. - The relay PTY env builder never applied the scrub-safe aliases, so remote DSH status silently never appeared even with the remote hook installed. - Source Control AI sent the whole diff on argv; send it over stdin with DSH's '-' marker. - dsh-tui/dst already chose the interactive profile, so a workspace folder named 'web' or 'plugin' no longer marks a live agent pane non-interactive. - Isolate USERPROFILE as well as HOME so a Windows run cannot edit the real home. - Drop the duplicate README badge and revert an incidental doc reformat. * refactor(dsh): share the managed-hooks reader and tighten the new modules Reuse before reimplementing: readManagedDshHookEvents was a near-verbatim copy of Muse's, with byte-identical private helpers. Both now call one readManagedHookEventsFromJson. Also: one readTextOrAbsent instead of two spellings of the same read (dropping an existsSync TOCTOU), one status() builder instead of four inline literals, rmSync(force) instead of exists-then-unlink, and a redundant empty-string guard before JSON.parse. The patch-file transforms lose their index juggling for a predicate plus a filter. * fix(dsh): refuse a flow-style patch file, keep its mode, and stop the relay inheriting a pane - applyManagedDshPatch matched only an exact `[]`, so `[] # keep empty` or a non-empty flow sequence got a block entry appended after it — invalid YAML that would leave DSH unable to load the user's own patch layer either. It now strips the token from an empty sequence (keeping a trailing comment) and returns null for a non-empty one; install reports that and changes nothing. - The patch rewrite dropped an owner-only file to the umask default (CWE-732); pass preserveMode. - The relay PTY env never dropped inherited pane identity the way the local and daemon builders do, so a spawn that specified none could inherit the relay's own and every agent's hook would report against that pane. * fix(dsh): keep the flow-style refusal in every status read, and scope the mode test to POSIX A refused patch file carries no managed region, so getStatus() fell through to a bare not_installed with detail null — the actionable 'rewrite it as a block sequence' message only ever reached the one-shot install() return. Export the predicate and check it first, behind one shared message constant. The owner-only mode assertion cannot hold on Windows, where chmod only toggles the read-only attribute and mode & 0o777 reads 0o666 for any writable file. * docs(readme): restore the DeepSeek Harness badge lost in the rebase * test(mobile): repin the session-route closure to the measured 4221 Measured, not derived: 4220 without the DSH icon entry, 4221 with it. Two of the three modules above main's 4218 pin are not this change's — they arrived with the mobile work after #22570 and were never repinned; the changelog records that split explicitly. * fix(dsh): settle tui-idle on the agent's own hook, so supervised workers see it ready Reported by a tester on the adhoc build: `terminal wait --for tui-idle` ran to its 90s timeout against an already-ready DSH composer, so a supervised worker never sees the agent as ready. Every existing tier reads the title, and DSH deliberately carries no title status: its rest prefix is Gemini's working glyph, so the detector reports none. A fresh first-party `done` is better evidence than any title anyway — it is the agent's own account of its own turn, and normalizeDshEvent drops subagent events, so it is the lead's. Scoped to DSH: for agents whose hooks report child turns, a mid-turn `done` is the #6011 class this file prevents. * test(daemon): record the DSH transcript's true-colour I2 divergences Adding the dsh-tui capture to __fixtures__ enrolled it in the serialize replay sweep, where it reports 10 I2 divergences and failed the unlisted-transcript default of 0. Every one is the same shape — visible-grid row=0, a 24-bit background the round trip does not restore to default — which is DSH's whale intro painting whole rows of true colour. Verified as an upstream limitation rather than a regression by replaying against the previous build (build-serialize-addon-at-ref.mjs --ref origin/main): I1 and I3 both hold. * fix(dsh): return the new tui-idle verdict from the first-party done lane Main refactored isTuiIdleSatisfied into evaluateTuiIdle, which returns a verdict rather than a boolean. The DSH lane still returned `true`; it is tier-1 positive evidence, so it returns READY_STRONG like the title/body lane above it. Re-verified the regression test still fails without the lane. * test(relay): pin the scrub-safe pane-identity aliases on the relay spawn path The relay builds a remote pane's env itself, so the alias mirroring there had no test: removing the call left every suite green while remote DSH status silently vanished. Both cases fail without it. * docs(dsh): point the hook service at the integration reference The reference doc had no inbound link from anywhere in the repo. |
||
|
|
708123b868 |
Improve PTY device error messages with localization support (#23538)
* Improve PTY device error messages with localization support - Extract error hints to shared module for reuse across host and renderer - Change multi-line hints from space to newline separator for readability - Add localization of resource-limit hints in the renderer - Prevent duplicate issue requests when toast renders its own link - Handle legacy hint formats from older hosts * Prevent duplicate PTY allocation hints on legacy messages - Extract hint-detection logic into hasPtyAllocationHint() helper - Check for both current and legacy PTY allocation hint variants - Prevents duplication when messages already contain legacy hints |
||
|
|
5219b8ada9 |
fix(relay): reset input modes a dead program left on in SSH terminals (#23488)
* fix(relay): ground input modes a dead app left armed on SSH terminals SSH relay PTYs now run the same recovery barrier as the local daemon: between startup ingress and the replay buffer/publish sink, it pauses at an OSC 133;D that closes a command which left input modes or the alternate screen armed, proves the shell owns the PTY foreground on the execution host, and on proof injects the process-boundary ground ahead of the prompt. Teardown flushes held bytes through releaseRelayIngress. The barrier now holds only the D marker's terminator and carries the ground on it as one transformed emission over exactly one raw unit. Previously the ground was a zero-raw emission, which source-credit delivery accepts but never sends, so live output and replay diverged. Every emission now covers at least one raw unit; refuted, timed-out, overflowed and flushed episodes release the terminator unmodified. * fix(terminal): keep the held 133;D terminator inside its emission's raw span Treat a trigger end below 1 as unsplittable instead of trusting the scanner's clamp, arm the bail timer before queueing the terminator, and build the relay's startup ingress unconditionally beside its barrier. * fix(terminal): hold the whole 133;D mark so a mid-proof snapshot ends on an escape boundary The scanner now reports where the unclean-death D mark starts; the barrier releases everything before it and holds the mark itself (bounded to 4K, so the grounded span stays inside a relay source frame). A snapshot taken mid-proof therefore has no open OSC for consumers that do not restore the pending escape tail. The relay defines PTY liveness once. |
||
|
|
2119730ec0 |
fix(terminal-wait): unattended launches report Claude's trust dialog instead of timing out or typing into it (#22927)
* fix(terminal-wait): recognise Claude's workspace trust dialog as a blocking prompt
Claude's first-launch "trust this folder?" dialog parks the cursor above its
options, and the host's line tail drops the lines below it, which are the only
ones the trust matcher knew ("trust this folder", "Enter to confirm"). An
unattended Claude launch into a fresh folder therefore waited out its whole
budget and reported a timeout instead of the blocking prompt. The dialog's
opening question ("... one you trust?") survives in the tail, so it is now
recognised, pinned by a captured transcript of the real dialog.
* fix(terminal-wait): read the rendered screen for blocked prompts on the tui-idle poll
Claude's workspace-trust dialog parks the cursor on its highlighted option with a
cursor-up, and the host line tail deletes every retained row below the cursor, so
"Yes, I trust this folder" and "Enter to confirm" never reach the blocked-prompt
detector. In a live launch the dialog also arrives in 1024-byte reads, and the tail's
plain path blanks each line that ends in a carriage return before the newline, so
the opening question does not survive either. An unattended launch waited out its
whole budget and reported a timeout.
The runtime already feeds every PTY chunk into its own headless emulator. The tui-idle
poll now also runs the existing blocked-prompt rules over that emulator's visible
screen (no provider or host round trip), after the tail checks and before the
quiet-foreground idle fallback. The "one you trust" phrase is dropped: it only matched
when the whole dialog arrived in one chunk, wrapped away on narrow panes, and could
match prose.
Replays three live Claude 2.1.280 captures through the runtime: the dialog in one
chunk and in 1024-byte reads, a 60-column pane, and the dialog answered with "Yes",
which must report ready rather than blocked.
* fix(terminal-wait): skip the rendered-screen blocked check while the agent reports working
The screen check runs on every tui-idle poll, and the automation observer holds a tui-idle
wait open for a whole agent turn. A working Claude whose screen showed dialog wording (a diff
of the detector, say) was reported blocked where main kept waiting. The dialogs only the
screen reveals are start-up ones painted before any title, so a working title now vetoes it.
* fix(terminal-wait): settle weak tui-idle evidence only after a clean screen read
A shell auto-title (oh-my-zsh's `claude`, fish's `claude <cwd>`) names Claude before
its workspace trust dialog paints, and the line tail loses that dialog. The wait took
the bare name as rest, settled ready, and the launch typed its brief into the dialog.
Every tui-idle settle site now asks one evaluator for a verdict: blocked, strong ready,
working, weak ready or pending. The pre- and post-registration checks and the
title-change resolvers settle only blocked or strong ready; weak ready is left to the
poll, which settles it only once the rendered screen shows no blocker. A name-only
Claude title is held to the same quiet window as Codex and Devin, since Claude
announces rest with its own explicit title.
* test(serialize): record the answered Claude trust capture's known serializer divergences
The captured answered-dialog transcript added by this PR is replayed by the
serialize round-trip suite and diverges at 13 checkpoints. It diverges
identically on origin/main and on the pre-#22586 addon build (13 both-fail,
0 regressions): the live SGR pen leaks into the alt buffer, and an alt buffer
first entered after a shrink keeps hidden scrollback. Pin the count like the
other known captures and correct the comment, which called these upstream.
|
||
|
|
433986fa3b |
fix(runtime): read Codex readiness from the live screen (#23475)
* fix(runtime): read Codex readiness from the live screen Codex 0.157 runs in embedded mode when Orca passes `-c model_reasoning_effort=…`, shows a startup warning, and repaints its header by cell diff (`ESC[5;3Hdir ESC[5;7Hctory:`). The tui-idle body check read the line-folded tail, which drops those cursor moves and reads `dirctory:`, so worker-start timed out at agent_readiness while Codex sat idle at its prompt. For Codex (or unknown) panes whose live emulator screen shows the Codex header, the screen now decides readiness: `model:` and `directory:` present and neither still `loading`. Otherwise today's text rules apply unchanged. All six tui-idle satisfaction sites share one helper so they cannot disagree. Fixes STA-8628 / #23241. * refactor(runtime): read the Codex screen lazily and only for Codex panes Pass the screen as a thunk so non-Codex panes never build the grid, hoist the pane agent lookup, and share the unblocked-ready check with the Muse rule. * fix(runtime): let the Codex screen only add readiness A grid out of step with the PTY (size mismatch or a resize mid-paint) can garble Codex's header. Keep every verdict the text rules give today and consult the screen only when they say not ready, so no flow that settles today can stop. * fix(runtime): read Codex readiness from the header box only Chat below the header can mention "OpenAI Codex" or "model: loading", so the screen rule now reads model/directory/loading only inside the header box. The serialize round-trip sweep picks up every runtime fixture; record the pre-existing header-border attribute divergence the new Codex 0.157 recordings expose, which this change does not touch. |
||
|
|
618a8b0758 |
fix(terminal): keep a dead app's input modes recoverable after a refuted proof (#23474)
A command's armed input modes were demoted at the first OSC 133;D whether or not the foreground proof confirmed, and the alternate-screen trigger was spent the same way. A full-screen agent's nested command shells leak their own D onto the main PTY; the refuted proof for that stray D used up the only trigger, so the app's real death later went unrecovered. Every D now re-asks while a command's mode (or the alternate screen) is still up; only the confirmed ground or the app's own disable clears it. Proofs that can never succeed (wsl.exe, no Windows job reads) already refute immediately without spawning anything. The accepted cost is one process read plus a bounded output hold per D while a command-owned mode stays up and the proof keeps refuting: a live agent leaking D, a stopped job, a nested subshell, or an rc that execs another shell. |
||
|
|
bdb897b735 |
Respect disabled OpenCode variants and refresh WSL settings safely (#23328)
Respect disabled OpenCode variants, preserve explicit config ownership, and refresh WSL guest settings safely across reconnects. Based on Harshul Rathod proposal #22805. Co-authored-by: Harshul Rathod <harshulrathod1640@gmail.com> |
||
|
|
4b6fe95943 |
fix(windows): preserve relocated terminals and native process scans (#22872)
* fix(windows): ship the process-table addon to the relocated daemon host The Windows terminal daemon runs from a copy of the app under %LOCALAPPDATA%\Orca\daemon-host\<version>. That copy took node-pty but not @vscode/windows-process-tree, so the daemon's bare require of the addon found nothing and every process-table read (foreground tracking, descendant sweeps) fell back to a powershell.exe Get-CimInstance scan (#16905). - Copy the addon's runtime files (package.json, lib/, the .node binary) into the host; the ~25MB of gyp intermediates beside them are filtered out. - Treat a host missing those files as unmaterialized, so hosts built before this are rebuilt, and skip relocation if the install itself lacks them. - Log the daemon's native/CIM capability at startup and warn once when the process table falls back to CIM. Revives #19525 on current main. * test(windows): locate update-survival loss before relaunch * test(windows): preserve daemon tree before update-survival proof * test(windows): distinguish Electron exit from launcher close timeout * test(windows): verify process exit when inherited pipes delay close * test(windows): trace installer process checks in isolated survival runs * fix(windows): probe process-query capability before installer sweep * fix(windows): match installer probe and process-check profile behavior * fix(windows): use NSIS separators for the process-check include * test(windows): dismiss session-search overlay in survival harness --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
f6631ddeee |
fix(daemon): release terminal attach cancellation listeners (#23191)
* fix(daemon): release terminal attach cancellation listeners * fix(daemon): preserve attach wait settlement ordering * fix(i18n): register existing diff note fallback strings --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
dd46f51278 | perf(terminal): avoid intermediate history frame buffers (#23005) | ||
|
|
8b7a2a5393 |
Wait for foreground job readiness before testing Ctrl-Z (#23158)
Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
6fc3cdcad6 |
Bundle Bun for headless Orca and profile persistence (#22635)
Bundle a pinned, verified Bun runtime for headless Orca so existing Node launch commands can hand off before opening a profile. Keep desktop execution on Electron. Add the Bun SQLite adapter and terminal backend, bounded shutdown, process inspection and cross-platform artifact qualification. Keep future managed SSH deployment separate from current production launch paths. |
||
|
|
82412dab8b |
Persist profile state in SQLite with background writes (#22612)
Migrate profile state to SQLite and move writes and backups into a background worker. Acknowledge terminal, SSH and automation changes only after durable saves. Preserve JSON import, recovery, rollback and compatibility exports. Validate migration, worker failures, maintenance, cross-profile moves and terminal lifetime races with unit, integration and end-to-end coverage. |
||
|
|
58d1ff3b6a |
Provide the Orca CLI automatically in managed WSL terminals (#22761)
* Provide the Orca CLI automatically in managed WSL terminals * Simplify managed WSL CLI provisioning Never block a shell on CLI availability, keep the shared WSL login-shell builder unchanged, provision from PTY env assembly only, drop the error variable and command probing, and reuse the existing WSLENV helper. * Scope the managed WSL CLI to WSL terminals Provision only for WSL panes and publish the directory through addOrcaWslInteropEnv, so daemon terminals keep inherited WSLENV entries and non-WSL builds never see the variable. Write the bridge with a UTF-8 BOM so Windows PowerShell 5.1 keeps non-ASCII user-data paths, give the dev bridge the dev launcher's app-launch env, and drop the unused skill-setup wiring and runtime capability. * Tighten the managed WSL CLI bridge and setup Launch the bridge child exactly like the registered bridge (no hidden window or output relay; verified through WSL with Node and Electron), give the dev bridge the dev launcher's NODE_OPTIONS stash, clear the guest-only directory before starting Windows processes, collapse setup into one function, warn once, and guard WSL env routing with tests. * Harden managed WSL CLI quoting and inheritance PowerShell also ends single-quoted strings at typographic quotes, so a user-data path such as O'Brien with a curly apostrophe broke the managed bridge. Fix the shared quotePowerShellLiteral and reuse it. Drop an inherited ORCA_WSL_CLI_DIR on the daemon path, remove the unreachable PATH dedupe, and cover failed setup with a stale caller value. * Cover the managed WSL CLI in zsh and on POSIX CI Add a live zsh case that reaches a real prompt, a POSIX test that runs the PATH restore snippet in bash and zsh under set -u, and a null result for unwritable user data. Say what a failed write actually costs, and document per-spawn write logging and older-daemon behaviour. * Keep system bashrc out of the PATH restore test CI runners make bash -c read /etc/bash.bashrc, which fails under set -u. --------- Co-authored-by: Orca Worker <orca-worker@localhost> |
||
|
|
2796a3ac15 |
fix(claude): prove a stopped chat's child processes gone when they exit with it (#22918)
* fix(claude): prove a stopped chat's child processes gone when they exit with it Stopping a Claude chat snapshots its child processes, closes Claude, and then verifies each child is gone before the stop counts as proven. The verifier only accepted a child as gone after that child had appeared in one of its own process-table reads. When Claude exits gracefully it takes its short-lived children with it before the first read, so none of them was ever seen again. Every read confirmed them absent, and the verdict was still "unverifiable" after the full 3.5 s window. Measured live: 37 complete reads, target absent from all, verdict unverifiable, on every idle stop. The snapshot is itself a table read that saw each child alive, so it now counts as the first sighting. An absence counts only from a read that started after the child was last seen, which keeps what the old rule protected against: a shared or in-flight read begun before the snapshot cannot list a child forked since. Two such absences prove a child gone. Live, the same stop now proves the tree gone in about 150 ms. The daemon's terminal shutdown uses the same verifier and gets the same rule. Test reads that reused one capture stamped with the snapshot's own time now stamp each read when it starts, as real scans do. * fix(claude): keep the latest sighting and count the final read as an absence A matching read that started earlier but resolved later could move a target's last sighting back and let an older absence count; the sighting now only moves forward. The read after the deadline now records its absences the same way the polling loop does, so a second qualifying absence there proves the target gone. |
||
|
|
2c7609bf6a |
fix(terminal): serialize only the visible width after a column shrink (#22586)
* fix(terminal): serialize only the visible width after a column shrink xterm does not reflow the alternate buffer (or a normal buffer under pre-21376 ConPTY), so after a shrink each line keeps its old length. SerializeAddon walked every non-final row to line.length, so any snapshot taken after a shrink carried stale right-hand cells that wrapped into extra rows on replay; restores repainted that garbage and a differential TUI such as OpenCode never cleared it. Clamp the row walk and the wrap-boundary lookups to the terminal's columns in Orca's addon-serialize source patch, and regenerate the bundles, maps and lockfile hash per docs/reference/xterm-patch-regeneration.md. * test(terminal): read shrink-snapshot fixtures through public APIs Drops the private-terminal casts the casting gate flags; the normal-buffer case now drives a plain pre-21376 ConPTY terminal and its SerializeAddon directly. * fix(terminal): blank a wide glyph clipped by a column shrink when serializing After a non-reflowing shrink a width-2 glyph can have its lead half in the last column and its trailing half past the grid. Serializing the lead half makes the replay wrap it to the next row and shift every row below, so serialize that cell as a blank and keep the row exactly the grid's width. A glyph ending exactly at the edge is unchanged. * test(mobile): move the session closure pin past the main agent status modules #22452 added src/shared/main-agent-status.ts and src/shared/agent-turn-outcome.ts, which agent-status-types.ts imports, so the session route's closure grew by two local modules (4218 -> 4220). That change was src/shared-only, so its own CI never ran this suite; main has been at 4220 since, and any PR that fires the mobile web app job fails on the stale pin. Measured on |
||
|
|
f559c0588a |
fix(terminal): ground a program that dies with input modes armed on the normal screen (#22739)
* fix(daemon): rebase durable checkpoints on the live terminal A durable checkpoint was folded from the previous checkpoint plus recorded output, so it inherited that checkpoint's modes forever. After a daemon restart killed a full-screen TUI and a new process started inline, the chain kept the dead TUI's alt screen and mouse tracking (?1049h ?1003h ?1006h) while the live emulator was clean. Every reattach and getBufferSnapshot served the stale chain, the renderer re-armed mouse tracking, and wheel scrolling went to a program that never asked for it: scrolling froze. Each full checkpoint is now the live snapshot verbatim (screen, layout, alt frame, modes, owner) with only the normal-buffer rows live has evicted taken from the durable replay. A checkpoint can no longer carry a dead process's modes, and checkpoints already poisoned on disk heal on the next compaction. - The first fold after a cold restore replays the same seed segments live was given, so rows line up even over a dead TUI's alt screen. - Idle zero-record folds keep the disk copy when it already agrees with live, so quit and relaunch bursts don't replay every session. - Held teardown bytes are already in the drained records and the live snapshot, so they are no longer replayed twice or appended as a tail. - The bounded getBufferSnapshot path honors the requested depth even when the live window is deeper, without phantom link rows. - The fold's ownership scanner and frame merge are removed; owner and frame come from live. * fix(terminal): one process-boundary ground for every known or proven boundary Three copies of the "the process that armed these modes is gone" reset had drifted: the cold-restore seed cleared only pen and mouse, the recovery barrier used the renderer's dead-TUI profile, and the cold-restore payload had none. A cold restore therefore left the dead process's focus reporting, bracketed paste, application cursor and keypad modes armed in the live emulator, the first checkpoint, and main's mirror. And the seed wrote the dead process's torn escape after the reset, so the new shell's first bytes could complete it (for example retitling the pane). PROCESS_BOUNDARY_GROUND replaces them: CAN, leave the alt screen without moving the normal-buffer cursor, every mouse protocol and encoding off, focus/paste/app-cursor/keypad off, cursor shown and style reset, kitty popped, SGR reset, grounded DECSC. It stays inert for the lifecycle scanner. The seed, the recovery barrier, and the cold-restore payload all use it, and the seed no longer carries the torn tail. The first fold after a cold restore now always rebases on live, because focus and keypad are not in TerminalModes and the zero-record shortcut could not see them differ. * fix(terminal): ground a program that dies with input modes armed on the normal screen The daemon's in-stream crash detector only fired when a program died with the alternate screen up. A normal-buffer program that armed mouse tracking, focus reporting, keypad or kitty keyboard flags and exited without disabling them was cleaned up only in the renderer, so the daemon kept the modes and re-armed them on the next reattach, mobile included (#13077's garbage-at-the-prompt family). The lifecycle scanner now tracks armed input modes (mouse protocols and encodings, ?1004, ?66, and kitty flags as per-screen stacks that mirror xterm's main/alt swap and its 16-entry cap). ?2004 and ?1 are excluded: shells arm them at their own prompts. Modes armed when a command starts (OSC 133;C) count as the shell's, so a prompt that leaves modes on never triggers. At OSC 133;D the trigger is now "alt screen or a program-armed input mode", still one-shot and still gated by the shell proof, and the existing PROCESS_BOUNDARY_GROUND is recorded through the stream so live and durable history change together. WSL panes spawn wsl.exe, which the shell proof does not recognise, so the detector never grounds them; a test pins that and the renderer keeps covering them. The mouse-leak e2e now keeps its arming process alive until the live pane is checked, because the daemon grounds a proven exit. * fix(terminal): keep shell- and host-armed input modes through the process-boundary ground ConPTY arms focus reporting (?1004h) before the first prompt, and the live recovery ground cleared it for the rest of the pane. The barrier now re-arms the modes that were on at OSC 133;C right after the ground, so only the dead program's modes are reset. * fix(terminal): re-assert only modes the shell or host armed outside a command A mode a program leaked past a refuted proof was still on at the next OSC 133;C, so the baseline snapshot re-armed it after a later ground. Record who armed each mode instead: only enables outside a command (before any marker, or between 133;A/D and C) form the baseline. * fix(daemon): keep OSC links and kitty flags through durable checkpoint folds and trims Stop seeding persisted OSC link ranges into the fold replay: they index the base buffer, so rows evicted by pending output left a link on the wrong text. The serializer already writes OSC 8 into the ANSI the fold replays. Re-apply kitty keyboard flags when replaying a snapshot for trimming, since rehydrateSequences omits them. Bound a smaller restore request by trimming the committed checkpoint instead of re-reading disk and rebasing the live window at a smaller depth. * test(daemon): follow the isFirstTake rename in the process-boundary ground suite * refactor(daemon): drop the unreachable deep-live branch from the durable fold The live window's override cap now derives from the restore depth, so live can never be deeper than the fold. pendingRecords and isFirstTake are required. * test(daemon): pass pendingRecords to the process-boundary ground fold * fix(terminal): reset alt-screen kitty flags in the process boundary ground Kitty keyboard stacks are per screen, so resetting only after ?1049l left a dead TUI's alt-screen flags for the next alt-screen app. Also drop the inert CAN from the ground (every site grounds after complete bytes) and correct two stale comments. * fix(terminal): track input-mode ownership in one map Each armed mode now has one owner: host (before any marker, or a prompt a 133;C proved), prompt (unproven until C), command, or stale (left past a D). Host arming is sticky, 133;D demotes command modes (the one-shot), and the ground re-asserts only host modes. Fixes a D without C triggering on host modes, an ESC c mid-command turning later enables into host modes, and a program's repeated host enable dropping host ownership. The reattach e2e now keeps the arming program alive so only the reattach reset can disarm it. * refactor(terminal): stop treating kitty flags as host state fish, the one shell that pushes kitty flags at its prompt, pops them before running a command and re-pushes at the next prompt, so the ground never needs to restore them. Only host private modes are re-asserted now. * fix(terminal): keep host input-mode ownership across RIS ConPTY answers a mid-command ESC c by re-sending ?1004h, which reset() had recorded as the command's, so the ground turned host focus reporting off for the rest of the pane. RIS now drops only non-host ownership. * fix(terminal): let only the host own focus reporting and leave it in the ground Host ownership covered every mode armed before the first marker, so a tmux that died with mouse on had it re-armed by the ground. And the re-assert's ?1004h enable made the runtime's ownership mirror revoke, so remote owners never settled on Windows. Only ?1004 can be host-owned now, and the ground skips its ?1004l instead of turning it off and back on, so injected bytes carry no enables. |
||
|
|
fe46138716 |
fix(terminal): one process-boundary ground for every known or proven boundary (#22735)
* fix(daemon): rebase durable checkpoints on the live terminal A durable checkpoint was folded from the previous checkpoint plus recorded output, so it inherited that checkpoint's modes forever. After a daemon restart killed a full-screen TUI and a new process started inline, the chain kept the dead TUI's alt screen and mouse tracking (?1049h ?1003h ?1006h) while the live emulator was clean. Every reattach and getBufferSnapshot served the stale chain, the renderer re-armed mouse tracking, and wheel scrolling went to a program that never asked for it: scrolling froze. Each full checkpoint is now the live snapshot verbatim (screen, layout, alt frame, modes, owner) with only the normal-buffer rows live has evicted taken from the durable replay. A checkpoint can no longer carry a dead process's modes, and checkpoints already poisoned on disk heal on the next compaction. - The first fold after a cold restore replays the same seed segments live was given, so rows line up even over a dead TUI's alt screen. - Idle zero-record folds keep the disk copy when it already agrees with live, so quit and relaunch bursts don't replay every session. - Held teardown bytes are already in the drained records and the live snapshot, so they are no longer replayed twice or appended as a tail. - The bounded getBufferSnapshot path honors the requested depth even when the live window is deeper, without phantom link rows. - The fold's ownership scanner and frame merge are removed; owner and frame come from live. * fix(terminal): one process-boundary ground for every known or proven boundary Three copies of the "the process that armed these modes is gone" reset had drifted: the cold-restore seed cleared only pen and mouse, the recovery barrier used the renderer's dead-TUI profile, and the cold-restore payload had none. A cold restore therefore left the dead process's focus reporting, bracketed paste, application cursor and keypad modes armed in the live emulator, the first checkpoint, and main's mirror. And the seed wrote the dead process's torn escape after the reset, so the new shell's first bytes could complete it (for example retitling the pane). PROCESS_BOUNDARY_GROUND replaces them: CAN, leave the alt screen without moving the normal-buffer cursor, every mouse protocol and encoding off, focus/paste/app-cursor/keypad off, cursor shown and style reset, kitty popped, SGR reset, grounded DECSC. It stays inert for the lifecycle scanner. The seed, the recovery barrier, and the cold-restore payload all use it, and the seed no longer carries the torn tail. The first fold after a cold restore now always rebases on live, because focus and keypad are not in TerminalModes and the zero-record shortcut could not see them differ. * fix(daemon): keep OSC links and kitty flags through durable checkpoint folds and trims Stop seeding persisted OSC link ranges into the fold replay: they index the base buffer, so rows evicted by pending output left a link on the wrong text. The serializer already writes OSC 8 into the ANSI the fold replays. Re-apply kitty keyboard flags when replaying a snapshot for trimming, since rehydrateSequences omits them. Bound a smaller restore request by trimming the committed checkpoint instead of re-reading disk and rebasing the live window at a smaller depth. * test(daemon): follow the isFirstTake rename in the process-boundary ground suite * refactor(daemon): drop the unreachable deep-live branch from the durable fold The live window's override cap now derives from the restore depth, so live can never be deeper than the fold. pendingRecords and isFirstTake are required. * test(daemon): pass pendingRecords to the process-boundary ground fold * fix(terminal): reset alt-screen kitty flags in the process boundary ground Kitty keyboard stacks are per screen, so resetting only after ?1049l left a dead TUI's alt-screen flags for the next alt-screen app. Also drop the inert CAN from the ground (every site grounds after complete bytes) and correct two stale comments. |
||
|
|
1c1b7829ec |
fix(daemon): rebase durable checkpoints on the live terminal (#22732)
* fix(daemon): rebase durable checkpoints on the live terminal A durable checkpoint was folded from the previous checkpoint plus recorded output, so it inherited that checkpoint's modes forever. After a daemon restart killed a full-screen TUI and a new process started inline, the chain kept the dead TUI's alt screen and mouse tracking (?1049h ?1003h ?1006h) while the live emulator was clean. Every reattach and getBufferSnapshot served the stale chain, the renderer re-armed mouse tracking, and wheel scrolling went to a program that never asked for it: scrolling froze. Each full checkpoint is now the live snapshot verbatim (screen, layout, alt frame, modes, owner) with only the normal-buffer rows live has evicted taken from the durable replay. A checkpoint can no longer carry a dead process's modes, and checkpoints already poisoned on disk heal on the next compaction. - The first fold after a cold restore replays the same seed segments live was given, so rows line up even over a dead TUI's alt screen. - Idle zero-record folds keep the disk copy when it already agrees with live, so quit and relaunch bursts don't replay every session. - Held teardown bytes are already in the drained records and the live snapshot, so they are no longer replayed twice or appended as a tail. - The bounded getBufferSnapshot path honors the requested depth even when the live window is deeper, without phantom link rows. - The fold's ownership scanner and frame merge are removed; owner and frame come from live. * fix(daemon): keep OSC links and kitty flags through durable checkpoint folds and trims Stop seeding persisted OSC link ranges into the fold replay: they index the base buffer, so rows evicted by pending output left a link on the wrong text. The serializer already writes OSC 8 into the ANSI the fold replays. Re-apply kitty keyboard flags when replaying a snapshot for trimming, since rehydrateSequences omits them. Bound a smaller restore request by trimming the committed checkpoint instead of re-reading disk and rebasing the live window at a smaller depth. * refactor(daemon): drop the unreachable deep-live branch from the durable fold The live window's override cap now derives from the restore depth, so live can never be deeper than the fold. pendingRecords and isFirstTake are required. |
||
|
|
7a71e20860 |
fix(pi): isolate status ownership in new terminals (#22717)
* fix(pi): isolate status ownership in new terminals * docs(pi): explain terminal ownership boundaries * docs(pty): clarify environment rescrubbing |
||
|
|
0b16a31e6e |
fix(runtime): budget explicit terminal close for the daemon's immediate-kill verdict (#22385)
* fix(runtime): budget explicit terminal close for the daemon's immediate-kill verdict Explicit terminal close (worker-release, worker-stop, `orca terminal close`) gave the daemon kill RPC a fixed 2s deadline. The daemon's immediate kill captures descendants, SIGTERMs them with a 2.5s verification window, then waits up to 8s for the root's physical exit. An agent that runs exit hooks after SIGTERM (Muse: ~3s) outlived main's 2s timeout, so close reported the PTY unverifiable and worker-release returned release_unknown even though the daemon confirmed the exit ~200ms later. Derive the close budget from the daemon's own immediate-kill reply budget (now in an import-free module) plus 2s for the post-kill inventory check. A process that exits within the daemon's budget is released; a wedged process or unreachable host still times out as unverifiable. * fix(runtime): budget the force-kill retry and exercise an expired close deadline * test(runtime): drop tautological deadline-expiry assertion |
||
|
|
7c46a69049 |
feat(telemetry): report the macOS daemon's code identity on adoption and folder-denial events (#22171)
* feat(daemon): import the macOS process code-identity probe from PR #21826 Takes `daemon-mac-code-identity.ts` and its test verbatim from David Bebawy's community PR #21826 (stablyai/orca). The probe asks Security.framework, via `codesign --display --verbose=1 +<pid>`, where a live process's code lives on disk — the question Node cannot answer, and the one that decides whether tccd can still resolve a running daemon's code identity after an app update. Imported unchanged here so the adaptation that follows is reviewable as a diff against the author's original. Co-authored-by: David Bebawy <david.ayad2@gmail.com> * feat(telemetry): report the daemon pid's macOS code identity on the two adoption events Community PR #21826 argues that macOS terminal daemons lose Documents/Desktop/ Downloads access after an update because the daemon's own executable is unlinked — Squirrel parks the outgoing bundle under a ShipIt staging directory and later deletes it — so tccd can no longer map the daemon pid to on-disk code. Today's `spawner_path_class` and `tcc_attribution` read the binary that forked the daemon, which an in-place update deletes and recreates, so neither can see that state. This adds the detector as a measurement only. `code_identity` rides on `daemon_adopted` and `daemon_pty_cwd_denied`, the two events that already describe an adopted daemon, so denied daemons can be cross-tabbed against healthy ones. Nothing reads the verdict: no replacement, no notice, no UI. The probe is David Bebawy's, narrowed from a path-carrying union to the closed enum the wire allows, and memoised per pid so one codesign spawn answers for a whole daemon generation. Off macOS, or with no pid, it reports `probe-failed`, which keeps both schemas strict and non-optional. Co-authored-by: David Bebawy <david.ayad2@gmail.com> * fix(telemetry): read the daemon's code identity fresh on every adoption event The probe memoised its verdict per pid and never expired it, so `daemon_pty_cwd_denied` reported whatever the probe saw at adoption rather than what was true at the denial. That breaks the measurement in both directions: a transient codesign failure during startup pinned `probe-failed` for the rest of the run, and the `parked` to `unresolvable` transition became invisible. Squirrel leaves the parked bundle in place until the next update, which can be days, so a daemon adopted as `parked` and denied as `unresolvable` is the exact crossover this study exists to catch, and the cache hid it. Now every ask runs its own codesign. Only concurrent asks about the same pid share a probe, and that entry is cleared as soon as it settles, so nothing survives to be reported later. Both events are rare enough that one spawn each is not worth a cache. * fix(telemetry): drop the dead existence check from the code-identity probe The classifier stat'd the path codesign displayed and called a missing one unresolvable. That path is unreachable: once the executable is unlinked, `codesign --display` prints no `Executable=` line at all and exits 1 with "No such file or directory", which the fallback below already classifies as unresolvable. Verified directly on Darwin 25.5 against a signed binary deleted out from under a running pid. All the branch actually covered was the window between codesign reading the path and this process stat'ing it, and it paid for that with a synchronous stat on the main thread. * fix(telemetry): never classify a timed-out codesign probe as a verdict `runProcess` kills the child at the deadline and reports `timedOut`, but the runner type dropped that field, so a codesign killed mid-display could still have printed an `Executable=` line and been read as `resolved` or `parked`. A half-written display proves nothing about where the daemon's code lives. The runner result now carries `timedOut`, and a timed-out probe returns `probe-failed` before the output is looked at. * docs(telemetry): state what each code-identity verdict actually asserts A reviewer read `resolved` as a claim that the executable sits inside the installed app and asked for that to be validated. It is not that claim, and we are not making it: proving containment needs the pid record's spawner path, and deciding anything from where the code lives is #21826's proposed behaviour rather than this measurement. The enum doc now spells out all four verdicts in the terms the probe can actually support, and says plainly why `resolved` stops at "exists and is not parked". A matching note sits beside the parked-path pattern. * docs(telemetry): stop asserting how long a parked bundle survives The probe's rationale claimed Squirrel keeps the parked bundle "until the next update". A reviewer claimed the opposite, that it is deleted at the end of the same install. Neither holds up against this Mac's ShipIt log: the install moves the outgoing bundle to a TMPDIR ShipIt directory and logs no removal of it at all, and the one "Couldn't remove owned bundle" line names the incoming download staging copy, not the parked one. Every parked bundle from the last two days is nevertheless gone now. So the rationale in the probe doc, the enum doc, and the reprobe test comment now assert only what is established: the outgoing bundle is moved aside at install and disappears later on a schedule we have not pinned down. That is already enough to justify the design, since one pid's verdict can change within an app run, which is exactly why every ask reads fresh. * feat(telemetry): report readable TCC-gated spawns as the code-identity control `daemon_pty_cwd_denied` gives code_identity's hit rate on denials, but a readable spawn emitted nothing, so an `unresolvable` adoption with no denial could not be told apart from a user who never opened a terminal in Documents, Desktop, or Downloads. The false-positive rate that gates #21826's auto-replacement was unmeasurable. `daemon_pty_cwd_readable` now fires when a daemon reads a TCC-gated cwd, once per daemon and folder class per app run, with the same origin properties as the denial event. The read-out becomes a 2x2 of code_identity against readable/denied on protected-folder spawns. Fire-and-forget on the spawn path like the denial emit, and no app-side directory read. * refactor(telemetry): one emitter and schema for both cwd verdicts, no dedupe state The once-per-daemon dedupe on `daemon_pty_cwd_readable` was keyed before the probe ran, so a daemon first seen readable while `parked` never reported again once it turned `unresolvable` — the one cell that would count most against #21826. It also counted per daemon while denials count per spawn, so the 2x2 mixed units. Readable now reports every spawn, like denied, and both events share one emitter (`trackDaemonPtyCwdVerdict`) and one schema. The TCC-folder gate lives in the verdict branch. The origin fields are one shape spread into both schemas. The codesign probe calls `runProcess` directly and tests mock it, replacing a test-only runner parameter. The repeated "never cached" rationale is now said once. * fix(telemetry): rename the shared origin schema fields for the anti-slop gate no-shape-in-symbol-names rejects daemonOriginShape; the fields are event props. --------- Co-authored-by: David Bebawy <david.ayad2@gmail.com> |
||
|
|
ba742a86bb |
fix(linux): release orphaned processes when their owner exits (#22247)
* fix(linux): release orphaned processes when their owner exits * fix(linux): handle inhibitor errors until streams close --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
632ae1320b |
fix(daemon): reap terminal descendants during shutdown (#22232)
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
7650abe224 |
fix(macos): tell the user when Orca's terminal service can't read their folder, and walk them through the fix (#21923)
* fix(macos): tell the user when Orca's terminal service can't read their folder
On macOS, a terminal daemon that survived an app update can be refused access to
a workspace under Documents, Desktop, or Downloads while the Orca app itself can
still read it. Terminals opened there die with "Operation not permitted" and
nothing on screen explains why. The daemon has reported `cwdReadableByDaemon` on
every create since #18043 and main has emitted `daemon_pty_cwd_denied` on proven
divergence since then; the field data says 1,438 users hit it in 21 days. What
was missing was the notice.
The verdict itself moves off `access()`. A grant-less probe on an affected
machine showed a TCC mode where `access(R_OK|X_OK)` passes on `~/Documents` and
`opendir` still fails, so the check now does what a shell listing its cwd does:
`opendirSync`, one `readSync`, `closeSync`. Only EPERM/EACCES reads as denial —
a missing path, a non-directory, or an unexpected error still reads as readable,
so a non-permission failure can never masquerade as one. The same probe is what
the app side compares with, through one oracle shared by the telemetry emitter
and the notice, so the spawn path reads the directory once.
Proven divergence now also records evidence in main: one entry, keyed by the
daemon's pid, start time and launch nonce, carrying an opaque digest of that
identity and the folder class. No path leaves main. The existing focus-time
`macTccAttribution` poll carries it to the renderer, which raises a second toast
latched per daemon scope: dismissed stays dismissed, and a restart mints a new
identity so the poll returns null and the toast clears with no post-restart
probe. If the replacement daemon is denied too, about 31% of cases, the next
spawn re-records under the new scope and the notice returns, now with the
re-allow sentence doing the work.
No new IPC channel, no daemon protocol field, no polling change, and nothing new
on the spawn path beyond one `opendir`. `daemon_folder_access_notice` counts
shown, dismissed and open_manage_sessions against `daemon_pty_cwd_denied` as the
denominator; `shown` is emitted from main the first time a scope leaves the IPC
handler, so the renderer carries no telemetry plumbing for it.
* fix(macos): clear folder-access evidence only when the same folder class reads back
A readable spawn in ~/code said nothing about a Documents denial but was
hiding the notice; retire the evidence only when the daemon reads a folder
of the class it was denied on.
* fix(macos): say what a terminal-service restart actually does
The Manage Sessions restart confirmation still described the product as it was
before agents resumed themselves: it promised panes showing "Process exited"
that the user reopens by hand, and mentioned legacy-protocol sessions nobody
outside the daemon code can act on. Open terminals and agents come back on
their own now, so the old copy made a routine remedy sound like data loss.
It also called the thing a "daemon". The same restart is about to be offered
from a user-facing fix dialog, so both surfaces now say "terminal service", and
the confirm button is just "Restart".
The new body adds the one fact the old one never stated: terminals on remote
hosts are not affected. Translations of the two changed strings are dropped so
the five non-English locales fall back to English rather than keep showing copy
that is now wrong.
* feat(macos): give the denied-folder notice a fix the user can follow
The folder-access toast told the user their terminal service could not read
Documents and then handed them a paragraph: restart from Manage Sessions, and
if that does not work, re-allow Orca in System Settings. Both halves were
guesses. Roughly a third of restarts do not fix it, and the user had no way to
know which case they were in before spending every open terminal on finding
out.
Main can now answer that. `daemon-folder-access-probe.ts` forks a short-lived
child of the app binary the same way the daemon itself is forked, runs one
opendir/readdir/closedir against the denied path, and prints a single JSON
line. macOS attributes a TCC grant to the process that forked the child, so a
child of the app running now answers exactly the question the running daemon
cannot: would a replacement daemon get in? The child goes through the shared
child-process wrapper, never a shell, with a 3s deadline, a 1KB output cap and
an environment scrubbed to PATH/HOME/TMPDIR. Every failure — timeout, bad
output, spawn error — reads as `unknown`, never as a verdict.
That answer rides out as `restartWillHelp` on the evidence the existing
focus-time poll already carries, and the toast becomes a title and two buttons:
Fix… and Not now. Fix opens a dialog with the two real steps. When the grant is
already in place, step one is shown as done and Restart is live. When it is
not, step one is open and Restart is disabled until it completes — which it
does by itself, because the poll re-probes while the answer is still no, and
returning from System Settings is the moment that lands. An unanswered probe
never accuses the user of a missing grant; it leaves both steps open.
Restart calls the management API directly rather than stacking the Manage
Sessions confirmation on top, since the dialog already states the consequence.
Success replaces the steps with a done line and takes the toast down; failure
says so inline and leaves the button usable.
System Settings opens through the existing developer-permissions pane opener,
which takes an id rather than a URL, with Files and Folders added to it. The
event's action enum now also counts fix_opened, settings_opened,
restart_clicked and — emitted from main when a replacement daemon's first spawn
lands in the folder class the previous one was denied on — whether the restart
actually worked.
* fix(macos): let the folder-access notice return after a poll that read no daemon
A daemon identity reads as null during any reconnect blip, and the poll reports that as
"no mismatch". The notice dismissed itself and then never showed again for that daemon,
because the once-per-daemon latch still held its scope. Only "Not now" should latch.
* fix(macos): say what the folder-access notice costs the user
One line read like a stray warning. The toast now says who is blocked and what fails,
and still leaves the steps to the fix dialog.
* fix(macos): give the folder-access toast one action and the X, like every other toast
"Fix" is the only button; the X dismisses. Sonner fires onDismiss for programmatic
dismissals too, so the post-restart takedown now goes through the store and the hook,
and only a user's X is counted as dismissed.
* fix(macos): keep the fix dialog's steps a checklist and put the one action in the footer
Buttons inside each step made the list look like a form, and a footer Close duplicated
the X. The footer now carries the active step's action, with a ghost Cancel; a probe
that could not answer says so under step 1 instead of showing a check.
* fix(macos): let the checklist show the fix landed instead of saying so
A hedged sentence addressed to the user read like chat. On success both steps check
off and the footer offers Done; the unanswered-probe helper is a status, not advice.
* chore(i18n): drop the fix dialog's unused close key
* Revert "chore(i18n): drop the fix dialog's unused close key"
This reverts commit
|
||
|
|
88f2f01061 |
fix(daemon): escape the terminal daemon into its own systemd scope so a service restart no longer kills every live PTY (#19430)
* fix(daemon): escape the terminal daemon into its own systemd scope so a service restart no longer kills every live PTY Root cause: daemon-launched-child.ts forks the detached terminal daemon with detached: true, which escapes the POSIX process group (setsid) but never the systemd cgroup. Every PTY the daemon owns is itself an undetached direct child of the daemon (native-pty-spawn.ts). Under a combined systemd unit (Type=simple, KillMode=mixed, per docs/reference/headless-linux-server.md), a systemctl restart/stop SIGKILLs every process still in the cgroup at the stop timeout -- the daemon and every live terminal -- even though the codebase already has a fully-built adoption/reattachment path for a surviving daemon (orcad-entry.ts's refreshRestoredOrchestrationAuthority + reconcileLegacyWorkerTerminals, gated on daemonOwnsFreshPersistentPtys()). That path never fires today because the daemon never survives long enough. Fix: when systemd is actually supervising the process and the OS user has a reachable systemd --user manager (isDurableDaemonScopeSupported(), Linux only), launch the daemon via systemd-run --user --scope so it lands in a cgroup that is a sibling of the service unit's cgroup, not a descendant of it. A systemctl restart of the combined unit then never reaches it. Any failure of the scoped launch (no reachable bus, D-Bus policy rejection, etc.) falls back transparently to the existing plain fork() launch, so every platform/environment without this capability is unaffected. The daemon self-detects its own resulting cgroup scope via /proc/self/cgroup (detectOwnCgroupScopeUnit()) rather than trusting the launcher's intent, and publishes it as cgroupUnit in its pid record and orcad's health/readiness payload (health.terminalDaemon.cgroupUnit), so a running deployment can be observed to confirm the fix actually engaged. No new session registry is added: the existing daemon pid-record + adoption protocol (publishDaemonPidFile, daemon-pid-record-quarantine.ts's dead-record reclaim, refreshRestoredOrchestrationAuthority) already implements durable, crash-safe reattachment for a surviving daemon -- it was simply never exercised against a full unit restart before now. Proven via a systemd-in-Docker recovery test: a live PTY session's shell process, its daemon, and the daemon's cgroup scope were all confirmed unchanged across a real systemctl restart of a Type=simple/KillMode=mixed unit, while the main process pid changed (confirming the unit actually restarted) and the new process's health payload recognized the surviving daemon as adopted and live. A fresh write into the same PTY post-restart reached the same running shell. Ordinary terminal create/work/release and the #18789/#18790 worker-release reap-fix regression tests are unaffected. Fixes stablyai/orca#19408 * fix(daemon): probe the real per-UID XDG_RUNTIME_DIR before trusting the process's own env isDurableDaemonScopeSupported()/buildDurableDaemonScopeCommand() trusted the current process's own XDG_RUNTIME_DIR env var first, falling back to /run/user/<uid> only when that var was unset entirely. On mtl-02, orca-serve@factory.service's RuntimeDirectory= hardening directive makes systemd export XDG_RUNTIME_DIR=/run/orca_serve/factory into the unit's process -- a private scratch dir that shares the env var's name but has nothing to do with the user session bus. /proc/<pid>/environ on that host confirmed exactly that path plus DBUS_SESSION_BUS_ADDRESS=disabled:, while the real bus was reachable the whole time at /run/user/985 (confirmed via systemctl --user is-system-running with that dir exported by hand). The probe treated the hardened override as authoritative, found no bus socket there, and reported unsupported on every launch -- so the cgroup-escape fix from #19408/#19430 never actually engaged on real hardware, even though tonight's factory deployment picked it up. Fix: resolveUserRuntimeDir() now always tries the conventional /run/user/<uid> path first (computed independently via getuid(), never trusted from env), checking for a genuinely connectable bus socket via statSync(...).isSocket() rather than a bare existsSync. It falls back to the process's own XDG_RUNTIME_DIR only when that canonical path has no reachable bus -- covering hosts that legitimately have no /run/user/<uid> at all but do have a working bus wherever their own environment points. buildDurableDaemonScopeCommand() now explicitly sets XDG_RUNTIME_DIR to whichever path this resolution picked, rather than inheriting the spread env's (possibly hardened-wrong) value. Both isDurableDaemonScopeSupported() and buildDurableDaemonScopeCommand() gained an injectable canonicalRuntimeDir parameter (defaulting to the real computed path) so tests can exercise the hardened-override scenario deterministically with a real, connectable AF_UNIX socket fixture instead of the live host's actual runtime directory. Docker's stock jrei/systemd-ubuntu test container never had this hardening directive, so this gap was structurally invisible to the container-based verification in #19430 -- only caught against real mtl-02 hardware. * fix(daemon): report the daemon's own pid over the ready handshake, not systemd-run's The launcher used to infer the daemon's identity pid from the immediate spawned child (`child.pid`). On the durable-scope path that child is `systemd-run --user --scope`, not the daemon, so the launcher was asserting an identity it had no authority over. `DaemonReadyIdentity` now carries a required `pid` populated from `process.pid` inside the daemon itself, and `daemon-launched-child.ts` takes `launchedIdentity.pid` from that self-report. Both sides of the `holdDaemonAdoptionLease` pid comparison therefore originate inside the daemon process, which is the idiom this branch already uses for cgroup membership (`detectOwnCgroupScopeUnit` reads `/proc/self/cgroup` rather than trusting what the launcher intended). Note on the reported consequence: `systemd-run --scope` registers its *own* pid on the transient scope unit and then `execvpe()`s the target command -- same pid, no intermediate process -- so adoption did not in fact fail on systemd >= 206 (verified against systemd 255.4-1ubuntu8.17 and current main, `src/run/run.c` `start_transient_scope()`). The fix stands on its own merits: it removes a silent dependency on that exec-vs-fork implementation detail, which a `systemd-run` shim earlier in PATH or any future systemd change would have broken with no diagnostic. `terminateLaunchedDaemonChild` was audited and deliberately left on `child.pid`: for the same execve-preserves-pid reason that pid is either still systemd-run mid-scope-setup (killing it correctly aborts the launch) or already the daemon, so it targets the right process either way. Regression coverage: `daemon-launched-child-identity.test.ts` pins the identity source, and `daemon-ready-identity.test.ts` gains pid-validation cases. Ready-message fixtures across the `daemon-init-*` suites were updated for the now-mandatory field. Addresses: https://github.com/stablyai/orca/pull/19430#discussion_r3953722704 https://github.com/stablyai/orca/pull/19430#discussion_r3954346518 * test(daemon): assert cgroupUnit in the pid-file parse contract `parseDaemonPidFile` returns `cgroupUnit` on every branch as of the durable-scope commit on this branch, but five exhaustive `toEqual` assertions in daemon-health.test.ts still described the pre-scope shape, so they failed on the branch independently of any later change. Adds the field to those expectations. Deliberately not relaxed to `toMatchObject`: asserting the full parsed shape is what makes these tests catch a field silently dropped from the pid-file contract. * refactor(daemon): resolve the canonical user runtime dir at one point The per-UID path cannot change for a live process, so compute it once into a module const instead of threading the same default call through three signatures, and drop the try/catch around a getuid() that cannot throw once it exists. Trims the module prose to the non-obvious facts and corrects the pid-file record comment: an unscoped daemon writes null; only records no daemon wrote are absent. * test(daemon): clean up the cgroup-scope fixtures and assert a verdict The cgroup fixture tracked only the file it wrote, leaking one temp dir per case. Drains both fixture lists with splice so the pop-may-be-undefined guards go away, and replaces a not-throw/typeof-boolean pair with the verdict it was circling: no resolvable runtime dir means unsupported. * refactor(daemon): share the detached child options across both launch paths cwd, detached and stdio were repeated in the fork and systemd-run branches, which left the two comments explaining them hovering over the env block instead. Names them once so each branch carries only its own delta. * refactor(daemon): validate the ready pid like every other field typeof-first narrows the value, so the two 'as number' casts the isSafeInteger check needed disappear and the pid guard reads like the startedAtMs guard below it. * fix(daemon): don't retry the launch unscoped after losing the endpoint race A scoped attempt that lost the endpoint to another daemon was retried unscoped: a second doomed fork, a misleading 'cgroup-scope launch failed' warning, and the same DaemonEndpointUnavailableError the caller was already going to adopt on. Rethrows it instead, since no launch mode can win a race that is already lost. Also drops a private alias for DaemonChildSpawnOptions and the two 'as number' casts on child.pid in the startup-failure cleanup. * fix(daemon): unlink the pid record by the pid the daemon published The record holds the daemon's self-reported pid, so match on that rather than on the immediate child's, which is the systemd-run wrapper's until it execs. * fix(daemon): route the scope launch through the child-process chokepoint The two files this PR added imported `node:child_process` directly, which `child-process-import-boundary.test.ts` fails on deterministically: the offender count went 155 -> 157 against a pin of exactly 155. Raising the pin or listing the files is what that test explicitly forbids, and the allowlist's own note says a split "moved the import, it did not add one" -- so the fix is to get both new files off the module and put the count back at 155. - `daemon-cgroup-scope.ts`: the `systemd-run --version` probe now uses `runProcessSync` instead of `execFileSync`, so it gets the shared spawn decisions. Kept synchronous deliberately: `launchDaemonChild` attaches the readiness listener in the same tick it is called, and an await before the spawn moves the child past that tick. A non-zero exit is data rather than a throw here, so the verdict now checks `code === 0 && !timedOut`. - `daemon-launched-child-spawn.ts`: the scoped launch uses `spawnProcess`, and the long-standing unscoped launch keeps `fork` semantics through a new `forkProcess`. - `src/shared/child-process/fork-process.ts`: the fork arm of the chokepoint. `spawnProcess` cannot express a Node child with an IPC channel started from a module path under an overridden `execPath`, and the existing launch tests are written against `fork`'s contract, so a spawn rewrite would have changed module resolution, `execPath` and `execArgv` at once. It passes `windowsHide: true` -- the flag every other call site in that directory sets, reachable via an assertion because `ForkOptions` omits it -- which keeps `windows-console-visibility.test.ts` at its pin of 65 too. Both ratchets pass with both pins and both allowlists untouched. Docs: `orcad-operations.md` and `headless-linux-server.md` still described the limitation this PR removes as permanent. Both now describe the durable-scope survival path and its preconditions (systemd as PID 1, a reachable user bus / `loginctl enable-linger`, `systemd-run` on PATH), and scope the old text to the unscoped-fallback case, pointing at `health.terminalDaemon.cgroupUnit` as the way to tell the two apart on a running host. * fix(daemon): seal the cgroup capability probe from the host and correct KillMode=mixed docs The capability probe consulted the host's own /run/systemd/system marker and spawned the real systemd-run binary, so the hermetic unit tests could only pass on a systemd host (and fail closed otherwise, even with faked bus sockets). - Thread systemdBootPath and runVersionProbe as test seams through isDurableDaemonScopeSupported, defaulting to the real boot marker and systemd-run --version probe in production. - Narrow the injected probe to the ProcessResult slice it consumes. - Cover: no-systemd-boot, non-zero probe exit, and probe-timeout cases. - Correct KillMode=mixed semantics in the docs: the cgroup-wide SIGKILL fires the instant the main process exits, not after TimeoutStopSec; document the Docker-container caveat and add KillMode=mixed to the multi-service template. * fix(daemon): satisfy assertion checks in scoped launch * fix(daemon): satisfy anti-slop and console guards * test(serve): update shutdown docs assertions for daemon scope * fix(daemon): migrate adopted legacy scopes * docs: qualify restart safety by daemon scope * docs(daemon): qualify Upgrade restart prose with durable scope caveat Align the Upgrade section in docs/reference/headless-linux-server.md with the earlier preservation section and docs/reference/orcad-operations.md: a service restart terminates live processes only when running under the unscoped fallback, and stops should be treated as destructive unless health.terminalDaemon.cgroupUnit names an orca-daemon-*.scope. Update the shutdown workflow test assertion in config/scripts/headless-serve-shutdown-workflow.test.mjs to match. * fix(daemon): harden legacy scope migration --------- Co-authored-by: Lesley Murfin <260182349+LesleyMurfin@users.noreply.github.com> Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
4feaaf5c5c |
feat(terminal): configure interactive Unix shell arguments (#21904)
* feat(terminal): configure interactive Unix shell args * fix(settings): clarify Unix shell argument defaults * fix(settings): improve Unix shell argument guidance * fix(settings): simplify shell argument guidance * fix(settings): clarify empty shell args * fix(settings): explain empty shell args * feat(settings): make shell argument modes explicit * fix(settings): keep no args inside custom mode * fix(terminal): apply configured shell args on the renderer spawn path The renderer's pty:spawn handler builds options in ipc/spawn-options, not the runtime controller, so the configured profile never reached a terminal pane. The local launch plan also dropped the args whenever shellOverride was set -- which the spawn path always fills from terminalDefaultShell. Both spawn paths now share one resolver. * chore(i18n): allowlist the new terminal shell argument strings Matches how the sibling Terminal shell settings strings are already handled. |
||
|
|
4085e1cf60 |
fix(memory): release stale session registries (#21734)
* fix(memory): bound session and lifecycle registries * fix(memory): bound transient filesystem registries * fix(memory): cap path and locale caches * fix(memory): bound runtime recovery registries * fix(memory): bound host mirror gap verdicts * fix(memory): bound shell startup env cache * fix(memory): bound gitlab host context cache * fix(memory): release removed ssh generations * fix(memory): expire cloud refresh replay guards * fix(memory): release retired plugin generations * fix(memory): bound plugin log key retention * fix(memory): bound automation authority generations * fix(memory): bound native chat enrichment cache * fix(memory): bound web session tracking generations * fix(memory): bound codex credential absence paths * fix(memory): bound WSL canonical path cache * fix(memory): bound sparse checkout cache * fix(memory): bound shared directory cache * fix(memory): bound advertised URL scan snapshots * fix(memory): bound automation manager cache * fix(memory): bound web session reorder intents * fix(memory): bound web session focus intents * fix(memory): bound web session handoffs * fix(memory): bound automation dispatch tokens * fix(memory): bound host mirror waiters * fix(memory): bound retained session activity * fix(memory): bound retained session activity * fix(memory): bound web session close intents * fix(memory): bound cloud session cache * fix(memory): bound WSL home cache * fix(memory): bound SSH capability cache * fix(memory): bound trust grant cooldowns * fix(memory): bound WSL auth drain state * fix(memory): bound Linear workspace credential cache * fix(memory): bound local Git capability cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL preflight cache * fix(memory): keep hot cache entries warm * fix(memory): preserve generation fences across eviction * fix(memory): close remaining eviction fences * fix(memory): align evicted upstream generations * fix(memory): trim successful capability probes * fix(auth): retain expired refresh replay evidence --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
84d827a6ab |
fix(daemon): pause producers when stream backlogs grow (#20947)
* fix(daemon): pause producers when stream backlogs grow * fix(daemon): reset stream backpressure on socket replacement * docs(daemon): point retention audit at current reproducer * test(daemon): validate stream retention audit outcomes * fix(daemon): bound the stream producer stall and leave a visible gap Stream backpressure pauses a session's PTY with no deadline: the only un-pause comes from the consumer draining, so a half-open peer that stops reading without closing freezes the shell for the rest of the session. Arm a 60s watchdog on the false->true stream-pause transition (not on the re-assertions refresh() makes for neighbouring sessions). On fire, mark the session stall-released: it becomes keep-tail droppable, its backlog is thinned behind a dataGap, and the producer runs again. The existing dataGap path makes the renderer restore that pane from the daemon's snapshot, so the user sees the terminal jump to current rather than sit frozen. The mark clears once the session's last byte leaves the daemon, restoring ordinary pausing. Nothing here reports a process exit - loss of contact with a consumer is not evidence about the child. Also enable TCP keepalive on the stream socket so a genuinely dead peer closes and onStreamDisconnected clears the pause. * test(daemon): put each casting SAFETY: directive on one line `oxlint-disable-next-line` covers only the line directly after it, so a rationale wrapped onto a second comment line suppressed nothing and the casts failed the changed-code quality gate. Drop the remaining JSON.parse cast for an annotated binding. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
09073086a8 |
feat(terminal): inline images via @xterm/addon-image (perf-first) (#19512)
* feat(terminal): inline images via @xterm/addon-image, perf-first Add opt-in inline terminal images (SIXEL, iTerm2 IIP, Kitty graphics) through @xterm/addon-image, designed to keep idle terminals unaffected. Performance: - The addon (base64-inlined wasm decoders + protocol handlers) loads off the boot critical path via a deferred loader that mirrors the WebGL addon: primed after first paint only when the setting is on, read back synchronously at attach, with a 3-attempt cap so a transient failure never disables images for the session and a missing chunk never refetches per pane. renderer-boot-graph guards against eager import. - enableSizeReports:false so the addon never sets windowOptions and double-answers Orca's own CSI 14t/16t responder. - Perf-tuned decode/storage limits (storageLimit, sixel/iip/kitty size caps) in one place. Correctness: - Orca's DA1 handler wins over the addon's (last-registered-first), and the default DA1 response never advertised Sixel (;4), so DA1-detecting tools (chafa, img2sixel, viu, timg) never emitted it. The winning handler now appends ;4 while the setting is on, resolved per query so a live toggle changes the next DA1; idempotent against the ConPTY response that already lists it. - ORCA_IMAGE_PROTOCOL=kitty is exported to spawned shells (local, daemon, relay/SSH) and forwarded across the WSL boundary, so image-capable agents can pick an encoder. Unknown image sequences are swallowed by xterm when the addon is detached, so this never garbles output. - Settings toggle (default on) gates rendering and DA1 advertisement. Cross-checked against community PRs #7775, #11706, and #19201 at the end; credited below. Co-authored-by: s546126 <s546126@users.noreply.github.com> Co-authored-by: XRX193 <XRX193@users.noreply.github.com> Co-authored-by: lmsh7 <lmsh7@users.noreply.github.com> * fix(terminal): bound inline image memory and classify Kitty replies * fix(terminal): bound image decode and release image resources on cleanup * fix(terminal): address image addon review feedback * test(terminal): stub setPaneInlineImagesEnabled in appearance manager fakes * fix(terminal): evict unplaced kitty payloads before displayed images Byte-budget eviction dropped the oldest transmitted blob regardless of placement, so a new upload could erase a visible image while abandoned blobs still held budget. Unplaced payloads now go first and displayed ones only when that is not enough. The incoming image is always stored, so an oversized one overshoots the cap by one payload instead of being dropped after the protocol already acked OK. * fix(terminal): gate DA1 Sixel on real addon attachment; claim SSH image spec in CI - DA1 advertised Sixel from the setting alone, so a pane whose lazy addon chunk was still loading (or had failed all three attempts) told feature-detecting tools to emit DCS that nothing could render. Track the attached decoder per terminal and require it before setting the ;4 bit. - tests/e2e/terminal-inline-images-ssh.spec.ts was Docker-gated but claimed by no lane runner, so pr-e2e-gate-contract failed and the spec would have self-skipped green forever. - Reject non-positive PNG IHDR dimensions before decode: they are parsed with signed shifts, so a dimension >= 0x80000000 came back negative and slipped past the pixel-limit comparison. - One resolveTerminalInlineImagesEnabled() for the default-on setting; the four call sites mixed '?? true' with '!== false', which disagree on null. - One readInlineImageResources() walk of the addon internals instead of two copies that could drift against the patched dependency. - Isolate the deferred-attach drain per pane; make the zoom-invariance and backing-storage e2e assertions fail when the feature is dead. * refactor(terminal): one lazy xterm addon loader for webgl and image terminal-image-addon-loader was a structural clone of the webgl one — same memo, attempt cap, and .then(ok,err)-clears-memo recovery. Both now wrap createLazyXtermAddonLoader; each keeps its literal import() specifier so the bundler still splits the chunk (verified against a fresh build: addon-image stays out of the boot graph). * refactor(terminal): name openTerminal's addon flags; pin image addon limits Two adjacent optional booleans could be swapped without a type error once inline images added the second one. * docs(terminal): state the real per-pane image ceiling; drop test ordering dependency storageLimit:32 reads like the pane's budget but keys three pools — decoded pixels, retained encoded Kitty blobs, and pending WASM decoders — so the worst case is ~98 MB per pane with no cross-pane governor. Say so at the constant. pane-inline-images.test.ts's deferred case needed to run first; it now takes a fresh module instead, and the rest prime in beforeAll. Verified by running the file with that test moved last. * fix(terminal): satisfy rebased static analysis gate * fix(terminal): complete casting gate cleanup * fix(terminal): recover failed image addon loads * fix(terminal): bound image decoder allocations --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: s546126 <s546126@users.noreply.github.com> Co-authored-by: XRX193 <XRX193@users.noreply.github.com> Co-authored-by: lmsh7 <lmsh7@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
1fa6fac17c |
fix(daemon): answer the per-pty snapshot predicate for the pty it was asked about (#21381)
canProvideAuthoritativeBufferSnapshot is contracted as "whether this exact PTY can return a sequence-safe provider snapshot" (pty-provider-contract.ts), and two of the three layers already route it per id: DaemonPtyRouter forwards to adapterFor(id), and DegradedDaemonPtyProvider forwards to the provider that owns the session. The daemon adapter was the leaf that discarded the id and returned supportsAuthoritativeBufferSnapshots — a negotiated protocol version, which is a fact about the connection, not about a pty. That is reachable, not theoretical. getProviderForPty falls back to the local provider for any id it cannot place, so a remote-runtime id (whose pty lives on another machine) resolves to the local daemon adapter, and pty:getAuthoritativeBufferSnapshotCapabilities answered `true` for a session this daemon has never owned. The renderer caches that as a definitive per-pty verdict, and because the leaf discarded the id it could not tell it had been asked about something it does not own. Today the wrong answer is masked: allowOrdinaryParkRestore short-circuits remote and SSH ptys before the cached verdict is read, so nothing consults it. This closes the gap before something relies on it — a caller reaching for a per-pty answer should not be handed a confident one that is wrong. Not touching that short-circuit. It is deliberate: SSH bytes transit the client's own main process into its headless mirror, so those panes have a local copy the predicate says nothing about, and the direct-SSH lane was confirmed to repaint from a daemon-backed restore with the park capture disabled entirely. Routing SSH around a daemon-snapshot predicate is correct, and removing the short-circuit would disable SSH parking for no correctness gain. The existing protocol-compatibility test asserted `true` for a made-up session id, which encoded the bug. It now spawns a real session, so it still proves the protocol-version gate without depending on an unowned id reading as supported. |
||
|
|
691d9692e6 |
fix(pty): stop detached OMP tools on immediate terminal close (#20642)
* test(omp): add opt-in owned PTY closure probe * fix(pty): sweep detached tools on immediate unrecognized shell close * test(omp): create close probe evidence root in fresh worktrees * test(pty): account for asynchronous immediate descendant cleanup * test(pty): reject inconclusive descendant cleanup probes |
||
|
|
d04b05b5c8 |
Detach retained CI and terminal tails from oversized strings (#20960)
* fix(memory): detach retained CI and terminal tails from oversized strings * fix(terminal): detach retained error and reattach string slices * fix(terminal): release oversized recent-output backing strings * fix(terminal): release backing strings held by PTY detectors * fix(memory): own bounded Claude background task labels * fix: detach retained terminal mode scan tails * fix: own retained plugin worker output strings * fix: own incomplete OSC 133 carry strings --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> |
||
|
|
98998b18ad |
fix: release retired shared daemon owner metadata (#21162)
Co-authored-by: m4air <m4air@Mac.localdomain> |