# Migrate storage from SQLite to PostgreSQL — full dashboard cutover
Migrates Fusion's storage layer to the embedded PostgreSQL
`AsyncDataLayer` (the default backend) and **completes the
satellite-store + feature cutover** so every dashboard and Command
Center surface works in PG mode.
## Status — every surface works in embedded-PG mode
Verified live against a running embedded-Postgres dashboard (all
**200**, zero 5xx) and gate-tested (**23 files / 99 tests** on embedded
PG, plus engine-core 294 and ci-shape 63 in the blocking merge gate;
core/engine/cli/dashboard typecheck clean).
| Area | Surfaces | State |
|---|---|---|
| Satellite stores | workflows, todos, insights, research, missions,
goals, mailbox | ✅ |
| Views | artifacts, documents, evals | ✅ |
| Command Center | activity, productivity, team, tokens, tools,
**workflows**, **github**, **signals**, **plugin-activations**, **live**
(all 10) | ✅ |
| Run execution | insight generation, research run execution | ✅
(store-path; AI step needs a provider) |
| Live updates | SSE push for mission/research/insight events | ✅ |
| Workflow editing | create / update / delete / select (+ id counter) |
✅ |
| Engine | mission autopilot, incident-signal ingestion, regression
storm-guard, agent wake-on-message | ✅ |
| Core | tasks, agents, secrets, automations, memory, chat, usage, PRs,
git | ✅ |
## Approach
Each satellite store gets an `Async<Store>` wrapper exposing the sync
store's method names over the existing `async-*-store.ts` helpers;
`get<Store>Store()` returns a `Sync | Async` union; consumers `await`
(harmless on sync), and engine/CLI paths that can't convert use
`instanceof Sync` graceful fallback. Analytics aggregators branch on
`"ping" in dbOrLayer` to run schema-qualified raw SQL over `project.*`
(snake_case) in PG. Executors/orchestrators/autopilot are
await-converted to drive the union store; the async store wrappers
extend `EventEmitter` so SSE live-push fires in both backends.
Not-yet-ported capabilities degrade gracefully (never 500) and are
individually called out in commits.
## Sync with main
The branch is kept continuously merged with `main` (currently through
FN-7845, 2026-07-12); the earlier "final rebase deferred" note no longer
applies. Use **Create a merge commit** (or squash) to land it — GitHub's
rebase-merge cannot replay a merge-maintained branch.
## Residual Review Findings
Multi-agent code review of the PostgreSQL satellite-store ports (U1–U5)
applied 3 safe fixes (see `fix(review): apply autofix feedback`). The
following are **real but gated** — recorded here as follow-up work
rather than auto-applied. All are SQLite→PostgreSQL
**concurrency/atomicity regressions**: the sync stores were immune only
by SQLite's single-writer, single-threaded-handler execution; the async
ports open multi-await read-modify-write windows. **Reachability is low
today** because the execution engines that generate concurrent same-run
mutations (insight run executor, research orchestrator/dispatcher) are
`instanceof`-gated to sync mode in PG. No process-crash class survived
(all engine fallbacks correctly guard the sync store).
- **[P1] Research `appendResearchEvent` dual-write is non-atomic**
(`packages/core/src/async-research-store.ts`, corroborated: adversarial
+ reliability). The `research_run_events` insert (own transaction) and
the `run.events` jsonb update are separate writes — a crash between
them, or two concurrent appends, splits the table count from the jsonb
array. **Fix:** perform the seq-insert and the jsonb update in one
`layer.transactionImmediate`.
- **[P1] Research run terminal-reversion via stale full-row persist**
(`async-research-store.ts` `persistResearchRun`/`updateResearchStatus`).
Concurrent `PATCH /runs/:id/status` + `POST /runs/:id/events` can revert
a terminal run to `running` by overwriting the whole row, bypassing the
transition guard. **Fix:** scoped column `UPDATE`s with a `WHERE status
…` guard, or optimistic version column.
- **[P2] `updateResearchRun`/`updateInsightRun` read-then-write TOCTOU**
— concurrent PATCHes last-writer-wins on the lifecycle merge. **Fix:**
`SELECT … FOR UPDATE` / enclosing transaction.
- **[P2] `upsertRun`/`createRunOrThrowConflict` check-then-create race**
(`async-insight-store.ts`) — two callers can each create an "active"
run. **Fix:** partial unique index on `(projectId, trigger) WHERE status
IN ('pending','running')`.
- **[P3] `createResearchRetryRun` return-value divergence** — sync
returns the pre-update `queued` snapshot; async returns the reloaded
`retry_waiting` run (persisted state is identical). Pick one side for
cross-backend parity.
- **[P2/perf] Mission `getMissionWithHierarchy`/`getMissionHealth` N+1
fan-out** — O(milestones×slices) sequential round-trips hold one pool
slot per request; can starve the pool for large hierarchies. **Fix:**
batched/joined reads.
- **Testing gaps:** no PG-mode concurrency tests (interleaved
status/event mutations), no sync↔async parity assertion for the
lifecycle-error codes, and no mission status/health rollup parity test
vs the sync `MissionStore`.
~~Out of scope (deferred): AI run *execution* (insight/research) +
mission autopilot + live SSE mission events remain sync-gated/degraded
in PG mode.~~ **Since ported** — insight/research run execution, mission
autopilot, and SSE live push all run on the async layer now, which also
makes the concurrency findings above genuinely reachable; they remain
open follow-ups.
---
## Update — 2026-07-12: production-readiness hardening & live acceptance
Everything below landed on this branch since the description above was
written:
**Production blockers from review — fixed**
- `recoverStaleTransitionPending` ported to the async layer (backend
moves write + clear the crash-safe marker; startup/maintenance sweeps no
longer throw).
- Lost-update class fixed: `atomicWriteTaskJson`/`WithAudit` write
changed columns only (full-row upserts silently resurrected stale fields
across concurrent store instances — the "task stuck unplanned forever"
bug).
- First-boot **auto-migration**: booting the PG backend over a project
with a legacy `fusion.db` migrates it automatically (loud failure,
SQLite kept as backup), and the dashboard shows a one-time **"your data
was migrated" banner** with the backup paths and a Need-help Discord
link.
- `pg_dump`/`pg_restore` discovered from common install locations for
embedded-mode backups.
- The PG suite is part of the blocking merge gate (`test:pg-gate`).
**Multi-project isolation (PR #2007, merged into this branch)**
- `project_id` partition key on tasks / archived tasks / config,
`taskProjectScope` threaded through every scan/claim/count, per-project
config rows, layer bound to the project at startup.
- Review P1 follow-up: the shared cold-storage `archive.archived_tasks`
table is also partitioned and all archived-board reads/counts/searches
are scoped.
- Schema drift self-heal generalized to schema-qualified columns so
existing databases upgrade in place.
**Other changes**
- Node settings sync **removed** in PG mode (409
`settings-sync-disabled-postgres`) — nodes share state by connecting to
the same database; auth sync kept (per-machine file).
- Perf (review findings): `listTasks` pushes column filter + ORDER BY +
LIMIT/OFFSET into SQL; `getConversation` capped to the most recent 200
messages.
- Fixed a false "operator action required" pause-abort log fired on
every successfully auto-merged task.
**Live acceptance — PASSED (2026-07-12)**
A sandboxed instance (isolated HOME, embedded PG, real Opus executor)
ran a task through the complete cycle: create → triage (AI spec) →
execute → in-review → AI squash-merge landed on the project's `main` →
done. A write+read sweep of every data surface (settings, comments,
documents, attachments + artifact bridge + artifact edit, chat with real
generation, goals, missions, agent mail, secrets, workflows, memory, CC
analytics) was green on embedded PG.
**Known remaining work**
- The per-project `config` PK re-key has no upgrade path for
pre-isolation embedded-PG databases (needs a real `DROP
CONSTRAINT`/re-key migration; fresh databases are fine).
- `pg_dump`/`pg_restore` binaries are not yet bundled in release
artifacts (PATH/common-location discovery only).
- The satellite-store concurrency findings listed above.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Phil Larson <hello@phillarson.xyz>
Co-authored-by: fusion-merge <fusion-merge@local>
## Summary
Fixes shard 3 failures from runs 29258546612 + 29259574946 (FN-7936
drift).
## Fixes
### `package-config.test.ts` — stale TRANSITIVE_EXTERNALS entry
FN-7936 aliased `@fusion/core` to a runtime shim in bundled plugin
outputs; it's no longer a tsup external. Removed the stale allowlist
entry.
### `bundle-output.test.ts` — stale dashboard client hash ENOENT
**Root cause:** Two test files (`bundle-output.test.ts` +
`extension-integration.test.ts`) call
`buildCliWithRealDashboardAssets()` which triggers concurrent vite/tsup
builds. Vitest runs them in parallel (`pool: "forks"`, `fileParallelism:
true`). Without coordination, two builds clean and write `dist/client`
simultaneously, causing `ENOENT` on content-hashed chunk files.
**Fix (3 parts):**
1. **`workspace-tools.ts buildDashboardClient`** — `rm dist/client`
before vite build. Prevents stale content-hash references from previous
builds.
2. **`bundle-output-helpers.ts`** — atomic `mkdirSync` file lock around
`buildCliWithRealDashboardAssets()`. Winner builds; losers poll with
`Atomics.wait`, then re-check `hasBuiltDashboardAssets()`. On timeout,
**throws** (never builds without owning the lock).
3. Lock uses `Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0,
0, 500)` for sync sleep — no child process spawning.
## Verification
- Gate: exit 0 ✅
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved dashboard asset builds by removing stale files before
rebuilding.
* Prevented concurrent builds from producing incomplete or corrupted
dashboard assets.
* Added safeguards to detect stalled asset builds and fail with clearer
errors.
* **Tests**
* Updated package validation checks to reflect current runtime bundling
behavior.
* Improved reliability of CLI build-related test execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Narrative: Reworked TaskCard footer/meta layout so the cost badge (and its sibling footer-right chips) render inline at the bottom-right of the card-meta row when the footer has no leading content, instead of always sitting in a separate footer row beside the time badge.
- Extracted the footer-right chip cluster (cost, time, retry, near-duplicate, undo-of, GitHub tracking) into a shared `footerRightCluster` render, computed once instead of duplicated inline.
- Added `footerHasLeadingContent`/`footerRightHasContent`/`placeFooterRightInMeta` derivations so the cluster moves into `.card-meta` (bottom-right, inline with other tags) when there's no files-changed button or GitHub-import leading content, and the meta row is visible; otherwise it keeps the existing `.card-footer-row` placement for in-progress/tracked cards.
- Updated dashboard-guide.md wording to describe the cost badge as appearing 'with the card's other footer/meta chips' rather than 'beside the execution-time badge'.
- Extended TaskCard.test.tsx coverage for the new placement behavior.
- Desktop local-runtime.ts: kept the previously-unused `reason` parameter on `requestRestart` explicitly referenced (void reason) for API parity/lint cleanliness, unrelated cosmetic cleanup carried in the same branch.
Files changed:
docs/dashboard-guide.md | 2 +-
packages/dashboard/app/components/TaskCard.tsx | 242 +++++++++++----------
.../app/components/__tests__/TaskCard.test.tsx | 96 +++++++-
packages/desktop/src/local-runtime.ts | 4 +-
4 files changed, 222 insertions(+), 122 deletions(-)
Fusion-Task-Id: FN-7923
Fusion-Task-Lineage: 7e4c3109-f39e-45c0-af13-358ce54f945c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
- pnpm dev / new pnpm start default to the dashboard command
- fn dashboard (and bare fn/fusion/npx, incl. packaged binaries) now runs
supervised by default via an attached foreground child (TUI-safe);
--no-supervise opts out; FUSION_RESTART_EXIT_CODE=86 = intentional restart
- New /api/system routes: info, restart, rebuild jobs with SSE output,
engine restart, agents restart-all, plugins reload-all, log tail
- System tab: rebuild & restart (source checkouts only, hidden elsewhere),
restart server/engine/agents, backup DB, live server logs, copy
diagnostics, report bug; new Plugins tab reusing PluginManager
- Desktop restart via Electron app.relaunch(); DashboardLogSink now keeps a
bounded history + listener feed for the log viewer
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broadens regression coverage for the npm-release update-check invariant so it holds across every consuming surface, not just the reported repro.
- Replace the update-check route/service semver spot-checks with a parametrized case matrix (equal, newer, older, prerelease/build metadata, short/long version segments) to close false-positive/false-negative gaps.
- Add dedicated route-level tests asserting the update-check API route surfaces the same invariant.
- Add CLI update command tests covering notification rendering across version-comparison cases.
- Add desktop native update-check tests covering the same invariant on the desktop shell.
- Add dashboard useUpdateCheck hook tests verifying consistent notification behavior for the hook consumers.
Files changed:
packages/cli/src/commands/__tests__/update.test.ts | 59 ++++++++++++++++++
.../app/hooks/__tests__/useUpdateCheck.test.ts | 25 ++++++++
.../src/__tests__/update-check-route.test.ts | 71 ++++++++++++++++++++++
.../dashboard/src/__tests__/update-check.test.ts | 42 +++++++------
packages/desktop/src/__tests__/native.test.ts | 27 ++++++++
5 files changed, 206 insertions(+), 18 deletions(-)
Fusion-Task-Id: FN-7762
Fusion-Task-Lineage: 6ab34312-fb4e-487a-aefc-2ab133bf79af
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Move the host-agnostic bundled-plugin auto-install logic (manifest loading, entry-path
resolution, install/update/enable flow) out of the CLI package into @fusion/core so the
desktop embedded runtime can auto-install bundled runtime plugins without depending on
the CLI package; the CLI module becomes a thin adapter that supplies its own bundle-dir
resolution to the shared helper.
- Add packages/core/src/plugins/bundled-plugin-install.ts with the shared, host-agnostic
ensureBundledPluginInstalled / ensureBundledDependencyGraphPluginInstalled /
ensureBundledCursorRuntimePluginInstalled implementation and BUNDLED_PLUGIN_IDS/
isBundledPluginId/resolvePluginEntryPath, exported from @fusion/core's index.
- Slim packages/cli/src/plugins/bundled-plugin-install.ts to a CLI-specific
candidate-bundle-dir resolver that delegates to @fusion/core and re-exports the same
public surface dashboard.ts/serve.ts/daemon.ts already depend on.
- Remove the now-redundant packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts
(coverage moved with the implementation to @fusion/core).
- Add packages/desktop/src/bundled-plugin-dirs.ts to resolve each bundled plugin's staged
package directory via import.meta.resolve, mirroring the CLI's dist/plugins/<id> resolver.
- Wire local-runtime.ts and local-server.ts to call ensureBundledPluginInstalled before
loadAllPlugins() and expose a lazy-install callback for PUT /api/plugins/:id/settings,
mirroring the CLI dashboard command's startup auto-install pass.
- Update docs/PLUGIN_AUTHORING.md to describe the shared bundled-plugin-install location.
Files changed:
docs/PLUGIN_AUTHORING.md | 11 +
.../__tests__/bundled-plugin-install.test.ts | 619 ++-------------------
.../resolve-plugin-entry-path-sync.test.ts | 97 ----
packages/cli/src/plugins/bundled-plugin-install.ts | 250 +--------
packages/core/src/index.ts | 8 +
.../__tests__/bundled-plugin-install.test.ts | 391 +++++++++++++
.../core/src/plugins/bundled-plugin-install.ts | 186 +++++++
.../src/__tests__/bundled-plugin-dirs.test.ts | 59 ++
.../desktop/src/__tests__/local-runtime.test.ts | 183 +++++-
.../desktop/src/__tests__/local-server.test.ts | 96 +++-
packages/desktop/src/bundled-plugin-dirs.ts | 61 ++
packages/desktop/src/local-runtime.ts | 66 ++-
packages/desktop/src/local-server.ts | 36 +-
13 files changed, 1171 insertions(+), 892 deletions(-)
Fusion-Task-Id: FN-7637
Fusion-Task-Lineage: 953c5b82-a079-4600-b3af-45c974cd5014
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the desktop app's plugin subsystem, which was never wired into createServer(), breaking Settings > Plugins Browse registry and plugin install.
- local-runtime.ts: construct a PluginStore + PluginLoader (mirroring the CLI dashboard command), load enabled plugins, run plugin schema-init hooks, and pass pluginStore/pluginLoader/pluginRunner into createServer()
- local-server.ts: apply the same wiring to the legacy desktop local server path for consistency
- Both paths fail soft: a broken plugin subsystem (e.g. corrupt manifest) is logged/traced but no longer blocks embedded dashboard startup
- Extend local-runtime.test.ts and local-server.test.ts to cover plugin wiring and the fail-soft path
- Add changeset (patch) documenting the fix
Files changed:
.changeset/fn-7623-desktop-plugin-wiring.md | 7 ++
.../desktop/src/__tests__/local-runtime.test.ts | 123 ++++++++++++++++++++-
.../desktop/src/__tests__/local-server.test.ts | 69 +++++++++++-
packages/desktop/src/local-runtime.ts | 50 ++++++++-
packages/desktop/src/local-server.ts | 41 ++++++-
5 files changed, 286 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7623
Fusion-Task-Lineage: c6f291fb-e6aa-4ac1-a3f3-4189fc831c60
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds bounded internal retry to the desktop embedded-start path so a transient first-attempt failure self-heals before the operator ever sees the "Couldn't start local Fusion" error screen.
- LocalRuntimeManager.startEmbedded() now retries startEmbeddedAttempt() up to startupRetries (default 3) total attempts with a startupRetryDelayMs (default 150ms) delay between attempts, both overridable via constructor options for deterministic zero-delay tests.
- status.state stays "starting" across retried attempts; only the final attempt's real error sets state "error" and is thrown/surfaced, so genuine failures still report their real message unchanged.
- Only affects the embedded-start path (never external-cli or already-running paths).
- Adds regression tests covering retry-then-success, exhausted-retries-surfaces-final-error, and status transitions across attempts.
Files changed:
.../desktop/src/__tests__/local-runtime.test.ts | 158 ++++++++++++++++++++-
packages/desktop/src/local-runtime.ts | 76 +++++++++-
2 files changed, 228 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-7617
Fusion-Task-Lineage: 2baf90f0-052c-42ff-a5b1-bc0b802d0ddc
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
First-run project setup surfaced a blocking "Agent with this name already exists" error when creating
the default CEO. The default first agent can be created from more than one first-run surface (the
unified ModelOnboarding agent step and the project-setup SetupWizard sub-flow), and agent names are
unique per store, so the second create returned 409. The step's goal — a first agent exists — was
already satisfied, so both onboarding surfaces now treat a name collision as success and advance
instead of blocking. The user still creates the agent; they just aren't punished for the flow offering
it twice.
Separately, the desktop header "Desktop local mode / Switch server" button did nothing: main relays
the click as a `shell:open-connection-manager` IPC (webContents.send), but the preload never forwarded
it to the `window` DOM event ShellContext listens for, so the signal was dropped. Add the preload
bridge so the button opens NativeShellConnectionManager (Local/Remote toggle + remote-server profiles),
letting operators switch between running locally, connecting to a remote server, or a different remote.
Regression tests: SetupWizard advances on an already-exists agent; preload dispatches the window event
on the IPC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #1878 (merged). While building the Windows desktop
installer locally, `pnpm deploy`'s final temp-dir rename hits `EPERM` on
the orca-managed Windows workspace mount and litters deeply-nested
`deploy_tmp_*` dirs that are painful to remove.
This lets local Windows installer builds redirect the staged production
closure to an external, plain-NTFS path via a new
`FUSION_DESKTOP_DEPLOY_DIR` env var. **CI and the default in-tree
`packages/desktop/deploy` path are unchanged** — the override is opt-in.
Verified locally: with
`FUSION_DESKTOP_DEPLOY_DIR=C:\Users\...\fusion-build\deploy`, `pnpm
--filter @fusion/desktop build` stages the full closure externally
without the EPERM race, and `electron-builder --projectDir <that dir>
--win nsis --x64` produces a signed installer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Desktop deployment staging can now be configured with an environment
variable, making it easier to customize where build files are prepared.
* **Documentation**
* Added notes about updated desktop deployment behavior on Windows,
including common rename and cleanup issues.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Add a one-time copy of the previous default Electron profile into
~/.fusion/desktop-user-data (user-data-migration.ts) so upgrading operators
keep window geometry/session instead of starting fresh (report Issue 8).
- Verify main.js/preload.js/client assets in scripts/build.ts (pre-package
staging) and in the packaged app.asar via desktop-windows.yml, so an
incomplete package fails the build/CI instead of dead-ending at runtime with
a silent missing preload (report Issue 5, recommendation #2).
- Fix two pre-existing stale workflow-assertion tests that expected a bare
'electron-builder --win' after the scripts moved to '--projectDir deploy'.
- Update the field report with a per-issue resolution matrix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Regenerate pnpm-lock.yaml for the new electron@^33.4.11 CLI dependency so
--frozen-lockfile stops failing every PR-check job.
- Gate the GPU/sandbox-disabling Electron flags (--no-sandbox, --disable-gpu,
etc.) to Windows only via os.platform(); applying them on macOS/Linux was a
security and rendering regression. This also resolves the unused os import
that failed lint.
- Add FNXC comments for the Windows GPU flags, desktop user-data isolation, and
CLI dashboard-server reuse per project comment convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnpm deploy's final temp-dir rename hits EPERM on the orca-managed Windows workspace mount and litters
deeply-nested deploy_tmp_* dirs. Let local Windows installer builds redirect the staged closure to an
external plain-NTFS path via FUSION_DESKTOP_DEPLOY_DIR; CI and the default in-tree path are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add reports/desktop-release-issues-2026-07-03.md documenting the
regressions observed in the Fusion 0.52.0 Windows desktop release,
including:
- fusion desktop fails to launch because electron is a devDependency
- native launcher walks ancestor dirs and fails on unrelated workspace JSON
- Manage Projects opens Settings instead of overview
- Windows Terminal Help version dialogs on dashboard load
- packaged preload.cjs missing in unpacked release layout
- port collisions and dashboard/gateway drift on Windows
- GPU/sandbox rendering instability on Windows Electron
- isolated user-data path needed
- CLI desktop command not reusing an already-running dashboard server
Also include the two experimental mitigations we applied locally:
- disable GPU/sandbox Electron flags in desktop.ts
- skip embedded local runtime when FUSION_SERVER_PORT is already set
These changes are intended as supporting evidence and starting points
for the Fusion team, not as a final fix.
createServer's modelRegistry is optional and, when absent, the /api/models endpoint returns an
empty list — so after connecting a provider (e.g. Anthropic) the onboarding model picker showed
"no models". The desktop wired authStorage but no ModelRegistry.
Add a shared createFusionModelRegistry(authStorage) factory to @fusion/engine (which already depends
on @earendil-works/pi-coding-agent) so non-CLI hosts don't need that dep directly, and pass its
result as modelRegistry from both desktop server paths. Verified against a clean embedded server:
/api/models now returns 24 Claude models instead of an empty list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
local-server now imports createFusionAuthStorage from @fusion/engine; add it to the test's engine
mock so the startup path resolves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop embedded server called createServer WITHOUT an authStorage, so GET /api/auth/status
returned 500 "Authentication is not configured". The dashboard's first-run onboarding hook
(useAuthOnboarding -> fetchAuthStatus) catches that failure silently and never opens the model
onboarding wizard — so the desktop skipped straight to project creation with no AI/GitHub setup,
regardless of onboarding-completion flags (which is why clearing them did nothing). Providers also
couldn't be authenticated at all.
Wire createFusionAuthStorage() (the same storage the CLI dashboard/serve commands use) into both the
primary (local-runtime) and legacy (local-server) desktop server paths. Verified against a clean
embedded server: /api/auth/status now returns 200 with the provider list instead of 500. (Full
API-key provider wrapping stays CLI-only for now; OAuth + CLI providers are available on desktop.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DesktopLaunchGate: the chooser onPick("local") path still called the removed
applyServerBaseUrl helper (broke Typecheck and the desktop local chooser flow) —
call navigateToLocalRuntimeOrigin. Also re-read shell.getState() immediately before the
self-healing start check so a stale mount snapshot doesn't fire a redundant
setDesktopMode("local") on normal boots (Greptile).
- register-fn-binary-routes: on Windows the npm install runs under a shell, so a timeout's
child.kill only stopped cmd.exe and left npm.cmd/node running — kill the whole process tree
via taskkill /T on win32 (CodeRabbit).
- local-server.test: align with the new resolver contract — assert the runtime never
auto-registers the root project and starts engine-less when no projects exist (CodeRabbit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop embedded runtime auto-registered its runtime root (the user's HOME directory) as a
project on first launch, and bare `fusion` / `fn` / `fn dashboard` / `fusion dashboard`
auto-registered the CWD as a project. Both silently created a "cwd-mode" project the operator
never chose and dropped them onto a board for it.
- Desktop: replace ensureDesktopRuntimeProject (which registered home) with
resolveDesktopRuntimePrimaryProject, which only PICKS an already-registered project as the
primary engine target and registers nothing. With no projects the server starts engine-less
(createServer's engine is optional) and the dashboard shows its onboarding empty state.
Applied to both the primary (local-runtime) and legacy (local-server) desktop server paths.
- CLI dashboard command: ensureCwdProjectRegistered now runs with autoRegister:false, so it uses
the CWD project only if already registered, else starts with none and the dashboard onboards.
(serve/daemon keep their existing --no-auto-register flag; the CLI `desktop` launcher unchanged.)
Verified: with zero projects the embedded server starts, /api/health -> 200, /api/projects -> [],
/ serves the client. Unit test asserts resolveDesktopRuntimePrimaryProject registers nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Desktop startup used two independent persisted sources of truth that could
disagree: desktop-launch-mode.json decided whether main STARTS the embedded
local runtime, while shell-connections.json (desktopMode) decided whether the
renderer launch gate WAITS for it. shell:setDesktopMode persists shell settings
before the fallible startLocalRuntimeOnce()/saveDesktopLaunchMode(), so a first
"local" selection whose runtime start failed or was interrupted left
shell=local / launch-mode=choose permanently. Every subsequent launch then sat
at "Starting local Fusion runtime…" polling a runtime nobody started, timing out
after 30s.
Fix (defense in depth):
- initializeApp reconciles: a completed shell "local" selection is authoritative;
it heals the launch-mode file and starts the runtime.
- onDesktopModeChange/onDesktopLaunchModeChange persist launch-mode BEFORE the
fallible start so an interrupted start cannot re-create the desync.
- DesktopLaunchGate no longer assumes main started the runtime; if it is not
running/starting it actively (re)starts via setDesktopMode("local") before polling.
- Add env-gated startup trace (FUSION_STARTUP_TRACE) so packaged builds, which
otherwise log nothing, can diagnose this class of stall.
Regression tests assert the invariant (split-brain -> runtime starts + file heals;
agreement-on-choose -> no start). flushPromises now drains via a macrotask so
run()-based tests observe a fully-initialized app regardless of async chain length.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Desktop "Local" mode crashed with ERR_MODULE_NOT_FOUND because electron-builder's
pnpm collector drops `deduped` subtrees, so engine's transitive closure
(@modelcontextprotocol/sdk, pi-ai provider SDKs, etc.) was never packed. Stage the
complete flat prod closure with `pnpm deploy --legacy --config.node-linker=hoisted`
and package it via `electron-builder --projectDir deploy`, bypassing the lossy
collector entirely.
Also make the dashboard-imported example plugins loadable under plain Node (the
Electron main runtime): cursor/droid/roadmap now expose compiled `dist` on the
`import` condition (keeping `source`→src for the bun CLI) and are built during the
desktop build. Add `source` conditions to paperclip/agent-browser/even-cards/
even-realities-glasses/whatsapp-chat so the bun `--conditions=source` Windows CLI
compile resolves them from source.
Validated on macOS: @fusion/core|engine|dashboard import cleanly from the staged
deploy; packing yields a complete 705-package asar; bun-windows-x64 cross-compiles
with all plugin dist removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The packaged desktop "Local" runtime dynamically imports @fusion/engine, whose
tsc dist is gitignored. desktop-windows.yml built only `@fusion/desktop build`
(no root `pnpm build`), so it packaged an empty engine/dist and the app crashed
on Local mode with ERR_MODULE_NOT_FOUND for app.asar/node_modules/@fusion/engine.
Make the desktop build self-contained (build core then engine before packaging)
and add the parity `pnpm build` step to desktop-windows.yml.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>