Skip to content

Commit 4661bf6

Browse files
mbelinkymbelinky
andauthored
memory: chunk daily dreaming ingestion (openclaw#61583)
Merged via squash. Prepared head SHA: 88816a0 Co-authored-by: mbelinky <17249097+mbelinky@users.noreply.github.com> Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Reviewed-by: @mbelinky
1 parent e02ef07 commit 4661bf6

3 files changed

Lines changed: 334 additions & 68 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
4242
- Providers/CLI: remove bundled CLI text-provider backends and the `agents.defaults.cliBackends` surface, while keeping ACP harness sessions and Gemini media understanding on the native bundled providers.
4343
- Matrix/exec approvals: clarify unavailable-approval replies so Matrix no longer claims chat approvals are unsupported when native exec approvals are merely unconfigured. (#61424) Thanks @gumadeiras.
4444
- Docs/IRC: replace public IRC hostname examples with `irc.example.com` and recommend private servers for bot coordination while listing common public networks for intentional use.
45+
- Memory/dreaming: group nearby daily-note lines into short coherent chunks before staging them for dreaming, so one-off context from recent notes reaches REM/deep with better evidence and less line-level noise.
4546

4647
### Fixes
4748

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

Lines changed: 207 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ afterEach(async () => {
2222
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
2323
});
2424

25-
function createHarness(config: OpenClawConfig) {
25+
function createHarness(config: OpenClawConfig, workspaceDir?: string) {
2626
let beforeAgentReply:
2727
| ((
2828
event: { cleanedBody: string },
@@ -36,7 +36,18 @@ function createHarness(config: OpenClawConfig) {
3636
};
3737

3838
const api = {
39-
config,
39+
config: workspaceDir
40+
? {
41+
...config,
42+
agents: {
43+
...config.agents,
44+
defaults: {
45+
...config.agents?.defaults,
46+
workspace: workspaceDir,
47+
},
48+
},
49+
}
50+
: config,
4051
pluginConfig: {},
4152
logger,
4253
registerHook: vi.fn(),
@@ -65,26 +76,29 @@ describe("memory-core dreaming phases", () => {
6576
"utf-8",
6677
);
6778

68-
const { beforeAgentReply } = createHarness({
69-
plugins: {
70-
entries: {
71-
"memory-core": {
72-
config: {
73-
dreaming: {
74-
enabled: true,
75-
phases: {
76-
light: {
77-
enabled: true,
78-
limit: 20,
79-
lookbackDays: 2,
79+
const { beforeAgentReply } = createHarness(
80+
{
81+
plugins: {
82+
entries: {
83+
"memory-core": {
84+
config: {
85+
dreaming: {
86+
enabled: true,
87+
phases: {
88+
light: {
89+
enabled: true,
90+
limit: 20,
91+
lookbackDays: 2,
92+
},
8093
},
8194
},
8295
},
8396
},
8497
},
8598
},
8699
},
87-
});
100+
workspaceDir,
101+
);
88102

89103
const readSpy = vi.spyOn(fs, "readFile");
90104
try {
@@ -103,7 +117,7 @@ describe("memory-core dreaming phases", () => {
103117
const dailyReadCount = readSpy.mock.calls.filter(
104118
([target]) => String(target) === dailyPath,
105119
).length;
106-
expect(dailyReadCount).toBe(1);
120+
expect(dailyReadCount).toBeLessThanOrEqual(1);
107121
await expect(
108122
fs.access(path.join(workspaceDir, "memory", ".dreams", "daily-ingestion.json")),
109123
).resolves.toBeUndefined();
@@ -129,26 +143,89 @@ describe("memory-core dreaming phases", () => {
129143
});
130144
expect(before).toHaveLength(0);
131145

132-
const { beforeAgentReply } = createHarness({
133-
plugins: {
134-
entries: {
135-
"memory-core": {
136-
config: {
137-
dreaming: {
138-
enabled: true,
139-
phases: {
140-
light: {
141-
enabled: true,
142-
limit: 20,
143-
lookbackDays: 2,
146+
const { beforeAgentReply } = createHarness(
147+
{
148+
plugins: {
149+
entries: {
150+
"memory-core": {
151+
config: {
152+
dreaming: {
153+
enabled: true,
154+
phases: {
155+
light: {
156+
enabled: true,
157+
limit: 20,
158+
lookbackDays: 2,
159+
},
144160
},
145161
},
146162
},
147163
},
148164
},
149165
},
150166
},
167+
workspaceDir,
168+
);
169+
170+
await beforeAgentReply(
171+
{ cleanedBody: "__openclaw_memory_core_light_sleep__" },
172+
{ trigger: "heartbeat", workspaceDir },
173+
);
174+
175+
const after = await rankShortTermPromotionCandidates({
176+
workspaceDir,
177+
minScore: 0,
178+
minRecallCount: 0,
179+
minUniqueQueries: 0,
180+
nowMs: Date.parse("2026-04-05T10:05:00.000Z"),
151181
});
182+
expect(after).toHaveLength(1);
183+
expect(after[0]?.dailyCount).toBeGreaterThan(0);
184+
expect(after[0]?.startLine).toBe(3);
185+
expect(after[0]?.endLine).toBe(4);
186+
expect(after[0]?.snippet).toContain("Move backups to S3 Glacier.");
187+
expect(after[0]?.snippet).toContain("Keep retention at 365 days.");
188+
});
189+
190+
it("keeps section context when chunking durable daily notes", async () => {
191+
const workspaceDir = await createTempWorkspace();
192+
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
193+
await fs.writeFile(
194+
path.join(workspaceDir, "memory", "2026-04-05.md"),
195+
[
196+
"# 2026-04-05",
197+
"",
198+
"## Emma Rees",
199+
"- She asked for more space after the last exchange.",
200+
"- Better to keep messages short and low-pressure.",
201+
"- Re-engagement should be time-bounded and optional.",
202+
].join("\n"),
203+
"utf-8",
204+
);
205+
206+
const { beforeAgentReply } = createHarness(
207+
{
208+
plugins: {
209+
entries: {
210+
"memory-core": {
211+
config: {
212+
dreaming: {
213+
enabled: true,
214+
phases: {
215+
light: {
216+
enabled: true,
217+
limit: 20,
218+
lookbackDays: 2,
219+
},
220+
},
221+
},
222+
},
223+
},
224+
},
225+
},
226+
},
227+
workspaceDir,
228+
);
152229

153230
await beforeAgentReply(
154231
{ cleanedBody: "__openclaw_memory_core_light_sleep__" },
@@ -162,8 +239,86 @@ describe("memory-core dreaming phases", () => {
162239
minUniqueQueries: 0,
163240
nowMs: Date.parse("2026-04-05T10:05:00.000Z"),
164241
});
165-
expect(after.length).toBeGreaterThan(0);
166-
expect(after.some((candidate) => (candidate.dailyCount ?? 0) > 0)).toBe(true);
242+
expect(after).toHaveLength(1);
243+
expect(after[0]?.startLine).toBe(4);
244+
expect(after[0]?.endLine).toBe(6);
245+
expect(after[0]?.snippet).toContain("Emma Rees:");
246+
expect(after[0]?.snippet).toContain("She asked for more space");
247+
expect(after[0]?.snippet).toContain("messages short and low-pressure");
248+
});
249+
250+
it("splits noisy daily notes into a few coherent chunks instead of one line per item", async () => {
251+
const workspaceDir = await createTempWorkspace();
252+
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
253+
await fs.writeFile(
254+
path.join(workspaceDir, "memory", "2026-04-05.md"),
255+
[
256+
"# 2026-04-05",
257+
"",
258+
"## Operations",
259+
"- Restarted the gateway after auth drift.",
260+
"- Tokens now line up again.",
261+
"",
262+
"## Bex",
263+
"- She prefers direct plans over open-ended maybes.",
264+
"- Better to offer one concrete time window.",
265+
"",
266+
"11:30",
267+
"",
268+
"## Travel",
269+
"- Flight lands at 08:10.",
270+
].join("\n"),
271+
"utf-8",
272+
);
273+
274+
const { beforeAgentReply } = createHarness(
275+
{
276+
plugins: {
277+
entries: {
278+
"memory-core": {
279+
config: {
280+
dreaming: {
281+
enabled: true,
282+
phases: {
283+
light: {
284+
enabled: true,
285+
limit: 20,
286+
lookbackDays: 2,
287+
},
288+
},
289+
},
290+
},
291+
},
292+
},
293+
},
294+
},
295+
workspaceDir,
296+
);
297+
298+
await beforeAgentReply(
299+
{ cleanedBody: "__openclaw_memory_core_light_sleep__" },
300+
{ trigger: "heartbeat", workspaceDir },
301+
);
302+
303+
const after = await rankShortTermPromotionCandidates({
304+
workspaceDir,
305+
minScore: 0,
306+
minRecallCount: 0,
307+
minUniqueQueries: 0,
308+
nowMs: Date.parse("2026-04-05T10:05:00.000Z"),
309+
});
310+
expect(after).toHaveLength(3);
311+
expect(after.map((candidate) => candidate.snippet)).toEqual(
312+
expect.arrayContaining([
313+
expect.stringContaining(
314+
"Operations: Restarted the gateway after auth drift.; Tokens now line up again.",
315+
),
316+
expect.stringContaining(
317+
"Bex: She prefers direct plans over open-ended maybes.; Better to offer one concrete time window.",
318+
),
319+
expect.stringContaining("Travel: Flight lands at 08:10."),
320+
]),
321+
);
167322
});
168323

169324
it("records light/rem signals that reinforce deep promotion ranking", async () => {
@@ -210,32 +365,35 @@ describe("memory-core dreaming phases", () => {
210365
expect(baseline).toHaveLength(1);
211366
const baselineScore = baseline[0]!.score;
212367

213-
const { beforeAgentReply } = createHarness({
214-
plugins: {
215-
entries: {
216-
"memory-core": {
217-
config: {
218-
dreaming: {
219-
enabled: true,
220-
phases: {
221-
light: {
222-
enabled: true,
223-
limit: 10,
224-
lookbackDays: 7,
225-
},
226-
rem: {
227-
enabled: true,
228-
limit: 10,
229-
lookbackDays: 7,
230-
minPatternStrength: 0,
368+
const { beforeAgentReply } = createHarness(
369+
{
370+
plugins: {
371+
entries: {
372+
"memory-core": {
373+
config: {
374+
dreaming: {
375+
enabled: true,
376+
phases: {
377+
light: {
378+
enabled: true,
379+
limit: 10,
380+
lookbackDays: 7,
381+
},
382+
rem: {
383+
enabled: true,
384+
limit: 10,
385+
lookbackDays: 7,
386+
minPatternStrength: 0,
387+
},
231388
},
232389
},
233390
},
234391
},
235392
},
236393
},
237394
},
238-
});
395+
workspaceDir,
396+
);
239397

240398
await beforeAgentReply(
241399
{ cleanedBody: "__openclaw_memory_core_light_sleep__" },

0 commit comments

Comments
 (0)