Admission for a renderer session write consults pairing authority, which can
throw `folder_workspace_connection_ambiguous` for an unrelated workspace. Every
admission call site treated that throw as "not admitted" and silently dropped
the write, so one ambiguous folder workspace could discard a user's session save
on `session:set`, `session:patch`, `session:set-sync`, and the before-unload
staging checkpoint. Those paths now admit the write and log; a resurrected
partition is recoverable on the next unpair, a lost save is not.
GUI unpair did not catch that throw at all, so an ambiguous verdict surfaced as
an opaque removal error. It is now caught and read as custody held, which
completes the unpair and preserves the partition — deletion still requires a
positive "nothing owns this" verdict.
The checked-in audit artifact was computed on a tree stacked on #21113 and
cannot be re-baselined by editing its pinned commit; its prose is corrected to
match the shipped code and its recorded proofs are marked stale.
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>