fix: include primary dreaming workspace

This commit is contained in:
Peter Steinberger
2026-05-02 09:24:54 +01:00
parent 5f6adaf157
commit 99f1db33bf
10 changed files with 213 additions and 22 deletions

View File

@@ -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");

View File

@@ -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,

View File

@@ -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).",
);
});
});

View File

@@ -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);
}