fix(memory-core): skip cleanup after narrative fallback

This commit is contained in:
Peter Steinberger
2026-04-27 10:44:14 +01:00
parent 14a27e11f7
commit 16eae4b4b4
5 changed files with 19 additions and 46 deletions

View File

@@ -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.

View File

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

View File

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

View File

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

View File

@@ -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<typeof generateAndAppendDreamNarrative>[0]["subagent"],
sessionKey: string,
): Promise<void> {
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<string, unknown>;
@@ -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);
}
}
}