

























@@ -29,6 +29,9 @@ import { createMemoryCoreTestHarness } from "./test-helpers.js";
29293030const { createTempWorkspace } = createMemoryCoreTestHarness();
3131const DREAMS_FILE_LOCKS_KEY = Symbol.for("openclaw.memoryCore.dreamingNarrative.fileLocks");
32+const NARRATIVE_SESSION_LOCKS_KEY = Symbol.for(
33+"openclaw.memoryCore.dreamingNarrative.sessionLocks",
34+);
3235const EXPECTS_POSIX_PRIVATE_FILE_MODE = process.platform !== "win32";
33363437type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
@@ -80,6 +83,7 @@ async function expectPathMissing(targetPath: string): Promise<void> {
8083afterEach(() => {
8184vi.restoreAllMocks();
8285resolveGlobalMap<string, unknown>(DREAMS_FILE_LOCKS_KEY).clear();
86+resolveGlobalMap<string, unknown>(NARRATIVE_SESSION_LOCKS_KEY).clear();
8387});
84888589describe("buildNarrativePrompt", () => {
@@ -647,7 +651,7 @@ describe("generateAndAppendDreamNarrative", () => {
647651const logger = createMockLogger();
648652const nowMs = Date.parse("2026-04-05T03:00:00Z");
649653const workspaceHash = createHash("sha1").update(workspaceDir).digest("hex").slice(0, 12);
650-const expectedSessionKey = `dreaming-narrative-light-${workspaceHash}-${nowMs}`;
654+const expectedSessionKey = `dreaming-narrative-light-${workspaceHash}`;
651655652656await generateAndAppendDreamNarrative({
653657 subagent,
@@ -664,14 +668,14 @@ describe("generateAndAppendDreamNarrative", () => {
664668665669expect(subagent.run).toHaveBeenCalledOnce();
666670const runOptions = mockObjectArg(subagent.run, "subagent run");
667-expect(runOptions.idempotencyKey).toBe(expectedSessionKey);
671+expect(runOptions.idempotencyKey).toBe(`${expectedSessionKey}-${nowMs}`);
668672expect(runOptions.sessionKey).toBe(expectedSessionKey);
669673expect(runOptions.lane).toBe(`dreaming-narrative:${expectedSessionKey}`);
670674expect(runOptions.lightContext).toBe(true);
671675expect(runOptions.deliver).toBe(false);
672676expect(runOptions.model).toBe("anthropic/claude-sonnet-4-6");
673677expect(subagent.waitForRun).toHaveBeenCalledOnce();
674-expect(subagent.deleteSession).toHaveBeenCalledOnce();
678+expect(subagent.deleteSession).toHaveBeenCalledTimes(2);
675679676680const content = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8");
677681expect(content).toContain("The repository whispered of forgotten endpoints.");
@@ -685,7 +689,7 @@ describe("generateAndAppendDreamNarrative", () => {
685689const logger = createMockLogger();
686690const nowMs = Date.parse("2026-04-05T03:00:00Z");
687691const workspaceHash = createHash("sha1").update(workspaceDir).digest("hex").slice(0, 12);
688-const expectedSessionKey = `dreaming-narrative-light-${workspaceHash}-${nowMs}`;
692+const expectedSessionKey = `dreaming-narrative-light-${workspaceHash}`;
689693const retrySessionKey = `${expectedSessionKey}-retry-1`;
690694691695await generateAndAppendDreamNarrative({
@@ -712,8 +716,16 @@ describe("generateAndAppendDreamNarrative", () => {
712716sessionKey: retrySessionKey,
713717limit: 5,
714718});
715-expect(subagent.deleteSession).toHaveBeenCalledOnce();
716-expect(subagent.deleteSession).toHaveBeenCalledWith({ sessionKey: retrySessionKey });
719+expect(subagent.deleteSession).toHaveBeenCalledTimes(3);
720+expect(mockObjectArg(subagent.deleteSession, "delete session")).toEqual({
721+sessionKey: expectedSessionKey,
722+});
723+expect(mockObjectArg(subagent.deleteSession, "delete session", 1)).toEqual({
724+sessionKey: retrySessionKey,
725+});
726+expect(mockObjectArg(subagent.deleteSession, "delete session", 2)).toEqual({
727+sessionKey: retrySessionKey,
728+});
717729expectLogIncludes(logger.warn, "session default");
718730});
719731@@ -729,7 +741,7 @@ describe("generateAndAppendDreamNarrative", () => {
729741const logger = createMockLogger();
730742const nowMs = Date.parse("2026-04-05T03:00:00Z");
731743const workspaceHash = createHash("sha1").update(workspaceDir).digest("hex").slice(0, 12);
732-const expectedSessionKey = `dreaming-narrative-rem-${workspaceHash}-${nowMs}`;
744+const expectedSessionKey = `dreaming-narrative-rem-${workspaceHash}`;
733745const retrySessionKey = `${expectedSessionKey}-retry-1`;
734746735747await generateAndAppendDreamNarrative({
@@ -750,13 +762,19 @@ describe("generateAndAppendDreamNarrative", () => {
750762sessionKey: retrySessionKey,
751763limit: 5,
752764});
753-expect(subagent.deleteSession).toHaveBeenCalledTimes(2);
765+expect(subagent.deleteSession).toHaveBeenCalledTimes(4);
754766expect(mockObjectArg(subagent.deleteSession, "delete session")).toEqual({
755767sessionKey: expectedSessionKey,
756768});
757769expect(mockObjectArg(subagent.deleteSession, "delete session", 1)).toEqual({
758770sessionKey: retrySessionKey,
759771});
772+expect(mockObjectArg(subagent.deleteSession, "delete session", 2)).toEqual({
773+sessionKey: expectedSessionKey,
774+});
775+expect(mockObjectArg(subagent.deleteSession, "delete session", 3)).toEqual({
776+sessionKey: retrySessionKey,
777+});
760778expectLogIncludes(logger.warn, "unknown model");
761779});
762780@@ -781,7 +799,7 @@ describe("generateAndAppendDreamNarrative", () => {
781799782800expect(subagent.run).toHaveBeenCalledOnce();
783801expect(subagent.waitForRun).not.toHaveBeenCalled();
784-expect(subagent.deleteSession).not.toHaveBeenCalled();
802+expect(subagent.deleteSession).toHaveBeenCalledOnce();
785803expectLogIncludes(logger.warn, "narrative generation failed");
786804});
787805@@ -871,6 +889,7 @@ describe("generateAndAppendDreamNarrative", () => {
871889it("falls back to a local narrative when subagent runtime is request-scoped", async () => {
872890const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
873891const subagent = createMockSubagent("");
892+subagent.deleteSession.mockRejectedValueOnce(new RequestScopedSubagentRuntimeError());
874893subagent.run.mockRejectedValue(new RequestScopedSubagentRuntimeError());
875894const logger = createMockLogger();
876895@@ -888,8 +907,9 @@ describe("generateAndAppendDreamNarrative", () => {
888907expectLogIncludes(logger.info, "request-scoped");
889908expectLogExcludes(logger.warn, "request-scoped");
890909expectLogExcludes(logger.warn, workspaceDir);
910+expectLogExcludes(logger.warn, "narrative pre-cleanup");
891911expectLogExcludes(logger.warn, "narrative session cleanup failed");
892-expect(subagent.deleteSession).not.toHaveBeenCalled();
912+expect(subagent.deleteSession).toHaveBeenCalledOnce();
893913});
894914895915it("falls back when the request-scoped runtime error is detected by stable code", async () => {
@@ -916,7 +936,7 @@ describe("generateAndAppendDreamNarrative", () => {
916936expect(content).toContain("A durable candidate surfaced.");
917937expectLogIncludes(logger.info, "request-scoped");
918938expectLogExcludes(logger.warn, "request-scoped");
919-expect(subagent.deleteSession).not.toHaveBeenCalled();
939+expect(subagent.deleteSession).toHaveBeenCalledOnce();
920940});
921941922942it("does not fall back for non-Error objects that only spoof the stable code", async () => {
@@ -1049,12 +1069,11 @@ describe("generateAndAppendDreamNarrative", () => {
10491069expect(firstSessionKey).not.toBe(secondSessionKey);
10501070expect(firstSessionKey).toContain("dreaming-narrative-light-");
10511071expect(secondSessionKey).toContain("dreaming-narrative-light-");
1052-expect(mockObjectArg(subagent.deleteSession, "delete session").sessionKey).toBe(
1053-firstSessionKey,
1054-);
1055-expect(mockObjectArg(subagent.deleteSession, "delete session", 1).sessionKey).toBe(
1056-secondSessionKey,
1072+const deleteKeys = subagent.deleteSession.mock.calls.map(
1073+(call: unknown[]) => (call[0] as { sessionKey: string })?.sessionKey,
10571074);
1075+expect(deleteKeys.filter((key: string) => key === firstSessionKey)).toHaveLength(2);
1076+expect(deleteKeys.filter((key: string) => key === secondSessionKey)).toHaveLength(2);
10581077});
10591078});
10601079@@ -1100,10 +1119,12 @@ describe("runDetachedDreamNarrative", () => {
1100111911011120it("caps the number of in-flight detached narratives at 3", async () => {
11021121const { subagent, runDeferreds } = createBlockingSubagent();
1103-const workspaceDir = await createTempWorkspace("openclaw-dreaming-detach-");
1122+const workspaceDirs = await Promise.all(
1123+Array.from({ length: 5 }, () => createTempWorkspace("openclaw-dreaming-detach-")),
1124+);
11041125const logger = createMockLogger();
110511261106-for (let i = 0; i < 5; i += 1) {
1127+for (const [i, workspaceDir] of workspaceDirs.entries()) {
11071128runDetachedDreamNarrative({
11081129 subagent,
11091130 workspaceDir,
@@ -1137,7 +1158,65 @@ describe("runDetachedDreamNarrative", () => {
11371158d.resolve({ runId: "drain" });
11381159}
11391160await vi.waitFor(() => {
1140-expect(subagent.deleteSession).toHaveBeenCalledTimes(5);
1161+expect(subagent.deleteSession).toHaveBeenCalledTimes(10);
1162+});
1163+expect(subagent.run).toHaveBeenCalledTimes(5);
1164+expect(subagent.waitForRun).toHaveBeenCalledTimes(5);
1165+});
1166+1167+it("serializes detached narratives that reuse a workspace and phase session", async () => {
1168+let nextRunId = 0;
1169+const waitDeferreds: Array<Deferred<{ status: string }>> = [];
1170+const subagent = {
1171+run: vi.fn(() => {
1172+nextRunId += 1;
1173+return Promise.resolve({ runId: `run-${nextRunId}` });
1174+}),
1175+waitForRun: vi.fn(() => {
1176+const d = deferred<{ status: string }>();
1177+waitDeferreds.push(d);
1178+return d.promise;
1179+}),
1180+getSessionMessages: vi.fn().mockResolvedValue({ messages: [] }),
1181+deleteSession: vi.fn().mockResolvedValue(undefined),
1182+};
1183+const workspaceDir = await createTempWorkspace("openclaw-dreaming-detach-");
1184+const logger = createMockLogger();
1185+1186+for (let i = 0; i < 5; i += 1) {
1187+runDetachedDreamNarrative({
1188+ subagent,
1189+ workspaceDir,
1190+data: { phase: "light", snippets: [`fragment-${i}`] },
1191+nowMs: Date.parse("2026-04-28T03:00:00Z"),
1192+ logger,
1193+});
1194+}
1195+1196+await vi.waitFor(() => {
1197+expect(waitDeferreds.length).toBe(1);
1198+});
1199+1200+expect(subagent.run).toHaveBeenCalledTimes(1);
1201+expect(subagent.waitForRun).toHaveBeenCalledTimes(1);
1202+// The first run is still active, so later same-key jobs must not pre-delete its session.
1203+expect(subagent.deleteSession).toHaveBeenCalledTimes(1);
1204+1205+for (let i = 0; i < 5; i += 1) {
1206+const currentDeferred = waitDeferreds[i];
1207+if (!currentDeferred) {
1208+throw new Error(`Expected wait deferred ${i} to exist`);
1209+}
1210+currentDeferred.resolve({ status: "timeout" });
1211+if (i < 4) {
1212+await vi.waitFor(() => {
1213+expect(waitDeferreds.length).toBeGreaterThan(i + 1);
1214+});
1215+}
1216+}
1217+1218+await vi.waitFor(() => {
1219+expect(subagent.deleteSession).toHaveBeenCalledTimes(10);
11411220});
11421221expect(subagent.run).toHaveBeenCalledTimes(5);
11431222expect(subagent.waitForRun).toHaveBeenCalledTimes(5);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。