* fix(codex): safely re-land WSL direct homes
* fix(codex): finish WSL direct-home cutover
* fix(codex): coalesce WSL launch hook installs
* perf(codex): avoid duplicate retired WSL session scan
* fix(codex): retain canonical WSL retired-home path
* fix(codex): fail closed before retiring WSL auth
* fix(codex): reopen WSL drain after rollback
* fix(codex): preserve WSL source on unknown panes
* fix(codex): harden repeated WSL runtime drains
* perf(codex): bound pending WSL session scans
* fix(codex): recover invalid WSL session watermarks
* fix(codex): validate retained WSL scan state
* fix(codex): accept durable WSL scan state
* test(codex): cover the drain's inode-identity guard against destination replacement
Removing the four `target_auth -ef temporary_destination_auth` assertions left
all 33 apply-script tests passing, so a regression deleting them would have
shipped silently. Reproduced before writing this.
A hash check cannot catch the case. The pinned hard link keeps the original
inode, so it still hashes correctly after another writer atomically renames a
different file over the destination path; only inode identity sees it. Without
the guard the script exits 0 and retires the source, leaving the user holding
bytes nothing validated. The new case asserts the source survives.
The harness is split by responsibility so no file exceeds its max-lines budget:
fixtures, the coreutils interference shims, the run types, the apply runner, and
the recovery/absent runners. The atomic-rename hook is deliberately separate
from the in-place rewrite shim because different guards catch them.
* fix(codex): keep the split drain harness inside the child-process boundaries
Extracting the harness into non-test modules moved it out of the exemptions the
single test file had: three new files import child_process, and two spawned
without windowsHide.
Adds the three to the import allowlist, and sets windowsHide on the spawns
rather than exempting them - the flag is correct for these calls regardless of
the ratchet, and they are skipped on win32 anyway.
---------
Co-authored-by: Merge Sim <sim@local>
* feat: add show-more button for recent tabs in empty-query palette
* Reveal all recent tabs in one show-more click, limit badges to 9
The show-more button now expands the entire recent tabs list instead of
paging through it. Badges are limited to the first 9 rows since only
those digit positions are addressable in the keyboard chord.
* perf(diff): defer large diffs until user loads them
Rendering very large diffs would freeze the UI. Diffs exceeding
MAX_AUTOMATIC_DIFF_CHANGED_LINES now show a prompt allowing users
to load them on demand instead of automatically rendering.
* perf(diff): defer large diffs until user loads them
Diffs with >10,000 changed lines are now deferred and only rendered when
the user explicitly clicks "Load diff" in a prompt. This improves initial
render performance for large file changes while maintaining full access
when needed.
* perf(diff): defer large diffs until user loads them
Prevents UI freeze when opening files with very large diffs by
deferring render until the user explicitly loads them.
* fix(diff-view): defer loading large untracked files and refactor fallbac
Split on-demand load decision logic to distinguish tracked vs untracked files — large untracked files now properly defer loading while untracked images remain automatic. Extract fallback height computation into a dedicated function to centralize the logic for render-limited and in-flight-loading states, reducing code duplication and clarifying when to use bounded fallback heights.
* fix(diff-view): defer loading large SVG files
SVG renders as source text in the diff view rather than a preview, so should defer like other text files. Also fix Windows e2e test cleanup by using post-Electron shutdown.
* fix(ssh): fence stale kills and retired pane replay
* fix(ssh): support cancellable interactive authentication
* fix(ssh): await remote catalog before snapshot adoption
* fix(pty): contain Windows ConPTY input failures
* fix(power): avoid redundant macOS display blocking
* perf(editor): narrow markdown override subscriptions
* fix(quick-open): close directory handles after reads
* refactor(linux): remove unused proc socket scanner
* fix(usage): apply flat Sonnet 4.6 pricing
* ci: prime Node next native test cache
* docs(skills): resolve snapshot cleanup data path
* fix(ssh): recover install locks after host reboot
* test(ssh): recognize boot-aware install locks
* test(ssh): prove previous-boot lock recovery live
* test(wire): pin pre-metadata release coverage
* fix(terminal): preserve remote tab ownership through recovery races
* test(runtime): fence replaced terminal handles in agent guard
* fix(ssh): preserve remote snapshot authority across polls
* fix(pty): contain late ConPTY output EPIPE
* test(pty): register Windows exit watcher before kill
* fix: close SSH and tab readiness race gaps
* fix(tabs): retain headless order and placeholder titles
* fix(build): avoid parallel electron-vite config race
* test(windows): avoid MSYS temp path rewriting
* test(windows): avoid killing exited PTY
* fix(pty): avoid late ConPTY input teardown race
* fix(terminal): sync reconnect error ownership after commit
* fix(runtime): use canonical worktree identity comparison
* test(ssh): assert complete cold-hydration baseline
* test(windows): invoke quoted retention fixture via PowerShell
* test(windows): read ConPTY grid through mode con
* fix(terminal): publish PTY replacements atomically
* fix(terminal): infer stale identity on reattach
* fix(terminal): fence stale pane PTY callbacks
* fix(terminal): fence stale pane binds after rebind
* fix(terminal): reject stale pane transport callbacks
* fix(terminal): fence mirrored reattach spawn callbacks
* fix(terminal): replace stale pane PTYs on remount
* fix(ci): size the Windows launcher-compile test budget from measurement
`native-smoke (windows-latest)` fails ~4.5% of runs on
`preserves a multiline argument through the compiled remote launcher`
with "Test timed out in 15000ms" — on unrelated PRs, for reasons that
have nothing to do with them. Across 176 sampled attempts it is the only
red that job produced, and it hit seven different PRs in two days:
#16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085.
The test is six process creations: powershell.exe forks csc.exe, then
the freshly compiled orca.exe forks node.exe, twice. Hosted Windows
runners periodically slow process creation down, and this test amplifies
that far harder than anything else in the job. Comparing the 80 attempts
where it ran under 3s against the 12 where it ran over 12s, its own
median goes 2198ms -> 15917ms (7.2x) while the same file's
powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash
process tests in the neighbouring file move 1.4x, and the other 35 files
put together move 1.5x.
Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms,
correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%)
exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s
testTimeout, so deleting the override and inheriting the config is not
enough on its own. 60s clears all 176 with 1.7x headroom on the worst.
This is slow, not hung. Every body here is synchronous spawnSync, so
Vitest cannot interrupt one — the timer fires only after the body
returns and the reported duration is real elapsed time. That is why a
failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The
work finished; the stopwatch was short. Seven reruns at one identical
head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the
last of those would have been red on code that had not changed.
The 15s came from #8897, which raised this test off Vitest's built-in 5s
default because the job then ran bare `pnpm vitest run`. #8909 landed
3h27m later and pointed the job at config/vitest.config.ts, which is the
real fix for that. The constant stayed behind and has been the binding
budget ever since.
* fix(terminal): fence stale remount reattach ownership
* fix(terminal): reconcile mounted pane identity after replacement
* fix(terminal): fence stale reattach fallback ownership
* fix(terminal): fence deferred SSH reattach ownership
* fix(terminal): fence stale split pane ownership callbacks
* fix(terminal): keep stale spawns from consuming startup
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* perf(relay): stop ACK boundary scans at first pending boundary
* test(relay): pin PTY source boundary cleanup and guard ascending sends
The early-`break` in advanceCredit is only correct while sentBoundaries is
inserted in ascending sentEndSu order. Turn that implicit invariant into a
throw at the sole live write site (commitPtySourceSend), and assert the
post-state directly instead of inferring it from an iteration budget:
- assert the surviving boundary set after the 1,023-ACK benchmark
- cover the jump-ahead cumulative ACK that must delete many boundaries in
one pass (the case an over-eager `break` would get wrong)
- cover the settleReservedPtySourceAck -> advanceCredit entry point
- drop an arithmetically-implied assertion and CI benchmark log noise
* perf(relay): reclaim ACK boundaries with a monotone cursor
The early-break Set scan still rebuilt a Set iterator per ACK, so V8 walked
delete tombstones and the drain stayed superlinear; the visit-count test could
not see it because it stubbed sentBoundaries with a generator over a private
Set. Replace the Set with an ascending boundary list plus a monotone cursor,
assert the real structure, and add a benchmark over the shipped code.
* test(relay): enforce ascending sent-boundary inserts in the collection
Move the ascending-order precondition into PtySourceSentBoundaries.add so
both insert sites are covered, and assert per-ACK span reclamation in the drain.
* test(relay): collapse ledger test record accessors into getDeliveryRecord
Rebase onto #17490 left two structurally identical internals accessors
(getCursorRecord, getBoundaryRecord); one typed accessor covers both.
* perf(relay): index PTY source-credit send spans
* perf(relay): maintain PTY source-credit retention totals
* test(relay): pin PTY send-cursor rebase across ACK reclaim
Cover the Math.max clamp branch in reclaimCreditedSpans where reclaim
removes spans at or past the send cursor, and widen the seeded fuzz case
to 20 spans per seed so the cursor actually traverses spans; assert the
cursor never overshoots the span containing sentEndSu.
* refactor(relay): drop dead retained-total helpers and pin retention counters
The incremental PtySourceCreditRetention counters replaced the recompute-from-records
helpers; delete the now-unreferenced exports and recompute the totals from the live
records inside the ledger tests so the counters have an independent oracle.
* test(relay): bound send-span reads instead of pinning the read pattern
Address review feedback on the send-span cursor coverage:
- replace the exact indexed-read pin and the tautological naive-visit
assertion with a linear bound that still fails on the old Array.find path
- drop the per-run bench console.log
- assert retention totals immediately after rotate(), the only path that
removes and re-adds a record in one call
Also count the replacement delivery in retention as it enters the delivery
map so the "in deliveries <=> counted" invariant never has a hole.
* fix(daemon): bound the whole boot-recovery sequence with one budget (STA-5732)
* fix(daemon): keep socket probes inside recovery budget
* fix(daemon): size the recovery budget against the real post-kill tail
The 24s budget reserved only 9s for everything after the deadline, leaving
27s of the startup PTY gate's fail-open cap unused — and every unused second
is one where a daemon that would have drained gets killed with its live PTYs
instead. Reserve each post-deadline stage's actual hard cap (kill 10.5s, fork
10s, lease 5s) and spend the rest: 24s -> 32s of adopt window.
* fix(daemon): keep the last-resort endpoint rescue outside the recovery budget
The rescue probe in the launcher's outer catch was clamped to the recovery
budget's remainder, but it runs *after* that budget by construction — past
prepareDaemonReplacement, killStaleDaemon, the fork and the adoption lease.
The remainder is therefore essentially always negative, so Math.max(1, ...)
handed a live socket a 1ms connect window. On the loaded machine this path
exists for the probe loses to its own timer, the launcher rethrows, and a
recoverable degraded adoption becomes total daemon loss for the whole run —
the outcome the comment above it exists to prevent. Restore the 1s default
and pin the window with a test that drives the launcher to that catch with
the budget already spent.
Also make the deliberate narrowing legible instead of implicit:
- daemon-recovery-budget.ts: TRANSIENT_WEDGE_DRAIN_MS documented 20s as the
grace #8697 sized, but #8697's merged second commit (840d3277d1) widened
it to 11 retries ~= 60s. Record that 20s is the drain estimate and that the
budget deliberately sits under #8697's shipped grace.
- daemon-init-wedged-daemon-grace.test.ts: pin the trade directly — a wedge
draining after the budget is replaced and loses its live sessions.
- Rewrite 'preserves a daemon that stays wedged until the LAST allowed grace
retry' onto the simulated clock. It never mocked Date.now, so its 12 probes
elapsed ~0ms and asserted a retry grace the wall clock can no longer
deliver; it now pins the last drain the budget still adopts.
* fix(daemon): name the socket probe default and correct the grace-retry rationale
Answers the review round on the budget accounting: the outer-catch endpoint
rescue is deliberately outside it, and the preflight clamp no longer duplicates
probeDaemonSocket's default as a bare literal.
* test(updater): cancel the real timers an abandoned updater instance leaks
#17649 stamped `loadElectronAutoUpdater()` with a generation so an abandoned `updater`
module instance could no longer drive the shared `autoUpdater` spies. That fenced one spy
graph but left the leak channel itself open: `resetUpdaterMocks()` still cannot cancel the
real timers the previous instance armed, so the stale instance keeps running and keeps
reaching every shared spy the fence does not cover.
Exposed chains, all with exact call-count assertions on them:
- 1s `updateCheckSilentSettleTimer` -> `completeSilentUpdateCheck()` ->
`scheduleAutomaticUpdateCheck()` on the next test's fake clock -> `runBackgroundUpdateCheck()`
-> `pinDefaultReleaseFeed()` -> `fetchNewerReleaseTagsWithReadiness` -> `fetchNewerReleaseTagsMock`
(updater.check-preflight.test.ts:59,309,528; updater.publishing-window-feed.test.ts:382,458)
- `scheduleUpdateNudgeCheck()` -> `fetchNudgeMock` / `shouldApplyNudgeMock`
(updater.nudge-campaign.test.ts:168,175)
- the previous test's `webContents.send` mock, which still receives a stale 'not-available'
- `completeSilentUpdateCheck()`'s 1h retry, which several files straddle with 59min + 1min
Close the channel instead of ignoring its effects. The harness now wraps the real
`setTimeout`/`setInterval`/`clearTimeout`/`clearInterval` globals while a test file is using
it, and `resetUpdaterMocks()` cancels every real handle armed since the last reset. Fake
handles are already discarded by `vi.useRealTimers()`, so real handles were the only leak
channel left.
The patch installs only after `vi.useRealTimers()` (never over a fake clock, so it cannot
capture fake handles), restores only the globals still holding its wrappers, hands back
untouched Node `Timeout` objects so `unref()` keeps working, and is removed in `afterAll` so
no unrelated file in the same worker sees it. Vitest arms its own test timeouts through
`getSafeTimers()`, snapshotted at worker setup, so nothing here can capture or cancel them.
The #17649 generation fence stays in place — this is additive defense in depth.
* fix: drop fake clocks before handing the timer globals back
The afterAll uninstall silently no-opped in 4 of the 10 harness files. Its
identity guard (globalThis.setTimeout === wrapper) fails whenever a file's
last test leaves a fake clock installed, and no updater test calls
vi.useRealTimers() — the only restore is the next beforeEach, which never
runs after the last test. Affected: check-settlement, publishing-window-feed,
quit-and-install, and this PR's own leaked-timers test.
Nothing broke because vitest defaults isolate:true, so the stranded wrapper
died with the per-file process. Under --no-isolate it would have been a real
leak: the wrapper stays installed for every later file in the worker, the
armed-handle sets retain every Timeout forever, and a later updater file's
reset would cancel live timers belonging to unrelated suites.
Also scope the module docstring — node:timers/promises and util.promisify
bypass the globals entirely, so a future `await setTimeout(...)` in
updater.ts would reopen the leak with no failing test.
* fix(worktree): gate agent activation on the live surface census, not renderer state (STA-5701)
* fix(worktree): seed a pane when the surface census cannot prove ownership (STA-5701)
Failing closed must not also fail silent. When the census is unverifiable
the sweep adopts nothing and mints nothing, yet the gate still reported
'adopted' — and both callers suppress their own seeding on any outcome but
'empty', so the workspace ended with zero surfaces. The sweep now reports
whether any live PTY holds a surface and the gate hands the caller its seed
when none does. Also folds equivalent workspace-path spellings in the census
index and in exact-surface binding, so a host row spelled differently is
neither dropped (mint a duplicate) nor unbindable (no pane).
* fix(worktree): name the live PTYs the surface census declined (STA-5701)
The adoption sweep can leave a live PTY without a surface — an unreadable
census, two host surfaces claiming one PTY, or a host-named leaf the
persisted layout does not have. The gate already stops reporting 'adopted'
in that case so the caller seeds a shell, but the decline itself was mute.
- adoptLiveWorkspacePtySurfaces now returns { surfaced, declinedPtyIds }
and the gate warns with the workspace and the PTY ids left unsurfaced.
- Pin the host-named-leaf decline, which had no test either way.
- Pin the superseded-inventory race in terminal.list: a concurrent refresh
makes hostScope.hostIds empty, which is what makes the renderer's
'unverifiable' verdict reachable on a plain local machine.
Root cause of the `updater.startup-scheduling` flake: `resetUpdaterMocks()` calls
`vi.resetModules()`, which abandons the previous test's `updater` module instance but
cannot cancel the real timers that instance already armed. The earlier real-timer tests
leave a 1s `updateCheckSilentSettleTimer` pending; it fires a second or so later, i.e.
during a *later* test that has since installed a fake clock. The abandoned instance then
runs `completeSilentUpdateCheck()` -> `scheduleAutomaticUpdateCheck(24h)`, arming that
timer on the running test's fake clock at its epoch. `reschedules the next automatic
check 24 hours after finding an available update` advances 1h + 23h, so the stale 24h
timer lands exactly at the end of the 23h window, and the stale instance calls the
shared `autoUpdaterMock.checkForUpdates` spy -> 2 calls instead of 1.
Whether the leaked real timer fires before or after the next test installs its fake
timers is real-clock dependent, which is why it reproduced ~1 in 12 runs and only when
the whole file runs (30/30 pass with `-t` filtering to the single test).
This is a test-isolation bug, not a product bug: production has exactly one updater
module instance and one clock, so no stale instance can exist.
Fix: the harness already detaches abandoned instances on the event side (it clears the
`app`/`autoUpdater` handler maps on reset); extend the same idea to the call side.
`loadElectronAutoUpdater()` now hands each module instance a generation-stamped view of
`autoUpdaterMock`, and `reset()` bumps the generation, so a stale instance's calls and
property writes are dropped instead of driving the spies the running test asserts on.
Verified: 40/40 clean runs of `pnpm test src/main/updater.startup-scheduling.test.ts`
(0 failures), plus all 22 `src/main/updater*` files (264 tests) green.
* perf(rpc): compile Zod request schemas lazily
* chore(deps): pin zod 4.5.4 and except it from the release-age gate
4.5.4 is the first release fixing isRecursiveSchema (upstream 84e416f, #6500),
which compile() calls on every schema — on 4.5.0 it fired .default() factories
at compile time. Verified: compile-time factory calls 0 on 4.5.4, 1 on 4.5.0.
`serializes FETCH_HEAD callers before they enter admission` assumed that two
same-repo fetches join the FETCH_HEAD lock lane in call order. They do not.
`runWithGitFetchHeadLock` first `await`s `fetchLockPath`, which walks the
filesystem (`realpath`, `stat` per parent directory, `readFile` of `commondir`,
`realpath` again) before it calls `runWithGitOperationLock`, and the lane is
registered only after that walk resolves. For a non-existent `/repo` that is
five libuv threadpool round-trips per caller. Two callers issued back to back
run their chains concurrently, so lane order is threadpool completion order,
not call order.
When the `interactive` fetch won that race it entered the lane ahead of the
`background` fetch. On the first caller's release it reached admission
immediately and, being interactive, took the free network headroom slot instead
of queueing, while the background fetch stayed parked on the lock. `queued`
therefore settled at 0 and never reached the asserted 1. Measured inversion
rate for the bare lock-path walk was 54/500 on an idle machine; the test itself
failed 5/20 locally, always at the same assertion, matching the two CI failures
on unrelated PRs (#17530, #17630) at the same line.
Fix the premise rather than the symptom: stub only the key derivation, keeping
the real FIFO `runWithGitOperationLock` that the test actually exercises, so the
lane is registered synchronously with the call. Key derivation keeps its own
coverage in `src/shared/git-fetch-head-lock.test.ts`. This also stops the fetch
tests in this file from sharing one global `/.git/FETCH_HEAD` lane with each
other and from touching the real filesystem.
Verified deterministic: 40/40, then 30/30 clean runs, plus 25/25 with twelve CPU
hogs and a concurrent `src/main/git/command-runner/` run saturating the box.
* perf(wsl): warn at project-add when the tree sits on a Windows drive
Worktree placement now puts new workspaces inside the distro, but a project
whose own tree is on C:\ still pays the 9p/drvfs crossing on every git command
it runs — measured at ~20x for a clean `git status` against the same tree on
ext4. Nothing in the UI says so, so the project just feels slow.
Warn once, right after the add succeeds, naming the distro the project's git
actually runs in. The advisory is wrapped so it can never fail the add.
Two path shapes cross the boundary and both warn: a Windows drive path under a
WSL project runtime, and the UNC spelling of a distro's own drvfs mount
(\\wsl.localhost\Ubuntu\mnt\c\...), which crosses it however the runtime is set.
A tree already inside the distro, a drive path under Windows-host git, a plain
UNC share, and every POSIX/SSH path stay silent.
* chore(i18n): register the WSL filesystem boundary advisory keys in en.json
Track scroll-restore generation to invalidate stale callbacks that were
resetting the scroll position to 0 when reopening a detail page. Prevent
restoration while a detail page is open.
Update automation test to use runtime.call RPC instead of removed preload
CRUD method. Hide browser import hint in E2E profile to prevent overlay from
intercepting test setup clicks.