From 236dccbf87fe45d4f6bb3ac2d1fa15f67a6ac26b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 8 Sep 2026 23:34:18 -0400 Subject: [PATCH] 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 --- .../orca-runtime-notify-ssh-state-changed.ts | 3 ++ .../lineage-and-scan-cache-part-03.spec.ts | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/main/runtime/orca-runtime-notify-ssh-state-changed.ts b/src/main/runtime/orca-runtime-notify-ssh-state-changed.ts index 12c4c275928..be824ea7adb 100644 --- a/src/main/runtime/orca-runtime-notify-ssh-state-changed.ts +++ b/src/main/runtime/orca-runtime-notify-ssh-state-changed.ts @@ -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' }) } diff --git a/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-03.spec.ts b/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-03.spec.ts index 92ad36b3541..d8ad5b0647b 100644 --- a/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-03.spec.ts +++ b/src/main/runtime/orca-runtime-tests/lineage-and-scan-cache-part-03.spec.ts @@ -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`