The mobile terminal rendered blank even though the WebSocket was Connected and
the shell prompt had already streamed in. Root cause (reproduced live): on the
mobile fullscreen layout xterm's CharSizeService can measure a 0-width character
cell, so FitAddon.fit() proposes 0 columns/rows and .xterm-screen (plus the WebGL
canvas) collapses to 0x0 — prompt bytes arrive and are written into xterm's row
DOM but paint into a zero-size box. Renderer-independent and mobile-layout-
specific; not fixed by resize/font-size re-fits because prior guards only validate
the container width and font load, never the resulting measured screen/cell width.
Add guardAgainstCollapsedTerminalScreen (app/utils/terminalPreferences.ts) and arm
it from both terminal surfaces (TerminalModal + SessionTerminal) right after their
initial fit. While the container has a width but .xterm-screen does not, it forces
a genuine DOM-strategy remeasure (forceTerminalFontRemeasure) + fit, re-driven by a
ResizeObserver until the screen has a real width. It waits (does not give up) while
the container is not yet measurable, is bounded so it never spins, and is disposed
on every re-init/close path. Recurrence of FN-7620/FN-7686.
- Add isTerminalScreenCollapsed + guardAgainstCollapsedTerminalScreen with tests
- Wire + dispose the guard across all xterm (re)init/close paths in both surfaces
- Add changeset (patch) and a docs/solutions write-up
Note: reproduced via mobile emulation (393px, iPhone UA, forced touch), not a
physical device; the guard is the structural fix — confirm on a real device.
Fusion-Task-Id: FN-7692
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Investigated whether --login in TerminalService first-prompt latency is a meaningful contributor and added a one-time diagnostic hint plus documentation of findings.
- Add SLOW_LOGIN_PROFILE_HINT_MS (2000ms) threshold and one-time, non-blocking console.info hint in createSession()'s PTY onData handler when a login shell is slow to produce first output
- Track spawnStartedAt and loginProfileHintLogged per session, and whether the succeeding spawn attempt used --login, without altering spawn args, timeouts, or the retry-without-login fallback
- Add regression tests covering the slow-login-profile hint behavior in terminal-service.test.ts
- Document the investigation and findings in docs/solutions/developer-experience/login-shell-profile-latency.md and link it from docs/dashboard-guide.md
- Add a patch changeset for @runfusion/fusion describing the new server-log hint
Files changed:
.changeset/fn-7688-login-shell-profile-latency.md | 7 ++
docs/dashboard-guide.md | 17 +++
.../login-shell-profile-latency.md | 79 ++++++++++++++
.../src/__tests__/terminal-service.test.ts | 121 +++++++++++++++++++++
packages/dashboard/src/terminal-service.ts | 57 ++++++++++
5 files changed, 281 insertions(+)
Fusion-Task-Id: FN-7688
Fusion-Task-Lineage: 08d5dd47-ea9f-4973-9f0e-a8d5fdeae111
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
On tablet-width viewports (769-1024px) the terminal header was overcrowded, so shortcut/zoom/preference controls now move into the bottom status-bar footer instead, mirroring the existing FN-7560 mobile layout while true desktop keeps the header controls.
- Add an isTabletTerminal detection flag (769-1024px, non-mobile) alongside the existing mobile flag
- Render the shared terminalActionControls fragment in the .terminal-status-bar footer for tablet widths, keeping desktop pin/pop-out toggles available there too
- True desktop (>1024px) continues to render font size / clear / shortcuts / preferences controls in the header
- Update TerminalModal.css with footer layout styles for the tablet action controls
- Update TerminalModal tests to cover the new tablet breakpoint rendering
- Update docs/dashboard-guide.md to describe the tablet footer control location
- Add a changeset (@runfusion/fusion: patch) documenting the fix
Files changed:
.changeset/fn-7684-tablet-terminal-footer.md | 7 ++
docs/dashboard-guide.md | 6 +-
.../dashboard/app/components/TerminalModal.css | 34 ++++++
.../dashboard/app/components/TerminalModal.tsx | 103 ++++++++++++-----
.../components/__tests__/TerminalModal.test.tsx | 126 ++++++++++++++++++---
5 files changed, 231 insertions(+), 45 deletions(-)
Fusion-Task-Id: FN-7684
Fusion-Task-Lineage: 627c40af-a152-451a-a045-65ad8babea9e
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Speed up initial terminal load by short-circuiting the no-op server session list call when there are no persisted local tabs to validate.
- useTerminalSessions: when readTabsFromStorage returns zero tabs, skip the listTerminalSessions HTTP call entirely and mark bootstrap ready immediately, unblocking auto-create/WebSocket connect instead of serializing behind a provably-discarded round trip
- Reload-with-persisted-tabs path is unchanged and still awaits the list call since its result is decision-relevant there
- Add regression tests covering the fresh-load fast path and the persisted-tabs path
- Add changeset (patch) and a docs/solutions write-up of the bootstrap-list-serialized-before-auto-create issue
Files changed:
.changeset/fn-7686-slow-terminal-initial-load.md | 7 ++
...bootstrap-list-serialized-before-auto-create.md | 87 ++++++++++++++++++++++
.../hooks/__tests__/useTerminalSessions.test.ts | 73 ++++++++++++++++++
.../dashboard/app/hooks/useTerminalSessions.ts | 23 +++++-
4 files changed, 189 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7686
Fusion-Task-Lineage: 9c708329-6362-4c2e-967f-aea12849c47c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the mobile top header wrapping onto a second line after a foldable phone is unfolded then refolded (a live resize, not a reload).
- .header now explicitly sets flex-wrap: nowrap instead of relying on the flex default
- .header-left gets flex: 1 1 auto; min-width: 0 promoted from the mobile-only media query to the base rule, so it shrinks/truncates during the resize before the width media query re-settles
- .header-actions gets flex: 0 0 auto; min-width: 0 so the action icon cluster stays at intrinsic size and is never squeezed off-row
- Added Header.test.tsx coverage asserting the nowrap/shrink contract across populated and empty header states, on mobile/tablet/desktop
- Added useViewportMode.test.ts regression reproducing a fold->unfold->refold visualViewport resize cycle, confirming mode resolves back to mobile
- Added changeset for @runfusion/fusion (patch/fix)
Files changed:
.changeset/fn-7687-mobile-header-single-line-refold.md | 7 ++
packages/dashboard/app/components/Header.css | 14 ++++
packages/dashboard/app/components/__tests__/Header.test.tsx | 78 ++++++++++++++++++++++
packages/dashboard/app/hooks/__tests__/useViewportMode.test.ts | 59 ++++++++++++++++
4 files changed, 158 insertions(+)
Fusion-Task-Id: FN-7687
Fusion-Task-Lineage: 2b88a26e-d380-458c-b602-b6496e39311d
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Upgrades Quick Add action-row height parity from a bare min-height floor to a true fixed box height, then adds a mobile-only Save correction per operator feedback so Save matches its siblings at the <=768px breakpoint without touching desktop/tablet sizing.
- Pair min-height with an equal max-height (plus tokenized line-height and centered alignment) on `.quick-entry-actions .btn, .quick-entry-actions .wf-optional-steps-dropdown-trigger` at both the desktop base rule and the <=768px touch-target media block (FN-5751: never a breakpoint-only fix)
- Add a mobile-only override scoped to `[data-testid="quick-entry-save"]` inside the <=768px media query (zero vertical padding, line-height:1) so Save's text+icon content fits the same fixed box as its siblings, leaving desktop/tablet Save sizing untouched
- Add regression coverage asserting the fixed min-height==max-height contract at both breakpoints, that shared .btn-sm/.btn-icon/.dep-trigger rules are untouched, and that the Save-only override exists only inside the mobile media query
- Add a patch changeset documenting the fix and follow-up for @runfusion/fusion
Files changed:
.changeset/FN-7683-quick-add-height-parity.md | 7 +
.../quick-entry-workflow-trigger-height.test.tsx | 181 ++++++++++++++++++++-
.../dashboard/app/components/QuickEntryBox.css | 53 ++++++
3 files changed, 234 insertions(+), 7 deletions(-)
Fusion-Task-Id: FN-7683
Fusion-Task-Lineage: d3df638a-812e-4fc2-aada-cfee58d29f2b
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Summary: The Planner Chat idle send button now renders icon-only, dropping its visible "Send" text span to match TaskChatTab's regular chat send button, while keeping the accessible name "Send" via aria-label.
- Removed `showSendText` prop from the planner chat send/stop button so the idle send button no longer shows a visible text span.
- Updated the CSS FNXC comment documenting that the send-text-hiding rule is now solely load-bearing for the streaming Stop button's icon-visibility contract, not the idle Send button.
- Updated the corresponding test to assert the send button has no visible text span (icon-only) while still asserting the accessible name resolves to "Send".
Files changed:
packages/dashboard/app/components/TaskPlannerChatTab.css | 7 +++++++
packages/dashboard/app/components/TaskPlannerChatTab.tsx | 5 ++++-
.../dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx | 4 +++-
3 files changed, 14 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7685
Fusion-Task-Lineage: 5ec896c5-7ca3-415b-bd5f-99a49f6a21ec
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Narrative: swaps a hardcoded px padding value for token-based spacing on the quick-entry workflow-selector trigger so it complies with the tokenized-CSS lint rule, while preserving visual sizing parity.
- Replace `padding: 4px 10px` with `padding: var(--space-sm) var(--space-md)` on `.quick-entry-workflow-trigger`.
- Add FNXC:QuickAddWorkflow comment explaining the token substitution and confirming height parity is governed by the existing FN-7680 min-height normalization, not this padding.
Files changed:
packages/dashboard/app/components/QuickEntryBox.css | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7682
Fusion-Task-Lineage: e2213e7d-5c38-460b-9727-57199d86b153
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>
Remove the self-grantable FUSION_RELEASE_AUTHORIZED env signal and replace
it with an interactive prompt: a real release now requires a live human to
type "authorized" at a TTY. Releases can no longer run non-interactively
(no TTY is blocked outright), and --yes does not bypass the typed phrase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task cards previously could show the progress/steps breakdown while still in the Planning (triage) column when a review gate was active; this changes it to only show once a task leaves Planning, matching ListView's behavior.
- TaskCard.showProgressSection now only shows for `in-progress`/`executing` tasks, dropping the special-case `triage` + active-progress-count branch
- Updated FNXC:TaskCardWorkflowProgress comment to document the FN-7676 requirement
- Updated TaskCard tests to cover the new triage-column behavior
- Added changeset for the patch
Files changed:
.changeset/fn-7676-planning-steps-breakdown.md | 7 +++
packages/dashboard/app/components/TaskCard.tsx | 7 ++-
.../app/components/__tests__/TaskCard.test.tsx | 50 ++++++++++++++++++----
3 files changed, 51 insertions(+), 13 deletions(-)
Fusion-Task-Id: FN-7676
Fusion-Task-Lineage: 85fcf8f9-1010-4b68-9323-9d8ce03aa66f
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
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>
## Problem
A task's model fields (`modelProvider`/`modelId` and the
planning/validator
equivalents) are snapshotted once at task-creation time from the
workflow's
model-lane default in `workflow_settings`. Nothing re-syncs, flags, or
surfaces drift when that default is later changed.
Concretely: the `builtin:coding` workflow's execution default was
`claude-sonnet-4-6` until it was corrected on 2026-07-05. Every task
created
before that correction stayed permanently, invisibly pinned to the stale
model id — 52 tasks were found silently stuck on it.
## Fix
- `TaskStore.getModelLaneDrift(workflowId, before, after)`
(`packages/core/src/store.ts`):
read-only diff over the three model lanes
(execution/planning/validator).
For any lane whose provider+modelId actually changed, it lists the
non-terminal (`column` not `archived`/`done`, not soft-deleted) tasks on
that workflow still pinned to the old value. Never mutates `tasks`.
- Wired into `PATCH /workflows/:id/setting-values`
(`packages/dashboard/src/routes/register-workflow-routes.ts`): captures
a
`before` snapshot, runs the existing `updateWorkflowSettingValues`
unchanged, then attaches an optional `modelDrift` field to the response
when a lane change orphans existing tasks. Backward compatible — the
field
is only present when non-empty.
- Operators can act on the surfaced drift via the existing
`POST /tasks/batch-update-models` endpoint; this change intentionally
does
not auto-rewrite any task (avoids touching tasks mid-execution).
## Testing
- New tests in `packages/core/src/__tests__/workflow-settings.test.ts`
(`TaskStore.getModelLaneDrift`): verifies a task pinned to a changed
lane's
old value is surfaced, a task already on the new value and a
`done`-column
task are excluded, and an unrelated/unchanged lane produces no drift
entry.
- `packages/core`: `npx vitest run
src/__tests__/workflow-settings.test.ts` — 24/24 pass.
- `npx tsc --noEmit` clean in both `packages/core` and
`packages/dashboard`.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Workflow setting updates can now return a lane-based model drift
summary (execution, planning, validator) showing task IDs still pinned
to the previous model configuration.
* The PATCH workflow setting response conditionally includes
`modelDrift` when impacted tasks are found.
* **Bug Fixes**
* Drift detection now compares a consistent “before” snapshot with the
updated values to avoid stale pairing.
* “No workflow selection” tasks are handled correctly based on the
default-workflow behavior.
* **Tests**
* Added coverage for lane drift across model changes and null-selection
inclusion rules.
* **Documentation**
* Added a release note entry for the change.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- getModelLaneDrift now takes an explicit includeNullSelection option; the
setting-values route passes it when patching the project default workflow so
no-workflow-selection tasks (which resolve through the default) are counted
instead of silently dropped (Greptile P1).
- Add updateWorkflowSettingValuesWithPrevious so the drift baseline is captured
inside the settings write transaction, removing the stale-read race against a
concurrent patch of the same row (Greptile P2).
- Broaden getModelLaneDrift tests to cover planning and validator lanes and the
null-selection/default-workflow case (FN-5893 invariant across surfaces).
- Add changeset.
CodeRabbit's effective-values suggestion is intentionally skipped: model-lane
declarations carry no declaration-level default (KTD-7), so effective == raw for
these keys and comparing effective values is a no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 -->
The repo eslint config omits react-hooks/exhaustive-deps, so a disable
directive for it is itself a lint error (rule-not-found). Replace with a
plain comment documenting the intentional dep omission.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Rename the Command Center SDLC funnel's intake stage display text from "Triage" to "Planning" to match the renamed board column, updating English locale copy and tests; the underlying aggregator/i18n key names stay "triage".
- SdlcFunnel.tsx: stage label fallback, enteredInRange, and completionRateAria copy changed to Planning/Entered planning/in-range planning entrants; added FNXC comment explaining the key-vs-label distinction
- packages/i18n/locales/en/app.json: synced English strings for the same three keys
- SdlcFunnel.test.tsx: updated assertions to match new Planning copy
- Added changeset (patch) documenting the label rename
Files changed:
.changeset/fn-7665-funnel-planning-label.md | 7 +++++++
.../app/components/command-center/SdlcFunnel.tsx | 16 ++++++++++++----
.../command-center/__tests__/SdlcFunnel.test.tsx | 6 +++---
packages/i18n/locales/en/app.json | 6 +++---
4 files changed, 25 insertions(+), 10 deletions(-)
Fusion-Task-Id: FN-7665
Fusion-Task-Lineage: d8aec609-6b8b-4f36-bb43-61d7c7c421f9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Removes duplicated SSE streaming logic between the /automations/:id/run/stream and /routines/:id/run/stream endpoints by extracting a single generic makeRunStreamHandler factory.
- Add makeRunStreamHandler<TStore, TEntity> factory in routes.ts, parameterized by store resolver, entity getter, and not-found message
- Replace the two near-identical inline SSE handlers (connect headers, buffered replay, run subscription, auto-attach, teardown, error handling) with calls to the shared factory
- Import ScopeValue type from ./routes/types.js to type the factory's scope parameter
- Add regression tests in routes-automation.test.ts covering the consolidated handler behavior for both routes
Files changed:
.../src/__tests__/routes-automation.test.ts | 35 +++
packages/dashboard/src/routes.ts | 246 +++++++++------------
2 files changed, 137 insertions(+), 144 deletions(-)
Fusion-Task-Id: FN-7663
Fusion-Task-Lineage: 55ede468-c4ed-4a72-a428-7d19686e8ea5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Boot-smoke's post-verdict temp-dir cleanup could throw ENOTEMPTY on macOS when async writers (fsevents/Spotlight, the just-killed child) still touched the throwaway HOME/project dirs, turning an already-decided PASS into a pnpm verify:fast failure.
- Add removeTempDir() helper in scripts/boot-smoke.mjs: wraps rmSync with maxRetries/retryDelay and swallows any residual error (never throws) since cleanup always runs after the smoke verdict is decided/printed
- Replace both isolatedHome/isolatedProject rmSync call sites (exit handler and EADDRINUSE retry-port branch) with removeTempDir()
- Guard main() behind an import.meta.url === pathToFileURL(process.argv[1]) check so requiring boot-smoke.mjs from tests doesn't spawn a real server
- Add scripts/__tests__/boot-smoke.test.mjs covering ENOTEMPTY tolerance, always-failing rm, retry/backoff params, and the no-boot-on-import guard
Files changed:
scripts/__tests__/boot-smoke.test.mjs | 77 +++++++++++++++++++++++++++++++++++
scripts/boot-smoke.mjs | 44 +++++++++++++++++---
2 files changed, 115 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-7662
Fusion-Task-Lineage: 3288d055-4f70-4487-8462-933563214ce5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes fn_task_archive and fn_task_delete rejecting tasks still referenced as a lineage parent, with no tool-exposed way to clear that reference.
- Add optional removeLineageReferences boolean param to fn_task_archive and fn_task_delete tool schemas, forwarded to store.archiveTask/store.deleteTask
- Update tool descriptions and prompt guidelines to advertise the recovery path (removeLineageReferences:true) when a lineage-parent block occurs
- Add task-lineage-unlink.test.ts covering the new parameter behavior
- Document the change in docs/storage.md
- Add changeset (@runfusion/fusion minor, category: fix)
Files changed:
.changeset/fn-7661-lineage-unlink-tools.md | 7 +
docs/storage.md | 1 +
packages/cli/src/__tests__/task-lineage-unlink.test.ts | 199 +++++++++++++++++++++
packages/cli/src/extension.ts | 28 ++-
4 files changed, 232 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7661
Fusion-Task-Lineage: 414c046c-43df-4995-85a5-ff00b345de50
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>
Retains the GitHub Import Tasks modal's provider/tab/filter/selection state so returning to Import Tasks doesn't reset the user's in-progress import setup.
- GitHubImportModal now persists provider, active tab, label filter, remote, and issue selection per project via a new modalPersistence hook, restoring them on remount instead of always defaulting.
- Added packages/dashboard/app/hooks/modalPersistence.ts to encapsulate the persisted-state read/write logic backed by projectStorage.
- projectStorage.ts gains the `kb-dashboard-github-import-state` storage key.
- Falls back to the existing default-remote auto-detect behavior when no persisted state exists.
- Added extensive test coverage in GitHubImportModal.test.tsx for the new persistence behavior.
- Updated docs/dashboard-guide.md to describe the retained state.
- Added a patch changeset for @runfusion/fusion.
Files changed:
.changeset/fn-7657-github-import-state-retained.md | 7 +
docs/dashboard-guide.md | 2 +
.../dashboard/app/components/GitHubImportModal.tsx | 204 +++++++++++++--
.../__tests__/GitHubImportModal.test.tsx | 282 +++++++++++++++++++++
packages/dashboard/app/hooks/modalPersistence.ts | 72 ++++++
packages/dashboard/app/utils/projectStorage.ts | 1 +
6 files changed, 547 insertions(+), 21 deletions(-)
Fusion-Task-Id: FN-7657
Fusion-Task-Lineage: c8086340-368c-4efd-a12a-4cddeeb0aa26
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Task model fields (execution/planning/validator provider+modelId) are
snapshotted once at task-creation time from the workflow's model-lane
default and never re-synced. Changing a workflow's default (e.g. fixing
a stale model id) silently leaves already-created tasks pinned to the
old value with no visibility — this is exactly how 52 tasks stayed
pinned to a stale claude-sonnet-4-6 default after it was corrected.
Add TaskStore.getModelLaneDrift(workflowId, before, after), a read-only
diff over the three model lanes that lists non-terminal tasks still
pinned to a lane's old value. Wire it into
PATCH /workflows/:id/setting-values so the response includes
`modelDrift` whenever a lane change orphans existing tasks. Operators
can then act via the existing POST /tasks/batch-update-models.
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
Fixes chat sessions not showing the working/"Thinking…" indicator when returning to a session whose generation was already in flight but hadn't emitted its first delta yet.
- useChat.ts: selectSession's authoritative fetchChatSession refresh now reattaches whenever refreshedSession.isGenerating===true, instead of also requiring a populated inFlightGeneration snapshot (which is null pre-first-delta)
- Added a guard so the reattach only proceeds if the user hasn't navigated away from the session while the refresh was in flight (activeSessionRef.current?.id === id)
- Calls attachIfGenerating(id, refreshedSession.inFlightGeneration, { silent: true }) when no stream is already attached, reusing existing double-attach guarding
- Added regression tests in useChat.test.ts covering the reattach-on-isGenerating-alone behavior and the stale-session navigation guard
- Added a patch changeset documenting the fix
Files changed:
.changeset/fn-7656-chat-reattach-working-state.md | 7 ++
.../dashboard/app/hooks/__tests__/useChat.test.ts | 113 +++++++++++++++++++++
packages/dashboard/app/hooks/useChat.ts | 22 +++-
3 files changed, 141 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7656
Fusion-Task-Lineage: c923c16a-391f-4475-aab8-6194bbc675f7
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fix the Board Auto-approve plan shortcut leaking onto hold (Todo-like) columns; it must render only on the intake/planning column.
- Board.tsx: gate the planAutoApproveEnabled/onTogglePlanAutoApprove prop pair on columnDef.flags.intake only (dropped the || columnDef.flags.hold clause) for both the aggregate and single-workflow column renders
- Column.tsx: update the FNXC comment to document that Board.tsx is the single source of truth for this intake-only gating; Column.tsx just renders whatever prop it receives
- docs/dashboard-guide.md: correct the Board guide to describe the toggle as intake/planning-column-only, not hold columns
- Board.test.tsx: fix the existing intake/hold test expectation to assert the hold column does NOT get the toggle, and add a regression test reproducing the built-in Coding workflow's Todo (hold) column symptom
- add a patch changeset documenting the fix
Files changed:
.changeset/FN-7653-plan-auto-approve-intake-only.md | 7 +++++++
docs/dashboard-guide.md | 5 +++--
packages/dashboard/app/components/Board.tsx | 6 ++++--
packages/dashboard/app/components/Column.tsx | 5 ++++-
.../app/components/__tests__/Board.test.tsx | 20 +++++++++++++++++---
5 files changed, 35 insertions(+), 8 deletions(-)
Fusion-Task-Id: FN-7653
Fusion-Task-Lineage: 10c5f01a-b00c-4039-9b83-a8a8965fc56c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Reconcile the automation live-run panel's terminal status with the authoritative run result so successful runs no longer flash "Run failed".
- ScheduledTasksModal/RoutineCard now reconcile SSE terminal status against the POST/registry result instead of trusting raw stream teardown.
- Gate benign SSE teardown (post-terminal close, reconnect exhaustion) from being surfaced as a failure state.
- Apply the same reconciliation to both /routines/:id/run/stream and /automations/:id/run/stream routes in routes.ts.
- Add regression coverage in RoutineCard, ScheduledTasksModal, and routes-automation tests.
- Add a patch changeset documenting the fix.
Files changed:
.changeset/fn-7652-automation-live-run-false-failure.md | 7 ++
packages/dashboard/app/components/ScheduledTasksModal.tsx | 66 ++++++++++++++--
packages/dashboard/app/components/__tests__/RoutineCard.test.tsx | 37 +++++++++
packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx | 78 +++++++++++++++++++
packages/dashboard/src/__tests__/routes-automation.test.ts | 87 ++++++++++++++++++++++
packages/dashboard/src/routes.ts | 52 ++++++++++++-
6 files changed, 319 insertions(+), 8 deletions(-)
Fusion-Task-Id: FN-7652
Fusion-Task-Lineage: 61fc89f5-d25d-44de-9699-bc8ad2a0dea6
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Removes the visible "Stop generation" text label from the planner chat's stop button while keeping it accessible via aria-label.
- Add showStopText prop to StandardChatActionButton (defaults to showSendText) to independently control Send vs Stop visible text
- Set showStopText={false} in TaskPlannerChatTab so the streaming stop button renders icon-only
- Update TaskPlannerChatTab test to assert no visible text span on the stop button while aria-label is retained
- Add changeset for @runfusion/fusion (patch)
Files changed:
.changeset/fn-7655-planner-stop-icon-only.md | 7 +++++++
.../dashboard/app/components/StandardChatSurface.tsx | 16 ++++++++++++++--
packages/dashboard/app/components/TaskPlannerChatTab.tsx | 3 +++
.../app/components/__tests__/TaskPlannerChatTab.test.tsx | 4 +++-
4 files changed, 27 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7655
Fusion-Task-Lineage: f68a8bfa-30ba-439e-97d0-28654a614c54
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
usage_events was absent from Database.pruneOperationalLogs, so the
per-tool telemetry log grew unbounded (~187k rows / ~28MB observed) and
became a dominant driver of .fusion DB bloat once runAuditEvents was
already 30-day capped. Prune it on the same operationalLogRetentionDays
cadence, keyed off its `ts` column (not `timestamp`), alongside the other
column-name exceptions. Adds a regression test and changeset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes project-switch hydration so a persisted "settings" view resolves to the Board instead of re-opening Settings.
- Extend resolveLandingTaskView() in useViewState.ts to treat "settings" the same as "command-center", resolving both to "board" for the auto-restored/hydrated landing view only
- Add regression tests covering the settings->board landing resolution in useViewState.test.ts
- Add changeset documenting the patch-level fix
Files changed:
.changeset/fn-7649-project-switch-board-landing.md | 7 ++
.../app/hooks/__tests__/useViewState.test.ts | 74 ++++++++++++++++++++++
packages/dashboard/app/hooks/useViewState.ts | 5 +-
3 files changed, 85 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7649
Fusion-Task-Lineage: 7179efd6-bb1b-4ea4-a062-479b9b1fffa3
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Follow-through on FN-5048: the four residual fixed 100ms sleeps in the
websocket badge integration tests (3 subscription-establishment waits + 1
cross-instance pub/sub propagation wait) are replaced with deterministic
awaits on the server-side WebSocketManager subscription:changed event and the
shared pub/sub message event. Removes ~400ms of unconditional real-time waiting
and closes the ordering races the sleeps papered over.
Fusion-Task-Id: FN-5048
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>