From f37258a21d10aeeaed3f8bee8184373112e89601 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 04:46:49 -0700 Subject: [PATCH] perf: bound branch rename dedupe cache (#4115) --- .../first-work-branch-rename.test.ts | 36 +++++++++++++++++++ .../agent-hooks/first-work-branch-rename.ts | 17 ++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/agent-hooks/first-work-branch-rename.test.ts b/src/main/agent-hooks/first-work-branch-rename.test.ts index 80d5c493833..a9255541af4 100644 --- a/src/main/agent-hooks/first-work-branch-rename.test.ts +++ b/src/main/agent-hooks/first-work-branch-rename.test.ts @@ -38,6 +38,7 @@ vi.mock('../text-generation/commit-message-agent-environment', () => ({ vi.mock('../ipc/worktree-logic', () => ({ computeBranchName: computeBranchNameMock })) import { + FIRST_WORK_BRANCH_RENAME_SETTLED_CACHE_LIMIT, maybeAutoRenameBranchOnFirstWork, resetFirstWorkBranchRenameState, type FirstWorkBranchRenameDeps, @@ -203,6 +204,41 @@ describe('maybeAutoRenameBranchOnFirstWork', () => { expect(generateBranchNameMock).not.toHaveBeenCalled() }) + it('bounds settled worktree dedupe while keeping recent entries', async () => { + gitExecFileAsyncMock.mockImplementation( + gitResponder({ currentBranch: 'you/my-feature', hasUpstream: false }) + ) + const { deps } = makeDeps() + const firstWorktreeId = `${REPO_ID}${WORKTREE_ID_SEPARATOR}/repo/wt-0` + const lastWorktreeId = `${REPO_ID}${WORKTREE_ID_SEPARATOR}/repo/wt-${FIRST_WORK_BRANCH_RENAME_SETTLED_CACHE_LIMIT}` + + for (let index = 0; index <= FIRST_WORK_BRANCH_RENAME_SETTLED_CACHE_LIMIT; index += 1) { + await maybeAutoRenameBranchOnFirstWork( + workingEvent({ + tabId: undefined, + paneKey: '', + worktreeId: `${REPO_ID}${WORKTREE_ID_SEPARATOR}/repo/wt-${index}` + }), + deps + ) + } + expect(gitExecFileAsyncMock).toHaveBeenCalledTimes( + FIRST_WORK_BRANCH_RENAME_SETTLED_CACHE_LIMIT + 1 + ) + + gitExecFileAsyncMock.mockClear() + await maybeAutoRenameBranchOnFirstWork( + workingEvent({ tabId: undefined, paneKey: '', worktreeId: firstWorktreeId }), + deps + ) + await maybeAutoRenameBranchOnFirstWork( + workingEvent({ tabId: undefined, paneKey: '', worktreeId: lastWorktreeId }), + deps + ) + + expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(1) + }) + it('retries on a later event after a transient failure (does not poison the worktree)', async () => { generateBranchNameMock.mockResolvedValueOnce({ success: false, error: 'agent not ready' }) const { deps, onRenamed } = makeDeps() diff --git a/src/main/agent-hooks/first-work-branch-rename.ts b/src/main/agent-hooks/first-work-branch-rename.ts index 20a73d753ec..59bd8f04403 100644 --- a/src/main/agent-hooks/first-work-branch-rename.ts +++ b/src/main/agent-hooks/first-work-branch-rename.ts @@ -68,6 +68,7 @@ export type FirstWorkBranchRenameDeps = { // settled, so the real first prompt can still succeed on a later event. const inFlightWorktreeIds = new Set() const settledWorktreeIds = new Set() +export const FIRST_WORK_BRANCH_RENAME_SETTLED_CACHE_LIMIT = 500 /** Test seam: clear the per-process dedupe sets. */ export function resetFirstWorkBranchRenameState(): void { @@ -75,6 +76,20 @@ export function resetFirstWorkBranchRenameState(): void { settledWorktreeIds.clear() } +function rememberSettledWorktreeId(worktreeId: string): void { + // Why: the app can see unbounded worktree ids over a long session; evicting + // oldest entries trades a rare re-probe for bounded process memory. + settledWorktreeIds.delete(worktreeId) + settledWorktreeIds.add(worktreeId) + while (settledWorktreeIds.size > FIRST_WORK_BRANCH_RENAME_SETTLED_CACHE_LIMIT) { + const oldest = settledWorktreeIds.values().next().value + if (typeof oldest !== 'string') { + return + } + settledWorktreeIds.delete(oldest) + } +} + export async function maybeAutoRenameBranchOnFirstWork( event: FirstWorkBranchRenameEvent, deps: FirstWorkBranchRenameDeps @@ -107,7 +122,7 @@ export async function maybeAutoRenameBranchOnFirstWork( // ineligible); false means a transient bail that should retry later. const settled = await runAutoRename(worktreeId, prompt, event.assistantMessage, deps) if (settled) { - settledWorktreeIds.add(worktreeId) + rememberSettledWorktreeId(worktreeId) } } catch (error) { // Why: best-effort, opt-in convenience. A failure must never disrupt the