Commit Graph
9218 Commits
Author SHA1 Message Date
Neil 91a500712c fix(crash-reporting): see the renderer memory the heap counters never report (#16449)
* fix(crash-reporting): see the renderer memory the heap counters never report

Windows renderer crash 36048e26 arrived with 618MB of private renderer memory
and a `renderer_memory` breadcrumb reporting a 150MB V8 heap. Both numbers were
right: xterm scrollback lives in `Uint32Array` backing stores and glyph atlases
live in GPU transfer buffers, and neither is counted by `usedHeapSize`,
`mallocedMemory`, or Blink's allocator.

That made the report unanalyzable. `renderer_memory_highwater` is the crumb
carrying the subsystem census that names what grew, and it is armed on
`usedHeapSize / heapSizeLimit`. At 150MB of a 4192MB limit that ratio is 3.6% —
nowhere near the 60% mark — so the census never reached a single one of these
reports.

Measured on Windows (6 worktrees x 4 terminal tabs, 8000 lines each, this app
at 4218d505): filling 24 mounted panes moved the renderer working set from
210MB to 656MB while `usedJSHeapSize` stayed at 43MB for the whole run.

Sample the renderer's own OS footprint through `process.getProcessMemoryInfo()`
(available in the sandboxed preload) and:

- report `privateMB`, `residentMB`, and `outsideHeapMB` — the footprint minus
  everything V8 and Blink admit to holding — on every `renderer_memory` crumb;
- arm the highwater census on private-footprint marks (600MB / 1000MB) as well
  as the heap ratio, so growth outside the JS heap now carries the pane and
  store census that names it.

The footprint read is async, so a sample annotates with the previous read and
refreshes in the background: one interval of staleness is irrelevant to a
footprint trend, and awaiting it would make every sample reentrant. A shell
without the bridge, or a runtime that withholds the read, keeps sampling
exactly as before.

Retained-breadcrumb keys now distinguish the two threshold ladders; keying only
on `thresholdPct` collapsed every footprint crumb onto one slot.

crash-diagnostics.ts split at the max-lines budget: memory sampling moves to
renderer-memory-sampling.ts and the shared payload shaping to
crash-breadcrumb-data.ts.

* fix(crash-reporting): retain all renderer memory marks
2026-08-25 18:38:42 -07:00
Neil 76f7e785fc style(github): trim repo identity cache comments (#16517) 2026-08-25 18:23:57 -07:00
Brennan Benson 630b71730b fix(sleep): restore agent auto-hibernation for non-Pi agents (#16430)
* fix(sleep): let non-Pi agents hibernate again, and stop repaints resetting the idle clock

Auto-hibernation could never fire for claude, codex, gemini, opencode, grok, or
any other resumable TUI agent — only pi/omp/prime-agent.

#10238 broadened the `origin: 'live'` resume anchor so every resumable agent
keeps its `--resume` handle when a turn ends. The planner rejects any pane that
already has a sleeping record, and its exemption was still Pi-only. Since the
planner's eligibility conditions are the same conditions that write the anchor,
that rejection covered every otherwise-eligible non-Pi pane.

- Split the conflated predicate. `isLiveResumeAnchorForCompletedAgent` answers
  "is this record just this pane's own live anchor?" with no vendor gate; the
  Pi-gated wrapper keeps today's exact semantics for the manual-sleep and quit
  capture call sites; `isAutomaticHibernationAllowed` carries the
  `automaticResumeBlockedBy` fence on its own.
- Fence automatic hibernation. A fenced worker must not be auto-relaunched, and
  the capture does not copy the flag — so hibernating one would erase it. Checked
  in the planner and again inside the shutdown action against freshest state,
  re-evaluated after the synchronous capture callback that could itself fence it.
- Anchor the idle clock on `stateStartedAt`, not `updatedAt`. Same-state
  repaints (OSC 9999, reconnect replays) advance `updatedAt`, restarting the
  30-minute countdown and invalidating the two-tick confirmation.
- Floor that anchor on PTY-binding age and a boundary-resolution stamp, so a
  wake or app restart still gets a full idle window instead of sleeping the
  whole backlog on the ancient timing main replays. The boundary stamp is
  written synchronously where the flag clears; sampling it per tick would miss
  a boundary written and cleared between two samples.
- Signature drops `updatedAt` and gains agent kind plus full resume identity,
  which is the change detection `updatedAt` was providing by accident.

Splits the planner into planner / pane-eligibility / snapshot to stay under the
file length limit.

* fix(sleep): drain pane teardowns sequentially

`runAgentHibernationTick` launched every confirmed shutdown unawaited, so a backlog
fanned all of them out at once. Each shutdown re-runs a full runtime-liveness sweep
(one `terminal.list` per runtime-owned worktree, 10s timeout) and then a
`terminal.stopExact` (15s timeout) — so ~100 overdue panes meant ~100 concurrent
sweeps plus ~100 concurrent stops plus interleaved persistence writes. On an SSH
runtime that is hundreds of near-simultaneous RPCs at the relay.

The fanout predates this branch, but auto-hibernation could not fire for non-Pi
agents, so it never ran at scale. Restoring eligibility is what exposes it.

Awaiting each teardown also makes `tickInFlight` real: it was cleared in the
`finally` as soon as the promises were launched, so it never covered the drains it
was meant to guard. Each candidate still re-validates against a fresh plan at its
own turn, so a slow drain cannot act on stale confirmation, and per-candidate
failures are already caught so one stuck teardown cannot abort the rest.

* perf(sleep): scope hibernation rechecks to pane owner
2026-08-25 18:22:53 -07:00
Neil 67f2fc9e7c perf(github): stop re-spawning the repo-identity probe every 30 seconds (#16450) 2026-08-25 18:16:58 -07:00
github-actions[bot] cac5388545 Update README downloads badge 2026-08-26 00:28:37 +00:00
Brennan Benson d2a35eebe3 fix(mobile): avoid unsupported Hermes array sorting (#16506) 2026-08-25 17:11:09 -07:00
Jinwoo Hong c8567eb16e fix(sidebar): preserve hidden rows in manual order (#16488) 2026-08-25 16:46:42 -07:00
Jinwoo Hong 0e0a8c943b fix(mobile): recover Relay connections after resume (#16498)
* fix(mobile): recover relay sessions on resume

* fix(mobile): expedite relay retry on app resume

* fix(mobile): keep relay reconnect controller under lint limit

* fix(mobile): rebuild relay client after pairing rejection

* refactor(mobile): keep relay reconnect policy under lint limit
2026-08-25 16:25:00 -07:00
Brennan Benson efa3b972c2 fix(native-chat): prevent duplicate mobile prompt echoes (#15656) 2026-08-25 15:47:40 -07:00
Brennan Benson 29f1f4e545 test(perf): warm palette matcher before timing
Warm the palette matcher before measuring steady-state p95 performance.
2026-08-25 15:42:13 -07:00
Jinwoo HongandJinwoo-H a9781a4118 STA-4150: client-hosted remote browser (consolidated) (#15448)
Co-authored-by: Jinwoo-H <jinwoo@stably.ai>
2026-08-25 15:36:51 -07:00
Brennan Benson 98bdd653ab fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript

A brand-new agent session can take minutes to write its first JSONL line,
and one that is never prompted never writes it at all. The host emitted no
stream frame until the file resolved, so every native-chat client sat on a
bare spinner with the composer enabled but the transcript blank -- forever,
in the never-prompted case.

The resolve poll now reports the transcript as pending after a short grace,
and both host handlers emit a `pending: true` snapshot. It is deliberately
not a plain empty snapshot: an empty window sold as a settled read would
capture over retained history and unblock consumers that require a
trustworthy transcript (the launch-draft adoption would re-offer a prompt
the agent may already have taken).

Clients render it as the "start a chat" empty state while keeping the read
unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on
desktop, which also stops the seed loop expiring into an error card for a
session that is simply new. New optional field only, so older clients
ignore it and still stop spinning.

* fix(native-chat): negotiate pending transcript frames
2026-08-25 15:06:47 -07:00
Brennan Benson bc98655a39 fix(remote): stop an empty host inventory settling the mirror (#16414)
* fix(remote): stop an empty host inventory settling the mirror

An inventory with zero published snapshots satisfied
`settles.length === fullInventory.publishedSnapshotCount` as `0 === 0` and
fired the environment-wide host-mirror verdict with no host evidence behind
it. A live relay/SSH-paired host answers exactly that until its renderer's
first publish, so the drained resume sweep forked a second agent onto a PTY
the host was still running.

Gate that one case behind a `terminal.list` readiness probe: it reads the PTY
controller, not the session-tab mirror, so it sees live PTYs the mirror has
not published. Only "no terminals" settles; live or unverifiable leaves
waiters parked for the next inventory or per-worktree frame — a host with
genuinely zero terminals still settles, so panes do not park forever.

Fixes STA-5377

* fix(remote): fence host readiness probes by generation

* fix(remote): preserve legacy terminal probe fallback
2026-08-25 10:59:25 -07:00
Jinjing 61c7b51c8c docs(AGENTS): add code-reuse guidance and verification commands (#16451)
- Add "Reuse Before Reimplementing" section guiding developers to check for existing implementations before writing new code
- Add "Verifying Changes" section with quick reference for typecheck, test, and lint commands
- Fix typo: "Non-obviosu" → "Non-obvious"
2026-08-25 04:41:59 -07:00
Jinjing 32df073e44 fix(browser): focus unified tab on browser page palette activation (#16366)
* fix(browser): focus unified tab on browser page palette activation

When activating a browser page from the palette, find and focus the
corresponding unified tab before setting active state. Ensures the
tab group receives focus. Also increase e2e test timeouts to improve
stability on slower runners.

* test(e2e): read latest restored terminal frame

* Fail browser page activation when unified tab is missing

Without a unified tab, the workspace can't render in the pane. Reporting
success leaves the previous tab on screen. Fail the activation to prevent
this confusing state.
2026-08-25 04:00:59 -07:00
Jinjing e361da7fb7 Deleting skill (#16357)
* Add skill deletion with cross-platform transaction safety

Implements end-to-end skill removal with placement enumeration, dependency guards, and transactional recovery. Covers native, WSL, and remote hosts; users can delete canonical directories and alias placements (symlinked directories or files) in a single atomic batch. Includes UI selection flow, preview, confirmation, and results band. Block reasons (bundled, plugin, unowned, stale) gate deletions that would fail or contradict user intent.

* Organize IPC handlers into module subdirectories

Move register-core-handlers and skill-delete-ipc-handlers into
dedicated subdirectories for improved code organization and to
reduce the flat structure in src/main/ipc/.

* Make skill deletion recovery transactions idempotent

Defer journal cleanup until both staging removal and receipt cleanup succeed, leaving the journal in place for startup to retry if either operation fails. This ensures the recovery process is safe to run multiple times without leaving partially-deleted skills.

* Consolidate skill-delete files into dedicated module

Reorganize skill deletion functionality into a modular structure under
`src/main/skills/skill-delete/` with simplified file names. Remove the
redundant `skill-delete-` prefix from file names since they now live in
the dedicated directory. Update all import paths throughout the codebase
to reflect the new structure, including imports from IPC handlers and
RPC methods.

* Fix broken import paths and add deletion robustness improvements

Import paths using `..//'` were invalid and broken. Replace with explicit
module names (`skill-discovery-sources`, `skill-install-filesystem`, etc.)
to clarify dependencies.

- Bind WSL filesystem methods to preserve `this` context
- Keep recovery journal when rollback rename fails, so startup can retry
- Skip symlink-based tests on Windows where they cannot run
- Only treat ENOENT/ENOTDIR as empty directories; propagate other errors
- Fix cross-platform path parent calculation to handle drive roots
- Replace shared constant with localized string for user-facing message
- Use `runProcess` for WSL integration test instead of bare `execFile`

* Add batch limit for skill deletion and improve host availability checkin

- Limit concurrent deletions to prevent remote host overload
- Add retry logic for capability probing to handle transient unavailability
- Add reprobe() method to recheck capability after errors or user refresh
- Fix status logic: receipt cleanup is best-effort, completion depends only on content removal
- Improve error message for unreachable hosts
2026-08-25 03:58:48 -07:00
Neil 4218d5068e fix(cli): seed nvm's default version, not the newest install (#16420)
* fix(cli): seed nvm's default version, not the newest install

#16314 stopped the login-shell probe inheriting the seeded PATH, but left the
seed itself picking the newest installed nvm version. That ordering decides
which node a CLI runs under whenever the probe does not land — a timeout, or a
login shell whose rc never initializes nvm — and newest is precisely the wrong
guess: it is usually the version the user just added and has installed nothing
into. That is the root cause reported in #10932.

Resolve `alias/default` instead, mirroring nvm: follow the alias chain
(`default` -> `lts/*` -> `lts/krypton` -> a version), resolve a partial version
like `24` to the highest matching install, and treat `system`/`node`/`stable`
as no preference. The chain is bounded and cycle-guarded because nvm's own
resolver tracks seen aliases and hand-edited files can point at each other.

Ordering is a preference, not a restriction: the remaining versions stay behind
the default, so a CLI installed outside it is still reachable.

Measured on a real machine with nvm default=24 and a bare v26.7.0 installed:
the old resolver seeds v26.7.0/bin (no CLIs), the new one seeds v24.18.0/bin
(every CLI). Tests were written first and verified to fail on the three bug
cases against main before the fix existed.

Also raise the probe budget from 5s to 10s. The old value was never measured
against a real profile: a bash -ilc loading nvm, rvm, conda and gcloud takes
~1s idle but 6-7s on a loaded machine, so a cold start under load silently
fell back to the seed. Startup does not block on the probe, and the one
awaited consumer is agent detection, which is better served by a probe that
finishes late than one that gives up early.

* fix(cli): reject non-version alias tokens instead of matching v0.x

Review finding, and a real bug I introduced. parseVersionSegment coerces
every unparseable segment to 0, so an unresolvable default alias — `garbage`,
`iojs`, `lts/nonexistent`, any hand-named alias — became [0] and prefix-matched
a `v0.12.x` install, or any stray non-version directory. Orca would then seed a
decade-old node as the preferred runtime. Real nvm answers N/A for all of them.

The `wanted.length === 0` bail could never have caught this: ''.split('.') is
[''], never empty. Replaced with a shape check that still admits legitimate
numeric prefixes — verified against nvm itself, which resolves `24` to
v24.18.0 and `0` to an installed v0.x while answering N/A for the rest.

Also corrects two comments that no longer described the code: the seed is no
longer "newest install", and the probe budget note claimed startup never blocks
on hydration, which is false on packaged Windows where it gates terminal
services and git. The traversal-guard comment claimed a containment join()
already normalizes away; the real guarantee is that matchNvmVersion can only
return an entry of the versions directory.

* fix(cli): match nvm's version-token grammar, not just its first character

Round-2 review finding, and the same bug one layer down. The previous guard
anchored only the first character, but parseInt stops at the first non-digit,
so `0x18`, `00` and `0abc` still parsed to [0] and prefix-matched a v0.12.x
install — the decade-old-node seed the earlier fix was supposed to close.

Reachable: `nvm alias default 0x18` warns that the version does not exist and
writes the alias anyway, then resolves it to N/A.

Use nvm's actual grammar, leading zeros included — nvm calls `00` and `024`
N/A while parseInt reads them as 0 and 24. Verified by executing 17 tokens
against a five-version fixture: every one now agrees with nvm, including the
legitimate prefixes `0`, `0.12`, `24` and `v24.18.0`.

Also drops a dead disjunct (the hop bound already caps the loop, so seen.size
can never exceed it) and corrects the log comment in index.ts, which still
told the reader a failed probe leaves the newest install in front. It leaves
the default version in front now, which is usually survivable but still not
what the shell would have resolved.

* test(cli): skip the lts/* chain fixture on Windows

Round-3 review finding. makeNvmHome materializes each alias as a real file,
and the chain case uses nvm's actual `lts/*` alias — `*` is a reserved Win32
filename character, so writeFileSync fails with EINVAL. PR CI runs a Windows
allowlist that excludes this file, so the breakage only reaches a Windows
developer running the suite locally.

Skipped rather than renamed: `lts/*` is the alias nvm really ships, and the
assertion pins platform: 'darwin' anyway, so the real name costs no coverage.
Matches the skipIf convention already used across src/shared.

Also reflows a comment line that a previous edit ran to 143 characters;
oxfmt does not reflow comments, so nothing would have caught it.
2026-08-25 03:05:24 -07:00
NeilandNeil 822087c8ec refactor(git): split runner.ts into focused command-runner modules (#16395)
* refactor(git): split runner.ts into focused command-runner modules

* chore(ratchets): repoint child_process and wsl.exe allowlists at the split modules

---------

Co-authored-by: Neil <n@example.com>
2026-08-25 02:34:11 -07:00
Neil 1f39c93b01 refactor(ipc): split ssh.ts into focused modules (#16394) 2026-08-25 02:32:21 -07:00
NeilandSeongho.Bak fcf55f2d68 fix(terminal): stop Orca mangling the OMP/Pi title it writes itself (#16381)
* fix(terminal): collapse identity group in the title churn signature

Replaces the ingest-time title rewrite from #16373 with a non-destructive
fix at the actual cause.

The churn suppressor `isDecorativeAgentTitleFrameChange` keyed on the
literal label, so `working:OMP` and `working:Pi` compared unequal and every
alternating frame from a wrapped harness committed a store patch. #16373
made the labels agree by rewriting the stored title to the tab's launch
owner — but `runtimePaneTitlesByTabId` is also the Windows Shift+Enter
byte-encoding input, so normalizing at ingest destroyed evidence other
consumers read (fixed separately in #16376).

Collapse the identity group inside the signature instead. Which member of
a group a frame names is decoration, exactly like the spinner glyph the
signature already strips, so frames compare equal without touching what is
stored. Suppression now changes only WHETHER a frame commits, never WHAT
it says.

Also fixes the flap under a multiplexer (#8032): the collapse runs over
wrapper segments, so "zsh | ⠋ Pi" and "zsh | ⠙ OMP" compare equal, which
the anchored owner-relabel in #16373 never matched.

Reverts the store changes from #16373 and drops the helper it added.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* fix(terminal): fold only bare identity frames into the group token

A legacy "π - <session> - <cwd>" title is Pi-compatible too, so folding
every profile match collapsed two different sessions to the same signature
and suppressed the change outright — reintroducing #16093 through the
churn signature.

Fold only exact bare identity frames, matched per wrapper segment, so
semantic session titles keep comparing on their own text.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* docs(terminal): correct the flap diagnosis in the repro header

Verified against the OMP source: it emits only π-glyph frames
(`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an
Orca-hosted pane its native titler cedes to Orca's own injected extension,
which writes `⠋ π - <session> - <cwd>`.

So OMP emits neither "OMP" nor "Pi". Both flap sides are Orca's:
"OMP" from driveSyntheticTitleFromHook, "Pi" from normalizeTerminalTitle
collapsing our own extension's output to a hardcoded literal.

The prior header credited the wrapped harness for frames it never sends,
which is the same wrong narrative that produced eight fixes at eight
layers. No behavior change.

* fix(terminal): stop Orca mangling the OMP/Pi title it writes itself

Verified against the OMP source: it emits only π-branded frames
(`DEFAULT_TERMINAL_TITLE = "π"`, title-generator.ts:25), and on an
Orca-hosted pane its native titler cedes to Orca's OWN injected extension,
which writes `π - <session> - <cwd>` / `⠋ π - <session> - <cwd>` at 80ms.

So neither flapping string came from OMP. Orca made both:
  "Pi"  — normalizeTerminalTitle collapsing our extension's output to a
          hardcoded literal, discarding the session name and cwd (#16093)
  "OMP" — driveSyntheticTitleFromHook injecting over it every 80ms

Fixed at the source:
- normalizeTerminalTitle canonicalizes only the rotating braille frame and
  keeps the rest, in both spinner positions and through a multiplexer
  prefix (#8032). Status still round-trips through normalization.
- detectAgentStatusFromTitle reads the π state separator, so `π ! <label>`
  is permission instead of the blanket idle that hid a blocked agent.
- normalizeCompatibleAgentTitleForOwner swaps only the brand for the
  owner's label, so a pane still reads as its launch owner (#6689, #7633,
  #9077) without losing the session text.
- pi/omp set synthesizeWorkingTitle: false — the agent animates its own
  working title. Terminal states still synthesize; they carry the pane's
  agent identity downstream.

Reverts the ingest-time title rewrite from #16373, whose normalization of
runtimePaneTitlesByTabId also changed Windows Shift+Enter bytes (#16376).

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* fix(terminal): match the state separator only in exact profile casing

The separator check runs on every title, so `omp - deploy notes` and
`pi - refactor the parser` read as an idle agent. The owner rewrite only
ever emits the exact profile labels, so dropping case-insensitivity keeps
`OMP - tmp` classifying while ordinary prose stops matching.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* test(terminal): pin one real OMP turn to two committed patches

Drives 30 working frames as Orca's injected extension emits them plus the
idle transition, and asserts what survives the churn gate. Before the fix
every frame alternated "⠋ Pi"/"⠋ OMP" and each one committed — ~12 store
patches per second on a working tab.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* fix(terminal): carry the permission guard inside the separator reader

`-` is both a π state separator and the delimiter in the synthetic
permission label, so `OMP - action required` read as idle. It resolved
correctly only because detectAgentStatusFromTitle happens to check the
synthetic label first — and the separator fn is exported, so a direct
caller inherited the bug.

Also pins the owner rewrite's fixed-point property, which holds only
because getAgentLabel does not tokenize omp/pi, and corrects a comment
that overstated how tightly the brand swap is scoped.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

* docs(terminal): name the flag the code actually sets

The suite header cited `synthesizeTerminalTitle: false`; the profiles set
`synthesizeWorkingTitle: false`. The distinction is the whole reason the
narrower flag was chosen — terminal-state frames still carry the pane's
agent identity downstream — so the wrong name buried the rationale.

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>

---------

Co-authored-by: Seongho.Bak <49228032+psh4607@users.noreply.github.com>
2026-08-25 02:28:08 -07:00
Neil 33587fb77d refactor(git): split status.ts into source-control modules (#16393) 2026-08-25 02:24:11 -07:00
Neil 127fa7fae0 refactor(ipc): split repos.ts into focused modules (#16392)
* refactor(ipc): split repos.ts into focused modules

* test: point repo notification mocks at the extracted module

* fix(ipc): repoint the child-process allowlists after the repos split

The type-only `import type { ChildProcess }` moved from repos.ts to
repos/repo-clone-lifecycle.ts, so the import-boundary entry follows it and the
windows-console entry (now stale, and that list only shrinks) is dropped.
Fixture-only; the base file had no runtime child_process use at all.
2026-08-25 02:23:53 -07:00
Neil e7c5263a93 perf(git): make local Git metadata observation event-driven (no scheduler) (#16404)
* perf(git): make local Git metadata observation event-driven

Replaces the recurring per-repo metadata scan with native filesystem events on
macOS, Linux, and Windows. Polling is retained purely as a fallback.

- Narrow @parcel/watcher stream over <common>/worktrees, extended from macOS to
  Linux and Windows, with the Windows backend pinned explicitly.
- New shallow watcher mode over the allowlisted primary metadata leaves. It
  watches the containing directory rather than each file, so Git's atomic
  write-and-rename does not orphan the binding.
- Selected upstream refs stay on the existing bounded stat poll.

Verified on real hosts rather than in principle:

- Windows: `git worktree remove` and `git worktree prune` both succeed while the
  narrow stream holds the directory. The historical concern that an open handle
  would block prune does not reproduce.
- Linux: inotify costs one instance per event loop, not one per watch, so the
  watch budget is not a constraint.
- macOS/Linux/Windows: shallow events survive repeated commit, checkout,
  config, and pack-refs cycles.

Failure handling, each reproduced before being fixed:

- fs.watch binds an inode and reports nothing once that inode is replaced, with
  no error. Directory bindings are re-checked on a bounded cadence and rebound.
- A host whose notification path is dead accepts registrations and stays mute
  forever. Observed on a macOS machine whose fseventsd had grown to ~15GB and
  saturated a core. A one-shot delivery probe now fails the shallow subscribe on
  such a host so it falls back to polling instead of showing stale metadata.

This change stands alone on main and does not depend on the metadata poll
scheduler.

* test(git-watch): hold reserved inodes across root replacements

Linux returns a released reservation to the free list, so the second
replacement could land back on the first replacement's inode and look
unchanged to reconciliation. Verified on ext4: releasing yields inodes
[N, N+43, N+43] while holding yields [N, N+43, N+44]. macOS never
recycles, which is why this only failed on CI.

* refactor(git-watch): share one single-flight helper between watcher fallbacks

Both fallbacks tracked their in-flight promise with the same self-comparison
on settle, duplicated verbatim. Hoisting it removes a subtle invariant that
was being hand-maintained in two places.

* fix(git-watch): close the silent-staleness paths in primary metadata

Two independent reviews converged on the same root cause: nothing bounded
how long primary metadata could stay wrong once the shallow watcher stopped
reporting. Four distinct paths led there.

- A terminal watch error arriving while the status-ref poll was still starting
  left the repo with status-ref coverage only. handleWatcherError ran its
  teardown against nulls, then the in-flight poll installed itself, and the
  fallback guard mistook it for coverage and discarded the fallback. Primary
  metadata was then never observed again. The guard no longer treats status-ref
  polling as primary coverage, and startup re-checks watcher liveness after its
  awaits.

- Nothing re-read the six primary files while the watcher was nominally live.
  A lossy notification path, a dropped batch, or inotify queue overflow raises
  no error, so the error-driven fallback never fired and the inode rebind sweep
  does not detect loss. A 15-tick backstop re-stats them, turning permanent
  staleness into one tick. Measured cost is ~0.2 stats/s/repo against the 3/s
  the old poll cost.

- Reconciliation treated any late-observed entry create as a root replacement,
  so an ordinary  tore down a healthy stream ~30s later and
  opened a deaf window. It now also requires the root itself to be recreated.

- The shallow watcher recorded directory identity from a stat issued after
  binding, so a replacement in that gap pinned the dead inode's watcher to the
  new identity and the sweep would never rebind. Identity is now read first,
  which errs toward a harmless extra rebind.

Test helper: replacing the worktrees root frees several inodes at once, so
reserving one still let the recreated root reuse its own. It now verifies the
inode actually changed. Confirmed on ext4, where three holds were needed.
2026-08-25 01:22:07 -07:00
Neil 8217e6838f refactor runtime contracts and web transports (#16197)
* refactor runtime contracts and transports

* test(web): repoint two-phase timeout seam at the transport that now owns call()
2026-08-25 00:59:29 -07:00
Neil d456549c22 refactor runtime Git bridges (#16189)
* refactor runtime git bridges

* fix(test): avoid computed namespace import access in runtime Git client contract test
2026-08-25 00:54:43 -07:00
Neil f482725355 refactor terminal coordination modules (#16335)
* Refactor terminal coordination modules

* preserve terminal completion and stale-connect guards

* restore pre-spawn E2E barrier and stale-connect check order in ipc-pty-connect

* restore merge-base title-working replay and stamped-tail delete semantics

* fix(terminal): merge the duplicated shortcut-matching import

Two adjacent imports of the same module tripped oxlint's
no-duplicate-imports under --deny-warnings. Import-only; no behavior change.
2026-08-25 00:53:54 -07:00
Neil fc0d947983 refactor(rate-limits): split Codex and Claude fetchers (#16140)
* refactor(rate-limits): split Codex and Claude fetchers

* refactor(rate-limits): restore base error-message defaulting

The split moved the 'Unknown error' fallback from inside String() to the call site, which changed behavior for an Error with an empty .message: base surfaced '', head surfaced 'Unknown error'. Restore the base form.
2026-08-25 00:53:44 -07:00
Neil 5ebc4bb2bc refactor(renderer): split repos store slice (#16191)
* refactor(renderer): split repos store slice

* test(store): align repos/folder-workspace characterization tests with main's owner-scoped delete + host-qualified visit recency

* fix(composer-state): repoint RepoUpdate import to repos/repo-state after split
2026-08-25 00:42:59 -07:00
Neil a782935ab3 refactor(checks): split panel responsibilities (#16186)
* refactor(checks): split panel responsibilities

* fix(checks): consolidate comment audience import

* fix(checks): handle open review state exhaustively

* fix(checks): restore merge-base check presentation values

The split into check-presentation.tsx silently changed icon, opacity, and
PR-state token values. Restore them verbatim.
2026-08-25 00:33:36 -07:00
Neil 83ffc0df24 refactor Electron facilities modules (#16333)
* refactor oversized Electron facilities

* fix interactive process timeout and shortcut repeat guard

* chore(child-process): drop stale cli-installer allowlist entry

cli-installer.ts now routes privileged spawns through runProcess via
cli-privileged-processes.ts, so the shrink-only ratchet flags it as stale.

* refactor(child-process): extract the bounded output sink

runProcess's timeoutMs opt-out (required to preserve the unbounded osascript
admin prompt) pushed run-process.ts past the 300-line cap. Move createOutputSink
to its own module rather than add a max-lines bypass, which AGENTS.md forbids.
Moved verbatim; no behavior change.
2026-08-25 00:31:31 -07:00
Neil 1cf562deea refactor: split source control AI modules (#16179) 2026-08-25 00:31:06 -07:00
Neil e0bc4e1126 refactor(editor): split notebook, editor, and external-watch surfaces (#16143)
* refactor(editor): split editor and watch surfaces

* fix(editor): revert behavior changes smuggled into the surface split

Restore merge-base React keys in IpynbCellOutputs: the content-identity keys
JSON.stringify'd every output value, including raw base64 image payloads, on
every keystroke.

Collapse the duplicated lazy() declarations into editor-lazy-views so each
viewer keeps a single React.lazy identity across the extracted surfaces.
2026-08-25 00:30:47 -07:00
Neil 6103b78e3c fix(crash-reporting): correlate concurrent process deaths instead of blaming the renderer (#15251)
* fix(crash-reporting): correlate concurrent process deaths on a renderer report

Two 1.4.184 reports (a326935a, 1862f316) are renderer "crashed"/-1 crash reports
whose renderer only died alongside a sibling Chromium child that died at the same
instant:

  F0BQMB30GJX  network.mojom.NetworkService crashed/-1  -21ms -> renderer crashed/-1
  F0BRPP8TC0Y  audio.mojom.AudioService     crashed/-1   -2ms -> renderer crashed/-1
                                                  GPU crashed/-1 +180ms

process-gone-classification.ts classifies each event in isolation:
isRecoverableChromiumChildProcess discards the utility/GPU halves as recoverable
churn, and `if (reason !== 'killed') return true` then reports the renderer half as
a genuine renderer crash before any cross-source signal exists. Triage reads
"renderer crashed" for what died with three other processes.

process-gone-sibling-correlation keeps a bounded ring of child deaths, populated
before the suppression early-return so churn-suppressed siblings stay visible, and
matches a renderer death against child deaths sharing its failure signature.

What the timing can and cannot support:

- The window is asymmetric. 1s of lookback (a child that died first can plausibly
  have taken the renderer with it), but only 250ms of lookahead: a child dying well
  after the renderer is at least as likely to be an effect of it — Chromium tearing
  down the dead renderer's channels, or renderer_recovery_reload at +264ms — and a
  symmetric window would retro-label a genuine lone crash as collateral.
- crashAttribution is 'concurrent-process-deaths', not a causal claim. The largest
  1.4.184 cluster is an external taskkill /T where renderer and children are
  co-victims; no sibling caused anything there.
- The verdict is not derived from timing alone. A host with a child looping at the
  observed 1459/min drops a death into every window, so crashAttribution is set only
  when the nearest sibling is within 250ms and no identity repeats. Looser or
  repeating deaths still ship as evidence (siblingProcessDeathCount, signed offsets,
  siblingProcessDeathRepeats) with no attribution.
- The signature match buckets `crashed` with `abnormal-exit` and gates on the exit
  code only on win32. Both fixtures are win32, where every process in a collateral
  pair reports crashed/-1; POSIX surfaces a per-process wait status, so an equality
  gate would mean this never fires on macOS or Linux.

The report stays reportable and gains evidence rather than being suppressed
(#14667). Both arrival orders are covered without delaying persistence: a sibling
that dies first is folded into the initial record, a sibling that dies after amends
the record already on disk through attachDetails, the same way the minidump
signature does. Late amends are capped at two per report and skipped when the
rendered evidence is unchanged, so a crash-looping child cannot rewrite the store
during renderer recovery, and a failed amend now leaves a
sibling_attribution_attach_failed breadcrumb instead of vanishing.

Relationship to #12484: it is still OPEN and adds process-tree-kill-window.ts, the
same ring/lookback bookkeeping with a 250ms settle, patching the same recorder
hunks with the opposite policy (suppress the killed/1 renderer report instead of
keeping it). #14667 is test-only — it pinned the keep-the-report policy in tests, it
did not remove a shipped implementation. #12484 has to be closed or rebased out
before this lands.

* chore: remove merge hook formatting drift
2026-08-25 00:11:05 -07:00
hjkwonandClaude Opus 5 8e8c6d7ed2 fix(linear): make new-issue dialog popovers scrollable (#16382)
* fix(linear): make new-issue dialog popovers scrollable

`[data-slot='popover-content']` already caps every popover to
`--radix-popover-content-available-height`, but PopoverContent's base class is
`overflow-hidden`. A team list taller than that cap is therefore clipped at the
window edge with no scrollbar and no way to reach the entries past the cut.

The dialog's other attribute popovers had an inner max-h-60 box, but none of the
six carried the popover-scroll-content / popover-wheel-scroll marker that
popover.tsx's wheel shim needs, so Radix's dialog scroll-lock swallowed the wheel
there too.

Move all six to the popover-scroll-content pattern already used by
LinearItemDrawer, JiraIssueWorkspace, and github-item-dialog: it re-declares the
cap as min(15rem, available-height) and adds overflow-y: auto, and the class name
opts the content into the wheel shim.

Measured on the team switcher with 25 teams:
  before  max-height 611px, overflow-y hidden, 609 of 735px visible
  after   max-height 240px, overflow-y auto,   scrollTop reaches 497

The inner max-h-60 boxes are dropped because stacking them under the outer cap
creates nested scrollers whose combined height exceeds it, leaving the bottom of
each list unreachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(linear): match the inner-scroller classes regardless of order

The previous assertion pinned one exact class order, so reintroducing the
wrapper as `scrollbar-sleek overflow-y-auto max-h-60` slipped through. Collect
the section's `<div>` classNames and check the three tokens as a set instead.

Scoped to wrapper divs on purpose: the dialog's description textarea caps its
own growth with those same classes and is not a popover child, so a plain
whole-section match flags it as a false positive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 00:01:54 -07:00
Neilandinnocarpe 49acb93e2e fix(daemon): let PTY inventory recover from a dead terminal host (#16363)
Worktree removal inventories PTYs through DaemonPtyAdapter.listProcesses. That
called ensureConnected bare, so once the terminal-host pipe was dead the
removal failed with `connect ENOENT \\?\pipe\orca-terminal-host-...` and stayed
broken until the whole app was restarted.

spawn already wrapped its work in withDaemonRetry and recovered from exactly
this. Inventory did not — so the one path that must not get stuck was the only
one that could not heal itself.

Both the connect and the listSessions request go inside the retry: a host that
dies between them throws the same daemon-gone error, so retrying only the
connect would still fail. The reconciliation after the request is deliberately
outside it; retrying that would be wrong.

Reproduced first, with a real daemon killed mid-test: listProcesses threw
DaemonConnectionLostError while a control asserting spawn recovery from the
identical kill passed. Both are now regression tests, so the asymmetry cannot
come back silently.

Co-authored-by: innocarpe <innocarpe@users.noreply.github.com>
2026-08-24 23:57:43 -07:00
Neil aa4c9c707c Refactor mobile home, worktree modal, and RPC client (#16165)
* refactor(mobile): split home modal and rpc client

* fix(mobile): restore render-phase remount key in NewWorktreeModal

The split moved the form-reset epoch from render-phase refs into
useState + useLayoutEffect, which changed when the remount key is
computed. On the render where visible flips false->true the key was
still the old epoch, so the previous session's NewWorktreeModalContent
rendered with visible === true carrying stale form state. Child layout
effects run before the parent's, so visible-gated hooks
(useNewWorkspaceRepositories, useNewWorktreeDrawerNavigation,
useNewWorkspaceRuntimeContext) fired for that stale instance before the
parent bumped the epoch and remounted.

Restore the ref-based computation so the key is correct on the first
render where visible flips true, keeping the composite open/client
epoch semantics and the file split intact.
2026-08-24 23:50:10 -07:00
Brennan Benson 656e4891de fix(ssh): restore the reconnect model-paint gate dropped by #15166 (#16361)
* fix(ssh): restore the reconnect model-paint gate dropped by #15166

#15166 split pty-connection.ts and dropped the "paint from main's model on SSH
reconnect" half of the reattach gate that shipped in v1.4.188 (#14844), leaving
only the park-reveal half. A non-park SSH reconnect has repainted from the
~100KiB relay tail ever since, which cannot rebuild a full-screen frame whose
start it no longer holds.

Restores followsDirectSshReconnect (PENDING-only retry read), reconnectMayUseModel,
the exited-transition veto computed before the probe, and the kitty scanReplay
layered after the snapshot baseline. Adds a call-site test over
createReattachPayloadHandlers, because the surviving pure-function test stayed
green through the entire removal.

Fixes STA-5395

* fix(ssh): restore empty-tail reconnect snapshots
2026-08-24 23:49:18 -07:00
Neil 5c3df831b7 refactor composer and jira slice modules (#16196) 2026-08-24 23:49:08 -07:00
Neil 75103667b0 test(cli): ratchet the exec/fork family, not just spawn (#16390)
The pairing ratchet matched spawn|spawnProcess|spawnSync|runProcess only, so
a resolved CLI handed to execFile was the same unpaired launch with none of
the enforcement. codex-trust-grant-host.ts resolves codex and calls
execFileSync, and escaped the ratchet purely through that omission.

Widen to the exec/fork family. The negative lookbehind keeps method calls
such as `RE.exec(` out, which is what made the bare `exec` name safe to
include; a fixture mutation confirms `/x/.exec('x')` does not trip it, and
adding execFileSync(resolvedCli) to a paired file does.

codex-trust-grant-host is allowlisted rather than changed: its only exec is
a wsl.exe identity probe for the binary stamp, and its actual codex launch
is a CodexAppServerInvocation paired centrally in codex-app-server-session.
The entry records what would invalidate it.
2026-08-24 23:45:51 -07:00
deae7212d9 fix(orchestration): derive inject's agent guidance from the recognized-agent roster (#15874)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: vam <a@a.com>
2026-08-24 23:32:30 -07:00
Neil 6faaf3af74 fix(lint): match moved code by ordered near-match, not strict contiguity (#16385)
A diagnostic's span often reaches past the block a split moved — most commonly
to a hook dependency array, which legitimately grows when closure variables
become props. Requiring every line of the span to match contiguously reported
the moved body as new.

The block must still start at the same line in the base and appear in order,
and >=90% of it must be present. Genuinely new code shares neither the anchor
nor the ordering.
2026-08-24 23:17:04 -07:00
Neil 48e63c015f refactor agent config and auth services (#16195)
* refactor: split agent config and auth services

* chore: repoint wsl and global-fetch guards at split module paths

* fix: restore merge-base Claude CLI error propagation

Drop the secret-redaction rewriting added to Claude CLI error paths in the
refactor: spawn errors again reject with the original Error (preserving
.code/.errno/.syscall/.stack) and command output/auth-status logs are no
longer rewritten.
2026-08-24 23:15:01 -07:00
Neil 32ffdd2b9a refactor(renderer): split composer state (#16182)
* refactor(renderer): split composer state

* fix(renderer): satisfy composer static analysis

* test(renderer): migrate composer boundary contracts

* fix(composer): restore project group reset effect

Revert read-side mask back to the merge-base state clear so a momentarily
unavailable host permanently drops the folder group instead of silently
retargeting Create when the host reappears.
2026-08-24 23:14:42 -07:00
Neil 2b1b094aa8 fix(cli): pair every resolved CLI with its runtime, and ratchet it (#16383)
Follow-up to #16365, which paired 8 spawn sites by hand. Hand-pairing is how
the class got introduced, so close it structurally instead.

cliPath is now required on CodexAppServerInvocation, `null` only for the
guest-side wsl.exe launcher where a host path pairs nothing. Optional let a
native builder omit it and silently fall back to pairing against a cmd.exe
wrapper with no type error. Every production site already passed it; only
test fixtures needed updating, which is the type doing its job.

Four more sites now pair. codex-state-db-backfill-recovery spawns the same
`codex app-server` subcommand #16365 fixed elsewhere. cli/handlers/account
was the worst case: addAgentNodePaths prepends the *newest* version-manager
bin, which is not necessarily where the CLI being launched lives, so it
actively created the mismatch — pairing now runs last so the CLI's own node
wins. commit-message-text-generation and skills/skill-update-run spawn
resolved binaries with inherited env.

cli/handlers/skills had grown its own buildNpxPath: a weaker local copy that
prepended unconditionally, ignored the Windows `Path` key, and special-cased
a '.' dirname. Deleted in favor of the shared helper, which checks the
sibling node actually exists — the behavior change one test had pinned.

The ratchet is the point: any file that resolves a CLI and spawns must
reference withCliRuntimeOnPath, with a shrink-only allowlist. It caught
skill-update-run, which I had missed. Its first draft required a call paren
and so let dependency-injected resolvers (`resolveCommand: resolveCodexCommand`)
through — verified by removing a pairing and watching it stay green, then
widened until it failed. A second assertion fails on a stale allowlist entry
so an exemption cannot outlive its reason.

external-editor-launch stays allowlisted: it launches a GUI editor, not a
Node CLI whose ABI matters.
2026-08-24 23:12:37 -07:00
Neil 20c1a61401 fix(orcad): answer host paths honestly and ship the watcher child (#16369)
orcad's AppEnvironment implemented three of seven AppPathNames and returned the
userData directory for the rest — including 'exe', where a data directory is not
an executable. Every name now has a Node answer: 'appData' is the platform's
per-user application-data root, 'logs' lives inside the data root so a headless
deployment stays one removable directory, 'downloads' honours XDG_DOWNLOAD_DIR,
and 'exe' is the Node binary. getAppPath() is the directory orcad was launched
from rather than cwd, so children resolve against the bundle instead of wherever
the supervisor happened to be.

The watcher child was the load-bearing consequence: resolveWatcherProcessEntryPath
probed for the adjacent entry only when !isPackaged, so orcad resolved a desktop
out/main path that no deployment has — and build-orcad never emitted the child
anyway. isPackaged stays true (consumers read it as "production, not a dev
checkout" and it gates HTTPS-only skill downloads); the resolver now asks whether
the app root is an asar archive, which is the question it actually meant. The
child ships beside orcad.js, and the build forks it to prove it runs.
2026-08-24 23:01:17 -07:00
Wooseong Kimandm4air 60a3fd8873 fix(i18n): localize the keep-awake corner chip (#14775)
* fix(i18n): localize the keep-awake corner chip

Route the status-bar keep-awake chip through the shared Agents copy
helpers and add missing locale entries for chip-only words.

Fixes #14490

* test(i18n): restore previous language after keep-awake locale suite

* test(i18n): render component in localization tests instead of static che

Converts the keep-awake localization test from static source-code validation to actual component rendering with React Testing Library, providing more reliable verification that the UI displays correctly across all supported languages. Improves translated descriptions for consistency and accuracy.

* test(i18n): add aria labels and descriptions to localization test

- Adds missing localization keys to test data for Spanish, Japanese, Korean, and Simplified Chinese
- Updates test assertions to verify `ariaLabel`, `onDescription`, `autoDescription`, and `offDescription` are properly translated
- Completes localization coverage for the keep-awake corner chip component

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-08-24 22:46:29 -07:00
Neil c4988fa81c refactor workspace lifecycle modules (#16334)
* refactor(workspaces): split lifecycle modules

* preserve workspace cleanup consent contract

* restore workspace delete shortcut hint in context menu view

* restore host-qualified visit recency and viewed-candidate predicate

* test(cleanup): pin the viewed-mark upgrade path and host-qualified visit reads

Two invariants a refactor broke in this PR, both silent:

- viewed marks are persisted, so gating `shouldPreserveCleanupInspection` on any
  newer field voids the grace period for every entry written by an older build
- visits are stamped under `${hostId}|${worktreeId}` whenever the host is known
  (the normal case, including 'local'), so a bare map[worktreeId] read misses
  every modern entry and yields 0, disabling the recent-visible-context blocker

Verified discriminating: reintroducing each bug fails exactly its own test.
2026-08-24 22:44:08 -07:00
Neil 73f709217b test(linear): pin project out of the hydration-preserved field set (#16364)
Linear list issues never carry `project` (only getIssue maps it, via
includeProject: true). If `project` joins EDITED_LINEAR_ISSUE_FIELDS, an edit
made while getIssue is in flight overwrites the hydrated project with the list
issue's undefined, permanently blanking it — the sidebar shows 'Add to project'
and LinearIssueSubIssues then files sub-issues with projectId: null instead of
inheriting the parent's project.

Verified discriminating: re-adding 'project' to the field set fails these.
2026-08-24 22:43:42 -07:00
Neil 33c9353f29 fix(lint): exempt verbatim-moved code from the changed-lines quality gate (#16359)
A file-splitting refactor makes every line of the new module an added line, so
pre-existing lint debt in code that merely moved starts failing the gate. The
only way to satisfy it is to edit the moved code, which is what a
behavior-preserving refactor must not do. Exempt a diagnostic when its
highlighted lines already existed verbatim and contiguous in the base revision.
2026-08-24 22:43:24 -07:00
Jinjing 0ac68f0d4b Add background color to search button in sidebar (#16130)
Applies a subtle background color to the search button to make it more visually distinct.
2026-08-24 22:42:11 -07:00