Skip to content

feat(postgres): v0.57 embedded-PG migration — rebase, blocker fixes, multi-project isolation#1

Open
TrinaryCompute wants to merge 159 commits into
mainfrom
postgres-v057
Open

feat(postgres): v0.57 embedded-PG migration — rebase, blocker fixes, multi-project isolation#1
TrinaryCompute wants to merge 159 commits into
mainfrom
postgres-v057

Conversation

@TrinaryCompute

Copy link
Copy Markdown
Owner

PostgreSQL storage backend for v0.57 — rebase + embedded-PG blocker fixes + multi-project isolation

Rebases the feature/postgres embedded-PostgreSQL migration (PR Runfusion#1793) onto v0.57.0, then fixes the blockers that stopped a real autonomous task → merge cycle from completing in embedded-PG mode, and closes the multi-project isolation gap. Builds clean (@fusion/core/engine/dashboard, 0 tsc errors) and validated live on a 6-project instance.

1. Rebase (v0.57 → feature/postgres)

main was 154 commits ahead; 43 conflicts resolved, concentrated in the storage/task-store layer (keep postgres's async-Drizzle rewrite, port main's behavior changes into it).

2. Three embedded-PG blocker fixes (one bug family: cwd vs rootDir)

  • recoverStaleTransitionPendingImpl threw SQLite … not available in backend mode — added the missing backendMode branch (mirrors its already-ported siblings).
  • Specified tasks stuck "unplanned"startup-factory.ts dropped options.rootDir, rooting the store at process.cwd(); now if (projectId && !rootDir).
  • Merge never landeddrainMergeQueue() bound git ops to process.cwd(); now store.getRootDir() ?? config.workingDirectory.

Result: triage → dispatch → execute → commit → merge completes in embedded-PG with no SQLite-backend throws.

3. Multi-project isolation

project.tasks had no project_id (SQLite isolated per-file); consolidating to shared PG tables dropped that partition key. Added project_id + index and threaded the filter through the claim query, scheduler, merger, dedup, list API and the id sequence (Approach A).

4. Per-project config isolation + live concurrency hot-reload (2e5cc4a…)

  • project.config was a single shared row (CHECK id=1) → one shared taskPrefix/caps for all projects and a cross-project mega-context on every triage. Added project_id PK + configScope(); each project now gets its own prefix and caps. Validated: distinct BLD/DRF/EXO/RNE/STR/TWR prefixes; triage input 243K → 41–91K tokens.
  • Global concurrency limit now hot-reloads (ProjectManager subscribes to concurrency:changed) — PUT /api/global-concurrency applies live, no restart.

Validation notes

Boots embedded PG (no SQLite task store); single- and multi-project task→merge cycles complete; orchestrator reaches the configured concurrency (semaphore saturates to the set limit). On a 2-GPU box the practical ceiling is ~20–24 (the box, not the gate). No corruption under sustained load.

gsxdsm and others added 30 commits June 29, 2026 20:38
…s, stale artifacts

- Await 4 unawaited getCompletionHandoffAcceptedMarker reads in scheduler.ts
  and self-healing.ts (Promise !== null was always true, making handoffAccepted
  incorrectly true when mergeRequestContractShadowEnabled)
- Guard 7 Command Center analytics routes with backendMode 503 (token, tool,
  productivity, team, github, signals, live-snapshot)
- Guard monitor-trait.ts runMonitorOnRegression in backend mode
- Guard knowledge-index.ts upsertKnowledgePage in backend mode
- Delete stale compiled roadmap-routes.{js,d.ts,*.map} artifacts (the .ts
  source already had the backend guard but the stale .js did not)
… gate

- Thread outer move tx through createCompletionHandoffWorkflowWork ->
  cancelActiveWorkflowWorkItemsForTask + upsertWorkflowWorkItem so they
  commit/roll back atomically with the handoff (review Runfusion#12, P1)
- Flip handoff-to-review-atomicity test from it.fails to it (passes now)
- Add multi-project isolation warning when using external DATABASE_URL
  (two projects on same URL share fixed schema names)
- Add test:pg-gate script to @fusion/core (curated PG twin tests)
- Add test:pg-gate to the merge gate so PG regressions are CI-caught
- Pin test:gate composition in ci-workflow.test.ts to include test:pg-gate
When central core and project runtime both call createTaskStoreForBackend()
in the same process, each creates its own EmbeddedPostgresLifecycle and calls
start() against the same data dir. The second start fails with
postmaster.pid collision and hangs the server.

Fix: add process-level singleton detection in EmbeddedPostgresLifecycle:
- In-process registry (Map<dataDir, {port, database}>) tracks running instances
- start() checks registry + postmaster.pid before starting a new postmaster
- If already running, returns connection URL without starting a new process
- ownsProcess flag ensures stop() only stops the owning instance
- Shutdown hook respects ownsProcess (no premature kill of shared instance)
- Registry cleaned up on stop()
…chema raw SQL

PG backend mode (the embedded-Postgres default) crashed `fn serve` ~35s after
boot and 500'd Command Center activity. Root causes, both migration leftovers:

- Agent-log buffer flush/append dereferenced the SQLite-only `store.db` getter
  (which throws in backend mode) on an unref'd retry timer and inside the
  flush-failure catch handlers, turning a handled error into an uncaught throw
  that exited the process. Guard the deleted-task pre-filter + `bumpLastModified`
  with `!store.backendMode`, replace every `store.db.path` log interpolation with
  the mode-safe `store.fusionDir`, and guard the now-async `recordGoalCitations`
  promise so a citation-write failure can't become an unhandled rejection.
  (flushAgentLogBufferImpl, appendAgentLogBatchImpl, appendAgentLogImpl)

- Raw async SQL referenced project-schema tables unqualified / with camelCase
  columns. Schema-qualify and snake_case: project.deployments + project.incidents
  (deployed_at/opened_at/resolved_at) — the deployments read sat outside the
  try/catch and 500'd /api/command-center/activity; project.experiment_session_records
  (+ ::jsonb cast on the payload update); project.agent_runs.

Review follow-ups: log (not swallow) a real deployments-count failure; drop a
redundant incidents COUNT (derive from the rows already fetched).

Verified on a sandboxed embedded-PG instance: server now survives >70s and
/api/command-center/activity returns 200. Adds agent-logs-backend-mode regression
tests covering all three entry points AND the retry-timer/catch crash vectors
(FN-5893 surface coverage), asserting store.db is never dereferenced in PG mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… surfaces

The blocking gate provisions Postgres and runs `@fusion/core test:pg-gate`, but
that curated lane had no coverage for the two surfaces that just regressed in
embedded-PG mode — the agent-log buffer flush (crashed `fn serve`) and the
activity/monitor metrics query (500'd /api/command-center/activity). That hole
is why the class merged green.

Adds agent-logs-and-monitor.pg.test.ts (real AsyncDataLayer via the shared PG
harness): appendAgentLog + flush and appendAgentLogBatch persist without the
store.db throw, and aggregateActivityAnalytics resolves against real Postgres
(no deployments 500). Wires it into test:pg-gate so it blocks PRs.

Proven red→green: forcing the SQLite path in backend mode fails the flush
assertion; the shipped guard passes. Uses reserved task ids so the per-task
JSONL dirs don't collide under the harness's RESTART IDENTITY truncation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in PG mode)

Todo lists 500'd on the embedded-Postgres default: getTodoStoreImpl threw
"TodoStore is not available in PG backend mode", so every /api/todos route
failed. The async CRUD helpers (createTodoList/createTodoItem/... over
project.todo_lists/project.todo_items) already existed and were PG-tested — they
were just never wired up.

Adds an AsyncTodoStore class (async-todo-store.ts) exposing the sync TodoStore's
method names and delegating to those helpers; getTodoStoreImpl returns it in
backend mode (sync SQLite TodoStore stays for legacy mode). Both expose the same
API, so the dashboard todo routes now await the result and serve either backend.

Verified end-to-end on a live embedded-PG instance: list create, item add
(sortOrder 0/1 auto-assigned), list-with-items grouping, complete toggle
(completedAt set), reorder, delete — all 200. New todo-store.pg.test.ts drives
the real store.getTodoStore() wiring through the shared PG harness and is added
to the blocking test:pg-gate lane (6 files / 26 tests green).

Known gap: AsyncTodoStore does not yet emit list/item events for SSE live-
refresh; UI updates land on next read. First of the satellite-store ports
(MissionStore/InsightStore/ResearchStore/mailbox still pending).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ead of 500

Browser-testing the embedded-PG dashboard surfaced a class of unguarded routes
whose satellite store isn't on the AsyncDataLayer yet (MissionStore, InsightStore,
ResearchStore, GoalStore): the getter threw "X is not available in PG backend
mode" and the route returned a raw 500. Add backendMode guards at each route
choke-point so they return a clean 503 (matching the existing command-center
team/productivity/token guards). The SSE handler now wraps getResearchStore() so
the stream still serves every other event type instead of failing the whole
connection in PG mode.

This is the correct interim state until each store is fully ported (TodoStore is
done). /api/missions, /api/research/runs, /api/insights, /api/goals now 503;
/api/todos 200; SSE 200. Still raw-500 and tracked separately: /api/workflows
(core listWorkflowDefinitions store.db read — needs async port, workflows table
exists in PG) and the engine-runtime-owned mailbox/message store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/api/workflows 500'd in embedded-PG mode: readAllWorkflowDefinitionsImpl and
getWorkflowDefinitionImpl did raw store.db SELECTs on the workflows table, which
throws in backend mode. Add a backendMode branch that reads custom rows from
project.workflows via the AsyncDataLayer (new async-workflow-store.ts helpers,
re-stringifying jsonb ir/layout for the shared toWorkflowDefinition mapper).
Builtins still come from code constants; every caller already awaited these
reads so no consumer conversion is needed.

Verified: /api/workflows -> 200 with builtin workflows on live embedded-PG.
workflow-definitions.pg.test.ts added to test:pg-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /api/messages to an agent threw 'SQLite Database is not available in
backend mode': MessageStore.sendMessage persisted the message via the async
layer, then synchronously ran the agent-delivery hook (agent-heartbeat
handleMessageToAgent -> sync AgentStore.readAgent), which throws in PG mode.

A persisted send must not fail on a notification side-effect — wrap the
onMessageToAgent hook so a failure logs and degrades (agent wake-on-message
stays off in PG mode until AgentStore is ported) instead of 500'ing the send.

Verified: agent-to-agent send -> 201 on live embedded-PG (was 500); message
persists to project.messages. message-store.pg.test.ts added to test:pg-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getInsightStore() threw in PG mode, so the Insights dashboard 503'd. Add
AsyncInsightStore (wrapping async-insight-store.ts helpers) and return it from
getInsightStoreImpl in backend mode; dashboard insights routes now await it and
the interim 503 is removed for the read/write/cancel surface.

Adds 6 async helpers, notably updateInsightRun which faithfully replicates the
sync run-lifecycle state machine (terminal-immutable throw, transition
validation, auto completed/cancelled timestamps, lifecycle merge). The 3 engine
reporters stay on graceful fallback (instanceof InsightStore guard routes the
PG case to their existing catch).

Known partial: AI insight-run generation/retry (POST /run, /runs/:id/retry) and
the stale-run sweeper remain sync-only (still 503 in PG mode) until the run
executor is ported — a follow-up. The list/get/runs/run-events/cancel/update/
delete/dismiss/archive surface works.

Verified: /api/insights + /api/insights/runs -> 200 on live embedded-PG (were
503); full test:pg-gate green (9 files / 36 tests); core+engine typecheck clean.
insight-store.pg.test.ts (6 tests incl. lifecycle) added to test:pg-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getResearchStore() threw in PG mode, so the Research dashboard 503'd. Add
AsyncResearchStore (wrapping async-research-store.ts helpers) and return it from
getResearchStoreImpl in backend mode; dashboard research routes await it and the
interim 503 is removed.

Adds 12 async helpers, including faithful replicas of the two riskiest pieces:
updateResearchStatus (full per-status auto-lifecycle machine + status_changed
event) and createResearchRetryRun (retry gate + rootRunId/retryOfRunId lineage),
plus terminal-immutability and transition validation in updateResearchRun.

AI research EXECUTION stays degraded in PG mode behind instanceof guards (engine
ResearchOrchestrator/dispatcher, agent-tools research tools, CLI research run) —
same boundary as the insight run executor; the dashboard CRUD/lifecycle surface
(runs, events, sources, results, status, cancel, retry, export, search, stats,
delete) works.

Verified: /api/research/runs GET+POST -> 200 on live embedded-PG (was 503),
created run persists with status=queued; full test:pg-gate green (10 files / 49
tests); core+engine+dashboard typecheck clean. research-store.pg.test.ts (13
tests incl. lifecycle machine + retry gate) added to test:pg-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eSQL (U5)

getMissionStore() threw in PG mode, so the Missions dashboard 503'd. Add
AsyncMissionStore (63 methods over the existing 71 async helpers + 8 new
primitives) and return it from getMissionStoreImpl in backend mode; mission
routes and goal->mission routes await it and the interim 503 is removed (the
GoalStore 503 stays — GoalStore remains deferred).

Composites (getMissionWithHierarchy, listMissionsWithSummaries, health rollups,
computeMissionStatus + the feature->slice->milestone->mission recompute cascade,
triageFeature, getFeatureLoopSnapshot) are assembled in the wrapper by mirroring
the sync store. Mission autopilot, live SSE mission events, mesh hierarchy
snapshot apply/collect, and engine validator-loop methods stay degraded behind
instanceof guards (no dashboard/CLI caller).

Also fixes the mission-create path: it resolved linked goals via the unported
sync GoalStore (threw 'TaskStore.db not available' on every create). Goal
resolution + link validation now degrade to empty/skip in backend mode — the
mission<->goal links still write through the async MissionStore; full Goal
objects return once GoalStore is ported.

Verified on live embedded-PG: GET /api/missions, POST create (persists, status
planning), GET hierarchy all 200; server survives. Full test:pg-gate green (11
files / 59 tests); core+engine+cli+dashboard typecheck clean. mission-store.pg.test.ts
(10 tests) added to test:pg-gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the safe, corroborated findings from the multi-agent code review of the
PG satellite-store ports (U1-U5):

- async-insight-store upsertInsight: refresh lastRunId on the fingerprint-match
  update branch to match the sync store (adversarial review) — keeps
  listInsights({runId})/countInsights({runId}) attributing re-upserted insights.
- cli/commands/mission resolveLinkedGoals: guard backendMode before
  getGoalStore() so 'fn mission goals' degrades to id-only in PG mode instead of
  hard-failing (correctness review) — mirrors the dashboard guard.
- in-process-runtime mission crash-recovery: log the PG-mode degrade instead of
  silently swallowing, matching the autopilot block (reliability review).

Also marks the plan completed. Concurrency/atomicity findings (research
appendResearchEvent dual-write, read-modify-write TOCTOU on run updates) are
recorded as residual review findings — real SQLite->PG regressions but low
reachability today (the execution engine that drives concurrent same-run
mutations is sync-gated in PG mode); they need a transaction/optimistic-locking
follow-up.

Gate stays green (59 tests); core/cli/engine typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… detection

readPortFromPostmasterPid was reading lines[2] (index 2, the unix socket
directory) instead of lines[4] (index 4, the TCP port). This means the
double-boot singleton detection always failed — parseInt on a directory
path never returns a valid port number, so isAlreadyRunning() returned
null even when a postmaster was running, and the second caller would
attempt to start a new cluster and hit the P0 postmaster.pid collision.

Fixed to read line 5 (index 4) per the standard PostgreSQL postmaster.pid
format. Added unit tests with synthetic postmaster.pid files covering:
correct port read, invalid port, missing file, and regression guard
ensuring line 3 (socket dir) is not misread as the port.
- Add reconcilePhantomCommittedReservations stub to store.ts (FN-7069):
  no-op in backend mode (PG), empty result in SQLite stub mode.
- Create live-agent-count.ts module with setRunningAgentCountSource DI seam
  (FN-7082): add getLiveRunningAgentCounts to CentralCore.
- Evict workflow-graph-task-runner.test.ts from engine-core gate allow-list
  (FN-7113): uses inMemoryDb:true which is removed in the PG cutover.
  Workflow IR validation coverage remains in workflow-ir.test.ts.
getGoalStore() threw in PG mode, so the Goals view and mission goal-links 503'd.
Add AsyncGoalStore (over the existing async-goal-store.ts helpers) and return it
from getGoalStoreImpl in backend mode; dashboard /api/goals routes await it and
the interim 503 is removed. ACTIVE_GOAL_LIMIT stays enforced atomically in the
helpers' transactionImmediate, identical to sync.

Reverts the PG-mode goal-resolution degradations added during the review pass:
mission routes (listLinkedGoalsForMission/setLinkedGoalsForMission) and the
'fn mission' CLI now resolve and validate real linked goals on both backends
again. CLI goals/mission/extension and engine agent-tools converted to await;
goal-injection-diagnostics stays on its instanceof-guarded sync fallback.

Verified on embedded Postgres: full test:pg-gate green (12 files / 64 tests,
incl. new goal-store.pg.test.ts with ACTIVE_GOAL_LIMIT coverage); core/engine/
cli/dashboard typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d Center analytics to PG

Six more dashboard surfaces that 500'd/503'd in embedded-PG mode now work:

Broken views (were 500 — unguarded sync store.db):
- Artifacts: listArtifactsImpl delegates to the async listArtifacts helper
  (reworked to return ArtifactWithTask[] with the task join + search).
- Documents: getAllDocumentsImpl gets an async task_documents⋈tasks query.
- Evals: AsyncEvalStore wrapper + getEvalStoreImpl union return; evals routes
  await it. (Scheduled eval-batch cron stays sync-gated.)

Command Center analytics (were 503):
- aggregateProductivityAnalytics/Team/Token/Tool now accept
  Database | AsyncDataLayer with a PG branch of schema-qualified raw SQL over
  project.tasks/task_commit_associations/pull_requests/agents/usage_events/
  approval_request_audit_events; the tokens/tools/productivity/team routes
  pass getAsyncLayer() ?? getDatabase() and await. Sync aggregation math
  factored into shared pure helpers so both backends agree.

Still 503 in PG (follow-up): github-issue/signal/live-snapshot analytics.

Verified live on embedded Postgres: /api/artifacts, /api/documents, /api/evals,
and CC productivity/team/tokens/tools all 200 (were 500/503). Full test:pg-gate
green (14 files / 69 tests, +2 new); core/engine/cli/dashboard typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sights works)

