Skip to content

Commit db2488b

Browse files
authored
fix(memory): record structured dreaming outcomes (#97723)
Summary: - Merged fix(memory): record structured dreaming outcomes after ClawSweeper review. Automerge notes: - PR branch already contained follow-up commit before automerge: chore(plugin-sdk): update surface budgets Validation: - ClawSweeper review passed for head 9fa7d20. - Required merge gates passed before the squash merge. Prepared head SHA: 9fa7d20 Review: #97723 (comment) Co-authored-by: momothemage <[email protected]> Approved-by: momothemage
1 parent cfd8ba8 commit db2488b

10 files changed

Lines changed: 213 additions & 25 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
ad74d16da51b0d6da0b1fad75b4903c5554e02ac41c00ee5dae89fa63ceef802 plugin-sdk-api-baseline.json
2-
d1a9c33c40c039cda1f5d1a7d29ed20fb88d073b6a84899faca0a44fbc5a5092 plugin-sdk-api-baseline.jsonl
1+
2db6de6d4d7beb886de3077cd9d59cd364422e4175467b03c834fb41e6c9d695 plugin-sdk-api-baseline.json
2+
cb4b4efc0e33ade171cd8ebd2ff73f850bdd752232700ec983448c77a46a2002 plugin-sdk-api-baseline.jsonl
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Memory Core plugin module implements structured dreaming event helpers.
2+
import type { MemoryDreamingPhaseName } from "openclaw/plugin-sdk/memory-core-host-status";
3+
import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events";
4+
import { formatErrorMessage } from "./dreaming-shared.js";
5+
import { resolveMemoryCoreNowMs, resolveMemoryCoreTimestamp } from "./time.js";
6+
7+
type Logger = {
8+
warn: (message: string) => void;
9+
};
10+
11+
export async function appendFailedDreamingEvent(params: {
12+
workspaceDir: string;
13+
phase: MemoryDreamingPhaseName;
14+
error: string;
15+
storageMode: "inline" | "separate" | "both";
16+
nowMs?: number;
17+
logger: Logger;
18+
}): Promise<void> {
19+
try {
20+
await appendMemoryHostEvent(params.workspaceDir, {
21+
type: "memory.dream.completed",
22+
timestamp: resolveMemoryCoreTimestamp(resolveMemoryCoreNowMs(params.nowMs)),
23+
phase: params.phase,
24+
outcome: "failed",
25+
error: params.error,
26+
lineCount: 0,
27+
storageMode: params.storageMode,
28+
});
29+
} catch (err) {
30+
params.logger.warn(
31+
`memory-core: failed to write ${params.phase} dreaming outcome event for workspace ${params.workspaceDir}: ${formatErrorMessage(err)}`,
32+
);
33+
}
34+
}

extensions/memory-core/src/dreaming-markdown.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export async function writeDailyDreamingPhaseBlock(params: {
112112
type: "memory.dream.completed",
113113
timestamp: resolveMemoryCoreTimestamp(nowMs),
114114
phase: params.phase,
115+
outcome: "completed",
115116
...(inlinePath ? { inlinePath } : {}),
116117
...(reportPath ? { reportPath } : {}),
117118
lineCount: params.bodyLines.length,
@@ -147,6 +148,7 @@ export async function writeDeepDreamingReport(params: {
147148
type: "memory.dream.completed",
148149
timestamp: resolveMemoryCoreTimestamp(nowMs),
149150
phase: "deep",
151+
outcome: "completed",
150152
inlinePath,
151153
...(reportPath ? { reportPath } : {}),
152154
lineCount: params.bodyLines.length,

extensions/memory-core/src/dreaming-phases.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2020
import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime";
2121
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
22+
import { appendFailedDreamingEvent } from "./dreaming-events.js";
2223
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
2324
import {
2425
generateAndAppendDreamNarrative,
@@ -1902,31 +1903,55 @@ export async function runDreamingSweepPhases(params: {
19021903
cfg: params.cfg as Parameters<typeof resolveMemoryLightDreamingConfig>[0]["cfg"],
19031904
});
19041905
if (light.enabled && light.limit > 0) {
1905-
await runLightDreaming({
1906-
workspaceDir: params.workspaceDir,
1907-
cfg: params.cfg,
1908-
config: light,
1909-
logger: params.logger,
1910-
subagent: params.subagent,
1911-
nowMs: sweepNowMs,
1912-
detachNarratives: params.detachNarratives,
1913-
});
1906+
try {
1907+
await runLightDreaming({
1908+
workspaceDir: params.workspaceDir,
1909+
cfg: params.cfg,
1910+
config: light,
1911+
logger: params.logger,
1912+
subagent: params.subagent,
1913+
nowMs: sweepNowMs,
1914+
detachNarratives: params.detachNarratives,
1915+
});
1916+
} catch (err) {
1917+
await appendFailedDreamingEvent({
1918+
workspaceDir: params.workspaceDir,
1919+
phase: "light",
1920+
error: formatErrorMessage(err),
1921+
storageMode: light.storage.mode,
1922+
nowMs: sweepNowMs,
1923+
logger: params.logger,
1924+
});
1925+
throw err;
1926+
}
19141927
}
19151928

19161929
const rem = resolveMemoryRemDreamingConfig({
19171930
pluginConfig: params.pluginConfig,
19181931
cfg: params.cfg as Parameters<typeof resolveMemoryRemDreamingConfig>[0]["cfg"],
19191932
});
19201933
if (rem.enabled && rem.limit > 0) {
1921-
await runRemDreaming({
1922-
workspaceDir: params.workspaceDir,
1923-
cfg: params.cfg,
1924-
config: rem,
1925-
logger: params.logger,
1926-
subagent: params.subagent,
1927-
nowMs: sweepNowMs,
1928-
detachNarratives: params.detachNarratives,
1929-
});
1934+
try {
1935+
await runRemDreaming({
1936+
workspaceDir: params.workspaceDir,
1937+
cfg: params.cfg,
1938+
config: rem,
1939+
logger: params.logger,
1940+
subagent: params.subagent,
1941+
nowMs: sweepNowMs,
1942+
detachNarratives: params.detachNarratives,
1943+
});
1944+
} catch (err) {
1945+
await appendFailedDreamingEvent({
1946+
workspaceDir: params.workspaceDir,
1947+
phase: "rem",
1948+
error: formatErrorMessage(err),
1949+
storageMode: rem.storage.mode,
1950+
nowMs: sweepNowMs,
1951+
logger: params.logger,
1952+
});
1953+
throw err;
1954+
}
19301955
}
19311956
}
19321957

@@ -1977,6 +2002,13 @@ async function runPhaseIfTriggered(
19772002
});
19782003
}
19792004
} catch (err) {
2005+
await appendFailedDreamingEvent({
2006+
workspaceDir,
2007+
phase: params.phase,
2008+
error: formatErrorMessage(err),
2009+
storageMode: params.config.storage.mode,
2010+
logger: params.logger,
2011+
});
19802012
params.logger.error(
19812013
`memory-core: ${params.phase} dreaming failed for workspace ${workspaceDir}: ${formatErrorMessage(err)}`,
19822014
);

extensions/memory-core/src/dreaming.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import fs from "node:fs/promises";
33
import path from "node:path";
44
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
5+
import { readMemoryHostEvents } from "openclaw/plugin-sdk/memory-host-events";
56
import {
67
enqueueSystemEvent,
78
resetSystemEventsForTest,
@@ -2717,6 +2718,65 @@ describe("short-term dreaming trigger", () => {
27172718
expectLogContains(logger.info, "memory-core: dreaming applied details");
27182719
});
27192720

2721+
it("records a failed deep dreaming outcome when report writing fails", async () => {
2722+
const logger = createLogger();
2723+
const workspaceDir = await createTempWorkspace("memory-dreaming-failed-event-");
2724+
const targetPath = path.join(workspaceDir, "outside-dreams.md");
2725+
const dreamsPath = path.join(workspaceDir, "DREAMS.md");
2726+
await fs.writeFile(targetPath, "outside\n", "utf-8");
2727+
await fs.symlink(targetPath, dreamsPath);
2728+
await writeDailyMemoryNote(workspaceDir, "2026-04-02", ["Move backups to S3 Glacier."]);
2729+
2730+
await recordShortTermRecalls({
2731+
workspaceDir,
2732+
query: "backup policy",
2733+
results: [
2734+
{
2735+
path: "memory/2026-04-02.md",
2736+
startLine: 1,
2737+
endLine: 1,
2738+
score: 0.9,
2739+
snippet: "Move backups to S3 Glacier.",
2740+
source: "memory",
2741+
},
2742+
],
2743+
});
2744+
2745+
const result = await runShortTermDreamingPromotionIfTriggered({
2746+
cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT,
2747+
trigger: "heartbeat",
2748+
workspaceDir,
2749+
config: {
2750+
enabled: true,
2751+
cron: constants.DEFAULT_DREAMING_CRON_EXPR,
2752+
limit: 10,
2753+
minScore: 0,
2754+
minRecallCount: 0,
2755+
minUniqueQueries: 0,
2756+
recencyHalfLifeDays: constants.DEFAULT_DREAMING_RECENCY_HALF_LIFE_DAYS,
2757+
verboseLogging: false,
2758+
},
2759+
logger,
2760+
});
2761+
2762+
expect(result?.handled).toBe(true);
2763+
const failedEvent = (await readMemoryHostEvents({ workspaceDir })).find(
2764+
(event) => event.type === "memory.dream.completed" && event.outcome === "failed",
2765+
);
2766+
if (failedEvent?.type !== "memory.dream.completed") {
2767+
throw new Error("expected failed dreaming event");
2768+
}
2769+
expect(failedEvent).toMatchObject({
2770+
type: "memory.dream.completed",
2771+
phase: "deep",
2772+
outcome: "failed",
2773+
lineCount: 0,
2774+
storageMode: "separate",
2775+
});
2776+
expect(failedEvent.error).toContain("Refusing to write symlinked DREAMS.md");
2777+
expectLogContains(logger.error, "memory-core: dreaming promotion failed");
2778+
});
2779+
27202780
it("fans out one dreaming run across configured agent workspaces", async () => {
27212781
const logger = createLogger();
27222782
const workspaceRoot = await createTempWorkspace("memory-dreaming-multi-");

extensions/memory-core/src/dreaming.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
uniqueStrings,
2828
} from "openclaw/plugin-sdk/string-coerce-runtime";
2929
import { peekSystemEventEntries } from "openclaw/plugin-sdk/system-event-runtime";
30+
import { appendFailedDreamingEvent } from "./dreaming-events.js";
3031
import type { NarrativePhaseData } from "./dreaming-narrative.js";
3132
import {
3233
formatErrorMessage,
@@ -570,8 +571,8 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
570571
import("./short-term-promotion.js"),
571572
]);
572573
for (const workspaceDir of workspaces) {
574+
const sweepNowMs = Date.now();
573575
try {
574-
const sweepNowMs = Date.now();
575576
await runDreamingSweepPhases({
576577
workspaceDir,
577578
pluginConfig,
@@ -581,7 +582,15 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
581582
detachNarratives,
582583
nowMs: sweepNowMs,
583584
});
585+
} catch (err) {
586+
failedWorkspaces += 1;
587+
params.logger.error(
588+
`memory-core: dreaming sweep failed for workspace ${workspaceDir}: ${formatErrorMessage(err)}`,
589+
);
590+
continue;
591+
}
584592

593+
try {
585594
const reportLines: string[] = [];
586595
const repair = await repairShortTermPromotionArtifacts({ workspaceDir });
587596
if (repair.changed) {
@@ -691,9 +700,18 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
691700
}
692701
} catch (err) {
693702
failedWorkspaces += 1;
703+
const error = formatErrorMessage(err);
694704
params.logger.error(
695-
`memory-core: dreaming promotion failed for workspace ${workspaceDir}: ${formatErrorMessage(err)}`,
705+
`memory-core: dreaming promotion failed for workspace ${workspaceDir}: ${error}`,
696706
);
707+
await appendFailedDreamingEvent({
708+
workspaceDir,
709+
phase: "deep",
710+
error,
711+
storageMode: params.config.storage?.mode ?? "separate",
712+
nowMs: sweepNowMs,
713+
logger: params.logger,
714+
});
697715
}
698716
}
699717
params.logger.info(

extensions/memory-core/src/memory-events.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "node:path";
44
import {
55
readMemoryHostEventRecords,
66
readMemoryHostEvents,
7+
resolveMemoryHostEventLogPath,
78
} from "openclaw/plugin-sdk/memory-host-events";
89
import { describe, expect, it } from "vitest";
910
import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js";
@@ -185,7 +186,36 @@ describe("memory host event journal integration", () => {
185186
throw new Error("expected dream completion event");
186187
}
187188
expect(dreamEvent.phase).toBe("light");
189+
expect(dreamEvent.outcome).toBe("completed");
188190
expect(dreamEvent.lineCount).toBe(2);
189191
expect(dreamEvent.storageMode).toBe("both");
190192
});
193+
194+
it("keeps legacy dreaming completion events without outcome readable", async () => {
195+
const workspaceDir = await createTempWorkspace("memory-core-legacy-dream-events-");
196+
const eventLogPath = resolveMemoryHostEventLogPath(workspaceDir);
197+
await fs.mkdir(path.dirname(eventLogPath), { recursive: true });
198+
await fs.writeFile(
199+
eventLogPath,
200+
`${JSON.stringify({
201+
type: "memory.dream.completed",
202+
timestamp: "2026-04-05T13:00:00.000Z",
203+
phase: "deep",
204+
inlinePath: path.join(workspaceDir, "DREAMS.md"),
205+
lineCount: 2,
206+
storageMode: "inline",
207+
})}\n`,
208+
"utf8",
209+
);
210+
211+
const events = await readMemoryHostEvents({ workspaceDir });
212+
213+
expect(events).toHaveLength(1);
214+
const dreamEvent = events[0];
215+
if (dreamEvent?.type !== "memory.dream.completed") {
216+
throw new Error("expected dream completion event");
217+
}
218+
expect(dreamEvent.outcome).toBeUndefined();
219+
expect(dreamEvent.phase).toBe("deep");
220+
});
191221
});

scripts/plugin-sdk-surface-report.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
174174
"memory-core-engine-runtime": 15,
175175
"memory-core-host-multimodal": 3,
176176
"memory-core-host-query": 2,
177-
"memory-core-host-events": 11,
177+
"memory-core-host-events": 12,
178178
"memory-core-host-status": 1,
179179
"memory-core-host-runtime-core": 1,
180180
"memory-host-core": 1,
@@ -202,11 +202,11 @@ let publicDeprecatedExportsByEntrypointBudget;
202202
try {
203203
budgets = {
204204
publicEntrypoints: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", 322),
205-
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10398),
205+
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10400),
206206
publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5219),
207207
publicDeprecatedExports: readBudgetEnv(
208208
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
209-
3255,
209+
3256,
210210
),
211211
publicWildcardReexports: readBudgetEnv(
212212
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_WILDCARD_REEXPORTS",

src/memory-host-sdk/events.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,18 @@ export type MemoryHostPromotionAppliedEvent = {
5454
}>;
5555
};
5656

57+
/** Normalized outcome for a dreaming phase run. */
58+
export type MemoryDreamOutcome = "completed" | "failed";
59+
5760
/** Event emitted after a dreaming phase writes inline memory and/or reports. */
5861
export type MemoryHostDreamCompletedEvent = {
5962
type: "memory.dream.completed";
6063
timestamp: string;
6164
phase: MemoryDreamingPhaseName;
65+
/** Missing on older event logs; readers should treat absent as "completed". */
66+
outcome?: MemoryDreamOutcome;
67+
/** Error detail when outcome is "failed". */
68+
error?: string;
6269
inlinePath?: string;
6370
reportPath?: string;
6471
lineCount: number;

src/plugin-sdk/memory-host-events.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe("memory host event journal helpers", () => {
5757
type: "memory.dream.completed",
5858
timestamp: "2026-04-05T13:00:00.000Z",
5959
phase: "light",
60+
outcome: "completed",
6061
lineCount: 4,
6162
storageMode: "both",
6263
inlinePath: path.join(workspaceDir, "memory", "2026-04-05.md"),
@@ -74,6 +75,10 @@ describe("memory host event journal helpers", () => {
7475
expect(events).toHaveLength(2);
7576
expect(events[0]?.type).toBe("memory.recall.recorded");
7677
expect(events[1]?.type).toBe("memory.dream.completed");
78+
if (events[1]?.type !== "memory.dream.completed") {
79+
throw new Error("expected dream completion event");
80+
}
81+
expect(events[1].outcome).toBe("completed");
7782
expect(tail).toHaveLength(1);
7883
expect(tail[0]?.type).toBe("memory.dream.completed");
7984
});

0 commit comments

Comments
 (0)