mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(memory-core): skip cleanup after narrative fallback
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user