* 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
* 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
* 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
* 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
- 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"
* 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.
* 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
* 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.
* 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>
* 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>
* 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.
* 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.
* 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.
* 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.
* 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.
* 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.
* 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
* 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>
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>
* 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.
* 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
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.
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.
* 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.
* 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.
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.
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.
* 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>
* 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.
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.
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.