Skip to content

Commit 1c0a90c

Browse files
committed
fix(doctor): match timestamp-suffixed and slugged daily memory files in isShortTermMemoryPath
Align the gateway doctor predicate with the memory-core short-term path contract. The regex now matches any slug suffix (not just -HHMM numeric), handling llmSlug output like memory/YYYY-MM-DD-vendor-pitch.md. Add a focused regression test proving doctor.memory.status counts both timestamp-suffixed (-HHMM) and slugged (-vendor-pitch) recall-store entries. Fixes #90896
1 parent cd74a5a commit 1c0a90c

2 files changed

Lines changed: 200 additions & 1 deletion

File tree

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

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,205 @@ describe("doctor.memory.status", () => {
642642
}
643643
});
644644

645+
it("counts timestamp-suffixed and slugged recall-store entries in dreaming stats", async () => {
646+
const now = Date.parse("2026-06-06T00:30:00.000Z");
647+
vi.useFakeTimers();
648+
vi.setSystemTime(now);
649+
const recentIso = "2026-06-05T23:45:00.000Z";
650+
const olderIso = "2026-06-03T10:00:00.000Z";
651+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-slug-"));
652+
const mainWorkspaceDir = path.join(workspaceRoot, "main");
653+
const mainStorePath = path.join(
654+
mainWorkspaceDir,
655+
"memory",
656+
".dreams",
657+
"short-term-recall.json",
658+
);
659+
const mainPhaseSignalPath = path.join(
660+
mainWorkspaceDir,
661+
"memory",
662+
".dreams",
663+
"phase-signals.json",
664+
);
665+
await fs.mkdir(path.dirname(mainStorePath), { recursive: true });
666+
// Include both timestamp-suffixed (-HHMM) and slugged (-vendor-pitch) paths
667+
await fs.writeFile(
668+
mainStorePath,
669+
`${JSON.stringify(
670+
{
671+
version: 1,
672+
updatedAt: recentIso,
673+
entries: {
674+
"memory:memory/2026-06-04-1503.md:1:2": {
675+
path: "memory/2026-06-04-1503.md",
676+
startLine: 1,
677+
endLine: 2,
678+
snippet: "Timestamp-suffixed entry.",
679+
source: "memory",
680+
recallCount: 4,
681+
dailyCount: 2,
682+
lastRecalledAt: recentIso,
683+
promotedAt: recentIso,
684+
},
685+
"memory:memory/2026-06-03-vendor-pitch.md:1:2": {
686+
path: "memory/2026-06-03-vendor-pitch.md",
687+
startLine: 1,
688+
endLine: 2,
689+
snippet: "Slugged llmSlug entry.",
690+
source: "memory",
691+
recallCount: 7,
692+
dailyCount: 4,
693+
promotedAt: olderIso,
694+
},
695+
"memory:memory/2026-06-05-0930.md:1:2": {
696+
path: "memory/2026-06-05-0930.md",
697+
startLine: 1,
698+
endLine: 2,
699+
snippet: "Another timestamp-suffixed entry.",
700+
source: "memory",
701+
recallCount: 3,
702+
dailyCount: 1,
703+
},
704+
},
705+
},
706+
null,
707+
2,
708+
)}\n`,
709+
"utf-8",
710+
);
711+
await fs.writeFile(
712+
mainPhaseSignalPath,
713+
`${JSON.stringify(
714+
{
715+
version: 1,
716+
updatedAt: recentIso,
717+
entries: {
718+
"memory:memory/2026-06-04-1503.md:1:2": {
719+
lightHits: 2,
720+
remHits: 3,
721+
},
722+
"memory:memory/2026-06-03-vendor-pitch.md:1:2": {
723+
lightHits: 1,
724+
remHits: 2,
725+
},
726+
"memory:memory/2026-06-05-0930.md:1:2": {
727+
lightHits: 1,
728+
remHits: 1,
729+
},
730+
},
731+
},
732+
null,
733+
2,
734+
)}\n`,
735+
"utf-8",
736+
);
737+
738+
getRuntimeConfig.mockReturnValue({
739+
agents: {
740+
defaults: {
741+
userTimezone: "America/Los_Angeles",
742+
memorySearch: {
743+
enabled: true,
744+
},
745+
},
746+
list: [],
747+
},
748+
plugins: {
749+
entries: {
750+
"memory-core": {
751+
config: {
752+
dreaming: {
753+
enabled: true,
754+
frequency: "0 */4 * * *",
755+
phases: {
756+
deep: {
757+
recencyHalfLifeDays: 21,
758+
maxAgeDays: 30,
759+
},
760+
},
761+
},
762+
},
763+
},
764+
},
765+
},
766+
} as OpenClawConfig);
767+
resolveAgentWorkspaceDir.mockReturnValue(mainWorkspaceDir);
768+
const close = vi.fn().mockResolvedValue(undefined);
769+
getMemorySearchManager.mockResolvedValue({
770+
manager: {
771+
status: () => ({ provider: "gemini", workspaceDir: mainWorkspaceDir }),
772+
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
773+
close,
774+
},
775+
});
776+
const cronList = vi.fn(async () => [
777+
{
778+
name: "Memory Dreaming Promotion",
779+
description: "[managed-by=memory-core.short-term-promotion] test",
780+
enabled: true,
781+
payload: {
782+
kind: "systemEvent",
783+
text: "__openclaw_memory_core_short_term_promotion_dream__",
784+
},
785+
state: { nextRunAtMs: now + 60_000 },
786+
},
787+
]);
788+
const respond = vi.fn();
789+
790+
try {
791+
await invokeDoctorMemoryStatus(respond, { cron: { list: cronList } });
792+
const payload = respondPayload(respond);
793+
const dreaming = expectRecordFields(payload.dreaming, {
794+
enabled: true,
795+
// All 3 entries (including timestamp and slugged) should be counted
796+
// Active (non-promoted) entries: only 2026-06-05-0930.md
797+
shortTermCount: 1,
798+
recallSignalCount: 3, // from active entry
799+
dailySignalCount: 1, // from active entry
800+
totalSignalCount: 4, // recallSignalCount + dailySignalCount
801+
phaseSignalCount: 2, // lightHits 1 + remHits 1 from active entry
802+
lightPhaseHitCount: 1,
803+
remPhaseHitCount: 1,
804+
promotedTotal: 2, // both promoted entries counted
805+
promotedToday: 1, // only 2026-06-04-1503 promoted today
806+
});
807+
// Verify timestamp-suffixed entry is present and counted
808+
expectRecordFields(
809+
findRecordByField(dreaming.promotedEntries, "path", "memory/2026-06-04-1503.md"),
810+
{
811+
promotedAt: recentIso,
812+
snippet: "Timestamp-suffixed entry.",
813+
},
814+
);
815+
// Verify slugged entry is present and counted
816+
expectRecordFields(
817+
findRecordByField(dreaming.promotedEntries, "path", "memory/2026-06-03-vendor-pitch.md"),
818+
{
819+
promotedAt: olderIso,
820+
snippet: "Slugged llmSlug entry.",
821+
},
822+
);
823+
// Verify short-term entry with timestamp suffix is present
824+
expectRecordFields((dreaming.shortTermEntries as unknown[])[0], {
825+
path: "memory/2026-06-05-0930.md",
826+
snippet: "Another timestamp-suffixed entry.",
827+
totalSignalCount: 4,
828+
lightHits: 1,
829+
remHits: 1,
830+
phaseHitCount: 2,
831+
});
832+
// Verify signal entries include slugged path
833+
expectRecordFields((dreaming.signalEntries as unknown[])[0], {
834+
path: "memory/2026-06-05-0930.md",
835+
totalSignalCount: 4,
836+
});
837+
expect(close).toHaveBeenCalled();
838+
} finally {
839+
vi.useRealTimers();
840+
await fs.rm(workspaceRoot, { recursive: true, force: true });
841+
}
842+
});
843+
645844
it("scopes dreaming status to the requested agent workspace", async () => {
646845
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-selected-"));
647846
const mainWorkspaceDir = path.join(workspaceRoot, "main");

src/gateway/server-methods/doctor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ function normalizeMemoryPathForWorkspace(workspaceDir: string, rawPath: string):
350350
function isShortTermMemoryPath(filePath: string): boolean {
351351
const normalized = normalizeMemoryPath(filePath);
352352
// Status only counts short-term source shapes; promoted diary/report files stay out.
353-
if (/(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})(?:-\d{4})?\.md$/.test(normalized)) {
353+
if (/(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})(?:-[^/]+)?\.md$/.test(normalized)) {
354354
return true;
355355
}
356356
if (

0 commit comments

Comments
 (0)