mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
fix(memory-core): isolate dreaming narrative sessions per workspace (#61674)
* fix(memory-core): isolate dreaming narrative sessions per workspace * chore(changelog): add narrative isolation note --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Dreaming/diary: use the host local timezone for diary timestamps when `dreaming.timezone` is unset, so `DREAMS.md` and the UI stop defaulting to UTC. (#65034) Thanks @neo1027144-creator and @vincentkoc.
|
||||
- Dreaming/diary: include the timezone abbreviation in diary timestamps so `DREAMS.md` and the UI make UTC or local host time explicit. (#65057) Thanks @Yanhu007 and @vincentkoc.
|
||||
- Dreaming/narrative: harden transient narrative cleanup by retrying timed-out deletes and scrubbing stale dreaming session artifacts through the lock-aware session-store path. (#65320) Thanks @serkonyc and @vincentkoc.
|
||||
- Dreaming/narrative: isolate transient narrative session keys per workspace so simultaneous dreaming phases cannot cross-read or delete each other's session state. (#61674) Thanks @GaosCode and @vincentkoc.
|
||||
- Plugins/memory: restore cached memory capability public artifacts on plugin-registry cache hits so memory-backed artifact surfaces stay visible after warm loads. Thanks @sercada and @vincentkoc.
|
||||
- Gateway/cron: preserve requested isolated-agent config across runtime reloads so subagent jobs and heartbeat overrides keep the right workspace and heartbeat settings when the hot-loaded snapshot is stale. Thanks @l0cka and @vincentkoc.
|
||||
- Gateway/plugins: always send a non-empty `idempotencyKey` for plugin subagent runs, so dreaming narrative jobs stop failing gateway schema validation. (#65354) Thanks @CodeForgeNet and @vincentkoc.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as configRuntimeModule from "openclaw/plugin-sdk/config-runtime";
|
||||
@@ -583,7 +584,8 @@ describe("generateAndAppendDreamNarrative", () => {
|
||||
const subagent = createMockSubagent("The repository whispered of forgotten endpoints.");
|
||||
const logger = createMockLogger();
|
||||
const nowMs = Date.parse("2026-04-05T03:00:00Z");
|
||||
const expectedSessionKey = `dreaming-narrative-light-${nowMs}`;
|
||||
const workspaceHash = createHash("sha1").update(workspaceDir).digest("hex").slice(0, 12);
|
||||
const expectedSessionKey = `dreaming-narrative-light-${workspaceHash}-${nowMs}`;
|
||||
|
||||
await generateAndAppendDreamNarrative({
|
||||
subagent,
|
||||
@@ -848,4 +850,37 @@ describe("generateAndAppendDreamNarrative", () => {
|
||||
expect(sessionFiles).toContain("still-live.jsonl");
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining("dreaming cleanup scrubbed"));
|
||||
});
|
||||
|
||||
it("isolates narrative sessions across workspaces even at the same timestamp", async () => {
|
||||
const firstWorkspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
const secondWorkspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
|
||||
const subagent = createMockSubagent("A quiet memory took shape.");
|
||||
const logger = createMockLogger();
|
||||
const nowMs = Date.parse("2026-04-05T03:00:00Z");
|
||||
|
||||
await generateAndAppendDreamNarrative({
|
||||
subagent,
|
||||
workspaceDir: firstWorkspaceDir,
|
||||
data: { phase: "light", snippets: ["first workspace fragment"] },
|
||||
nowMs,
|
||||
logger,
|
||||
});
|
||||
await generateAndAppendDreamNarrative({
|
||||
subagent,
|
||||
workspaceDir: secondWorkspaceDir,
|
||||
data: { phase: "light", snippets: ["second workspace fragment"] },
|
||||
nowMs,
|
||||
logger,
|
||||
});
|
||||
|
||||
const firstSessionKey = subagent.run.mock.calls[0]?.[0]?.sessionKey;
|
||||
const secondSessionKey = subagent.run.mock.calls[1]?.[0]?.sessionKey;
|
||||
expect(firstSessionKey).toBeTypeOf("string");
|
||||
expect(secondSessionKey).toBeTypeOf("string");
|
||||
expect(firstSessionKey).not.toBe(secondSessionKey);
|
||||
expect(firstSessionKey).toContain("dreaming-narrative-light-");
|
||||
expect(secondSessionKey).toContain("dreaming-narrative-light-");
|
||||
expect(subagent.deleteSession.mock.calls[0]?.[0]?.sessionKey).toBe(firstSessionKey);
|
||||
expect(subagent.deleteSession.mock.calls[1]?.[0]?.sessionKey).toBe(secondSessionKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Dirent } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
@@ -176,6 +177,15 @@ async function startNarrativeRunOrFallback(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function buildNarrativeSessionKey(params: {
|
||||
workspaceDir: string;
|
||||
phase: NarrativePhaseData["phase"];
|
||||
nowMs: number;
|
||||
}): string {
|
||||
const workspaceHash = createHash("sha1").update(params.workspaceDir).digest("hex").slice(0, 12);
|
||||
return `dreaming-narrative-${params.phase}-${workspaceHash}-${params.nowMs}`;
|
||||
}
|
||||
|
||||
// ── Prompt building ────────────────────────────────────────────────────
|
||||
|
||||
export function buildNarrativePrompt(data: NarrativePhaseData): string {
|
||||
@@ -835,7 +845,11 @@ export async function generateAndAppendDreamNarrative(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionKey = `dreaming-narrative-${params.data.phase}-${nowMs}`;
|
||||
const sessionKey = buildNarrativeSessionKey({
|
||||
workspaceDir: params.workspaceDir,
|
||||
phase: params.data.phase,
|
||||
nowMs,
|
||||
});
|
||||
const message = buildNarrativePrompt(params.data);
|
||||
let runId: string | null = null;
|
||||
let waitStatus: string | null = null;
|
||||
|
||||
Reference in New Issue
Block a user