Mobile mailbox overlays now close through the shared navigation history before leaving the message list.
- Register mobile message, composer, and approval overlays as navigation modals.
- Remove matching history entries when mailbox overlays close or change state.
- Add mobile back-navigation coverage and document the behavior.
Files changed:
docs/dashboard-guide.md | 3 +-
packages/dashboard/app/components/MailboxView.tsx | 117 ++++++++++++----
.../app/components/__tests__/MailboxView.test.tsx | 148 +++++++++++++++++++++
3 files changed, 241 insertions(+), 27 deletions(-)
Fusion-Task-Id: FN-8231
Fusion-Task-Lineage: b6f06ed9-b03f-45d9-b90b-28b565231bcc
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Repair CLI test mocks and retry-reset expectations for the current store contracts.
- Add the backend store factory to the experiment-finalize mock.
- Provide global settings directory access in backup test stores.
- Assert all manual retry reset fields in task command tests.
Files changed:
.../extension-experiment-finalize.test.ts | 13 ++++++++++
.../commands/__tests__/backup-lock-retry.test.ts | 2 ++
packages/cli/src/commands/__tests__/backup.test.ts | 17 ++++++++++++-
packages/cli/src/commands/__tests__/task.test.ts | 28 +++++++++++++++++-----
4 files changed, 53 insertions(+), 7 deletions(-)
Fusion-Task-Id: FN-8222
Fusion-Task-Lineage: 033ce6a6-c699-409c-a59e-2d1f5e041cfc
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Align daemon startup mocks with the model runtime initialization path.
- Add the auth-storage setModelRuntime mock.
- Stub the Fusion model registry factory to use the shared test registry.
Files changed:
packages/cli/src/commands/__tests__/daemon.test.ts | 9 +++++++++
1 file changed, 9 insertions(+)
Fusion-Task-Id: FN-8220
Fusion-Task-Lineage: 6e00980f-2e24-4502-a4d8-e91849df33b9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
An audit of the SQLite→PostgreSQL store migration found data-store paths
still reaching the removed SQLite stub in backend (PG) mode. In backend
mode `store.db`/`getDatabase()` throw the removed-SQLite error, so each
of these either threw on every run or — worse — had the throw swallowed
into a silent wrong result. This PR routes all of them through the
`AsyncDataLayer` (and removes one dead primitive).
## The 6 live bugs fixed
| Fix | Was |
|-----|-----|
| `executor.ts` authoritative assigned-agent fallback now inherits the
TaskStore `asyncLayer` | silently returned `null` → model drift to the
pi built-in (the exact thing its comment guards) |
| `pruneAgentLogFilesAsync` replaces the sync self-healing prune call |
threw `SQLite Database is not available` every maintenance sweep →
agent-log pruning never ran |
| `cleanupOrphanedMaterializedSteps` deletes PG `workflow_steps` rows on
a failed create | swallowed the throw → leaked rows |
| `deleteTaskBackendImpl` now runs the async mission feature/task-link
unlink | PG hard delete left orphaned mission links |
| `getWorkflowSettingsProjectId` returns `rootDir` in backend mode
without touching the stub | swallowed throw for unscoped backend stores
|
| `fn plugin` unregistered-project fallback bootstraps a `CentralCore`
`AsyncDataLayer` | layerless `PluginStore` threw in PG |
## The 4 latent traps, fixed properly
- **`cleanupArchivedTasks`** — real async port (enumerate archived
soft-deleted rows, guarantee cold snapshot, hard-delete project row +
purge selection rows + rm dir).
- **`deleteWorkflowStep`** — real async port (delete `workflow_steps`
via the layer with `.returning()` to preserve the not-found contract).
- **`applyTaskPatch`** — **removed** (zero-caller SQLite column-patch
primitive with no backend analogue; impl + facade + import deleted).
- **`AgentStore.importLegacyFileRuns`** — clean backend no-op (no legacy
SQLite run-files exist in a PG deployment; its only `init()` caller
early-returns in backend mode).
## Symptom Verification
New PG regression suite
`packages/core/src/__tests__/postgres/store-sqlite-residue-fixes.pg.test.ts`
reproduces the original failures against real embedded Postgres and
asserts they're gone:
- orphaned `workflow_steps` are actually deleted (no swallowed throw)
- `pruneAgentLogFilesAsync` resolves and prunes inactive-task log files
- hard delete unlinks the mission feature from the task
- `deleteWorkflowStep` removes the row / reports not-found
- `cleanupArchivedTasks` hard-deletes the project row while retaining
the cold snapshot
## Verification
- `@fusion/core`, `@fusion/engine`, `@runfusion/fusion` typecheck clean
- ~50 existing + 5 new PG tests pass; lint clean; changeset validates
🤖 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 PostgreSQL backend maintenance from hitting removed legacy
SQLite code paths, avoiding datastore failures and residue cleanup
issues.
* Fixed workflow-step deletion and “not found” behavior in backend mode.
* Ensured backend hard-deletes correctly unlink related mission
feature/task links and clean orphaned materialized steps.
* Prevented legacy file-run imports from incorrectly reporting success
in backend mode.
* **New Features**
* Added async agent-log pruning for inactive tasks and updated
maintenance to use it.
* **Tests**
* Added PostgreSQL regression coverage for residue fixes and
archive/workflow cleanup.
* **Refactor**
* Removed an unused task patch operation and updated task-store cleanup
methods to be async where needed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
Permanent (durable) agent heartbeats went silent while the rest of the
engine kept running. Investigation of the live DB showed **every**
permanent agent's `heartbeatTimerRepair` metadata carrying
`nonAdvancingEscalated: true` with **3–63 consecutive** zombie re-arms —
the audit re-arming a timer every 60s for hours while emitting
`heartbeat-rearm-nonadvancing-escalated` warnings that never recovered
anything.
## Root cause
The heartbeat trigger audit classified a "zombie" (dead) timer **solely
from a stale `lastHeartbeatAt`**. But that column advances *only* on a
successful `"ok"` delivery (`agent-store.ts` `recordHeartbeat`). It
stays frozen whenever a heartbeat is intentionally skipped or no-op'd:
- agent over budget / over budget threshold
- `globalPause` / `enginePaused`
- `skipHeartbeatWhenIdle` on an idle agent
- idle "org" agents whose runs complete as `no_assignment_identity_run`
In all of these the interval keeps firing perfectly — the timer is
alive, delivery is just (correctly) skipped. Keying zombie detection off
`lastHeartbeatAt` misread those healthy timers as dead, re-armed them
every 60s, and escalated forever. Re-arming a live timer is a no-op, so
the loop could never recover — it only produced churn and phantom
warnings.
## Fix (the invariant)
Key zombie detection off **whether the interval physically fired**, not
whether delivery advanced.
- New `lastTimerFireAtMs` map, stamped at the top of `onTimerTick`
**before any gate** — a fired-but-skipped tick still counts as proof of
liveness.
- In the audit: a present + stale timer that fired within its stale
window is **left untouched** (no re-arm, no escalation, non-advancing
counter reset). Only a timer with **no recent fire** (a genuinely dead
interval) falls through to the existing re-arm/escalation path.
- Map cleaned up in `unregisterAgent()` / `stop()`.
This preserves the FN-7645 zombie repair (a timer that stops firing goes
stale in lockstep on both clocks and is still re-armed) and the FN-7939
watchdog, while eliminating the phantom churn for live-but-skipping
timers.
Why not "force a heartbeat" or "park the agent": forcing delivery would
bypass budget/pause governance, and parking a healthy idle agent would
be wrong. The correct action for a live-but-skipping timer is to leave
it alone — its next real tick delivers once the skip condition clears.
## Tests
- Rewrote the old `skipHeartbeatWhenIdle` test that codified the buggy
escalation → now asserts a **live** idle-skipping timer is left
untouched (no zombie re-arm, no escalation).
- Added a budget/no-assignment surface: a live timer that dispatches but
leaves `lastHeartbeatAt` frozen must not be misclassified.
`heartbeat-scheduler.test.ts` 120/120; broader heartbeat + concurrency
suites 349/349; `@fusion/engine` typecheck 0 errors.
## Review
Self-reviewed at medium effort. Two acknowledged, bounded trade-offs
(kept intentionally): a genuinely-dead-but-recently-fired timer's repair
latency is bounded at ~2× interval (same as the original FN-7645
latency), and the escalation warning is suppressed for live timers (it
only ever fired because of the churn this removes; per-tick error logs +
a new "left live-but-skipping timer" log retain visibility). One trivial
cleanup applied (single `Date.now()` sample).
## Notes
- Engine is a private package → no changeset.
- Complementary to a separate in-flight fix for the
agentStore/scheduler-not-constructed bug (why heartbeats stopped
*entirely*); this PR ensures that once the scheduler runs again, the
audit stops the phantom churn/escalation.
🤖 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 heartbeat timer monitoring to distinguish healthy timers from
genuinely stopped timers.
* Prevented unnecessary timer re-registration and warning escalation
when heartbeats are intentionally skipped due to idle, paused,
budget-limited, or unassigned states.
* Improved recovery when a replacement timer stops firing, ensuring it
is detected and repaired reliably.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Keep the Settings default-description coverage aligned with surfaced and global-only settings.
- Add missing Notifications and Scheduling description keys to the coverage map
- Allowlist the global-only LAN discovery setting so the guard does not require a UI description
Files changed:
.../sections/__tests__/settings-default-descriptions.test.tsx | 9 +++++++++
1 file changed, 9 insertions(+)
Fusion-Task-Id: FN-8216
Fusion-Task-Lineage: a5449429-c5b2-4d88-915b-26baba3fb60b
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Ensure task chat role icons reflect each agent's actual runtime model.
- Parse runtime model markers from status and text log entries.
- Prefer runtime and effective models over stale task provider overrides.
- Cover provider icon precedence and fallback behavior for all chat roles.
Files changed:
packages/dashboard/app/components/TaskChatTab.tsx | 8 +-
packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx | 88 ++++++++++++++++++----
2 files changed, 76 insertions(+), 20 deletions(-)
Fusion-Task-Id: FN-8214
Fusion-Task-Lineage: 1eb02ca5-469f-4a9d-932c-5cf02383b7fe
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Restore the CLI package configuration test to the active test lane with current build expectations.
- Allowlist WhatsApp plugin-only tsup externals as non-runtime CLI dependencies.
- Assert the full workspace build command in the verification contract.
- Remove the stale package-config test quarantine.
Files changed:
packages/cli/src/__tests__/package-config.test.ts | 12 +++++++++++-
packages/cli/vitest.config.ts | 9 ++++-----
2 files changed, 15 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-8210
Fusion-Task-Lineage: aa29d866-430f-4097-affa-a89d107474b2
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
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>