feat(postgres): v0.57 embedded-PG migration — rebase, blocker fixes, multi-project isolation#1
Open
TrinaryCompute wants to merge 159 commits into
Open
feat(postgres): v0.57 embedded-PG migration — rebase, blocker fixes, multi-project isolation#1TrinaryCompute wants to merge 159 commits into
TrinaryCompute wants to merge 159 commits into
Conversation
…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)
- 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>
…date docs screenshot list
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PostgreSQL storage backend for v0.57 — rebase + embedded-PG blocker fixes + multi-project isolation
Rebases the
feature/postgresembedded-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)
mainwas 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:
cwdvsrootDir)recoverStaleTransitionPendingImplthrewSQLite … not available in backend mode— added the missingbackendModebranch (mirrors its already-ported siblings).startup-factory.tsdroppedoptions.rootDir, rooting the store atprocess.cwd(); nowif (projectId && !rootDir).drainMergeQueue()bound git ops toprocess.cwd(); nowstore.getRootDir() ?? config.workingDirectory.Result: triage → dispatch → execute → commit → merge completes in embedded-PG with no SQLite-backend throws.
3. Multi-project isolation
project.taskshad noproject_id(SQLite isolated per-file); consolidating to shared PG tables dropped that partition key. Addedproject_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.configwas a single shared row (CHECK id=1) → one sharedtaskPrefix/caps for all projects and a cross-project mega-context on every triage. Addedproject_idPK +configScope(); each project now gets its own prefix and caps. Validated: distinctBLD/DRF/EXO/RNE/STR/TWRprefixes; triage input 243K → 41–91K tokens.ProjectManagersubscribes toconcurrency:changed) —PUT /api/global-concurrencyapplies 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.