* 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>
* fix: retire closed editor models from the app shell
* test(editor): use checked Monaco attachment calls
* Preserve bounded editor view caches when retiring closed models
* docs(editor): describe batched model retirement
* fix(editor): preserve cleanup work across registry replacement
* fix(editor): build editor model URIs with the file scheme
Monaco keys its model registry by `uri.toString()`, and both
`@monaco-editor/react` (via the `path` prop) and the closed-tab disposal
path built that key with `Uri.parse`. On Windows a raw path such as
`C:\repo\a.ts` parses as scheme `c`, which fails the scheme gate in
`modelService._schemaShouldMaintainUndoRedoElements`, so closed-file undo
history was dropped for every file at any size — not only the large files
the tradeoff note covers.
Add `toEditorModelUri`, the one filesystem-path -> model-key function,
built on `Uri.file` so the result always carries the `file:` scheme and
re-parses to itself. Route model creation, disposal lookup and the
still-open ownership comparison through it so all three agree; a
divergence there would dispose a model an open editor is still editing.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(ai-vault): bound incremental transcript record assembly
* fix(ai-vault): skip one oversized record instead of dropping the session
An agent transcript record over the 10 MiB budget threw out of the JSONL
fold, so the whole session vanished from Agent Session History and from
search. A 10 MiB base64 image or a runaway tool result is ordinary.
The reader now discards the offending record up to its newline and keeps
folding. The in-progress record always starts at `consumedThrough`, which
is what makes both its running size and the resume offset past a discarded
span exact; an unterminated oversized tail leaves the cursor at the
record's start so a still-growing record is re-read rather than guessed at.
Skips accumulate on the resume point keyed by start offset, and the scanner
reports them as a per-session `notice` so nothing is silently lost.
The budget itself is unchanged.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(runtime): persist acknowledged terminal tab retirement
* test(runtime): drain tab retirement fixture writes before teardown
* fix(runtime): explain a refused workspace terminal close
The Sleep-workspace path threw the raw refusal enum ("stale-terminal") as an
Error message, which reaches a CLI user verbatim and a Sleep toast via
describeSleepFailure. Map each refusal reason to a sentence instead.
Also pins two behaviours that had no coverage: the user-visible outcome of a
republished stale-terminal refusal on the web client (the caller cannot tell it
from a real close), and the one-call-per-close invariant that keeps a successor
terminal alive.
The bounded close retry was NOT implemented: notifier.closeTerminalTab carries
only a tab id, so a second call destroys whatever successor took that id.
* test(runtime): build refusal fixtures without type assertions
The changed-code quality gate rejects new `as` casts. Replace the
branded-outcome cast with refusedMobileSessionTabClose, and model the
wire-skew reason as a decoded host answer instead of `as never`.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(claude): enforce history window quota while reading
* test: repair history quota audit dependency and CI import
* fix(native-chat): record why restart reconciliation leaves work unconfirmed
Two silent paths hid the cause of an unconfirmed submission. The reconciler's
bare `continue` on an `unknown` outcome dropped the reason it already carried,
and the transcript read swallowed its error, collapsing an oversize file and a
genuine read failure into the same verdict.
Log both. No control flow changes.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(browser): bound CDP output for stalled clients
* fix(browser): log CDP outbound overflow before terminating the client
The outbound queue terminated the automation client silently on overflow, so
the client saw a socket close indistinguishable from a crash. Surface the cap
that tripped and the backlog held when it did.
The queue dropped its backlog before invoking onOverflow, so the counters were
already zero at the callback. Snapshot them first and pass them through.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix(terminal): align IME preedit to terminal cell grid
* fix(terminal): preserve native shaping and reuse IME preedit on repaint
* fix(terminal): preserve native shaping with bounded IME spacing runs
* test(terminal): account for inline preedit subpixel rounding
* test(terminal): keep the IME grid fixture wide at every DPI
* chore: regenerate xterm patch after rebase
* test(terminal): remove IME assertion lint findings
* test(terminal): avoid reflective IME fixture access
* test(e2e): run IME renderer matrix with WebGL available
* fix(ci): restore editor line budget
Merged after fresh run 35448889017 passed all required checks, including static analysis, typecheck, package jobs, all test shards, changed E2E, Docker SSH E2E, and verify.
Merge fully verified: all required CI checks pass. This lands bounded startup timing instrumentation for the open Windows OMP first-paint investigation in #19333; it does not claim the latency fix itself.
* fix(omp): answer startup Kitty queries before renderer handoff
Forward actual renderer capability through local and remote spawn. Preserve source ranges and following keyboard mode pushes, and retain independent ConPTY color authority.
Refs #17081. Secondary review: #17082.
Co-authored-by: stevelliu <stevelliu@tencent.com>
* test(omp): cover fragmented keyboard modes and ConPTY handoff
* fix: preserve keyboard startup intent without terminal colors
* fix: negotiate keyboard support for host-authoritative agent launches
* fix: keep terminal creation within line budget
* fix(omp): negotiate keyboard support for paired web launches
* test: remove obsolete message type import after main integration
* fix: validate paired launch results and retry incomplete SSH test snapshots
* fix(omp): negotiate keyboard support for background paired launches
---------
Co-authored-by: stevelliu <stevelliu@tencent.com>
* fix(omp): fence pane status to the root session manager
* test(omp): preserve root preview and recovery through child hooks
* test(omp): exercise status ownership through actual runtime runner
* fix(omp): honor runtime subagent provenance when available
* test(omp): avoid writes to the read-only hook status view
* fix(omp): preserve child status ownership provenance
* fix(omp): normalize child transcript paths across platforms
* fix(omp): clean status handler rebase
* fix(omp): keep prefill inside session ownership fence
* fix(runtime): detect a same-size terminal artifact swap the granted stat cannot see
A local terminal-artifact grant pinned the file as `dev:ino:nlink:size:mtimeMs`.
On Linux every one of those can survive an unlink+recreate: ext4 reuses the
just-freed inode (measured: 100% of the time), nlink and size are unchanged for a
same-size replacement, and the mtime clock is tick-quantized to 1ms, so a swap
inside one tick produces a byte-identical identity string. The grant then served
the attacker's bytes as if nothing had changed.
Local grants now also pin a sha256 of the artifact's content, taken from the same
handle as the stat so nothing can swap the file between them, and every local
read, preview and write re-checks it before returning or committing content.
The stat identity string itself is unchanged: the relay recomputes it verbatim to
honour `expectedStatIdentity`, so its format is a wire contract. Remote grants
keep the stat-only check and are untouched.
This is also the mechanism behind the intermittent
`orca-runtime-files-terminal-artifact-io.test.ts` failure on
`rejects stale absolute terminal artifact previews before returning changed
content`: it replaces an 8-byte artifact with 8 different bytes, so whenever the
two writes shared a 1ms tick the product genuinely could not tell them apart.
* docs(runtime): record what the terminal artifact grant checks do not close
The digest makes the same-size swap detectable; it does not make the sequence
atomic. A reader arriving at the access module would reasonably assume otherwise,
so write down the measured limits of the stat identity, why the identity string
cannot change, and the four windows that stay open — the write path's surviving
rename() gap above all.
* 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>
* fix(terminal): retire captured remote handles when pending panes close
A restored pane can hold a scoped `remote:<environment>@@<handle>` layout
binding while `remote.attach()` is still waiting for `terminal.resolvePane`.
The transport's `getPtyId()` is null, so an explicit split close passed null to
`closeWebRuntimeTerminal`, dropped the binding and destroyed only the viewer.
The host terminal stayed connected.
Only an exact scoped handle whose environment matches the owning workspace's
runtime authorizes the close. The provider helper captures the pairing
revision, runs its existing compatibility check, then rechecks pairing and
ownership immediately before dispatch.
Rebased onto main after #21001 was squash-merged. The previous head was a merge
commit that carried its own conflict-resolution content -- the runtime branch in
`terminal-pane-close-admission.ts` and the restored `it.each([false, true])`
parameter -- which a plain rebase drops along with the merge. Rebuilt from the
recorded net diff instead and verified byte-identical at 15 files,
906 insertions, 41 deletions.
* test(memory): rebase the pending runtime-close proof onto the squashed base
`fix.patch` recorded a baseline taken against #21001's pre-squash branch tip.
Squash-merging #21001 replaced that tip with a single commit, so the recorded
hunks no longer reverse-applied and `reproduce.mjs` aborted with
`Source changed: use-terminal-pane-close-actions.ts` -- confirmed by running it
before regenerating rather than assuming the rebase alone would fix it.
Regenerated against `main` and re-run: 5 pass / 10 fail before, 15 pass / 0
fail after, exit 0, and every `results.json` hash recomputed from the run
rather than hand-edited.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
* fix: avoid retaining foreign SSH file frames before metadata
* test(ssh): exercise empty metadata through the streaming mux fixture
* fix(ssh): fail the file read when beforeResolve never runs
Moving the metadata install from .then() to beforeResolve moved it from a
mandatory callback to an optional one, and handleResponse clears the request
timer before beforeResolve runs. That left "response fulfilled, metadata never
installed" with no deadline: the read never settled, holding its notification
and dispose closures until mux disposal. Before this PR the same state failed
after the 60s inactivity deadline.
Unreachable with the concrete mux, which calls resolve on the line after
beforeResolve, but the hook is optional in the type and nothing enforces the
pairing. The guard is a no-op on every real path: empty, missing streamId,
cap-exceeded and alloc-failure all settle first, and the success path sets
metadataReady.
Found during review of #21167; raised at
https://github.com/stablyai/orca/pull/21167#issuecomment-5726058832
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(terminal): retire explicitly closed pending split connections
* test(memory): keep pending split proof compatible with formatted source
* fix(terminal): confirm pending split retirement before stopping work
* fix(terminal): restore the pending split-close gates CI checks
Three CI gates were red on this branch and all three were this branch's own.
The hook-order parity snapshot did not count the `confirmedCloseRef` this
branch adds to `use-terminal-pane-close-actions.ts`. Dumping the flattened
order against clean `main` shows exactly one added `useRef` at position 148
and no reordering, so the count moves 211 -> 212 and the digest with it.
`pending-split-close-test-fixture.ts` is Vitest support code, but it sits
outside the `*.test` / `*.spec` / `tests` globs that already switch
`anti-slop/no-module-mocking` off, so the gate failed on all twelve of its
`vi.mock` calls. It carries a file-scoped disable with the reason, matching
`work-item-search-test-harness.ts`.
`fix.patch` still described the pre-confirmation shape of the close hook, so
`reproduce.mjs` aborted with `Source changed` and the cited ablation could not
run at this head. Regenerated against the committed sources; the harness again
reports 10 pass / 14 fail before and 24 pass / 0 fail after.
Merges `main` rather than rebasing: #21005 is stacked on this branch.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
The renderer expressed every non-answer as one nullable `status`, so a probe in
flight, a probe that failed, a host that refused us and a retired pairing all
reached readers as the same `null` -- and readers spent that `null` on decisions
of very different weight, including destructive ones.
`RuntimeHostContact` names the four. Nothing changes yet: the connection-state
derivation is rewritten on top of it and a 384-case parity table asserts the
result is identical to a frozen copy of the old one on every combination of
verification, transport, retired, answered and remote-control state.
* fix(chat): release provider children after lost resume holds
* test: load audit fixtures as modules and verify combined mobile payload
---------
Co-authored-by: m4air <m4air@Mac.localdomain>