`findExactRepoOwner` already refuses a repo carrying both a runtime
`executionHostId` and a `connectionId` -- `resolveRepoOwnershipEvidence` calls
that pair contradictory, and one non-owned candidate voids the whole lookup.
There is also no way for a `connectionId` to yield a `runtime:` host id, since
`toSshExecutionHostId` always emits `ssh:`. The runtime arm of
`connectionMatchesHost` could therefore never decide anything, and the test
meant to pin it was passing through the contradiction gate instead.
Keep the SSH arm, which does gate, and record where the runtime refusal
actually comes from. Unreachable code on a destructive path reads as a
guarantee it is not making.
Refs #17776
A paired client's WorktreeMeta for a runtime host is exempt from
gcStaleWorktreeMeta -- that GC skips any row that is not local on both the
repo and the meta's hostId -- so a scan-proven removal is the only thing that
ever retires one. Both halves of that path were gated to `ssh:`, so the client
kept a row for every remote worktree it had ever seen and dropped none.
The renderer already computed the removals for runtime hosts and purged its
own in-memory state with them; only the persisted half bailed. Widen it, and
the matching main-side handler, to runtime hosts. `OffHostExecutionHostId`
names the set precisely: the hosts the local-only GC skips.
Also require `source === 'git'` before retiring anything. `session-fallback`
reports `authoritative: true` but is the truncated, visibility-filtered
`worktree.list` reply from a host too old for `worktree.detectedList`; its
omissions are no evidence a checkout is gone. That guard did not matter while
this only ran the in-memory purge, and does now that it deletes rows.
A repo that reaches its checkouts over a connection is still never condemned
under a runtime host id -- the host that executes owns that verdict.
Refs #17776
The row was stamped `ssh:build-box`, which
`captureNativeLocalWorktreeMetadataScanExpectation` filters out before the
prune runs -- so it survived whether or not any host gate existed and pinned
nothing.
Stamp it `local` so it is a genuine prune candidate whose directory really is
missing, and make the fixture identical to the first case apart from
`connectionId`. That pairing is what proves the behavior: the same fixture
without a connection loses the row. Deleting any single gate would not show
it, since four independent checks derive from `connectionId` on this path.
Refs #17776
`pruneMetadataMissingFromAuthoritativeLocalScan` had exactly one caller:
`ipcMain.handle('worktrees:listAll')`. A headless runtime host has no
renderer, so it never ran, and that host's `worktreeMeta` grew without bound
even for its own local repos -- 129 of 139 rows dangling on the profile in
#17776.
Run it from the runtime's own detected listing instead. That is the same
trigger on the same evidence: `listDetected` already prunes lineage on an
authoritative scan, and a paired client refreshing a remote repo calls
`worktree.detectedList`, so the host now sweeps exactly when the desktop
would have.
The expectation is captured before the scan, because listing can mutate
metadata synchronously before its first await. WSL-routed repos are excluded
for the reason the desktop listing excludes them: the listing runs in the
distro and reports Linux paths while metadata can hold UNC ones, and v1
cannot prove those aliases equivalent. A runtime needing repair throws rather
than resolving routing, which is likewise no basis for deleting rows.
The prune's own gates still apply, so an SSH- or otherwise off-host repo is
never swept from a local stat -- the execution host owns that verdict.
Refs #17776
`activeWorktreeId`, `activeWorkspaceKey` and `activeWorktreeIdsOnShutdown` are
pruned by bespoke rules rather than by owner key, so no owner-key loop reaches
them and each has to be able to seed the sweep alone. The sweep already handles
all three -- the census seeds from them and `removeRepoFromWorkspaceSession`
clears them -- but nothing pinned it, and dropping that seeding turns all three
cases red.
The `activeWorkspaceKey` case uses the canonical `worktree:<id>` form, so it
also covers unwrapping the workspace key before the repo id is visible.
Refs #17776
The self-clearing check loaded a second store, but that constructor runs the
sweep itself. If the first flush had not persisted the cleanup, the second load
would have redone it in memory and the assertion would have passed without
meaning anything. Read the profile back and assert the map is empty there first.
Refs #17776
Review found three holes in the load-time sweep.
`sleepingAgentSessionsByPaneKey` and `terminalSurfaceTombstonesByPaneKey` are
pruned by the worktreeId they name, not by their own key, but
`pruneWorktreeStateForRepo` only collected owner keys from `worktreeMeta` and
`lastVisitedAtByWorktreeId`. An orphan whose only residue was a sleeping agent
therefore survived the sweep and re-seeded it on the next load, so the store
never self-cleared and every launch scheduled another save. Collect owner keys
from those records too, which fixes `removeProject` for the same shape.
`ownerKeyBelongsToRepo` is restored to its original body. Reordering its two
readings was not behavior-preserving as claimed: for a repo named `folder` or
`worktree`, checking the workspace-key reading first flips the result. The
census now uses `ownerKeyWorktreeIds`, which returns both readings, and seeds
only when neither names a live repo -- seeding one reading of a key whose
other reading is live would hand the removal pass a live row to delete.
Seed from `activeWorktreeId`, `activeWorkspaceKey` and
`activeWorktreeIdsOnShutdown`, which are pruned by bespoke rules and so were
reachable by no owner-key loop, and record why
`terminalTopologyRevisionByRepoId` stays excluded.
Refs #17776
Deregistering a project stranded every row it owned. Each pruning path is
gated on the repo still being in `state.repos`, so once an id leaves the
catalogue its metadata, identity aliases, lineage and session rows became
unreachable forever -- and on a paired client they rendered as phantom
worktrees under an "Unknown" project.
Reconcile against the repo catalogue on load instead: any repo id that owns
rows but is absent from `state.repos` has its rows removed through the same
path `removeProject` uses. Host-independent and session-independent, because
an orphan has no owner that could object -- which is also why this reaches a
client's mirror of a remote host's session partition, something no local
removal can do.
Only a full `<repoId>::<path>` locator seeds the orphan set; bare keys can be
folder workspace ids or repo-keyed revisions, and guessing wrong there would
delete live state. `retiredWorktreeNamesByRepo` is deliberately untouched so a
re-added repo cannot reissue a name onto a cwd that still holds a prior
occupant's agent state.
Test fixtures that wrote worktree rows without registering their repo were
relying on orphans surviving a reload; they now register the repo they name.
Refs #17776
pn dev crashes on macOS in any worktree that adopted the shared Electron dist.
publishSharedElectronDist marks the cache entry read-only, which hardlink
sharing needs, but clonefile preserves mode -- so the dist lands 0555, the dev
runner copies it into out/electron-dev unchanged, and the first plutil -replace
on Info.plist fails with a permission error. The shipped zip has that file at
0644; on disk it is 0555, so the mode is ours, not upstream's.
copyPrivateTree now restores write permission. Its contract is a private tree
the caller goes on to patch, and its one production caller is the dev runner.
The test that should have caught this ran the wrapper with stdio: 'ignore', so a
hard crash presented as a bare 20s timeout. It now captures the wrapper's output
into the failure message, and waits long enough for the two synchronous swiftc
builds and a codesign --deep over ~280MB that precede the assertion.
Acknowledging is one action against two records, but only
clearTerminalPaneUnread cleared unreadAgentCompletionPanes. Acking from
the Activity page, the dashboard drawer or the popout bridge left the tab
dot, the ⌘J row and the floating-workspace dot lit with nothing left to
read; only the terminal-view auto-ack path cleared both.
Cleared inside the existing set so one ack is one commit, and only the
agent marker is touched — clearTerminalPaneUnread also drops
unreadTerminalPanes, which would silence a BEL the user never saw.
Refs #15445 (step 2 of that issue's fix; steps 1 and 3 remain open).
Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
GIT_HISTORY_COMMIT_FORMAT asked for decorations with %(decorate:…), which
Git 2.43 introduced. Older Git prints the placeholder verbatim and exits
zero, so nothing raised and every commit in the Source Control panel
silently lost its branch, remote and tag badges.
The record now also carries %D (Git 2.10) on its own line, selected by an
exact match against the unexpanded placeholder — a ref name can never
contain the \x1f that Git expands inside the echoed text. %n emits the %D
line on both sides of the boundary, so the message index is fixed and a
missed match degrades to no badges rather than a corrupted message.
The decoration separator is now bound to the field that produced the text
instead of sniffed from it. A lone decoration carries no separator, so the
old sniff split `refs/heads/feat,one` into two bogus refs.
Verified against real Git 2.38.1 and 2.49.1.
Co-authored-by: kaluli123123 <295758798+kaluli123123@users.noreply.github.com>
The payload is one concatenated string, so slice boundaries follow document
order rather than responsibility -- but join is associative, so cutting a slice
into consecutive slices is byte-identical by construction. Splits the widest
slice, which carried fit-scale, a DECSET scanner and the write queue together
with no room left under the line cap.
Adds a hash guard. The behavioral tests each execute one region of the payload
in a vm, so an edit to an uncovered region shipped silently; the composed output
is now pinned by sha256 and length.
Derives the source-file list from the composer's own imports instead of a second
hardcoded list a new slice had to be added to by hand -- the same silent
subject-loss shape already found twice elsewhere in this repo.
buildQuickActionContext reads runtimeStatusByEnvironmentId transitively through
getClientCreationActionPolicy, but the split dropped it from the memo deps. The
store replaces the Map identity on update, so the palette held availability from
a snapshot that never refreshed -- offering a browser action against a provider
that had gone away, or hiding one that had come back.
exhaustive-deps could not catch it: the read is behind a void statement, which
the rule does not see.
never grows asserted offenders.length <= ALLOWLIST.length, but the two
membership assertions already force those equal, so it could not fail. The
comment claimed it caught a swap -- one file migrated off child_process, one
added -- which is exactly the case it let through.
Pins the true count and asserts both directions, so a swap fails and a pin left
stale-high after a migration also fails rather than banking ground twice. Gives
the console-visibility ratchet the same test: it had no count assertion at all
and the same gap.
Also anchors the owner-directory exemption with a trailing slash, so a future
src/shared/child-process-foo.ts is scanned rather than silently exempt.
* fix(worktrees): reclaim orphaned pr-* fork remotes
pr-* remotes Orca adds for fork-PR worktrees were only ever pruned by a
single worktree's own removal, and only when that removal had complete
provenance metadata, no branch pinning it, and actually ran through Orca.
Legacy metadata missing remoteCreated, "preserve branch on delete" pinning
the remote via branch.*.remote config after the worktree is gone, and
worktrees removed outside Orca entirely all left the remote behind
forever -- one real user accumulated ~50 leaked remotes this way.
Add a repo-scoped reconciliation sweep that inverts the existing cleanup
predicates over every pr-* remote instead of one removal, reusing
sameGitHubRemoteUrl/hasBranchConfigUsingRemote so no new safety logic is
introduced. It only touches a remote some worktree's persisted pushTarget
explicitly recorded Orca creating (remoteCreated: true) -- naming and URL
shape alone are not proof of provenance. Runs opportunistically alongside
existing single-target cleanup (including RuntimePreservedBranchCleanup's
force-delete path), rate-limited per repo, and fire-and-forget so it never
adds latency to the worktree-removal path a user is waiting on.
Fixes#17828
* test(worktrees): set a local git identity in the pr-remote fixture
CI runners have no global git identity, so `git commit` in the fixture
repos failed with "Author identity unknown" -- only passed locally because
dev machines have one. Set user.name/user.email (plus commit.gpgSign and
core.hooksPath, matching src/main/git/repo-remote-drift-real.test.ts) as
local repo config in both the main and cloned "fork" fixture repos, so the
test is independent of the runner's global config, signing setup, or hooks.
* fix(native-chat): ignore stale restored working status
* fix(native-chat): sample status freshness per epoch
* Revert "fix(native-chat): sample status freshness per epoch"
This reverts commit 8e49b1badd.
* test(native-chat): include hook status timestamps
* test(native-chat): include visibility hook timestamp
---------
Co-authored-by: Merge Sim <sim@local>
The extracted-payload cache key hashed ctime alongside dev/ino/size/mtime.
ctime moves on any inode metadata write -- `chmod +x`, which every AppImage
user is told to run, plus `chown`, an ACL or SELinux relabel, and a backup
restore -- none of which alter a byte of the payload.
Measured on Ubuntu 24.04: `chmod +x` leaves dev, ino, size and mtime
identical and moves ctime alone, so the key changed and the next launch paid
a full ~519 MB re-extraction and a multi-second stall to rebuild a payload it
already had, then pruned the old generation.
Key on content identity instead. An in-place content change moves mtime and
almost always size; a replacement moves the inode. The existing
replace-in-place test still passes.
`retries: 1000` caps the attempt count, not elapsed time, so at up to 1s per
attempt an IPC-driven registration could hang ~16 minutes against a wedged
holder with no feedback.
A legitimate holder is bounded by the extraction timeout, so wait that plus
slack and then fail with a message naming the lock file, rather than hanging.
`maxRetryTime` is forwarded verbatim to the `retry` package by proper-lockfile.
Pruning removed 3215 of 3216 files from a superseded generation and always
stranded resources/app.asar, leaking ~105 MB per version update. Electron's
asar shim reports a *.asar file as a directory, so the recursive remove tried
to rmdir a real file and failed with ENOTEMPTY; the .catch(() => {}) hid it.
Reproduced end to end on Ubuntu 24.04: 519M -> 623M across one update, and
519M again once the payload is actually reclaimed.
removeExtractedAppImagePayload holds process.noAsar for the removal, counted
so overlapping removals cannot hand the shim back early, and the prune site
now warns with the path instead of swallowing the rejection. All three
removal sites use it -- staging cleanup and displaced roots leaked the same
way.
Also reclaim symlinks left by a packaged deb/rpm install, which the
extracted-cache-only rule turned into a hard conflict on a deb -> AppImage
migration, and name the remedy in the conflict error.
The page had two competing splits: an Aug-25 folder split that the Aug-30
oversized-surfaces split stranded, and the 47 flat files that replaced it. The
orphaned tree had no non-test importers, yet eight ratchet files still asserted
against it, so their invariants stopped constraining shipping code -- which is
how six regressions reached main unnoticed. Those ratchets were repointed and
the regressions fixed earlier; this removes the tree they were guarding.
Moves the live files into task-page/{github,gitlab,jira,linear} and drops the
now-redundant prefix, matching the new-workspace sibling.
Makes the source-family walker recursive first: it listed a single flat
directory, so moving the files under it would have emptied the family and turned
every ratchet built on it into a no-op without failing.
The oversized-UI-surfaces split was cut from a stale branch and reverted merged
work. getClientCreationActionPolicy entered Terminal.tsx in #13909 and left in
the split, taking six call sites with it, so every action-time creation gate in
the terminal and floating surfaces was gone. Restores those and the other
behavior the split dropped, each ported from the pre-split reference:
- Cmd/Ctrl+S dispatched a bare Event with no detail, so the only listener always
bailed on detail?.fileId and the chord never saved. Its resolver had been left
orphaned, imported by nothing but its own test.
- Terminal and floating create actions lost their availability gates, their
toasts, and their catch handlers; one path throws on unavailable, so it was a
silent unhandled rejection.
- Both outermost workbench wrappers lost the browser guest paint retention
branch, and the census entry covering them was deleted in the same commit.
- The Space Analyzer header counted omitted items the list no longer rendered,
and a worktree whose items were all omitted showed the empty state.
- The terminal root lost its tab topology projection, so every tab-title update
re-rendered it.
- The titlebar tab bar stopped being passed clientHostedBrowserRows, leaving
client-hosted pages uncloseable before a worktree has a layout.
- Parking diagnostics lost their exempt-route counts and crash breadcrumb.
- A suppressed inherited-terminal frame began buying a freshness scan the
pre-split early return skipped.
Adds regression tests for each, all verified to fail against the pre-fix code.
Restores three deleted assertions whose invariants are still live, and replaces
a concatenated source-boundary fixture with per-module pinning so a symbol is
again asserted against the module that must own it.
Deletes three orphaned trees the splits stranded: a duplicate ResourceUsage
surface, cmd-j-match-relevance, and an agent-session claim-key module whose
logic the record store already owns. Makes two non-recursive test walkers
recursive, one of which silently skipped every nested CLI handler group.
Renames seven -helpers modules for the concept their functions operate on, and
splits three that were genuine grab-bags -- each had a clean cleavage along its
importers, which is the signal AGENTS.md describes for a file holding more than
one responsibility.
Leaves keybindings/definitions-core-1..4 alone: definitions.ts spreads them in
order, so their concatenation order is the command palette order and regrouping
them thematically would be a user-visible change. Records that reasoning in a
comment so it is not re-litigated.
fragment-01..10 were arbitrary line-count slices of one template literal. Two
seams fell mid-expression -- inside buildMouseClickInput and inside the touchmove
listener -- so those pieces had no identity to name. Re-splits at real statement
boundaries and names each for what it holds.
The composed output is byte-identical: sha256 42cc000f..., 729776 bytes, verified
before, after the regroup, and after formatting. Also fixes two ratchet tests that
read fragment paths directly, one of which duplicated the composer's file list.
The split named these -part-N, which says nothing. Renames each for the group of
bridge methods it actually exposes and folds the single-method window-reveal
module into the window-controls module it belongs with.
Verified by walking the composed contextBridge surface before and after: 1060
keys, identical nesting and value types, zero delta. The bridge modules carry no
satisfies annotation, so a dropped key here is a runtime error in the renderer
rather than a typecheck failure.
The split left this logic inline in use-task-page-github-detail.ts at its
pre-STA-5949 shape: a 5s deadline whose expiry overwrote the remembered offset
with the committed 0 -- the permanent-loss bug the extracted module's header
says was removed -- observing only the children rather than the container, and
with no MutationObserver, so late-mounting rows never retriggered a retry. The
list scroll handler also blanket-returned while a restore was pending instead
of classifying echoes, dropping a user's scroll during an unreachable restore.
Promotes the module to a live path and wires the hook and scroll handler to it,
which also gives the ten behavioral cases in the scroll-restore suite something
live to assert against.
Five regressions shipped in the oversized-UI-surfaces split because the ratchet
tests guarding them still pointed at an earlier, orphaned split of the same file:
- GitLab rows lost the target/currentTarget guard, so Enter on the nested
open-in-browser button also opened the task detail.
- GitLab row keys dropped the repo prefix; work-item ids collide across hosts.
- GitHub Enterprise avatars lost their onError fallback in three slots, so an
unauthenticated avatarUrl rendered a broken image (#8784, #13976).
- Linear new-issue popovers lost the viewport-aware scroll container and
reverted to a fixed inner scroller the outer cap clips.
- Jira issue creation lost its catch, so a transport failure told the user
nothing.
Retargets the eight ratchet files at the live modules so these stay guarded.
probeRequiredNativeDeps mapped any thrown error to available:false, which
both triggered the repair and fed resetDeps — so one dropped exec channel
rm -rf'd node_modules/node-pty on a healthy relay and forced a node-gyp
source build. Verdicts are now ok / blocked / unverifiable; only an
answered probe may repair, and only an answered probe may name reset deps.
GitHub reports a release's createdAt as the date of the commit its tag
points at. Every adhoc tag is cut against orca-adhoc's single seed commit
(ff9ca5b6, 2026-08-02T09:46:58Z), so all of them share that one createdAt.
The 30-day cutoff crossed it today: the 06:53 run logged "Nothing to
prune", and the 10:34 run marked the entire channel expired and deleted
40+ releases -- including the one it had published two minutes earlier.
The picker had nothing newer than Aug 13 left to offer.
Age on publishedAt instead, keep any release missing one rather than
guessing, exclude the tag the run just shipped, and prune only after a
live publish. Hourly and daily already moved to publishedAt for the
adjacent sort bug; adhoc was the last one still on createdAt.
Two main-process `resolveGitDir` call sites dropped the WSL distro their caller
already held, so they could only resolve a gitdir pointer whose spelling carries
its own translation: a `//wsl.localhost/<Distro>/...` base, or a `/mnt/<letter>`
drvfs pointer that maps to a drive letter on its own.
The layout that needs the third case is a repo inside the distro's filesystem
with its worktrees on the Windows drive. `git worktree list` reports the worktree
as `/mnt/c/wt/x`, which the listing translates to `C:\wt\x` — a base that no
longer names a distro — while the `.git` gitfile beside it points at
`/home/me/repo/.git/worktrees/x`, which has no drive to derive. Win32 then treats
that pointer as absolute and reads a path that names nothing:
- `detectSparseCheckout` stats `info/sparse-checkout` under the fabricated path,
always misses, and reports the worktree as non-sparse — no sparse badge, and
the file list claims files that are not on disk.
- `readWorktreeDiffStamp` reads HEAD under the same path, gets nothing, and
returns null. Null is the safe answer ("cannot prove unchanged"), but it
retires the settled-diff cache for every file in that worktree, so each diff
respawns Git.
Both callers already have the distro: the listing threads its
`GitWorktreeExecOptions` to `annotateSparseCheckoutStatus`, on through
`detectSparseCheckoutCached` (the annotation cache added by #17859) and its
background revalidation probe, and finally to `detectSparseCheckout`; and
`file-diff` already forwards its `GitRuntimeOptions` to `readWorktreeDiffStamp`,
which now forwards it to `resolveGitDir` as well.
`resolveGitMetadataPath` still prefers a UNC base's distro and still tries drvfs
before the caller-named distro, so nothing that resolved before resolves
differently.
The cache hop matters twice over. It is the only remaining caller of
`detectSparseCheckout`, so without threading it the fix would not reach the
probe at all. And the cache is where the bug turns sticky. #17859 keyed entries
on `repoPath` + `worktreePath` alone, on the reasoning that the distro is a
property of the repo and so every read for a given `repoPath` carries the same
one. That invariant does not hold. `listRepoWorktrees(repo)` is called with no
options at all from the filesystem-auth root rebuild
(`registered-worktree-roots-cache.ts`, reached from `ensureAuthorizedRootsCache`
on any auth check with a dirty cache) and from the local worktree-ownership
check in `filesystem-worktree-helpers.ts`. Both land on the *same* key as the
distro-carrying listing, because `translateWslOutputPaths` derives the distro
from the cwd spelling before falling back to `options.wslDistro`, so a
UNC-spelled repo path yields the identical `C:\...` worktree row either way.
Measured on Windows in one process, branch build: a distro-less read followed by
a distro-carrying read reported the sparse worktree as non-sparse both times.
So `wslDistro` now joins the cache key -- trimmed and lowercased, matching how
the rest of the codebase compares distro names, and appended last so the
repo-scoped prefix delete still matches every variant. The per-path invalidate
becomes a prefix delete for the same reason, dropping every distro variant of a
removed or moved worktree.
Keying on it closes both halves of the defect. A correct caller can no longer be
served an answer derived without the distro it supplied. And because the entry a
reader reaches is now selected by the same distro it would re-probe with,
`revalidateInBackground` can no longer re-derive a warm entry under weaker
options -- which mattered on its own: a distro-less reader crossing the
five-minute window would otherwise flip a correct `true` to `false`, and the
resulting change notification runs the registered invalidator, clearing the
whole repo's cache and re-probing every worktree cold, on a five-minute loop.
Cost of the extra key dimension is bounded by the number of distinct distros a
given repo is actually read under: one where a distro is threaded everywhere,
two while the distro-less callers above still exist. Entries are still
repo-scoped, and both clears already sweep by prefix.
Per-platform delta:
- macOS/Linux: no change. Guest-pointer translation is gated to win32 and a
caller-named distro is ignored off Windows; no caller supplies one there, so
the cache keys and probes exactly as before.
- native Windows, no WSL: no change. `wslDistro` is undefined, so the resolver
takes exactly the branches it took before and every read keys on the same
empty distro component, so the cache behaves exactly as it did.
- Windows + WSL, UNC-spelled worktree: no change. The base already names the
distro and outranks the caller's.
- Windows + WSL, drvfs-spelled worktree with a drvfs pointer: no change. The
drive-letter derivation still runs first.
- Windows + WSL, drvfs-spelled worktree with a non-drvfs pointer: the sparse
badge appears and the settled-diff cache starts hitting. Both previously
failed toward "not sparse" / "do not cache", so neither can now serve a stale
answer, and the distro-less listings no longer share the badge's cache entry.
- SSH/relay: none. Those paths return through the provider branch before
reaching either function.
- folder workspaces, GitLab: none. Neither is on these code paths.
Not in this change:
- `readRepoCommonDirFromDisk` (worktree-listing). Passing the distro there is
inert: a repo root's `.git` is a directory, so the gitfile-pointer branch never
runs, and when `repoPath` itself is guest-spelled the preceding `stat` already
fails — which no `resolveGitDir` option can fix.
- The two `findExistingWorktreeSymlinkPaths` calls on the removal paths. Both
receive `registeredWorktree.path` from `listWorktreesStrict`, which already
translates every row out of the guest namespace, so the distro would be a
no-op. The `removeWorktreeLinkedPaths` unlink beside them is untranslated too,
so a half-threaded fix would only move the refusal from Orca's preflight to
`git worktree remove`.
- An absolute `commondir` payload, which `resolveGitCommonDir` still resolves
untranslated. Git writes that file relative in the layouts above, and the
failure direction is unchanged.
- Giving the two distro-less `listRepoWorktrees(repo)` callers a distro. Neither
reads `isSparse` -- both use only `worktree.path` -- so the distro would buy
them nothing they consume, while resolving a project runtime inside the
filesystem-auth rebuild would put a call that throws on `repair-required`
behind a catch that skips the whole repo's authorized roots. The cache key
makes their reads harmless; skipping the annotation for callers that never
read it is a separate, larger change. The third no-options call in
`hosted-review.ts` is inside the `repo.connectionId` branch and returns
through the SSH provider, so it never reaches this cache.
Agent Session History exceeded its 130-second deadline on large local Codex
histories. Three costs combined: excluded worker transcripts were recognized on
their first line but still drained to EOF (1,011 files / ~18.3 GiB on the
reported corpus), large ignored records were fully decoded and JSON.parsed, and
the persisted parse cache was discarded on every app update.
- Stop resumable reads the moment `session_meta` marks a worker transcript.
- Skip decode + `JSON.parse` for records the parser only feeds to the timeline.
The skip set is the complement of what `consumeCodexRecordLine` reads, and
applies only above the bounded prefix limit, so a long opening prompt (which
is the session title) still takes the exact parser.
- Prove cross-volume rollout aliases from a bounded `session_meta` read routed
through the WSL transcript FS gate, carrying the scan's AbortSignal, fanned
out across contested candidates with bounded concurrency.
- Make parse-cache schema 2 the semantic compatibility boundary so an update no
longer forces a multi-gigabyte cold scan, fenced by a build-time ratchet on
the persisted session shape.
- Report early-stopped transcripts as their own `aiVault.scan` attribute.
Verified on macOS, Ubuntu over SSH, and Windows: read volume drops 336 -> 49.5
MiB identically on all three; the Windows failing-test set is byte-identical to
main. Reported corpus: 130s timeout -> 58.6s, 244 sessions, 0 issues.
Fixes#17888.
* fix(git): narrow fork-remote fetch refspecs to tracked branches
git remote add with no -t writes the wide +refs/heads/*:refs/remotes/<name>/*
refspec, so any later plain `git fetch` (user, agent, or Orca's own Fetch
action) re-imports a fork's entire branch set and its tags -- one real
machine had ~50 leaked/wide fork remotes producing 59,716 remote-tracking
refs. Mint and reuse now pin -t <branch> --no-tags; a rate-limited sweep
narrows and cleans up remotes minted before this fix; gitFetch self-heals
when a narrowed remote's tracked branch is later deleted upstream.
Refs #17828
* fix(git): soften narrow fork-remote refspec against deleted upstream branches
A bare `git fetch` in a worktree checked out on a fork-PR branch resolves to
the pr-* remote via branch.<name>.remote -- not origin -- making it the
dominant fetch shape in Orca's terminal-centric, agent-driven usage. The
previous literal-refspec design hard-failed that fetch ("couldn't find
remote ref") the moment the tracked branch was deleted/renamed upstream,
which is not the narrow edge case it was first described as.
Switch to a trailing-`*`-suffixed refspec source/destination
(refs/heads/<branch>*:refs/remotes/<name>/<branch>*). Verified against real
git: this restores wildcard zero-match tolerance (silent no-op instead of a
hard failure) and lets plain `git fetch --prune` reclaim the stale ref once
the branch disappears, at the cost of also matching sibling branches that
share the literal name as a prefix -- a materially smaller widening than the
original unbounded-import bug.
Also close a race with #17842's orphaned-pr-remote reconciliation sweep:
both sweeps read the same worktree-metadata store to pick candidate remotes,
so reconciliation can `remote remove` a remote this migration is
concurrently narrowing. `ensureRemoteTracksBranchNarrowly`'s plain `config
--add` would silently resurrect a url-less config section in that case;
re-check `remote.<name>.url` (via the new `remoteHasUrl`, plumbing rather
than porcelain `remote get-url`, which falls back to echoing the remote name
as a bogus URL) after the narrowing writes and remove the section if it's
gone.
* fix(git): update stale fork-remote mint assertions for -t/--no-tags and wildcard-suffix refspec
Four test files still asserted the pre-#17828 remote-add shape or the
literal (non-wildcard-suffixed) fetch refspec from before the
deleted-upstream-branch softening commit, so CI went red on that HEAD:
- worktree-push-target-refspec-real-git.test.ts: the migration fixture
asserted a hardcoded tracked-ref count before narrowing. Under git
>= 2.44, `followRemoteHEAD` auto-creates a `refs/remotes/<name>/HEAD`
symref on the first fetch matching the full wildcard refspec, adding
one untracked ref. Made the count/assertions robust to that ref's
presence instead of hand-tuning the constant per git version.
- worktrees-wsl-runtime-routing.test.ts: assertions predated both the
`-t <branch> --no-tags` mint change and the wildcard-suffix refspec
change; updated to the full, correct call sequence and confirmed the
WSL routing options (cwd, wslDistro) are threaded to every call.
- worktrees-create-metadata-persistence.test.ts and
orca-runtime-tests/worktree-removal-and-reconciliation.spec.ts: same
class of staleness, found via CI job log cross-referencing rather
than being explicitly flagged.
Verified out of scope: the SSH fork-remote mint path
(prepareWorktreePushTargetSsh) is untouched by this PR -- it never
persists a `remote.<name>.fetch` refspec at all, using
provider.fetchRemoteTrackingRef for a targeted per-branch fetch
instead -- so worktrees-ssh-fork-push-target-remote.test.ts needed no
change.
* fix(git): migrate pr-* remotes with zero worktree-metadata trace too
The migration sweep's candidate discovery was purely metadata-driven
(store.getAllWorktreeMeta()), so a pr-* remote whose every referencing
worktree was removed outside preserve-on-delete (metadata purged, not
just the worktree) was permanently invisible to it and stayed on the
wide default forever.
Field data from a manual migration run against a real user's repo (31
pr-* remotes, 34,637 tracking refs, only 18 actually needed) found
exactly this: 15 of 31 remotes had no branch pinning them at all.
Widen discovery to every pr-* remote git reports on disk, in addition
to metadata-derived candidates. For a remote with no branch provenance
from either metadata or surviving branch.*.remote/.pushRemote config,
there's nothing to narrow *to* -- clear its fetch refspec entirely
instead (stays pushable, imports nothing on a plain fetch), gated on
it still carrying the untouched stock wide default so a user's own
custom pr-*-named remote isn't touched. Removing the remote outright
stays #17842's job.
Adds clearForkRemoteFetchRefspec (fork-remote-refspec.ts), 3 new
mocked-exec tests, and a real-git integration test proving a
subsequent plain `git fetch` on the cleared remote imports nothing.
* perf(git): cache sparse-checkout annotation on worktree listing
`git worktree list` never reports sparse-checkout state, so every listing paid a
per-worktree fs.stat + config read to detect it -- measured at ~9x the cost of
the `git worktree list` call it decorates on a 1000-worktree repo. Cache the
result per worktree path, invalidated by the existing worktree-change
invalidator registry plus explicit remove/move hooks, with a 5-minute
reconcile window bounding the one unwitnessed edge case (external
`git sparse-checkout` toggle with extensions.worktreeConfig off), matching the
precedent already accepted in readRepoWorktreeAdminFingerprint.
* perf(git): normalize/scope sparse-checkout cache keys, add SWR
Address independent-review follow-ups on the sparse-checkout annotation
cache (#17859):
- Extract canonicalWorktreePath() from areWorktreePathsEqual and key/invalidate
the cache through it on both read and write, closing the disclosed
path-spelling P2 outright instead of leaving it as a residual risk.
- Scope cache entries and clears by repo path (derived from the invalidator
registry's repoId via a store lookup, falling back to a full clear when the
repo can't be resolved), so churn in one repo no longer evicts a sibling
repo's warm cache.
- Replace the hard 5-minute cutoff with stale-while-revalidate: past the
window, callers get the cached value immediately while a deduplicated
background probe corrects it and, on a flip, drives the existing
worktrees-changed notification -- collapsing visible staleness from the
full window to one refresh cycle at zero added listing latency.
Also corrects a stale claim in the original PR description: newer Git does
emit a `sparse` porcelain line (which annotateSparseCheckoutStatus already
skips), but Orca's Git 2.25 compatibility baseline predates it, so the
fallback detection this caches remains necessary.
* fix(git): stop background sparse-checkout revalidation resurrecting invalidated entries
Readiness-loop finding: a stale-while-revalidate probe in flight when a
worktree is removed/moved (or a repo's cache is cleared) would still write
its result back afterward, resurrecting an entry that was deliberately
dropped. Guard the write with a presence check so an invalidated key stays
absent until the next real read.
* fix(git): identity-check the sparse-checkout SWR write-back guard
The has()/presence guard from the previous commit only proved some
entry existed at the key, not that it was the one this revalidation
started from. A worktree removed and re-created at the same path while
a background re-detect was in flight would repopulate the key with a
fresh cold read, and the stale in-flight result would then overwrite
it -- exactly the race greptile (P1) and pullfrog both flagged as
still open. Compare the map's current entry by reference to the entry
captured when the revalidation began; a mismatch means something else
(invalidate, clear, or a fresh cold read) replaced it, and the stale
result must not be written back.
Added a regression test that fails against the old has() guard and
passes with the identity check: invalidate and repopulate the key with
a different value mid-flight, then let the stale revalidation settle
and assert the fresh value survives.
Git running inside a WSL distro writes `.git` gitdir pointers, and answers
`status --porcelain`, in the guest namespace. Node reads both back in the
Windows main process, where `/mnt/c/repo/.git` resolves to `C:\mnt\c\repo\.git`
and `/home/me/wt` names nothing at all. Four fs probes were built on those
fabricated paths and always came back "absent":
- `detectConflictOperation`'s four marker probes, so merge/rebase/cherry-pick
badges silently went missing.
- `parseUnmergedEntry`'s compat existence check, so every `deleted_by_us` /
`added_by_them` conflict rendered as 'deleted' regardless of the working tree.
- `findExistingWorktreeSymlinkPaths`' `lstat` from status, so Orca's own shared
symlinks (node_modules and friends) showed as user changes.
- the same `lstat` from the hosted-review dirty preflight, which fails closed:
an unreadable shared symlink read as uncommitted work and blocked PR/MR
creation outright.
`resolveGitDir` computes the host spelling of the worktree once and uses it for
both the gitfile read and the pointer resolve, so a guest-spelled worktree path
is reached at all, and a relative pointer (`worktree.useRelativePaths`, git
2.48+) resolves against a spelling Win32 understands. The pointer itself now
goes through the already-landed `resolveGitMetadataPath`, and the function gains
an optional `{ wslDistro }` for a caller whose base path does not encode a
distro. `detectConflictOperation` forwards it, and the three callers that reach
it -- status-read, the runtime RPC, the `git:conflictOperation` IPC -- pass the
git options they already hold. The return type stays `Promise<string>`.
`resolveWorktreeHostPath` is the same rule applied to a worktree path, used by
status-read for the two working-tree probes and by the review preflight. Both it
and `resolveGitMetadataPath` now treat only a single-leading-slash path as guest
namespace: `//wsl.localhost/...` is already a host UNC spelling, and translating
it prepended a second share prefix.
`readWorktreeDiffStamp` needed the same one-namespace guarantee, since moving
translation inside `resolveGitDir` would otherwise make its HEAD and index real
while the working-tree stat stayed fabricated, letting a settled diff survive
every edit. #17896 landed that change first, so it is no longer in this diff;
its version is a superset and all four components already resolve from one
`hostWorktreePath`. What remains here is the `resolveGitDir` gitfile-pointer
fix that #17896 explicitly deferred, which `worktree-diff-stamp-host-paths.test.ts`
pins.
`getConflictCompatibilityStatus` moves from `existsSync` to async `access`, for
the same reason `detectConflictOperation` did: once these paths are real they
are `\\wsl.localhost\...` shares, and a sync probe per asymmetric conflict
blocks the Electron main thread for a 9p round trip on every status poll.
Per-platform delta:
- native Windows, no WSL: no behavioral change. Nothing here starts with a
single `/`, so no path is translated. An absolute pointer is now returned
verbatim rather than separator-normalized; every consumer re-joins or
normalizes it before use.
- macOS/Linux: no change. Guest-pointer translation is gated to win32, and a
caller-named distro is ignored off Windows.
- Windows + WSL: drvfs pointers and drvfs-spelled worktrees now resolve to their
drive spelling instead of `C:\mnt\...`; a non-drvfs guest path resolves
through the named distro's UNC share, or stays verbatim (ENOENT -> existing
fail-safe) when none is named.
- SSH/relay: none. Those paths return before any of this via the provider
branch; `src/relay/git-handler-status-ops.ts` keeps its own resolveGitDir.
- folder workspaces, GitLab: none. Neither is on these code paths.
Doubles the maximum UTF-8 bytes accepted for manually shared artifacts,
enabling users to share larger content while maintaining recovery and
transport constraints.
Terminal sessions now report startup command delivery details (whether written, presence, length, and delivery method) without logging the command text—preventing credential leakage and distinguishing missing commands from lost ones in diagnostics.
Setup scripts now announce completion on both POSIX and Windows before executing the startup command, so healthy setups don't appear stuck in the UI with "Waiting for setup..." as the last visible line.
Diagnostics failures are caught and ignored so they never break session creation.
* Move workspace search toggle to floating button
Extract the search bar into a separate component and move the search toggle button from the toolbar to a bottom-left floating action button, positioned above the new workspace FAB. This consolidates phone-only floating actions in one location.
* Remove SearchWorkspacesFab component
Consolidates search functionality into bottom-left floating action button as part of mobile search button repositioning.
* perf(worktree): make head-identity refresh incremental
Head-identity refresh re-read `gitdir` + `HEAD` + a loose ref for every
linked worktree on every watcher burst. On a 973-worktree checkout that is
~2,800 metadata reads (~1.0s of main-process fs I/O) per event, and the
debounced pipeline fires on every commit in any worktree — so fleet-wide
agent activity degenerated into a continuous scan loop.
Watcher events already name the admin dir that changed. Classify each event
into a head-identity scope, memoize per-entry identities, and re-read only
the scoped entries. Refs resolved during a pass are replayed onto cached
entries that share the same branch, so `git worktree add --force` siblings
stay current without extra reads.
Invalidation stays conservative: an absent scope (watcher failure, event
overflow, cold start) means a full re-read, `packed-refs` writes invalidate
every entry, misses are never memoized, and one refresh per minute is
promoted back to a full re-read to bound the window where a ref moves with
no event under any admin dir.
Measured on the reported 989-entry checkout (macOS/APFS): one-worktree
commit 2,816 -> 2 file reads, 61ms -> 0.5ms p50 with an identical page
cache; a 20-worktree debounce burst costs 57 reads / 9.7ms; an external
`git worktree add`/`remove` costs one readdir / 1.0ms.
Refs #17828
* fix(worktree): harden incremental head-identity invalidation
Two holes found in self-review:
- An admin entry name removed and immediately reused inside one debounce
window coalesced into a listing-only scope, so the reused entry kept
serving the removed worktree's cached head. Name the entry alongside the
listing on every `worktrees/<name>` create/delete.
- A non-ENOENT `readdir` failure on `worktrees/` collapsed the memo to the
primary row, which then re-emitted every identity on recovery. Mirror
worktree-git-common-polling: only a genuinely absent dir means empty; any
other error keeps the previous listing.
* fix(worktree): let empty-scope bursts still take the head re-baseline
Adversarial review found the 60s full-rebaseline promotion was unreachable
whenever the triggering burst had an empty head-identity scope: the skip
guarded on the raw caller scope and returned before `resolveScope` ran, so
`lastFullReadAtMs` was never re-evaluated. A repo whose only churn is
`git worktree lock`/`unlock` or a sparse toggle — Orca's own prepared-checkout
flow locks and unlocks on every create — could starve the promotion forever
and hold a stale head indefinitely. Resolve the scope first and skip on the
effective scope.
Also stop deferring an add/remove that arrived while the `worktrees/` listing
was transiently unreadable: forget the memoized listing so the next refresh
re-enumerates whatever its scope, instead of waiting for another listing event.
Both fixes carry a test verified to fail without them.
* fix(worktree): return head-read completeness instead of sniffing the memo
Adversarial review round two. Six fixes, each with a test verified to fail
without it.
- `readGitCommonHeadIdentities` now returns `{ identities, listingComplete }`.
The refresh layer was inferring "enumeration failed" from `cache.entryNames
=== null`, a reader-owned field whose null also means "cold start" — fragile
in production and impossible to express in a mock.
- A read discarded by teardown, or one that could not enumerate `worktrees/`,
no longer arms the 60s freshness clock.
- A queued refresh whose re-run met a destroyed window (macOS recreates the
window while the watch lives on) was cleared and dropped. It now stays armed
and is folded into the next request.
- An incomplete listing carries forward the baseline rows it could not observe,
so recovery does not report every linked worktree as changed.
- The baseline advances after notifying, so a send into destroyed chrome leaves
the move to be retried instead of diffing it away.
- A scope naming an entry the memoized listing does not know now forces a
re-enumeration instead of resolving to zero work — this removes an unstated
dependency on `diffGitCommon` emitting a dir-level create for new entries.
- Overflow states FULL at its construction site rather than relying on a
downstream `?? FULL` for an absent field.
Also documents the load-bearing invariant behind the empty-scope skip (an empty
scope only reaches the refresh from a structural burst, which forces
`emit: false` and is always paired with a catalog notification for every repo
on the watch), and strengthens two tests that could not distinguish the
behaviour they claimed.
* fix(worktree): bound head-identity staleness with a one-shot catch-up
The previous re-baseline was opportunistic: it rode the next refresh, so a ref
that moves with no watched write (`git update-ref refs/heads/x` from a sibling
worktree) stayed stale until an event happened to arrive after the interval.
Pre-PR the very next event anywhere in the repo corrected it, so this was a
real narrowing of correctness, not just a pre-existing gap.
Arm a one-shot, unref'd timer when a SCOPED pass completes, firing one full
re-baseline an interval after the last full read, then disarming. A full pass
disarms instead of arming, so it never becomes a background poll, and the timer
only exists after an event — an idle repo still schedules nothing and reads
nothing. Cost is O(1) timer per active repo and at most one full read per
interval: the same operation the old code ran per event, 60x rarer.
This also converts "stale until some later event" into "stale at most one
interval, period", which is what bounds the blast radius of any invalidation
bug in the scoping itself.
Cleared on watch disposal. Three tests, each verified to fail without its fix:
the catch-up runs with no further events; a quiet repo issues no background
reads and the timer disarms after firing; disposal stops it.
* fix(worktree): treat an unreadable head as unknown, not absent
Reported independently by two PR reviewers. `readTrimmedFile` collapsed every
errno to `null`, so an EIO/EACCES/ENFILE on a `gitdir`, `HEAD`, loose ref, or
`packed-refs` read was indistinguishable from the file being absent — and the
caller deletes the cached identity on `null`. Same conflation AGENTS.md forbids
for the SSH verdict vocabulary: loss of contact is not evidence of absence.
Reads now report three outcomes, and an unknown:
- keeps the entry's last verified identity instead of evicting it,
- is never replayed onto siblings sharing the branch as "this ref is gone",
- marks the entry unverified so the very next pass re-reads it whatever its
scope, and
- reports the pass incomplete, so it cannot arm the freshness clock.
The reviewers' stated consequence — that an evicted entry stays evicted until
the next full pass — did not hold, because `!cache.entries.has(name)` already
forced a re-read. The real cost was that one EMFILE evicted every entry it
touched and the next pass re-read all of them, which is exactly the full scan
this PR exists to remove, plus a spurious re-publish of every row.
Renames `listingComplete` to `complete`: it now covers entry reads too.