Commit Graph
910 Commits
Author SHA1 Message Date
Neil fdfe354045 test(relay): bind test WebSocket servers to loopback
A control-handshake test that expects a timeout was instead getting
'Unexpected server response: 401' about once in fourteen runs. A slow machine
cannot turn a timeout into a 401 -- that needs a real HTTP response, so the
connection was reaching a different server.

new WebSocketServer({ port: 0 }) binds the wildcard address while the client
dials 127.0.0.1. On macOS those differ, and with SO_REUSEADDR a foreign process
can hold the more specific 127.0.0.1:P and win the connection. Caught live: a
wildcard bind took port 52584, which a running Orca app already held on
loopback, and Orca answered the probe. A listener that checks a token answers
401.

Ten constructions across seven files now pass host: '127.0.0.1', so the
reservation covers the address the client dials and a duplicate bind is refused.

Adds a ratchet, because this is not authors forgetting a convention: all 30+
.listen(0, ...) sites already pass '127.0.0.1', while 7 of 7 ws constructions
did not. ws accepts { port } alone and binds the wildcard silently, so nothing
told them. The guard pins the wildcard count, and pins separately at zero the
option shapes it cannot read -- spreads and variable option objects fail rather
than being exempted, and a recognized-construction floor catches the matcher
going blind, which otherwise reads exactly like a clean tree.

