From c745990aa294ba7d915fcf5862a2dc634af57bb3 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 12 Jul 2026 17:02:33 -0700 Subject: [PATCH] FN-7879: deliver one-time Postgres-migration inbox notice on first 0.59 startup Adds a best-effort, idempotent dashboard inbox notice announcing the upcoming embedded-Postgres storage migration, delivered once per project on the first engine start under the Fusion 0.59.x release line. - New `deliverPostgresMigrationNoticeIfNeeded` in `@fusion/engine` (`postgres-migration-notice.ts`) builds and sends a `system` -> `user` inbox message via `MessageStore`, gated to version `0.59.x` by `isPostgresMigrationNoticeVersion` - Idempotency via existing inbox message `metadata.kind = "postgres-migration-notice"` marker (no new settings key or table), so restarts never duplicate the notice - Delivery is fully best-effort: any `MessageStore` failure is caught, logged as a warning, and never blocks or fails `ProjectEngine.start()` - `ProjectEngine.start()` invokes the notice after runtime start, using an injected `cliPackageVersion` threaded from the CLI layer through `EngineManagerOptions` / `ProjectEngineOptions` so the engine never imports CLI/dashboard code directly - `daemon.ts`, `dashboard.ts`, and `serve.ts` resolve the published `@runfusion/fusion` version via `getCliPackageVersion` / `isUnresolvedCliPackageVersion` and pass it into `ProjectEngineManager` - Exported new symbols (`POSTGRES_MIGRATION_HELP_URL`, `POSTGRES_MIGRATION_NOTICE_KIND`, `deliverPostgresMigrationNoticeIfNeeded`, `isPostgresMigrationNoticeVersion`, related types) from `@fusion/engine`, and `isUnresolvedCliPackageVersion` from `@fusion/dashboard` - New unit tests covering version matching and single-delivery/idempotency behavior - Docs updated (`docs/agents.md`, `docs/dashboard-guide.md`) to describe the one-time notice and its dedup key - Changeset added for `@runfusion/fusion` (minor, feature) Files changed: .changeset/fn-7879-postgres-migration-inbox-notice.md | 7 ++ docs/agents.md | 1 + docs/dashboard-guide.md | 1 + packages/cli/src/commands/daemon.ts | 6 +- packages/cli/src/commands/dashboard.ts | 5 + packages/cli/src/commands/serve.ts | 6 +- packages/dashboard/src/index.ts | 2 +- packages/engine/src/__tests__/postgres-migration-notice.test.ts | 140 +++++++++++++++++++++ packages/engine/src/index.ts | 9 ++ packages/engine/src/postgres-migration-notice.ts | 107 ++++++++++++++++ packages/engine/src/project-engine-manager.ts | 6 + packages/engine/src/project-engine.ts | 12 ++ 12 files changed, 299 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-7879 Fusion-Task-Lineage: 201877e5-6bdc-4168-a8ac-ae0e50ec8308 Co-authored-by: Fusion (runfusion.ai) --- ...fn-7879-postgres-migration-inbox-notice.md | 7 + docs/agents.md | 1 + docs/dashboard-guide.md | 1 + packages/cli/src/commands/daemon.ts | 6 +- packages/cli/src/commands/dashboard.ts | 5 + packages/cli/src/commands/serve.ts | 6 +- packages/dashboard/src/index.ts | 2 +- .../postgres-migration-notice.test.ts | 140 ++++++++++++++++++ packages/engine/src/index.ts | 9 ++ .../engine/src/postgres-migration-notice.ts | 107 +++++++++++++ packages/engine/src/project-engine-manager.ts | 6 + packages/engine/src/project-engine.ts | 12 ++ 12 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-7879-postgres-migration-inbox-notice.md create mode 100644 packages/engine/src/__tests__/postgres-migration-notice.test.ts create mode 100644 packages/engine/src/postgres-migration-notice.ts diff --git a/.changeset/fn-7879-postgres-migration-inbox-notice.md b/.changeset/fn-7879-postgres-migration-inbox-notice.md new file mode 100644 index 0000000000..643f78416a --- /dev/null +++ b/.changeset/fn-7879-postgres-migration-inbox-notice.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Deliver a one-time inbox notice about the upcoming Postgres storage migration on first 0.59 startup. +category: feature +dev: New best-effort, idempotent `deliverPostgresMigrationNoticeIfNeeded` in `@fusion/engine`, invoked from `ProjectEngine.start()`; gated to version `0.59.x` via injected `cliPackageVersion` (threaded through `EngineManagerOptions`/`ProjectEngineOptions`); idempotency via inbox `metadata.kind = "postgres-migration-notice"` marker. Links to Discord (`https://discord.gg/ksrfuy7WYR`). diff --git a/docs/agents.md b/docs/agents.md index 43bf49b993..c97aacfcdf 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -49,6 +49,7 @@ Artifact tools operate on the shared artifact registry, so artifacts are visible - `fn_artifact_list` lists artifacts across agents and tasks with optional `type`, `authorId`, `taskId`, `search`, `limit`, and `offset` filters. Dashboard chat's scoped variant requires `task_id` and otherwise supports `type`, `authorId`, `search`, `limit`, and `offset` for that task. - `fn_artifact_view` fetches one artifact by `id`, returning registry metadata plus inline `content` when present or the stored `uri`/path reference for media artifacts. - Successful registration emits a best-effort `system` → `user` inbox notification to `DASHBOARD_USER_ID` with `artifactId`, `artifactType`, `title`, `authorId`, and optional `taskId` metadata. Notification delivery failures are logged and must never fail or roll back the artifact registration. +- On first startup under Fusion `0.59.x`, the engine also sends one best-effort `system` → `user` inbox notice per project about the upcoming embedded-Postgres storage migration, keyed by `metadata.kind = "postgres-migration-notice"` so restarts do not duplicate it. For the user-facing gallery and notification UX, see [Artifacts View](./dashboard-guide.md#artifacts-view) and [Mailbox View](./dashboard-guide.md#mailbox-view). For storage layout and hydration semantics, see [Artifact registry](./storage.md#artifact-registry-fn-6777). diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 9b43d13777..789300e99b 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -619,6 +619,7 @@ Mailbox view shows inbox/outbox communication threads and unread state. - clicking a message in the Mail tab opens the task detail pane with full message content and conversation context - reply rows in the mailbox modal can expand inline to show the replied-to message context for easier thread reading - when an agent or dashboard chat session registers an artifact with `fn_artifact_register`, Fusion sends a best-effort `system` → user inbox message announcing the new artifact (for example, `New image artifact registered: `) with metadata for `artifactId`, `artifactType`, `title`, optional `mimeType`, `authorId`, and optional `taskId`; notification delivery is informational and never blocks or rolls back the artifact registration. Artifact notifications are actionable in message detail views: image artifacts show an inline preview plus **Open artifact**, while video/audio/document/other artifacts show an **Open artifact** link to the managed media URL. +- on first engine startup under Fusion `0.59.x`, each project receives one best-effort `system` inbox notice about the upcoming embedded-Postgres storage migration with the Discord help link; `metadata.kind = "postgres-migration-notice"` prevents duplicates across restarts. - mailbox now includes an **Approvals** tab with pending and history filters (`approved` / `denied` / `completed`), approval detail context, and inline approve/deny actions for pending requests - for approvals gated by an agent's permission policy (permanent agents and task-worker heartbeats), the Approvals detail pane renders the gated action's real payload — tool name, shell command line or structured arguments, and working directory when present — instead of only a generic "Agent gated action for `<tool>`" summary; a stateless heartbeat retrying the same gated command reuses the existing pending approval instead of creating a duplicate (FN-7609) - in the **Agents** tab, the agent selector now includes **All agents**, which shows one combined agent-to-agent stream (with sender + recipient labels); selecting a specific agent still shows Inbox/Outbox subtabs diff --git a/packages/cli/src/commands/daemon.ts b/packages/cli/src/commands/daemon.ts index cddbcdb194..df20800dfa 100644 --- a/packages/cli/src/commands/daemon.ts +++ b/packages/cli/src/commands/daemon.ts @@ -29,7 +29,7 @@ import { registerBuiltInZaiProvider, } from "@fusion/core"; import type { AutomationRunResult, ScheduledTask } from "@fusion/core"; -import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, refreshAllCustomProviderModels, registerGithubTrackingHook } from "@fusion/dashboard"; +import { createServer, GitHubClient, createSkillsAdapter, getCliPackageVersion, getProjectSettingsPath, isUnresolvedCliPackageVersion, loadTlsCredentialsFromEnv, refreshAllCustomProviderModels, registerGithubTrackingHook } from "@fusion/dashboard"; import { ProjectEngineManager, PeerExchangeService, @@ -341,7 +341,11 @@ export async function runDaemon(opts: DaemonOptions = {}) { // Some tests partially mock @fusion/dashboard and omit this export. } + const resolvedCliPackageVersion = getCliPackageVersion(import.meta.url); + const cliPackageVersion = isUnresolvedCliPackageVersion(resolvedCliPackageVersion) ? undefined : resolvedCliPackageVersion; + const engineManager = new ProjectEngineManager(sharedCentralCore, { + cliPackageVersion, getMergeStrategy, processPullRequestMerge: (s, wd, taskId, pool) => processPullRequestMergeTask(s, wd, taskId, githubClient, getTaskMergeBlocker, pool), diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index ab6d139956..05a68f0ab1 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -42,6 +42,7 @@ import { GitHubClient, createSkillsAdapter, getCliPackageVersion, + isUnresolvedCliPackageVersion, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook, @@ -1854,7 +1855,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?: // Some tests partially mock @fusion/dashboard and omit this export. } + const resolvedCliPackageVersion = getCliPackageVersion(import.meta.url); + const cliPackageVersion = isUnresolvedCliPackageVersion(resolvedCliPackageVersion) ? undefined : resolvedCliPackageVersion; + const engineManager = new ProjectEngineManager(centralCoreForEngine, { + cliPackageVersion, getMergeStrategy, processPullRequestMerge: (s, wd, taskId, pool) => processPullRequestMergeTask(s, wd, taskId, githubClient, getTaskMergeBlocker, pool), diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index ef09dffa79..ab4ece736e 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -29,7 +29,7 @@ import { registerBuiltInZaiProvider, } from "@fusion/core"; import type { AutomationRunResult, ScheduledTask } from "@fusion/core"; -import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, refreshAllCustomProviderModels, registerGithubTrackingHook } from "@fusion/dashboard"; +import { createServer, GitHubClient, createSkillsAdapter, getCliPackageVersion, getProjectSettingsPath, isUnresolvedCliPackageVersion, loadTlsCredentialsFromEnv, refreshAllCustomProviderModels, registerGithubTrackingHook } from "@fusion/dashboard"; import { ProjectEngineManager, PeerExchangeService, @@ -366,7 +366,11 @@ export async function runServe( // Some tests partially mock @fusion/dashboard and omit this export. } + const resolvedCliPackageVersion = getCliPackageVersion(import.meta.url); + const cliPackageVersion = isUnresolvedCliPackageVersion(resolvedCliPackageVersion) ? undefined : resolvedCliPackageVersion; + const engineManager = new ProjectEngineManager(sharedCentralCore, { + cliPackageVersion, getMergeStrategy, processPullRequestMerge: (s, wd, taskId, pool) => processPullRequestMergeTask(s, wd, taskId, githubClient, getTaskMergeBlocker, pool), diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index 1fd6635826..57fa313c09 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -112,7 +112,7 @@ export { export { GitHubTrackingCommentService, formatTrackingComment } from "./github-tracking-comments.js"; export { GitHubTrackingStateService, decideIssueAction } from "./github-tracking-state.js"; export { GitHubTrackingReconciler, RECONCILE_CONCURRENCY_LIMIT, RECONCILE_SCAN_LIMIT } from "./github-tracking-reconciler.js"; -export { getCliPackageVersion, resolveCliPackageVersionInfo, type CliPackageVersionInfo } from "./cli-package-version.js"; +export { getCliPackageVersion, isUnresolvedCliPackageVersion, resolveCliPackageVersionInfo, type CliPackageVersionInfo } from "./cli-package-version.js"; export { ApiError, type ApiErrorResponse, diff --git a/packages/engine/src/__tests__/postgres-migration-notice.test.ts b/packages/engine/src/__tests__/postgres-migration-notice.test.ts new file mode 100644 index 0000000000..89d10de0d9 --- /dev/null +++ b/packages/engine/src/__tests__/postgres-migration-notice.test.ts @@ -0,0 +1,140 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { DASHBOARD_USER_ID, Database, MessageStore, type MessageStore as MessageStoreType } from "@fusion/core"; +import { + POSTGRES_MIGRATION_HELP_URL, + POSTGRES_MIGRATION_NOTICE_KIND, + deliverPostgresMigrationNoticeIfNeeded, + isPostgresMigrationNoticeVersion, +} from "../postgres-migration-notice.js"; + +describe("postgres migration notice", () => { + let db: Database; + let store: MessageStore; + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "kb-postgres-notice-test-")); + db = new Database(tempDir, { inMemory: true }); + db.init(); + store = new MessageStore(db); + }); + + afterEach(() => { + vi.restoreAllMocks(); + db.close(); + rmSync(tempDir, { recursive: true, force: true }); + }); + + it("matches only the 0.59 release line", () => { + expect(isPostgresMigrationNoticeVersion("0.59.0")).toBe(true); + expect(isPostgresMigrationNoticeVersion("0.59.3")).toBe(true); + expect(isPostgresMigrationNoticeVersion("0.59.10")).toBe(true); + + expect(isPostgresMigrationNoticeVersion(undefined)).toBe(false); + expect(isPostgresMigrationNoticeVersion("")).toBe(false); + expect(isPostgresMigrationNoticeVersion("0.58.0")).toBe(false); + expect(isPostgresMigrationNoticeVersion("0.60.0")).toBe(false); + expect(isPostgresMigrationNoticeVersion("1.59.0")).toBe(false); + expect(isPostgresMigrationNoticeVersion("0.0.0")).toBe(false); + expect(isPostgresMigrationNoticeVersion("0.0.0-dev")).toBe(false); + expect(isPostgresMigrationNoticeVersion("not-semver")).toBe(false); + }); + + it("delivers exactly one system inbox notice for 0.59.x", async () => { + const result = await deliverPostgresMigrationNoticeIfNeeded({ + messageStore: store, + version: "0.59.3", + }); + + expect(result).toBe("delivered"); + const inbox = store.getInbox(DASHBOARD_USER_ID, "user", { type: "system" }); + expect(inbox).toHaveLength(1); + expect(inbox[0]).toEqual(expect.objectContaining({ + fromType: "system", + toType: "user", + toId: DASHBOARD_USER_ID, + type: "system", + metadata: expect.objectContaining({ + kind: POSTGRES_MIGRATION_NOTICE_KIND, + version: "0.59.3", + helpUrl: POSTGRES_MIGRATION_HELP_URL, + }), + })); + expect(inbox[0]?.content).toContain("Postgres backend for data storage"); + expect(inbox[0]?.content).toContain(POSTGRES_MIGRATION_HELP_URL); + }); + + it("is idempotent across restart-like repeated calls", async () => { + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: store, + version: "0.59.0", + })).resolves.toBe("delivered"); + + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: store, + version: "0.59.0", + })).resolves.toBe("already-delivered"); + + const inbox = store.getInbox(DASHBOARD_USER_ID, "user", { type: "system" }); + expect(inbox).toHaveLength(1); + }); + + it("does not deliver for version mismatches", async () => { + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: store, + version: "0.58.0", + })).resolves.toBe("version-mismatch"); + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: store, + version: undefined, + })).resolves.toBe("version-mismatch"); + + expect(store.getInbox(DASHBOARD_USER_ID, "user", { type: "system" })).toHaveLength(0); + }); + + it("returns no-store when the message store is unavailable", async () => { + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: undefined, + version: "0.59.0", + })).resolves.toBe("no-store"); + }); + + it("swallows getInbox failures and logs a warning", async () => { + const warn = vi.fn(); + const throwingStore = { + getInbox: vi.fn(() => { + throw new Error("inbox unavailable"); + }), + sendMessage: vi.fn(), + } as unknown as MessageStoreType; + + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: throwingStore, + version: "0.59.0", + log: { warn }, + })).resolves.toBe("already-delivered"); + + expect(warn).toHaveBeenCalledWith(expect.stringContaining("inbox unavailable")); + }); + + it("swallows sendMessage failures and logs a warning", async () => { + const warn = vi.fn(); + const throwingStore = { + getInbox: vi.fn(() => []), + sendMessage: vi.fn(() => { + throw new Error("send unavailable"); + }), + } as unknown as MessageStoreType; + + await expect(deliverPostgresMigrationNoticeIfNeeded({ + messageStore: throwingStore, + version: "0.59.0", + log: { warn }, + })).resolves.toBe("already-delivered"); + + expect(warn).toHaveBeenCalledWith(expect.stringContaining("send unavailable")); + }); +}); diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 1b62544c42..d0e23407a6 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -70,6 +70,15 @@ export { traitListParams, executeApprovedAgentProvisioning, } from "./agent-tools.js"; +export { + POSTGRES_MIGRATION_HELP_URL, + POSTGRES_MIGRATION_NOTICE_KIND, + deliverPostgresMigrationNoticeIfNeeded, + isPostgresMigrationNoticeVersion, + type DeliverPostgresMigrationNoticeArgs, + type PostgresMigrationNoticeLog, + type PostgresMigrationNoticeResult, +} from "./postgres-migration-notice.js"; export { AgentSemaphore, PRIORITY_MERGE, PRIORITY_EXECUTE, PRIORITY_SPECIFY } from "./concurrency.js"; export { TriageProcessor, type TriageProcessorOptions } from "./triage.js"; export { TaskExecutor, type TaskExecutorOptions } from "./executor.js"; diff --git a/packages/engine/src/postgres-migration-notice.ts b/packages/engine/src/postgres-migration-notice.ts new file mode 100644 index 0000000000..5b5deaf67f --- /dev/null +++ b/packages/engine/src/postgres-migration-notice.ts @@ -0,0 +1,107 @@ +import { DASHBOARD_USER_ID, type MessageStore } from "@fusion/core"; + +export const POSTGRES_MIGRATION_NOTICE_KIND = "postgres-migration-notice"; +export const POSTGRES_MIGRATION_HELP_URL = "https://discord.gg/ksrfuy7WYR"; + +export type PostgresMigrationNoticeResult = "delivered" | "already-delivered" | "version-mismatch" | "no-store"; + +export interface PostgresMigrationNoticeLog { + warn(message: string): void; + log?(message: string): void; +} + +export interface DeliverPostgresMigrationNoticeArgs { + messageStore: MessageStore | undefined; + version: string | undefined; + log?: PostgresMigrationNoticeLog; +} + +const POSTGRES_MIGRATION_NOTICE_TITLE = "Storage update coming in the next Fusion version"; +const POSTGRES_MIGRATION_NOTICE_BODY = "The next Fusion version will replace the current SQLite data store with an embedded Postgres backend for data storage, and project databases will be served from the central Fusion database instead of each project's local .fusion/fusion.db SQLite file. No migration runs from this notice; it is an advance heads-up for operators who rely on the current storage layout."; + +/* +FNXC:StorageMigrationNotice 2026-07-12-00:00: +Fusion 0.59.x is the only release line that should deliver the durable Postgres-migration heads-up: it is the first-start announcement window before the storage backend change, while dev/unresolved sentinels and all other versions must stay silent so local source builds and later restarts do not surprise operators. +*/ +export function isPostgresMigrationNoticeVersion(version: string | undefined): boolean { + const trimmed = version?.trim(); + if (!trimmed) return false; + + const match = /^(\d+)\.(\d+)\.(\d+)(?:[+-].*)?$/.exec(trimmed); + if (!match) return false; + + const major = Number.parseInt(match[1] ?? "", 10); + const minor = Number.parseInt(match[2] ?? "", 10); + const patch = Number.parseInt(match[3] ?? "", 10); + if (!Number.isSafeInteger(major) || !Number.isSafeInteger(minor) || !Number.isSafeInteger(patch)) { + return false; + } + + if (major === 0 && minor === 0 && patch === 0) { + return false; + } + + return major === 0 && minor === 59; +} + +function buildPostgresMigrationNoticeContent(): string { + return [ + POSTGRES_MIGRATION_NOTICE_TITLE, + "", + POSTGRES_MIGRATION_NOTICE_BODY, + "", + `Need help? Join us on Discord: ${POSTGRES_MIGRATION_HELP_URL}`, + ].join("\n"); +} + +/* +FNXC:StorageMigrationNotice 2026-07-12-00:00: +The startup notice is per project because each project has its own dashboard mailbox. Idempotency uses the durable system-inbox metadata.kind marker instead of a new settings key or marker table: the mailbox survives engine restarts and is the operator-visible record we are protecting from duplication. + +FNXC:StorageMigrationNotice 2026-07-12-00:00: +Delivery is best-effort by design. ProjectEngine.start() must never fail or stall because an informational support message could not be queried or written, so every MessageStore interaction is contained here and downgraded to a warning plus a non-delivered result. +*/ +export async function deliverPostgresMigrationNoticeIfNeeded({ + messageStore, + version, + log, +}: DeliverPostgresMigrationNoticeArgs): Promise<PostgresMigrationNoticeResult> { + let fallbackResult: PostgresMigrationNoticeResult = "version-mismatch"; + + try { + if (!messageStore) { + return "no-store"; + } + fallbackResult = "no-store"; + + if (!isPostgresMigrationNoticeVersion(version)) { + return "version-mismatch"; + } + fallbackResult = "already-delivered"; + + const inbox = messageStore.getInbox(DASHBOARD_USER_ID, "user", { type: "system" }); + if (inbox.some((message) => message.metadata?.kind === POSTGRES_MIGRATION_NOTICE_KIND)) { + return "already-delivered"; + } + + messageStore.sendMessage({ + fromType: "system", + toType: "user", + toId: DASHBOARD_USER_ID, + type: "system", + content: buildPostgresMigrationNoticeContent(), + metadata: { + kind: POSTGRES_MIGRATION_NOTICE_KIND, + version, + helpUrl: POSTGRES_MIGRATION_HELP_URL, + }, + }); + + log?.log?.("Delivered Postgres migration dashboard inbox notice"); + return "delivered"; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log?.warn(`Postgres migration inbox notice delivery failed (continuing startup): ${message}`); + return fallbackResult; + } +} diff --git a/packages/engine/src/project-engine-manager.ts b/packages/engine/src/project-engine-manager.ts index f862a46403..296413af77 100644 --- a/packages/engine/src/project-engine-manager.ts +++ b/packages/engine/src/project-engine-manager.ts @@ -34,6 +34,11 @@ import { runtimeLog } from "./logger.js"; * These are injected by the CLI layer (dashboard.ts / serve.ts). */ export interface EngineManagerOptions { + /** + * FNXC:StorageMigrationNotice 2026-07-12-00:00: + * The manager carries the resolved CLI package version to each per-project engine so the one-time Postgres-migration inbox message is evaluated per project while remaining gated to the same released runtime version. + */ + cliPackageVersion?: ProjectEngineOptions["cliPackageVersion"]; getMergeStrategy?: ProjectEngineOptions["getMergeStrategy"]; processPullRequestMerge?: ProjectEngineOptions["processPullRequestMerge"]; createGroupPr?: ProjectEngineOptions["createGroupPr"]; @@ -533,6 +538,7 @@ export class ProjectEngineManager { ): ProjectEngineOptions { return { projectId: project.id, + cliPackageVersion: this.options.cliPackageVersion, getMergeStrategy: this.options.getMergeStrategy, processPullRequestMerge: this.options.processPullRequestMerge, createGroupPr: this.options.createGroupPr, diff --git a/packages/engine/src/project-engine.ts b/packages/engine/src/project-engine.ts index f2f286f169..fc38695a49 100644 --- a/packages/engine/src/project-engine.ts +++ b/packages/engine/src/project-engine.ts @@ -75,6 +75,7 @@ import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js"; import { isTransientError } from "./transient-error-detector.js"; import { classifyTransientMergeError } from "./transient-merge-error-classifier.js"; import { TunnelProcessManager } from "./remote-access/tunnel-process-manager.js"; +import { deliverPostgresMigrationNoticeIfNeeded } from "./postgres-migration-notice.js"; import type { ExternalTunnelInfo, TunnelProvider, @@ -261,6 +262,11 @@ export interface ProjectEngineOptions { projectId?: string; /** Base URL for ntfy.sh notifications */ ntfyBaseUrl?: string; + /** + * FNXC:StorageMigrationNotice 2026-07-12-00:00: + * The CLI layer injects the resolved published @runfusion/fusion version so startup-only operator notices can be gated to release lines without importing dashboard/CLI code into the engine. When absent or unresolved, the Postgres-migration inbox notice is skipped safely. + */ + cliPackageVersion?: string; /** * An already-initialized TaskStore to use instead of creating a new one. * When provided, InProcessRuntime will skip TaskStore construction and init(). @@ -613,6 +619,12 @@ export class ProjectEngine { // 1. Start the core runtime (TaskStore, Scheduler, Executor, Triage, etc.) await this.runtime.start(); + await deliverPostgresMigrationNoticeIfNeeded({ + messageStore: this.runtime.getMessageStore(), + version: this.options.cliPackageVersion, + log: runtimeLog, + }); + const store = this.runtime.getTaskStore(); const cwd = this.config.workingDirectory; const settings = await store.getSettings();