Commit Graph
9700 Commits
Author SHA1 Message Date
Neil 89ea30f8c8 chore(stack): restack backend on renderer parity fixes 2026-08-31 06:29:19 -07:00
Neil 80d2e620f7 chore(stack): restack preload on renderer parity fixes 2026-08-31 06:28:47 -07:00
Neil 92d39fc984 chore(child-process): prune stale import allowlist 2026-08-31 06:27:20 -07:00
Neil 962cbdf3a4 fix(renderer): preserve accepted visibility snapshots 2026-08-31 06:18:33 -07:00
Neil e88338577c test(terminal): follow Hangul lifecycle extraction 2026-08-31 06:18:32 -07:00
Neil f03339a223 test(main): follow browser cookie process runner 2026-08-31 06:09:31 -07:00
Neil e321eccb41 fix(main): route browser cookie key commands through runner 2026-08-31 06:03:32 -07:00
Neil 369496b27c fix(main): resolve runtime lazily in observers 2026-08-31 05:47:44 -07:00
Neil ac1c652e8f fix(main): remove rebase conflict residue 2026-08-31 05:47:44 -07:00
Neil 2146cff06a refactor(main): split filesystem git remote handlers 2026-08-31 05:47:44 -07:00
Neil 068535b8ef fix(main): isolate runtime file path naming 2026-08-31 05:47:44 -07:00
Neil c0f38db5de fix(main): merge split startup imports 2026-08-31 05:47:43 -07:00
Neil 19f838e482 test(main): census split browser command owners 2026-08-31 05:47:43 -07:00
Neil a33573328b refactor(main): split backend services and startup 2026-08-31 05:47:43 -07:00
Neil aad2e1ec54 chore(max-lines): prune preload baseline entry 2026-08-31 05:44:34 -07:00
Neil 682ff4f5be fix(preload): merge split plugin imports 2026-08-31 05:44:33 -07:00
Neil cd08e4e91c test(preload): census split GitHub bridge owners 2026-08-31 05:44:33 -07:00
Neil b77e31873b refactor(preload): split bridge API modules 2026-08-31 05:44:33 -07:00
Neil 08820a4386 chore(max-lines): prune runtime baseline entries 2026-08-31 05:43:35 -07:00
Neil 90bf505afe fix(renderer): merge split runtime imports 2026-08-31 05:43:35 -07:00
Neil 207bef0198 refactor(renderer): split runtime and store modules 2026-08-31 05:43:35 -07:00
Neil b13c2b1960 fix(renderer): preserve palette create telemetry 2026-08-31 05:42:39 -07:00
Neil 81f4803e6f fix(renderer): preserve Codex sign-in behavior 2026-08-31 05:35:17 -07:00
Neil 89368c1203 fix(renderer): restore floating tab drag context 2026-08-31 05:33:33 -07:00
Neil a793b07c12 fix(automations): preserve destination form save semantics 2026-08-31 04:50:52 -07:00
Neil bcf715b76e fix(renderer): preserve floating editor visibility 2026-08-31 04:39:24 -07:00
Neil 0a802c70e1 fix(renderer): preserve status localization parity 2026-08-31 04:28:46 -07:00
Neil 42bf3eb674 chore(max-lines): prune stale lifecycle baseline 2026-08-31 04:12:02 -07:00
Neil 6fc78dd869 fix(renderer): document intentional render-time refs 2026-08-31 04:07:58 -07:00
Neil ea8e6e595a refactor(renderer): finish worktree palette extraction 2026-08-31 03:49:50 -07:00
Neil 1e93d66854 style(renderer): format palette project candidates 2026-08-31 03:49:41 -07:00
Neil 0b78c1cbbc fix(renderer): restore worktree palette behavior after split 2026-08-31 03:49:35 -07:00
Neil 790680dfc2 fix(automations): preserve Escape drill-out behavior 2026-08-31 03:49:28 -07:00
Neil 9455e318fa fix(status-bar): preserve workspace-space review and agent freshness 2026-08-31 03:49:23 -07:00
Neil 4a3bc23670 fix(lint): preserve TaskPage effect suppressions after split 2026-08-31 03:49:18 -07:00
Neil e65d298afb fix(status-bar): preserve Git refresh ordering after split 2026-08-31 03:49:10 -07:00
Neil 787228ee46 chore(max-lines): prune refactored file suppressions 2026-08-31 03:49:01 -07:00
Neil 1731f2a2f3 fix(renderer): preserve extracted lifecycle and retention behavior 2026-08-31 03:48:52 -07:00
Neil c778ac7a7a fix(renderer): keep split imports lint-clean 2026-08-31 03:48:45 -07:00
Neil da89be4345 refactor(renderer): split oversized UI surfaces 2026-08-31 03:48:44 -07:00
Neil 47706a5388 fix(mobile): restore session parity after extraction 2026-08-31 03:47:59 -07:00
Neil f129f2926a refactor(mobile): split session and terminal surfaces 2026-08-31 02:49:18 -07:00
Neil ae2eeff55d perf(relay): index PTY source-credit send spans (#17490)
* 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.
2026-08-31 02:41:15 -07:00
Neil e17c98d425 fix(daemon): bound the whole boot-recovery sequence with one budget (STA-5732) (#17427)
* 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.
2026-08-31 02:41:11 -07:00
Neil 7cb1db63db test(updater): cancel the real timers an abandoned updater instance leaks (#17663)
* 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.
2026-08-31 02:17:00 -07:00
Neil 6bbed15a11 fix(worktree): gate agent activation on the live surface census, not renderer state (STA-5701) (#17428)
* 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.
2026-08-31 01:40:54 -07:00
Neil b5746724d4 perf(relay): account pending PTY output incrementally (#17639) 2026-08-31 01:28:29 -07:00
Neil 97eb762b27 refactor(packaging): prune declaration and source-map artifacts in one walk (#17659)
* refactor(packaging): prune declaration and source-map artifacts in one walk

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

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

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

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

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

Also narrow isPrunablePackagedRuntimeArtifact to isPrunableTypeOrSourceMapArtifact
(node-pty prebuilds and duplicate sherpa dylibs are prunable runtime
artifacts too, but this predicate returns false for them), and add the
missing .d.mts fixture so every branch of the (?:c|m)? alternation is
exercised against the exact-survivor assertion.
2026-08-31 01:19:32 -07:00
Neil e22c4ee1ac ci(docs): skip releases without docs source
Skips stable tags that predate docs/site before entering the protected production environment.
2026-08-31 01:00:47 -07:00
Neil 22a9e30ba1 test(updater): detach stale updater module instances from the shared autoUpdater mock (#17649)
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.
2026-08-31 00:55:01 -07:00