diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index 042a0deb7..3ffd71e2b 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -584,6 +584,14 @@ export function createMemoryCore( } } + function scheduleStartupRecovery(label: string, task: () => Promise): void { + void task().catch((err) => { + log.debug(`${label}.failed`, { + err: err instanceof Error ? err.message : String(err), + }); + }); + } + // ─── Lifecycle ── async function init(): Promise { if (shutDown) { @@ -617,14 +625,18 @@ export function createMemoryCore( }); } if (stale.length > 0) { - await recoverOpenEpisodesAsSessionEnd(stale); + scheduleStartupRecovery("startup.open_recovery", async () => { + await recoverOpenEpisodesAsSessionEnd(stale); + }); } } const dirtyClosed = handle.repos.episodes .list({ status: "closed", limit: 500 }) .filter((ep) => episodeRewardIsDirty(ep)); if (dirtyClosed.length > 0) { - await recoverDirtyClosedEpisodes(dirtyClosed); + scheduleStartupRecovery("startup.dirty_closed_recovery", async () => { + await recoverDirtyClosedEpisodes(dirtyClosed); + }); } } catch (err) { log.debug("init.orphan_scan.failed", { diff --git a/apps/memos-local-plugin/tests/unit/startup-recovery.test.ts b/apps/memos-local-plugin/tests/unit/startup-recovery.test.ts new file mode 100644 index 000000000..0e8944644 --- /dev/null +++ b/apps/memos-local-plugin/tests/unit/startup-recovery.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const source = readFileSync( + join(__dirname, "../../core/pipeline/memory-core.ts"), + "utf8", +); + +function initBody(): string { + const start = source.indexOf(" async function init(): Promise {"); + expect(start, "init() function should be present").toBeGreaterThanOrEqual(0); + const end = source.indexOf("\n function", start + 1); + expect(end, "init() should be followed by another function").toBeGreaterThan(start); + return source.slice(start, end); +} + +function stripScheduledRecoveryCallbacks(body: string): string { + return body.replace( + /scheduleStartupRecovery\([\s\S]*?\n \}\);/g, + "scheduleStartupRecovery();", + ); +} + +describe("memory-core startup recovery", () => { + it("does not block init on stale/dirty episode recovery", () => { + const synchronousInitBody = stripScheduledRecoveryCallbacks(initBody()); + + expect(synchronousInitBody).not.toContain("await recoverOpenEpisodesAsSessionEnd(stale)"); + expect(synchronousInitBody).not.toContain("await recoverDirtyClosedEpisodes(dirtyClosed)"); + expect(initBody()).toContain("scheduleStartupRecovery(\"startup.open_recovery\""); + expect(initBody()).toContain("scheduleStartupRecovery(\"startup.dirty_closed_recovery\""); + }); +});