Make pinned model provider headers sit flush against the fixed dropdown header stack.
- Remove top padding that exposed scrolling rows above sticky provider headers.
- Cover the no-seam invariant across desktop and mobile layouts with and without thinking controls.
- Add a patch changeset for the dropdown scrolling fix.
Files changed:
.changeset/fn-8212-model-dropdown-sticky-gap.md | 7 ++++
.../app/components/CustomModelDropdown.css | 5 ++-
.../__tests__/CustomModelDropdown.test.tsx | 46 ++++++++++++++++++++++
3 files changed, 57 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-8212
Fusion-Task-Lineage: d7ce42d1-bcbe-4000-8107-794ec8e05307
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
WhatsApp rejects handshakes advertising Baileys' baked-in stale protocol
version with a 405 close, so the plugin cycled starting->disconnected and
never issued a QR. connect() now fetches the current WA Web version per
socket build. /status also exposes lastError so this failure mode is
diagnosable from the documented troubleshooting surface.
The published bundled.js also failed to load entirely ("Dynamic require
of 'crypto' is not supported"): plugin bundles are ESM but Baileys is
CJS. bundlePluginEntry now injects the same createRequire banner as
dist/bin.js.
Verified end-to-end against an isolated fn serve: bundled.js loads,
status reaches awaiting-qr, /qr serves a scannable QR data URL,
/pair-code validates input, /logout clears auth state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full-screen mobile task-detail sheet hides all resize handles, so
FN-8015's `margin-inline-end: var(--space-lg)` gutter on the shared
`.floating-window__body` (added to keep the scrollbar clear of desktop
resize hot zones) only added dead space on the right and shifted the
entire panel left. Zero it for `.floating-window--task-detail` inside
the mobile breakpoint so `.detail-body`'s own padding defines both
insets equally; desktop resize-handle clearance is untouched.
Refined the FN-8015 invariant test to enforce the desktop hot-zone gutter
media-aware (strips @media blocks) and added a regression guard for the
mobile zeroing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add tokenized spacing below the Settings current-theme selector before Font Size.
- Scope the spacing to the Settings current-theme row without affecting compact dropdowns.
- Add a regression test for the established spacing token.
- Add a patch changeset for the Settings layout fix.
Files changed:
.changeset/fn-8200-theme-selector-spacing.md | 6 ++++++
packages/dashboard/app/components/ThemeDropdown.css | 8 ++++++++
.../dashboard/app/components/__tests__/ThemeDropdown.test.tsx | 6 ++++++
3 files changed, 20 insertions(+)
Fusion-Task-Id: FN-8200
Fusion-Task-Lineage: 6b72d0f7-759f-49aa-a8f9-f788cc9ec5c5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## fix(postgres): scope the cross-process merge guard to the project
The guard's own comment (`project-engine.ts`) says it checks whether
another process is merging a task **"for this project"** — and in SQLite
mode the per-project DB file made that scoping implicit.
`getActiveMergingTaskImpl`'s `backendMode` branch queries the shared PG
`tasks` table with **no `project_id` filter**, so one merging task
anywhere serializes merges across **all** projects.
### Production evidence
6-project embedded-PG deployment: **697 cross-project `Merge deferred …
is already merging (cross-process guard)` retries in 10 minutes** — six
independent repos waiting on each other's serialized merger, collapsing
merge throughput ~6x and letting `in-review` pile up to 95 tasks.
### Fix
Add the existing `taskProjectScope(layer)` filter to the query's
conditions (one line + import). It is a no-op when the layer carries no
`projectId`, so single-project deployments and the SQLite path are
unchanged. Same pattern as the other project-scoped task queries.
Deployed on the affected instance: cross-project merges now proceed in
parallel; per-project serialization (the guard's documented intent) is
preserved.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved merge task handling so activity in one project no longer
unnecessarily blocks merge operations in other projects.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: TrinaryCompute <fusion-merge@trinarycompute.dev>
## Summary
- Follow-up after #2229: full suite on main still failed on dashboard
curated inventory (21 ungated files) and mass engine failures
(`this.store.getAgentLogCount is not a function`).
- Harden executor tool-failure cursor capture for minimal/test
`TaskStore` adapters (same optional-API pattern as `project-engine`),
keep mock fixtures in lockstep, and quarantine inventory-only dashboard
files with ledger + vitest exclude.
## Changes
- **Executor**: optional `getAgentLogCount` / `getAgentLogs` /
`updateTask` at graph entry and trailing-failure detection.
- **Mocks**: `createMockStore`, soft-delete guard, post-done
continuation, cron `getGlobalSettingsDir`, executor-prompt
`bulkCompletionRefusalAt` (FN-8141).
- **i18n** (prior commit): es/fr/ko/zh-CN/zh-TW triage-duplicate keys.
- **Inventory**: 21 dashboard files → `test-quarantine.json` +
`vitest.config.ts` lockstep (VAL-REMOVAL SQLite / load flakes /
build-only dist assert).
## Test plan
- [x] `node scripts/check-test-inventory.mjs --dashboard-curated`
- [x] `pnpm test:gate`
- [x] engine: soft-delete, prompt, cron, post-done, tool-failure-retry,
and related samples
- [x] `@fusion/core` schema-applier + `@fusion/i18n` parity
- [ ] Full Suite (non-blocking) on this PR / main after merge
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added localized text for triage duplicate-resolution settings and
near-duplicate task actions in Spanish, French, Korean, Simplified
Chinese, and Traditional Chinese.
- Users can now see translated options and confirmations to keep or
delete detected duplicate tasks.
- **Bug Fixes**
- Improved resilience during task execution and recovery when optional
activity-log services are unavailable, preventing avoidable failures
during error handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
pi >=0.80.8 moved session request auth to ModelRuntime.getAuth -> pi-ai
resolveProviderAuth, which reads credentials.read("anthropic") and refreshes an
OAuth credential via credentials.modify("anthropic"). Fusion stores the
subscription login under `anthropic-subscription` with no raw `anthropic` row,
so the refresh callback saw current===undefined, bailed, and auth resolved to
undefined -> "Provider is not configured: anthropic" (then fell back).
Resolve read("anthropic") through fusion's getApiKey (refresh + raw/legacy/
subscription/fallback precedence) and hand pi-ai a ready api_key credential;
pi-ai routes it as OAuth by the sk-ant-oat token prefix. Supersedes the prior
read-alias, which fixed lookup but not the broken OAuth refresh-via-modify path.
Verified end-to-end: ModelRuntime.getAuth(anthropic/opus) now resolves the
subscription token instead of returning undefined.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pi-ai >=0.80 resolves provider auth via credentials.read(provider.id) and
performs OAuth refresh/derivation itself, bypassing fusion's getApiKey()
where the anthropic-subscription -> anthropic alias lived. A subscription-only
login surfaced as "Provider is not configured: anthropic" at prompt time even
though the status card showed connected.
- Alias the subscription OAuth credential into read("anthropic") at the
credential-store layer (createFusionCredentialStore); raw/legacy rows still win.
- Match "not configured" in isRetryableModelSelectionError so an unresolved
provider triggers the configured fallback model instead of hard-failing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## fix(engine): reclaim leaked semaphore slots when the system is busy,
not only at total idle
### The bug
`recoverIdleSemaphoreLeakCandidate` only reclaims stale `AgentSemaphore`
slots when the system is **completely** idle (`persistedActive === 0 &&
inFlightCount === 0` → reconcile to 0). If even one in-progress row
persists — e.g. a zombie task whose agent session died without its
`finally` release — the valve never opens, and slots leaked by abnormal
teardown accumulate monotonically until `activeCount` pins the limit.
At that point the engine deadlocks in a distinctive way:
- every hold/release sweep logs `Hold release for <task> deferred — no
reservable slot for in-progress`
- triage/plan report `planning=0 … processing=0, semaphore
active=<limit>/<limit>, available=0`
- the merge queue grows unboundedly (merges also need a slot)
- only a process restart recovers
`reapLeakedConcurrencySlots` (FN-6782) doesn't help — it reconciles
**worktree** slots, not the shared semaphore.
### Production evidence
Observed twice on a 6-project embedded-PG deployment driving a local
model:
- After ~5 days of continuous operation: `semaphore active=24/24`,
`planning=0/24, processing=0`, 5 persisted in-progress rows (dead
sessions), merge queue at 88, **zero merges for >24h**. Restart
immediately restored merging.
- Same signature earlier at `active=40/40` with both LLM backends idle
(`kvcache≈0`).
The handful of zombie in-progress rows kept `persistedActive` nonzero
indefinitely, so the idle-only valve could never fire.
### The fix
Generalize the valve: clamp `activeCount` down to the **persisted +
in-flight bound** whenever the semaphore over-holds **continuously** for
a repair window.
- The strict-idle case (`bound === 0`) keeps its existing fast 5s window
— behavior unchanged, existing tests pass as-is.
- The non-idle case uses a deliberately conservative new window
(`STALE_SEMAPHORE_EXCESS_REPAIR_MS = 600_000`, 10 min): nested helper
agents (`runNested`) legitimately push `activeCount` above the persisted
top-level count for the duration of a nested run, so the excess must
outlive any plausible nested session before it is treated as leaked. The
candidate timestamp resets the moment the excess clears.
- `reconcileActiveCount` only ever lowers the count, so the clamp cannot
inflate capacity; a late release from a genuinely live agent after a
(worst-case, mis-timed) clamp is absorbed by the existing excess-release
guard (FN-6423).
Call-site changes are limited to the two log messages (the old
parenthetical claimed "no persisted … agent work", which is no longer
the only repair case).
### Tests
- existing idle-valve tests pass unchanged (same window, same
reconcile-to-0)
- new: stale excess above a nonzero persisted bound is repaired only
after the long window, and clamps exactly to the bound
- new: candidate resets when the excess clears (nested overshoot ending)
- new: caller in-flight sessions count into the bound (no false
candidate)
### Files
- `packages/engine/src/concurrency.ts` — generalized valve +
`STALE_SEMAPHORE_EXCESS_REPAIR_MS`
- `packages/engine/src/scheduler.ts`, `packages/engine/src/triage.ts` —
log message accuracy
- `packages/engine/src/__tests__/concurrency.test.ts` — 3 new tests
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved stale “semaphore excess” recovery by using a configurable
repair window when excess persists.
* Prevented premature capacity corrections by accounting for in-flight
top-level work during reconciliation.
* Correctly handles nested helper activity so only leaked excess is
reclaimed, preserving legitimate nested runs.
* Updated reconciliation to clamp excess to the appropriate reclaim
floor instead of waiting indefinitely.
* **Improvements**
* Refreshed diagnostic warning text to clarify the over-held vs
persisted+in-flight work comparison.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: TrinaryCompute <fusion-merge@trinarycompute.dev>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
PR #2260 added project.tasks.bulk_completion_refusal_at to the Drizzle model
and the 0000 baseline but shipped no forward migration. Databases created
before #2260 already carry the 0000 marker, so the applier skips the baseline
and they never gained the column — every such cluster crashed on the first
TaskStore SELECT ("column bulk_completion_refusal_at does not exist"), taking
down dashboard/app boot.
Adds forward migration 0018 (wired via BULK_COMPLETION_REFUSAL_AT_VERSION;
SCHEMA_BASELINE_VERSION -> "0018") so existing clusters heal on next startup.
Prevention:
- Per-column upgrade regression test reproducing the exact existing-DB failure.
- Migration-wiring-integrity guard (no PostgreSQL): SCHEMA_BASELINE_VERSION must
equal the highest migration file, and every .sql must be registered in the
applier so none silently never runs.
- Repairs 6 pre-existing schema-applier tests left stale by the 0017 addition
(baseline-marker identity + version-list enumerations).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Eliminates the remaining backend/PostgreSQL-mode sync-SQLite
(`store.db`) call sites — both the crashing ones and the
try/catch-masked ones that silently degraded features. Found via a full
audit of `store.db`/`archiveDb` residue after the PG cutover's per-site
routing missed them.
**Crashes fixed:**
1. **refineTask / duplicateTask** threw `TaskStore.db: SQLite Database
is not available in backend mode`. Both create rows through
`createTaskWithId` callbacks calling `store.atomicCreateTaskJson()`
directly, bypassing `_createTaskInternal`'s backend routing. The shared
helper now routes itself (soft-delete conflict check + non-destructive
insert in one AsyncDataLayer transaction).
2. **Merger verification cache**: `getVerificationCacheHit` ran sync
SQLite unguarded *outside* any try/catch in
`runDeterministicVerification`; `recordVerificationCachePass` was
swallowed so the cache never warmed. Both are now async with a PG
branch.
**Silent degradations fixed (features that were dead on PG):**
- Workflow run-branch + foreach step-instance persistence
(`saveWorkflowRunBranch`, `loadWorkflowRunBranches`,
`clearWorkflowRunBranches`, `saveWorkflowRunStepInstance`,
`loadWorkflowRunStepInstances`, `clearWorkflowRunStepInstances`) —
executor crash-resume checkpoints were silently never persisted.
- `getBranchProgressByTask` — returned an empty map, dropping
`branchProgress` from task payloads.
- `runPluginColumnTransitionHooks` — plugin `onEnter`/`onExit`
column-transition hooks never fired (marker bookkeeping + non-locking
task read now async).
- `getTaskColumns` — dashboard treated all agent-linked tasks as
non-terminal.
- `getWorkflowStep` / `listWorkflowSteps` — stored workflow-step rows
now read from `project.workflow_steps` (listing previously returned
plugin steps only); `getLegacyWorkflowStepSnapshot` returns `undefined`
on PG (legacy snapshot exists only in pre-migration SQLite).
- `readRawProjectSettings` / `listWorkflowPromptOverridesForProject` —
now read via the async layer.
These store methods became **async**; engine/dashboard callers await
them (the workflow persistence interfaces already accepted
`Promise`-returning impls).
**PG gotcha encoded in the fixes:** migration `0006_project_ownership`
rebuilds every project-schema PK to lead with `project_id`, so
column-list `ON CONFLICT` inference fails (42P10) — upserts target the
PK by constraint name.
## Surface Enumeration
- Creators through `atomicCreateTaskJson`: `refineTaskImpl`,
`duplicateTaskImpl` (fixed); `_createTaskInternalImpl` unaffected
(already routed).
- Verification-cache callers (all merger, all 3 sites now awaited).
- Run-branch/step-instance callers: executor persistence adapters,
parse-steps foreach probe, integration-queue flip, crash-resume
reconcile, graph-reset cleanup; triage replan cleanup; dashboard
spec-rebuild pin clears; agent-reflection rework summing — all awaited.
- Audit classified everything else as guarded or sync-mode-only (dead in
production — every entry point constructs stores via
`createTaskStoreForBackend`).
## Symptom Verification
- **Original symptoms:** refinement/duplicate creation threw; merge
verification threw; workflow checkpoints/branch progress/plugin
hooks/task-column lookups silently no-oped on PostgreSQL.
- **Exact reproduction:** `refine-duplicate-task.pg.test.ts`,
`verification-cache.pg.test.ts`, and
`sync-db-residue-backend.pg.test.ts` exercise each surface against
embedded-PostgreSQL backend-mode TaskStores.
- **Assertion it is gone:** all suites pass (14 + 5 tests), plus
`transition-pending-and-status-clear.pg.test.ts`,
`create-task-reserved-id.pg.test.ts`, dashboard `routes-github.test.ts`
(123), engine `triage.test.ts` (221) and `agent-reflection.test.ts`
(31). Core/engine/dashboard typecheck fully clean: the 13 errors from
the FN-8142 pi SDK migration are fixed by bumping
@earendil-works/pi-ai/pi-coding-agent to ^0.80.10 (FN-8142 used APIs
absent from the previously locked 0.80.6). Locally green: `pnpm
verify:fast` (scoped typecheck + build + CLI build + boot smoke), `pnpm
test:gate`, and `pnpm lint`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed refinement/duplication task creation in PostgreSQL-backed
backend mode.
* Improved backend-mode persistence for workflow checkpoints,
foreach-step instances, branch progress, and cleanup flows (including
retries/resets/transitions), so stored data reliably round-trips.
* Hardened backend-mode reads for workflow steps, task columns, project
settings, and prompt overrides.
* Made verification-cache reads/writes complete reliably, including
command-specific cache behavior.
* **Tests**
* Added PostgreSQL integration/regression coverage for
refinement/duplication, sync residue, and verification caching.
* **Chores**
* Bumped `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent`
to `^0.80.10`.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Applying the full schema baseline on every fresh test database cost ~530ms
per file and dominated PG gate wall-time under fork contention. Apply the
schema once per worker process into a pid-keyed template database, then
create each test DB via CREATE DATABASE ... TEMPLATE (fast server-side copy).
Dead-pid templates from crashed/prior runs are swept before creating a new
one; template copies are serialized since CREATE DATABASE ... TEMPLATE forbids
concurrent access to the source. Removes the throwaway probe2 timing test.
Fusion-Task-Id: FN-8146
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## What
FN-8141 follow-up 3. Tightens `deriveExecutorSignalMemory`
(packages/engine/src/overseer-noop-finalize-veto.ts) so a mid-execution
`progressing` overseer observation can no longer clear the executor
no-op-finalize veto.
## Why
The prior derivation took the **newest** executor `observe` entry and
cleared `incompleteWork` whenever it was anything but the canonical
failed reason. But the planner overseer emits a `progressing`
observation ("Task is actively executing in-progress work") the
**moment** a task re-enters execution — long before that execution
finishes. The defeating shape:
> task parks failed-incomplete → requeued → re-execution starts
(overseer observes `progressing`) → execution dies or reverts again
**without** a newer failed observation → newest observation is
`progressing` → `incompleteWork:false` → an empty no-op finalize is
**not** vetoed → the reverted branch launders into `done`.
`progressing` is not "completed green" — the veto's own contract says
the failure must be superseded by a green completion.
## Change
The executor stage in `planner-overseer.ts` emits only
`progressing`/`failed`/`stuck`/`blocked` — **no** green-completion
observation — so the timeline alone cannot distinguish progressing from
completed. Per the follow-up spec, the derivation now:
1. Scans the executor `observe` timeline for the newest
**failed-with-incomplete-work** observation.
2. Keeps `incompleteWork` TRUE unless a durable **clean-completion
task-log marker** is **strictly newer** than that failure park. Reuses
the shared `CLEAN_COMPLETION_MARKERS` set (now exported from
`@fusion/core`, single-sourced with
`evaluateCompletedPromotionFailureProvenance`) so it automatically
tracks sibling follow-up F2's removal of the promotion-output marker.
3. Fails safe on a malformed failure timestamp (stays vetoed).
4. `merger-ai.ts` threads `task.log` into the derivation.
All existing precedence rules are preserved: non-empty merges are never
vetoed; human-control deferral (user-paused / autoMerge:false) still
defers; a missing task fails open.
## Test evidence
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/overseer-noop-finalize-veto.test.ts
src/__tests__/merger-ai.test.ts` → **59 passed**. New/updated cases:
failed→progressing (no completion) ⇒ still vetoed (the regression this
fixes); progressing between two failed parks (FN-8141 timeline) ⇒
vetoed; clean-completion marker newer than failure ⇒ not vetoed; older
completion marker ⇒ still vetoed; no failure park ⇒ not vetoed; bounded
tail-scan preserved. Integration: empty lane with
failed-then-progressing timeline blocks the finalize; genuinely
re-executed green task finalizes to done.
- `pnpm --filter @fusion/core exec vitest run
src/__tests__/completed-promotion-failure-provenance.test.ts` → 9
passed.
- `pnpm --filter @fusion/engine exec tsc --noEmit` → clean. `pnpm
--filter @fusion/core exec tsc --noEmit` → clean.
- `pnpm verify:fast` → PASS (3 steps green).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved empty-merge finalization safeguards so an in-progress task
cannot incorrectly clear a previously detected incomplete-work failure.
* Finalization can now proceed when a newer clean-completion record
confirms successful completion.
* Added bounded task-history evaluation to ensure completion records are
interpreted safely and consistently.
* **Tests**
* Expanded coverage for progressing, failed, and clean-completion task
timelines.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus <noreply@anthropic.com>
## What
FN-8141 follow-up 2. Removes `"Auto-recovered: task work was complete
but stranded"` from `CLEAN_COMPLETION_MARKERS` in
`packages/core/src/completed-promotion-failure-provenance.ts`.
Clean-completion evidence is now **execution outcomes only**: `"Task
marked done by agent"` (accepted explicit fn_task_done, also covers the
PREMISE STALE skip-then-done flow) and `"All steps complete — implicit
fn_task_done"` (implicit-completion success).
## Why
That string is the PROMOTER'S OWN OUTPUT — self-healing's
`recoverCompletedTasks` (executor.ts:4594) narrating "I promoted this
task" — not evidence of a clean execution outcome. Any task whose
durable log contains a promotion written by the pre-#2257 buggy sweep
(the real FN-8141 row, or any pre-guard history) carried a permanent
"clean" marker: the tail scan hit the promotion line before the older
failure park and returned not-blocked, re-enabling the exact laundering
the guard exists to stop.
Audit confirmed no other genuine execution-outcome success markers are
missing — the PREMISE STALE accepted `fn_task_done` writes the
already-listed `"Task marked done by agent"` line (executor.ts:14939),
and the honest-blocked exit (`BLOCKED: ...`) is correctly NOT counted.
`grep` confirmed the removed string has only one other consumer: its
writer at executor.ts:4594. A task already promoted to in-review/done is
out of the promoters' todo/in-progress scan scope, so
legitimately-recovered old tasks are not wedged (verified by test rather
than assumed).
## Test evidence
- Core `completed-promotion-failure-provenance.test.ts`: **11 passed** —
added pre-fix-history shape (failure park → promoter recovery line →
blocked), promoter-line-alone → blocked, and positive coverage of each
remaining marker.
- Engine `self-healing.test.ts`: **405 passed** — added promoter
withholds on the pre-fix-history shape and emits the existing
`task:reconcile-stranded-completed-no-action` (reason
`failure-provenance`) event.
- `pnpm --filter @fusion/engine exec tsc --noEmit`: clean.
- `pnpm verify:fast`: PASS (3 steps green, no tests run).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Prevented failed tasks with prior failure history from being
automatically promoted.
- Ensured recovery messages cannot override authoritative failure
records or be mistaken for successful completion.
- Preserved the existing no-action behavior and audit event when
promotion is blocked by failure provenance.
- **Tests**
- Added regression coverage for failed-task promotion and stranded-task
recovery scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus <noreply@anthropic.com>
## What
Follow-up 1 to the FN-8141 guard series (#2254–#2260). Makes the honest
`fn_task_done(outcome="blocked")` park (`status:"failed"`,
`error:"BLOCKED: <reason>"`, blockedBy → dependencies, added in #2256)
**survive the graph-teardown machinery** that bounced FN-8141's failed
park back to `todo`.
## Why
In the original FN-8141 incident, the executor's parked-failed state did
not stick: the pause-abort classifier and the workflow-graph failure
handler either rehomed the task to `todo` (clearing `status`/`error`) or
overwrote the distinctive `BLOCKED:` error with a generic "Workflow
graph terminated with failure" string. #2256 added the blocked exit but
nobody proved the park survives that bounce. Any path that
clears/overwrites the marker re-opens the laundering hole, because
self-healing (#2257/#2260) and dependency-gated scheduling key off
exactly that `BLOCKED:` error plus the recorded `blockedBy`
dependencies.
`handleGraphFailure` now detects a live blocked park (`status ===
"failed" && error.startsWith("BLOCKED:")`) **before every other
classifier** and honors it, following the existing non-graph honor-park
precedent (executor `~12163`):
- no requeue to `todo`, no engine-internal auto-continue, no `BLOCKED:`
error overwrite;
- clears the in-memory pause-abort marker so
`recoverPausedAbortFailures` has nothing to chase;
- **releases the worktree / `maxWorktrees` slot** (FN-6782 leaked-holder
precedent — the graph `finally` does not delete `activeWorktrees`);
- leaves `status`/`error`/`column`/`dependencies`/steps untouched.
Unblocking still works: the operator requeue (`moveTask`
in-progress→todo, `moves.ts ~628`) and `buildManualRetryResetPatch`
clear the `BLOCKED:` error; the guard keys off the **live** error, so a
cleared row is never re-wedged, and dependency-gated scheduling leaves
the parked row untouched while `blockedBy` deps are unmet.
## Surfaces covered
Pause-abort classifier (hard-cancel), engine-internal auto-continue, and
the plain terminal graph-failure sink — all routed through
`handleGraphFailure`, so a single top-of-method guard composes across
them.
## Test evidence
Extended `executor-task-done-blocked.test.ts` (drives
`handleGraphFailure` against a live blocked park):
- honors the park under a hard-cancel pause-abort bounce (no requeue /
clear / auto-continue);
- honors it under a plain terminal graph failure (sink never overwrites
`BLOCKED:`);
- releases the worktree/concurrency slot + clears the pause-abort
marker;
- NON-blocked failed park keeps existing behavior (guard scoped to
`BLOCKED:`);
- a cleared (unblocked) row is NOT re-honor-parked.
```
pnpm --filter @fusion/engine exec vitest run src/__tests__/executor-task-done-blocked.test.ts → 13 passed
pnpm --filter @fusion/engine exec vitest run executor-paused-abort-todo-benign + executor-graph-requeue-gate → 53 passed
pnpm --filter @fusion/engine exec tsc --noEmit → clean
pnpm verify:fast → PASS (3 steps green)
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus <noreply@anthropic.com>
## What & why
**FN-8141 laundered a failed task into `done` with zero net changes and
no sign-off.** After the executor's
`bulk-step-completion-without-review` refusal fired (steps had no
APPROVE verdicts), the agent used the sanctioned skip affordance
(`fn_task_update status="skipped"`) on the remaining unreviewed steps.
Because every completion check counts `skipped` as complete, the task
then satisfied the exact condition the refusal was protecting, and
downstream **automatic** promotion (implicit `fn_task_done`,
self-healing `recoverStrandedCompletedTodoTasks`) moved it to in-review
— where the AI merger found an empty diff and finalized it as a no-op
`done`.
This PR restores the invariant: **steps skipped while a
bulk-step-completion refusal marker is active on the task are "tainted"
and cannot carry the task to review through any automatic path.** The
taint clears on an honest exit — an accepted `fn_task_done` (explicit or
non-tainted implicit) or an operator manual retry — so the legitimate
`PREMISE STALE` skip-then-done flow is unaffected.
## Design
- **Persisted marker**: new nullable `Task.bulkCompletionRefusalAt` (ISO
timestamp), stamped when the `bulk-step-completion-without-review`
refusal fires (explicit `fn_task_done` handler + implicit
`handleImplicitTaskDoneRefusal`). Survives requeue so a refusal on
attempt N taints attempt N+1's promotion. Full store plumbing (types,
descriptors, serialization, SQLite/PG schema + health self-heal).
- **Pure evaluator** `evaluateSkipBypassTaint(task)` in `@fusion/core`
(next to `evaluateNoCommitsNoOpFinalize`): `blocked` iff the marker is
set AND ≥1 step is `skipped`. Single rule every AUTO-promotion check
calls.
- **Clearing**: accepted explicit `fn_task_done`, accepted
implicit/retry completion (the success-reset `updateTask`s), and
`buildManualRetryResetPatch` (operator retry). A fresh lifecycle that
genuinely re-does the work leaves zero skipped steps, so it is never
blocked even if a marker lingers.
## Surface enumeration (every consumer of "all steps done/skipped" that
gates AUTO-promotion)
- **executor.ts**: `getCompletedTaskFinalizationDecision` (gated on the
`isTaskWorkComplete` branch only, never on an accepted `taskDone`);
`recoverCompletedTask` (shared chokepoint for unpause resume,
completed-task watchdog, orphan resume);
`evaluateImplicitCompletionRefusal` (both implicit-completion loops);
`isTaskAlreadyCompleteForNonContinuableSession`; graph merge-boundary
`getWorkflowMergeImplementationProofFailure`.
- **self-healing.ts**: `recoverCompletedTasks` (stuck in-progress) and
`recoverStrandedCompletedTodoTasks` (the exact FN-8141 promoter).
- **Verified-safe, left as-is**: per-step graph node projections
(executor ~6274/6298) and progress-render checks — they don't gate
whole-task auto-promotion.
## Test evidence
Scoped runs (all green):
```
CORE: pnpm --filter @fusion/core exec vitest run \
src/__tests__/skip-bypass-taint-guard.test.ts \
src/__tests__/skip-bypass-taint-persistence.test.ts \
src/__tests__/manual-retry-reset.test.ts
→ 17 passed
ENGINE: pnpm --filter @fusion/engine exec vitest run \
src/__tests__/executor-skip-bypass-taint.test.ts \
src/__tests__/self-healing.test.ts
→ 401 passed
```
Coverage: pure-evaluator (skip-before-refusal counts, skip-after-refusal
doesn't, taint-clearing, empty-marker/empty-steps edges); store
round-trip of the marker (set→read→clear); executor white-box (implicit
completion refused when tainted, allowed when clean or fully re-done,
graph merge-boundary reports missing proof, and the **explicit
`fn_task_done` PREMISE-STALE honest exit stays accepted**); self-healing
(FN-8141 sequence does not promote from either recovery path; a clean
legitimately-skipped task still promotes); manual-retry clears the
marker.
## Note on `pnpm verify:fast`
`verify:fast` currently fails at the workspace-artifact bootstrap on
**pre-existing** pi-SDK type errors in
`packages/engine/src/{auth-storage,pi,provider-registration}.ts` — the
FN-8145 upstream migration breakage (pi 0.80.x removed
`AuthStorage`/`ModelRegistry.create`). **None of those files are in this
diff.** `@fusion/core` builds clean (`packages/core build: Done`), and
`@fusion/engine` `tsc` reports **no errors in the files this PR
touches** (`executor.ts`, `self-healing.ts`); the only engine build
errors are the FN-8145 files. This base failure is the same condition
FN-8141 describes and is out of scope for this task.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus <noreply@anthropic.com>
## What & why
FN-8141 (\"Update pi SDK … verify Kimi K3 end to end\") was **laundered
into `done`** despite producing zero net changes. The executor reverted
the impossible work 5 times; the planner overseer emitted
`stage=executor signal=failed` (\"Executor stage parked failed with work
incomplete\") **twice**, then an hour later — because the overseer is
**stage-scoped and memoryless** — classified the same task `stage=merger
signal=progressing` and let the AI merger's **empty no-op finalize**
promote it to `done`. No reviewer ever saw it (skipped steps request no
review; the merge-review pass reviews an empty diff).
This restores the invariant: **a task whose most-recent executor signal
is failed-with-incomplete-work, with no subsequent green completion,
must not reach `done` via a zero-diff no-op merge finalize.**
## Change
Two pure, unit-testable, never-throw functions
(`packages/engine/src/overseer-noop-finalize-veto.ts`), following the
FN-7514 `evaluateOverseerHumanControl` precedent (pure predicate +
ids/outcomes-only audit metadata):
- **`deriveExecutorSignalMemory`** — reconstructs the most-recent
executor signal from the **durable `overseer:intervention` timeline the
overseer already writes** (no new persisted column / migration; \"the
existing oversight state storage the controller uses\"). A later green
executor observation supersedes an earlier failure, which is how \"no
subsequent execution completed green\" is derived. Keys on the
newly-exported `EXECUTOR_FAILED_INCOMPLETE_REASON` constant (already
load-bearing for FN-7577 feed dedup) as the single source of truth.
- **`evaluateNoOpFinalizeExecutorVeto`** — the veto decision.
Wired into the AI **empty-merge lane** (`merger-ai.ts`), composed with
(and independent of) the FN-6461 no-commits guard: on veto it sets
`error`, writes a durable log entry, emits
`overseer:no-op-finalize-vetoed-failed-executor`, and moves the task
back to `todo` with progress preserved — mirroring the FN-6461 blocked
lane. The move-to-todo transition takes the task out of the merge lane,
so the event isn't re-emitted every poll (equivalent to the
`overseer:oversight-withheld-human-control` per-(taskId, reason) dedup).
Independent of the sibling Task 2 merger-layer lineage guard: both can
fire; **either alone stops FN-8141**.
## Surface enumeration
- **Empty (zero-diff) no-op merge** — vetoed when memory is
failed-incomplete. ✅
- **Non-empty (real squash landed) merge** — **never** vetoed
(reviewers/merge review cover real diffs). ✅
- **failed-incomplete then later green execution** — memory superseded →
no veto. ✅
- **No executor memory / store lacks the async reader** — fail open (no
veto); FN-6461 + sibling guard remain the safety net. ✅
- **user-paused / autoMerge:false / approval-blocked** — defers to
FN-7514 human-control; never fights those semantics. ✅
- **Timeline entry filtering** — only `stage=executor` +
`action=observe` entries count as signals (retry/escalate messages
ignored). ✅
## Test evidence
`pnpm --filter @fusion/engine exec vitest run
src/__tests__/overseer-noop-finalize-veto.test.ts
src/__tests__/merger-ai.test.ts --silent=passed-only --reporter=dot`
```
Test Files 2 passed (2)
Tests 48 passed (48)
```
Covers: derivation (failed→veto, failed-then-green→no-veto,
non-executor/non-observe ignored, empty→null); evaluator (veto, green,
no-memory, non-empty never-vetoed, user-paused defer, autoMerge:false
defer, missing-task fail-open); and an engine integration test driving
an **FN-8141-shaped** empty merge through `runAiMerge` → asserts
move-to-todo + `overseer:no-op-finalize-vetoed-failed-executor` audit
event + main untouched, plus the later-green case finalizing done.
`@fusion/core` builds clean (`pnpm --filter @fusion/core build`).
**Note on `pnpm verify:fast`:** it currently fails to build
`@fusion/engine`, but **only** in `auth-storage.ts` / `pi.ts` /
`provider-registration.ts` — the pre-existing pi-SDK breakage that *is*
this incident (pi 0.80.x removed
`AuthStorage`/`ModelRuntime`/`ModelRegistry`; tracked as FN-8145).
Verified identical errors with my changes stashed; **my diff touches
none of those files and adds zero new type errors** (tsc reports all
program errors before failing — none were in my files).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Prevented zero-change tasks from being incorrectly finalized when the
latest executor attempt failed with unfinished work.
- Preserved task progress and returned affected tasks to **Todo** for
continued processing.
- Allowed finalization to proceed after a subsequent successful executor
result.
- Maintained existing human-control and non-empty merge behavior.
- Added audit visibility for blocked finalization events.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus <noreply@anthropic.com>
## What / Why
**Reworked after FN-8179 (`fd43a57a4`) landed on `main`.** FN-8179 did
most of what the original PR #2261 did — pinned `@earendil-works/pi-ai`
/ `pi-coding-agent` to `^0.80.10` everywhere, added the
`createSessionOptions` `NonNullable` typing in `pi.ts`, and regenerated
`pnpm-lock.yaml`. This PR was rebased onto current `origin/main` and
reduced to **only the unique residual not covered by FN-8179**.
### Residual change 1 — defensive `getAuth` guard
The FN-8142 migration rewrote `attachSessionRoutingHeaders` from the
`ModelRegistry.getApiKeyAndHeaders` seam to `ModelRuntime.getAuth`, but
dropped the pre-migration defensive invariant: a missing resolution
method must **not** break session creation. On `main` the function now
calls `modelRuntime.getAuth.bind(...)` unguarded, which throws if
`getAuth` is ever absent.
This restores the guard: no-op (warn) when `getAuth` is missing, so a
future pi rename degrades to un-tagged requests instead of a hard
failure at every agent start.
### Residual change 2 — test realignment (needed against current main)
FN-8179 aligned the SDK but did **not** update the two `#1675`
routing-header test suites, which still asserted the old
`getApiKeyAndHeaders` seam. **Verified RED on current `main` before
touching them:**
- `pi-create-fn-agent.test.ts` — **60 / 104 failing** (mock had no
`ModelRuntime` export; `createAgentSession` now receives
`modelRuntime`).
- `pi-session-routing-headers.test.ts` — **4 / 5 failing**
(`attachSessionRoutingHeaders` signature is `getAuth`, not
`getApiKeyAndHeaders`).
Both are realigned to the `ModelRuntime.getAuth` seam (the mock gains
the `ModelRuntime` export) → **109 / 109 green**. Assertions were
strengthened to the new behavior, not weakened; the #1675 precedence
invariant (taskId > pi session id > no-wrap), header merge,
apiKey/provider-header passthrough, failed/undefined passthrough, and
absent-method no-op are all still asserted.
## Surface enumeration
- **Routing-header seam**: both the `createFnAgent` path and the
`attachSessionRoutingHeaders` unit (taskId / pi-session-id / no-id
precedence; header merge; apiKey + provider-header passthrough;
failed/undefined resolution; absent-method no-op).
- **Both mock forms** in `pi-create-fn-agent.test.ts`: the top-level
`vi.mock` and all three `vi.doMock` skill-selection blocks now export
`ModelRuntime`.
## Test evidence
- `pnpm --filter @fusion/engine exec tsc --noEmit` → **exit 0**.
- `pnpm --filter @fusion/engine exec vitest run` on both suites →
**109/109 pass** (was 64 failing on main).
- `pnpm verify:fast` → **PASS** (typecheck + build scoped to changed
packages + CLI build + boot smoke `GET /api/health 200`; no tests run).
Do not merge without CI. No release performed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved session creation reliability when authentication support is
unavailable.
* Preserved existing authentication details while adding session-routing
headers when supported.
* Prevented session creation from failing when authentication
information cannot be resolved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus <noreply@anthropic.com>
Allow mobile issue lists to fill the available import-sheet height.
- Make the mobile import pane a constrained flex column with internal scrolling.
- Remove the 50vh list cap while preserving pagination and action access.
- Cover the responsive list sizing rules with a regression test.
Files changed:
.../dashboard/app/components/GitHubImportModal.css | 18 +++++++++++++++++-
.../components/__tests__/GitHubImportModal.test.tsx | 17 +++++++++++++++++
2 files changed, 34 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-8150
Fusion-Task-Lineage: 92841921-7769-4304-b1f3-8c91aa18efb6
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>