From 4cc69c38cfe4bd13f9ac42af0b97548f18e482f0 Mon Sep 17 00:00:00 2001 From: chiyouYCH <563318445@qq.com> Date: Wed, 15 Apr 2026 14:28:37 +0800 Subject: [PATCH] fix(memory-core): prevent dreaming-narrative session leaks (GitHub #66358) Root cause: `runDreamingSweepPhases` passed `params.nowMs` through to each phase without normalizing it first. Each phase recomputed `Date.now()` at call time, producing a slightly different timestamp than the session key used by `generateAndAppendDreamNarrative`. The resulting session key mismatch meant the `deleteSession` call cleaned up a different (or non-existent) key than the one that was created. Fix: - Normalize `nowMs` once at the top of `runDreamingSweepPhases` and pass the consistent value to all phases. - Add a defensive `deleteSession` call in `runDreamingSweepPhases` for each phase after `runLightDreaming`/`runRemDreaming` completes. This acts as a safety net even if the narrative function's primary cleanup is skipped. - Guard the `finally` block in `generateAndAppendDreamNarrative` with `if (params.subagent)` to prevent TypeError if the subagent runtime becomes unavailable mid-flight. This fixes the observed session list pollution where `dreaming-narrative-*-` entries accumulated indefinitely. --- .../memory-core/src/dreaming-narrative.ts | 17 +++++++----- extensions/memory-core/src/dreaming-phases.ts | 26 +++++++++++++++++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/extensions/memory-core/src/dreaming-narrative.ts b/extensions/memory-core/src/dreaming-narrative.ts index 2c0381b27fb..58b8ab74b6a 100644 --- a/extensions/memory-core/src/dreaming-narrative.ts +++ b/extensions/memory-core/src/dreaming-narrative.ts @@ -911,7 +911,7 @@ export async function generateAndAppendDreamNarrative(params: { `memory-core: narrative generation failed for ${params.data.phase} phase: ${formatErrorMessage(err)}`, ); } finally { - if (runId && waitStatus === "timeout") { + if (params.subagent && runId && waitStatus === "timeout") { try { const settle = await params.subagent.waitForRun({ runId, @@ -929,12 +929,15 @@ export async function generateAndAppendDreamNarrative(params: { } } - try { - await params.subagent.deleteSession({ sessionKey }); - } catch (cleanupErr) { - params.logger.warn( - `memory-core: narrative session cleanup failed for ${params.data.phase} phase: ${formatErrorMessage(cleanupErr)}`, - ); + // Guard against subagent becoming unavailable mid-flight (throws TypeError without this). + if (params.subagent) { + try { + await params.subagent.deleteSession({ sessionKey }); + } catch (cleanupErr) { + params.logger.warn( + `memory-core: narrative session cleanup failed for ${params.data.phase} phase: ${formatErrorMessage(cleanupErr)}`, + ); + } } await scrubDreamingNarrativeArtifacts(params.logger).catch((scrubErr: unknown) => { diff --git a/extensions/memory-core/src/dreaming-phases.ts b/extensions/memory-core/src/dreaming-phases.ts index 993129c7a70..99abdb14047 100644 --- a/extensions/memory-core/src/dreaming-phases.ts +++ b/extensions/memory-core/src/dreaming-phases.ts @@ -1641,6 +1641,9 @@ export async function runDreamingSweepPhases(params: { subagent?: Parameters[0]["subagent"]; nowMs?: number; }): Promise { + // Normalize nowMs once so all phase timestamps and narrative session keys are consistent. + const sweepNowMs = Number.isFinite(params.nowMs) ? params.nowMs : Date.now(); + const light = resolveMemoryLightDreamingConfig({ pluginConfig: params.pluginConfig, cfg: params.cfg, @@ -1652,8 +1655,18 @@ export async function runDreamingSweepPhases(params: { config: light, logger: params.logger, subagent: params.subagent, - nowMs: params.nowMs, + nowMs: sweepNowMs, }); + // Defensive cleanup: ensure the light-phase narrative session is deleted even if + // generateAndAppendDreamNarrative's primary cleanup was skipped due to an error. + if (params.subagent) { + const lightSessionKey = `dreaming-narrative-light-${sweepNowMs}`; + await params.subagent + .deleteSession({ sessionKey: lightSessionKey }) + .catch(() => { + // Swallow errors — this is best-effort cleanup. + }); + } } const rem = resolveMemoryRemDreamingConfig({ @@ -1667,8 +1680,17 @@ export async function runDreamingSweepPhases(params: { config: rem, logger: params.logger, subagent: params.subagent, - nowMs: params.nowMs, + nowMs: sweepNowMs, }); + // Defensive cleanup: ensure the REM-phase narrative session is deleted. + if (params.subagent) { + const remSessionKey = `dreaming-narrative-rem-${sweepNowMs}`; + await params.subagent + .deleteSession({ sessionKey: remSessionKey }) + .catch(() => { + // Swallow errors — this is best-effort cleanup. + }); + } } }