Commit Graph
1 Commits
Author SHA1 Message Date
Neil 9cda5a9dc0 fix(worktrees): stop a resolved-worktree snapshot answering for repos it never saw (#18295)
* fix(worktrees): stop a resolved-worktree snapshot answering for repos it never saw

`listResolvedWorktrees` caches one fleet-wide snapshot for
RESOLVED_WORKTREE_CACHE_TTL_MS (1s) and reuses it on time alone. Nothing
invalidates it when a repo is registered, so for up to a second after a repo
row lands, every caller reads a snapshot computed before that repo existed --
and reads the gap as a verdict.

The visible failure is the SSH skill install. `resolveSkillSshTarget` resolves
a workspace-scope destination through that snapshot, so installing into a
worktree on a host connected moments earlier threw
`skill-install-workspace-not-found`: the client asserting a remote workspace is
absent on the strength of client-side bookkeeping that had never looked at the
host. That is the shape `docs/reference/ssh-execution-boundary.md` rules out --
absence from a client-side set is not evidence about the execution host. It
made `tests/e2e/ssh-skill-installation.spec.ts:108` fail 3 runs in 4 locally
and deterministically in the Docker SSH lane, where connect-then-install lands
inside the one-second window every time.

The snapshot now carries the repo-registration revision it was computed under
and is only reused while that revision still holds. The counter is the one
`bumpLocalWorktreeScanGeneration` already advances on every repo add, removal
and update, so the check is O(1) and cannot drift from the mutation sites.

* fix(worktrees): key the snapshot on repo mutations only, not on generation reads

Two things the headless-reattach lane surfaced.

The revision I keyed the snapshot on was `generationSequence`, which
`getLocalWorktreeScanGeneration` also advances when it mints a key for a repo
id nothing has scanned yet. That is a read, not a mutation, so a read path
could discard a snapshot that was still perfectly valid -- the mirror image of
the staleness this fixes, and a way to make a lookup fail that would otherwise
have succeeded. The counter now advances only where the scan generation is
actually bumped: repo add, removal, update, and scan-cache invalidation.

Separately, `pty-restore-record-seeding.test.ts` primed the cache by writing
its private `resolved` field with a literal spelling out `worktrees`,
`platformByRepoId` and `expiresAt`. That literal is a second copy of the
cache's freshness contract, so adding a field to the real entry left the fake
one failing the check: the primed snapshot was rejected, resolution fell
through to a real scan, and the headless fixture -- which has no git -- got
`selector_not_found`. It now primes through `getSnapshot` so the cache stamps
its own entry and the two cannot drift again.

The revision never moved during that test (0 before and after), so nothing was
being invalidated; the fake entry simply never satisfied the contract.
2026-09-02 18:13:08 -07:00