Why: at ≥150 cols Stats was lifted next to Logs but Utilities/Settings
sat in a separate bottom row. Keep them stacked under Stats in the left
column so Logs gets the full height on the right; Stats flex-grows to
absorb leftover space while Utilities/Settings stay at fixed heights.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FN-2910 surfaced concurrent reviewer + merger activity on the same task.
Root cause: asymmetric in-flight guards let an unpause-resume kick off a
fresh executor session while a recovery path was already running, and the
auto-merge handoff fired before the executor's finally block finished
cleanup. This sweeps the surrounding lifecycle paths for similar races and
tightens the reviewer pause gate against TOCTOU through runtime setup.
- Symmetric in-flight tracking across `executing`, `recoveringCompleted`,
and `resumingUnpaused`; `recoverCompletedTask` bails when any are set.
- Atomic claim of the recovery slot in the completed-task watchdog before
any awaited work.
- Workflow-rerun bounce returns "bounced" | "skipped-pending" so the
watchdog can no longer log a false-success retry when the original
bounce is still mid-flight.
- Self-healing's completed-task scan re-checks executing IDs inside the
loop instead of trusting a pre-await snapshot.
- 300ms grace period before auto-merge enqueue, giving the executor's
finally block (session disposal, child cleanup) time to drain and
eliminating the residual log-overlap symptom from FN-2910. Test uses
fake timers, no real sleep added.
- New AgentSemaphore.runNested for synchronously nested helper agents
(reviewers): bumps activeCount for honest observability while bypassing
the wait queue, preserving forward-progress fairness for the parent at
low maxConcurrent. Both createReviewStepTool and triage's
createReviewSpecTool now use it.
- New beforeSpawnSession hook on AgentRuntimeOptions/AgentOptions fired
inside createFnAgent immediately before createAgentSession, past every
awaited setup step. Reviewer wires a pause re-check that throws a
sentinel error converted to UNAVAILABLE, closing the TOCTOU window
where pause flipped during runtime resolution or resource loading.
All 2887 engine tests pass; engine + core + cli + dashboard + plugin-sdk
+ pi-claude-cli + desktop typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a worktree was rebased onto newer main, baseCommitSha remained an
ancestor of HEAD but the range baseCommitSha..HEAD then swept in upstream
main commits as if they were task changes (FN-2840 showed 33 files for a
4-file task). The previous display-recovery only fired when baseCommitSha
was no longer an ancestor, missing this case.
When enableDisplayRecovery is on and baseBranch is missing, also compute
merge-base(HEAD, main) and prefer it when it's a descendant of
baseCommitSha (tighter fork point). When merge-base is not a descendant
(FN-2855: baseCommitSha is on a deleted feature branch), keep
baseCommitSha so we don't widen to unrelated upstream files.
Also stabilizes the card-vs-Changes-tab divergence: resolution no longer
depends on whether a rebase happened between polls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The [c] copy handler was writing a success/failure log entry to the
buffer, but in expanded mode the panel hides the buffer so users got no
indication the copy ran. Add a transient inline "Copied!" flash in both
list and expanded views, and clamp the index to match the display
cursor so the copy never silently misses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Expand core test coverage for task node overrides, unavailable node policies, and defaultNodeId settings contracts
- Restrict dashboard task change views to files actually modified by the task, with updated API route handling and tests
- Improve quick chat UX and styling with extended component behavior and hook updates
- Harden memory dream extraction by reading assistant text from session state and covering undefined-output cases
- Enhance merger commit summarization and update architecture/settings/multi-project/task-management docs
Fusion-Task-Id: FN-2840
Two fixes for the in-review/in-progress "files changed" count on task
cards and the Changes tab:
- Drop untracked files from /tasks/:id/diff and /tasks/:id/file-diffs.
At review time these are almost always build artifacts/cache/logs not
in .gitignore, not real task changes, and they inflated the count.
- Add display-only `enableDisplayRecovery` option to resolveDiffBase.
When a worktree was rebased onto origin/main after baseCommitSha was
recorded and baseBranch was not set, the prior code fell through to
HEAD~1 — undercounting to just the last commit's files (e.g. FN-2957
showed 2 in review, 6 after merge). Recovery now tries
merge-base(HEAD, main) / origin/main before HEAD~1.
Routes opt into recovery; the merger's mirrored copy
(resolveTaskDiffBaseRef) is intentionally untouched so merge-time
scope warnings still evaluate the strict task base.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update dashboard mocks to push assistant text into session.state.messages
(matching real session.prompt() void return) and add a regression test for
array-shaped content. Add core unit tests for extractDreamProcessorResult
to lock in undefined/null safety.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
session.prompt() resolves to void; the reply lives in session.state.messages.
The dream route was awaiting its return value, so parsing crashed with
"Cannot read properties of undefined (reading 'match')" once the dream
completed. Extract the assistant text post-prompt and fail soft on
non-string input in extractDreamProcessorResult.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Squash merge commits previously landed with only a bare "merge
fusion/fn-XXXX" subject and a single bullet from the branch's commit
log, leaving git log readers without insight into what actually
changed. Now buildDeterministicMergeMessage calls summarizeCommitBody
(title-summarizer lane when configured, default model otherwise) with
the step commits + diffstat, and emits a three-section body: AI summary
+ Commits merged + Files changed. The deterministic sections always
ship so AI failure / timeout still yields a substantive message.
Also extends summarizeCommitBody to take an optional commitLog and
loosens its prompt for more detail when the change warrants it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-existing local modifications that had been sitting in the
working tree across this session, neither produced by the recovery /
fallback work in adjacent commits. Committing them so the tree is clean.
- packages/dashboard/app/components/SettingsModal.css
Removes one trailing blank line at end-of-file. No semantic change.
- packages/dashboard/app/components/__tests__/ListView.test.tsx
Drops one test case ("shows NodeHealthDot for selected bulk node
override", 15 lines) from the "ListView - Bulk Selection" describe
block. The test asserted that selecting a bulk node override
rendered a `.status-dot--online` indicator in the DOM. Sibling tests
(option labels, batchUpdateTaskModels invocation) are preserved.
Likely debris from the FN-2729 NodeHealthDot work that didn't survive
one of the in-flight squash merges; the underlying behavior is still
exercised by the kept tests.
Verified: pnpm --filter @fusion/dashboard test ListView passes
(125/125), so the removed assertion isn't load-bearing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the commit-body AI helper out of merger.ts into the existing
core/ai-summarize.ts module so all short-summary AI work (titles,
chat titles, fallback merge commit bodies) shares one home with
consistent dispatch semantics, error handling, and session lifecycle.
Core (ai-summarize.ts):
- New `summarizeCommitBody(diffStat, rootDir, provider, modelId, opts)`
exported alongside `summarizeTitle`. Same shape (provider/modelId
args), same get-engine-or-bail dynamic loading via `getFnAgent`,
same readonly-tools session, same disposal-in-finally pattern.
- Differs from `summarizeTitle` in three deliberate ways suited to the
commit-body job:
1. Returns null on any failure instead of throwing — the caller is
always the merger, which has a deterministic fallback chain
behind it. Throwing would force the merger to wrap every call
in try/catch.
2. Accepts an optional `signal` to forward engine-pause / shutdown
cancellation, plus a configurable `timeoutMs` (default 30s)
so a wedged AI session can't stall a merge indefinitely.
3. Larger output ceiling (2000 chars vs title's 60) and larger
input ceiling (4000 chars truncated diff) — commit bodies are
multi-line and need more room than a 60-char title.
- Exported alongside `summarizeTitle` from `@fusion/core`. Constants
(`COMMIT_BODY_SYSTEM_PROMPT`, `MAX_COMMIT_BODY_INPUT_LENGTH`,
`MAX_COMMIT_BODY_LENGTH`, `DEFAULT_COMMIT_BODY_TIMEOUT_MS`) re-exported
for callers that want to override behavior.
Engine (merger.ts):
- Dropped the local `aiGenerateCommitBody` function (~70 lines) — it
duplicated the session-creation pattern from `summarizeTitle` while
living in a place where future maintainers wouldn't think to look.
- `resolveSafeCommitBody` now imports `summarizeCommitBody` from
`@fusion/core` and delegates. The cascade behavior is unchanged
(commitLog → AI → diff stat → synthetic) and the title-summarizer
model preference is preserved (provider/modelId resolved here, then
passed through).
Tests:
- 6 new test cases in `ai-summarize.test.ts` covering:
empty input → null, missing engine → null (graceful, never throws),
missing engine + model selection → null, pre-aborted signal → null,
custom timeout (returns quickly under 1s ceiling), exposed constants.
- Core: 3136/3136 pass (was 3130 — +6 new). Engine: 2887/2887 pass.
- Typecheck clean, workspace lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the AI commit-body generator (introduced in 60217ce19) to the
existing dedicated title-summarization model lane, and updates the
settings UI so users understand the model is now used for two
short-summary jobs instead of just one. Also de-duplicates the
project-scope settings UI which was exposing the same model setting in
two places.
Engine (merger.ts):
- aiGenerateCommitBody now prefers settings.titleSummarizerProvider /
titleSummarizerModelId over the merger's default model. The
summarization lane is the right tier for this work — small, fast,
cheap. Falls back to the default merger model when the summarization
lane is unset.
Dashboard (SettingsModal.tsx):
- MODEL_LANES summarization lane label updated:
"Title Summarization Model" → "Title and Git Commit Message
Summarization Model". Helper text updated to mention the dual purpose
(auto-generated task titles + fallback merge commit message bodies).
This change flows through automatically to BOTH the global model
lanes view and any project-scope rendering of MODEL_LANES.
- Removed the duplicate summarization picker from the project-scope
Model Lanes section: previously it appeared once under Model Lanes
AND once under "AI Summarization". Now lives only in the dedicated
picker so users have a single source of truth in project scope.
- The dedicated picker section heading + description rewritten:
"AI Summarization" → "AI Title and Git Commit Message Summarization",
with a paragraph explaining both jobs the model performs.
- Inner dropdown label updated for consistency.
Tests + checks: engine 2887/2887 pass, dashboard typecheck clean,
workspace lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit of merge commit sites surfaced four paths that could produce an
empty commit body when the branch had no unique commits relative to its
base (so `git log <base>..<branch>` returns empty), or when the `git log`
collection itself failed. Subject was always non-empty so git accepted
the commit, but the body was missing — breaking downstream consumers
(release notes, dashboard summaries, mergeDetails) that read it.
The AI merge agent is the primary author of merge commit messages; these
fallback paths only run when the agent didn't commit and the merger has
to commit on its behalf. Previously they used `-m "${commitLog}"` which
silently produced `-m ""` on empty input.
Now uses a 4-tier resolveSafeCommitBody cascade — most informative
first, with a deterministic floor so the function never returns empty
and never throws:
1. The branch's commit log if non-empty.
2. AI-generated body via aiGenerateCommitBody — a fresh readonly
session that summarizes the diff stat into 2–6 bullet points.
Bounded by a 30s timeout (forwards the caller's abort signal too)
so engine pause / shutdown tears it down promptly. Any failure
falls through.
3. The diff stat itself, formatted as a "Files changed" listing.
4. A synthetic `- merge <branch>` placeholder.
Wired into all three merger fallback commit sites:
- Auto-resolved-conflicts commit (Attempt 2 success path)
- -X ours / -X theirs side-strategy commit (Attempt 3)
- Agent-didn't-commit fallback commit (post-AI verification)
Also defensive: removed `--allow-empty-message` from the executor's
squash-import commit. The message is hardcoded non-empty (subject +
body), but the flag was a footgun — git would silently accept an empty
message if the construction ever broke. Switched to two `-m` args
(subject and body separately) so empty would now correctly fail at
git's level rather than silently land a blank-message commit.
Tests + checks: engine 2887/2887 pass, typecheck clean, workspace lint
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Resolve merge conflicts in InlineCreateCard and QuickEntryBox using the existing node picker UX
- Replace ad-hoc node status rendering with NodeHealthDot in task creation surfaces
- Keep advanced quick-create controls and portal-based dropdown behavior intact
- Preserve related dashboard styling and tests included in the staged squash
Fusion-Task-Id: FN-2729
Self-review of the recovery cascade surfaced three issues; this commit
addresses all of them.
1. AI didn't actually receive the safety constraint under Layer 3.
The previous commit logged the safety preamble to the task log via
`store.logEntry`, but the merge agent doesn't read task log entries as
prompt context — so the AI was running blind. The "no silent
re-introduction of main's deletions" guarantee was therefore relying
*entirely* on the deterministic verification gate (test + build),
which is correct as a backstop but doesn't help the AI produce a
correct first attempt.
Fixed by threading `preMergeRebaseFallthrough` through
`MergeAttemptParams` → `executeMergeAttempt` → `runAiAgentForCommit` →
`MergePromptParams` → `buildMergePrompt`, where it now injects an
explicit "⚠️ Pre-merge rebase recovery exhausted" preamble at the top
of the user prompt with three concrete rules:
- Prefer main's deletion when branch re-adds removed lines
- Prefer main's version on ambiguous hunks
- Call `fn_report_build_failure` rather than commit a regression
Also includes the original rebase failure message (truncated) so the
AI has diagnostic context.
The truncated-context retry path also forwards the preamble — it's the
safety constraint, not bulk context, so we keep it even when stripping
diff stat / commit log to fit the window.
2. Layer 2's branch-restore could fail with "uncommitted changes".
When a cherry-pick midway through Layer 2's replay fails, the worktree
is in a half-applied state with conflicts in the index. The previous
restore did `git checkout <branch>` (no -f) followed by
`git reset --hard <originalSha>`. The plain checkout would refuse with
"would overwrite local changes" if there were unmerged paths,
preventing the reset from running and leaving the branch at the
half-replayed tip.
Fixed by reordering: hard-reset to the captured original SHA first
(this clears index/working tree of any cherry-pick state), then
`git checkout -f <branch>` to ensure HEAD points at the named branch,
then a final hard-reset to the original SHA as belt-and-suspenders.
Worst case the worktree is at the original branch tip — never worse
than where Layer 2 started.
3. Pre-existing unrelated lint error blocking workspace lint.
`packages/dashboard/src/server.ts` had an unused `resolve` import from
`node:path` left behind by a recent refactor that extracted
`PACKAGE_VERSION` into its own file. The user explicitly asked to
clean it up so workspace lint passes. One-line drop.
Tests + checks:
- Engine: 2886 / 2886 pass (added safety preamble didn't break any
existing prompt-content assertions)
- Core: 3120 / 3120 pass
- Workspace lint: clean
- Engine typecheck: clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a layered recovery cascade to the merger's pre-rebase stage so tasks
no longer get stuck in in-review when their declared dependency was
squash-merged to main and left orphan raw commits in the dependent's
history. Also prevents the orphan situation at the source for new tasks.
Why:
- 13 tasks were stuck in in-review for hours, all hitting the same
pre-merge rebase abort because they shared 6 raw commits inherited
from FN-2729's branch (declared baseBranch). FN-2729 was then
squash-merged to main, turning those raw commits into orphans whose
content is in main but in a different commit shape, conflicting with
later-merged tasks. The merger's `smart-prefer-main` strategy
correctly refused -X ours (which would silently re-introduce main's
deletions), but the only escape hatch was a 30-min cooldown loop
that retried the same impossible rebase forever.
Recovery cascade (merger.ts pre-rebase stage):
- Layer 1: surgical `git rebase --onto <main> <dep-tip> <branch>` when
task.baseBranch is set. Resolves the dep tip from the live branch ref
or recorded baseCommitSha; peels off the dep's inherited commits
cleanly. Captures the squash-merge-of-dep case end-to-end.
- Layer 2: generic patch-id duplicate-content stripping. Walks the last
500 main commits, computes patch-ids, then drops branch commits whose
patch-id matches and cherry-picks the remainder onto main. Captures
manual cherry-picks, double-merges, and any other duplicate-content
variant Layer 1 doesn't see. Restores the branch's pre-mutation SHA
on partial-failure so worst case leaves the worktree no worse than
before the recovery attempt.
- Layer 3: AI arbitration fall-through. If Layers 1+2 fail, log the
situation and proceed to the existing 3-attempt AI merge cascade
instead of throwing. The deterministic post-merge verification
(test + build) gates whatever the AI produces — that gate is what
enforces prefer-main's safety contract under fall-through (no silent
re-introduction of main's deletions).
- Critical: the unsafe `-X ours` Attempt 3 is suppressed under
fall-through. AI Attempts 1+2 are the only paths that can complete
the merge; if both fail and verification rejects them, the task
bounces back to in-progress via the existing engine path rather than
silently merging.
Prevention (executor.ts worktree creation):
- When a task declares a non-main `baseBranch`, branch the worktree
off main (origin/<defaultBranch> when worktreeRebaseBeforeMerge is
enabled and a remote is resolvable; otherwise local rootDir HEAD)
and `git merge --squash` the dep's content as a single import commit.
The dependent branch then carries main's history + 1 commit instead
of inheriting the dep's raw commits, so a future squash-merge of the
dep produces patch-id-matching content that rebases cleanly.
- Honors settings: respects `worktreeRebaseBeforeMerge`,
`worktreeRebaseRemote`, and falls back to local HEAD when no remote
is resolvable. Fully fail-soft: any squash-import error falls back to
the legacy fork-from-dep behavior so worktree creation still works
for setups where the squash flow can't run.
Engine-side last-retry fix (project-engine.ts):
- Changed conflict-retry condition from `currentRetries < MAX` to
`currentRetries + 1 < MAX` so the bounce-to-in-progress code fires
in the same engine tick as the failing attempt, rather than relying
on a setTimeout-scheduled Nth attempt that dies on engine restart.
Without this, a dev-time engine restart between the 3rd and 4th
retry left the task with mergeRetries=MAX and only the 30-min
cooldown sweep could try again.
Tests:
- New "Layer 1 recovery" test asserts the surgical --onto rebase fires
when baseBranch is set and primary rebase aborts, and that Layer 3
fall-through is NOT triggered when Layer 1 succeeds.
- Updated the "no silent fall-through to -X ours" test to cover the
new fall-through path: even after Layers 1+2 fail and the merge
cascade proceeds, -X ours must not run, and the task log must record
both the Layer 3 fall-through entry and the Attempt 3 suppression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Default Cloudflare remote access to Quick Tunnel mode in settings documentation
- Replace the Quick Tunnel checkbox with an Advanced (Named Tunnel) details disclosure in SettingsModal
- Auto-sync quick tunnel state from the details open/close state while preserving named tunnel field editing
- Update SettingsModal tests to cover the new advanced-toggle behavior and persisted quick tunnel payload
Fusion-Task-Id: FN-2916
Tasks were getting stuck in `in-review` forever when auto-merge could not
resolve conflicts within MAX_AUTO_MERGE_RETRIES. The conflict-exhaustion
branch silently cleared `status` (no error, no log entry, no comment),
and the 30-min cooldown sweep would reset retries and re-attempt the
same impossible merge — looping silently with no user-facing surface.
Why:
- FN-2918 and FN-2903 both spent hours in this loop with no error/comment
visible on the task. The only log evidence was repeated
"Auto-merge retry cooldown elapsed (30m idle)" entries with no
follow-up outcome.
How to apply:
- Every merge failure now writes a `<Manual|Auto>-merge failed: <msg>`
entry to the task log so the dashboard surfaces the reason.
- Conflict-retry exhaustion now bounces the task back to `in-progress`
with a comment + log entry so the executor re-rebases against main
and retries — mirroring the verification-failure-bounce pattern.
- New `mergeConflictBounceCount` task field caps outer bounces
(`MAX_MERGE_CONFLICT_BOUNCES = 2`); past the cap, the task is parked
in `in-review` with `status="failed"` and a follow-up triage task is
created so a human can resolve the conflict manually.
- Non-conflict and non-direct-strategy errors now also set
`status="failed"` so the cooldown sweep can't re-pick them up.
- `canMergeTask` skips tasks with `status="failed"` so terminal
failures (verification cap, bounce cap, non-conflict error) are no
longer eligible for cooldown re-attempts.
Schema migration v52 adds the `mergeConflictBounceCount` column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add attachment-first chat compose flow with paperclip picker, drag-and-drop, and paste support in ChatView input
- Render pending attachment preview chips with per-file remove actions and drag-over visual affordance
- Display sent message attachments with image/file presentation and accessible focus/interaction styling
- Update ChatView tests and icon mocks to cover attachment-only sends, file selection, and preview behavior
Fusion-Task-Id: FN-2911