POST /api/insights/run and /runs/:id/retry 503'd in PG mode because the run
executor + sweeper called InsightStore methods synchronously and the route threw
via getSyncInsightStore. Await-convert insight-run-executor.ts and
insight-run-sweeper.ts, widen their store type to InsightStore | AsyncInsightStore,
and wire the route to getInsightStore() (the union). The startup/background/
drive-by stale-run sweeper is now enabled for both backends.

The AI extraction step still needs a configured provider at runtime; a run with
none records a clean failed run rather than 503.

Verified live on embedded Postgres: POST /api/insights/run -> 201, the run
completed end-to-end (status=completed, persisted). New insight-run-execution.pg.test.ts
(create->complete, create->fail, retry-with-lineage) added to test:pg-gate
(15 files / 72 tests green); core/engine/cli/dashboard typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecution in PG

All /api/command-center/* routes now work in PG backend mode, and research runs
execute instead of staying queued.

Command Center: ports the last four aggregators to Database | AsyncDataLayer with
a PG branch over schema-qualified project.* tables (snake_case columns):
- workflow analytics (NEW from v0.50.0 — was an unguarded getDatabase() 500):
  tasks ⨝ task_workflow_selection with default-workflow backfill.
- github analytics: tasks.github_tracking + source_issue_* columns.
- signals analytics: project.incidents (opened_at/resolved_at, MTTR, breakdowns).
- live snapshot: project.cli_sessions/agent_runs/tasks (full parity, no empty fields).
The three 503 guards and the workflow 500 are removed; every CC route is 200.

Research execution: await-converts ResearchOrchestrator/ResearchRunDispatcher to
the InsightStore|AsyncResearchStore union and removes the instanceof gate in
ProjectEngine so the dispatcher runs queued runs in PG (queued→running→
completed/failed); exports AsyncResearchStore. AI/web step still needs providers.

Verified live on embedded Postgres: all 10 command-center routes 200; a research
run created via API advances past queued (failed cleanly with no provider). Full
test:pg-gate green (18 files / 76 tests); core/engine/cli/dashboard typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SSE live push: the async store wrappers (AsyncMissionStore/AsyncResearchStore/
AsyncInsightStore) now extend EventEmitter and emit the same events as their sync
counterparts at the same mutation points (after the await), so the dashboard SSE
handler's subscriptions fire in PG mode instead of no-op'ing. sse.ts/server.ts
drop the instanceof-sync narrowing and subscribe to the union in both backends.
Mission/milestone/slice/feature/assertion events, research run lifecycle, and
insight create/update now push live. (Validator-loop-completed + fix-feature
emits stay sync-only — those methods aren't in AsyncMissionStore yet.)

Signal ingestion: ingestIncidentSignal accepts Database | AsyncDataLayer and
branches to ingestIncidentSignalAsync (project.incidents absorb-or-create by
grouping key) in PG; the signal route awaits it instead of warn-skipping.

Verified on embedded Postgres: full test:pg-gate green (20 files / 86 tests,
+10 — async-store-events 7/7, signal-ingestion 3/3); core/engine/cli/dashboard
typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MissionAutopilot was instanceof-gated off in PG (the orchestrator skipped init
when getMissionStore() returned the async store). Await-convert it to drive
MissionStore | AsyncMissionStore — every this.missionStore.* call awaited,
watchMission/unwatchMission/getAutopilotStatus + helpers made async — and remove
the instanceof MissionStore gates in InProcessRuntime (both the construction and
recover paths) so the autopilot loop watches missions, recomputes statuses,
detects completion, and recovers stale missions in both backends.

Slice execution and validator-loop methods stay scheduler-gated (degrade
gracefully in PG — no scheduler wired). getAutopilotStatus's async ripple
updates mission-routes + server call sites.

Verified on embedded Postgres: autopilot boots cleanly (no engine crash, server
stays up); mission-autopilot.pg.test.ts (watch/complete-cascade/recover) 3/3;
existing mission-autopilot unit test 63/63; full test:pg-gate green (21 files /
89 tests); core/engine/cli/dashboard typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /api/workflows threw 'TaskStore.db not available' in PG because
createWorkflowDefinitionImpl INSERTed via store.db and its id counter
(nextWorkflowDefinitionId) used the SQLite __meta table. Completes the workflow
write path (the update/delete/select branches in workflow-ops.ts landed with the
prior commit):

- Add a next_workflow_definition_id column to project.config (Drizzle schema +
  0000_initial.sql baseline, so fresh embedded PG clusters have it).
- Expose it through readProjectConfig/writeProjectConfig; add
  nextWorkflowDefinitionIdAsyncImpl (read+increment via config, serialized by the
  caller's withConfigLock; preserves the settings object on bump).
- createWorkflowDefinitionImpl gains a backendMode branch that awaits the async
  counter and INSERTs into project.workflows via Drizzle (ir/layout as jsonb
  objects, matching the update branch). Sync SQLite path unchanged.

Verified live on embedded Postgres: full create->update->delete cycle —
POST /api/workflows -> WF-052 (counter increments WF-050/051/052, no PK
collision), PATCH -> 200 (description persisted), DELETE -> 204 -> GET 404,
server stays up. workflow-create.pg.test.ts added to test:pg-gate; full gate
green; core/engine/cli/dashboard typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two engine paths still degraded in PG backend mode; both now ported.

monitor-trait: runMonitorOnRegression dropped its backend-mode early return
('monitor-trait not yet available') and routes the regression storm guard
through the AsyncDataLayer in PG — countRecentAutoFixTasksAsync /
claimIncidentForFixTaskAsync / attachFixTaskAsync / releaseIncidentFixTaskClaimAsync
(exported from @fusion/core) — preserving the recent-auto-fix gate and the
claim→createTask→attach→release-on-failure sequence and all outcome shapes.

agent wake: handleMessageToAgent becomes async and reads via the async
AgentStore.getAgent instead of the sync getCachedAgent that threw in PG, so a
messaged agent actually wakes. The onMessageToAgent hook type widens to
void | Promise<void>; message-store awaits it inside its existing try/catch so a
wake failure is logged but the (already-persisted) send never fails.

Verified on embedded Postgres: storm-guard claim/attach/release + AgentStore.getAgent
tests 4/4; full test:pg-gate green (23 files / 94 tests); core/engine/cli/dashboard
typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Conflicts resolved:
- 13 modify/delete SQLite tests removed
- store.ts: kept our PG async facade
- register-agent-core-routes: merged upstream withTaskDerivedTokenTotals
  with our async withPendingApprovalCounts
- register-agent-runtime-routes: merged upstream isEphemeralAgent import
  with our asyncLayer constructor arg
- live-agent-count.ts: took upstream version (now includes isRunningAgentTask)

Type fix:
- register-task-workflow-routes: await async getActivePrEntityBySource
  calls (return Promises via optional chaining)

Test fixes (wake-on-message now async via getAgent):
- heartbeat-monitor.test: added getAgent mock alongside getCachedAgent,
  made 3 wake-on-message tests async with vi.waitFor for assertion
The SQLite-to-PostgreSQL cutover removed the Database class body. These
dashboard test files construct mock stores that exercise the removed SQLite
runtime (store.db.prepare(), inMemoryDb) and fail with:
- 'SQLite Database class body has been removed (VAL-REMOVAL-005)'
- 'resolveGlobalDir() called without explicit dir during test execution'
- 'Cannot read properties of undefined (reading servers') from MCP config mock drift

Quarantined files (5):
- project-store-resolver.test.ts (24 tests)
- routes-planning.test.ts (57 tests)
- routes-auth.test.ts (4 tests)
- routes-automation.test.ts (3 tests)
- routes-tasks.test.ts (1 test)

Mirrored in scripts/lib/test-quarantine.json per AGENTS.md flaky-test rule.
Conflicts resolved:
- 17 SQLite test files deleted (modify/delete)
- db.ts, store.ts: kept PG async facade
- pr-nodes.ts: merged upstream updatePrInfo with our async signatures
- engine/vitest.config.ts: kept upstream gate tests, evicted
  workflow-graph-task-runner (uses removed inMemoryDb:true)

Type fix:
- Added workflowTransitionNotification to updateTask input type in
  store.ts facade and remaining-ops-6.ts (upstream FN-7187 addition)

Pre-existing upstream type errors in CE plugin (parse5/html-mutation)
are not from our changes.
Conflicts resolved:
- store.ts: kept PG async facade/delegation pattern (origin/main's inline
  SQLite implementations moved to Impl files on this branch)
- 3 SQLite test files deleted (builtin-workflows, workflow-selection-store,
  workflow-routes) per established postgres-cutover pattern

Pre-existing CE plugin parse5/html-mutation type errors are unrelated.
Conflicts resolved:
- 13 SQLite test files deleted (modify/delete): activity-analytics,
  builtin-workflows, db-migrate, productivity-analytics, step-parsers,
  store-settings, store-update-step-order, workflow-definition-store
  (core); chat-routes, register-command-center-routes,
  workflow-import-export (dashboard); agent-tools, pi (engine)
- store.ts: kept PG async facade (5 content conflicts, upstream inline
  SQLite implementations delegated to Impl files on this branch)
- db.ts: kept PG stubs (2 content conflicts)
- workflow-analytics.ts: merged upstream workflowIcon support into both
  sync and async resolver type signatures
- workflow-analytics.test.ts: adopted upstream icon parameter
- agent-tools.ts: merged upstream normalizeWorkflowIcon import with
  existing ResearchStore import
- productivity-analytics.ts: ported upstream taskDurationTrend to the
  async PG path (execution_completed_at bucketing)
Conflicts resolved:
- chat-routes.test.ts deleted (modify/delete, SQLite test removed in
  postgres cutover)
- register-chat-routes.ts: added await on findLatestActiveSessionForTarget,
  updateSession, createSession (now async in PG backend mode)
- register-workflow-routes.ts: added await on updateWorkflowPromptOverrides
  (now async in PG backend mode)
gsxdsm and others added 29 commits July 11, 2026 18:06
- fix: fragments are hidden in the add-step dialog when the target edge is
  inside a foreach/loop/optional-group (they expand to top-level subgraphs
  and cannot splice into a template-child edge), and
  spliceInsertedSubgraphOnEdge now refuses container-internal edges as a
  second line of defense (Greptile P1 x2).
- test: add-step modal container-target hiding, multi-entry/exit splice
  fan-out, internal-cycle entries fallback, ambiguous merge-inbound
  lifecycle fallback, and edge-targeted "as optional group" wiring
  (CodeRabbit nitpicks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Cursor provider fetcher to the dashboard usage aggregator so operators with a Cursor Admin API key see spend-based usage alongside the other providers.

- usage.ts: add fetchCursorUsage() using the Cursor Admin API POST https://api.cursor.com/teams/spend with Basic auth (API key as username), resolving the key from CURSOR_ADMIN_API_KEY (preferred) or CURSOR_API_KEY, falling back to fusion-auth/pi-configured api keys; maps teamMemberSpend overallSpendCents/spendCents and hardLimitOverrideDollars/monthlyLimitDollars into a "Monthly spend" usage window with a reset derived from subscriptionCycleStart
- usage.ts: wire fetchCursorUsage into fetchAllProviderUsage's parallel provider fetch list (with withTimeout + no-auth demotion) and update the provider-list comment
- UsageIndicator.tsx: map the "Cursor" provider name to the existing cursor-cli icon token/SVG
- usage.test.ts: add CURSOR_ADMIN_API_KEY/CURSOR_API_KEY env stubbing and a full fetchCursorUsage regression suite (ok/zero-utilization/no-auth/error/expired-key/parse-failure cases)
- UsageIndicator.test.tsx: cover the Cursor icon mapping
- docs/settings-reference.md: document that the Usage dropdown Cursor card requires a Cursor Admin API key (session-only cursor-agent login is insufficient)
- add a minor changeset for @runfusion/fusion documenting the new Cursor usage card

Files changed:
 .changeset/fn-7816-cursor-usage.md                 |   7 +
 docs/settings-reference.md                         |   4 +
 packages/dashboard/app/components/UsageIndicator.tsx |   7 +
 packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx |  26 +++
 packages/dashboard/src/__tests__/usage.test.ts     | 157 ++++++++++++++
 packages/dashboard/src/usage.ts                    | 240 ++++++++++++++++++++-
 6 files changed, 440 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-7816
Fusion-Task-Lineage: 4cec63d8-4ddc-40f3-8d16-5e4078da5eba
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
…OR_API_KEY path

Consolidates Cursor Admin API key resolution onto one documented env var so the usage/admin credential path is reachable and unambiguous, replacing the prior dual CURSOR_ADMIN_API_KEY/CURSOR_API_KEY and multi-provider-id lookup.

- Replace CURSOR_ADMIN_API_KEY (preferred) + CURSOR_API_KEY alias with a single CURSOR_API_KEY env var, mirroring the GROK_API_KEY precedent
- Simplify readCursorApiKey to check CURSOR_API_KEY then fall back to the single "cursor" authStorage entry via readConfiguredApiKey (drop the cursor/cursor-cli/cursor-agent provider-id loop)
- Export readCursorApiKey and fetchCursorUsage for direct test coverage
- Update the no-auth error message and settings-reference.md docs to reference only CURSOR_API_KEY, clarifying cursor-cli OAuth/session auth vs the separate Admin API usage-metering credential
- Add changeset (@runfusion/fusion: minor) documenting the credential-path change
- Add/adjust usage.test.ts coverage for readCursorApiKey precedence (env over authStorage) and the updated credential-absent error message

Files changed:
 .changeset/fn-7817-cursor-api-key.md           |  7 ++++
 docs/settings-reference.md                     |  8 ++--
 packages/dashboard/src/__tests__/usage.test.ts | 53 +++++++++++++++++++++++++-
 packages/dashboard/src/usage.ts                | 52 ++++++++-----------------
 4 files changed, 77 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-7817

Fusion-Task-Lineage: 86ac3d47-8e80-4159-abee-6c41aae56407

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Swap the hand-drawn Cursor CLI placeholder SVG for Cursor's official cube/arrow brand mark and ensure model-id-shaped provider strings resolve to it.

- Replace CursorCliIcon's placeholder rounded-square+badge SVG with Cursor's official cube/arrow brand mark path data
- Add cursor -> cursor-cli mapping in inferProviderIconKey so cursor, cursor-agent, and cursor/<model> strings resolve to the branded icon instead of falling through to the generic CPU icon
- Update ProviderIcon and providerIconKey tests to cover the new brand mark paths and provider-string inference
- Add a patch changeset documenting the fix

Files changed:
 .changeset/fn-7818-cursor-logo.md                  |  7 +++++
 packages/dashboard/app/components/ProviderIcon.tsx | 33 +++++++++++-----------
 .../app/components/__tests__/ProviderIcon.test.tsx | 12 ++++++--
 .../app/utils/__tests__/providerIconKey.test.ts    |  4 +++
 packages/dashboard/app/utils/providerIconKey.ts    |  8 ++++++
 5 files changed, 44 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-7818

Fusion-Task-Lineage: 73e85e18-e98a-4dc3-b89d-f831525620de

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
…piry notifications

Aligns OAuthExpiryMonitor's ntfy push notifications with the /api/auth/status refresh-then-recheck logic that drives the in-app OAuthReloginBanner, so providers that silently auto-refresh (e.g. GitHub Copilot's ephemeral token) no longer trigger false "OAuth token expired" pushes with no matching banner.

- OAuthExpiryMonitor.check() now performs a best-effort authStorage.getApiKey() refresh and reloads/re-resolves the credential before dispatching oauth-token-expired, instead of relying solely on the stored expiry timestamp
- resolveEffectiveOAuthCredential() now also guards against non-finite expires values in addition to non-numeric ones
- Updated docs/dashboard-guide.md and docs/settings-reference.md to describe the refresh-then-recheck behavior generically (not just Claude/Anthropic) and documented the FN-7821 fix in FNXC provenance comments
- Added regression tests covering the refresh-then-recheck flow in oauth-expiry-monitor.test.ts
- Added a patch changeset describing the fix for release notes

Files changed:
 .changeset/fn-7821-oauth-expiry-notification-banner-consistency.md               |   7 +
 docs/dashboard-guide.md                                                          |   6 +-
 docs/settings-reference.md                                                       |   6 +-
 packages/engine/src/notification/__tests__/oauth-expiry-monitor.test.ts          | 146 ++++++++++++++++++++-
 packages/engine/src/notification/oauth-expiry-monitor.ts                         |  48 ++++++-
 5 files changed, 199 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-7821

Fusion-Task-Lineage: 5954592c-adda-4fd4-b205-265860eddf3d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds a shared cost-derivation utility and surfaces token/cost info in a new Cost tab on the task detail modal, plus an opt-in per-card cost badge on the board.

- Extract token-cost calculation into a shared taskTokenCost helper (read-time costFor derivation) reused by the Summary tab, new Cost tab, and card badge
- Add TaskDetailModal Cost tab (TaskCostTab.tsx/.css) showing cost breakdown for a task
- Simplify TaskSummaryTab by delegating cost math to the shared helper
- Add default-off project setting showCostBadgeOnCards (settings-schema.ts, types.ts) with a SettingsModal/AppearanceSection toggle
- Add CostBadgeContext to thread the setting into TaskCard without prop drilling
- Show an optional cost badge on TaskCard when the setting is enabled
- Update i18n strings across en/es/fr/ko/zh-CN/zh-TW locales
- Update docs (dashboard-guide.md, settings-reference.md) and add changeset fn-7820-cost-tab-and-card-badge.md

Files changed:
 .changeset/fn-7820-cost-tab-and-card-badge.md      |   7 ++
 docs/dashboard-guide.md                            |   4 +
 docs/settings-reference.md                         |   1 +
 .../core/src/__tests__/settings-defaults.test.ts   |  13 ++
 packages/core/src/settings-schema.ts               |   5 +
 packages/core/src/types.ts                         |   5 +
 packages/dashboard/app/App.tsx                     |   5 +
 .../dashboard/app/components/SettingsModal.tsx     |   6 +
 packages/dashboard/app/components/TaskCard.css     |   7 +-
 packages/dashboard/app/components/TaskCard.tsx     |  27 +++-
 packages/dashboard/app/components/TaskCostTab.css  |  51 ++++++++
 packages/dashboard/app/components/TaskCostTab.tsx  |  91 ++++++++++++++
 .../dashboard/app/components/TaskDetailModal.tsx   |  14 ++-
 .../dashboard/app/components/TaskSummaryTab.tsx    | 123 +-----------------
 .../app/components/__tests__/TaskCard.test.tsx     |  81 +++++++++++-
 .../app/components/__tests__/TaskCostTab.test.tsx  |  55 ++++++++
 .../TaskDetailModal.attachments-and-tabs.test.tsx  |  11 +-
 .../settings/sections/AppearanceSection.tsx        |   8 ++
 .../sections/__tests__/AppearanceSection.test.tsx  |  20 +++
 .../settings-default-descriptions.test.tsx         |   1 +
 .../dashboard/app/context/CostBadgeContext.tsx     |  19 +++
 packages/dashboard/app/hooks/useAppSettings.ts     |  15 +++
 .../app/utils/__tests__/taskTokenCost.test.ts      |  62 +++++++++
 packages/dashboard/app/utils/taskTokenCost.ts      | 139 +++++++++++++++++++++
 packages/i18n/locales/en/app.json                  |  29 ++++-
 packages/i18n/locales/es/app.json                  |  30 ++++-
 packages/i18n/locales/fr/app.json                  |  27 +++-
 packages/i18n/locales/ko/app.json                  |  30 ++++-
 packages/i18n/locales/zh-CN/app.json               |  30 ++++-
 packages/i18n/locales/zh-TW/app.json               |  30 ++++-
 30 files changed, 789 insertions(+), 157 deletions(-)

Fusion-Task-Id: FN-7820

Fusion-Task-Lineage: d33c5678-a68c-4b29-9db1-8ff0369dfd72

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Nodes in PG mode connect to the same shared PostgreSQL database, so
replicating settings between nodes over the mesh is redundant and can only
introduce churn/clobber against the shared rows. In backend mode:

- POST /api/mesh/sync ignores inbound settings payloads (diagnostic emitted)
  and never includes settings in its response.
- PeerExchangeService force-disables settings gossip regardless of options.
- GET/POST /nodes/:id/settings{,/push,/pull,/sync-status} and
  POST /settings/sync-receive answer 409 with code
  "settings-sync-disabled-postgres" and an explanatory message.
- useNodeSettingsSync treats that 409 as a quiet steady state: stops polling
  the node, renders no sync chips, raises no error banner; explicit push/pull
  clicks surface the server message.

Provider AUTH sync (/nodes/:id/auth/sync, /settings/auth-receive,
/settings/auth-export) is intentionally NOT gated: auth material lives in
per-machine auth.json files, not the shared database, so multi-node
credential propagation still needs it. The legacy SQLite topology keeps the
full sync path unchanged.

Covered by 5 new route tests in routes-system.test.ts. (The pre-existing
system-stats failure in that file predates this change — fails identically
at fdb1d88.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gaps

Both files were red since the PG cutover for missing mock surface, not real
regressions: MockStore lacked getAsyncLayer() (createServer's chat-layer
resolution) and the mesh sync response path constructs a REAL AgentStore
(sqlite init throws on this branch) — now stubbed; routes-system's
createMockStore lacked getAsyncLayer() so system-stats' agent counting
silently zeroed. mesh-routes 31/31 and routes-system 27/27 green for the
first time since the cutover, which also puts real coverage behind the
mesh-sync settings block gated in the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anels

Prevent the terminal header's shortcut/status text and action controls from wrapping onto multiple lines when the floating or docked terminal panel is narrow at desktop breakpoints.

- Add white-space: nowrap to .terminal-shortcuts--header so header help text stays on one line
- Add white-space: nowrap to .terminal-connection-status to keep connection status text from wrapping
- Rely on existing .terminal-actions min-width: 0 + overflow-x: auto pattern for horizontal scrolling instead of wrapping
- Add regression test asserting the nowrap/scroll rules and that mobile still hides these elements
- Add changeset documenting the fix

Files changed:
 .changeset/fn-7823-terminal-header-nowrap.md        |  7 +++++++
 packages/dashboard/app/components/TerminalModal.css |  9 +++++++++
 .../app/components/__tests__/TerminalModal.test.tsx | 21 +++++++++++++++++++++
 3 files changed, 37 insertions(+)

Fusion-Task-Id: FN-7823

Fusion-Task-Lineage: 36426985-a489-41ec-9d79-f8b02862d504

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Removes the single-worktree gate on the Task Detail Terminal tab so it renders for every task, defaulting its first shell to the task worktree when present and otherwise falling back to the project root.

- TaskDetailModal.tsx: showWorktreeTerminalTab is now always true (drops the isWorkspaceTask/single-worktree gate and its stale fallback effect); taskWorktreeCwd still feeds defaultCwd when a worktree is recorded, and the tab renders without requiring taskWorktreeCwd
- Adds a changeset documenting the behavior change (minor, feature) for @runfusion/fusion
- docs/dashboard-guide.md: updates the Terminal tab description to state it is always available, with worktree-or-project-root cwd fallback, including for multi-repo workspace tasks
- Expands TaskDetailModal.worktree-terminal.test.tsx coverage for the no-worktree and workspace-task cases now that the tab is always shown

Files changed:
 .changeset/FN-7826-worktree-terminal-always-available.md          |  7 +++
 docs/dashboard-guide.md                                           |  4 +-
 packages/dashboard/app/components/TaskDetailModal.tsx             | 12 ++---
 .../TaskDetailModal.worktree-terminal.test.tsx                    | 57 +++++++++++++++++++---
 4 files changed, 63 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-7826

Fusion-Task-Lineage: f87504fe-9330-4392-bdbe-33bba006d96c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
…odal and tracks resizes

Fixes the workflow editor shell inheriting the shared .modal max-height: 80vh clamp, which prevented the canvas from filling the FloatingWindow/embedded host and tracking resizes.

- Add max-width: none and max-height: none to .wf-editor-modal so FloatingWindow and the embedded pane own sizing instead of the shared .modal clamp.
- Add regression test asserting the shell stays unclamped across desktop FloatingWindow, embedded, and mobile hosts (not just the reported 80vh repro).

Files changed:
 .../app/components/WorkflowNodeEditor.css          |  6 ++++++
 .../__tests__/WorkflowNodeEditor.css.test.ts       | 22 ++++++++++++++++++++++
 2 files changed, 28 insertions(+)

Fusion-Task-Id: FN-7827

Fusion-Task-Lineage: a39ff6ba-3e72-4f7b-802c-ba1317ca37e9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Store.ts ports into the extraction layout:
- TaskDetailPromptResilience: best-effort PROMPT.md reads in getTask/
  listTasks/searchTasks (reads.ts), archive prompt reader (remaining-ops-5),
  checkbox reset (remaining-ops-6), updateStep auto-init with cause
  attribution (merge-queue-ops); updateTask now writes an explicit prompt
  BEFORE the row commit (atomic) and treats the title/description heading
  sync as best-effort (task-update.ts)
- Video attachments: mime allowlist + 100MB video cap + video artifact
  bridge (store.ts statics, remaining-ops-2)
- updateArtifact: dual-backend port (async updateArtifactRow in
  async-comments-attachments.ts + updateArtifactImpl in remaining-ops-7),
  artifact:registered/updated added to TaskStoreEvents; PG regression test
  added to artifacts-documents-evals.pg.test.ts
- mission-autopilot normalizeCompleteMissionAutopilotState ported async

Kept ours: db.ts / sqlite-adapter.ts stubs (SqliteFinalRemoval — upstream's
v141 sqlite migration and connection-reopen healer target the removed
runtime); sqlite-coupled test files stay deleted per cutover precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps the failure-notification mode fields in the shared .notification-provider-body wrapper so the card's padding/gutters match sibling provider cards on desktop and mobile.

- Wrap failure-notification mode select and delay input in .notification-provider-body within NotificationsSection.tsx
- Add FNXC:SettingsLayout comment documenting why .notification-provider-body is needed (parent .notification-provider-card has no own padding)
- Add regression test asserting the failure-notification mode field nests inside .notification-provider-body, itself inside .notification-provider-card
- Add changeset (patch) documenting the fix

Files changed:
 .changeset/fuzzy-notification-padding.md           |  7 +++
 .../app/__tests__/settings-sections.test.tsx       | 18 ++++++++
 .../settings/sections/NotificationsSection.tsx     | 50 ++++++++++++----------
 3 files changed, 53 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-7828

Fusion-Task-Lineage: 734c652d-400e-4cb1-b6a9-3a001f0b10b7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Removes the hard divider between the Settings navigation rail and content, keeps section rows single-line with ellipsis overflow, and adds a draggable/keyboard-resizable handle that persists the rail's width in localStorage across the standalone modal and embedded Settings page.

- Add a resize handle (.settings-nav-resize-handle) between .settings-navigation and .settings-content, draggable via pointer events and resizable with ArrowLeft/ArrowRight when focused
- Persist chosen width to localStorage (fusion:settings-nav-width), clamped between 200px and 420px, defaulting to 248px; restore on mount
- Make .settings-navigation the sole owner of rail width via a --settings-nav-width CSS custom property instead of a fixed width, and drop the border-right divider
- Keep nav section labels on one line with white-space: nowrap + text-overflow: ellipsis in both the modal (SettingsModal.css) and embedded (styles.css) nav item styles
- Hide the resize handle on mobile; mobile keeps the stacked section picker unaffected
- Update docs/dashboard-guide.md to describe the new divider-less rail and resize behavior
- Add SettingsModal.navResize.test.tsx covering drag-resize, keyboard-resize, and width persistence
- Add changeset .changeset/fn-7825-settings-nav-resizable.md (minor)

Files changed:
 .changeset/fn-7825-settings-nav-resizable.md                                    |   7 +
 docs/dashboard-guide.md                                                         |   3 +
 packages/dashboard/app/components/SettingsModal.css                             |  56 ++++-
 packages/dashboard/app/components/SettingsModal.tsx                             | 124 +++++++++-
 packages/dashboard/app/components/__tests__/SettingsModal.navResize.test.tsx     | 268 +++++++++++++++++++++
 packages/dashboard/app/styles.css                                               |  27 ++-
 6 files changed, 467 insertions(+), 18 deletions(-)

Fusion-Task-Id: FN-7825
Fusion-Task-Lineage: 34b940b4-2698-46a4-b47c-cda0b0cac564
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
…isconnected

Terminal WebSocket sessions now retry with capped backoff through cold-start failures instead of giving up and requiring a manual Reconnect click.

- useTerminal tracks whether a socket has ever successfully opened via hasEverConnectedRef
- a never-connected initial connect ignores MAX_RECONNECT_ATTEMPTS and keeps retrying at capped backoff, staying in the reconnecting affordance until it opens
- mid-session drops (sockets that opened at least once) keep the existing bounded give-up behavior, and permanent 4000/4004 closes remain terminal
- context-change invalidation now uses a ref flag (contextChangedSinceLastEffectRef) consumed inside the effect instead of a transient boolean dependency, avoiding cleanup re-runs that tore down the replacement socket during context-switch/reconnect races
- manual reconnect() and context/session changes reset hasEverConnectedRef so cold-start behavior reapplies per session
- added a patch changeset and expanded useTerminal test coverage for first-launch reconnect vs. mid-session disconnect behavior
- documented the first-launch reconnect behavior in docs/dashboard-guide.md

Files changed:
 .changeset/FN-7824-terminal-first-launch-autoreconnect.md         |   7 +
 docs/dashboard-guide.md                                           |   3 +
 packages/dashboard/app/hooks/__tests__/useTerminal.test.ts        | 208 ++++++++++++++++++++-
 packages/dashboard/app/hooks/useTerminal.ts                       |  34 +++-
 4 files changed, 234 insertions(+), 18 deletions(-)

Fusion-Task-Id: FN-7824

Fusion-Task-Lineage: 7ed696d0-449e-4dc0-9be0-48b429b8c844

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
…ation + conversation cap)

- readLiveTaskRows: column filters and pagination pushed into SQL — WHERE
  column [NOT] = ?, ORDER BY (created_at, numeric id suffix) matching the JS
  comparator, LIMIT/OFFSET — so paginated listTasks stops scanning and
  hydrating the entire live task table per board read; backend listTasks
  drops its client-side filter/slice.
- getConversation (async + sqlite): capped to the most recent 200 messages
  by default (options.limit overrides), oldest-first order preserved — the
  CLI chat no longer loads unbounded history into AI context per exchange.
- Regression tests: store-list.pg.test.ts pins SQL-page == old JS-page
  (numeric id tiebreak, offset windows, column-filter composition);
  satellite-db-injected-stores pins the cap window + ordering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… view; fix Add-comment CSS-order regression

- Task Documents right pane gains in-place editing with the shared CodeMirror
  FileEditor (Save via PUT /tasks/:id/documents/:key); task documents now
  render markdown by default.
- Project Files pane is editable the same way via the project workspace file
  API, replacing the Read-only badge contract.
- Fix "Add comment" doing nothing again: `.selection-comment-trigger:active`
  tied the global `.btn:active` at (0,2,0) and lost to a bundle-order flip,
  teleporting the trigger mid-press so click never fired. `:active` rules now
  use `.btn.selection-comment-trigger` (0,3,0); regression test asserts the
  prefix so a plain-selector revert fails.
- Align the task-document header: path box and Plain/Edit (Cancel/Save)
  actions share one row, meta line sits below.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- scope session collections to the active project\n- clean up runtime stage registration in discovery tests\n- model brainstorms and plans as repeatable artifact collections\n- compact the singleton Strategy presentation
…e Usage dropdown

Claude per-model weekly usage is parsed generically from the OAuth payload's
limits[] scoped entries (live probe disproved the seven_day_fable key guess).
Grok now prefers ~/.grok/auth.json OIDC credentials against
cli-chat-proxy.grok.com/v1/billing?format=credits for a real percent-used
weekly credits window, falling back to the xAI API-key validity card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflicts were confined to the compound-engineering plugin's session store:
upstream's plan-handoff claim (atomic ce_plan_handoff_claims transaction in
create/delete) is kept and rebased onto this branch's nullable-db backend
seam (this.db → syncDb() local). The two sqlite-harness plugin test files
stay deleted per cutover precedent (upstream's claim-coverage additions rode
on the removed makeHarness).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uster (task project_id, Approach A)

Squashed net change of TrinaryCompute/postgres-v057 rebased onto
feature/postgres so the PR is a single linear commit (GitHub rebase-merge
cannot replay the original merge-commit history).

- project_id partition key on project.tasks / project.archived_tasks
  (schema, baseline DDL, EXPECTED_PROJECT_COLUMNS self-heal), stamped on
  every insert/upsert via the layer-bound projectId
- taskProjectScope folded into every scan: readLiveTaskRows (composing
  with the SQL column filter + pagination), counts, merge-lease candidate
  scan, search, allocator
- per-project config rows (project_id-keyed upserts) + test-harness seed
- startup factory binds the AsyncDataLayer to options.projectId; first-boot
  SQLite auto-migration scopes its emptiness check per-project and stamps
  project_id on migrated rows
- ProjectManager: live global-concurrency listener (concurrency:changed)
  applied to the shared semaphore immediately

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants