fix(runtime): retire the resolved-worktree snapshot when the repo catalog changes

listResolvedWorktrees is served from a 1s-TTL snapshot derived from
store.getRepos(), but nothing retired it when the repo set changed. Adding an
SSH repo through repos:addRemote therefore left it invisible for the rest of
the TTL.

ssh-skill-installation lost that race deterministically: notifySshRelayReady
fires while ssh.connect is still running and warms the snapshot through
refreshRestoredOrchestrationAuthority, ~60ms before the helper registers the
remote repo. The worktree-scoped install lands ~500ms later, inside the window,
so resolveSkillSshTarget matched zero worktrees and threw
skill-install-workspace-not-found.

Invalidate in notifyReposChangedForRemoteClients, the runtime funnel every repo
IPC mutation already reaches, alongside the sibling worktree notifier that
invalidates the same cache. Runtime-side repo mutations already invalidate at
their own call sites.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-08 23:34:18 -04:00
parent 98df62d8ff
commit 236dccbf87
2 changed files with 38 additions and 0 deletions
@@ -136,6 +136,9 @@ export class OrcaRuntimeWithNotifySshStateChanged extends OrcaRuntimeWithGetStat
// clients need an explicit catalog invalidation; the local renderer already
// got its own repos:changed and must not be re-notified (#11994).
notifyReposChangedForRemoteClients(): void {
// Why here: the resolved-worktree snapshot is derived from the repo list, so a repo
// registered while a snapshot is warm stays invisible for the whole TTL.
this.invalidateResolvedWorktreeCache()
this.emitClientEvent({ type: 'reposChanged' })
}
@@ -289,6 +289,41 @@ describe('OrcaRuntimeService', () => {
expect(reposChanged).not.toHaveBeenCalled()
})
// A repo added while a resolved snapshot was warm stayed invisible for the whole TTL, so a
// worktree-scoped call landing in that window resolved nothing for the freshly added host.
it('notifyReposChangedForRemoteClients retires the warm resolved-worktree snapshot', async () => {
vi.mocked(listWorktrees).mockClear()
vi.mocked(listWorktrees).mockResolvedValue([makeWorktreeInfo(TEST_WORKTREE_PATH)])
const localRepo = store.getRepo(TEST_REPO_ID)!
const lateRepo = {
...localRepo,
id: 'repo-registered-late',
path: '/remote/repo',
connectionId: 'ssh-late'
}
const provider = {
listWorktrees: vi.fn().mockResolvedValue([makeWorktreeInfo('/remote/worktree')])
}
registerSshGitProvider('ssh-late', provider as never)
let repos = [localRepo]
const runtime = new OrcaRuntimeService({ ...store, getRepos: () => repos } as never)
try {
const warm = await runtime.listManagedWorktrees()
expect(warm.worktrees.map((worktree) => worktree.path)).not.toContain('/remote/worktree')
repos = [localRepo, lateRepo]
runtime.notifyReposChangedForRemoteClients()
const afterRegistration = await runtime.listManagedWorktrees()
expect(afterRegistration.worktrees.map((worktree) => worktree.path)).toContain(
'/remote/worktree'
)
} finally {
unregisterSshGitProvider('ssh-late')
}
})
it('persists changed worktree order once and emits targeted invalidations', () => {
const firstId = `${TEST_REPO_ID}::/tmp/first`
const secondId = `${TEST_REPO_ID}::/tmp/second`