mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
fix/diff-shift-wheel-horizontal-scroll
3654
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
28373fcea7 |
fix(windows): repair the install-dir package ACL that blanks the window (#17740)
* fix(windows): repair the install-dir package ACL that blanks the window An install tree carrying an orphan AppContainer ACE (S-1-15-2-<x>) with no ALL RESTRICTED APPLICATION PACKAGES grant denies Chromium's LPAC children read on the shipped modules; they die at init with 0x80000003 and the window stays blank forever (electron/electron#51761). - Tighten the probe verdict to require the S-1-15-2-2 grant specifically: an ALL APPLICATION PACKAGES (S-1-15-2-1) ACE, the Program Files default, does not appear in an LPAC token and cannot satisfy the orphan. - Drop BUILTIN from the English-locale heuristic (fr-FR/es-ES print it verbatim) so a localized icacls is correctly reported as un-name-checkable. - Add an additive, marker-guarded icacls self-repair: an inheritable root grant plus a flagless (RX) /T pass, never /grant:r. - Route the crash-loop dialog through a testable prompt module that names the permission cause, offers Copy Commands without dismissing itself, and keeps the graphics-driver hint. The repair only runs on win32, off serve mode, and only on the exact probe verdict that reproduced the crash. * docs(windows): correct the install-tree ACL walk cost model * fix(windows): keep the install-ACL poison gate at the reproduced shape An orphan package ACE alongside the Program Files ALL APPLICATION PACKAGES default launches clean on win32 10.0.26200 / Electron 43.4.1, so requiring S-1-15-2-2 specifically declared poison on healthy installs - and this branch acts on that verdict with a tree-wide icacls write and the crash-recovery dialog's primary cause. hasRestrictedPackageGrant stays reported for triage; only the verdict reverts. |
||
|
|
e6257b6e32 |
perf(worktree): resolve the WSL workspace root off the main thread when preparing (#17792)
`computeWorkspaceRoot` resolves a WSL repo's mirror root through `getWslHome`,
which is a synchronous `execFileSync('wsl.exe', ...)` with a 5s timeout. Two
worktree preparation paths ran it on the Electron main thread:
`prepareLocalWorktreeRootForRepo` (repo registration, clone completion, repo
update, project host setup, folder->git upgrade) and `prepareWorktreeCreateForRepo`
(the speculative checkout started while the create composer is open). On a stopped
or cold distro that froze every window for up to 5s. Being fire-and-forget did not
help: only 3 of the 16 `prepareLocalWorktreeRootForRepo` call sites are `void`-ed,
the other 13 are awaited inside IPC handlers, and the sync probe blocks the main
thread either way. `prepareLocalWorktreeRootsForRepos` runs the same probe for
every repo from the settings-save handler.
Adopt the existing `computeWorkspaceRootAsync` (now exported) at those two call
sites, and give the two resolvers a shared mirror-distro decision and shared
root-from-home layout so they cannot drift apart.
Also thread the mirror distro into the prepare-side path settings.
`createLocalWorktree` passes `getWorktreeMirrorDistro(store, repo)` and
`prepareWorktreeCreateForRepo` did not, so a `C:\` repo on a WSL project runtime
prepared under `C:\workspaces` while the create click looked under the mirrored
WSL root: the keys never matched and every prepared checkout was discarded, after
paying for a full checkout that sat until the 5min TTL. Pre-existing on main;
included because it is the same line and the same resolver.
Scope of the win, stated precisely: only those two preparation paths stop
blocking. On a reachable distro `getWslHome` caches on success, so before this
change the first repo paid one blocking probe and the rest were cache hits -- the
change makes that one probe non-blocking, it does not remove N probes. Failed
probes are never cached, so on a stopped distro N repos did pay N sequential 5s
blocking probes and now share one in-flight async probe.
Costs: five sync `computeWorkspaceRoot` callers remain (allowed-roots resolution,
the create click in worktree-remote, CLI create, watch targets, worktree trash),
and `getWslHome` reads only `wslHomeCache` -- it cannot join an in-flight async
probe. The guaranteed synchronous cache warm-up therefore becomes a window in
which one of those callers can still block and can spawn a second concurrent
`wsl.exe`. Concretely: opening the create composer and clicking Create within a
few hundred ms on a cold distro now pays the freeze on the click instead of on the
background prep. Separately, the mirror-distro fix makes prepare spawn an async
`wsl.exe` home probe for `C:\` repos on a WSL runtime, where it previously spawned
none.
No race added: `prepareWorktreeCreateForRepo` computes the preparation key and
inserts the registry entry in one synchronous run after the await, so two
concurrent creates still dedupe to a single prepared checkout.
`worktree-create-preparation-wsl-root.test.ts` runs the real resolver through
prepare and then claims the entry with the production consume-side call shape
(including the mirror distro), so a divergence between the two resolvers fails a
test instead of silently discarding every prepared checkout.
|
||
|
|
5c831c1846 |
fix(crash-reporting): record Linux MemAvailable at process-gone (#17733)
Linux process-gone crash reports emitted only systemMemoryFreeMB, which is /proc/meminfo MemFree — it excludes page cache and other reclaimable memory, so an OOM-killed renderer could report gigabytes "free" and hide the pressure that caused the kill. Electron 43 exposes MemAvailable as `available` on Linux, and getSystemMemoryAtGoneDetails already had the getSystemMemoryInfo() result in hand, so emit it as systemMemoryAvailableMB through the existing field table. The field is omitted on macOS/Windows (where Electron does not report it) and on a non-finite reading, matching every other bucket. |
||
|
|
a4cd00ed18 |
fix(wsl): drop the linked-worktree Git route cache after worktree mutations (#17791)
On Windows with a WSL distro configured, `prepareWslLinkedWorktreeGitRouting` caches for 30s which Git owns a drive-letter checkout, by reading that checkout's `.git` marker. `git worktree add/move/remove` rewrites exactly that marker, so the verdict could stay authoritative for up to 30s after it stopped being true. - Add `invalidateWslLinkedWorktreeGitRouting(cwd)`: drops the cached route and the probe retry backoff for that path and for anything under it (a submodule inside the worktree derived its route from the same marker walk). Eight calls at six sites: `worktree add`, `worktree move` (both paths), `worktree remove`, the prepared checkout's add, the finalize move (both paths), and the prepared-worktree discard. Five sites invalidate from a `finally`, because a Git failure can still have rewritten the marker; the prepared checkout's add invalidates on the success path only, since its failure path runs the discard, which has its own `finally`. - Split the parent-directory marker walk into `wsl-linked-worktree-git-route-probe.ts` (the routing module was at the `max-lines` ceiling) and have it report whether the walk settled. A `.git` file with no `gitdir:` line is a half-written marker mid `worktree add`: it is now retried under the existing backoff instead of cached for 30s. The route it yields is unchanged (distro); only the number of parent walks changes. An invalidation only drops cached state; a probe already in flight is left to finish and cache normally, so a mutation landing mid-probe is no worse off than main's 30s TTL. The cost is that a failed mutation also drops a still-correct route: `gitExecFileAsync` and `gitStreamStdout` re-resolve the route after their own `prepareWslLinkedWorktreeGitRouting`, and `gitSpawn` resolves again after the git-admission wait, so a command already in flight for that path can take the empty-cache default and run a host-owned checkout under `wsl.exe git`. It fails once and self-heals on the next command. Reachable only on win32 + configured WSL distro + drive-letter cwd; every other platform and configuration never populates this cache, so the new calls scan two empty maps. |
||
|
|
abc099e4c7 |
fix(worktree): run the create-base warm-up on the routed git host (#17794)
The speculative warm-up that runs while the create composer is open resolved
refs and fetched with host Git even when the project's runtime is a WSL distro,
while both the checkout preparation it feeds (`prepareWorktreeCreateForRepo`,
which already resolves `{ wslDistro }` itself) and the real create path run
inside the distro.
The concrete cost was a discarded fetch: `getCanonicalFetchKey` namespaces the
runtime's remote-fetch cache `wsl:<distro>` vs `local`, so the warm-up's fetch
landed in a namespace create never looks at, and create fetched again. On a
Windows host with no usable host-side Git the probes also failed outright, so
that cohort got no warm-up at all.
Thread the project's worktree Git options through the prefetch (resolved by a
non-throwing helper, because an optimistic warm-up must not surface a
repair-required runtime as a failure) so every probe and fetch runs where create
runs. `gitOptions` is a required argument, so a caller cannot drop the routing
silently. Host-routed calls keep their original arity, so macOS, Linux,
native-Windows-host projects, SSH repos and folder workspaces are unchanged.
Narrower than it looks: for a repo under \\wsl.localhost\<distro>\... the probes
were already routed by cwd, and for a repo on a Windows drive letter host Git
and WSL Git read the same on-disk repository, so the answers were already
correct there. What those cohorts gain is a fetch create can reuse; what they
pay is that the probes now run inside the distro (over /mnt/c for drive-letter
repos, which also newly arms the linked-worktree routing probe) and the
speculative fetch now shares create's per-remote fetch queue, as it always has
on native platforms.
Also collapse the three byte-equivalent copies of `hasLocalWorktreeBaseRef`
(create, prefetch, remote-repo create) into one in
git/worktree-base-ref-probe.ts, drop the host-only `hasLocalCommitObject` that
caused the routing bug, and add the first routing assertions on the create-path
consumers of the now-shared probe.
|
||
|
|
7f63db7d7a |
fix(git): resolve WSL drvfs Git metadata pointers on a Windows host (#17790)
When Orca's runtime is a WSL distro but the repo sits on a Windows drive, git inside the distro writes `/mnt/c/...` into a worktree's `.git` gitfile and its `commondir`, while Orca reads those files back through Win32. `repo-git-marker-scan` returned the pointer verbatim, Windows read it as drive-relative `C:\mnt\c\...`, and the worktree was reported `invalid`. Move that resolver out of `repo-git-marker-scan` into `src/shared/git-metadata-path.ts` and give it exactly one new case: on win32, a drvfs pointer resolved against a base path that is not a WSL UNC path now gets its drive spelling. Every other base/pointer/platform combination is byte-identical to the deleted helper, verified differentially across a base x pointer x platform matrix — macOS, Linux and native Windows are unchanged. `toWindowsWslDrivePath` is factored out of `toWindowsWslPath` so the drvfs matcher has one home; `toWindowsWslPath` itself is unchanged for all inputs, including the line terminators JS `.` excludes (fuzzed 2M inputs, 0 divergences). This changes the marker scan's verdict only. `resolve-git-dir.ts` and the relay's own copy still `path.resolve` the same `/mnt/c/...` pointer in the Win32 namespace, so a worktree that is now accepted still degrades quietly in conflict detection, sparse-checkout detection, the diff stamp and worktree listing. Those parsers are deliberately untouched here; see the PR description. Co-authored-by: Neil <neil@example.com> |
||
|
|
8b2d72114b |
fix(gitlab): stop the native glab known-hosts probe waking an idle WSL distro (#17789)
On Windows with no host `glab.exe`, the cwd-less `glab auth status` known-hosts
probe fell through to `wsl.exe -d <default distro>`. Probe failures are never
cached, so when that WSL leg also fails (glab absent or logged out inside the
distro) every forge detection re-booted the distro; when it succeeds it cached
that distro's auth hosts under the 'native' execution key, which the comment two
lines above the call already forbids. gitlab-auth-and-rate-limit.ts already
passes allowDefaultWslFallback: false for this exact command; the known-hosts
probe now agrees with it.
Connection-keyed probes keep the fallback: glab has no SSH/relay dispatch, so the
`glab api` calls this gates run the same local CLI with no cwd and would
otherwise disagree with the probe. wsl:<distro>-keyed probes were already
unreachable by the fallback, so passing the flag there is inert.
Counted child_process.execFile calls over 3 sequential probes, process.platform
forced to 'win32', host glab mocked ENOENT (wsl.exe / glab.exe spawns):
native key, glab absent in the distro too: 3/3 -> 0/3
native key, glab logged in in the distro: 1/1 -> 0/3, and that distro's
self-hosted hosts stop reaching the native known-hosts list
connection key: 1/1 -> 1/1, unchanged
Residual risk on that second config: a repo on a \\wsl$\ UNC path can be keyed
'native' (no project runtime match) while its own glab calls still route into
WSL by cwd, so it loses the seeded host and must re-derive it through
`glab auth status --hostname`. A co-resident Windows-path repo on the same host
can now write a shared `native\0<host>` unauthenticated negative that stalls that
recovery for one NEGATIVE_ENTRY_TTL_MS window. Fixing that properly means keying
the cache by the host that actually served the call, which needs an exec-layer
API change and is deliberately out of scope here.
Also splits the getGlabKnownHosts suite out of gl-utils.test.ts (796 counted
lines against the 800-line cap for tests) into gitlab-known-host-probe.test.ts.
|
||
|
|
ad760c8b92 |
fix(worktree): reject Windows drive-qualified shared paths in the symlink guard (#17793)
getSafeRelativePath strips leading `/` and `\` before testing absoluteness, so the only rooted spelling that can still reach the guard is a Windows drive designator. It tested that with the host `path.isAbsolute`, which left two gaps: the drive-absolute form `C:/payload` was refused on Windows but admitted as an ordinary relative filename on macOS/Linux, and the drive-RELATIVE form `C:payload` was admitted on every host including Windows, where `win32.resolve(root, 'C:payload')` discards the worktree root and lands under C:'s current directory. That holds for a drive root (`D:\wt`) and for the `\\wsl.localhost\<Distro>\...` UNC root a WSL project uses, both verified. The value reaches the guard from two configs: the per-user Worktree Shared Paths setting alone on the create path (createWorktreeLinkedPaths, called with `repo.symlinkPaths` from orca-runtime.ts:27820 and worktree-remote.ts:2636), and that setting merged with the repo's checked-in `orca.yaml` `worktree.sharedDirectories` on the removal and detection paths (getWorktreeSharedLinkPaths). No repo config is required to reach it. Replace both `isAbsolute` calls with a `/^[a-zA-Z]:/` test, verified by fuzz to be a strict superset of `posix.isAbsolute || win32.isAbsolute` for every post-strip input. No filesystem escape is closed on macOS/Linux, where such an entry resolves to a literal in-worktree filename. Cost: on POSIX, `:` is a legal filename character, so a shared/linked path whose first segment is `<letter>:...` is now refused where it previously worked — it stops being created, and if a worktree already holds an Orca-created symlink there it stops being excluded from the untracked-file filters in all four findExistingWorktreeSymlinkPaths callers, which means a refused non-force worktree removal (remove-registered-local-worktree.ts:91, orca-runtime.ts:30205), a phantom untracked row in Source Control (status-read.ts:90), and a blocked hosted-review creation (hosted-review-creation-git-state.ts:290) — and removeWorktreeLinkedPaths no longer unlinks it, so nothing cleans it up. Accepted because a per-host verdict would defeat the point of judging the same config identically on every host it is evaluated on. Co-authored-by: Neil <neil@example.com> |
||
|
|
a5796ec8eb |
refactor(runtime): split OrcaRuntimeService and compatibility tests (#17605)
* refactor(runtime): split OrcaRuntimeService into focused modules
* test(runtime): cover admission tiers and strict worktree reconciliation
* fix(runtime): preserve owner and structured session visibility
* fix(runtime): port post-extraction compatibility fixes
* fix(runtime): preserve skill-share cancellation barrier
* test(runtime): update identity inventory after extraction
* fix(runtime): preserve hook transport environment cleanup
* fix(runtime): consolidate idle probe imports
* test(runtime): retire split file process allowlist entry
* fix(runtime): route child process types through shared boundary
* test(runtime): preserve worktree host metadata precedence
* fix(runtime): update extracted test seams
* fix(runtime): gate the split's ts-nocheck set and restore the stop-confirmed contract
Audit follow-ups for the OrcaRuntimeService split:
- Freeze the 171 @ts-nocheck files behind a ratchet so no new file can disable
type checking. The split's linear mixin chain cannot express forward
references yet, so the existing suppressions are grandfathered; the baseline
may only shrink.
- Drop the stray @ts-nocheck at the end of orca-runtime-get-status.ts. It sat
after the first statement, where TypeScript ignores it, so the module was
already checked.
- Restore `retireRejectedPty(ptyId, stopConfirmed: boolean)` as a required
argument. The split widened it to optional and patched the resulting error
with `stopConfirmed === true`; an omitted argument would have silently taken
the unverified-stop path instead of failing to compile.
- Guard that every orca-runtime-tests fragment is imported by the compatibility
entrypoint. The fragments are .spec.ts, which no Vitest include glob matches,
so one left out of the list would silently stop running.
* fix(runtime): restore four behaviors the OrcaRuntimeService split dropped
Audit findings against the refactor's true base (
|
||
|
|
12be5aed9e | fix(browser): refuse devtools for offscreen guests (#17485) | ||
|
|
f116d2ca2a |
test(ci): retry Windows teardown EPERM and restart evaluate misses (#17780)
Restart-survival polls treated a recycled renderer as a hard failure. Wrap those evaluates so "Execution context was destroyed" is a pending miss. Windows package-lane teardowns after a force-kill used rmSync with force:true only, which does not absorb EPERM; put them on the shared maxRetries:8 policy. |
||
|
|
d2aab68ae7 |
Automations ux improvement (#17626)
* Add keyboard navigation to automations UI Improves workflow efficiency by enabling keyboard-driven navigation across automations list, run history, and detail pane tabs. * Add Escape key support to automations detail pane Pressing Escape now clears external and automation run page views, then returns to the automations list. Also improves cross-browser compatibility of keyboard event handling by using Element checks and getAttribute instead of dataset access. * Fix keyboard navigation to let Enter key reach focused controls - Enter key now passes through to focused buttons, links, and other interactive controls - Arrow key navigation through automation run history still works - Prevents intercepting native keyboard behavior of interactive elements * improve test * Move keyboard focus to follow row selection When navigating automation runs with arrow keys, focus must follow the selection so Enter key acts on the newly selected row rather than the previously focused one. |
||
|
|
50938b2dbd |
Serialize filesystem watcher batch flush operations (#17602)
* Serialize filesystem watcher batch flush operations - Prevent dropped events during rapid concurrent file changes - Queue and drain follow-up batches to preserve event ordering - Cancel pending batch work when watchers are torn down * Prevent queued batch drain while debounce timer is armed An armed timer means the debounce window is still open. Drain only after the window closes to avoid splitting related filesystem events across separate payloads. * Remove redundant batch timer cleanup Rely on cancelLocalBatchFlush to handle the batch timer teardown, eliminating duplicate logic in the watcher cleanup path. |
||
|
|
2222e54754 | refactor(test): organize SSH and terminal recovery fixtures (#17751) | ||
|
|
ae35e044f2 |
fix(terminal): keep restored OSC-8 ranges across a no-op resize (#17759)
Cold restore seeded a checkpoint's OSC-8 link ranges and then replayed records
that resize, so any resize record after the checkpoint dropped them and
restored hyperlinks in scrollback lost clickability. Same-size resize records
reach the durable log routinely, because every attach re-asserts the pane's
dimensions and session-output-plane records each one without a same-size
dedupe — so an ordinary reattach was enough to lose the links.
Restored ranges are row-indexed, so clearing them on a reflow is right; a
resize to the size already applied is not a reflow. Gate on the dimensions
actually changing.
Introduced in
|
||
|
|
704167197a |
perf(relay): serve one ps capture per window and pin the batched inventory path (#17763)
Follow-up defect fixes for the batched PTY-inventory evidence path (#17525), now on main. - One memoized `ps` capture serves both the lenient and strict views. The two readers ran byte-identical argv behind separate caches, so a relay serving both forked `ps` twice per 500ms window — the doubling issue #6288 removed. - Drop the `byPgid`/`byTpgid` indexes no resolver reads, plus the zero-caller `parseProcessTableRowsStrict` and `getFreshStrictProcessTableSnapshot`; the batch resolver now reuses the shared index lookup and candidate score instead of private copies. - Restore `getForegroundProcessName`'s ladder contract: the extracted table scan answers null again, so an unconfirmed wrapper fallback publishes the recognized (normalized) name rather than node-pty's raw one. - Pin the SHIPPED `pty.listProcesses` path: one capture and one linear row pass for N panes, and node-pty's own name (never "shell") when the capture cannot disambiguate a `node`/`python` wrapper. - Pin the hidden-pane cadence gate in the production option shape, and move the strict-parser coverage next to the parser it tests. |
||
|
|
26031ca317 |
fix(browser): scroll oversized viewport presets (#17569)
* fix(browser): scroll oversized viewport presets * fix(browser): preserve guest wheel scrolling at viewport edges * fix(browser): keep viewport scroll state synchronized * test: assert partial viewport wheel forwarding |
||
|
|
4ac8a8912c |
fix(startup): install the app environment with the userData decision (#17755)
src/main/index.ts decided where userData lives at module scope, then installed the AppEnvironment port ~180 lines later inside the single-instance-lock block. Every statement in that gap was a latent failure: a path resolve there either threw 'AppEnvironment not initialized' and killed the process, or — with the accessor installed but the decision not yet run — would have memoized the pre-override directory in getCanonicalUserDataPath() for the whole session. The first outcome shipped. #16761/#16698/#17509 were one statement landing in that gap and killing every macOS `orca serve` across 1.4.190-1.4.192; #16762 moved that call but left the gap. Install the port and capture the canonical path immediately after the two calls that decide them, so the window is zero rather than small. Both are inert at this point — ElectronAppEnvironment holds no state and calls `app` lazily per accessor, and initDataPath only joins strings — so nothing that depended on the old position moves with them. The secret store stays where its pre-ready Keychain note applies. The throw is kept and still covers the case it should: resolving a path before the decision has run. Guarded by a source-level assertion that the decision, the install and the capture stay adjacent. Fixes #17750 |
||
|
|
8ac1c6e2ac |
perf(git): bound ref and worktree scans (#17655)
* perf(git): bound ref and worktree scans * fix(repo-search): clamp oversized ref limits * fix(worktree): keep strict worktree listing unshared The shared-scan re-export flipped every `listWorktreesStrict` caller from an isolated subprocess to the coalesced scan. `git worktree prune` in the removal recovery path does not bump the scan generation, so a post-prune verification could join a pre-prune scan, see the stale row, and report a successful removal as a stale registration. The same gap defeats the post-archive-hook rechecks that exist to catch an external Git client locking the row. Restore the unshared export and make coalescing opt-in via `listWorktreesSharedStrict`, which existing callers already use deliberately. * fix(git): separate a proven absent ref from a failed probe `show-ref --verify --quiet` exits 1 for a missing ref, but so does `wsl.exe` when its own launch fails, so reading any exit 1 as absence collapsed `unverifiable` into `exited`. A genuine miss prints nothing while a wrapper failure always explains itself, so require empty stderr alongside the exit code; a runner that reports no stderr at all keeps its exit-code contract. That same signal removes a spawn regression: `show-ref` is a direct-git read under WSL, and the runner retried any numeric exit through the user's interactive login shell. The replaced `for-each-ref` exited 0 on a miss, so absence never retried; every absent probe now would. Treat a quiet exit 1 as Git control flow and skip the fallback. Also narrow the hosted-review suffix fallback: the replaced `refs/remotes/*/<base>` could not cross a slash, but `show-ref -- <base>` matches at any depth, so `origin/feature/main` answered a query for `main` and submitted a review against a base the provider rejects. Refresh the real-binary compatibility contract to the shipped excludes, and assert exact probe concurrency rather than an upper bound so a regression to serial probing fails. |
||
|
|
a5be10815c |
perf(terminal): drop the headless snapshot cache, keep the spawn lock (#17752)
Removes the snapshot memoization added in #17667 and everything that served it: the epoch, markMutated/markWritten, the no-op resize gate, and the HeadlessSnapshotCache module. The shared/exclusive spawn lock from the same PR stays — it is the half that carries the measured win. Why: a controlled A/B on merged main could not show the cache paying for its memory. Same worktree, same sessions, reattach stable_adoption per session: cache + resize gate (as merged) 16 / 67 / 78 / 87 ms cache off, gate on 17 / 55 / 68 / 80 ms cache off, gate off 13 / 62 / 73 / 83 ms Indistinguishable. Cold 4-tab activation was likewise unchanged (217-296ms without the cache vs 226-309ms with), which is expected — a first attach is always a miss. The reason it under-delivers is a design fact the original PR missed: attach does not request the full buffer. terminal-host-session-create.ts passes resolveDaemonSessionScrollbackRows() — a deliberate 1000-row live window, capped because unbounded retention once OOM-killed a host. Serializing 1000 rows is cheap, so there was little for a cache to save on that path. Against that, the cache retained up to MAX_CACHED_SNAPSHOT_BYTES (4MB) per entry across MAX_CACHED_SNAPSHOT_WINDOWS (2) entries per emulator, for the session's lifetime, with no aggregate budget across sessions. It also carried an invalidation contract that produced three separate over-invalidation bugs during review (the parse fence, setCwd/setLastTitle, and the no-op resize). The lock fix is unaffected and independently measured: the `options` phase, which is pure queueing, went 0/125/212/291ms -> 1/1/1/1ms across a 4-tab worktree activation and stays there. |
||
|
|
8f15f217a2 |
Preserve user-set workspace names across branch changes (#17448)
* fix(worktrees): preserve user workspace names across branch changes * test(worktrees): cover pinned rename metadata * fix(workspaces): address display-name review edge cases * fix(workspaces): keep automatic names fresh across refreshes * fix(workspaces): preserve legacy CLI labels * fix(workspaces): preserve display-name provenance across hosts * fix(workspaces): honor legacy display-name provenance * fix(workspaces): fence display-name refresh races * fix(workspaces): accept peer renames from provenance-less hosts The old-host preserve fence kept a pinned local label on every refresh, which also suppressed a legitimate rename another client persisted through the same host until app restart. Narrow it to labels the host re-derived itself (branch short name, or path basename when detached); any other changed label in a mode-less response is explicit meta a peer wrote there. Stale prior-label responses stay covered by the downstream staleness fence, in-flight writes by the pending fence. * refactor(workspaces): unify display-name pin derivation Three call sites (renderer optimistic update, local IPC updateMeta handler, remote worktree.set handler) each restated the same formula; a future edit to one would silently skew provenance between paths. |
||
|
|
20a12a6a46 |
perf(codex): share one launch-prep hook install across a spawn burst (#17669)
* perf(codex): share one launch-prep hook install across a spawn burst Codex launch prep runs a full managed-hook install on every local PTY spawn, and both install lanes serialize globally per Codex home. Opening a multi-pane worktree therefore paid N full installs back to back, and a resumed Codex pane prepares twice. Concurrent spawns for the same runtime home now share one run; the promise is dropped as soon as it settles, so the next launch still re-reads hooks.json and the user's trust state. Also split the `host_env` spawn-timing phase, which spanned the entire Codex preamble and pinned that cost on the env builder that ran last. * refactor(codex): unify the two hook-install single-flight lanes Both the WSL and launch-prep lanes now share one generic in-flight helper instead of duplicating the map bookkeeping. Also routes the WSL launch-prep install through the serialized variant, which closes the same per-spawn serialization gap on WSL that the native lane just got. * refactor: extract the shared in-flight run dedupe The codex hook service and the GitHub conflict-summary cache had grown near-identical private copies of the same single-flight helper. Both now use one module, which also keeps the hook service clear of the 300-line budget. The shared copy keeps the identity check on clear so a late settle cannot evict a newer entry for the same key. |
||
|
|
4ca232c159 |
perf(terminal): cut cold worktree-switch attach latency (#17667)
* perf(terminal): let a worktree's terminal spawns run concurrently
The per-worktree terminal mutation guard was a FIFO mutex, so activating a
multi-tab worktree made each tab wait for every predecessor's whole spawn.
Measured with ORCA_PTY_SPAWN_TIMING=1 on a 4-tab worktree, the `options`
phase was a pure-queueing staircase: 0 / 125 / 212 / 291ms.
The invariant that guard protects is spawn-vs-sleep exclusion, never
spawn-vs-spawn. Replace it with a writer-preferring shared/exclusive lock:
spawns share, sleep still excludes, and a queued sleep blocks later spawns
so a stream of spawns cannot starve it into its 12s deadline.
Same worktree after: options = 2 / 3 / 12 / 34ms, and stable_adoption
flattens from 65/398/398/312ms to a steady ~253ms.
The existing folder-workspace control assertion asserted the FIFO behavior
this removes, so it is re-based on sleep-vs-spawn (which still queues) and
joined by a case asserting concurrent same-worktree spawns.
* perf(terminal): memoize the headless snapshot per mutation epoch
Attaching a viewer serializes the session's whole headless buffer
synchronously on the daemon event loop, so every reattach of a quiescent
session re-serialized identical bytes. Measured with ORCA_PTY_SPAWN_TIMING=1,
stable_adoption was 253-281ms per session on reattach.
Move snapshot assembly into HeadlessSnapshotCache and memoize its expensive
parts (the serialize, the OSC link walk, the frame-restore fields) on a
mutation epoch that every emulator state mutation bumps, so a cache hit is
byte-identical by construction rather than merely fresh-enough. The async
write path bumps on entry and again in the parse-completion callback, so a
snapshot taken mid-parse can never be retained.
Reattach of a quiescent session after: stable_adoption 12ms. Sessions with
output since their last snapshot re-serialize exactly as before.
Retention is capped: an entry is held for the session's lifetime once it goes
quiescent, and a renderer may request 50k scrollback rows, so oversized
payloads serve normally but are not retained. Cache hits clone nested values
so a caller mutating its snapshot cannot corrupt later ones.
* perf(terminal): keep the snapshot cache warm across zero-byte parse fences
Review follow-up. flushParsedWrites() is write(''), used purely as a parse
fence, and every getSettledSnapshot runs one — so the epoch bump on an empty
write evicted the attach entry on each checkpoint read, defeating the cache
for any session that gets checkpointed.
Zero bytes cannot mutate the buffer: the OSC and mouse-mode scans are no-ops
on '' and the partial-escape tail is idempotent, so skip the bump for empty
data. Real writes still bracket themselves, and any write a fence orders
behind has already bumped on its own completion.
Also invalidate on dispose, so a post-dispose read can never be served a
pre-dispose entry, and freeze the emulator's public method surface in a test:
the cache's correctness rests on every mutator calling markMutated(), which is
convention rather than a type, so a new method should be a deliberate decision
about invalidation instead of a silent stale-snapshot bug.
* refactor(terminal): apply elegance review to the attach-latency fixes
Reuse: waitForMutationGrant hand-rolled the deadline race that
settleBeforeDeadline (same directory, four existing callers) already owns.
Using it also picks up the timer.unref() the local copy lacked, which was
keeping the Node event loop alive for up to the 12s sleep deadline.
Simplify: drop the waiter `abandoned` flag. The timeout path sets it and
splices the waiter out in the same synchronous block, so no queued waiter can
ever be observed abandoned and both reads were unreachable. The splice is what
actually does the work; the comment now carries why that makes a
grant-after-timeout unrepresentable. drain() then collapses into its loop
condition and reuses markActive() instead of inlining it twice.
Extract markWritten() so the zero-byte parse-fence rationale lives at one
mutation gate instead of being restated at three call sites.
Match the daemon's byte-accounting convention: the retention cap is now
expressed in bytes with code-unit sizing, like MAX_COLD_RESTORE_CACHE_BYTES
next door, so the two retention budgets read in one unit. Same effective cap.
The public-surface guard test caught markWritten on the first run, which is
the behavior it was added for.
* perf(terminal): stop discarding the snapshot cache on non-memoized fields
Second elegance round, and it found the same class of bug as the parse-fence
one: cwd and lastTitle are read fresh on every build and were never memoized,
yet setCwd/setLastTitle bumped the epoch — discarding a whole serialize to
update a field the cache does not hold. OSC 7 cwd updates land on every `cd`,
so this was a live cost on exactly the busy sessions the cache targets. The
invariant is "every mutation of a memoized part", not "every state mutation";
both docblocks said the latter and are corrected.
Drop the dispose bump too. A post-dispose getSnapshot re-serializes the
disposed terminal to byte-identical content, so the bump bought nothing and
only reached into a disposed xterm — verified by probe, not assumed. Its test
asserted zero serializations after dispose, which no implementation could
violate; it passed with the bump deleted.
Also memoize rehydrateSequences (a string, so no clone needed) instead of
rebuilding it on every hit, derive the frameRestore type from
buildFrameRestoreSnapshotFields so a new field cannot flow through at runtime
while the type omits it, inline the single-caller resolve() into build(), and
move the write() entry bump after the sync early-return so the three bumps map
1:1 onto sync / async-pre-parse / async-post-parse.
The surface guard is sorted in source and renamed to say what it freezes: the
prototype, TS-private members included.
* fix(terminal): correct the fence justification and key the cache by window
The markWritten docblock claimed "zero bytes cannot mutate the buffer". That
is false, and I verified it: `_core.writeSync('')` drains xterm's pending queue
and applies it. The exemption is still correct, but for a different reason —
a fence cannot introduce an *unattributed* mutation, because any bytes it
drains belong to a queued async write whose own completion callback bumps
first. The two write regimes are exhaustive: with writeSync present nothing
can queue, without it every write is async and self-bumps. A comment asserting
a false invariant is worse than no comment, since the next change may rely on
it, so it now states the real one.
Key the cache by scrollback window instead of a single slot. Consumers ask for
different windows against the same emulator — attach passes the full window
while agent/text reads pass 0 — so one slot thrashed to a 0% hit rate whenever
they alternated, silently removing the benefit on runtime-side emulators. Two
entries cover every caller pair in the tree.
Drop the epoch counter: markMutated already nulls the retained entry and an
entry is only ever stored under the current epoch, so the comparison could
never fail. Invalidation is simply "clear the cache".
* refactor(terminal): name the lock sides shared/exclusive for a third caller
Rebasing onto main surfaced a semantic conflict the merge applied cleanly:
main added runWorktreeTerminalMutation (terminal orphan adoption, #17159) as a
third caller of the guard this PR changed. It needs the exclusive side —
adoption reconciles a worktree's terminal records, so it must not interleave
with a spawn registering a pty or with a sleep, which is exactly the semantics
it was written under when the guard was a plain mutex.
With three operations, naming the sides after two of them no longer fits, so
the kinds are now `shared` (spawn) and `exclusive` (sleep, adoption) — what
they do rather than who calls them.
* perf(terminal): do not invalidate the snapshot cache on a no-op resize
Re-measuring the final rebased build caught the cache barely working on the
path it exists for. Every attach re-asserts the pane's dimensions, and resize()
bumped unconditionally, so a reattach of a fully idle session missed its own
cached snapshot and re-serialized.
Measured on the same 4-tab worktree, reattach of sessions verified quiescent
(no buffer change over 4s), stable_adoption per session:
before this commit: 98 / 382 / 395 / 418 ms
after: 16 / 67 / 78 / 87 ms
A resize to the size already applied changes nothing the snapshot reads, so it
now returns early — which also stops it clearing restoredOscLinks, correct
since no rows shifted.
|
||
|
|
18e8fe4770 |
fix(serve): install supervisor disconnect quit after app environment init (#16762)
Moves installServeSupervisorDisconnectQuit(isServeMode) out of module scope in src/main/index.ts to just after setAppEnvironment() and initDataPath(). The call resolves the serve update handoff path through getCanonicalUserDataPath(), which throws by design until the app environment accessor is installed. At module scope that throw was unconditional on macOS whenever the CLI set ORCA_SERVE_UPDATE_HANDOFF_PATH — which it does by default — so every `orca serve` process died at startup before it could listen, and the supervising service manager restarted it into the same crash. Reported in #16761, #16698 and #17509; shipped in 1.4.190 through 1.4.192. Guards added so it cannot drift back: a source-level ordering assertion that also pins the call synchronous and inside the single-instance block, and a runtime test that keeps the real path resolver, since the existing suite mocks it and therefore could never have caught this. Fixes #16761 Fixes #16698 Fixes #17509 |
||
|
|
8bebcf5def |
fix(terminal): preserve large agent prompt pastes (#17718)
* fix(terminal): preserve large agent prompt pastes * fix(terminal): guard oversized SSH PTY writes * fix(terminal): avoid timer delay for generic sends * fix(pty): propagate provider write refusals |
||
|
|
02a7742406 |
fix(artifacts): raise desktop sharing limit to 5 MiB (#17708)
* fix(artifacts): raise desktop sharing limit to 5 MiB * fix(artifacts): enforce recovery content limit * fix(artifacts): bound recovery request envelopes * fix(artifacts): clarify oversized request error |
||
|
|
aa658d28e3 |
test(updater): stop a slow module import from failing the next test (#17726)
* test(updater): stop a slow module import from failing the next test
`updater.ts` is 2.4k lines. Its first transform in a worker costs ~1.4s idle
but 45s+ when the machine is oversubscribed, which is past the 30s
`testTimeout`. Vitest cannot cancel the timed-out test body, so the abandoned
continuation went on to call `setupAutoUpdater` during the *next* test — with
the harness already reset — and failed it with:
AssertionError: expected "vi.fn()" to be called 1 times, but got 2 times
That is the exact signature of the abandoned-instance timer flake fixed in
#17649/#17663, so a machine-load timeout reads as that regression returning and
sends the reader hunting in the wrong place.
Two changes, in `updater-test-module-loader.ts`:
- `loadUpdaterModule()` replaces every `await import('./updater')` in the suite.
It records the test that asked for the module and throws if the import
resolves after that test ended, stranding the continuation so the timeout
stays the only reported failure. This removes the trap.
- `warmUpdaterModule()` imports the module once in `beforeAll`. The transform is
cached across `vi.resetModules()` — only a file's first import pays it — so
warming moves that one slow import onto the 60s `hookTimeout` and leaves every
in-test import at re-evaluation cost (~25ms idle).
Measured on a 16-core mac, first vs later import in one file: 1439ms / 25ms
idle, 8339ms / 149ms under 40 CPU hogs, 45521ms / 15182ms under 400.
Under 400 hogs the suite went from 15 files and 22 tests failing (15 timeouts
plus 7 misleading assertion failures) to 23/23 files and 269/269 passing. Under
900 hogs it degrades into 14 plain `Hook timed out in 60000ms` failures and zero
assertion failures.
* fix: tighten the fence, surface its warning, stop patching timers on warm-up
Review findings on the loader:
Drop trackRealTimers() from warmUpdaterModule(). It was inert — updater.ts
arms no timers at module scope — and actively harmful for the 5 files that
build their own mocks and never call clearTrackedRealTimers(). Those files
previously had pristine timer globals; the warm-up installed a wrapper that
was never restored and whose armed-handle set grew unbounded.
Key the fence on TestRunner.getCurrentTest() instead of currentTestName.
Nothing ever clears currentTestName, so the fence only fired once the *next*
test had started; a continuation resolving during the timed-out test's own
teardown, or after the file's last test, was still handed the module. The
last-test case mattered: the harness afterAll has already cleared timer
tracking by then.
Emit the diagnostic through process.emitWarning. The throw lands on a promise
vitest already settled, so the message explaining why the continuation was
stranded was discarded and reached nobody — which was the entire payoff.
Widen the loader test's race margin 50ms -> 500ms. It gated on the
test-to-test transition completing in 50ms, so the regression test for a
contention bug could itself fail under contention.
|
||
|
|
08d95b9979 |
test(native-chat): remove initial-snapshot recovery race in watch-error test (#17722)
Root cause: the test cleared the injected tail-reader failure *before*
writing the recovered transcript line. The capped rotation retry loop is
still firing at that point, so a retry drain could succeed against the
still-empty file, consume the pending initial drain, and emit an empty
initial snapshot (`[], false, 0, undefined, undefined`). The later manual
watch callback then took the append path, and `u-recovered` never reached
onInitialSnapshot -- producing the CI failure
`expected [ false, +0, ...(5) ] to deeply equal ArrayContaining{...}`.
That empty-snapshot-then-append sequence is correct product behavior, so
this is a test bug: write the content first, then clear the failure, so no
drain can ever observe a readable-but-empty transcript. The assertion now
checks the exact recovered snapshot instead of a flattened
arrayContaining, so an empty recovery snapshot fails loudly.
|
||
|
|
a381b47437 |
test(cursor): widen Windows hook spawn budget to fix ETIMEDOUT flake (#17721)
* test(cursor): widen Windows hook spawn budget to fix ETIMEDOUT flake `package (windows)` failed once on an unrelated packaging PR with `spawnSync cmd.exe ETIMEDOUT` at hook-service.test.ts:78. This is an infrastructure-timing flake, not a logic race: the assertion is `expect(result.error).toBeUndefined()` and `ETIMEDOUT` only means the spawnSync `timeout` elapsed. Cursor is the heaviest of the hook-service suites on Windows. Its managed command is the PowerShell encoded launcher, so one hook run is cmd.exe -> powershell.exe -> cursor-hook.cmd -> curl.exe: four process creations, one of them a CLR start that installer-utils.ts itself documents as ~300ms warm and "visibly slow". The sibling suites (codex, grok, agent-hooks/installer-utils) spawn the .cmd directly and set no per-spawn timeout at all, so 15s here was a one-off, not a convention. Raise the per-spawn budget 15s -> 30s to match WINDOWS_PROCESS_TEST_TIMEOUT_MS in src/shared/setup-agent-sequencing*.test.ts and the 30-90s used by the real-subprocess tests in src/main/browser. 30s is ~30x the warm cost of the chain, which leaves room for CPU contention and Defender scanning of the freshly written .cmd on a packaging runner. The default vitest testTimeout is also 30s, which would have become the new binding constraint (the protocol case runs 16 chains back to back), so give the four spawning cases 120s. That keeps ETIMEDOUT - which names the stuck process - as the failure you see, instead of an opaque case timeout. No product behavior changes and no end-to-end coverage of the Windows launcher is removed. * fix: halve the case timeout and correct the contention rationale Review found the stated cause wrong. pr.yml runs "Test Windows-specific boundaries" before "Build package inputs", so electron-builder is not running. The real contender is that vitest invocation itself: ~25 files at maxWorkers 4, including five real-Electron suites and two node-pty tests. 120s was over-provisioned. windows-hook-payload-delivery.test.ts drives the identical PowerShell chain on the same job with a 60s case budget; 60s gives the same property here (16 warm spawns plus one 30s outlier) and halves time-to-signal on a genuinely stuck chain. Also record that 30s deliberately exceeds the product's own MANAGED_HOOK_TIMEOUT_SECONDS (10s) — this test gates launcher correctness, not user latency, so the SLA is not the right bound. Left windows-hook-payload-delivery.test.ts at 15s: its value is deliberate, set to mirror Claude Code abandoning a hook at 10s. |
||
|
|
872bd51d47 |
fix(native-chat): reland large structured command results (#17720)
* fix(native-chat): preserve large structured command results (#17707) * fix(native-chat): preserve large structured command results * chore: place native chat validation artifacts under docs * chore: drop stale root package config * fix(native-chat): enforce rebuilt lifecycle append slots --------- Co-authored-by: Merge Sim <sim@local> * chore: omit native-chat reland planning docs * fix(native-chat): remove journal store import cycle * fix(native-chat): keep journal factory acyclic --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
dc5db4b01c |
fix(lint): merge duplicate process-table-snapshot imports (#17724)
main is red on `static analysis`: oxlint's code-quality pass runs with --deny-warnings, and agent-foreground-process-batch.test.ts imports '../../shared/process-table-snapshot' twice (lines 5 and 13), tripping "Modules should not be imported multiple times in the same file". Introduced by #17525. It blocks every open PR, none of which can go green until this lands. |
||
|
|
9477b5fcbb |
feat(ssh): batch process evidence in PTY inventory (#17525)
* feat(ssh): batch process evidence in PTY inventory * fix(ssh): accept Linux kernel process rows and make no-evidence polling push-driven * fix(ssh): preserve process evidence polling semantics --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
894ed75abb |
Revert "fix(native-chat): preserve large structured command results (#17707)" (#17719)
This reverts commit
|
||
|
|
5fe37729ea |
fix(native-chat): preserve large structured command results (#17707)
* fix(native-chat): preserve large structured command results * chore: place native chat validation artifacts under docs * chore: drop stale root package config * fix(native-chat): enforce rebuilt lifecycle append slots --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
68de1be517 |
fix: satisfy GitLab hook and test lint gates (#17694)
* fix: satisfy GitLab hook and test lint gates * Rename electron-vite target config to .cts The .cts extension keeps the config as CommonJS, allowing electron-vite to load each parallel target without sharing its timestamp-named ESM temp file. |
||
|
|
aabcc57366 |
fix(runtime): publish remote control outages to host surfaces (#17531)
* fix(runtime): publish remote control diagnostics to renderer * test(runtime): account for diagnostics bridge listener * fix(i18n): add runtime connection state labels * test(runtime): clean up shared control connection * fix(runtime): fence diagnostics by shared-control capability * fix(runtime): preserve authoritative transport state * fix(runtime): preserve diagnostic overlay lifecycle * fix(runtime): avoid publishing unchanged diagnostics state --------- Co-authored-by: Merge Sim <sim@local> |
||
|
|
db84894eef | fix(runtime): isolate paired terminal creates from host focus (#17713) | ||
|
|
63d60e0ef0 |
fix(mobile): unblock targeted SSH session tab refresh (#17486)
* Fix targeted mobile SSH session tab refresh * Preserve fail-open explicit workspace resolution * Strengthen SSH session refresh oracle |
||
|
|
d1350735ef | fix(orchestration): explain invalid send message types (#17487) | ||
|
|
faaf38ac45 | fix(orchestration): submit staged mail pointer while working (#17470) | ||
|
|
5ff1aa540e |
fix(codex): re-land WSL direct-home cutover with counsel findings fixed (#16854)
* 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> |
||
|
|
3cb9b5d87f | fix(serve): keep Chromium switches out of CLI redirect (#17633) | ||
|
|
fbe94ceff6 |
fix: close readiness gaps found by merged-change audit (#17159)
* fix(ssh): fence stale kills and retired pane replay * fix(ssh): support cancellable interactive authentication * fix(ssh): await remote catalog before snapshot adoption * fix(pty): contain Windows ConPTY input failures * fix(power): avoid redundant macOS display blocking * perf(editor): narrow markdown override subscriptions * fix(quick-open): close directory handles after reads * refactor(linux): remove unused proc socket scanner * fix(usage): apply flat Sonnet 4.6 pricing * ci: prime Node next native test cache * docs(skills): resolve snapshot cleanup data path * fix(ssh): recover install locks after host reboot * test(ssh): recognize boot-aware install locks * test(ssh): prove previous-boot lock recovery live * test(wire): pin pre-metadata release coverage * fix(terminal): preserve remote tab ownership through recovery races * test(runtime): fence replaced terminal handles in agent guard * fix(ssh): preserve remote snapshot authority across polls * fix(pty): contain late ConPTY output EPIPE * test(pty): register Windows exit watcher before kill * fix: close SSH and tab readiness race gaps * fix(tabs): retain headless order and placeholder titles * fix(build): avoid parallel electron-vite config race * test(windows): avoid MSYS temp path rewriting * test(windows): avoid killing exited PTY * fix(pty): avoid late ConPTY input teardown race * fix(terminal): sync reconnect error ownership after commit * fix(runtime): use canonical worktree identity comparison * test(ssh): assert complete cold-hydration baseline * test(windows): invoke quoted retention fixture via PowerShell * test(windows): read ConPTY grid through mode con * fix(terminal): publish PTY replacements atomically * fix(terminal): infer stale identity on reattach * fix(terminal): fence stale pane PTY callbacks * fix(terminal): fence stale pane binds after rebind * fix(terminal): reject stale pane transport callbacks * fix(terminal): fence mirrored reattach spawn callbacks * fix(terminal): replace stale pane PTYs on remount * fix(ci): size the Windows launcher-compile test budget from measurement `native-smoke (windows-latest)` fails ~4.5% of runs on `preserves a multiline argument through the compiled remote launcher` with "Test timed out in 15000ms" — on unrelated PRs, for reasons that have nothing to do with them. Across 176 sampled attempts it is the only red that job produced, and it hit seven different PRs in two days: #16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085. The test is six process creations: powershell.exe forks csc.exe, then the freshly compiled orca.exe forks node.exe, twice. Hosted Windows runners periodically slow process creation down, and this test amplifies that far harder than anything else in the job. Comparing the 80 attempts where it ran under 3s against the 12 where it ran over 12s, its own median goes 2198ms -> 15917ms (7.2x) while the same file's powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash process tests in the neighbouring file move 1.4x, and the other 35 files put together move 1.5x. Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms, correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%) exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s testTimeout, so deleting the override and inheriting the config is not enough on its own. 60s clears all 176 with 1.7x headroom on the worst. This is slow, not hung. Every body here is synchronous spawnSync, so Vitest cannot interrupt one — the timer fires only after the body returns and the reported duration is real elapsed time. That is why a failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The work finished; the stopwatch was short. Seven reruns at one identical head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the last of those would have been red on code that had not changed. The 15s came from #8897, which raised this test off Vitest's built-in 5s default because the job then ran bare `pnpm vitest run`. #8909 landed 3h27m later and pointed the job at config/vitest.config.ts, which is the real fix for that. The constant stayed behind and has been the binding budget ever since. * fix(terminal): fence stale remount reattach ownership * fix(terminal): reconcile mounted pane identity after replacement * fix(terminal): fence stale reattach fallback ownership * fix(terminal): fence deferred SSH reattach ownership * fix(terminal): fence stale split pane ownership callbacks * fix(terminal): keep stale spawns from consuming startup --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
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 (
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
c09810b641 |
perf(rpc): restore compiled Zod request schemas without override (#17374)
* 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. |
||
|
|
ca516a4306 |
test(git): pin FETCH_HEAD lock order in the admission lifetime test (#17641)
`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. |
||
|
|
6a65d8406a | perf(ssh): index source spans by ID (#17504) |