fix(FN-7952): establish PostgreSQL core authority (#2108)

## Summary

Fusion’s core runtime now treats PostgreSQL as the authoritative
metadata store without leaving current CLI, dashboard, desktop, or
engine composition roots uncompilable between stack layers. This is the
99-file foundation for the larger cutover: subsequent PRs migrate the
remaining consumers, plugins, and operator surfaces.

## Design decisions

- Runtime store construction fails closed when an asynchronous
PostgreSQL layer is unavailable; SQLite remains readable only at
explicit migration and identity-recovery boundaries.
- Project ownership is enforced across active, archived, workflow,
mission, analytics, and plugin-schema data.
- The small set of cross-package files in this layer are
compatibility-critical call sites required for a green intermediate
commit, not the complete consumer migration.
- Schema migration 0008 remains assigned to session-advisor state from
current `main`; mission lineage idempotency advances to 0009 so neither
invariant can be skipped.

## Validation

- All affected package typechecks pass: Core, Engine, Dashboard, CLI,
and Desktop.
- `pnpm test:gate` passes: 478 tests across the engine gate, PostgreSQL
core gate, and CLI workflow shape.
- The PR changes exactly 99 files.

## Stack

This is the base PR. Engine/dashboard, CLI/desktop/ops, plugins, and
docs/release follow as stacked PRs, each below 100 changed files.

Related: #2105


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* PostgreSQL is now the standard runtime backend, with embedded
PostgreSQL enabled by default.
* Added project-scoped storage for tasks, archives, chat sessions,
missions, knowledge pages, and operational data.
* Improved archived-task search, filtering, pagination, and restoration.
* Added safer plugin schema initialization with validation and project
isolation.
* Added PostgreSQL-backed workflow, mission, validator, and dashboard
capabilities.

* **Bug Fixes**
  * Improved startup timeout cancellation and resource cleanup.
* Prevented cross-project data access and phantom reservation cleanup
errors.
* Ensured archived tasks remain read-only and asynchronous writes
complete reliably.
  * Retired SQLite opt-out settings with clear startup errors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-14 22:13:30 -07:00
committed by GitHub
parent e97081fb77
commit 2e4fcfcaea
99 changed files with 7026 additions and 5166 deletions

View File

@@ -11,7 +11,7 @@ const WORKFLOW_THINKING_LEVEL_SET: ReadonlySet<string> = new Set(THINKING_LEVELS
import { delimiter, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
import { existsSync, lstatSync, realpathSync } from "node:fs";
import { readFile, rm, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext, AgentHeartbeatConfig, Agent, AgentMemoryInclusionMode, ProjectSettings, MergeResult, WorkflowIrNode, WorkflowIrNodeKind, WorkflowStepResult as CoreWorkflowStepResult, ThinkingLevel } from "@fusion/core";
import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings, WorkflowStep, MissionStore, AsyncMissionStore, Slice, AgentState, AgentCapability, RunMutationContext, AgentHeartbeatConfig, Agent, AgentMemoryInclusionMode, ProjectSettings, MergeResult, WorkflowIrNode, WorkflowIrNodeKind, WorkflowStepResult as CoreWorkflowStepResult, ThinkingLevel } from "@fusion/core";
import { getUnmetSchedulingDependencies } from "./scheduler.js";
import { RetryStormError, TaskDeletedError, serializeRetryStormError, isExperimentalFeatureEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isLiveSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON, THINKING_LEVELS, AgentStore } from "@fusion/core";
import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js";
@@ -1693,7 +1693,7 @@ export interface TaskExecutorOptions {
pluginRunner?: PluginRunner;
/** MessageStore for sending messages to other agents. When provided, executor agents gain fn_send_message capability. */
messageStore?: import("@fusion/core").MessageStore;
missionStore?: MissionStore;
missionStore?: MissionStore | AsyncMissionStore;
secretsStore?: Pick<import("@fusion/core").SecretsStore, "listEnvExportable">;
onSliceComplete?: (slice: Slice) => void;
onStart?: (task: Task, worktreePath: string) => void;
@@ -2344,7 +2344,9 @@ export class TaskExecutor {
private get approvalRequestStore(): ApprovalRequestStore {
if (!this._approvalRequestStore) {
const layer = this.store.getAsyncLayer();
this._approvalRequestStore = new ApprovalRequestStore(layer ? null : this.store.getDatabase(), { asyncLayer: layer });
if (!layer) throw new Error("Executor TaskStore is missing its PostgreSQL AsyncDataLayer");
/* FNXC:PostgresSatelliteCutover 2026-07-14-17:30: Runtime approval persistence is PostgreSQL-only; never reopen the removed project SQLite database when backend wiring is incomplete. */
this._approvalRequestStore = new ApprovalRequestStore(null, { asyncLayer: layer });
}
return this._approvalRequestStore;
}
@@ -6052,9 +6054,7 @@ export class TaskExecutor {
},
{ columnSequence: this.inferLegacyColumnSequence(live.column) },
);
const legacyAudit = typeof this.store.getRunAuditEvents === "function"
? this.store.getRunAuditEvents({ taskId })
: [];
const legacyAudit = await this.store.getRunAuditEventsAsync({ taskId });
await observeWorkflowParity({
settings,
@@ -13603,7 +13603,7 @@ export class TaskExecutor {
// FN-009: If worktree directory doesn't exist, skip git validation for task completion.
// This is safe because:
// 1. Task completion doesn't modify the worktree
// 2. Deliverables (task documents, follow-up tasks) are stored in fusion.db
// FNXC:PostgresRuntimeStorage 2026-07-14-18:47: Deliverables (task documents and follow-up tasks) are stored in the project-scoped PostgreSQL store.
// 3. If code changes were made, the worktree would exist
// 4. This prevents ENOENT errors when agents complete documentation/coordination tasks
if (!existsSync(worktreePath)) {
@@ -14583,7 +14583,7 @@ export class TaskExecutor {
}
if (branchDeleted) {
// FN-2165 regression guard: null baseBranch on any task that stored this branch
try { this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
try { await this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
}
// Clear worktree tracking
@@ -17737,7 +17737,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
});
await this.store.logEntry(taskId, `Deleted branch`, branch);
// FN-2165 regression guard: null baseBranch on any task that stored this branch
this.store.clearStaleExecutionStartBranchReferences([branch], taskId);
await this.store.clearStaleExecutionStartBranchReferences([branch], taskId);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
executorLog.warn(`${taskId}: failed to delete conflicting branch ${branch}: ${msg}`);
@@ -17825,7 +17825,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
}
try {
await execAsync(`git branch -D "${branch}"`, { cwd: this.rootDir });
this.store.clearStaleExecutionStartBranchReferences([branch], taskId);
await this.store.clearStaleExecutionStartBranchReferences([branch], taskId);
} catch {
// best-effort — branch may not exist, which is fine for a stale-path cleanup
}
@@ -17874,7 +17874,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
});
await this.store.logEntry(taskId, `Removed stale branch`, branch);
// FN-2165 regression guard: null baseBranch on any task that stored this branch
try { this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
try { await this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
return true;
} catch (branchDeleteError: unknown) {
const branchDeleteErrorMessage = branchDeleteError instanceof Error ? branchDeleteError.message : String(branchDeleteError);
@@ -17893,7 +17893,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
});
await this.store.logEntry(taskId, `Force-removed stale branch reference via update-ref`, refPath);
// FN-2165 regression guard: null baseBranch on any task that stored this branch
try { this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
try { await this.store.clearStaleExecutionStartBranchReferences([branch], taskId); } catch { /* best-effort */ }
return true;
} catch (updateRefError: unknown) {
const updateRefErrorMessage = updateRefError instanceof Error ? updateRefError.message : String(updateRefError);