From 16eae4b4b4437f0a4a925202e86a175f8db760b5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 10:44:14 +0100 Subject: [PATCH] fix(memory-core): skip cleanup after narrative fallback --- CHANGELOG.md | 1 + .../src/dreaming-narrative.test.ts | 6 ++- .../memory-core/src/dreaming-narrative.ts | 7 +++- .../memory-core/src/dreaming-phases.test.ts | 11 +++-- extensions/memory-core/src/dreaming-phases.ts | 40 +------------------ 5 files changed, 19 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad88d277664..b21ef343bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Memory-core/dreaming: skip subagent session cleanup after request-scoped narrative fallback and remove duplicate phase-level cleanup, preventing false cleanup warnings when no subagent session was created. Fixes #67152. Thanks @jsompis. - Config/doctor: stop masking unknown-key validation diagnostics such as `agents.defaults.llm`, and have `openclaw doctor --fix` remove the retired `agents.defaults.llm` timeout block. Thanks @aidiffuser. - CLI/plugins: preserve unversioned ClawHub install specs so `plugins update` can follow newer ClawHub releases instead of pinning to the initially resolved version. Fixes #63010; supersedes #58426. Thanks @kangsen1234 and @robinspt. - Memory-core/subagents: tag plugin-created subagent sessions with their plugin owner so dreaming narrative cleanup can delete its own ephemeral sessions without granting broad admin session deletion. Fixes #72712. Thanks @BSG2000. diff --git a/extensions/memory-core/src/dreaming-narrative.test.ts b/extensions/memory-core/src/dreaming-narrative.test.ts index 165f4d6ce64..fff1bad4fd8 100644 --- a/extensions/memory-core/src/dreaming-narrative.test.ts +++ b/extensions/memory-core/src/dreaming-narrative.test.ts @@ -721,7 +721,10 @@ describe("generateAndAppendDreamNarrative", () => { expect(content).toContain("API endpoints need authentication"); expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("request-scoped")); expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining(workspaceDir)); - expect(subagent.deleteSession).toHaveBeenCalledOnce(); + expect(logger.warn).not.toHaveBeenCalledWith( + expect.stringContaining("narrative session cleanup failed"), + ); + expect(subagent.deleteSession).not.toHaveBeenCalled(); }); it("falls back when the request-scoped runtime error is detected by stable code", async () => { @@ -746,6 +749,7 @@ describe("generateAndAppendDreamNarrative", () => { const content = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8"); expect(content).toContain("A durable candidate surfaced."); + expect(subagent.deleteSession).not.toHaveBeenCalled(); }); it("does not fall back for non-Error objects that only spoof the stable code", async () => { diff --git a/extensions/memory-core/src/dreaming-narrative.ts b/extensions/memory-core/src/dreaming-narrative.ts index cdbafc8bd82..dbefb315a7f 100644 --- a/extensions/memory-core/src/dreaming-narrative.ts +++ b/extensions/memory-core/src/dreaming-narrative.ts @@ -861,6 +861,7 @@ export async function generateAndAppendDreamNarrative(params: { }); const message = buildNarrativePrompt(params.data); let runId: string | null = null; + let shouldDeleteSession = false; try { runId = await startNarrativeRunOrFallback({ subagent: params.subagent, @@ -875,6 +876,7 @@ export async function generateAndAppendDreamNarrative(params: { if (!runId) { return; } + shouldDeleteSession = true; const result = await params.subagent.waitForRun({ runId, @@ -917,8 +919,9 @@ export async function generateAndAppendDreamNarrative(params: { `memory-core: narrative generation failed for ${params.data.phase} phase: ${formatErrorMessage(err)}`, ); } finally { - // Guard against subagent becoming unavailable mid-flight (throws TypeError without this). - if (params.subagent) { + // Only cleanup after a run was accepted. Request-scoped fallback writes a + // local diary entry without creating a subagent session. + if (shouldDeleteSession && params.subagent) { try { await params.subagent.deleteSession({ sessionKey }); } catch (cleanupErr) { diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index eb3d98f0cb1..703e9a6285a 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -249,12 +249,11 @@ describe("memory-core dreaming phases", () => { nowMs, }); - expect(subagent.deleteSession).toHaveBeenCalledTimes(2); - expect(subagent.deleteSession).toHaveBeenNthCalledWith(1, { sessionKey: expectedSessionKey }); - expect(subagent.deleteSession).toHaveBeenNthCalledWith(2, { sessionKey: expectedSessionKey }); + expect(subagent.deleteSession).toHaveBeenCalledOnce(); + expect(subagent.deleteSession).toHaveBeenCalledWith({ sessionKey: expectedSessionKey }); }); - it("swallows synchronous request-scoped cleanup failures after narrative fallback", async () => { + it("skips session cleanup after request-scoped narrative fallback", async () => { const workspaceDir = await createDreamingWorkspace(); await writeDailyNote(workspaceDir, [ `# ${DREAMING_TEST_DAY}`, @@ -320,6 +319,10 @@ describe("memory-core dreaming phases", () => { const dreams = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8"); expect(dreams).toContain("Move backups to S3 Glacier."); expect(logger.error).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalledWith( + expect.stringContaining("narrative session cleanup failed"), + ); + expect(subagent.deleteSession).not.toHaveBeenCalled(); }); it("does not re-ingest managed light dreaming blocks from daily notes", async () => { diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index f1737dd2c40..3215e559fb7 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -19,11 +19,7 @@ import { resolveMemoryRemDreamingConfig, } from "openclaw/plugin-sdk/memory-core-host-status"; import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js"; -import { - buildNarrativeSessionKey, - generateAndAppendDreamNarrative, - type NarrativePhaseData, -} from "./dreaming-narrative.js"; +import { generateAndAppendDreamNarrative, type NarrativePhaseData } from "./dreaming-narrative.js"; import { asRecord, formatErrorMessage, normalizeTrimmedString } from "./dreaming-shared.js"; import { filterLiveShortTermRecallEntries, @@ -1696,17 +1692,6 @@ async function runRemDreaming(params: { } } -async function deleteNarrativeSessionBestEffort( - subagent: Parameters[0]["subagent"], - sessionKey: string, -): Promise { - try { - await subagent.deleteSession({ sessionKey }); - } catch { - // Cleanup is best-effort; request-scoped runtimes can throw synchronously. - } -} - export async function runDreamingSweepPhases(params: { workspaceDir: string; pluginConfig?: Record; @@ -1733,19 +1718,6 @@ export async function runDreamingSweepPhases(params: { nowMs: sweepNowMs, detachNarratives: params.detachNarratives, }); - // Defensive cleanup: ensure the light-phase narrative session is deleted even if - // generateAndAppendDreamNarrative's primary cleanup was skipped due to an error. - // Skip when narratives are detached: the queued subagent run hasn't read the - // session yet, so eager cleanup would race the writer and silently drop the - // diary entry. The narrative function does its own cleanup in finally{}. - if (params.subagent && !params.detachNarratives) { - const lightSessionKey = buildNarrativeSessionKey({ - workspaceDir: params.workspaceDir, - phase: "light", - nowMs: sweepNowMs, - }); - await deleteNarrativeSessionBestEffort(params.subagent, lightSessionKey); - } } const rem = resolveMemoryRemDreamingConfig({ @@ -1762,16 +1734,6 @@ export async function runDreamingSweepPhases(params: { nowMs: sweepNowMs, detachNarratives: params.detachNarratives, }); - // Defensive cleanup: ensure the REM-phase narrative session is deleted. - // Skip when narratives are detached (see light-phase comment above). - if (params.subagent && !params.detachNarratives) { - const remSessionKey = buildNarrativeSessionKey({ - workspaceDir: params.workspaceDir, - phase: "rem", - nowMs: sweepNowMs, - }); - await deleteNarrativeSessionBestEffort(params.subagent, remSessionKey); - } } }