mobile/scripts/mock-server.ts stays on the wildcard deliberately: a phone
reaches it over the LAN.
2026-09-01 19:05:42 -07:00
Neil 4efc86a33c feat(app): open Markdown files from the OS in the floating workspace (#17906)
* feat(app): open Markdown files from the OS in the floating workspace

Registers Orca as a Markdown handler on macOS, Windows and Linux, and opens
an OS-handed .md/.markdown/.mdx file as a floating-workspace editor tab —
the one editor surface that needs no project. Works cold-start and when
Orca is already running.

Main buffers the paths and both pushes to a live renderer and answers a
pull on renderer mount, mirroring SkillShareDeepLinkState. The buffer is
only released once delivery is possible: the renderer's pull is what proves
its ui:openMarkdownFiles listener is attached, because a push into a window
whose renderer has not subscribed is dropped by Electron with no error. Both
the push and the pull restore an undelivered batch, and a renderer reload
clears the latch so the fresh renderer re-proves itself. Paths are stat'd and
proven to be files before authorizeExternalPath sees them.

Windows association is registered by hand in the NSIS include rather than
through electron-builder's `fileAssociations`: app-builder-lib emits
APP_ASSOCIATE, whose first line overwrites Software\Classes\.md's default
value with no backup — silently taking .md from whichever editor owns it,
for every existing user on their next update — and APP_UNASSOCIATE never
restores it. The hand-rolled registration is additive (ProgID +
OpenWithProgids + SupportedTypes) and leaves the user's default alone;
verified end to end on a real Windows 11 host.

Co-authored-by: Wooseong Kim <innocarpe@users.noreply.github.com>
Co-authored-by: Jaydev <java-jaydev@users.noreply.github.com>

Closes #10138

* fix(os-open): register the new listener in the IPC inventory, and guard a non-array payload

CI caught two things the local run did not.

useIpcEvents-lifecycle.test.ts is an inventory of every App-lifetime IPC
listener and the exact order they register in; ui.onOpenMarkdownFiles now
appears there, positioned after the workspace-shortcut bridge's last
listener, which is where it actually registers.

Chasing that failure surfaced a real gap: the pending-open payload crosses
the preload boundary, so a stale or mismatched preload can resolve with
something that is not an array, and reading .length off it threw inside the
promise chain instead of failing at the boundary. Array.isArray now gates it,
with a regression test.
2026-09-01 17:35:38 -07:00
Neil 519af49a58 fix(dev): keep the shared Electron dist writable for the dev app
pn dev crashes on macOS in any worktree that adopted the shared Electron dist.
publishSharedElectronDist marks the cache entry read-only, which hardlink
sharing needs, but clonefile preserves mode -- so the dist lands 0555, the dev
runner copies it into out/electron-dev unchanged, and the first plutil -replace
on Info.plist fails with a permission error. The shipped zip has that file at
0644; on disk it is 0555, so the mode is ours, not upstream's.

copyPrivateTree now restores write permission. Its contract is a private tree
the caller goes on to patch, and its one production caller is the dev runner.

The test that should have caught this ran the wrapper with stdio: 'ignore', so a
hard crash presented as a bare 20s timeout. It now captures the wrapper's output
into the failure message, and waits long enough for the two synchronous swiftc
builds and a codesign --deep over ~280MB that precede the assertion.
2026-09-01 17:16:26 -07:00
Neil da4a83bd22 fix(linux): give the CLI one entrypoint by extracting the AppImage once 2026-09-01 14:06:16 -07:00
Neil 2be53521a7 refactor(task-page): fold the task page into one provider-grouped tree
The page had two competing splits: an Aug-25 folder split that the Aug-30
oversized-surfaces split stranded, and the 47 flat files that replaced it. The
orphaned tree had no non-test importers, yet eight ratchet files still asserted
against it, so their invariants stopped constraining shipping code -- which is
how six regressions reached main unnoticed. Those ratchets were repointed and
the regressions fixed earlier; this removes the tree they were guarding.

Moves the live files into task-page/{github,gitlab,jira,linear} and drops the
now-redundant prefix, matching the new-workspace sibling.

Makes the source-family walker recursive first: it listed a single flat
directory, so moving the files under it would have emptied the family and turned
every ratchet built on it into a no-op without failing.
2026-09-01 13:16:43 -07:00
Jinwoo Hong 69fff5eaa3 fix(runtime): defer websocket heartbeat startup probe (#17810)
* fix(runtime): defer websocket heartbeat startup probe

* fix(runtime): defer heartbeat probes until websocket auth
2026-09-01 14:00:11 -04:00
Neil 5b4e7edb50 refactor(main): split backend services and startup
(cherry picked from commit a33573328b)
2026-09-01 02:07:33 -07:00
Neil c475d23d9b chore(max-lines): prune preload baseline entry
(cherry picked from commit aad2e1ec54)
2026-09-01 00:57:54 -07:00
Neil d1abe28471 refactor(preload): split bridge API modules
(cherry picked from commit b77e31873b)
2026-09-01 00:57:54 -07:00
Neil ca12be1921 chore(max-lines): prune runtime baseline entries
(cherry picked from commit 08820a4386)
2026-09-01 00:28:37 -07:00
Neil e8e94ecaab chore(max-lines): prune stale lifecycle baseline
(cherry picked from commit 42bf3eb674)
2026-09-01 00:06:23 -07:00
Neil b94a65a4fc fix(lint): preserve TaskPage effect suppressions after split
(cherry picked from commit 4a3bc23670)
2026-09-01 00:06:23 -07:00
Neil cc2451ca25 chore(max-lines): prune refactored file suppressions
(cherry picked from commit 787228ee46)
2026-09-01 00:06:23 -07:00
Neil 451002ba1a fix(mobile): honor host-follow tab snapshots
(cherry picked from commit a4a9c4da19d6967eceb8025dd01480a56261039f)
2026-08-31 23:35:08 -07:00
Neil d7d3114716 perf(wsl): single-flight the async WSL distro list (#17805)
On Windows, seven production call sites reach listWslDistrosAsync and on a cold
cache each spawned its own `wsl.exe --list --quiet` (5s timeout each): the
wsl:listDistros IPC behind the renderer capability read, the host.wsl.listDistros
RPC, the skill-install IPC, CLI registration reconciliation, the hook relay deps,
the kimi runtime home, plus relay preflight in the relay process. Concurrent
callers in one process now share one spawn.

Joining happens ahead of the negative cache, which also fixes a stranding bug: a
synchronous listWslDistros() landing an empty result mid-probe arms the 15s retry
window, and later async callers read that [] even though the pending probe is
about to see a distro that just finished provisioning. The non-empty-cache
short-circuit sits ahead of the join so a list already found synchronously is
still returned without waiting; that is main's existing behaviour preserved, not
a new fast path.

The shared promise cannot reject -- `catch` sits ahead of the stored promise, so
joiners get the same fail-safe [] the old per-caller catch returned -- and the
slot is cleared on settle, by the owning probe only.

wsl-directory-probe-command.ts is a verbatim move of the guest directory-probe
marker protocol and its parser out of wsl.ts, for oxlint max-lines headroom:
inlining it back makes wsl.ts 306 effective lines against a cap of 300. It takes
WslUncPathInfo from ../shared/wsl-paths -- the actual type of every value passed
at both call sites -- so it does not import from wsl.ts. _resetWslCachesForTests
and _setWslCachesForTests now share one resetWslDistroListState() instead of
repeating the same six assignments.

Per-platform delta:
- WSL on Windows: fewer wsl.exe spawns under startup fan-out, and a distro
  provisioned while a probe is pending is no longer hidden for the retry window.
- Native Windows without WSL: no behavioural change. The empty/failure retry
  windows, their backoff and the cache sequence guard are unchanged; N concurrent
  callers now cost one failed spawn instead of N.
- macOS, Linux, folder workspaces: no change. Both new early returns are
  unreachable off win32.
- SSH remote: no change for macOS/Linux hosts; a remote Windows host gets the
  Windows behaviour in its own process. No wire change -- host.wsl.listDistros
  keeps its string[] shape and its [] failure value.
- Relay: same single-flight inside the relay process. It stays per-process; the
  relay and main process still probe independently, as before.

Costs: a never-settling execFileUtf8 now pins the shared slot for the process
lifetime rather than only its own callers -- transient-to-permanent, not identical
exposure. And a joiner inherits the first probe's failure instead of making an
independent attempt.
2026-08-31 22:37:23 -07:00
Neil 2e30187560 feat(dev): sweep the backlog of idle dev Electron bundles (#17803)
* fix(dev): make reclaim report real sizes on Windows and keep setuid intact

Two bugs found by running the reclaim script on real Linux and Windows hosts.

The size report shelled out to `du`, which does not exist on Windows, so every
worktree measured 0 bytes and the script reported nothing reclaimable on the
platform with the largest dist (374MB). Walk the tree in Node instead.

makeTreeReadOnly chmod'd files to a flat 0o555, which clears setuid. On Linux
that would silently strip the bit from chrome-sandbox if a developer had run
the usual `sudo chown root && chmod 4755` workaround -- and under hardlink
sharing it would strip it from every worktree and the cache at once. Clear the
write bits and nothing else.

Measured after the fix: 7.30 GiB across 23 worktrees on one Windows host and
18.31 GiB across 56 on another, both previously reported as 0.

* feat(dev): sweep the backlog of idle dev Electron bundles

out/electron-dev holds one ~275MB patched Electron.app per branch title x
Electron version. The dev runner already prunes them, but only inside the
worktree it is starting and only when that worktree holds more than one bundle
-- and a worktree almost always holds exactly one, so the sweep returns early
every time and nothing ever reclaims another worktree's bundle.

pnpm reclaim:dev-bundles sweeps across every worktree of the repo. Bundles are
pure build output that pnpm dev rebuilds on demand, and rebuilding is cheap now
that the Electron dist is shared.

Reuses the runner's own staleness rules, so a bundle a live process is running
from, or one whose build is still in flight, is never removed. Refuses to run
at all if the process table cannot be read, rather than guessing.

Measured: 120 bundles, 32.2 GiB, on one machine.

Also guards both reclaim scripts behind a direct-invocation check; importing
one for tests previously ran a full sweep at import time.
2026-08-31 22:18:50 -07:00
Jinwoo Hong e2f326cad7 ci(release): prevent signing on workflow reruns (#17802) 2026-09-01 01:05:00 -04:00
Neil abe1d30881 fix(dev): make reclaim report real sizes on Windows and keep setuid intact (#17800) 2026-08-31 21:12:43 -07:00
Neil fe0f2f9be7 perf(dev): share one Electron dist per repo instead of per worktree (#17664)
* perf(dev): clone one Electron dist per repo instead of per worktree

Every worktree extracted its own ~295MB node_modules/electron/dist, measured
at 69GB across 241 worktrees on one machine.

Extract once per repository into <git-common-dir>/orca-cache/electron, then
APFS-clone it into each worktree: copy-on-write, so the second worktree
allocates ~0 bytes and still gets a real, private, writable directory.

Hangs off install-electron-package-binary.mjs, inside the transaction it
already uses to swap dist. Every cache path returns a boolean and false means
"install normally", so non-APFS, cross-volume, corrupt entry, no Git, folder
workspace and CI all keep today's behavior. No symlinks, no lifecycle changes.

out/electron-dev's per-branch Electron.app copy clones too, via the same helper.

Refs #13709

* perf(dev): share the Electron dist on Linux and Windows too

Extends the shared dist cache beyond macOS APFS. Three mechanisms, strongest
isolation first:

  macOS APFS    cp -c              private copy-on-write
  Linux btrfs   cp --reflink       private copy-on-write
  ext4 / NTFS   hardlink + 0555    shared inodes, forced read-only

Reflinks cover btrfs/XFS/bcachefs/ZFS but not ext4, and Windows block cloning
is ReFS-only, so most Linux and effectively all Windows developers need
hardlinks to get any saving at all. Extracted dist is 327MB on linux-x64 and
374MB on win32-x64, both larger than macOS.

Hardlinks share inodes, so a write through one worktree would rewrite every
sibling and the cache. Nothing in this repo writes inside dist -- every
mutation replaces the directory via rename -- but Electron's own install.js
extracts over an existing dist with O_TRUNC, and is reachable through
`pnpm rebuild electron`. Publishing the entry read-only turns that from silent
cross-worktree corruption into EPERM. Directories stay writable so the install
transaction's renames and unlinks still work.

out/electron-dev's per-branch Electron.app is patched and codesigned after it
is copied, so it uses copyPrivateTree, which never hardlinks.

Refs #13709

* test(dev): keep shared-dist tests honest across ext4 and NTFS

Verified on real hardware: Ubuntu 24.04/ext4 (no reflink support, so the
hardlink tier is the only thing that helps there) and Windows/NTFS.

Three tests faked platform: 'darwin' while invoking the real mechanism, so
they failed on Linux where /bin/cp -c does not exist. Mechanism selection is
now asserted with injected stubs; real filesystem behavior is asserted against
whatever the host actually supports.

Windows maps chmod onto the read-only attribute alone, so a directory never
reports 0o755 and a read-only file reports 0o444. Mode-bit assertions that
encoded POSIX semantics are now behavioral (the tree stays removable), and the
executable-bit assertion is POSIX-only -- confirmed on NTFS that a read-only
hardlinked .exe still runs.

* fix(dev): stop a losing publisher from discarding a good cache entry

Greptile caught a TOCTOU in the shared Electron dist cache. Quarantining an
invalid entry happened before sharing the replacement tree, which takes
seconds -- long enough for a sibling worktree to publish a good entry that this
one would then rename away. If the follow-up publish also failed, the cache was
left empty and every worktree re-downloaded.

Stage first, then re-validate immediately before the destructive rename, so an
entry that became good during the share is kept. On a failed swap, restore the
quarantined entry instead of leaving no entry at all: a stale entry still beats
an empty cache, because the next publisher re-validates and replaces it. An
entry that cannot be validated is never displaced, matching the pre-staging rule.

Also covers the Electron upgrade path end to end: a version bump gets its own
cache entry and leaves the previous one for worktrees still on the old branch.

* feat(dev): add a script to share existing worktrees' Electron dists

An install only shares when Electron is (re)installed, and rebuild-native-deps
returns early when the package is already usable -- so a worktree that already
has a working dist never reaches the sharing path and keeps its own copy until
the next Electron upgrade.

pnpm reclaim:electron-dists reports what it would share; --apply does it.
Each worktree is converted behind a rename, so an interrupted run leaves a
working dist either way, and any worktree that fails is left untouched.

Measured on one machine: 677 worktrees, ~195 GiB reclaimable.

* fix(dev): keep the reclaim script's error formatting type-safe
2026-08-31 20:35:54 -07:00
Jinwoo Hong 69120d5402 ci(release): tolerate legacy tags without source maps (#17788)
* test(e2e): seed source control diff before opening panel

* ci(release): tolerate legacy tags without source maps
2026-08-31 23:07:50 -04:00
Neil a5796ec8eb refactor(runtime): split OrcaRuntimeService and compatibility tests (#17605)
* refactor(runtime): split OrcaRuntimeService into focused modules

* test(runtime): cover admission tiers and strict worktree reconciliation

* fix(runtime): preserve owner and structured session visibility

* fix(runtime): port post-extraction compatibility fixes

* fix(runtime): preserve skill-share cancellation barrier

* test(runtime): update identity inventory after extraction

* fix(runtime): preserve hook transport environment cleanup

* fix(runtime): consolidate idle probe imports

* test(runtime): retire split file process allowlist entry

* fix(runtime): route child process types through shared boundary

* test(runtime): preserve worktree host metadata precedence

* fix(runtime): update extracted test seams

* fix(runtime): gate the split's ts-nocheck set and restore the stop-confirmed contract

Audit follow-ups for the OrcaRuntimeService split:

- Freeze the 171 @ts-nocheck files behind a ratchet so no new file can disable
  type checking. The split's linear mixin chain cannot express forward
  references yet, so the existing suppressions are grandfathered; the baseline
  may only shrink.
- Drop the stray @ts-nocheck at the end of orca-runtime-get-status.ts. It sat
  after the first statement, where TypeScript ignores it, so the module was
  already checked.
- Restore `retireRejectedPty(ptyId, stopConfirmed: boolean)` as a required
  argument. The split widened it to optional and patched the resulting error
  with `stopConfirmed === true`; an omitted argument would have silently taken
  the unverified-stop path instead of failing to compile.
- Guard that every orca-runtime-tests fragment is imported by the compatibility
  entrypoint. The fragments are .spec.ts, which no Vitest include glob matches,
  so one left out of the list would silently stop running.

* fix(runtime): restore four behaviors the OrcaRuntimeService split dropped

Audit findings against the refactor's true base (ad5ba2572e):

- retirePtyAgentLaunchAuthority collected pane keys after deleting the
  restored-authority receipt instead of before it. collectPaneKeysForPty reads
  that receipt, so a receipt-only pane lost its key and never had its agent-hook
  compatibility authority retired. on-pty-exit.ts already carried a comment
  naming this exact invariant.
- The PTY-exit path kept orchestrationMailboxNotifications.retirePty but lost
  the loop that schedules a debounced mail-pointer repoint for the dead pty's
  terminal handle and any run bound to its panes. Restores the schedule call
  count to 7, matching base.
- subscribeToPtyExit lost isPtyKnownExited's leaf fallback and its
  post-registration lifecycle-generation recheck. leavesByPtyId is rebuilt from
  the renderer graph independently of ptysById, so a leaf can outlive its pty
  record; without the fallback a caller waiting on an already-dead pty never
  gets released.
- The chain root declared `[key: string]: unknown`, which base had nowhere. It
  leaked through the exported runtime type into every consumer, so any misspelled
  member access typechecked as unknown instead of erroring, and it accounted for
  957 of the suppressed errors. Removing it costs zero type errors.

* fix(runtime): restore escalation prose and unscoped automation publication

Two more behaviors the split dropped, each with a regression test that fails
against the pre-fix code:

- The worker-exit escalation stopped deriving its title through
  buildOrchestrationTaskDisplayMetadata and inlined `task.spec` instead. That
  ignored an explicit task_title, dropped the single-line normalization and the
  80-character bound, and turned the no-spec case into a quoted, duplicated id.
  A multi-paragraph spec landed verbatim in the coordinator's banner. The
  existing 11 tests all use short single-line specs, where the derived title and
  the raw spec are identical, so none of them could see it.
  Also reverts an added `if (!handle) return` guard: the dispatch lookup is
  deliberately keyed on the pane as well, because a reminted handle no longer
  matches the row while the pane identity outlives the remint.
- updateAutomation stopped going through automationChangePublications and
  published `source` unconditionally while gating the fallback on a non-null
  destination. A destination the store can no longer name then published only
  the stale source, so subscribers scoped elsewhere kept rendering a row that
  had left them — the exact case the helper documents. The helper had been left
  with zero callers; all three sites use it again.

* fix(skills): stop swallowing lookup errors and hard-erroring on non-ssh hosts

Follow-ups from auditing the skill install path against the refactor's base:

- resolveWorktree wrapped showManagedWorktree in `.catch(() => null)`, so a
  transient git or IO failure surfaced to the user as
  skill-install-workspace-not-found with the real cause discarded. Errors
  propagate again; a genuine id mismatch still returns null.
- resolveSkillSshTarget threw skill-install-workspace-host-unavailable when the
  execution host was neither local nor ssh, on both the repo and folder
  branches. Base gated these on connectionId, so a runtime-owned repo simply
  was not an SSH install and fell through to the local path. Both return null
  again, and the error code the split invented is now unreferenced.
- listManagedSkillInstalls awaited the receipt walk and the worktree resolve in
  sequence. They are independent and either can hit disk, WSL, or an SSH scan,
  so Promise.all is restored.

Deliberately unchanged: resolving the worktree through listResolvedWorktrees
rather than showManagedWorktree, which disambiguates a worktree id colliding
across hosts and is covered by its own test, and the SSH-folder
skill-install-ssh-dispatch-required throw, which matches the repo branch.

* fix(runtime): merge duplicate worktree-logic imports

The #17448 port added a third import from ../ipc/worktree-logic, which the
code-quality oxlint config rejects under --deny-warnings. Plain oxlint does not
flag it, so it only surfaced in CI's static analysis job.

* ci: run the ts-nocheck ratchet in PR checks

pr-workflow-lint-parity requires every leaf command in `pnpm lint` to have a
matching step in pr.yml. The ratchet was wired into lint but not the workflow,
so PR CI would not have enforced it.

* Merge remote-tracking branch 'origin/main' and retry the paired-host launch evaluate

main advanced 9 commits; none touch the orca-runtime.ts this branch splits, so
nothing needed porting.

CI failed twice on `Execution context was destroyed` thrown from
headless-paired-runtime-host's first `evaluate` after launch — a different spec
each run, which is the signature of the flake #17780 describes rather than a
regression. That commit added retryTransientMainEvaluate and adopted it in five
helpers but not this call site, even though its docblock names exactly this
case: the first evaluate after electron.launch() resolves, before the app is
ready. Wrapped it the same way.
2026-08-31 19:34:55 -07:00
Neil f116d2ca2a test(ci): retry Windows teardown EPERM and restart evaluate misses (#17780)
Restart-survival polls treated a recycled renderer as a hard failure.
Wrap those evaluates so "Execution context was destroyed" is a pending
miss. Windows package-lane teardowns after a force-kill used rmSync
with force:true only, which does not absorb EPERM; put them on the
shared maxRetries:8 policy.
2026-08-31 18:53:01 -07:00
Jinjing d2aab68ae7 Automations ux improvement (#17626)
* Add keyboard navigation to automations UI

Improves workflow efficiency by enabling keyboard-driven navigation
across automations list, run history, and detail pane tabs.

* Add Escape key support to automations detail pane

Pressing Escape now clears external and automation run page views,
then returns to the automations list. Also improves cross-browser
compatibility of keyboard event handling by using Element checks and
getAttribute instead of dataset access.

* Fix keyboard navigation to let Enter key reach focused controls

- Enter key now passes through to focused buttons, links, and other interactive controls
- Arrow key navigation through automation run history still works
- Prevents intercepting native keyboard behavior of interactive elements

* improve test

* Move keyboard focus to follow row selection

When navigating automation runs with arrow keys, focus must follow the selection so Enter key acts on the newly selected row rather than the previously focused one.
2026-08-31 18:37:19 -07:00
Neil 2222e54754 refactor(test): organize SSH and terminal recovery fixtures (#17751) 2026-08-31 18:18:15 -07:00
Jinwoo Hong 40d245fe45 ci(release): gate signing behind release preflight
Prevents SignPath requests until all blocking release gates pass.
2026-08-31 21:17:31 -04:00
Neil c558d7e083 Activate terminal splits before inherited CWD resolution (#17601)
* perf(terminal): activate splits before cwd resolution

* test(terminal): prove split focus before cwd publish

* fix(terminal): release stale split cwd fence

* test(terminal): add visible split activation latency benchmark

* docs(reliability): clarify split benchmark provenance

* fix: preserve deferred split handoffs across remounts

* fix: fence late deferred split closes

* docs(reliability): record exact split benchmark runs

* test(reliability): fail benchmark on artifact write errors

* test(reliability): attribute split activation phases

* docs(reliability): record schema-v2 split benchmark

* refactor(terminal): collapse duplicated split-handoff and write-queue paths

- Drop the discardDeferredSplitPaneHandoff alias for its identical clear twin.
- Fold the deferred-cwd resolve/reject settle handlers into one applier.
- Extract settlePaneCwdDeferredSpawn for the repeated read-clear-write pattern.
- Share one head-index FIFO primitive between the ordinary and reply queues.

* fix(terminal): stop retaining a promise reaction per acknowledged write

Racing every accepted write against one queue-lifetime cancel promise kept a
reaction record alive until that promise settled: 200k acknowledged writes
retained 88.6MB, now 0.1MB. Give each in-flight write its own cancel, and
split the shared FIFO primitive into its own module.

Also sanitize the split-latency benchmark report at its single serialization
point so shared artifacts no longer carry the machine-local repo path or
unbounded cleanup error text.

* fix(terminal): settle deferred split input when the spawn is abandoned

An abandoned deferred spawn returns before transport.connect(), so nothing
drained the pre-connect buffer: sendInputAccepted's promise never settled and
a paste into that pane hung forever. Clear the buffer on the abandon fence.

Also re-derive the pre-connect retention cap from the clipboard-paste ceiling
rather than the 16MB single-write ceiling; it is held twice per pane across up
to 64 deferred splits, so 5.59M code units guarded the wrong thing.

* fix(terminal): release the deferred cwd fence on a rejected reattach

A daemon createOrAttach can turn an apparent fresh spawn into a reattach; when
that reattach is refused the spawn ends with deferredSplitSpawn/pendingCwd
still set, permanently arming the pre-bind detach refusal. The release no-ops
when a PTY did bind, so it only fires where the fence would otherwise leak.

The stale-generation return above is deliberately left alone: a newer connect
already owns the pane there, and the fence is not generation-scoped.
2026-08-31 16:45:36 -07:00
Jinwoo Hong 8f15f217a2 Preserve user-set workspace names across branch changes (#17448)
* fix(worktrees): preserve user workspace names across branch changes

* test(worktrees): cover pinned rename metadata

* fix(workspaces): address display-name review edge cases

* fix(workspaces): keep automatic names fresh across refreshes

* fix(workspaces): preserve legacy CLI labels

* fix(workspaces): preserve display-name provenance across hosts

* fix(workspaces): honor legacy display-name provenance

* fix(workspaces): fence display-name refresh races

* fix(workspaces): accept peer renames from provenance-less hosts

The old-host preserve fence kept a pinned local label on every refresh,
which also suppressed a legitimate rename another client persisted
through the same host until app restart. Narrow it to labels the host
re-derived itself (branch short name, or path basename when detached);
any other changed label in a mode-less response is explicit meta a peer
wrote there. Stale prior-label responses stay covered by the downstream
staleness fence, in-flight writes by the pending fence.

* refactor(workspaces): unify display-name pin derivation

Three call sites (renderer optimistic update, local IPC updateMeta
handler, remote worktree.set handler) each restated the same formula;
a future edit to one would silently skew provenance between paths.
2026-08-31 19:08:05 -04:00
Jinwoo Hong b44ef1e59d fix(skills): narrow computer-use discovery boundary (#17736)
* fix(skills): narrow computer-use discovery boundary

* chore: remove merge-formatting noise

* fix(skills): name browser page automation surfaces
2026-08-31 18:57:52 -04:00
Neil 20a12a6a46 perf(codex): share one launch-prep hook install across a spawn burst (#17669)
* perf(codex): share one launch-prep hook install across a spawn burst

Codex launch prep runs a full managed-hook install on every local PTY
spawn, and both install lanes serialize globally per Codex home. Opening
a multi-pane worktree therefore paid N full installs back to back, and a
resumed Codex pane prepares twice. Concurrent spawns for the same runtime
home now share one run; the promise is dropped as soon as it settles, so
the next launch still re-reads hooks.json and the user's trust state.

Also split the `host_env` spawn-timing phase, which spanned the entire
Codex preamble and pinned that cost on the env builder that ran last.

* refactor(codex): unify the two hook-install single-flight lanes

Both the WSL and launch-prep lanes now share one generic in-flight helper
instead of duplicating the map bookkeeping. Also routes the WSL launch-prep
install through the serialized variant, which closes the same per-spawn
serialization gap on WSL that the native lane just got.

* refactor: extract the shared in-flight run dedupe

The codex hook service and the GitHub conflict-summary cache had grown
near-identical private copies of the same single-flight helper. Both now
use one module, which also keeps the hook service clear of the 300-line
budget. The shared copy keeps the identity check on clear so a late settle
cannot evict a newer entry for the same key.
2026-08-31 15:16:38 -07:00
Jinwoo Hong faaf38ac45 fix(orchestration): submit staged mail pointer while working (#17470) 2026-08-31 14:25:34 -04:00
NeilandBrennan Benson fbe94ceff6 fix: close readiness gaps found by merged-change audit (#17159)
* fix(ssh): fence stale kills and retired pane replay

* fix(ssh): support cancellable interactive authentication

* fix(ssh): await remote catalog before snapshot adoption

* fix(pty): contain Windows ConPTY input failures

* fix(power): avoid redundant macOS display blocking

* perf(editor): narrow markdown override subscriptions

* fix(quick-open): close directory handles after reads

* refactor(linux): remove unused proc socket scanner

* fix(usage): apply flat Sonnet 4.6 pricing

* ci: prime Node next native test cache

* docs(skills): resolve snapshot cleanup data path

* fix(ssh): recover install locks after host reboot

* test(ssh): recognize boot-aware install locks

* test(ssh): prove previous-boot lock recovery live

* test(wire): pin pre-metadata release coverage

* fix(terminal): preserve remote tab ownership through recovery races

* test(runtime): fence replaced terminal handles in agent guard

* fix(ssh): preserve remote snapshot authority across polls

* fix(pty): contain late ConPTY output EPIPE

* test(pty): register Windows exit watcher before kill

* fix: close SSH and tab readiness race gaps

* fix(tabs): retain headless order and placeholder titles

* fix(build): avoid parallel electron-vite config race

* test(windows): avoid MSYS temp path rewriting

* test(windows): avoid killing exited PTY

* fix(pty): avoid late ConPTY input teardown race

* fix(terminal): sync reconnect error ownership after commit

* fix(runtime): use canonical worktree identity comparison

* test(ssh): assert complete cold-hydration baseline

* test(windows): invoke quoted retention fixture via PowerShell

* test(windows): read ConPTY grid through mode con

* fix(terminal): publish PTY replacements atomically

* fix(terminal): infer stale identity on reattach

* fix(terminal): fence stale pane PTY callbacks

* fix(terminal): fence stale pane binds after rebind

* fix(terminal): reject stale pane transport callbacks

* fix(terminal): fence mirrored reattach spawn callbacks

* fix(terminal): replace stale pane PTYs on remount

* fix(ci): size the Windows launcher-compile test budget from measurement

`native-smoke (windows-latest)` fails ~4.5% of runs on
`preserves a multiline argument through the compiled remote launcher`
with "Test timed out in 15000ms" — on unrelated PRs, for reasons that
have nothing to do with them. Across 176 sampled attempts it is the only
red that job produced, and it hit seven different PRs in two days:
#16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085.

The test is six process creations: powershell.exe forks csc.exe, then
the freshly compiled orca.exe forks node.exe, twice. Hosted Windows
runners periodically slow process creation down, and this test amplifies
that far harder than anything else in the job. Comparing the 80 attempts
where it ran under 3s against the 12 where it ran over 12s, its own
median goes 2198ms -> 15917ms (7.2x) while the same file's
powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash
process tests in the neighbouring file move 1.4x, and the other 35 files
put together move 1.5x.

Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms,
correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%)
exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s
testTimeout, so deleting the override and inheriting the config is not
enough on its own. 60s clears all 176 with 1.7x headroom on the worst.

This is slow, not hung. Every body here is synchronous spawnSync, so
Vitest cannot interrupt one — the timer fires only after the body
returns and the reported duration is real elapsed time. That is why a
failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The
work finished; the stopwatch was short. Seven reruns at one identical
head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the
last of those would have been red on code that had not changed.

The 15s came from #8897, which raised this test off Vitest's built-in 5s
default because the job then ran bare `pnpm vitest run`. #8909 landed
3h27m later and pointed the job at config/vitest.config.ts, which is the
real fix for that. The constant stayed behind and has been the binding
budget ever since.

* fix(terminal): fence stale remount reattach ownership

* fix(terminal): reconcile mounted pane identity after replacement

* fix(terminal): fence stale reattach fallback ownership

* fix(terminal): fence deferred SSH reattach ownership

* fix(terminal): fence stale split pane ownership callbacks

* fix(terminal): keep stale spawns from consuming startup

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-08-31 08:17:40 -07:00
Neil 75e5c996c1 perf(relay): stop ACK boundary scans at first pending boundary (#17491)
* perf(relay): stop ACK boundary scans at first pending boundary

* test(relay): pin PTY source boundary cleanup and guard ascending sends

The early-`break` in advanceCredit is only correct while sentBoundaries is
inserted in ascending sentEndSu order. Turn that implicit invariant into a
throw at the sole live write site (commitPtySourceSend), and assert the
post-state directly instead of inferring it from an iteration budget:

- assert the surviving boundary set after the 1,023-ACK benchmark
- cover the jump-ahead cumulative ACK that must delete many boundaries in
  one pass (the case an over-eager `break` would get wrong)
- cover the settleReservedPtySourceAck -> advanceCredit entry point
- drop an arithmetically-implied assertion and CI benchmark log noise

* perf(relay): reclaim ACK boundaries with a monotone cursor

The early-break Set scan still rebuilt a Set iterator per ACK, so V8 walked
delete tombstones and the drain stayed superlinear; the visit-count test could
not see it because it stubbed sentBoundaries with a generator over a private
Set. Replace the Set with an ascending boundary list plus a monotone cursor,
assert the real structure, and add a benchmark over the shipped code.

* test(relay): enforce ascending sent-boundary inserts in the collection

Move the ascending-order precondition into PtySourceSentBoundaries.add so
both insert sites are covered, and assert per-ACK span reclamation in the drain.

* test(relay): collapse ledger test record accessors into getDeliveryRecord

Rebase onto #17490 left two structurally identical internals accessors
(getCursorRecord, getBoundaryRecord); one typed accessor covers both.
2026-08-31 03:10:44 -07:00
Neil 97eb762b27 refactor(packaging): prune declaration and source-map artifacts in one walk (#17659)
* refactor(packaging): prune declaration and source-map artifacts in one walk

prunePackagedRuntimeTypeDeclarations and prunePackagedRuntimeSourceMaps
were byte-identical apart from their regex, and each did its own full
recursive walk of packaged Resources/node_modules (~1.7s per walk).
Collapse them into prunePackagedRuntimeTypeAndSourceMapArtifacts, which
runs a single walk with the OR of both predicates.

The two regexes are disjoint (.d.ts.map never ends in .js.map), so one
pass deletes exactly the union the two passes deleted. Neither old
function had a production caller outside prunePackagedRuntimeNodeModules,
so both exports are replaced by the combined one rather than kept as
wrappers, which would have reintroduced the duplicate walk.

Also moves prunePackagedZodSources ahead of the filename walk: zod/src is
removed wholesale, so traversing it first was pure wasted work. The
prunes are independent, so the reorder does not change the result.

* fix: correct the one-walk rationale and close the .d.mts coverage gap

The comment credited predicate disjointness for making the merge safe. That
is not the reason and is misleading: it implies a future overlapping
predicate would break the collapse. Passes commute because
pruneMatchingFiles only deletes files and never removes directories, so the
tree it walks is identical each time — verified by running the old two-walk
code with the passes reversed and diffing survivors.

Also narrow isPrunablePackagedRuntimeArtifact to isPrunableTypeOrSourceMapArtifact
(node-pty prebuilds and duplicate sherpa dylibs are prunable runtime
artifacts too, but this predicate returns false for them), and add the
missing .d.mts fixture so every branch of the (?:c|m)? alternation is
exercised against the exact-survivor assertion.
2026-08-31 01:19:32 -07:00
Neil 0293ebe3eb perf(packaging): prune JS source maps from all packaged node_modules (#17638)
Generalizes the @linear/sdk-scoped prune to every packaged dependency,
matching the existing type-declaration prune's single-predicate walk over
Resources/node_modules. Recovers ~1.01 MB beyond the SDK.

Nothing in the packaged app enables Node source-map support
(no --enable-source-maps, no setSourceMapsEnabled, no source-map-support
require), and the CLI launchers strip NODE_OPTIONS, so these maps were
never read. Orca's own main-process maps live outside node_modules and
already ship as a separate release artifact.
2026-08-31 00:19:09 -07:00
Neil 6aba202d5e feat(docs): publish OSS docs with stable releases
Publish the standalone docs site under docs/site and deploy it on stable desktop releases.
2026-08-30 23:51:25 -07:00
Neil fc73903beb ci(release): publish main-process source maps with each release (#17630)
* ci(release): publish main-process source maps with each release

Desktop bundles ship minified, and packaging drops out/**/*.map from
app.asar, so a stack trace from a released build cannot be mapped back to
source. main builds with sourcemap:'hidden' — the maps exist in CI but were
never published anywhere.

Zip them on the linux-x64 leg and upload to the draft release as
orca-sourcemaps-<tag>.zip (33.7MB raw, ~8MB zipped, 69 files). The main
bundle is platform-independent, so one leg covers the whole release. The
step fails loudly if no maps are found, so a regression of build.sourcemap
breaks the release instead of silently shipping undecodable builds.

* fix(release): stage source map bundle outside the checkout

Every entry in electron-builder's `files` is a negation, so app-builder hits
containsOnlyIgnore() and prepends `**/*` (fileMatcher.js:285). A zip left in
the workspace root would have been packed into the linux-x64 app.asar,
growing that platform's installers by ~8MB and diverging them from arm64 —
the same hazard the '!pr-evidence' exclusion already guards against.

Stage it in $RUNNER_TEMP, matching the release-state file at :444.
2026-08-30 23:21:00 -07:00
Neil 3e2d0f2118 perf(build): minify desktop JavaScript bundles without dropping crash context (#17527)
* perf(build): minify desktop JavaScript bundles

* perf(build): minify with rolldown's oxc and emit hidden main source maps

'esbuild' made rolldown disable its own minifier and re-print every chunk
through esbuild, which is not a declared dependency and resolves only via
pnpm's shamefullyHoist from electron-vite's tree (0.25.12 against a declared
peer of ^0.27.0). Switching to rolldown's in-process 'oxc' minifier drops
that second pass: main+renderer build falls 23.2s -> 11.9s and ships ~2.7MB
less JavaScript.

keepNames is dropped with it — it cost ~1.5MB and only recovered function
names. main now builds with sourcemap:'hidden', which restores names *and*
locations without emitting a sourceMappingURL. Packaging excludes
out/**/*.map so app.asar is unaffected; release CI publishes the maps.
2026-08-30 22:57:04 -07:00
Neil 9f0b94d9b6 perf(packaging): prune Linear SDK source maps (#17530) 2026-08-30 22:54:44 -07:00
Neil b892f05c34 perf(packaging): ship one native runtime per target (#17528) 2026-08-30 21:12:34 -07:00
OrcaWin 1ec13cbda2 Speed up CI dependency and computer E2E setup (#17513) 2026-08-30 18:19:08 -07:00
Jinjing b3912ebed2 Split up combined-diff viewer into feature-organized modules (#17341)
* Reorganize combined-diff components into feature-organized structure

Splits flat combined-diff files into feature-focused subdirectories
(browse-files, load-sections, resolve-changes, review-controls,
scroll-viewport) to improve code organization and reduce clutter in
the editor directory. Groups related logic by concern for easier
navigation and maintenance.

* Split up combined-diff viewer into feature-organized modules

Decompose the 221-line monolithic CombinedDiffViewer into smaller, focused modules organized by feature: entry resolution, section loading, view state memory, file tree navigation, review controls, and scroll viewport handling. Main component now composes these hooks to orchestrate the combined-diff view.

* fix(combined-diff): prevent replayed preference writes

Move preference write outside state updater callback since React may
replay state updaters, causing multiple writes. Add sideBySide to
dependency array.

* fix(combined-diff): re-resolve sections by key to handle list rebuilds

The section list can rebuild while a write is pending (due to rebase, file changes, etc.); re-resolve by key instead of stale index to apply updates to the correct section.

- Convert skipped conflicts message to structured i18n plural forms
- Add oldPath field to git status signature for rename tracking

* Suppress react-doctor diagnostics in combined-diff feature

Add suppressions for react-doctor diagnostics that are necessary patterns
for the combined-diff implementation, configured in both the quality check
script and package.json.
2026-08-30 16:43:37 -07:00
Neil e84042572c Upgrade xterm to 6.1.0-beta.303 and generate addon patches
* Upgrade xterm to 6.1.0-beta.303 and generate the addon patches

Takes the current xterm beta line: xterm 287 -> 303, addon-webgl 286 -> 299,
addon-serialize 287 -> 300, headless 302, the remaining addons -> 300, and the
same set on mobile. All four packages stamp upstream commit d3e32b3.

The reasons are upstream #6042/#6043/#6055 (a shared glyph atlas no longer
garbles sibling panes on a page merge, clear, or sampler-budget overflow) and
Note that core 303 is not image-addon-only over 302: it carries the buffer perf
work, including the new BufferLineStringCache.

addon-webgl and addon-serialize move into the patch generator
--------------------------------------------------------------
Both were hand-edited minified bundles, which is what the Known Gaps section of
docs/reference/xterm-patch-regeneration.md described. Both reproduce byte for
byte from the pinned commit, so they are now manifest entries generated from a
source patch like @xterm/xterm already was. Their sourcemaps now move with their
bundles; before this they shipped maps whose offsets did not match the code
beside them.

The webgl patch shrinks from a 1.06 MB hand-edited bundle to a 6.6 KB source
patch, because upstream took the invalidation half Orca had backported. What is
left is only what upstream still lacks: the fragment-shader else branch for a
v_texpage past the sampler budget, the clearTexture guard that no-ops once a
merged page holds index 0, spending the merge retry budget before beginFrame
latches the version it saw, and Orca's font-weight probe.

The serialize source patch is byte-for-byte the same fixes as before; upstream
changed nothing in that addon between 287 and 300.

Generator fixes, each of which failed silently
----------------------------------------------
- `--relative` was appended after the `--` separator in CHECKOUT_DIFF_FLAGS, so
  git read it as a pathspec and kept repo-root-relative paths, dropping every
  source hunk from an addon's patch.
- `git apply` run from a package subdirectory still resolves patch paths from
  the repo root, skips every hunk and exits 0. It now runs from the root with
  `--directory=<packageDir>`, and a source patch that leaves the checkout
  unchanged is a hard failure rather than an empty patch.
- An addon's own `tsgo -p .` has empty files/include and only project
  references, so it emits nothing and the addon webpack then fails on a missing
  ./out/. The root build now runs first.
- versionStampFile is optional; publish.js stamps an addon's package.json, which
  overlayBuildOutput never patches.
- On a version bump the lockfile has no entry under the new key yet, so --write
  reports the gap instead of aborting mid-run. --check still fails on it.

Adding the two addons pushed the generator and the Electron packaging contract
test over max-lines, so the patch-text helpers move to xterm-patch-text.mjs
(pure text: no checkout, no build) and the vendored-xterm assertions move out of
the packaging contract into xterm-webgl-runtime-contract.test.mjs.

Tests
-----
Four tests asserted upstream bugs that are now fixed, not Orca behaviour:

- xterm-user-scrolling-contract pinned headless and core by version string.
  Upstream bumps each package only when its own output changes, so headless 302
  and core 303 are the same source. It now asserts they share a commit.
- Five CSI 3 J assertions expected a reader stranded at the top after an erase.
  Upstream #6081 clears isUserScrolling there, so the erase releases them to the
  bottom instead. Orca's pin still lands them correctly, because its parser
  handler observes the erase before xterm's own handler runs.
- The IME transaction test hard-coded the xterm version; it now reads the
  installed package, since the point is that bundle, map and version agree.
- The Electron runtime contract asserted Orca's old clearModelGeneration. Shared
  atlas invalidation is upstream's now, so it asserts pageLayoutVersion on the
  resolved dependency, plus the Orca-only hunks on the patch.

Verified: 66,008 unit tests, mobile's 3,863, the four WebGL atlas e2e specs, and
`regenerate-xterm-patches.mjs --check` in sync on all three packages.

Left alone deliberately: resetAllTerminalWebglAtlases still fans out globally
even though clearTexture now self-heals siblings, and upstream #6068
(WebglAddon.dispose leaks the GL context) is still open.

* Drop the two unused WebGL atlas fan-out exports

resetAllTerminalWebglAtlases and presentAllTerminalPanesWithoutAtlasClear have
no callers, and had none at cadfc55102 either — the last call site went in
#6949, which routed reveal recovery through
resetAndRefreshAllTerminalWebglAtlases instead. Only a comment in
pane-manager.ts still named the first one; it now points at the live entry
point. scheduleRevealPresent leaves the registry's structural type with them,
though the manager method stays: terminal-visibility-resume.ts calls it
directly.

This is dead-code removal, not a consequence of the xterm bump. The live
recovery path is unchanged.

resetAndRefreshAllTerminalWebglAtlases stays, and so does the reveal-time
escalation in pane-reveal-repaint.ts. Upstream 299 does make a pane-local
clearTexture bump pageLayoutVersion so siblings rebuild on their next frame,
which is the bug the escalation was written for, but I could not demonstrate
that removing it is safe: with the escalation removed,
floating-workspace-shared-glyph-atlas.spec.ts still passed headful, and it also
passed with upstream's mechanism deliberately disabled (pageLayoutVersion
pinned to 0 in the installed bundle, verified present in the built renderer).
A guard that passes with the fix disabled cannot license removing the
workaround, so the escalation stays until that spec can reproduce the garbling.

Verified: pane-manager and terminal-pane suites (4,713 tests), typecheck, the
headful shared-atlas spec, and the three headless WebGL specs.

* Give the shared glyph atlas spec a trigger that can fail

floating-workspace-shared-glyph-atlas.spec.ts guards the corruption where one
terminal wiping the module-global atlas leaves sibling terminals drawing from
stale texture coordinates. Both of its tests drive that through a floating
panel reveal, and Orca's reveal paths escalate to a registry-wide atlas reset
that repaints every pane — so the recovery under test heals the damage before
the assertion runs, and the tests pass whether or not xterm propagates the
invalidation at all.

The new test clears the shared atlas straight through the floating manager with
the panel closed, so nothing else repaints the workspace terminal, then repaints
it with terminal.refresh(). That is the load-bearing detail: _updateModel skips
cells whose content is unchanged, so the refresh reuses vertices baked against
the pages that were just wiped, which is exactly the state the fix has to
recover from.

Verified as a discriminator rather than assumed. Pinning ITextureAtlas's
pageLayoutVersion getter to 0 in the installed bundle, which disables the
per-renderer invalidation upstream added in addon-webgl 0.20.0-beta.299, and
confirming that reached the built renderer:

  fix intact:   siblingClearIntact=true   1 passed
  fix disabled: siblingClearIntact=false  1 failed

The failure renders the workspace terminal completely blank — stale coordinates
into a wiped atlas sample nothing. The two reveal tests pass unchanged in both
configurations, which is the gap this closes.

* Compare shared-atlas screenshots with tolerance instead of byte equality

Byte equality fails on sub-pixel antialiasing noise that leaves every glyph
legible, so the headful spec flaked under xterm 303. Reuse the existing
compareTerminalScreenshots helper: real stale-model corruption blanks the
terminal at ~3% of pixels, twice the helper's 1.5% threshold, so the looser
oracle keeps its teeth. Log the ratio so failures are diagnosable.

* fix(xterm): cancel empty deferred IME compositions

* test(xterm): strengthen runtime patch contracts
2026-08-30 15:14:49 -07:00
Brennan BensonandMerge Sim 585b4086d3 test(codex): pin Codex read-repair with a real-binary contract check (#17300)
* test(codex): pin Codex read-repair with a real-binary contract check

Orca's session index-heal depends on a Codex behavior: a `thread/read` of an
unindexed rollout performs a read-repair that inserts the `threads` row. All 55
existing heal tests drive a stub app-server and assert "healed" as "the call did
not error", so if Codex ever dropped the repair they would all stay green while
the subsystem went silently inert.

Adds a real-binary contract check built to the same shape as the Git binary
compatibility contract (src/shared/git-binary-compatibility.test.ts): env-gated
test file, version asserted against the binary, dedicated path-filtered PR job.

Pins only the four arms ablation established Orca relies on:
  - a read of an unindexed rollout inserts the state row
  - a session with no read inserts nothing (the negative control that makes the
    insert causal rather than incidental)
  - re-reading an indexed thread inserts nothing
  - an archived thread stays archived rather than being resurrected

Written against codex-cli 0.150.1. The job sets ORCA_CODEX_CONTRACT_REQUIRED=1
so a missing or failed CLI install fails red instead of silently skipping.

Existing heal tests are unchanged.

* test(codex): register the contract job in the verify aggregate contract

`pr-workflow-parallelism.test.mjs` pins `verify.needs` exactly, so adding the
job to pr.yml without updating that list failed the shard. Adds the entry, and
adds a workflow contract test mirroring `git-binary-compatibility-workflow.test.mjs`:

  - the pinned CODEX_CLI_VERSION is the single source for both the npm install
    and the runtime version assertion, so the two cannot drift apart
  - the install prefix and the binary path the test is pointed at are the same tree
  - ORCA_CODEX_CONTRACT_REQUIRED=1 is set, so a failed install fails red rather
    than turning the job into a green no-op

Removing the REQUIRED env from pr.yml reddens the new test, confirming it is live.

* test(codex): make binary version guard exact and bounded

* ci(codex): cover index-heal transport dependencies

* test(ci): pin Codex contract dependency coverage

* test(codex): align contract watchdog with child deadlines

* test(codex): cover three-session contract watchdog

* fix(codex): add sqlite sync-database to index-heal scope

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-30 14:39:46 -07:00
Neil 81b4d88bd6 refactor(mobile): split tasks route into focused modules (#17438)
* refactor(mobile): split tasks route into focused modules

* fix(mobile): repair tasks refactor module boundaries

* chore(mobile): document intentional render resets

* fix(mobile): remove stale lint suppressions from tasks split

* test(mobile): keep parity checks stable with doctor suppressions

* test(mobile): follow tasks module split

* test(mobile): follow project routing module split
2026-08-30 14:10:36 -07:00
Neil 70df6f0224 fix(terminal): mask the agent composer's dim placeholder during a preedit (#17377)
Split out of #17170, which now carries only the xterm composition-overlay work.

Codex and Claude draw an all-dim, full-row ghost placeholder. The opaque preedit
overlay reproduces the committed row tail it covers, so without this the ghost is
repeated to the right of the composing syllable instead of staying masked. The
binding keys off the `.xterm-composition-remainder` class that #17170 adds and
hides it through CSS while a composition owns a structurally verified placeholder
row — bold prompt glyph plus a dimmed model footer below a blank gap for Codex, a
frame line above the prompt for Claude. Arbitrary dim output, shell lookalikes,
and any row carrying typed text keep their tail visible.

readTerminalCursorLineContext moves from src/main/daemon to src/shared because the
renderer now needs the same reader the daemon uses; the move is import-only.

Depends on #17170.
2026-08-30 03:12:22 -07:00
Neil 7f822a73e3 fix(terminal): render the IME caret and give the candidate anchor one owner (#17170)
* fix(terminal): render IME caret without placeholder overlap

* fix(terminal): preserve dim mid-line composition tails

* fix(terminal): keep IME caret visible at row edge

* fix(terminal): harden IME overlay lifecycle and layout

* test(terminal): type final-cell layout mock

* fix(terminal): keep final-cell IME anchor on-screen

* fix(terminal): bind IME masking to composer ownership

* fix(terminal): bound IME placeholder session ownership

* fix(terminal): track latest IME placeholder session

* test(terminal): share IME session event fixture

* fix(terminal): keep both writers of the IME candidate anchor in agreement

`textarea.style.left` has two writers: xterm's patched CompositionHelper and
Orca's terminal-ime-candidate-anchor.ts. The anchor module listens on
terminal.element, so within a composition event it writes after xterm's textarea
listener and reverted the final-column clamp the patch had just applied.

Moving the clamp into the anchor module and dropping the patch hunk does not fix
it, and the rendered e2e caught that: CoreBrowserTerminal.ts:444 drives
updateCompositionElements from onRender as well, so xterm re-asserts the textarea
position on every repaint, with no composition event for that module to hear. The
anchor survived only when no render happened to follow — measured as a flake at the
final column, 1561.28px against a 1557px screen edge, the fully unclamped value.

So both writers now compute the same clamp. The patch keeps it, because it is the
writer on the render path and already holds cursorLeft, maxWidth and the preedit
bounds. The anchor module applies the same one, so its composition-event write no
longer reverts the correction in the window before the next render. Both halves are
individually necessary and both are mutation-tested.

Also restores _getRowRemainderText's expression from main: translateToString(true,
x, line.length) and translateToString(false, x, getTrimmedLength()) are the same
call, since upstream does endCol = min(endCol, getTrimmedLength()) under trimRight.

Adds the two missing tests — one installing both anchor writers in a single rig, one
driving a render under an open composition — plus disposal cleanup and clamp-bound
coverage, and moves the Codex/Claude placeholder mask to a follow-up PR.
2026-08-30 02:23:04 -07:00
Neil 7b467bd0a6 ci: gate PRs on a real input method, and prove the lane engaged one (#17365)
* ci: gate PRs on a real input method, and prove the lane engaged one

No job on the PR gate has ever run a real input method. pr.yml and e2e.yml are
ubuntu-latest with CDP `Input.imeSetComposition`, which is a synthetic
composition; the only job that drives ibus-hangul through xdotool is
terminal-ime-e2e.yml, and it is schedule + dispatch only. A PR could turn the
real-IME path red and merge green.

Route IME source to that lane from pr.yml through the existing
pr-e2e-source-routing mechanism, so it runs on IME-touching PRs and nothing
else. The lane stays out of verify.needs — advisory, like `e2e` — because its
reliability is known only from nightly main runs. Deliberately no
continue-on-error: that reports green and hides the signal.

The harness fails open in ways that all look like success: Playwright reports a
skipped test as a pass, so an unset ORCA_E2E_NATIVE_IBUS_HANGUL, a renamed test,
or a session with no engine all exit 0 having exercised nothing. The specs now
append an engagement receipt only after observing real composition events, and
the runner requires one per expected test before the lane may report success.

Also drop the native spec from changed-e2e: it was already routed there by its
own filename, where it self-skips for want of an ibus session and reported that
skip as coverage.

* ci: let the real-IME step report even when the synthetic step failed
2026-08-30 01:58:32 -07:00
e741ff1318 fix(wsl): scan agent sessions only in running distros (#17072)
* fix(wsl): scan sessions only in running distros

* test(ai-vault): pin WSL discovery platform

* fix(wsl): suspend transcript watchers for stopped distros

* test(wsl): pin transcript scan gate platform

* fix(wsl): settle stopped transcript loading

* fix(wsl): add last-known-good fallback and backoff to running-distro discovery

listRunningWslDistrosAsync failed closed on any probe error (timeout, ENOENT,
wsl.exe hiccup), indistinguishable from "no distros running". A 2s poll
(wsl-transcript-running-observer.ts) calls it indefinitely while any WSL
transcript tab is open, so a persistently broken wsl.exe silently made every
WSL session vanish app-wide with no way to tell "discovery broken" from
"distro stopped", and re-spawned wsl.exe every 2s forever.

Extract a dedicated cache/backoff module (wsl-running-distro-cache.ts,
mirroring the sibling machinery already in wsl.ts for the full distro list)
so a probe failure falls back to the last-known-good running-distro list and
backs off further probes, while a genuine empty result (no distros running)
stays authoritative. Add a consumer-level test simulating a sustained wsl.exe
outage across a live transcript-watcher polling session, asserting the
observer keeps reporting "running" and that real wsl.exe spawns stay bounded.

* fix(build): list the new WSL cache module in the web typecheck project

config/tsconfig.tc.web.json enumerates its files explicitly, so a new
module imported by wsl.ts fails the full typecheck with TS6307 until it
is listed. pnpm tc:node passes without it, which is how this got missed.

  src/main/wsl.ts(13,8): error TS6307: File 'src/main/wsl-running-distro-cache.ts'
  is not listed within the file list of project 'config/tsconfig.tc.web.json'.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-30 01:49:05 -07:00
Jinwoo Hong 252dbd60ea fix(terminal): restore lossy initial remote snapshots (#17113)
* fix(terminal): restore lossy initial remote snapshots

* test(terminal): strengthen lossy snapshot causal oracle
2026-08-30 03:11:55 -04:00
Neil 72cf80dc18 fix(scripts): stop pnpm-cli-invocation test leaking npm_execpath into the fallback case (#17340) 2026-08-30 00:04:30 -07:00