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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user