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-*-<timestamp>` entries accumulated indefinitely.
This commit is contained in:
chiyouYCH
2026-04-15 14:28:37 +08:00
committed by Josh Lehman
parent a06f4d0808
commit 4cc69c38cf
2 changed files with 34 additions and 9 deletions

View File

@@ -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) => {

View File

@@ -1641,6 +1641,9 @@ export async function runDreamingSweepPhases(params: {
subagent?: Parameters<typeof generateAndAppendDreamNarrative>[0]["subagent"];
nowMs?: number;
}): Promise<void> {
// 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.
});
}
}
}