The executor's conflict-recovery paths (cleanupConflictingWorktree,
handleBranchConflict, and tryCreateWorktree's live-foreign/stale-resolved
branches) could force-remove a worktree even when it was currently bound
to an active executor session. This caused the FN-4781/FN-4804 cascade:
- 'Execution blocked: assigned worktree path disappeared mid-task' as
git deleted the live agent's filesystem out from under it
- Two parallel runs for the same task alive simultaneously, with the
second run started in a fresh worktree while the first was still
holding the old session
- Cross-task log attribution (an FN-4804 runContext writing to FN-4781)
- Post-merge 'branch tip misbound but content found on main via trailer'
rescues firing on every successful merge as the bookkeeping was
corrupted mid-merge
Adds a hard liveness gate centralized in findActiveWorktreeOwner(), which
checks both the in-memory activeWorktrees map and the DB for non-done,
non-paused, in-progress tasks bound to the worktree. The gate fires at
two points:
1. cleanupConflictingWorktree returns false (refuses removal) when an
active owner is found, logging an FN-4811 refusal entry.
2. handleBranchConflict short-circuits to 'sticky' BEFORE invoking
inspectBranchConflict, because some inspection branches force-remove
unconditionally.
When cleanup is refused, the live-foreign and stale-resolved branches in
tryCreateWorktree now FALL THROUGH to the suffix-rename path (rather
than returning null) so the requesting task can still proceed without
disturbing the live owner.
Tests:
- New reliability-interactions backstop at
src/__tests__/reliability-interactions/active-worktree-removal-liveness.test.ts
covers findActiveWorktreeOwner (5 cases: in-memory match, requesting
task excluded, DB-level match, paused exclusion, terminal-column
exclusion, self-exclusion), cleanupConflictingWorktree gate (3 cases:
in-memory refuse, DB refuse, no-owner proceed), and handleBranchConflict
gate (2 cases: short-circuit + inspection-skipped, no-owner proceeds).
- Updates existing executor-worktree.test.ts assertion that was
documenting the bug behavior (force-removing active worktree) to match
the new contract (refuses + falls through to suffix-rename).
Full engine suite: 307 files, 5035 tests pass.
Fusion-Task-Id: FN-4811
The executor's no-fn_task_done retry loop has two reclaim signals
(retryAbortedDueToReclaim=true): (1) pre-retry liveness recheck where the
task DB shows worktree/branch was cleared, and (2) session-start failure
because the worktree path no longer exists. Both are engine self-heal
situations triggered by FN-4546 stale-active-branch reclaim, FN-4742
self-healing removals, or related housekeeping paths — the agent never
got a fair retry attempt.
Previously these surfaced as task status=failed with error
'Worktree/branch reclaimed during no-fn_task_done retry — requeueing',
fired onError, and burned the taskDoneRetryCount budget. Three legitimate
problems followed: tasks accumulated spurious failures in the UI, the
exhausted-budget branch escalated reclaimed tasks to in-review instead of
retrying, and the noise masked the underlying worktree-removal regression
(FN-4811).
Now the reclaim branch silently:
- clears stale worktree/branch metadata so the next pickup creates a fresh worktree
- requeues to todo with preserveProgress
- logs an informational 'engine self-heal, no failure' line
- does NOT set status=failed, does NOT bump taskDoneRetryCount, does NOT call onError
The genuine 'agent finished without calling fn_task_done after N retries'
exhaustion path (retryAbortedDueToReclaim=false) is unchanged.
Tests updated in
packages/engine/src/__tests__/reliability-interactions/executor-no-task-done-vs-worktree-reclaim.test.ts
to assert the new silent-recovery contract on all three reclaim paths.
Fusion-Task-Id: FN-4806
Add dist-electron/ and common installer artifact patterns (*.dmg,
*.zip blockmaps, *.AppImage, etc.) to .gitignore so electron-builder
output never gets staged into commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without productName, Electron's app.getName() returns the package
name "@fusion/desktop", which macOS uses for the user-data dir and
some app surfaces. Setting productName=Fusion aligns runtime naming
with the electron-builder productName and the Info.plist bundle name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
esbuild now bundles main.ts with electron-updater (and its transitive
deps) inline, externalizing only electron, workspace packages, and the
native better-sqlite3. This sidesteps the pnpm hoisting issue where
electron-updater's transitive `ms` lived only inside the .pnpm content
store and never landed in the packaged app.asar, leaving the in-app
updater non-functional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate a 1024x1024 macOS app icon from the dashboard logo so packaged
builds carry the Fusion brand instead of the default Electron icon, and
load electron-updater dynamically inside a try/catch so packaged builds
tolerate CJS/ESM interop quirks and missing transitive deps. Widen the
electron-builder files whitelist to include electron-updater's
transitive dep tree so they are actually bundled into the asar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>