mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:20:44 +00:00
fix: include primary dreaming workspace
This commit is contained in:
@@ -691,6 +691,97 @@ describe("memory-core dreaming phases", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps primary session transcripts out of configured subagent workspaces", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
const subagentWorkspaceDir = await createDreamingWorkspace();
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", path.join(workspaceDir, ".state"));
|
||||
|
||||
const mainSessionsDir = resolveSessionTranscriptsDirForAgent("main");
|
||||
const subagentSessionsDir = resolveSessionTranscriptsDirForAgent("agi-ceo");
|
||||
await fs.mkdir(mainSessionsDir, { recursive: true });
|
||||
await fs.mkdir(subagentSessionsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(mainSessionsDir, "main-session.jsonl"),
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "user",
|
||||
timestamp: "2026-04-05T18:01:00.000Z",
|
||||
content: [{ type: "text", text: "Main workspace should stay in main dreams." }],
|
||||
},
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(subagentSessionsDir, "subagent-session.jsonl"),
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
message: {
|
||||
role: "user",
|
||||
timestamp: "2026-04-05T18:02:00.000Z",
|
||||
content: [{ type: "text", text: "CEO workspace should stay in CEO dreams." }],
|
||||
},
|
||||
}),
|
||||
].join("\n") + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { beforeAgentReply } = createHarness(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
list: [{ id: "agi-ceo", workspace: subagentWorkspaceDir }],
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": {
|
||||
config: {
|
||||
dreaming: {
|
||||
enabled: true,
|
||||
phases: {
|
||||
light: {
|
||||
enabled: true,
|
||||
limit: 20,
|
||||
lookbackDays: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspaceDir,
|
||||
);
|
||||
|
||||
try {
|
||||
await withDreamingTestClock(async () => {
|
||||
await triggerLightDreaming(beforeAgentReply, workspaceDir, 5);
|
||||
});
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
}
|
||||
|
||||
const mainCorpus = await fs.readFile(
|
||||
path.join(workspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
|
||||
"utf-8",
|
||||
);
|
||||
const subagentCorpus = await fs.readFile(
|
||||
path.join(subagentWorkspaceDir, "memory", ".dreams", "session-corpus", "2026-04-05.txt"),
|
||||
"utf-8",
|
||||
);
|
||||
expect(mainCorpus).toContain("Main workspace should stay in main dreams.");
|
||||
expect(mainCorpus).not.toContain("CEO workspace should stay in CEO dreams.");
|
||||
expect(subagentCorpus).toContain("CEO workspace should stay in CEO dreams.");
|
||||
expect(subagentCorpus).not.toContain("Main workspace should stay in main dreams.");
|
||||
});
|
||||
|
||||
it("redacts sensitive session content before writing session corpus", async () => {
|
||||
const workspaceDir = await createDreamingWorkspace();
|
||||
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
|
||||
|
||||
@@ -112,9 +112,14 @@ function resolveWorkspaces(params: {
|
||||
cfg?: DreamingHostConfig;
|
||||
fallbackWorkspaceDir?: string;
|
||||
}): string[] {
|
||||
const fallbackWorkspaceDir = normalizeTrimmedString(params.fallbackWorkspaceDir);
|
||||
const workspaceCandidates = params.cfg
|
||||
? resolveMemoryDreamingWorkspaces(
|
||||
params.cfg as Parameters<typeof resolveMemoryDreamingWorkspaces>[0],
|
||||
{
|
||||
primaryWorkspaceDir: fallbackWorkspaceDir,
|
||||
primaryAgentId: "main",
|
||||
},
|
||||
).map((entry) => entry.workspaceDir)
|
||||
: [];
|
||||
const seen = new Set<string>();
|
||||
@@ -125,7 +130,6 @@ function resolveWorkspaces(params: {
|
||||
seen.add(workspaceDir);
|
||||
return true;
|
||||
});
|
||||
const fallbackWorkspaceDir = normalizeTrimmedString(params.fallbackWorkspaceDir);
|
||||
if (workspaces.length === 0 && fallbackWorkspaceDir) {
|
||||
workspaces.push(fallbackWorkspaceDir);
|
||||
}
|
||||
@@ -641,13 +645,22 @@ function buildSessionRenderedLine(params: {
|
||||
return `[${source}] ${params.snippet}`.slice(0, SESSION_INGESTION_MAX_SNIPPET_CHARS + 64);
|
||||
}
|
||||
|
||||
function resolveSessionAgentsForWorkspace(cfg: DreamingHostConfig, workspaceDir: string): string[] {
|
||||
function resolveSessionAgentsForWorkspace(params: {
|
||||
cfg: DreamingHostConfig;
|
||||
workspaceDir: string;
|
||||
primaryWorkspaceDir?: string;
|
||||
}): string[] {
|
||||
const { cfg, workspaceDir, primaryWorkspaceDir } = params;
|
||||
if (!cfg) {
|
||||
return [];
|
||||
}
|
||||
const target = normalizeWorkspaceKey(workspaceDir);
|
||||
const workspaces = resolveMemoryDreamingWorkspaces(
|
||||
cfg as Parameters<typeof resolveMemoryDreamingWorkspaces>[0],
|
||||
{
|
||||
primaryWorkspaceDir,
|
||||
primaryAgentId: "main",
|
||||
},
|
||||
);
|
||||
const match = workspaces.find((entry) => normalizeWorkspaceKey(entry.workspaceDir) === target);
|
||||
if (!match) {
|
||||
@@ -706,6 +719,7 @@ async function appendSessionCorpusLines(params: {
|
||||
async function collectSessionIngestionBatches(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
lookbackDays: number;
|
||||
nowMs: number;
|
||||
timezone?: string;
|
||||
@@ -720,7 +734,11 @@ async function collectSessionIngestionBatches(params: {
|
||||
Object.keys(params.state.seenMessages).length > 0,
|
||||
};
|
||||
}
|
||||
const agentIds = resolveSessionAgentsForWorkspace(params.cfg, params.workspaceDir);
|
||||
const agentIds = resolveSessionAgentsForWorkspace({
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
primaryWorkspaceDir: params.primaryWorkspaceDir,
|
||||
});
|
||||
const cutoffMs = calculateLookbackCutoffMs(params.nowMs, params.lookbackDays);
|
||||
const batchByDay = new Map<string, SessionIngestionMessage[]>();
|
||||
const nextFiles: Record<string, SessionIngestionFileState> = {};
|
||||
@@ -1003,6 +1021,7 @@ async function collectSessionIngestionBatches(params: {
|
||||
async function ingestSessionTranscriptSignals(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
lookbackDays: number;
|
||||
nowMs: number;
|
||||
timezone?: string;
|
||||
@@ -1011,6 +1030,7 @@ async function ingestSessionTranscriptSignals(params: {
|
||||
const collected = await collectSessionIngestionBatches({
|
||||
workspaceDir: params.workspaceDir,
|
||||
cfg: params.cfg,
|
||||
primaryWorkspaceDir: params.primaryWorkspaceDir,
|
||||
lookbackDays: params.lookbackDays,
|
||||
nowMs: params.nowMs,
|
||||
timezone: params.timezone,
|
||||
@@ -1520,6 +1540,7 @@ export function previewRemDreaming(params: {
|
||||
async function runLightDreaming(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
config: LightDreamingConfig;
|
||||
logger: Logger;
|
||||
subagent?: Parameters<typeof generateAndAppendDreamNarrative>[0]["subagent"];
|
||||
@@ -1537,6 +1558,7 @@ async function runLightDreaming(params: {
|
||||
await ingestSessionTranscriptSignals({
|
||||
workspaceDir: params.workspaceDir,
|
||||
cfg: params.cfg,
|
||||
primaryWorkspaceDir: params.primaryWorkspaceDir,
|
||||
lookbackDays: params.config.lookbackDays,
|
||||
nowMs,
|
||||
timezone: params.config.timezone,
|
||||
@@ -1617,6 +1639,7 @@ async function runLightDreaming(params: {
|
||||
async function runRemDreaming(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: DreamingHostConfig;
|
||||
primaryWorkspaceDir?: string;
|
||||
config: RemDreamingConfig;
|
||||
logger: Logger;
|
||||
subagent?: Parameters<typeof generateAndAppendDreamNarrative>[0]["subagent"];
|
||||
@@ -1634,6 +1657,7 @@ async function runRemDreaming(params: {
|
||||
await ingestSessionTranscriptSignals({
|
||||
workspaceDir: params.workspaceDir,
|
||||
cfg: params.cfg,
|
||||
primaryWorkspaceDir: params.primaryWorkspaceDir,
|
||||
lookbackDays: params.config.lookbackDays,
|
||||
nowMs,
|
||||
timezone: params.config.timezone,
|
||||
@@ -1766,9 +1790,10 @@ async function runPhaseIfTriggered(
|
||||
if (!params.config.enabled) {
|
||||
return { handled: true, reason: `memory-core: ${params.phase} dreaming disabled` };
|
||||
}
|
||||
const primaryWorkspaceDir = normalizeTrimmedString(params.workspaceDir);
|
||||
const workspaces = resolveWorkspaces({
|
||||
cfg: params.cfg,
|
||||
fallbackWorkspaceDir: params.workspaceDir,
|
||||
fallbackWorkspaceDir: primaryWorkspaceDir,
|
||||
});
|
||||
if (workspaces.length === 0) {
|
||||
params.logger.warn(
|
||||
@@ -1786,6 +1811,7 @@ async function runPhaseIfTriggered(
|
||||
await runLightDreaming({
|
||||
workspaceDir,
|
||||
cfg: params.cfg,
|
||||
primaryWorkspaceDir,
|
||||
config: params.config,
|
||||
logger: params.logger,
|
||||
subagent: params.subagent,
|
||||
@@ -1794,6 +1820,7 @@ async function runPhaseIfTriggered(
|
||||
await runRemDreaming({
|
||||
workspaceDir,
|
||||
cfg: params.cfg,
|
||||
primaryWorkspaceDir,
|
||||
config: params.config,
|
||||
logger: params.logger,
|
||||
subagent: params.subagent,
|
||||
|
||||
@@ -2316,11 +2316,27 @@ describe("short-term dreaming trigger", () => {
|
||||
it("fans out one dreaming run across configured agent workspaces", async () => {
|
||||
const logger = createLogger();
|
||||
const workspaceRoot = await createTempWorkspace("memory-dreaming-multi-");
|
||||
const mainWorkspace = path.join(workspaceRoot, "main");
|
||||
const alphaWorkspace = path.join(workspaceRoot, "alpha");
|
||||
const betaWorkspace = path.join(workspaceRoot, "beta");
|
||||
|
||||
await writeDailyMemoryNote(mainWorkspace, "2026-04-02", ["Main workspace note."]);
|
||||
await writeDailyMemoryNote(alphaWorkspace, "2026-04-02", ["Alpha backup note."]);
|
||||
await writeDailyMemoryNote(betaWorkspace, "2026-04-02", ["Beta router note."]);
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir: mainWorkspace,
|
||||
query: "main workspace",
|
||||
results: [
|
||||
{
|
||||
path: "memory/2026-04-02.md",
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0.9,
|
||||
snippet: "Main workspace note.",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
await recordShortTermRecalls({
|
||||
workspaceDir: alphaWorkspace,
|
||||
query: "alpha backup",
|
||||
@@ -2353,7 +2369,7 @@ describe("short-term dreaming trigger", () => {
|
||||
const result = await runShortTermDreamingPromotionIfTriggered({
|
||||
cleanedBody: constants.DREAMING_SYSTEM_EVENT_TEXT,
|
||||
trigger: "heartbeat",
|
||||
workspaceDir: alphaWorkspace,
|
||||
workspaceDir: mainWorkspace,
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -2387,6 +2403,9 @@ describe("short-term dreaming trigger", () => {
|
||||
});
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(await fs.readFile(path.join(mainWorkspace, "MEMORY.md"), "utf-8")).toContain(
|
||||
"Main workspace note.",
|
||||
);
|
||||
expect(await fs.readFile(path.join(alphaWorkspace, "MEMORY.md"), "utf-8")).toContain(
|
||||
"Alpha backup note.",
|
||||
);
|
||||
@@ -2394,7 +2413,7 @@ describe("short-term dreaming trigger", () => {
|
||||
"Beta router note.",
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
"memory-core: dreaming promotion complete (workspaces=2, candidates=2, applied=2, failed=0).",
|
||||
"memory-core: dreaming promotion complete (workspaces=3, candidates=3, applied=3, failed=0).",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -511,8 +511,12 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
|
||||
|
||||
const recencyHalfLifeDays =
|
||||
params.config.recencyHalfLifeDays ?? DEFAULT_MEMORY_DREAMING_RECENCY_HALF_LIFE_DAYS;
|
||||
const fallbackWorkspaceDir = normalizeTrimmedString(params.workspaceDir);
|
||||
const workspaceCandidates = params.cfg
|
||||
? resolveMemoryDreamingWorkspaces(params.cfg).map((entry) => entry.workspaceDir)
|
||||
? resolveMemoryDreamingWorkspaces(params.cfg, {
|
||||
primaryWorkspaceDir: fallbackWorkspaceDir,
|
||||
primaryAgentId: "main",
|
||||
}).map((entry) => entry.workspaceDir)
|
||||
: [];
|
||||
const seenWorkspaces = new Set<string>();
|
||||
const workspaces = workspaceCandidates.filter((workspaceDir) => {
|
||||
@@ -522,7 +526,6 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
|
||||
seenWorkspaces.add(workspaceDir);
|
||||
return true;
|
||||
});
|
||||
const fallbackWorkspaceDir = normalizeTrimmedString(params.workspaceDir);
|
||||
if (workspaces.length === 0 && fallbackWorkspaceDir) {
|
||||
workspaces.push(fallbackWorkspaceDir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user