Agents that must edit files beyond a task's declared ## File Scope had no
way to keep the scope in sync, so those edits were stranded at merge (the
squash merge is scoped to ## File Scope, and cross-task overlap blocking +
the merge file-scope invariant both read it).
New executor tool fn_task_file_scope_add validates repo-relative
paths/globs with isValidFileScopeEntry, de-dupes against existing scope,
appends them to the ## File Scope section of PROMPT.md, and persists via
store.updateTask({ prompt }) (same validation + task.json/PROMPT.md sync as
fn_task_prompt_write). Registered in the main coding-agent tool list; the
base executor prompt now instructs the agent to call it when editing beyond
the declared scope. Merge-time peer-claim refusal is unchanged and remains
the cross-task backstop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FN-7690 changed resolveApiType() to return the registered pi-ai key
"anthropic-messages" for anthropic-compatible providers (the bare
"anthropic" key is never registered and throws at stream time), but left
behind a stale JSDoc and a stale test expectation:
- custom-provider-registry.ts: update the FN-7689 buildCustomProviderModels
comment that still described the anthropic/anthropic-messages drift as
unresolved.
- provider-registration.test.ts: assert config.api === "anthropic-messages"
(was still asserting the pre-fix "anthropic").
Also de-slow a retry-exhaustion test: the describe uses fake timers with
shouldAdvanceTime, so awaiting a 3-retry backoff (1s+5s+15s) burned ~21s of
real wall time. Drive the backoff with advanceTimersByTimeAsync instead
(Standing Rule: prefer fake timers over real time waits).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OAuth expiry monitor and validity logger iterated the un-aliased
getOAuthProviders() id `anthropic` and evaluated get("anthropic"), which
can resolve to a stale legacy/supplemental row (e.g. ~/.pi/agent/auth.json)
even when the fresh, actually-used token lives under `anthropic-subscription`.
That fired a false "Anthropic OAuth expired" notification while the real
subscription token had refreshed successfully.
Both surfaces now resolve the freshest of the two aliased ids via a shared
resolveEffectiveOAuthCredential helper (mirroring the refresh scheduler's
getRefreshCandidateIds alias handling), so a live subscription token
suppresses the false alert. Notification throttle/cadence unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an end-to-end integration test proving the artifact pipeline works against a real TaskStore, not just a mocked one.
- New test file exercises createArtifactRegisterTool/createArtifactListTool/createArtifactViewTool bound to a real TaskStore (inMemoryDb, real filesystem writes) instead of a mocked store
- Pins the register -> list -> view invariant for a real base64 PNG image artifact, verifying disk persistence, SQLite row fields (type, mimeType, sizeBytes, uri, taskId), and the list/view text surfaces
- Pins the invalid-base64-payload rejection path (non-image bytes for an image-typed artifact) to confirm no artifact row is persisted
- Pins the empty-state list text for a task with no registered artifacts
Files changed:
packages/engine/src/__tests__/agent-artifact-tools-real-store-integration.test.ts | 131 +++++++++++++++++++++
1 file changed, 131 insertions(+)
Fusion-Task-Id: FN-7693
Fusion-Task-Lineage: fd3493aa-6fb2-4e18-a735-c4a9d87c9c6c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
The 'publishes failed terminal workflow step activity' test awaited executeAll()
directly while the executor retried a failing step 3x with sleep() delays. Under
useFakeTimers({ shouldAdvanceTime: true }) those sleeps consumed REAL wall-clock
time (~22.6s locally, ballooning under CI load and busting the shard-2 watchdog).
Fast-forward the retry sleeps via vi.advanceTimersByTimeAsync like sibling retry
tests; the loop now completes in milliseconds.
Correct the planner-oversight confirmation messaging so it no longer claims a hard block when the active auto-merge policy will actually advance the merge/pull-request stage unattended.
- decidePlannerRecovery accepts an additive, messaging-only `autoMergeWillProceed` flag and picks accurate reason wording (advisory vs. genuine human-approval block vs. neutral/unknown) for merger/pull-request await_confirmation decisions
- PlannerRecoveryController.tick threads `allowsAutoMergeProcessing(task, settings)` into decidePlannerRecovery as `autoMergeWillProceed`
- project-engine's requestConfirmation steering comment prefix changed from "confirmation required" to neutral "merge checkpoint" so it doesn't contradict the now-accurate reason text
- added regression tests in planner-recovery.test.ts and planner-overseer-intervention-wiring.test.ts
- added changeset and doc note
Files changed:
.changeset/fn-7692-merger-confirmation-copy.md | 7 +++
docs/architecture.md | 10 +++-
packages/core/src/__tests__/planner-recovery.test.ts | 66 ++++++++++++++++++++++
packages/core/src/planner-recovery.ts | 36 +++++++++++-
packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts | 37 ++++++++++++
packages/engine/src/planner-recovery-controller.ts | 14 ++++-
packages/engine/src/project-engine.ts | 11 +++-
7 files changed, 176 insertions(+), 5 deletions(-)
Fusion-Task-Id: FN-7692
Fusion-Task-Lineage: 187684b8-1d24-425d-85d4-627587469908
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
A Staff Engineer pre-landing review (Greptile/CodeRabbit) on #1957
(merged) flagged four structural issues. This PR fixes the three that
were confirmed still present on `main`; the fourth (an unregistered-rule
`eslint-disable-next-line react-hooks/exhaustive-deps` comment) was
already fixed in #1957's second commit before merge and needed no
further change.
1. **`resolvePluginRuntime()` mislabeled "found but failed to init" as
`not_found`.** When a `runtimeHint` plugin registration is found but
`pluginContext`/`createRuntimeContext(...)` comes back falsy, the
resolver returned `reason: "not_found"` — indistinguishable from "never
registered" — defeating the point of a distinct `FallbackReason`. Now
returns `reason: "init_error"`. Updated the existing test that wrongly
asserted `"not_found"` for this path, and added a new test asserting all
three reachable `FallbackReason` values (`not_found`, `init_error`,
`factory_error`) are pairwise distinct.
2. **`ActiveAgentsPanel.tsx`/`AgentsView.tsx` hardcoded
`isInViewport={true}`.** Every agent card (live-agent header, board
card, list card) polled the runtime-fallback endpoint every 30s forever,
even scrolled off-screen — unlike `TaskCard.tsx`'s correct
`IntersectionObserver`-gated pattern. Both files now thread a real
`IntersectionObserver`-backed viewport signal into
`RuntimeFallbackBadge`. Added regression tests proving polling stops
once a badge instance's `isInViewport` transitions to `false` and
resumes once it goes back to `true` (desktop + a mobile-breakpoint
variant), plus verified via `tsc --noEmit` for `@fusion/dashboard`.
3. **Toast dedupe was per-hook-instance, not shared.**
`useRuntimeFallbackStatus`'s `lastToastedEventIdRef` was a local
`useRef`, so the same task rendered simultaneously in two card surfaces
(e.g. `ActiveAgentsPanel` + `AgentsView`) fired two separate toasts for
one fallback event. Dedupe now lives in module-level shared state (a
bounded `Map` keyed by `taskId:eventId`, FIFO-evicted past 500 entries)
so a fallback event toasts exactly once across every
simultaneously-mounted badge instance for the same task. Added a
cross-instance regression test mounting two badges for the same
`taskId`/`eventId` and asserting exactly one toast fires.
## Test evidence
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/runtime-resolution.test.ts --reporter=dot` — 25/25 pass
- `pnpm --filter @fusion/dashboard exec vitest run
app/components/__tests__/RuntimeFallbackBadge.test.tsx --reporter=dot` —
11/11 pass
- `pnpm --filter @fusion/dashboard run typecheck` — clean
- `pnpm --filter @fusion/engine run typecheck` — clean
## Scope
Isolated 6-file diff on top of current `main`
(`packages/engine/src/runtime-resolution.ts`,
`packages/engine/src/__tests__/runtime-resolution.test.ts`,
`packages/dashboard/app/hooks/useRuntimeFallbackStatus.ts`,
`packages/dashboard/app/components/ActiveAgentsPanel.tsx`,
`packages/dashboard/app/components/AgentsView.tsx`,
`packages/dashboard/app/components/__tests__/RuntimeFallbackBadge.test.tsx`).
No behavior outside the three findings above was touched.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- The desktop dashboard now supports plugin-backed runtime features,
improving how plugin-enabled workflows are loaded and run.
- Agent cards now pause background fallback polling when they’re
off-screen, helping the dashboard feel smoother and more responsive.
- **Bug Fixes**
- Improved runtime fallback handling so missing runtimes and
initialization failures are reported more accurately.
- Toast notifications are now better deduplicated, reducing repeated
alerts when multiple views show the same fallback state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Custom providers previously never enabled prompt-cache control, so agent turns re-billed the full context every request even on cache-capable backends.
- Add `CustomProvider.anthropicPromptCaching` opt-in flag in @fusion/core types
- Set pi-ai `compat.cacheControlFormat="anthropic"` on opted-in models in both registration paths: custom-provider-registry `toProviderConfig` and pi.ts `createFnAgent`
- Expose the new toggle in the dashboard CustomProvidersSection UI (with supporting CSS) and thread it through the legacy API + custom-provider routes
- Update docs (dashboard-guide, settings-reference) to document the new setting
- Add engine test coverage for the caching flag across provider registration and pi-create-fn-agent paths
- Add changeset for the fix
Files changed:
.changeset/fn-7689-custom-provider-prompt-caching.md | 7 +
docs/dashboard-guide.md | 1 +
docs/settings-reference.md | 2 +-
packages/core/src/types.ts | 15 ++
packages/dashboard/app/api/legacy.ts | 12 ++
packages/dashboard/app/components/CustomProvidersSection.css | 24 +++
packages/dashboard/app/components/CustomProvidersSection.tsx | 58 +++++-
packages/dashboard/src/routes/register-custom-provider-routes.ts | 16 ++
packages/engine/src/__tests__/pi-create-fn-agent.test.ts | 71 +++++++
packages/engine/src/__tests__/provider-registration.test.ts | 204 ++++++++++++++++++++-
packages/engine/src/custom-provider-registry.ts | 71 +++++--
packages/engine/src/pi.ts | 27 ++-
12 files changed, 473 insertions(+), 35 deletions(-)
Fusion-Task-Id: FN-7689
Fusion-Task-Lineage: b4f88f32-50da-4651-a546-432a95a1ab1c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Start OAuthRefreshScheduler before the refresh-blind OAuthExpiryMonitor so a
stale-but-refreshable access token is renewed before the monitor's first
awaited check() reads `expires`. Previously the monitor fired a false
"OAuth token expired" ntfy push on startup, moments before the refresher
silently renewed the token. Ordering locked by an invocationCallOrder
assertion in project-engine.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Agents were composing plans (e.g. reboot/wait-and-retry loops) that assumed they could keep acting even after the Fusion platform itself shut down, since prompts never told them they run inside Fusion. This adds a shared, docs-grounded self-awareness preamble prepended to chat, heartbeat, and executor base prompts so agents know their own runtime constraints.
- Added FUSION_RUNTIME_SELF_AWARENESS shared preamble in packages/core/src/agent-prompts.ts, exported via packages/core/src/index.ts
- Prepended the preamble to the chat system prompt (packages/dashboard/src/chat.ts)
- Prepended the preamble to the heartbeat session prompt (packages/engine/src/agent-heartbeat.ts)
- Prepended the preamble to the executor base prompt (packages/engine/src/executor.ts)
- Updated docs/agents.md and CONCEPTS.md to document the new self-awareness/capability-grounding behavior
- Added regression tests across core, dashboard, and engine covering the new prompt content
- Added changeset for @runfusion/fusion (minor, fix category)
Files changed:
.changeset/fn-7675-agent-runtime-self-awareness.md | 7 ++++
CONCEPTS.md | 4 +-
docs/agents.md | 17 ++++++++
packages/core/src/__tests__/agent-prompts.test.ts | 41 ++++++++++++++++++++
packages/core/src/agent-prompts.ts | 32 ++++++++++++++-
packages/core/src/index.ts | 1 +
packages/dashboard/src/__tests__/chat-system-prompt.test.ts | 17 ++++++++
packages/dashboard/src/chat.ts | 6 ++-
packages/engine/src/__tests__/executor-prompt.test.ts | 45 ++++++++++++++++++++++
packages/engine/src/__tests__/heartbeat-session-prompt.test.ts | 35 +++++++++++++++++
packages/engine/src/agent-heartbeat.ts | 10 +++--
packages/engine/src/executor.ts | 7 +++-
12 files changed, 213 insertions(+), 9 deletions(-)
Fusion-Task-Id: FN-7675
Fusion-Task-Lineage: 126d04a6-2c68-4347-9789-591b274277bf
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Closes/relates to Runfusion/Fusion#1956.
## Summary
Surfaces silent runtime-resolution fallback in the dashboard, and
threads the real `FallbackReason` ("not_found" vs "factory_error")
through instead of hardcoding `"not_found"` for every fallback.
## Changes
- `packages/engine/src/runtime-resolution.ts`: `resolvePluginRuntime()`
now returns a tagged miss result (`{ ok: false, reason }`)
distinguishing "not found" from "factory/instantiation error" instead of
collapsing both to `null`. `resolveRuntime()` threads the real reason
through to `logRuntimeFallback(...)` and returns it via
`ResolvedRuntime.fallbackReason`.
- `packages/engine/src/agent-session-helpers.ts`:
`createResolvedAgentSession()` includes `fallbackReason` in the
`session:runtime-resolved` audit event metadata when present.
- `packages/dashboard/src/routes/register-task-workflow-routes.ts`: new
`GET /api/tasks/:id/runtime-fallback` endpoint, returning the most
recent `session:runtime-resolved` event normalized for UI consumption
(`wasConfigured`, `runtimeHint`, `reason`, `showFallbackBadge`).
- `packages/dashboard/app/hooks/useRuntimeFallbackStatus.ts` (new):
polls the endpoint, dedupes toast firing per audit-event-id.
- `packages/dashboard/app/components/RuntimeFallbackBadge.tsx` (new):
renders the badge + fires the toast; wired into `TaskCard.tsx`,
`ActiveAgentsPanel.tsx`, and `AgentsView.tsx` (board and list variants).
## Test plan
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/runtime-resolution.test.ts` — 24/24 pass (21 pre-existing
+ 3 new, none weakened)
- `pnpm --filter @fusion/dashboard exec vitest run
src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts`
— 5/5 pass
(empty/configured-ok/fallback-with-hint/fallback-blank-hint/stale-superseded
states)
- `pnpm --filter @fusion/dashboard exec vitest run
app/components/__tests__/RuntimeFallbackBadge.test.tsx` — 8/8 pass (all
data states + mobile breakpoint + toast-fires-once)
- `pnpm --filter @fusion/dashboard run typecheck` and `pnpm --filter
@fusion/engine run typecheck` — both clean
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added runtime-fallback warning badges across task and agent views
(including board and “working on” sections) with automatic toast
notifications.
* Introduced a new backend API to surface the latest runtime-fallback
state for a task.
* Added runtime-fallback status polling and UI messaging to reflect the
most recent state.
* **Bug Fixes**
* Prevented repeated toasts by deduplicating notifications across
polling updates.
* Improved fallback reporting so the UI reflects the latest
runtime-resolved audit event.
* Enhanced diagnostics by distinguishing fallback reasons (e.g., missing
runtime vs factory failure) for clearer user guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Narrative: FN-7673 re-attempted the engine-core gate bundle lever with a single combined-entry engine-graph design (all 14 mock-safe roots redirected through a resolveId plugin to one synthetic packages/engine/.gate-bundle/engine.mjs) after FN-7670's 14-separate-root attempt was inconclusive. This update records the negative A/B result and closes the lever.
- Documented that the combined-entry design achieved its structural goal (149 first-party inputs -> 1 output file) and full 335/335 coverage parity
- Recorded a true interleaved A/B (5 warm + 1 cold pair) showing the combined-entry bundle is consistently slower than the @fusion/core-only baseline (warm median +29.1%, import-phase aggregate +74.0%)
- Captured the working theory: funnelling 14 relative-import sites through a resolveId-plugin redirect to one large synthetic export-* file adds more transform/resolution overhead than it saves, unlike @fusion/core's plain resolve.alias
- Noted the experiment was NOT landed; wiring (engine-graph scans, combined-entry builder, resolveId plugin) was fully reverted
- Marked this lever (bundling the @fusion/engine relative-import graph for the engine-core gate, in either 14-file or single-combined-entry shape) as CLOSED absent new evidence
Files changed:
packages/engine/vitest.config.ts | 32 +++++++++++++++++++++++++++++---
1 file changed, 29 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7673
Fusion-Task-Lineage: 46951e5f-e7dc-4f7c-9601-0cfa0b082d70
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Removes a dead test-file reference from the engine-core vitest gate include list, with a code comment documenting why.
- Remove the nonexistent `src/__tests__/merger-post-merge.test.ts` entry from packages/engine/vitest.config.ts's engine-core include list (retired by FN-7039; graph is now sole post-merge owner)
- Add FNXC comment noting the entry matched zero files and that graph post-merge coverage lives in workflow-graph-post-merge.test.ts (engine-default)
Files changed:
packages/engine/vitest.config.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7671
Fusion-Task-Lineage: 73447412-7b8a-4578-a2b8-07f83e381548
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Root-causes 4 correlated CTO-report agent failures where durable non-ephemeral agents got stuck in `error` state indefinitely because the heartbeat scheduler stops ticking error-state agents entirely, and self-healing's recovery sweep previously only considered them when their manager row was missing.
- SelfHealingManager: scope the `managerMissing` gate to the "running" orphan-detection path only, so "error"-state durable agents with a present/active manager now fall through to the existing transient/operator-actionable/active-execution/cooldown/retry-budget recovery guards instead of being skipped outright
- Add FNXC:AgentHeartbeat comment documenting the FN-7672 incident and rationale for the scoping change
- Extend self-healing.test.ts with coverage for manager-present durable agents in error state
- Add changeset (patch) describing the fix for release notes
- Update docs/agents.md accordingly
Files changed:
.changeset/fn-7672-durable-agent-recovery.md | 7 ++
docs/agents.md | 2 +
packages/engine/src/__tests__/self-healing.test.ts | 129 ++++++++++++++++++++-
packages/engine/src/self-healing.ts | 24 +++-
4 files changed, 160 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7672
Fusion-Task-Lineage: 6676dc9e-66e7-4f70-804a-cccf77e8d337
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Prototyped extending the @fusion/core pre-bundle alias lever to @fusion/engine's relative-import production graph reached by the 18 gate files, but an A/B showed no clear win over the @fusion/core-only bundle, so the change was not landed and only the rationale is recorded.
- Added an FNXC:EngineTests comment block in packages/engine/vitest.config.ts documenting the FN-7670 prototype (171 first-party files → 35 output files via esbuild multi-entry splitting)
- Recorded the negative A/B result: byte-size growth of 14 separate large root bundles offset per-file-dispatch savings, with no clear win beyond host run-to-run noise
- Left the vitest alias wiring unchanged at the @fusion/core-only bundle state, pointing future attempts to FN-7670's task docs for full analysis and to consider a single combined engine-graph entry instead of 14 separate root entries
Files changed:
packages/engine/vitest.config.ts | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
Fusion-Task-Id: FN-7670
Fusion-Task-Lineage: efd27f94-a6c4-49c7-a78e-50213fd42a24
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Prototype and land a rebuilt-every-run esbuild bundle of the @fusion/core gate-safe barrel closure, collapsing the engine-core gate's per-fork Vite SSR import-phase cost (18 forks x ~430-file closure re-resolved from scratch) into a single file load per fork.
- Add scripts/build-engine-core-gate-bundle.mjs: esbuild-bundles packages/core/src/index.gate.ts (220 first-party files, packages:"external" so third-party/node: imports stay external, treeShaking:false to preserve side effects) into packages/core/.gate-bundle/core.mjs + core.meta.json
- Wire the builder into packages/engine/vitest.config.ts's engine-core project globalSetup (alongside the existing vitest-teardown hook) so the bundle is rebuilt fresh before every gate invocation, and repoint the @fusion/core resolve.alias at the bundled output instead of index.gate.ts source
- Place the bundle output at packages/core/.gate-bundle/ as a sibling of packages/core/node_modules/ (not nested inside it) to avoid Vite SSR's external-dep heuristic, which would otherwise silently defeat vi.mock interception for imports nested in the bundle
- Gitignore packages/core/.gate-bundle/ and add a matching ESLint ignore entry so the generated bundle text is never linted or committed
- Add esbuild ^0.25.12 as a root devDependency (pnpm-lock.yaml updated accordingly)
- Document the pre-bundling rationale, placement constraints, and measured A/B wall-time results in docs/testing.md
Verified: pnpm test:gate passes (335/335 engine-core tests, 63/63 CLI ci-shape tests), engine package typecheck clean, eslint clean on touched files.
Files changed:
.gitignore | 11 ++
docs/testing.md | 3 +
eslint.config.mjs | 10 ++
package.json | 1 +
packages/engine/vitest.config.ts | 50 ++++++++-
pnpm-lock.yaml | 3 +
scripts/build-engine-core-gate-bundle.mjs | 174 ++++++++++++++++++++++++++++++
7 files changed, 247 insertions(+), 5 deletions(-)
Fusion-Task-Id: FN-7669
Fusion-Task-Lineage: 62b06b2a-4ac6-45ae-ac79-9771132bc303
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Introduces a project-scoped @fusion/core barrel used only by the engine-core
gate project, so new feature modules added to the full barrel don't silently
inflate the gate's transform/import cost.
- Add packages/core/src/index.gate.ts, a copy of the full @fusion/core barrel
minus export statements for modules added since the last re-audit baseline
(i.e. it still re-exports everything the full barrel does except newly
added, gate-irrelevant feature modules).
- Update packages/engine/vitest.config.ts to add a project-scoped
resolve.alias mapping @fusion/core -> packages/core/src/index.gate.ts for
the engine-core project only; engine-default/engine-reliability/engine-slow
and @fusion/engine continue to resolve the full barrel.
- Document the gate-safe barrel and its audit procedure in docs/testing.md.
Files changed:
docs/testing.md | 3 +
packages/core/src/index.gate.ts | 2102 ++++++++++++++++++++++++++++++++++++++
packages/engine/vitest.config.ts | 17 +
3 files changed, 2122 insertions(+)
Fusion-Task-Id: FN-7667
Fusion-Task-Lineage: 054ec89a-d973-44dd-b9ac-ad266f553f01
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Duplicate tasks created by the same agent are no longer auto-archived by default; they are flagged for review instead, controlled by a new opt-in project setting.
- Add project setting `autoArchiveDuplicateTasksEnabled` (default false) gating the FN-4892 same-agent duplicate intake path
- Add `flagSameAgentDuplicate` path and `nearDuplicateOf` metadata used when auto-archive is disabled; tombstone-resurrection blocking is unchanged
- Wire the setting through core settings schema/types/store, dashboard SchedulingSection UI, and i18n strings
- Update docs (settings-reference.md, task-management.md) to describe the new default-off behavior
- Add a changeset for the @runfusion/fusion minor release
- Extend duplicate-intake, tombstone-window, store-parent-task-dedup, and reliability-interaction tests to cover both flag states
Files changed:
$(cat /tmp/fn7658_stat.txt)
Fusion-Task-Id: FN-7658
Fusion-Task-Lineage: 7d0d1074-1020-48a8-b96f-186154c2c408
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes silent runtime fallback visibility (dashboard never read wasConfigured
or session:runtime-resolved) and threads the real FallbackReason
(not_found vs factory_error) through resolveRuntime()/logRuntimeFallback
instead of hardcoding "not_found" for every fallback.
- packages/engine/src/runtime-resolution.ts: resolvePluginRuntime() now
returns a tagged miss result distinguishing not_found from factory_error;
resolveRuntime() threads the real reason through and returns it as
ResolvedRuntime.fallbackReason
- packages/engine/src/agent-session-helpers.ts: includes fallbackReason in
the session:runtime-resolved audit event metadata
- packages/dashboard/src/routes/register-task-workflow-routes.ts: new
GET /api/tasks/:id/runtime-fallback endpoint
- packages/dashboard/app/hooks/useRuntimeFallbackStatus.ts +
packages/dashboard/app/components/RuntimeFallbackBadge.tsx: new polling
hook + badge/toast component wired into TaskCard, ActiveAgentsPanel, and
AgentsView
Ref: Fusion task FUX-022, investigations/FUX-017-hermes-runtime-fallback.md
recommendation #1
Blocks planning/intake column cards from entering processing columns regardless of literal column id, so renamed custom intake/planning columns are covered by the same guard as the legacy todo column.
- Add isUnplannedForExecution() in hold-release.ts: true when task.status==="planning", or when the card sits in the legacy todo column or a column carrying the intake trait AND its PROMPT.md still equals the bootstrap stub.
- Route issueRelease() (used by the sweep, promoteHeldTask, and releaseHeldTaskByEvent) through this guard before releasing into any countsTowardWip processing column.
- Update scheduler.ts's reserveSlot guard to use the same trait-based predicate instead of a hardcoded "todo" column id check.
- Add regression tests in hold-release.test.ts and scheduler-workflow-cutover.test.ts covering renamed intake/planning columns.
- Document the invariant in docs/architecture.md and docs/workflow-steps.md.
- Add changeset (patch) describing the fix.
Files changed:
.changeset/fn-7648-unplanned-intake-cards-never-execute.md | 7 +
docs/architecture.md | 2 +
docs/workflow-steps.md | 2 +
packages/engine/src/__tests__/hold-release.test.ts | 238 +++++++++++++++++++++
packages/engine/src/__tests__/scheduler-workflow-cutover.test.ts | 60 +++++-
packages/engine/src/hold-release.ts | 60 ++++++
packages/engine/src/scheduler.ts | 26 +--
7 files changed, 378 insertions(+), 17 deletions(-)
Fusion-Task-Id: FN-7648
Fusion-Task-Lineage: a4b54d30-f86d-4eb9-9cf2-6ac55b6dbe58
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fix API keys/OAuth credentials in ~/.fusion/agent/auth.json being clobbered when the desktop app and CLI-served web app run concurrently on one machine.
- Reload primary auth storage from disk (primary.reload()) before persisting a refreshed OAuth credential, so a concurrent process's newer login/refresh for the same provider isn't overwritten by this process's stale in-flight refresh.
- Re-check credential identity against the freshly reloaded disk state before writing the refreshed token back.
- Add cross-process regression coverage exercising concurrent auth.json read-modify-write scenarios.
- Add changeset documenting the fix and its dependency on the pi-coding-agent locked per-provider merge (>=0.80.x).
Files changed:
.changeset/fn-7646-auth-storage-coordination.md | 7 +
.../src/__tests__/auth-storage-concurrency.test.ts | 234 +++++++++++++++++++++
packages/engine/src/auth-storage.ts | 27 +++
3 files changed, 268 insertions(+)
Fusion-Task-Id: FN-7646
Fusion-Task-Lineage: de39f08d-2d9f-46ff-b293-c603e3268ecf
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the heartbeat timer audit so it repairs not just missing timer registrations but also 'zombie' ones — timer entries that remain present in memory after their underlying interval silently stopped firing. Long-interval (~1h) agents were most affected since a single lost tick compounded into hours of staleness before self-healing noticed.
- HeartbeatTriggerScheduler audit now computes staleness (elapsed vs repair-stale threshold) up front for every timer-eligible agent, not only for agents missing a timer entry
- Present-but-stale timer entries are now treated as non-advancing and force cleared/re-registered via registerAgent() (which already clears any existing timer before re-arming)
- Fresh (non-stale) present timers are left alone so healthy short-interval agents are never force-re-armed or double-ticked
- Repair reason/log messages now distinguish zombie-timer-rearmed repairs from missing-registration repairs, and the summary log reports counts for each
- Added heartbeat-scheduler tests covering the zombie-timer repair path
- Added changeset and a docs/architecture.md note
Files changed:
.changeset/fn-7645-heartbeat-rearm.md | 7 +
docs/architecture.md | 1 +
.../src/__tests__/heartbeat-scheduler.test.ts | 223 +++++++++++++++++++++
packages/engine/src/agent-heartbeat.ts | 42 +++-
4 files changed, 266 insertions(+), 7 deletions(-)
Fusion-Task-Id: FN-7645
Fusion-Task-Lineage: 652bc2eb-a660-4306-9f85-d2d5f9ca7e38
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the code-review/plan-review/CE gate workflow node failing with a blank "(no feedback captured)" message when a dispatch or infra exception (not a reviewer verdict) causes the step to fail.
- WorkflowGraphExecutor now synthesizes a non-blank WorkflowStepResult.output when an enabled optional-group (code-review, plan-review, browser-verification) or CE source:"node" skill-gate template node fails via dispatch/infra exception
- Diagnostic output is derived from the node:<id>:error context-patch key, falling back to the failure value, then a stable sentinel
- status, verdict extraction, edge routing, and self-healing's latestFailedPreMergeStep selection are unchanged
- Added regression test coverage: workflow-graph-optional-group-no-feedback.test.ts
- Added changeset (patch) documenting the fix for Runfusion/Fusion#1946
Files changed:
.changeset/fn-7642-code-review-no-feedback-diagnostic.md | 7 +
packages/engine/src/__tests__/workflow-graph-optional-group-no-feedback.test.ts | 246 +++++++++++++++++++++
packages/engine/src/workflow-graph-executor.ts | 104 ++++++++-
3 files changed, 355 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7642
Fusion-Task-Lineage: 1329e907-652f-4230-a945-5a9d7040ae69
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
Fixes the failing **full-suite** CI run on `main` ([run
28874651861](https://github.com/Runfusion/Fusion/actions/runs/28874651861))
— all 4 test shards were red. ~32 test files failing across engine +
dashboard (src + app), rooted in ~13 distinct causes from recent main
commits. All resolved; the merge gate and full engine/dashboard suites
are green locally.
## Root causes & fixes
### Engine (shards 1 & 2)
- **`appendAgentLog` 6th timing arg (FN-7503, `2797803c0`)** —
`agent-logger.ts` now passes an optional
`{durationMs,timeToFirstTokenMs}` 6th arg; many
executor/heartbeat/merger tests asserted the old 5-arg form. Added a
shared timing-tolerant helper `agent-log-assertions.ts` (asserts
`taskId/text/type`, tolerant of the timing object) and applied it across
affected files — so future timing fields won't re-break every executor
test.
- **`reconcileSupersededGeneratedFixFeatures` (mission)** —
`mission-execution-loop.ts` calls a method the test's missionStore mock
lacked; added a no-op stub (the real `MissionStore` already implements
it).
- **`ModelFallbackExhaustedError` / `proseSignalsClearApproval` /
`extractJsonObjectCandidates` missing from `vi.mock`** — converted stale
hand-written mocks (`../pi.js`, `../reviewer.js` in
`executor-test-helpers.ts`) to `importOriginal`-spread so real exports
carry through.
- **Workspace product fixes (2):**
- `merger-ai.ts` — `landWorkspaceTask` now recovers the integration-tip
sha as `landedSha` when the A1 trailer-fallback proved a sub-repo landed
but its sha was never persisted, so `finalizeWorkspaceTask` can build
merge proof (was stranding partial-land retries in-review).
- `worktree-acquisition.ts` — `acquireWorkspaceRepoWorktree` strips the
shared project `integrationBranch/baseBranch` overrides before
forwarding to `acquireTaskWorktree` (FN-7360's `freshStartPoint` was
resolving an absent shared branch).
- **FN-7360 extra `git symbolic-ref` exec** — updated worktree
exec-count assertions for the new `resolveIntegrationBranch` call.
- **Planner-overseer / stepwise-workflow / workflow-graph /
workflow-prompt / executor-step-session / liveness-gate / checkout /
ce-workflow / triage-split** — test-alignments for intentional behavior
changes (FN-7229 retry-cap, FN-7265 review-node removal, FN-7335
pause-abort logging, FN-7577 recovery-budget, FN-7577 overseer denial
loop, specifyTask single promptWithFallback call, FN-4944
already-on-main noop log, FN-7486 ownership short-circuit).
### Dashboard API (shard 3)
- **`store.on('task:moved')` (FN-7337)** — `createServer` now registers
the listener; backed the 4 affected MockStores with EventEmitter (shared
root cause across chat-routes.rooms, register-git-github,
routes-run-cited-goals, routes-sandbox-audit).
- **`routes-agent-import`** — core mock converted to
`importOriginal`-spread (was missing FN-7444 planning-deepening
constants).
- **`session-resume-history`** — engine mock missing
`resolveMcpServersForStore`.
- **`task-create-workflow-route`** — `builtin:legacy-coding`
defaultSteps now include `plan-review` (FN-7224/7226).
- **GitLab parity** — added the missing `[GitLab Parity Inventory]`
cross-link in `docs/signals-connectors.md`.
### Dashboard app (shard 4)
- Test-alignments for intentional product changes: FN-7057 (workflow
selection preservation), FN-7340 (footer concurrency geometry), FN-7156
(Missions overview default), FN-7342/FN-6825 (board scroll + workflow
switcher), FN-7352 (openDetailTask 3rd arg), FN-7261 (backdrop dismiss
default-off), FN-7234 (non-authoritative fetch failures), plus a missing
`fetchWorkflowOptionalSteps` mock.
### MCP coverage
- `mcp-surface-coverage` forwarding needle updated for FN-7446's
`resolvePlanningMcpServers` helper.
## Approach notes
- Each fix is the **minimal** change at the correct source (test-update
where a recent commit intentionally changed behavior; product-fix for
the 2 real regressions). No assertion was loosened/deleted to force a
pass; no timeout appeasement.
- Coordination: work was partitioned by package across parallel
subagents (engine / dashboard-src / dashboard-app) with Main as the sole
git committer (path-scoped commits) after an early shared-index reset
wiped in-progress edits — process was tightened mid-flight.
## Verification
- **Full engine suite**: green (9231 passed; the lone local-only
`custom-providers-openai-completions` import error is stale local
`pi-ai@0.79.9` vs the lockfile's `0.80.3` — CI's fresh install resolves
`/compat`; it passed in the original CI run).
- **Dashboard API** (`dashboard-api-quality-backfill`): 242 files / 3185
tests / 0 failures.
- **Dashboard app** (`dashboard-app-quality-backfill`): all targeted
files green (37 + 95 tests).
- **Merge gate** (`pnpm test:gate`): engine-core 326 + ci-shape 63, plus
nohup/4040/appeasement/changeset-format checks — all pass.
- 2 changesets added for the published-`@runfusion/fusion` behavior
fixes (workspace landedSha, sub-repo worktree branch-strip).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved reliability for partial workspace land retries by recovering
the exact proven landed commit so durable merge proofs can complete.
* Fixed per-sub-repo worktree creation by removing invalid branch
override settings, preventing worktree-add failures.
* Dashboard stability updates: preserve mobile board scroll during
stabilization/restore, correct task filtering when workflows are
missing, ensure the Chat tab appears for done tasks, and refine
modal-dismiss and responsive popover behavior.
* **Documentation**
* Expanded the GitLab connector section with GitLab parity context and a
GitLab Parity Inventory reference.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
findProvenLandedCommit now keeps --grep as a prefilter but verifies each
candidate carries an actual 'Fusion-Task-Id: <taskId>' trailer line via
git show -s --format=%B, so a later commit that merely mentions the trailer
text in its body cannot be selected. Regression covers a body-mention
intervening commit.
findProvenLandedCommit returns the task's own trailer commit (or recorded
landedSha when still an ancestor) instead of rev-parse on the integration
tip, so an intervening sub-repo land can't attribute a later unrelated
commit. Regression: intervening commit after lost persist recovers tipAfterFirst.
recoverActiveMissions (mission-execution-loop.ts:263) calls
missionStore.reconcileSupersededGeneratedFixFeatures per slice; the 5
MissionExecutionLoop-backed mocks here omitted it, so recovery threw
(TypeError) at the slice loop and aborted before processTaskOutcome /
ensureFeatureAssertionLinked / startValidatorRun ran — 4 tests failed.
Add a no-op stub (matches mission-execution-loop.test.ts reference) with
an FNXC:MissionReconcile note. No-op is correct: supersession is not
exercised by these tests.
A single retry can still lose the race on busy repos if origin moves again
in the pull-rebase/push window. Generalize the one-shot retry into a bounded
loop (3 attempts, 2s/5s/10s backoff), re-pulling+rebasing before each push
attempt and bailing early once the failure is no longer non-fast-forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When pushAfterMerge fails (or throws) in direct-merge mode, the daemon
only wrote to the process-wide mergerLog and a transient MergeResult
field, then unconditionally marked the task done. There was no durable
record on the task or in the audit trail, so a diverged local main
could go unnoticed indefinitely — this is how our local main drifted
162 commits from origin before it was caught by hand.
Record the failure through the two channels merger.ts already has for
this: the dormant "push:origin" GitMutationType via audit.git(), and a
task log entry via store.logEntry(). Both calls are best-effort
(.catch(() => undefined)) so a logging failure can't abort the merge
flow itself — task completion behavior is unchanged, the failure is
just no longer invisible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Executors could previously treat a pending approval as a normal turn end and go hunt for ungated workarounds instead of stopping. This change makes wait-for-approval a hard suspend point.
- wait-for-approval now suspends the in-flight executor session via awaitAbortInFlightTaskWork
- Dedupe identical pending approvals so repeated waits don't pile up
- Executor prompts now carve out awaiting-approval as a legitimate turn end (agent-prompts.ts)
- Extend provisioning-gate and agent-action-gate coverage for the new suspend/carveout behavior
- Add changeset (patch) documenting the fix for release notes
- Update docs/agents.md and docs/architecture.md to describe the new blocking behavior
Files changed:
.changeset/fn-7608-awaiting-approval-blocking.md | 7 ++
docs/agents.md | 1 +
docs/architecture.md | 1 +
packages/core/src/agent-prompts.ts | 5 +
.../engine/src/__tests__/agent-action-gate.test.ts | 82 +++++++++++++
.../executor-approval-gate-suspend.test.ts | 128 +++++++++++++++++++++
.../executor-approval-prompt-carveout.test.ts | 61 ++++++++++
packages/engine/src/agent-heartbeat.ts | 13 +++
packages/engine/src/executor.ts | 28 +++++
packages/engine/src/pi.ts | 22 +++-
.../sandbox/__tests__/provisioning-gate.test.ts | 29 +++++
packages/engine/src/sandbox/provisioning-gate.ts | 11 ++
12 files changed, 384 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7608
Fusion-Task-Lineage: 9e42d8ee-bda7-4ef1-b159-46c2100bbc48
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>