diff --git a/.changeset/dependent-worktree-base-refresh.md b/.changeset/dependent-worktree-base-refresh.md new file mode 100644 index 0000000000..60cd2edae5 --- /dev/null +++ b/.changeset/dependent-worktree-base-refresh.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Refresh reacquired execution worktrees against the current local integration branch. +category: fix +dev: Apply the existing stale-base reconciliation to native fresh and pooled acquisitions so a retained task branch cannot omit a dependency that landed while its original base branch disappeared. diff --git a/packages/engine/src/__tests__/worktree-acquisition-secrets-env.test.ts b/packages/engine/src/__tests__/worktree-acquisition-secrets-env.test.ts index 12675fd785..80a1aa0551 100644 --- a/packages/engine/src/__tests__/worktree-acquisition-secrets-env.test.ts +++ b/packages/engine/src/__tests__/worktree-acquisition-secrets-env.test.ts @@ -141,11 +141,12 @@ describe("worktree-acquisition secrets env hook", () => { expect(refreshReusedWorktreeBase).not.toHaveBeenCalled(); expect(git).toHaveBeenCalledWith(expect.objectContaining({ type: "worktree:base-refresh-blocked", - target: "FN-1", + target: existingWorktree, metadata: { taskId: "FN-1", outcome: "base-reconciliation-required", reconciliationOutcome: "git-dir-unavailable" }, })); + // FNXC:SecretsEnvMaterialization 2026-08-09-03:50: Audit targets identify the affected checkout; only + // resolver diagnostics and secret-derived values are redacted from the fixed failure outcome. expect(JSON.stringify(git.mock.calls)).not.toContain("secret-derived resolver detail"); - expect(JSON.stringify(git.mock.calls)).not.toContain(existingWorktree); }); it("isolates writer failures", async () => { diff --git a/packages/engine/src/__tests__/worktree-acquisition.test.ts b/packages/engine/src/__tests__/worktree-acquisition.test.ts index 6c08edddf6..5255fd3dd5 100644 --- a/packages/engine/src/__tests__/worktree-acquisition.test.ts +++ b/packages/engine/src/__tests__/worktree-acquisition.test.ts @@ -4,10 +4,11 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "no import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; -import { acquireTaskWorktree, RepoRootWorktreeError } from "../worktree/worktree-acquisition.js"; +import { acquireTaskWorktree, RepoRootWorktreeError, WorktreeBaseRefreshError } from "../worktree/worktree-acquisition.js"; import { classifyTaskWorktree, PoolDoubleLeaseError } from "../worktree/worktree-pool.js"; import * as desktopArtifacts from "../worktree/worktree-desktop-artifacts.js"; import * as branchConflicts from "../execution/branch-conflicts.js"; +import { NativeWorktreeBackend } from "../worktree/worktree-backend.js"; vi.mock("../worktree/worktree-pool.js", async () => { const actual = await vi.importActual("../worktree/worktree-pool.js"); @@ -344,6 +345,222 @@ describe("acquireTaskWorktree", () => { expect(store.updateTask).toHaveBeenCalledWith("FN-1", { worktree: null, branch: null, sessionFile: null }); }); + it("refreshes a recreated existing task branch after its dependency branch is deleted", async () => { + const rootDir = makeRepo(); + const staleBase = git(rootDir, "git rev-parse HEAD"); + git(rootDir, `git branch fusion/fn-4 ${staleBase}`); + git(rootDir, `git checkout -b fusion/deleted-dependency ${staleBase}`); + writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8"); + git(rootDir, "git add dependency-output.ts"); + git(rootDir, 'git commit -m "land dependency"'); + git(rootDir, "git checkout main"); + git(rootDir, "git merge --ff-only fusion/deleted-dependency"); + git(rootDir, "git branch -d fusion/deleted-dependency"); + const landedBase = git(rootDir, "git rev-parse HEAD"); + + const result = await acquireTaskWorktree({ + task: { + ...task, + id: "FN-4", + branch: "fusion/fn-4", + baseCommitSha: staleBase, + executionStartBranch: "fusion/deleted-dependency", + }, + rootDir, + store, + settings: { worktreeNaming: "task-id", recycleWorktrees: false }, + refreshStaleBase: true, + createWorktree: async (branch, path) => { + git(rootDir, `git worktree add ${JSON.stringify(path)} ${JSON.stringify(branch)}`); + return { path, branch }; + }, + }); + + expect(result).toMatchObject({ + source: "fresh", + baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase }, + }); + expect(git(result.worktreePath, "git rev-parse HEAD")).toBe(landedBase); + expect(existsSync(join(result.worktreePath, "dependency-output.ts"))).toBe(true); + expect(store.updateTask).toHaveBeenCalledWith("FN-4", { baseCommitSha: landedBase }); + }); + + it("refreshes a retained task branch acquired from the worktree pool", async () => { + const rootDir = makeRepo(); + const staleBase = git(rootDir, "git rev-parse HEAD"); + const pooledPath = join(rootDir, ".worktrees", "pooled-fn-4"); + git(rootDir, `git worktree add -b fusion/fn-4 ${JSON.stringify(pooledPath)} ${staleBase}`); + writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8"); + git(rootDir, "git add dependency-output.ts"); + git(rootDir, 'git commit -m "land dependency"'); + const landedBase = git(rootDir, "git rev-parse HEAD"); + const pool = { + acquire: vi.fn().mockReturnValue(pooledPath), + prepareForTask: vi.fn().mockResolvedValue({ + branch: "fusion/fn-4", + worktreePath: pooledPath, + reclaimed: false, + }), + release: vi.fn(), + } as any; + + const result = await acquireTaskWorktree({ + task: { ...task, id: "FN-4", branch: "fusion/fn-4", baseCommitSha: staleBase }, + rootDir, + store, + settings: { recycleWorktrees: true }, + pool, + refreshStaleBase: true, + }); + + expect(result).toMatchObject({ + source: "pool", + baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase }, + }); + expect(git(pooledPath, "git rev-parse HEAD")).toBe(landedBase); + expect(existsSync(join(pooledPath, "dependency-output.ts"))).toBe(true); + }); + + it("does not apply the native stale-base refresh to fresh Worktrunk acquisitions", async () => { + const result = await acquireTaskWorktree({ + task, + rootDir: process.cwd(), + store, + settings: { worktrunk: { enabled: true } } as any, + backend: { kind: "worktrunk" } as any, + createWorktreeBackendKind: "worktrunk", + refreshStaleBase: true, + createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/worktrunk-fresh", branch: "fusion/fn-1" }), + }); + + expect(result).toMatchObject({ source: "fresh", baseRefresh: undefined }); + }); + + it("persists the backend used by an internally created worktree", async () => { + const rootDir = makeRepo(); + + const result = await acquireTaskWorktree({ + task, + rootDir, + store, + settings: { recycleWorktrees: false }, + backend: new NativeWorktreeBackend(), + }); + + const markerPath = git(result.worktreePath, "git rev-parse --git-path fusion-worktree-backend-kind"); + expect(readFileSync(markerPath, "utf-8")).toBe("native\n"); + }); + + it("refreshes a native fallback acquisition even when Worktrunk is enabled", async () => { + const rootDir = makeRepo(); + const staleBase = git(rootDir, "git rev-parse HEAD"); + git(rootDir, `git branch fusion/fn-4 ${staleBase}`); + writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8"); + git(rootDir, "git add dependency-output.ts"); + git(rootDir, 'git commit -m "land dependency"'); + const landedBase = git(rootDir, "git rev-parse HEAD"); + + const result = await acquireTaskWorktree({ + task: { ...task, id: "FN-4", branch: "fusion/fn-4", baseCommitSha: staleBase }, + rootDir, + store, + settings: { worktreeNaming: "task-id", recycleWorktrees: false, worktrunk: { enabled: true } } as any, + backend: { kind: "worktrunk" } as any, + createWorktreeBackendKind: "native", + refreshStaleBase: true, + createWorktree: async (branch, path) => { + git(rootDir, `git worktree add ${JSON.stringify(path)} ${JSON.stringify(branch)}`); + return { path, branch }; + }, + }); + + expect(result).toMatchObject({ + source: "fresh", + baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase }, + }); + }); + + it("uses the persisted native backend when a Worktrunk fallback is reused", async () => { + const rootDir = makeRepo(); + const staleBase = git(rootDir, "git rev-parse HEAD"); + const worktreePath = join(rootDir, ".worktrees", "fallback-fn-4"); + git(rootDir, `git worktree add -b fusion/fn-4 ${JSON.stringify(worktreePath)} ${staleBase}`); + const markerPath = git(worktreePath, "git rev-parse --git-path fusion-worktree-backend-kind"); + writeFileSync(markerPath, "native\n", "utf-8"); + writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8"); + git(rootDir, "git add dependency-output.ts"); + git(rootDir, 'git commit -m "land dependency"'); + const landedBase = git(rootDir, "git rev-parse HEAD"); + + const result = await acquireTaskWorktree({ + task: { ...task, id: "FN-4", worktree: worktreePath, branch: "fusion/fn-4", baseCommitSha: staleBase }, + rootDir, + store, + settings: { worktrunk: { enabled: true } } as any, + backend: { kind: "worktrunk" } as any, + refreshStaleBase: true, + }); + + expect(result).toMatchObject({ + source: "existing", + baseRefresh: { kind: "reset-to-base", executionSafe: true, baseSha: landedBase }, + }); + expect(git(worktreePath, "git rev-parse HEAD")).toBe(landedBase); + }); + + it("preserves a persisted Worktrunk backend when an injected native creator is available", async () => { + const rootDir = makeRepo(); + const staleBase = git(rootDir, "git rev-parse HEAD"); + const worktreePath = join(rootDir, ".worktrees", "worktrunk-fn-4"); + git(rootDir, `git worktree add -b fusion/fn-4 ${JSON.stringify(worktreePath)} ${staleBase}`); + const markerPath = git(worktreePath, "git rev-parse --git-path fusion-worktree-backend-kind"); + writeFileSync(markerPath, "worktrunk\n", "utf-8"); + writeFileSync(join(rootDir, "dependency-output.ts"), "export const dependencyOutput = true;\n", "utf-8"); + git(rootDir, "git add dependency-output.ts"); + git(rootDir, 'git commit -m "land dependency"'); + + const result = await acquireTaskWorktree({ + task: { ...task, id: "FN-4", worktree: worktreePath, branch: "fusion/fn-4", baseCommitSha: staleBase }, + rootDir, + store, + settings: { worktrunk: { enabled: true } } as any, + backend: { kind: "worktrunk" } as any, + createWorktreeBackendKind: "native", + refreshStaleBase: true, + }); + + expect(result).toMatchObject({ source: "existing", baseRefresh: undefined }); + expect(git(worktreePath, "git rev-parse HEAD")).toBe(staleBase); + }); + + it("clears a pooled task binding before releasing a worktree that fails base refresh", async () => { + const rootDir = makeRepo(); + const pooledPath = join(rootDir, ".worktrees", "pooled-fn-4-dirty"); + git(rootDir, `git worktree add -b fusion/fn-4-dirty ${JSON.stringify(pooledPath)}`); + writeFileSync(join(pooledPath, "uncommitted.ts"), "dirty\n", "utf-8"); + const pool = { + acquire: vi.fn().mockReturnValue(pooledPath), + prepareForTask: vi.fn().mockResolvedValue({ + branch: "fusion/fn-4-dirty", + worktreePath: pooledPath, + reclaimed: false, + }), + release: vi.fn(), + } as any; + + await expect(acquireTaskWorktree({ + task: { ...task, id: "FN-4", branch: "fusion/fn-4-dirty" }, + rootDir, + store, + settings: { recycleWorktrees: true }, + pool, + refreshStaleBase: true, + })).rejects.toThrow(WorktreeBaseRefreshError); + + expect(store.updateTask).toHaveBeenCalledWith("FN-4", { worktree: null, branch: null, sessionFile: null }); + expect(store.updateTask.mock.invocationCallOrder.at(-1)).toBeLessThan(pool.release.mock.invocationCallOrder[0]); + }); + it("FN-6861 creates a fresh configured worktree when a resumed assignment points at the repo root", async () => { const rootDir = makeRepo(); const actualPool = await vi.importActual("../worktree/worktree-pool.js"); diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 10876aaf3e..48045d3915 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -10469,6 +10469,9 @@ export class TaskExecutor { runContext: this.getRunContextFor(task.id), runInitCommand: true, createWorktree: this.createWorktree.bind(this), + // FNXC:WorktreeAcquisition 2026-08-09-03:30: This injected creator is native even when project settings + // prefer Worktrunk; retain its actual backend so stale-base refresh remains enabled on creation and reuse. + createWorktreeBackendKind: "native", runConfiguredCommand: (command, cwd, timeoutMs, env) => runConfiguredCommand( command, @@ -14390,6 +14393,9 @@ export class TaskExecutor { runContext: this.getRunContextFor(task.id), runInitCommand: true, createWorktree: this.createWorktree.bind(this), + // FNXC:WorktreeAcquisition 2026-08-09-03:30: This injected creator is native even when project settings + // prefer Worktrunk; retain its actual backend so stale-base refresh remains enabled on creation and reuse. + createWorktreeBackendKind: "native", runConfiguredCommand: (command, cwd, timeoutMs, env) => runConfiguredCommand( command, diff --git a/packages/engine/src/worktree/worktree-acquisition.ts b/packages/engine/src/worktree/worktree-acquisition.ts index 9bf67d1821..b0a30c9287 100644 --- a/packages/engine/src/worktree/worktree-acquisition.ts +++ b/packages/engine/src/worktree/worktree-acquisition.ts @@ -1,5 +1,7 @@ import { existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; import { exec } from "node:child_process"; +import { isAbsolute, resolve } from "node:path"; import { promisify } from "node:util"; import {acquireWorktreePathReservation, canonicalizeWorktreePath, type RunMutationContext, type Settings, type Task, type TaskStore, type SecretsStore} from "@fusion/core"; import { generateWorktreeName, resolveTaskWorkingBranch, slugify } from "./worktree-names.js"; @@ -46,6 +48,29 @@ import { activeSessionRegistry, type ActiveSessionRegistry } from "../agents/act import { refreshReusedWorktreeBase, type WorktreeBaseRefreshResult } from "../worktree-base-refresh.js"; const execAsync = promisify(exec); +const WORKTREE_BACKEND_MARKER = "fusion-worktree-backend-kind"; + +async function resolveWorktreeBackendMarkerPath(worktreePath: string): Promise { + const { stdout } = await execAsync(`git rev-parse --git-path ${JSON.stringify(WORKTREE_BACKEND_MARKER)}`, { + cwd: worktreePath, + encoding: "utf-8", + }); + const markerPath = stdout.trim(); + return isAbsolute(markerPath) ? markerPath : resolve(worktreePath, markerPath); +} + +async function persistWorktreeBackendKind(worktreePath: string, backendKind: WorktreeBackend["kind"]): Promise { + await writeFile(await resolveWorktreeBackendMarkerPath(worktreePath), `${backendKind}\n`, "utf-8"); +} + +async function readPersistedWorktreeBackendKind(worktreePath: string): Promise { + try { + const backendKind = (await readFile(await resolveWorktreeBackendMarkerPath(worktreePath), "utf-8")).trim(); + return backendKind === "native" || backendKind === "worktrunk" ? backendKind : undefined; + } catch { + return undefined; + } +} /** * Worktree acquisition contract: @@ -71,6 +96,8 @@ export interface AcquireTaskWorktreeOptions { startPoint?: string, allowSiblingBranchRename?: boolean, ) => Promise<{ path: string; branch: string }>; + /** Actual backend used by an injected creator when it differs from the configured backend. */ + createWorktreeBackendKind?: WorktreeBackend["kind"]; runConfiguredCommand?: (command: string, cwd: string, timeoutMs: number, env?: NodeJS.ProcessEnv) => Promise<{ spawnError?: string | Error; timedOut?: boolean; @@ -213,8 +240,11 @@ async function pinnedWorktreeBranchMatches(rootDir: string, worktreePath: string export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Promise { const { task, rootDir, store, settings, pool, logger, audit, runContext, createWorktree, runConfiguredCommand, runInitCommand, taskEnv, secretsStore } = opts; - const refreshExistingWorktree = async (path: string): Promise => { - if (!opts.refreshStaleBase) return undefined; + const refreshExistingWorktree = async ( + path: string, + backendKind: WorktreeBackend["kind"], + ): Promise => { + if (!opts.refreshStaleBase || backendKind === "worktrunk") return undefined; /* * FNXC:SecretsEnvMaterialization 2026-08-07-23:13: * Reconcile the v0.75.1 root record before strict porcelain checking. A malformed, conflicting, or @@ -231,11 +261,14 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro } if (!reconciliation.executionSafe) { const refresh: WorktreeBaseRefreshResult = { kind: "base-reconciliation-required", executionSafe: false, detail: reconciliation.outcome }; - await audit?.git?.({ type: "worktree:base-refresh-blocked", target: task.id, metadata: { taskId: task.id, outcome: refresh.kind, reconciliationOutcome: reconciliation.outcome } }); + await audit?.git?.({ type: "worktree:base-refresh-blocked", target: path, metadata: { taskId: task.id, outcome: refresh.kind, reconciliationOutcome: reconciliation.outcome } }); await store.logEntry(task.id, `Worktree secrets record reconciliation blocked execution (${reconciliation.outcome})`, undefined, runContext); throw new WorktreeBaseRefreshError(refresh); } - const refresh = await refreshReusedWorktreeBase({ task, rootDir, worktreePath: path, store, settings, audit, logger }); + const refreshSettings = settings.worktrunk?.enabled === true + ? { ...settings, worktrunk: { ...settings.worktrunk, enabled: false } } + : settings; + const refresh = await refreshReusedWorktreeBase({ task, rootDir, worktreePath: path, store, settings: refreshSettings, audit, logger }); if (!refresh.executionSafe) { await audit?.git({ type: refresh.kind === "stale-base-conflict" ? "worktree:base-refresh-conflict" : "worktree:base-refresh-blocked", target: path, metadata: { taskId: task.id, outcome: refresh.kind } }); await store.logEntry(task.id, `Worktree base refresh blocked execution (${refresh.kind})`, refresh.detail, runContext); @@ -289,6 +322,8 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro throw error; } const branchName = resolveTaskWorkingBranch(task); + const resolveExistingWorktreeBackendKind = async (path: string): Promise => + (await readPersistedWorktreeBackendKind(path)) ?? opts.createWorktreeBackendKind ?? backend.kind; const naming = settings.worktreeNaming || "random"; /* * FNXC:TaskPinnedWorktrees 2026-07-16-00:00: @@ -373,9 +408,17 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro Acquisition delegates branch creation to the isolated-worktree primitive. The project root remains on its current branch; task branch selection must never use a root-checkout `git checkout` or `git switch`. */ - const createWorktreeImpl = createWorktree - ? createWorktree - : async (createBranch: string, createPath: string, createTaskId: string, startPoint?: string, allowRename?: boolean) => { + const createWorktreeImpl = async ( + createBranch: string, + createPath: string, + createTaskId: string, + startPoint?: string, + allowRename?: boolean, + ): Promise<{ path: string; branch: string; backendKind: WorktreeBackend["kind"] }> => { + if (createWorktree) { + const created = await createWorktree(createBranch, createPath, createTaskId, startPoint, allowRename); + return { ...created, backendKind: opts.createWorktreeBackendKind ?? backend.kind }; + } const reservation = await acquireWorktreePathReservation({ canonicalPath: await canonicalizeWorktreePath(createPath), worktreesDir: resolveWorktreesDir(rootDir, settings), @@ -413,7 +456,8 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro metadata: { branch: created.branch }, }); } - return created; + await persistWorktreeBackendKind(created.path, backend.kind); + return { ...created, backendKind: backend.kind }; } catch (error) { if (backend.kind === "worktrunk" && error instanceof WorktrunkOperationError) { // FNXC:WorktreeAcquisition 2026-07-16-00:00: FN-8132 requires native fallback collision dispositions to be audited just like direct native acquisition. @@ -426,7 +470,9 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro taskId: createTaskId, allowSiblingBranchRename: allowRename, }); - return await handleWorktrunkFailure("create", error, fallback) as { path: string; branch: string }; + const created = await handleWorktrunkFailure("create", error, fallback) as { path: string; branch: string }; + await persistWorktreeBackendKind(created.path, "native"); + return { ...created, backendKind: "native" }; } throw error; } finally { @@ -475,7 +521,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro }; const finalizeCreatedWorktree = async ( - created: { path: string; branch: string }, + created: { path: string; branch: string; backendKind: WorktreeBackend["kind"] }, source: "fresh" | "pool", logOrigin: "normal" | "return-guard", ): Promise => { @@ -503,6 +549,11 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro await store.logEntry(task.id, `Worktree created at ${worktreePath}`, undefined, runContext); } + // FNXC:WorktreeBaseRefresh 2026-08-09-03:30: Execution can recreate an existing task branch after its + // dependency branch was merged and deleted. Refresh fresh acquisitions too so that branch cannot resume + // from its stale pre-dependency tip. + const baseRefresh = await refreshExistingWorktree(worktreePath, created.backendKind); + const cleanup = await removeDesktopBuildArtifacts(worktreePath, logger); if (cleanup.removed.length > 0) { await store.logEntry(task.id, `Removed desktop build artifacts from worktree: ${cleanup.removed.join(", ")}`, undefined, runContext); @@ -555,7 +606,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro } catch (err) { logger?.warn?.(`${task.id}: secrets-env write failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`); } - return { worktreePath, branch, source, hydrated, isResume: false }; + return { worktreePath, branch, source, hydrated, isResume: false, baseRefresh }; }; const createFreshWorktreeFromReturnGuard = async (guardedPath: string, source: string): Promise => { @@ -593,7 +644,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro logger, runContext, }); - const baseRefresh = await refreshExistingWorktree(path); + const baseRefresh = await refreshExistingWorktree(path, await resolveExistingWorktreeBackendKind(path)); return guardAcquisitionReturn({ worktreePath: path, branch: resumedBranch, source, hydrated, isResume: true, baseRefresh }); }; @@ -719,7 +770,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro runContext, }); // FN-4912: resume path reuses the prior on-disk .env (and its fingerprint sidecar). Rewrite is owned by the next fresh acquisition. - const baseRefresh = await refreshExistingWorktree(worktreePath); + const baseRefresh = await refreshExistingWorktree(worktreePath, await resolveExistingWorktreeBackendKind(worktreePath)); return guardAcquisitionReturn({ worktreePath, branch: resumedBranch, source: "existing", hydrated, isResume: true, baseRefresh }); } @@ -810,6 +861,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro } else { await store.logEntry(task.id, `Acquired worktree from pool: ${worktreePath}`, undefined, runContext); } + const baseRefresh = await refreshExistingWorktree(worktreePath, "native"); const cleanup = await removeDesktopBuildArtifacts(worktreePath, logger); if (cleanup.removed.length > 0) { await store.logEntry(task.id, `Removed desktop build artifacts from worktree: ${cleanup.removed.join(", ")}`, undefined, runContext); @@ -845,6 +897,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro source: "pool", hydrated, isResume: false, + baseRefresh, reclaimed: prepared.reclaimed ? { existingTipSha: prepared.existingTipSha, @@ -854,6 +907,13 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro }); } } catch (poolErr) { + if (poolErr instanceof WorktreeBaseRefreshError) { + // FNXC:WorktreeBaseRefresh 2026-08-09-03:30: Clear every durable resume binding before returning the + // checkout to the pool. If persistence fails, retain the lease so no other task can mutate it. + await store.updateTask(task.id, { worktree: null, branch: null, sessionFile: null }); + pool.release(pooled, task.id); + throw poolErr; + } pool.release(pooled, task.id); if (poolErr instanceof PoolDoubleLeaseError) { const poolErrMessage = poolErr instanceof Error ? poolErr.message : String(poolErr);