diff --git a/extensions/memory-core/src/dreaming-narrative.test.ts b/extensions/memory-core/src/dreaming-narrative.test.ts index e28d1b7bffb..29e0438e804 100644 --- a/extensions/memory-core/src/dreaming-narrative.test.ts +++ b/extensions/memory-core/src/dreaming-narrative.test.ts @@ -1020,20 +1020,21 @@ describe("generateAndAppendDreamNarrative", () => { const storePath = path.join(sessionsDir, "sessions.json"); const orphanPath = path.join(sessionsDir, "orphan.jsonl"); const livePath = path.join(sessionsDir, "still-live.jsonl"); + const updatedAt = Date.now(); await sessionStoreRuntimeModule.saveSessionStore( storePath, { "agent:main:dreaming-narrative-light-1": { sessionId: "missing", - updatedAt: Date.now(), + updatedAt, }, "agent:main:kept-session": { sessionId: "still-live", - updatedAt: Date.now(), + updatedAt, }, "agent:main:telegram:group:dreaming-narrative-room": { sessionId: "still-missing-non-dreaming", - updatedAt: Date.now(), + updatedAt, }, }, { skipMaintenance: true }, @@ -1090,20 +1091,21 @@ describe("generateAndAppendDreamNarrative", () => { // A second dreaming row whose transcript is fresh (a live/just-started run) // must be preserved. const liveTranscript = path.join(sessionsDir, "live-dreaming.jsonl"); + const updatedAt = Date.now(); await sessionStoreRuntimeModule.saveSessionStore( storePath, { "agent:main:dreaming-narrative-deep-orphan": { sessionId: "orphan-dreaming", - updatedAt: Date.now(), + updatedAt, }, "agent:main:dreaming-narrative-deep-live": { sessionId: "live-dreaming", - updatedAt: Date.now(), + updatedAt, }, "agent:main:kept-session": { sessionId: "still-live", - updatedAt: Date.now(), + updatedAt, }, }, { skipMaintenance: true }, diff --git a/extensions/memory-core/src/dreaming-narrative.ts b/extensions/memory-core/src/dreaming-narrative.ts index 47f024a564a..11424880b6c 100644 --- a/extensions/memory-core/src/dreaming-narrative.ts +++ b/extensions/memory-core/src/dreaming-narrative.ts @@ -152,7 +152,7 @@ function buildRequestScopedFallbackNarrative(_data: NarrativePhaseData): string return "A memory trace surfaced, but details were unavailable in this run."; } -async function appendFallbackNarrativeEntry(params: { +export async function appendFallbackNarrativeEntry(params: { workspaceDir: string; data: NarrativePhaseData; nowMs: number; diff --git a/extensions/memory-core/src/dreaming.test.ts b/extensions/memory-core/src/dreaming.test.ts index 023cf2b07db..5af930fbd3a 100644 --- a/extensions/memory-core/src/dreaming.test.ts +++ b/extensions/memory-core/src/dreaming.test.ts @@ -2341,6 +2341,57 @@ describe("short-term dreaming trigger", () => { expect(memoryText).toContain("Move backups to S3 Glacier."); }); + it("writes fallback dream diary prose when managed cron has no subagent runtime", async () => { + const logger = createLogger(); + const workspaceDir = await createTempWorkspace("memory-dreaming-cron-no-subagent-"); + await writeDailyMemoryNote(workspaceDir, "2026-04-02", ["Move backups to S3 Glacier."]); + + await recordShortTermRecalls({ + workspaceDir, + query: "backup policy", + results: [ + { + path: "memory/2026-04-02.md", + startLine: 1, + endLine: 1, + score: 0.9, + snippet: "Move backups to S3 Glacier.", + source: "memory", + }, + ], + }); + + const result = await runShortTermDreamingPromotionIfTriggered({ + cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT, + trigger: "cron", + workspaceDir, + config: { + enabled: true, + cron: constants.DEFAULT_DREAMING_CRON_EXPR, + limit: 10, + minScore: 0, + minRecallCount: 0, + minUniqueQueries: 0, + recencyHalfLifeDays: constants.DEFAULT_DREAMING_RECENCY_HALF_LIFE_DAYS, + verboseLogging: false, + }, + logger, + }); + + expect(result?.handled).toBe(true); + const memoryText = await fs.readFile(path.join(workspaceDir, "MEMORY.md"), "utf-8"); + expect(memoryText).toContain("Move backups to S3 Glacier."); + const dreamsText = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8"); + expect(dreamsText).toContain(""); + expect(dreamsText).toContain( + "A memory trace surfaced, but details were unavailable in this run.", + ); + expect(dreamsText).not.toContain("Move backups to S3 Glacier."); + expect(logger.info).toHaveBeenCalledWith( + "memory-core: narrative generation used fallback for deep phase because subagent runtime is unavailable.", + ); + }); + it("keeps one-off recalls out of long-term memory under default thresholds", async () => { const logger = createLogger(); const workspaceDir = await createTempWorkspace("memory-dreaming-strict-"); diff --git a/extensions/memory-core/src/dreaming.ts b/extensions/memory-core/src/dreaming.ts index 8d32c7a3735..322183cc742 100644 --- a/extensions/memory-core/src/dreaming.ts +++ b/extensions/memory-core/src/dreaming.ts @@ -556,7 +556,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: { const detachNarratives = params.trigger === "cron"; const [ { writeDeepDreamingReport }, - { generateAndAppendDreamNarrative, runDetachedDreamNarrative }, + { appendFallbackNarrativeEntry, generateAndAppendDreamNarrative, runDetachedDreamNarrative }, { runDreamingSweepPhases }, { applyShortTermPromotions, @@ -652,13 +652,22 @@ export async function runShortTermDreamingPromotionIfTriggered(params: { storage: params.config.storage ?? { mode: "separate", separateReports: false }, }); // Generate dream diary narrative from promoted memories. - if (params.subagent && (candidates.length > 0 || applied.applied > 0)) { + if (candidates.length > 0 || applied.applied > 0) { const data: NarrativePhaseData = { phase: "deep", snippets: candidates.map((c) => c.snippet).filter(Boolean), promotions: applied.appliedCandidates.map((c) => c.snippet).filter(Boolean), }; - if (detachNarratives) { + if (!params.subagent) { + await appendFallbackNarrativeEntry({ + workspaceDir, + data, + nowMs: sweepNowMs, + timezone: params.config.timezone, + logger: params.logger, + reason: "subagent runtime is unavailable", + }); + } else if (detachNarratives) { runDetachedDreamNarrative({ subagent: params.subagent, workspaceDir,