Resolve direct binary manifests before scanning shim contents so dashboard status polling does not run the expensive shim regex against JavaScript entrypoints.
## Why
The Import Tasks panel routinely lists issues in languages the operator
cannot read. Translation already shipped in #2128, but deliberately
**opt-in and preview-only** — its header comment read *"Translation is
opt-in (never automatic) so import provenance stays faithful until the
operator asks."*
This reverses that decision **behind a default-off setting**, so
operators who never opt in keep byte-faithful import provenance. The
superseded comment is kept and annotated rather than deleted, so the
reason the rule changed stays in the code.
### The structural gap #2128 left
`POST /github/issues/import` accepts only `{owner, repo, issueNumber}`
and **re-fetches the issue server-side**. A translation held in React
state could never reach the created task, and the in-memory cache died
with the modal. That is why the cache here is server-side rather than in
the hook — it's what makes "imported issues carry the translated
version" actually true.
## What operators get
Auto-translate is **off by default**. When enabled:
- The **50 most recent OPEN** foreign-language issues translate on panel
load — **list titles**, not just the preview, so the list reads in your
language before you click anything.
- Translations show **by default**, with a toggle back to the original
(hover a translated list title to see the original).
- Translations **persist until the issue closes**, so re-opening the
panel neither waits nor re-bills.
- **Both single and batch import** carry the translation, so the created
task reads like the preview you approved.
- A **target language** setting (unset = follow the dashboard language)
and a dedicated **model lane**, so you can pin a cheap/fast model
without dragging the summarization lane onto it.
## Notable decisions
| Decision | Why |
|---|---|
| Detect **before** the model | An issue already in the target language
is never sent. Without this, an English repo with the setting on would
bill every issue to return its input unchanged. |
| Detection moved to `@fusion/core` | The panel and the server must not
disagree about which issues are foreign; two copies of a heuristic
drift. |
| Own rate-limit budget | Translation shared a 10/hour budget with
refine/goal-draft. Fanning out per-issue would fail partway **and**
starve refine for the hour. |
| Cache keyed on a **source hash** | An edited issue misses the cache
and re-translates instead of serving stale prose. |
| Import is **cache-read only** | A miss imports the original. Import
must never block on, or fail because of, translation. |
| `project_id` leads the cache PK + full RLS contract | All projects
share one flat `project` schema. `verification_cache`'s PK predates that
discipline; this table does not copy that mistake. |
## Verification
- ✅ `pnpm lint`, `@fusion/core` + `@fusion/dashboard` typecheck
- ✅ `pnpm verify:fast` — build + scoped typecheck + real boot smoke
(`/api/health`)
- ✅ `pnpm test:gate` — 479 tests
- ✅ 19 new tests covering the billing invariants
(off/closed/same-language ⇒ **no model call**), cache hit/miss-on-edit,
the 50 cap, and per-item fail-soft
- ✅ `schema-applier` real-Postgres suite (46 tests) exercises migration
`0010` and its isolation invariant
**Pre-existing failures NOT touched** (confirmed red on `HEAD` before
this branch): `AppearanceSection`'s task-popup test, and two PG-cutover
keys (`sqliteMigrationNotice`, `postgresMigrationInboxMessageSentAt`)
missing description mappings. I left the latter rather than guess an
allowlist entry that could mask a real coverage gap.
## Reviewer notes
- Short Latin-script prose (a one-line Spanish title) rates only
*medium* confidence and won't auto-translate — the existing heuristic is
deliberately conservative so English issues are never billed. CJK
detects regardless of length. The threshold is the knob if you'd rather
bias toward translating.
- The RLS/isolation contract in migration `0010` is the part most worth
a careful look.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Speeds up **time-to-HTTP-ready** for `fn dashboard` and `fn serve` after
the PostgreSQL cutover without reintroducing the historical 3s
cwd-engine race that degraded webhooks.
- **Dashboard store share (serve parity):** inject the factory-booted
`TaskStore` as `externalTaskStore` so cwd `ensureEngine` does not open a
second pool; share only when store root matches project working
directory (multi-project safe).
- **Serve multi-project:** stop awaiting `startAll()` before listen;
await only the primary engine; background the rest + reconciliation.
- **Defer non-route-critical engine work:** ordered OAuth (refresh →
monitor), automation schedule syncs, and auto-merge **enqueue** after
the engine handle is returnable.
- **Critical-path merge status clear:** still clear stale
`merging`/`merging-pr` before ready so manual merge is not blocked after
crash.
- **Serve `--paused`:** apply `enginePaused` before
`ensureEngine`/`startAll` (dashboard ordering).
- **Stop safety:** generation counter so deferred tails cannot resume
after `stop()` clears `shuttingDown`.
- **Phase timing:** shared `phaseTime` helper, factory substep logs,
serve time-to-listen.
Plan: `docs/plans/2026-07-14-001-feat-faster-startup-plan.md`
## Test plan
- [x] `packages/engine` — `project-engine-manager.test.ts` (path-matched
external store)
- [x] `packages/engine` — `project-engine-deferred-startup.test.ts`
(status clear, OAuth order, stop generation)
- [x] `packages/cli` — `startup-phase.test.ts`
- [x] `packages/cli` — `serve.test.ts` (60 tests, including `--paused`)
- [ ] Local: warm `fn dashboard` / `fn serve` and compare `startup phase
*` / `time-to-listen` logs
- [ ] `pnpm smoke:boot` (real serve `/api/health` on ephemeral port)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Performance**
* Improved dashboard and serve startup times, including faster
time-to-listen and time-to-ready.
* Moved non-essential background initialization off the critical startup
path.
* Parallelized dashboard service initialization where possible.
* **Reliability**
* Improved multi-project startup handling and project selection.
* Prevented cross-project task-store sharing.
* Added safer shutdown behavior for partially completed startup.
* **Diagnostics**
* Added startup phase timing logs to help identify performance
bottlenecks.
* **Tests**
* Expanded coverage for deferred startup, shutdown, project isolation,
and startup timing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- resolve the configured planning model when agent onboarding requests
omit an explicit override
- align the onboarding prompt with supported runtime/model hint fields,
allowing AI-created agents to select runtimes such as Hermes
- refresh the generated GitHub issue import limits required by the
repository sync gate
## Root cause
The agent onboarding route loaded project settings but passed only
request-body model fields. The AI Interview UI omits those fields, so
`createFnAgent` was called with `provider=undefined, model=undefined`;
the session returned no usable assistant JSON. The prompt catalog also
prohibited `runtimeHint` despite the parser and form already supporting
it.
## Verification
- targeted agent onboarding tests: 22 passed
- `pnpm --filter @fusion/core typecheck`
- `pnpm --filter @fusion/dashboard typecheck`
- `pnpm lint`
- `pnpm build`
- `pnpm smoke:boot`
- engine merge-gate subset: 294 passed
Full `pnpm test` reached the PostgreSQL gate but this host has no `psql`
binary, so 23 PostgreSQL suites could not start; this is an environment
prerequisite failure, not a test assertion failure.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Agent onboarding interviews now use the configured planning model when
no override is provided.
* Runtime suggestions and runtime-hint guidance are preserved during
onboarding and reflected in generated configurations.
* On onboarding start streaming, planning provider/model resolution now
comes from settings with stricter override validation, and test mode
continues to take priority.
* **Documentation**
* Updated onboarding prompt guidance to support additional configuration
fields and optional runtime draft hints.
* Reduced the maximum GitHub issue import/browse limit from 100 to 50.
* **Tests**
* Added coverage for runtime-hints prompting and planning-model override
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Treat **shared PostgreSQL** (`DATABASE_URL`) as the multi-node durable
data plane; mesh HTTP is membership + optional auth, not task/settings
replication.
- **Peer exchange**: under Postgres backend mode, write queue is
**topology/auth-only**; non-topology pending rows fail rather than
replaying multi-leader task/settings payloads.
- **Mesh routes**: task-ID reserve/commit/abort always hit local shared
allocator rows (ignore remote `coordinatorNodeId`); mesh sync ignores
settings and only exchanges `authMaterial`.
- **Docs**: rewrite multi-project runbook, shared cluster protocol, and
architecture mesh sections for shared-Postgres + claims/leases.
## Context
Follows the SQLite→Postgres cutover. Multiple Fusion nodes can share one
external Postgres while keeping **per-node execution** (worktrees,
processes, claims via `central.task_claims`). Explicit non-goals remain:
scheduler failover and live process migration.
Plan:
`docs/plans/2026-07-15-001-refactor-mesh-shared-postgres-multinode-plan.md`
## Test plan
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/peer-exchange-service.test.ts`
- [x] `pnpm --filter @fusion/dashboard exec vitest run
src/__tests__/mesh-routes.test.ts`
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/shared-mesh-state.test.ts`
- [ ] CI gate (lint/typecheck/build/gate)
- [ ] Manual (optional): two processes, same `DATABASE_URL`, create task
on A visible on B; settings change without mesh settings sync; claim
exclusivity
## Operator note
Multi-node shared board requires **external** `DATABASE_URL` on every
node. Default embedded Postgres is still single-host.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved multi-node deployments using shared PostgreSQL as the durable
source of execution state.
* Task ID reservation/commit/abort now run locally (no remote
coordinator forwarding).
* Mesh syncing now prioritizes topology visibility and authentication
material; settings replication is disabled in shared-Postgres mode.
* **Bug Fixes**
* Prevented task/settings replication over mesh HTTP in shared-Postgres
deployments.
* Refined lease ownership, recovery, and reconciliation to converge via
shared-database primitives.
* **Documentation**
* Updated architecture and shared-mesh protocol guidance, including
multi-node setup and lease/task-ID allocation behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
A rate-limited reviewer filled a task's Chat tab with 14 identical
"Reviewer using model: ..." markers and no review text, hammering an
already-limited provider.
Root cause: the reviewer was the only AI lane that never classified
provider errors, so a 429 became an UNAVAILABLE verdict. With no
validator fallback configured the fallback ladder re-ran the SAME model
instantly, and fn_review_step answered with "code review remains
blocking; retry once" — bounding the loop with prompt text rather than
code. The tool's catch-all also swallowed the error into tool output, so
withRateLimitRetry, UsageLimitPauser and RetryStormError never fired.
- reviewer: throw ReviewerProviderError for usage-limit/transient errors
instead of laundering them into UNAVAILABLE, and never spend the
fallback budget (which bounds bad reviews) on an outage.
- reviewer: absorb flaky-network blips in-lane via withRetry with
jittered backoff; rate limits still escalate immediately.
- executor: re-raise the fatal after the prompt via
throwDeferredReviewerFatal — pi-agent-core converts tool throws into
tool_error results, so a tool cannot throw out of session.prompt().
- executor: give code review a real MAX_CODE_REVIEW_UNAVAILABLE_RETRIES
counter, mirroring the plan/spec limiter.
- reviewer: dedupe the model marker on text, so same-model retries stay
silent while a genuine model switch still emits.
Also fixes the run-on rendering: AgentLogType gains `status` for complete
engine messages. `text` means "streamed delta" and is re-glued with
join(""), which is why N standalone markers rendered as one string. The
split is at the type, not a separator — a separator would reintroduce the
FN-5787/5789/5803 streamed-spacing regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make soft-delete return after the DB mutation while branch and agent cleanup run in the background.
- Schedule cleanupBranchForTask after the soft-delete transaction instead of awaiting it under withTaskLock
- Persist cleaned-branch log entries on the deleted row asynchronously; warn on deferred failures
- Respond from DELETE /tasks/:id after deleteTask and schedule execution-agent binding release off the HTTP path
- Add core and dashboard regression tests for non-blocking delete cleanup
- Document the fast-path contract in architecture.md and add a patch changeset
Files changed:
.changeset/fn-7968-task-delete-latency.md | 7 +
docs/architecture.md | 1 +
.../task-delete-nonblocking-cleanup.test.ts | 160 +++++++++++++++++++++
packages/core/src/task-store/archive-lifecycle.ts | 57 +++++++-
.../routes-task-delete-nonblocking.test.ts | 139 ++++++++++++++++++
.../src/routes/register-task-workflow-routes.ts | 19 ++-
6 files changed, 370 insertions(+), 13 deletions(-)
Fusion-Task-Id: FN-7968
Fusion-Task-Lineage: f218a91e-aee3-46c9-a80f-182751b3ccc4
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Reconcile active task segment anchors on full Global/Engine unpause so stopped-engine wall-clock does not inflate execution time, reusing the FN-7011 downtime path with a transition-captured heartbeat.
- Pass optional engineLastActiveAtOverride into reconcileActiveTimingForEngineDowntime so unpause callers freeze the stopped-window proof against racing scheduler heartbeats
- Await downtime reconciliation in resumeAfterUnpauseAndSweepInReview before resuming agentic work or sweeping in-review tasks
- Fold Global/Engine unpause into the unified pause-lifecycle listener (single reconcile when both clear together; no-op while either pause remains)
- Soft-fail reconcile errors so unpause resume still proceeds
- Add store and project-engine coverage for override, await-before-resume, dual-source clear, and fail-soft paths; document FN-7975 in AGENTS.md run-audit notes
- Add patch changeset for the operator-facing timing fix
Files changed:
.changeset/fn-7975-engine-pause-active-timing.md | 7 ++
AGENTS.md | 2 +-
.../core/src/__tests__/store-active-timing.test.ts | 86 +++++++++++++
packages/core/src/store.ts | 23 ++--
.../project-engine-unpause-active-timing.test.ts | 94 ++++++++++++++
.../engine/src/__tests__/project-engine.test.ts | 139 +++++++++++++++++++++
packages/engine/src/project-engine.ts | 64 +++++-----
packages/engine/src/self-healing.ts | 6 +-
8 files changed, 378 insertions(+), 43 deletions(-)
Fusion-Task-Id: FN-7975
Fusion-Task-Lineage: 84a46e6f-92bf-452a-ab67-c25ba85cbffb
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Teach idle heartbeat patrol prompts to skip fn_task_create when recent model-availability failures are visible, and to base progress claims only on board state fetched in the current heartbeat.
- Add standard and concise triage heartbeat guidance to check for model-availability, fallback exhaustion, 429/rate-limit, and 404/model-unavailable failures before creating work
- Require existing-task progress/status claims to come from fn_task_list or fn_task_show results in the current heartbeat run
- Extend agent-prompts tests to cover both template variants
- Add patch changeset for the published package
Files changed:
.changeset/fn-7962-idle-heartbeat-patrol-backoff.md | 7 +++++++
packages/core/src/__tests__/agent-prompts.test.ts | 14 ++++++++++++++
packages/core/src/agent-prompts.ts | 8 ++++++++
3 files changed, 29 insertions(+)
Fusion-Task-Id: FN-7962
Fusion-Task-Lineage: 0a233b5d-00d8-4dda-b627-95d4b8122a55
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Plan Review REVISE previously fed feedback without the rejected plan body, so triage rewrote from title/description and looped. Seed the draft for surgical revision, use reviewType spec for the pre-execution gate, and tighten planner/reviewer prompts toward blocking-only REVISE with concrete edits.
Give planner/reviewer pairs more room to converge before escalating, and surface why a task is parked for plan approval—especially plan-review-replan-cap non-convergence—on cards, detail, and notifications.
AI merge spends most of its time in reviewing and landing, not merging.
Treat the full merge pipeline as active so cards, workflow switcher, and
stall suppression show Merging… while the pump owns a task.
## Summary
- Extend the workspace content-hash skip cache to **all** packages (not
just plugins), with `--force` / `--full` flags
- Default local CLI packaging to a **fast mode** (bin/extension +
migrations only); full desktop/plugin/DTS staging runs on CI or `pnpm
build:full`
- Enable TypeScript `incremental` builds for warm recompiles
- Add `maxConcurrentVerifications` (default **1**) so concurrent tasks
cannot stack monorepo typecheck/build and peg CPU
Warm `pnpm build` measured ~**126s → ~0.8s** when nothing changed.
## Test plan
- [x] `node --test scripts/__tests__/build-workspace.test.mjs` (12 pass)
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/verification-concurrency.test.ts`
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/settings-parity.test.ts`
- [x] Local: first `pnpm build` rebuilds as needed; second warm `pnpm
build` skips all packages (~0.8s)
- [x] Fast CLI packaging logs skip of desktop/plugin staging without
`FUSION_CLI_FULL_PACKAGE`
- [ ] CI: `pnpm build` still full-packages under `CI=true` (plugin
staging / release surfaces)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a Scheduling setting to limit concurrent verification tasks from
1–8, with a default of 1.
* Verification tasks now support cancellation while waiting or running.
* Added options for forced and full workspace builds.
* **Performance**
* Local builds can skip unchanged packages and use incremental
compilation for faster rebuilds.
* Local CLI packaging is faster by default, while full packaging remains
available when needed.
* **Documentation**
* Updated the settings reference with the new verification concurrency
option.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Generated PROMPT.md (after triage/planning — not the bootstrap stub) now
keeps the operator's original task description near the top under `##
Original Description`, so executors always see the source request even
after Mission/Steps rewrites.
- **AI-planned path:** planning templates (standard/fast/concise)
require a verbatim `## Original Description` section;
`buildSpecificationPrompt` instructs the planner; `finalizeApprovedTask`
deterministically injects/rewrites it as hygiene.
- **Non-AI path:** `generateSpecifiedPrompt` uses the same pure helper
so direct creates into non-intake columns get the same contract.
- **Description edits:** real specs keep `## Original Description` in
sync when `task.description` changes.
- **Unchanged:** bootstrap stubs and `isUnplannedSeedPrompt` equality
detection.
## Surfaces
| Surface | Change |
|--------|--------|
| `original-description-policy.ts` | Shared inject/rewrite helper |
| `agent-prompts.ts` | Template + requirement text |
| `triage.ts` finalize + `buildSpecificationPrompt` | Instructions +
post-write pin |
| `generateSpecifiedPromptImpl` | Non-AI specified PROMPT.md |
| `task-update.ts` | Description sync on real specs |
## Test plan
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/original-description-policy.test.ts
src/__tests__/agent-prompts.test.ts
src/__tests__/mesh-task-replication.test.ts
src/__tests__/store-create-intake-column.test.ts --silent=passed-only
--reporter=dot`
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/triage.test.ts -t "Original Description|injects ##
Original" --silent=passed-only --reporter=dot`
- [ ] CI gate (Lint / Typecheck / Build / Gate)
## How to verify manually
1. Create a task with a distinctive description, let triage plan it (or
finalize a mock plan).
2. Open `.fusion/tasks/<id>/PROMPT.md` and confirm `## Original
Description` appears after title/metadata with the raw description,
before Mission / Before → After.
3. Direct-create into `todo` (non-intake) and confirm the non-AI
generated prompt also has the section.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Generated `PROMPT.md` specifications now include an `## Original
Description` section near the top.
- Operator task descriptions are preserved verbatim for AI-planned and
specified prompts.
- Updated prompts remain synchronized when task descriptions change.
- **Bug Fixes**
- Replaced paraphrased original descriptions with the correct task
description.
- Preserved existing prompt content during review and retry workflows.
- **Tests**
- Added coverage for placement, formatting, replacement, and idempotent
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Bundled plugins now persist shared runtime state in project-scoped
PostgreSQL tables instead of maintaining independent SQLite authority.
Reports, CLI Printing Press, Compound Engineering, Roadmap, Even
Realities, and WhatsApp all follow the same ownership and startup
contract as Fusion core.
## Design decisions
- Plugin schema hooks run through the host’s PostgreSQL owner and
enforce project isolation.
- The SDK exposes the host contract needed by bundled plugins without
importing engine internals.
- Legacy Roadmap ownership fixtures use the supported empty-owner
sentinel, preserving current composite primary/foreign keys while
exercising backfill behavior.
- The lockfile travels with the Even Realities PostgreSQL dependency so
packaged installs remain reproducible.
## Validation
- All six affected plugin builds pass.
- Affected plugin suites pass: 773 tests across Printing Press, Compound
Engineering, Even Realities, Reports, Roadmap, and WhatsApp.
- `pnpm test:gate` passes all 478 gate tests.
- This PR changes 40 files.
## Stack
- Depends on #2110 → #2109 → #2108.
- The documentation/release PR completes the stack.
Related: #2105
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Breaking Changes**
* PostgreSQL is now required for runtime storage; SQLite files are used
only as one-time migration inputs.
* The legacy `FUSION_NO_EMBEDDED_PG` fallback has been removed.
* **New Features**
* Added project-isolated PostgreSQL storage for plugins, reports, tasks,
notifications, and other plugin data.
* Added agent tools for reports and CLI service drafts.
* Added PostgreSQL schema initialization support for plugin authors.
* **Bug Fixes**
* Improved migration and recovery of legacy plugin state.
* Prevented cross-project data access and strengthened transactional
schema updates.
* **Documentation**
* Updated storage, migration, deployment, plugin authoring, CLI, and
dashboard guidance for PostgreSQL.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Windows embedded Postgres verification (CI `windows-latest` and elevated
desktop) fails because PostgreSQL refuses to run under an administrative
token:
> Execution of PostgreSQL by a user with administrative permissions is
not permitted.
GitHub Actions runners execute as `runneradmin` elevated, so the
existing `test:embedded-postgres` smoke (and any elevated Local-mode
desktop launch) cannot start the server.
### Fix
- When `isWindowsElevatedAdmin()` is true, **initdb / clients stay as
the launcher**, but the **postgres server** is started as a dedicated
non-admin local user (`fusion-pg`) via PowerShell `Start-Process
-Credential`.
- Readiness waits on the postgres log line `database system is ready to
accept connections` with a lightweight poll (no per-iteration
`tasklist`).
- Real-process vitest cases use a **180s** timeout on Windows (package
default is 15s, which killed healthy boots mid-start).
- Builds on top of the packaged-desktop asar materialization work
already on main (#2106).
## Test plan
- [x] `pnpm --filter @fusion/core test:embedded-postgres` on macOS
(33/33)
- [ ] `desktop-windows.yml` on `feature/win-pg-verify`:
- [ ] Smoke embedded Postgres on Windows
- [ ] Build + package Windows EXE
- [ ] Verify app.asar assets
- [ ] Optional: download portable EXE and manual Local mode smoke on a
Windows host
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Improved embedded PostgreSQL startup on Windows when Fusion runs with
elevated administrator privileges.
- When elevated, the embedded database now boots under a dedicated
non-administrator local account, with more reliable readiness detection,
logging, and shutdown cleanup.
- Enhanced database provisioning and now prefers `127.0.0.1` for Windows
connection addressing.
- **Tests**
- Added coverage for Windows elevation detection without starting
embedded PostgreSQL.
- Increased platform-dependent timeouts for embedded real-process tests
to avoid premature failures on Windows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Engine and dashboard traffic now stays on the authoritative PostgreSQL
layer across execution, recovery, project discovery, planning sessions,
analytics, and shutdown. The dashboard no longer presents a migration
notice for a cutover that is already mandatory.
## Design decisions
- Runtime composition requires an async data layer instead of
constructing a hidden SQLite fallback.
- Engine workflow, mission, claim, and self-healing reads await their
PostgreSQL-backed store contracts.
- Project-scoped dashboard stores retain and close their backend owner
exactly once.
- The dashboard test quarantine entry remains paired with its Vitest
exclusion, preserving the repository’s deletion-ratchet policy.
## Validation
- Core, Engine, Dashboard, CLI, and Desktop typechecks pass on the
stacked branch.
- `pnpm test:gate` passes all 478 gate tests.
- This PR changes 62 files.
## Stack
- Depends on #2108.
- CLI/desktop/ops, plugins, and docs/release follow in later PRs.
Related: #2105
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Project discovery now recognizes projects using the
`.fusion/project.json` marker.
* Knowledge indexing and search are more reliable across project-scoped
storage.
* **Bug Fixes**
* Improved session, audit timeline, approval, monitoring, and analytics
data consistency.
* Prevented stale planning-session updates and project-store shutdown
races.
* Ensured chat usage and CLI session status are saved before continuing.
* **UI Changes**
* Removed the storage migration notice banner now that the PostgreSQL
transition is complete.
* **Reliability**
* Improved shutdown handling, workflow execution, and worktree behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Fusion’s core runtime now treats PostgreSQL as the authoritative
metadata store without leaving current CLI, dashboard, desktop, or
engine composition roots uncompilable between stack layers. This is the
99-file foundation for the larger cutover: subsequent PRs migrate the
remaining consumers, plugins, and operator surfaces.
## Design decisions
- Runtime store construction fails closed when an asynchronous
PostgreSQL layer is unavailable; SQLite remains readable only at
explicit migration and identity-recovery boundaries.
- Project ownership is enforced across active, archived, workflow,
mission, analytics, and plugin-schema data.
- The small set of cross-package files in this layer are
compatibility-critical call sites required for a green intermediate
commit, not the complete consumer migration.
- Schema migration 0008 remains assigned to session-advisor state from
current `main`; mission lineage idempotency advances to 0009 so neither
invariant can be skipped.
## Validation
- All affected package typechecks pass: Core, Engine, Dashboard, CLI,
and Desktop.
- `pnpm test:gate` passes: 478 tests across the engine gate, PostgreSQL
core gate, and CLI workflow shape.
- The PR changes exactly 99 files.
## Stack
This is the base PR. Engine/dashboard, CLI/desktop/ops, plugins, and
docs/release follow as stacked PRs, each below 100 changed files.
Related: #2105
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* PostgreSQL is now the standard runtime backend, with embedded
PostgreSQL enabled by default.
* Added project-scoped storage for tasks, archives, chat sessions,
missions, knowledge pages, and operational data.
* Improved archived-task search, filtering, pagination, and restoration.
* Added safer plugin schema initialization with validation and project
isolation.
* Added PostgreSQL-backed workflow, mission, validator, and dashboard
capabilities.
* **Bug Fixes**
* Improved startup timeout cancellation and resource cleanup.
* Prevented cross-project data access and phantom reservation cleanup
errors.
* Ensured archived tasks remain read-only and asynchronous writes
complete reliably.
* Retired SQLite opt-out settings with clear startup errors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Fixes `CREATE ROLE fusion_runtime` race condition in migration
`0006_project_ownership.sql` that causes 30 compound-engineering test
failures on CI.
## Root Cause
Concurrent test databases on the same PostgreSQL service container race
on `CREATE ROLE fusion_runtime`: the `IF NOT EXISTS` check is not atomic
(roles are cluster-wide, not per-database). Between the check and the
`CREATE ROLE`, another session can create the role, causing error
`23505` (unique_violation).
## Fix
Replace the non-atomic `IF NOT EXISTS` guard with a `BEGIN...EXCEPTION
WHEN duplicate_object OR unique_violation THEN NULL; END;` block that
safely handles the race.
## Verification
| Check | Result |
|---|---|
| compound-engineering (pipeline-store + orchestrator + session-routes)
| ✅ 41 passed |
| Engine shard 1/2 | ✅ 3826 passed, 0 failed |
| Merge gate | ✅ 471 passed |
| Lint | ✅ exit 0 |
## Summary
Packaged Fusion desktop Local mode failed after the SQLite→Postgres
cutover:
1. **Embedded Postgres** could not start from `app.asar` — platform
packages resolve `initdb`/`postgres` via `import.meta.url` into the asar
virtual path, and `spawn` fails with `ENOTDIR`.
2. **After Postgres was fixed**, Local mode still fell back to the mode
chooser because `@fusion-plugin-examples/omp-runtime` was never built
into `dist/` (dashboard imports it from `runtime-provider-probes.ts`).
This PR makes packaged Local mode boot embedded Postgres reliably and
keep the dashboard shell up.
### Changes
- **CJS bootstrap** (`main-bootstrap.cjs`) as Electron `main`: patches
`child_process.spawn` / `fs.promises.stat|chmod` before the ESM main
loads so asar binary paths rewrite to real files.
- **Materialize** the full native PG install (`bin` + `lib` + `share`)
under `~/.fusion/embedded-postgres/runtime-bin/<plat-arch>/`.
- **electron-builder**: full `asarUnpack` of embedded-postgres packages;
allowlist PG deps and `@fusion-plugin-examples/**/*` (+ plugin-sdk / ACP
SDK).
- **Build** `fusion-plugin-omp-runtime` with the other dashboard-static
runtime plugins; export `DASHBOARD_RUNTIME_PLUGIN_PACKAGES` for tests.
- Unit coverage for asar path rewrite, packaging allowlists, and omp
build inclusion.
## Test plan
- [x] `pnpm --filter @fusion/core test:embedded-postgres` (23/23)
- [x] Desktop packaging unit tests (`build-bundling`,
`electron-builder-config`)
- [x] Packaged macOS `Fusion.app` Local mode:
- [x] `embedded postgres: ready on port … (database "fusion")`
- [x] `desktopMode` stays `"local"` (no chooser fallback)
- [x] `GET /api/health` → `status: ok`, `database.healthy: true`,
`engine.available: true`
- [x] Linux embedded binary lifecycle smoke (Docker aarch64,
`@embedded-postgres/linux-arm64`) — initdb/start/persist/restart
- [ ] CI release desktop jobs (macOS/Linux) when this lands
- [ ] Windows packaged desktop Local + PG (separate agent / host)
## Verification notes
| Platform | Embedded Postgres | Packaged Local shell |
|----------|-------------------|----------------------|
| macOS | Working | Working after this PR |
| Linux | Native binary smoke pass | Full AppImage not built on this
host |
| Windows | Out of scope here | Separate verification |
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved embedded PostgreSQL reliability in Electron-packaged apps by
rewriting bundled `app.asar` binary paths to their unpacked/materialized
locations.
* Ensured embedded PostgreSQL runtime binaries resolve correctly across
platforms/architectures, with best-effort executable permissions and
macOS dylib link normalization.
* **Packaging**
* Updated the desktop Electron entry to use a bootstrap module for
embedded PostgreSQL binary resolution.
* Expanded Electron Builder inclusion and asar-unpack rules for
embedded-postgres and related packages, plus required runtime plugin/sdk
assets.
* **Tests**
* Updated and added checks to match the new packaging and plugin/runtime
expectations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
Reported: planning gets stuck in a cycle of retrying and regenerating
after a response was already supplied.
After the user answers a planning question, `submitResponse` pushed the
answer to history but left `session.currentQuestion` pointing at the
just-answered question for the whole next generation. The planning SSE
route's catch-up path re-emits `currentQuestion` to every fresh
connection — and each FN-7946 auto-retry (#2073) opens a fresh
connection. So after any generation error:
1. Auto-retry connects a fresh stream → the server re-emits the
**already-answered** question.
2. The client treats any question event as progress: it **resets the
3-attempt auto-retry budget** and re-shows the answered question.
3. The retry regenerates; if it errors again the cycle repeats with a
fresh budget — an unbounded retry/regenerate loop. Re-answering the
stale question also 409-collided with the in-flight generation, feeding
the same loop.
## Fix
Invariant: `currentQuestion` is only set while the session is genuinely
awaiting user input.
- `submitResponse` clears it the moment an answer is accepted (normal
turns and the deepening checkpoint), while preserving the legacy 200
respond contract on generation failure (the modal ignores the body and
lets the SSE error drive recovery).
- `retrySession` scrubs stale questions persisted by pre-fix builds
before regenerating.
- `buildSessionFromRow` only restores a question when the persisted row
is `awaiting_input`.
- `didSubmitSameAnswer` now compares against the last history entry so
the duplicate-submit 409 message survives.
- Agent onboarding gets the same fix (its SSE route also re-emits
`currentQuestion` on connect); retry now asks the next question instead
of re-asking the answered one.
Surface enumeration: mission and milestone interviews keep questions the
same way but their SSE routes never re-emit on connect, and the
auto-retry budget machinery is Planning-Mode-only — planning +
onboarding were the two affected surfaces.
## Symptom Verification
- **Original symptom:** after answering a question, Planning Mode loops
between "Retrying…" and regenerating, re-showing the already-answered
question, with the auto-retry budget never exhausting.
- **Exact reproduction:** answer a question, have the next generation
fail (stuck watchdog/provider error), let the client auto-retry open a
fresh SSE connection.
- **Assertion it is gone:** new regression suite
`planning-answered-question-reemit.test.ts` asserts `currentQuestion` is
cleared mid-generation, on generation failure, on retry, and on restore
from non-`awaiting_input` rows — so the SSE catch-up path has nothing
stale to re-emit. All 5 tests fail against pre-fix code and pass with
the fix; an onboarding regression test covers the sibling surface.
## Verification
- New regression tests: 5/5 fail on pre-fix code, pass with the fix
(plus 1 onboarding test).
- Existing suites: 137 planning server tests pass (3 failures in
`routes-planning.test.ts` fail identically without this change —
pre-existing on the branch); all 69 `PlanningModeModal.planning-flow`
client tests pass; `tsc --noEmit` clean; `pnpm check:changesets` passes.
🤖 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**
* Made Planning Mode (and related planning controls) lock-free and
multi-tab—no more take-over/active-in-another-tab lock overlays.
* **Bug Fixes**
* Fixed Planning Mode retry/generation flows where already-answered
questions could reappear.
* Ensured answered questions clear immediately and aren’t re-emitted
during session recovery/SSE catch-up.
* Improved session restoration and preserved legacy recovery behavior
when generation fails after an answer.
* **Tests**
* Added regression coverage for the answered-question invariant and
updated existing tests to reflect lock-free behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---
## Follow-up: Planning Mode is now multi-tab via DB state (lock-free)
Second commit removes all cross-tab coordination from planning — the
persisted session row is the single source of truth and multiple tabs
can read and interact with the same session:
- **Server:** `/planning/*` routes no longer run `checkSessionLock` or
parse `tabId`; a stale `tabId` from an older client is ignored instead
of 409'd. Subtask/mission interview routes keep their existing lock
behavior.
- **Client:** `PlanningModeModal` drops `useSessionLock`, the
`useAiSessionSync` BroadcastChannel broadcasts,
`sessionTabId`/`lockSessionId` state, and the "Take Control" overlay.
Tabs stay current via the per-session SSE stream plus the global
`ai_session:updated` events `useBackgroundSessions` already consumes;
concurrent writes resolve via the server's generation-in-progress guard
(409).
- **API client:** planning functions lose their `tabId` params.
- **Fix uncovered by the refactor:** the 8s stuck-poll now resolves the
session id inside each tick — the removed lock state was what previously
re-armed the poll after Start Planning resolved the session id.
- Also fixes a pre-existing PG-cutover break in
`planning-generation-cancellation.test.ts` (`getSession` is async).
Verification: 144 client planning tests and 137 server planning tests
pass (the 3 remaining `routes-planning.test.ts` failures are
pre-existing on the branch and fail identically without these changes);
`tsc --noEmit` and eslint clean on changed files; `pnpm
check:changesets` passes. Lock-conflict route tests were rewritten to
assert lock-free semantics, plus a new modal test proving a session
stays fully interactive with no lock acquisition even when another tab
is active.
---
## Follow-up 2: the per-tab session lock is gone entirely
Third commit extends the multi-tab model from planning to **every** AI
interview surface (planning, subtask breakdown, mission interview,
milestone/slice interview) and deletes the lock machinery root and
branch.
**Server**
- Deleted the `/ai-sessions/:id/lock`, `/lock/force`, and `/lock/beacon`
routes.
- Dropped `checkSessionLock` from every
planning/subtask/mission/milestone route (both copies — `routes.ts` and
`mission-routes.ts`). A `tabId` from an older client is ignored, never
409'd; all `tabId` body parsing is gone.
- Dropped `acquireLock` / `releaseLock` / `forceAcquireLock` /
`getLockHolder` / `releaseStaleLocks` from `AiSessionStore`, plus the
`@fusion/core` async helpers (`acquireAiSessionLock` et al) and core's
re-exports.
- Removed `lockedByTab`/`lockedAt` from
`AiSessionRow`/`AiSessionSummary`, the upsert SQL, and all four session
producers.
**Client**
- Deleted `useSessionLock` and the now-orphaned `getSessionTabId` util.
- Removed the Take Control overlay, the "active in another tab" banners,
and `BackgroundTasksIndicator`'s active-elsewhere gate (the confirm
prompt and lock badge — sessions now just open).
- Reduced `useAiSessionSync` to what its own comments already called it
— a low-latency *status* supplement to SSE: no `activeTabMap`,
`broadcastLock/Unlock/Heartbeat`, `owningTabId`, `tab:*` messages, or
stale-heartbeat sweep.
- Dropped `tabId` from every session API client function; removed the
lock CSS.
**Deliberately kept: the two DB columns.** `ai_sessions.locked_by_tab` /
`locked_at` remain as dead, always-NULL columns with a deprecation note.
Dropping them is an irreversible migration, and released binaries still
name those columns explicitly in their upsert — an older install pointed
at the same database would fail every session write. They can be dropped
once no such binary can reach it. No code reads or writes them.
**Verification**: 397 client tests and 137 server planning tests pass
(the same 3 `routes-planning.test.ts` failures are pre-existing —
verified identical on a clean stash); `tsc --noEmit` clean for
`@fusion/core` and `@fusion/dashboard`; eslint clean on all changed
files; the 30 PG `schema-applier` tests pass (they exercise the retained
columns); `pnpm check:changesets` passes. The lock-conflict route tests
and both modal lock tests were rewritten to assert the inverse: routes
and modals stay fully interactive while another tab "holds" a lock, and
the lock API is never called.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
- reuse the dashboard command's backend-aware per-project `TaskStore`
cache during project-scoped plugin skill discovery
- obtain plugin state through `TaskStore.getPluginStore()` instead of
constructing bare SQLite-default `PluginStore` / `TaskStore` instances
- keep cached project stores alive for the dashboard process while still
stopping request-scoped plugin loaders
- add a regression covering the real Skills adapter callback and refresh
the dashboard test fixture with `getAsyncLayer()`
## Root cause
`GET /api/skills/discovered` resolved the project correctly, then
`getProjectScopedPluginSkills()` constructed new stores without an
`AsyncDataLayer`. After `VAL-REMOVAL-005`, that enters the physically
removed synchronous SQLite runtime and returns HTTP 500 even when
PostgreSQL health, projects, tasks, and both project engines are
healthy.
The existing route tests mocked the Skills adapter callback, so they did
not exercise this CLI wiring.
## Verification
- targeted dashboard regression: 1 passed, 91 skipped
- `pnpm lint`
- `pnpm --filter @runfusion/fusion typecheck`
- `pnpm --filter @runfusion/fusion build`
- `pnpm check:changesets --strict`
- `git diff --check`
Live Atlas validation against the migrated embedded PostgreSQL runtime:
- `/api/skills/discovered?projectId=proj_84f4645c2da64288`: HTTP 200, 36
skills
- `/api/skills/discovered?projectId=proj_7538a9dd46c24c5f`: HTTP 200, 36
skills
- local dashboard and Tailscale dashboard: HTTP 200
- controlled SIGTERM: launchd restarted the dashboard and both Skills
routes remained healthy
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Fixed dashboard project-scoped plugin-skill discovery in PostgreSQL
mode with safer store reuse/teardown and request-scoped plugin-loader
lifecycle.
- Improved dashboard cleanup to avoid duplicate concurrent store closes
and ensured proper shutdown behavior per root type.
- Made `fusion_runtime` role creation race-safe during concurrent
PostgreSQL migrations.
- **New Features**
- Added `persistRuntimeState` option to control whether plugin runtime
state changes are persisted.
- **Tests**
- Expanded dashboard and core hot-reload tests to verify scoped,
non-persistent runtime behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Route workflow selections, model lanes, goals, skills, and reliability reads through project-scoped async stores. Recover heartbeat agents parked against an unrelated project model and preserve workflow JSONB patches atomically.
Report source scans, per-table copy milestones, checksum phases, verification outcomes, and unambiguous failure or finalization status during first-boot and manual migrations.
## Summary
Fixes shard 4 full-suite failures: chat_sessions schema baseline gap +
two remaining PG auth bugs missed by PR #2086.
**Scope: shard 4 only.** Shards 1/2 (engine timeouts) and shard 3
(compound-engineering CI-only failure) are separate issues not addressed
here.
## Changes
### Schema baseline gap — `chat_sessions` missing columns (42703 error)
- **`0000_initial.sql`**: Added `validator_thinking_level` and
`planning_thinking_level` columns to `CREATE TABLE
project.chat_sessions`. These exist in the Drizzle schema
(`project.ts:1492-1493`) but were missing from the SQL baseline, causing
`column does not exist` on all chat_sessions inserts in fresh test
databases.
- **`postgres-health.ts`**: Added both columns to
`EXPECTED_PROJECT_COLUMNS` self-heal list so existing databases also get
them via ALTER TABLE.
**Fixes**: `chat-store-content-search-edit.pg.test.ts` (5 tests),
`satellite-db-injected-stores.test.ts` (2 tests)
### Remaining auth bugs (password auth failed for user "runner")
- **`allocator-cross-project.test.ts`**: Still had `process.env.USER` in
inline adminExec — missed by PR #2086's batch fix. Replaced with
`PG_TEST_URL_BASE` connection string.
- **`connection.test.ts`**: Used `FUSION_PG_TEST_URL` (not set on CI)
with a bare default URL lacking credentials. `postgres.js` fell back to
OS user `runner`. Changed to derive from `FUSION_PG_TEST_URL_BASE` which
includes credentials.
**Fixes**: `allocator-cross-project.test.ts` (2 tests),
`connection.test.ts` (3 tests)
## Verification
| Check | Result |
|---|---|
| Merge gate (`pnpm test:gate`) | ✅ 294 + 114 + 63 = 471 passed |
| chat-store-content-search-edit | ✅ 5 passed |
| satellite-db-injected-stores | ✅ 10 passed |
| allocator-cross-project | ✅ 2 passed |
| connection | ✅ 13 passed |
| Lint | ✅ exit 0 |
| Typecheck | ✅ clean |
## Not in scope
- **Shards 1/2**: Engine test suite timeouts with
`getAsyncLayer`/`updateSettings` mock warnings. Pre-existing.
- **Shard 3**: `compound-engineering stage-skill-loading.test.ts` — 14
tests fail on CI (`TypeError: Cannot read properties of undefined
(reading 'close')`), pass locally. Likely CI-specific teardown issue.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added separate `validator_thinking_level` and
`planning_thinking_level` fields to chat session data, including
database schema and health-check recognition.
* **Bug Fixes**
* Improved PostgreSQL test connectivity by using configured connection
URL settings instead of hardcoded local defaults.
* Made Postgres-related test teardown null-safe to avoid failures when
setup doesn’t complete.
* **Tests**
* Updated automated test quarantine/exclusions for known failing engine
and reliability-interaction cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Preserve legacy-only tables, recover partial migration ownership, and enforce project-local keys, relationships, agents, merge queues, task IDs, archives, and monitor state with PostgreSQL RLS.
Report successful cutovers once in the dashboard and system inbox with retained SQLite paths and Discord support details.
Migrate central SQLite state once per cluster, isolate project metadata, and preserve file-local revision identities while verifying accumulated shared tables.
## Summary
- Add `fusion-plugin-omp-runtime` so Fusion agents can run through
operator-installed **Oh My Pi (`omp`)** over the [Agent Client
Protocol](https://omp.sh/docs/acp) (`omp acp`).
- Wire staged/bundled install, Settings → Authentication card (enable +
binary path), model discovery (`omp models` → `omp-cli/*`), and MCP
eligibility for runtime id `omp`.
- Forward Fusion `systemPrompt` via ACP `session/new`
`_meta.systemPromptOverride`.
## How operators use it
1. Install/auth `omp` (credentials under `~/.omp`).
2. Enable **Oh My Pi — via omp ACP** in Settings → Authentication
(optional binary path).
3. Set agent **Runtime Source → OMP Runtime** (`runtimeHint: "omp"`), or
pick an `omp-cli/*` model when enabled.
## Known v1 gaps
- No Grok-style Fusion `fn_*` loopback tool bridge yet (operator MCP is
forwarded; in-process custom tools are not).
- Model is fixed at spawn (`omp --model … acp`); no mid-session Fusion
model switch.
## Test plan
- [x] `pnpm --filter @fusion-plugin-examples/omp-runtime test` (unit +
live ACP when `omp` is on PATH)
- [x] Auth routes: `POST /api/auth/omp-cli`, `GET
/api/providers/omp-cli/status`
- [x] Engine `runtimeSupportsMcp("omp")`
- [ ] Manual: enable card in dashboard, select OMP runtime on an agent,
run a short chat turn
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added Oh My Pi (OMP) CLI support as an ACP-backed runtime and model
provider, including model discovery and probing.
* Added dashboard auth/status controls to enable OMP, check readiness,
and configure the local binary path (with validation).
* Exposed OMP custom `fn_*` tools via an MCP loopback bridge, plus
optional filesystem capabilities and stricter tool permission gating.
* **Documentation**
* Added/expanded OMP runtime contract and integration docs (including
the ACP session/handshake flow).
* **Tests**
* Added Vitest coverage for settings wiring, provider status, model
discovery, runtime sessions, permissions, MCP bridging, and live
connectivity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- make SQLite-to-PostgreSQL cutover retryable, fail-closed, versioned,
and transactionally serialized
- isolate migration sessions from runtime traffic and apply schema
upgrades through `0002`
- enforce tenant ownership across automations, analytics, activity,
usage, agent runs, evals, and todos
- replace expired SQLite-only coverage with PostgreSQL parity and
concurrency coverage
This is PR 1 of 2. The stacked follow-up restores PostgreSQL parity for
CLI, engine, dashboard, and bundled integrations.
## Verification
- `pnpm check:changesets --strict`
- `pnpm --filter @fusion/core typecheck`
- migration schema, connection, and SQLite cutover suite: 57 tests
passed
- `pnpm test:gate`: 463 tests passed
## Post-Deploy Monitoring & Validation
- take a restorable PostgreSQL backup before deploy
- confirm `fusion_schema_migrations` contains `0002`
- confirm each expected project has a complete
`fusion_sqlite_migrations` row
- verify no null or empty tenant ownership in automations, activity
logs, agent runs, and usage events
- monitor for ownership inference failures, cutover verification
failures, and migration session errors
- restore the backup for data rollback; do not downgrade the
tenant-isolation schema in place
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* PostgreSQL-backed analytics and live dashboard metrics are now
project-scoped (activity, tools, monitor, signals, and live snapshots).
* Evaluation runs and scheduled eval batches received lifecycle
improvements (ordering, updates, and execution flow).
* Todo list changes now emit events; WhatsApp persistence and
project-scoped roadmap data are supported.
* **Bug Fixes**
* SQLite-to-PostgreSQL cutovers now fail safely with stronger
verification, serialized cutover handling, and safer project ownership.
* PostgreSQL backend writes and reads are now strictly project-isolated
and fail closed when project context is missing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
A task whose Plan Review step returns verdict `REVISE` can loop forever:
plan → plan-review REVISE → `needs-replan` → re-plan → near-identical
plan → REVISE → repeat. The triage **pre-execution** Plan Review gate
(`runPlanReviewBeforeExecution`) sets `status: "needs-replan"` on REVISE
with **no cap and no escape to `awaiting-approval`** — unlike the
executor graph path, which already has `PLAN_REVIEW_REPLAN_HARD_CAP`.
Under `planApprovalMode: require-all` there is also no human exit,
because the task never reaches `awaiting-approval`.
Separately, replan feedback (`triage.ts`) was derived only from
`task.log` comment actions + the latest user comment; it never consulted
the plan-review verdict stored in `task.workflowStepResults`.
## Fix
1. **Thread plan-review feedback into replan** — when re-planning with
no comment-derived feedback, seed `buildSpecificationPrompt` from the
most recent `plan-review` REVISE `output` in `workflowStepResults`
(existing user/AI-comment precedence preserved).
2. **Bounded cap** — new `planReviewReplanCount` counter (`types.ts`,
`store.ts` column + updateTask, `db.ts` migration 146,
`manual-retry-reset.ts`). After `PLAN_REVIEW_GATE_REPLAN_CAP = 3`
consecutive REVISE replans the task escalates to `awaiting-approval`
(`awaitingApprovalReason: "plan-review-replan-cap"`) instead of
replanning. Counter resets on APPROVE.
## Tests
Adds `triage-replan-feedback-from-plan-review.test.ts` and
`triage-plan-review-replan-cap.test.ts`. Merge gate green locally
(`verify:fast`, `test:gate` 337+63, `lint`); changeset included.
Made with Claude (see `Co-Authored-By` trailer).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Prevented Plan Review “REVISE” from looping indefinitely by enforcing
a bounded replan cap.
* After repeated Plan Review replans, tasks now escalate to an
approval-hold state with a dedicated reason.
* Improved replan feedback by seeding from the latest Plan Review output
when no explicit feedback is available; the counter clears when Plan
Review approves.
* Manual retries now reset the Plan Review replan cap counter.
* **Documentation**
* Added release notes describing the Plan Review replan safeguards.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
## Summary
- preserve target defaults when legacy SQLite rows contain `NULL` or
empty strings for `NOT NULL` jsonb columns
- derive the fallback from PostgreSQL column metadata instead of
hard-coding table or column names
- keep migration checksum conversion aligned with inserted values
- add regression coverage for legacy null JSON fields
## Test plan
- `corepack pnpm@10.33.0 --filter @fusion/core typecheck`
- `FUSION_PG_TEST_SKIP=1 corepack pnpm@10.33.0 --filter @fusion/core
exec vitest run src/__tests__/postgres/sqlite-migrator.test.ts`
- `corepack pnpm@10.33.0 --filter @fusion/core build`
The PostgreSQL-backed integration suite requires `psql`, which is
unavailable in this environment; CI should exercise the added migration
case against PostgreSQL.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved SQLite-to-PostgreSQL migration for legacy rows containing
`NULL` or empty JSON values.
* For eligible `NOT NULL` `jsonb` columns, the migrator now
preserves/apply compatible PostgreSQL column defaults instead of writing
SQL `NULL`.
* Migration verification now aligns with the final values inserted into
PostgreSQL to prevent checksum mismatches.
* **Tests**
* Added an end-to-end legacy migration case to confirm `jsonb` fields
materialize as empty defaults (e.g., `[]`) rather than staying `NULL`.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Closes the remaining PG-cutover partitioning gaps:
- getWorkflowSettingsProjectId resolves the bound AsyncDataLayer's central-
registry id first. In backend mode the SQLite stub's getProjectIdentity()
throws, so the old fallback ALWAYS keyed workflow_settings /
workflow_prompt_overrides by the rootDir path string — a namespace nothing
else reads, making workflow settings appear reset after cutover.
- Stamping is extracted into core stampMigratedProjectRows (tasks/archived
NULL->id, config ''->id, workflow_settings + workflow_prompt_overrides
rootDir-key->id, all guarded against clobbering per-project rows), shared by
startup-factory Step 5.5 and 'fn db migrate', which now resolves the
registered project by path after the copy and warns when unregistered.
- The task-id allocator and merge_queue are verified safe WITHOUT project
partitioning: task ids are a global PK, the per-prefix sequence scans are
intentionally global (only the per-project config floor can raise them), so
two projects sharing a prefix cannot mint duplicate ids. FNXC comments lock
the invariant; a cross-project PG regression test proves it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the central-project-identity architecture: cwd/rootDir is ONLY a
lookup key into central.projects; project identity (the partition key for
every task/config read and write) comes from the registry.
- createTaskStoreForBackend resolves the registered project id by path for
rootDir-only boots and binds the AsyncDataLayer to it. Previously
'fn dashboard' / 'fn serve' / desktop booted their main store UNBOUND, so
unscoped API requests wrote NULL-project_id rows the projectId-bound engine
could never see, and unbound config reads (id = 1) were indeterminate once
multiple per-project rows existed. The engine already worked registry-first
(resolveLocalProjectWorkingDirectory); this brings the store boots in line.
- Step 5.5 auto-migration now also re-keys the migrated legacy config row
('' -> project id, guarded against clobbering an existing per-project row).
configScope() has no bound->'' fallback, so the migrated project settings,
workflowSteps, taskPrefix, and nextId counters were silently invisible to
bound readers right after a successful migration.
- Unregistered paths resolve to undefined and boot unbound, preserving legacy
single-project behavior with unfiltered readers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SQLite -> PostgreSQL auto-migration leaves project_id NULL and Step 5.5
only stamped rows when options.projectId was bound — but 'fn dashboard' in the
project directory (the main cutover path) boots with rootDir only, so every
migrated row stayed NULL, project-bound readers (engine InProcessRuntime,
dashboard project-store-resolver) filtered them all out, and the board showed
no tasks right after a successful migration. The stamping id is now resolved
from the freshly-migrated central registry by matching the registered project
path to rootDir; projects never registered centrally keep NULL rows, matching
their unbound readers. Integration test covers the rootDir-only stamp.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
insertBatch read the driver wrapper's count (result.count ?? result.rowCount
?? rows.length), which reported 0 through drizzle's execute even when every
row landed — migration reports showed 'inserted 0' for fully-migrated tables
and the startup banner's migratedRows total was wrong. ON CONFLICT DO NOTHING
RETURNING 1 yields exactly one row per row actually inserted, making the count
driver-agnostic and correctly excluding conflict-skipped rows. Idempotency
test now asserts first-run insertedRows == sourceRows and re-run
insertedRows == 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>