Skip to content

Commit 7572f17

Browse files
committed
Dreaming: update multiphase stats and UI polish
1 parent 3600cec commit 7572f17

8 files changed

Lines changed: 253 additions & 23 deletions

File tree

src/gateway/server-methods/doctor.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ describe("doctor.memory.status", () => {
119119
dreaming: expect.objectContaining({
120120
enabled: true,
121121
shortTermCount: 0,
122+
totalSignalCount: 0,
123+
phaseSignalCount: 0,
122124
promotedTotal: 0,
123125
promotedToday: 0,
124126
phases: expect.objectContaining({
@@ -183,6 +185,18 @@ describe("doctor.memory.status", () => {
183185
".dreams",
184186
"short-term-recall.json",
185187
);
188+
const mainPhaseSignalPath = path.join(
189+
mainWorkspaceDir,
190+
"memory",
191+
".dreams",
192+
"phase-signals.json",
193+
);
194+
const alphaPhaseSignalPath = path.join(
195+
alphaWorkspaceDir,
196+
"memory",
197+
".dreams",
198+
"phase-signals.json",
199+
);
186200
await fs.mkdir(path.dirname(mainStorePath), { recursive: true });
187201
await fs.mkdir(path.dirname(alphaStorePath), { recursive: true });
188202
await fs.writeFile(
@@ -195,11 +209,15 @@ describe("doctor.memory.status", () => {
195209
"memory:memory/2026-04-03.md:1:2": {
196210
path: "memory/2026-04-03.md",
197211
source: "memory",
212+
recallCount: 2,
213+
dailyCount: 1,
198214
promotedAt: undefined,
199215
},
200216
"memory:memory/2026-04-02.md:1:2": {
201217
path: "memory/2026-04-02.md",
202218
source: "memory",
219+
recallCount: 9,
220+
dailyCount: 5,
203221
promotedAt: recentIso,
204222
},
205223
},
@@ -219,11 +237,15 @@ describe("doctor.memory.status", () => {
219237
"memory:memory/2026-04-01.md:1:2": {
220238
path: "memory/2026-04-01.md",
221239
source: "memory",
240+
recallCount: 7,
241+
dailyCount: 4,
222242
promotedAt: olderIso,
223243
},
224244
"memory:memory/2026-04-04.md:1:2": {
225245
path: "memory/2026-04-04.md",
226246
source: "memory",
247+
recallCount: 8,
248+
dailyCount: 3,
227249
promotedAt: recentIso,
228250
},
229251
},
@@ -233,6 +255,46 @@ describe("doctor.memory.status", () => {
233255
)}\n`,
234256
"utf-8",
235257
);
258+
await fs.writeFile(
259+
mainPhaseSignalPath,
260+
`${JSON.stringify(
261+
{
262+
version: 1,
263+
updatedAt: recentIso,
264+
entries: {
265+
"memory:memory/2026-04-03.md:1:2": {
266+
lightHits: 2,
267+
remHits: 3,
268+
},
269+
"memory:memory/2026-04-02.md:1:2": {
270+
lightHits: 9,
271+
remHits: 9,
272+
},
273+
},
274+
},
275+
null,
276+
2,
277+
)}\n`,
278+
"utf-8",
279+
);
280+
await fs.writeFile(
281+
alphaPhaseSignalPath,
282+
`${JSON.stringify(
283+
{
284+
version: 1,
285+
updatedAt: recentIso,
286+
entries: {
287+
"memory:memory/2026-04-01.md:1:2": {
288+
lightHits: 5,
289+
remHits: 5,
290+
},
291+
},
292+
},
293+
null,
294+
2,
295+
)}\n`,
296+
"utf-8",
297+
);
236298

237299
loadConfig.mockReturnValue({
238300
agents: {
@@ -308,6 +370,12 @@ describe("doctor.memory.status", () => {
308370
enabled: true,
309371
timezone: "America/Los_Angeles",
310372
shortTermCount: 1,
373+
recallSignalCount: 2,
374+
dailySignalCount: 1,
375+
totalSignalCount: 3,
376+
phaseSignalCount: 5,
377+
lightPhaseHitCount: 2,
378+
remPhaseHitCount: 3,
311379
promotedTotal: 3,
312380
promotedToday: 2,
313381
phases: expect.objectContaining({

src/gateway/server-methods/doctor.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { formatError } from "../server-utils.js";
1717
import type { GatewayRequestHandlers } from "./types.js";
1818

1919
const SHORT_TERM_STORE_RELATIVE_PATH = path.join("memory", ".dreams", "short-term-recall.json");
20+
const SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH = path.join("memory", ".dreams", "phase-signals.json");
2021
const MANAGED_DEEP_SLEEP_CRON_NAME = "Memory Dreaming Promotion";
2122
const MANAGED_DEEP_SLEEP_CRON_TAG = "[managed-by=memory-core.short-term-promotion]";
2223
const DEEP_SLEEP_SYSTEM_EVENT_TEXT = "__openclaw_memory_core_short_term_promotion_dream__";
@@ -56,11 +57,19 @@ type DoctorMemoryDreamingPayload = {
5657
storageMode: "inline" | "separate" | "both";
5758
separateReports: boolean;
5859
shortTermCount: number;
60+
recallSignalCount: number;
61+
dailySignalCount: number;
62+
totalSignalCount: number;
63+
phaseSignalCount: number;
64+
lightPhaseHitCount: number;
65+
remPhaseHitCount: number;
5966
promotedTotal: number;
6067
promotedToday: number;
6168
storePath?: string;
69+
phaseSignalPath?: string;
6270
lastPromotedAt?: string;
6371
storeError?: string;
72+
phaseSignalError?: string;
6473
phases: {
6574
light: DoctorMemoryLightDreamingPayload;
6675
deep: DoctorMemoryDeepDreamingPayload;
@@ -106,11 +115,19 @@ function resolveDreamingConfig(
106115
): Omit<
107116
DoctorMemoryDreamingPayload,
108117
| "shortTermCount"
118+
| "recallSignalCount"
119+
| "dailySignalCount"
120+
| "totalSignalCount"
121+
| "phaseSignalCount"
122+
| "lightPhaseHitCount"
123+
| "remPhaseHitCount"
109124
| "promotedTotal"
110125
| "promotedToday"
111126
| "storePath"
127+
| "phaseSignalPath"
112128
| "lastPromotedAt"
113129
| "storeError"
130+
| "phaseSignalError"
114131
> {
115132
const resolved = resolveMemoryDreamingConfig({
116133
pluginConfig: resolveMemoryCorePluginConfig(cfg),
@@ -180,31 +197,55 @@ function isShortTermMemoryPath(filePath: string): boolean {
180197
type DreamingStoreStats = Pick<
181198
DoctorMemoryDreamingPayload,
182199
| "shortTermCount"
200+
| "recallSignalCount"
201+
| "dailySignalCount"
202+
| "totalSignalCount"
203+
| "phaseSignalCount"
204+
| "lightPhaseHitCount"
205+
| "remPhaseHitCount"
183206
| "promotedTotal"
184207
| "promotedToday"
185208
| "storePath"
209+
| "phaseSignalPath"
186210
| "lastPromotedAt"
187211
| "storeError"
212+
| "phaseSignalError"
188213
>;
189214

215+
function toNonNegativeInt(value: unknown): number {
216+
const num = Number(value);
217+
if (!Number.isFinite(num)) {
218+
return 0;
219+
}
220+
return Math.max(0, Math.floor(num));
221+
}
222+
190223
async function loadDreamingStoreStats(
191224
workspaceDir: string,
192225
nowMs: number,
193226
timezone?: string,
194227
): Promise<DreamingStoreStats> {
195228
const storePath = path.join(workspaceDir, SHORT_TERM_STORE_RELATIVE_PATH);
229+
const phaseSignalPath = path.join(workspaceDir, SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH);
196230
try {
197231
const raw = await fs.readFile(storePath, "utf-8");
198232
const parsed = JSON.parse(raw) as unknown;
199233
const store = asRecord(parsed);
200234
const entries = asRecord(store?.entries) ?? {};
201235
let shortTermCount = 0;
236+
let recallSignalCount = 0;
237+
let dailySignalCount = 0;
238+
let totalSignalCount = 0;
239+
let phaseSignalCount = 0;
240+
let lightPhaseHitCount = 0;
241+
let remPhaseHitCount = 0;
202242
let promotedTotal = 0;
203243
let promotedToday = 0;
204244
let latestPromotedAtMs = Number.NEGATIVE_INFINITY;
205245
let latestPromotedAt: string | undefined;
246+
const activeKeys = new Set<string>();
206247

207-
for (const value of Object.values(entries)) {
248+
for (const [entryKey, value] of Object.entries(entries)) {
208249
const entry = asRecord(value);
209250
if (!entry) {
210251
continue;
@@ -217,6 +258,12 @@ async function loadDreamingStoreStats(
217258
const promotedAt = normalizeTrimmedString(entry.promotedAt);
218259
if (!promotedAt) {
219260
shortTermCount += 1;
261+
activeKeys.add(entryKey);
262+
const recallCount = toNonNegativeInt(entry.recallCount);
263+
const dailyCount = toNonNegativeInt(entry.dailyCount);
264+
recallSignalCount += recallCount;
265+
dailySignalCount += dailyCount;
266+
totalSignalCount += recallCount + dailyCount;
220267
continue;
221268
}
222269
promotedTotal += 1;
@@ -230,52 +277,118 @@ async function loadDreamingStoreStats(
230277
}
231278
}
232279

280+
let phaseSignalError: string | undefined;
281+
try {
282+
const phaseRaw = await fs.readFile(phaseSignalPath, "utf-8");
283+
const parsedPhase = JSON.parse(phaseRaw) as unknown;
284+
const phaseStore = asRecord(parsedPhase);
285+
const phaseEntries = asRecord(phaseStore?.entries) ?? {};
286+
for (const [key, value] of Object.entries(phaseEntries)) {
287+
if (!activeKeys.has(key)) {
288+
continue;
289+
}
290+
const phaseEntry = asRecord(value);
291+
const lightHits = toNonNegativeInt(phaseEntry?.lightHits);
292+
const remHits = toNonNegativeInt(phaseEntry?.remHits);
293+
lightPhaseHitCount += lightHits;
294+
remPhaseHitCount += remHits;
295+
phaseSignalCount += lightHits + remHits;
296+
}
297+
} catch (err) {
298+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
299+
if (code !== "ENOENT") {
300+
phaseSignalError = formatError(err);
301+
}
302+
}
303+
233304
return {
234305
shortTermCount,
306+
recallSignalCount,
307+
dailySignalCount,
308+
totalSignalCount,
309+
phaseSignalCount,
310+
lightPhaseHitCount,
311+
remPhaseHitCount,
235312
promotedTotal,
236313
promotedToday,
237314
storePath,
315+
phaseSignalPath,
238316
...(latestPromotedAt ? { lastPromotedAt: latestPromotedAt } : {}),
317+
...(phaseSignalError ? { phaseSignalError } : {}),
239318
};
240319
} catch (err) {
241320
const code = (err as NodeJS.ErrnoException | undefined)?.code;
242321
if (code === "ENOENT") {
243322
return {
244323
shortTermCount: 0,
324+
recallSignalCount: 0,
325+
dailySignalCount: 0,
326+
totalSignalCount: 0,
327+
phaseSignalCount: 0,
328+
lightPhaseHitCount: 0,
329+
remPhaseHitCount: 0,
245330
promotedTotal: 0,
246331
promotedToday: 0,
247332
storePath,
333+
phaseSignalPath,
248334
};
249335
}
250336
return {
251337
shortTermCount: 0,
338+
recallSignalCount: 0,
339+
dailySignalCount: 0,
340+
totalSignalCount: 0,
341+
phaseSignalCount: 0,
342+
lightPhaseHitCount: 0,
343+
remPhaseHitCount: 0,
252344
promotedTotal: 0,
253345
promotedToday: 0,
254346
storePath,
347+
phaseSignalPath,
255348
storeError: formatError(err),
256349
};
257350
}
258351
}
259352

260353
function mergeDreamingStoreStats(stats: DreamingStoreStats[]): DreamingStoreStats {
261354
let shortTermCount = 0;
355+
let recallSignalCount = 0;
356+
let dailySignalCount = 0;
357+
let totalSignalCount = 0;
358+
let phaseSignalCount = 0;
359+
let lightPhaseHitCount = 0;
360+
let remPhaseHitCount = 0;
262361
let promotedTotal = 0;
263362
let promotedToday = 0;
264363
let latestPromotedAtMs = Number.NEGATIVE_INFINITY;
265364
let lastPromotedAt: string | undefined;
266365
const storePaths = new Set<string>();
366+
const phaseSignalPaths = new Set<string>();
267367
const storeErrors: string[] = [];
368+
const phaseSignalErrors: string[] = [];
268369

269370
for (const stat of stats) {
270371
shortTermCount += stat.shortTermCount;
372+
recallSignalCount += stat.recallSignalCount;
373+
dailySignalCount += stat.dailySignalCount;
374+
totalSignalCount += stat.totalSignalCount;
375+
phaseSignalCount += stat.phaseSignalCount;
376+
lightPhaseHitCount += stat.lightPhaseHitCount;
377+
remPhaseHitCount += stat.remPhaseHitCount;
271378
promotedTotal += stat.promotedTotal;
272379
promotedToday += stat.promotedToday;
273380
if (stat.storePath) {
274381
storePaths.add(stat.storePath);
275382
}
383+
if (stat.phaseSignalPath) {
384+
phaseSignalPaths.add(stat.phaseSignalPath);
385+
}
276386
if (stat.storeError) {
277387
storeErrors.push(stat.storeError);
278388
}
389+
if (stat.phaseSignalError) {
390+
phaseSignalErrors.push(stat.phaseSignalError);
391+
}
279392
const promotedAtMs = stat.lastPromotedAt ? Date.parse(stat.lastPromotedAt) : Number.NaN;
280393
if (Number.isFinite(promotedAtMs) && promotedAtMs > latestPromotedAtMs) {
281394
latestPromotedAtMs = promotedAtMs;
@@ -285,15 +398,27 @@ function mergeDreamingStoreStats(stats: DreamingStoreStats[]): DreamingStoreStat
285398

286399
return {
287400
shortTermCount,
401+
recallSignalCount,
402+
dailySignalCount,
403+
totalSignalCount,
404+
phaseSignalCount,
405+
lightPhaseHitCount,
406+
remPhaseHitCount,
288407
promotedTotal,
289408
promotedToday,
290409
...(storePaths.size === 1 ? { storePath: [...storePaths][0] } : {}),
410+
...(phaseSignalPaths.size === 1 ? { phaseSignalPath: [...phaseSignalPaths][0] } : {}),
291411
...(lastPromotedAt ? { lastPromotedAt } : {}),
292412
...(storeErrors.length === 1
293413
? { storeError: storeErrors[0] }
294414
: storeErrors.length > 1
295415
? { storeError: `${storeErrors.length} dreaming stores had read errors.` }
296416
: {}),
417+
...(phaseSignalErrors.length === 1
418+
? { phaseSignalError: phaseSignalErrors[0] }
419+
: phaseSignalErrors.length > 1
420+
? { phaseSignalError: `${phaseSignalErrors.length} phase signal stores had read errors.` }
421+
: {}),
297422
};
298423
}
299424

@@ -472,6 +597,12 @@ export const doctorHandlers: GatewayRequestHandlers = {
472597
)
473598
: {
474599
shortTermCount: 0,
600+
recallSignalCount: 0,
601+
dailySignalCount: 0,
602+
totalSignalCount: 0,
603+
phaseSignalCount: 0,
604+
lightPhaseHitCount: 0,
605+
remPhaseHitCount: 0,
475606
promotedTotal: 0,
476607
promotedToday: 0,
477608
};

0 commit comments

Comments
 (0)