diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 4292da4688a..ea057e3eb1a 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -373,6 +373,9 @@ The remaining cleanup is mostly consolidation and deletion: filesystem existence checks. Its transcript-ingestion tests seed SQLite rows through neutral test locators instead of creating `agents//sessions` fixtures. +- Gateway doctor memory status reads short-term recall and phase-signal counts + from SQLite plugin-state rows instead of `memory/.dreams/*.json`; CLI and + doctor output now label that storage as a SQLite store, not a path. - Sandbox container/browser registries now use the shared `sandbox_registry_entries` SQLite table. Doctor imports legacy monolithic and sharded JSON registry files and removes successful sources. diff --git a/extensions/memory-core/src/cli.runtime.ts b/extensions/memory-core/src/cli.runtime.ts index 7afadb03a5e..af34871fe36 100644 --- a/extensions/memory-core/src/cli.runtime.ts +++ b/extensions/memory-core/src/cli.runtime.ts @@ -54,7 +54,7 @@ import { recordGroundedShortTermCandidates, recordShortTermRecalls, rankShortTermPromotionCandidates, - resolveShortTermRecallStorePath, + resolveShortTermRecallStoreLabel, type RepairShortTermPromotionArtifactsResult, type ShortTermAuditSummary, } from "./short-term-promotion.js"; @@ -926,7 +926,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { } if (audit) { lines.push(`${label("Recall store")} ${info(formatAuditCounts(audit))}`); - lines.push(`${label("Recall path")} ${info(shortenHomePath(audit.storePath))}`); + lines.push(`${label("Recall storage")} ${info(audit.storeLabel)}`); if (audit.updatedAt) { lines.push(`${label("Recall updated")} ${info(audit.updatedAt)}`); } @@ -1275,7 +1275,7 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) { } } - const storePath = resolveShortTermRecallStorePath(workspaceDir); + const storeLabel = resolveShortTermRecallStoreLabel(workspaceDir); const customQmd = asRecord(asRecord(status.custom)?.qmd); const audit = await auditShortTermPromotionArtifacts({ workspaceDir, @@ -1292,7 +1292,7 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) { if (opts.json) { defaultRuntime.writeJson({ workspaceDir, - storePath, + storeLabel, audit, candidates, apply: applyResult @@ -1310,7 +1310,7 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) { if (candidates.length === 0) { defaultRuntime.log("No short-term recall candidates."); - defaultRuntime.log(`Recall store: ${shortenHomePath(storePath)}`); + defaultRuntime.log(`Recall store: ${storeLabel}`); if (audit.issues.length > 0) { for (const issue of audit.issues) { defaultRuntime.log(issue.message); @@ -1328,7 +1328,7 @@ export async function runMemoryPromote(opts: MemoryPromoteCommandOptions) { `(${agentId})`, )}`, ); - lines.push(`${colorize(rich, theme.muted, "Recall store:")} ${shortenHomePath(storePath)}`); + lines.push(`${colorize(rich, theme.muted, "Recall store:")} ${storeLabel}`); lines.push(colorize(rich, theme.muted, `Store health: ${formatAuditCounts(audit)}`)); for (const candidate of candidates) { lines.push( @@ -1726,7 +1726,7 @@ export async function runMemoryRemBackfill(opts: MemoryRemBackfillOptions) { : {}), ...(shortTermRollback ? { - shortTermStorePath: shortTermRollback.storePath, + shortTermStoreLabel: shortTermRollback.storeLabel, removedShortTermEntries: shortTermRollback.removed, } : {}), @@ -1752,7 +1752,7 @@ export async function runMemoryRemBackfill(opts: MemoryRemBackfillOptions) { colorize( isRich(), theme.muted, - `shortTermStorePath=${shortenHomePath(shortTermRollback.storePath)}`, + `shortTermStoreLabel=${shortTermRollback.storeLabel}`, ), colorize( isRich(), diff --git a/extensions/memory-core/src/short-term-promotion.test.ts b/extensions/memory-core/src/short-term-promotion.test.ts index ff7bef19f85..681e97f7b2f 100644 --- a/extensions/memory-core/src/short-term-promotion.test.ts +++ b/extensions/memory-core/src/short-term-promotion.test.ts @@ -1584,7 +1584,7 @@ describe("short-term promotion", () => { }); const audit = await auditShortTermPromotionArtifacts({ workspaceDir }); - expect(audit.storePath).toBe( + expect(audit.storeLabel).toBe( "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", ); expect(audit.invalidEntryCount).toBe(0); diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index 975a835156e..7bb48bba95a 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -153,7 +153,7 @@ type ShortTermAuditIssue = { }; export type ShortTermAuditSummary = { - storePath: string; + storeLabel: string; updatedAt?: string; exists: boolean; entryCount: number; @@ -1626,12 +1626,12 @@ export async function applyShortTermPromotions( }); } -export function resolveShortTermRecallStorePath(workspaceDir: string): string { +export function resolveShortTermRecallStoreLabel(workspaceDir: string): string { void workspaceDir; return resolveSqliteStoreLabel(MEMORY_CORE_SHORT_TERM_RECALL_NAMESPACE); } -export function resolveShortTermPhaseSignalStorePath(workspaceDir: string): string { +export function resolveShortTermPhaseSignalStoreLabel(workspaceDir: string): string { void workspaceDir; return resolveSqliteStoreLabel(MEMORY_CORE_SHORT_TERM_PHASE_SIGNAL_NAMESPACE); } @@ -1644,7 +1644,7 @@ export async function auditShortTermPromotionArtifacts(params: { }; }): Promise { const workspaceDir = params.workspaceDir.trim(); - const storePath = resolveShortTermRecallStorePath(workspaceDir); + const storeLabel = resolveShortTermRecallStoreLabel(workspaceDir); const issues: ShortTermAuditIssue[] = []; const nowIso = new Date().toISOString(); const store = await readStore(workspaceDir, nowIso); @@ -1711,7 +1711,7 @@ export async function auditShortTermPromotionArtifacts(params: { } return { - storePath, + storeLabel, updatedAt, exists, entryCount, @@ -1778,9 +1778,9 @@ export async function repairShortTermPromotionArtifacts(params: { export async function removeGroundedShortTermCandidates(params: { workspaceDir: string; -}): Promise<{ removed: number; storePath: string }> { +}): Promise<{ removed: number; storeLabel: string }> { const workspaceDir = params.workspaceDir.trim(); - const storePath = resolveShortTermRecallStorePath(workspaceDir); + const storeLabel = resolveShortTermRecallStoreLabel(workspaceDir); const nowIso = new Date().toISOString(); let removed = 0; @@ -1817,7 +1817,7 @@ export async function removeGroundedShortTermCandidates(params: { } }); - return { removed, storePath }; + return { removed, storeLabel }; } export const __testing = { diff --git a/src/commands/doctor-memory-search.test.ts b/src/commands/doctor-memory-search.test.ts index 0c92eac2aa3..cf2c577b29c 100644 --- a/src/commands/doctor-memory-search.test.ts +++ b/src/commands/doctor-memory-search.test.ts @@ -101,7 +101,7 @@ import { detectLegacyWorkspaceDirs, formatRootMemoryFilesWarning } from "./docto function resetMemoryRecallMocks() { auditShortTermPromotionArtifacts.mockReset(); auditShortTermPromotionArtifacts.mockResolvedValue({ - storePath: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", + storeLabel: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", exists: true, entryCount: 1, promotedCount: 0, @@ -735,7 +735,7 @@ describe("memory recall doctor integration", () => { it("notes recall-store audit problems with doctor guidance", async () => { auditShortTermPromotionArtifacts.mockResolvedValueOnce({ - storePath: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", + storeLabel: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", exists: true, entryCount: 12, promotedCount: 4, @@ -767,7 +767,7 @@ describe("memory recall doctor integration", () => { it("runs memory recall repair during doctor --fix", async () => { auditShortTermPromotionArtifacts.mockResolvedValueOnce({ - storePath: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", + storeLabel: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", exists: true, entryCount: 12, promotedCount: 4, diff --git a/src/commands/doctor-memory-search.ts b/src/commands/doctor-memory-search.ts index f5f9b60ea9d..a5f3f67d1ae 100644 --- a/src/commands/doctor-memory-search.ts +++ b/src/commands/doctor-memory-search.ts @@ -173,7 +173,7 @@ function buildMemoryRecallIssueNote(audit: ShortTermAuditSummary): string | null return [ "Memory recall artifacts need attention:", ...issueLines, - `Recall store: ${audit.storePath}`, + `Recall store: ${audit.storeLabel}`, guidance, ].join("\n"); } diff --git a/src/gateway/server-methods/doctor.test.ts b/src/gateway/server-methods/doctor.test.ts index b9faa7cd21a..ffd0ebb4245 100644 --- a/src/gateway/server-methods/doctor.test.ts +++ b/src/gateway/server-methods/doctor.test.ts @@ -22,6 +22,7 @@ const writeBackfillDiaryEntries = vi.hoisted(() => vi.fn()); const removeBackfillDiaryEntries = vi.hoisted(() => vi.fn()); const removeGroundedShortTermCandidates = vi.hoisted(() => vi.fn()); const repairDreamingArtifacts = vi.hoisted(() => vi.fn()); +const readDreamingWorkspaceMap = vi.hoisted(() => vi.fn(async () => ({}))); vi.mock("../../config/config.js", () => ({ getRuntimeConfig, @@ -50,8 +51,21 @@ vi.mock("./doctor.memory-core-runtime.js", () => ({ repairDreamingArtifacts, })); +vi.mock("../../memory-host-sdk/dreaming-state-store.js", async () => { + const actual = await vi.importActual< + typeof import("../../memory-host-sdk/dreaming-state-store.js") + >("../../memory-host-sdk/dreaming-state-store.js"); + return { + ...actual, + readDreamingWorkspaceMap, + }; +}); + import { doctorHandlers } from "./doctor.js"; +const SHORT_TERM_RECALL_NAMESPACE = "dreaming.short-term-recall"; +const SHORT_TERM_PHASE_SIGNAL_NAMESPACE = "dreaming.phase-signals"; + const makeRuntimeContext = () => ({ getRuntimeConfig: () => getRuntimeConfig() }); const invokeDoctorMemoryStatus = async ( @@ -214,6 +228,7 @@ describe("doctor.memory.status", () => { removeBackfillDiaryEntries.mockReset(); removeGroundedShortTermCandidates.mockReset(); repairDreamingArtifacts.mockReset(); + readDreamingWorkspaceMap.mockReset().mockResolvedValue({}); }); it("returns gateway embedding probe status for the default agent", async () => { @@ -344,141 +359,77 @@ describe("doctor.memory.status", () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-status-")); const mainWorkspaceDir = path.join(workspaceRoot, "main"); const alphaWorkspaceDir = path.join(workspaceRoot, "alpha"); - const mainStorePath = path.join( - mainWorkspaceDir, - "memory", - ".dreams", - "short-term-recall.json", - ); - const alphaStorePath = path.join( - alphaWorkspaceDir, - "memory", - ".dreams", - "short-term-recall.json", - ); - const mainPhaseSignalPath = path.join( - mainWorkspaceDir, - "memory", - ".dreams", - "phase-signals.json", - ); - const alphaPhaseSignalPath = path.join( - alphaWorkspaceDir, - "memory", - ".dreams", - "phase-signals.json", - ); - await fs.mkdir(path.dirname(mainStorePath), { recursive: true }); - await fs.mkdir(path.dirname(alphaStorePath), { recursive: true }); - await fs.writeFile( - mainStorePath, - `${JSON.stringify( - { - version: 1, - updatedAt: recentIso, - entries: { - "memory:memory/2026-04-03.md:1:2": { - path: "memory/2026-04-03.md", - startLine: 1, - endLine: 2, - snippet: "Emma prefers shorter, lower-pressure check-ins.", - source: "memory", - recallCount: 2, - dailyCount: 1, - lastRecalledAt: recentIso, - promotedAt: undefined, - }, - "memory:memory/2026-04-02.md:1:2": { - path: "memory/2026-04-02.md", - startLine: 1, - endLine: 2, - snippet: "Use the Happy Together calendar for flights.", - source: "memory", - recallCount: 9, - dailyCount: 5, - promotedAt: recentIso, - }, + readDreamingWorkspaceMap.mockImplementation(async (namespace: string, workspaceDir: string) => { + if (namespace === SHORT_TERM_RECALL_NAMESPACE && workspaceDir === mainWorkspaceDir) { + return { + "memory:memory/2026-04-03.md:1:2": { + path: "memory/2026-04-03.md", + startLine: 1, + endLine: 2, + snippet: "Emma prefers shorter, lower-pressure check-ins.", + source: "memory", + recallCount: 2, + dailyCount: 1, + lastRecalledAt: recentIso, }, - }, - null, - 2, - )}\n`, - "utf-8", - ); - await fs.writeFile( - alphaStorePath, - `${JSON.stringify( - { - version: 1, - updatedAt: recentIso, - entries: { - "memory:memory/2026-04-01.md:1:2": { - path: "memory/2026-04-01.md", - startLine: 1, - endLine: 2, - snippet: "Bunji lives in London.", - source: "memory", - recallCount: 7, - dailyCount: 4, - promotedAt: olderIso, - }, - "memory:memory/2026-04-04.md:1:2": { - path: "memory/2026-04-04.md", - startLine: 1, - endLine: 2, - snippet: "Always book the covered valet option at Park & Greet BCN.", - source: "memory", - recallCount: 8, - dailyCount: 3, - promotedAt: recentIso, - }, + "memory:memory/2026-04-02.md:1:2": { + path: "memory/2026-04-02.md", + startLine: 1, + endLine: 2, + snippet: "Use the Happy Together calendar for flights.", + source: "memory", + recallCount: 9, + dailyCount: 5, + promotedAt: recentIso, }, - }, - null, - 2, - )}\n`, - "utf-8", - ); - await fs.writeFile( - mainPhaseSignalPath, - `${JSON.stringify( - { - version: 1, - updatedAt: recentIso, - entries: { - "memory:memory/2026-04-03.md:1:2": { - lightHits: 2, - remHits: 3, - }, - "memory:memory/2026-04-02.md:1:2": { - lightHits: 9, - remHits: 9, - }, + }; + } + if (namespace === SHORT_TERM_RECALL_NAMESPACE && workspaceDir === alphaWorkspaceDir) { + return { + "memory:memory/2026-04-01.md:1:2": { + path: "memory/2026-04-01.md", + startLine: 1, + endLine: 2, + snippet: "Bunji lives in London.", + source: "memory", + recallCount: 7, + dailyCount: 4, + promotedAt: olderIso, }, - }, - null, - 2, - )}\n`, - "utf-8", - ); - await fs.writeFile( - alphaPhaseSignalPath, - `${JSON.stringify( - { - version: 1, - updatedAt: recentIso, - entries: { - "memory:memory/2026-04-01.md:1:2": { - lightHits: 5, - remHits: 5, - }, + "memory:memory/2026-04-04.md:1:2": { + path: "memory/2026-04-04.md", + startLine: 1, + endLine: 2, + snippet: "Always book the covered valet option at Park & Greet BCN.", + source: "memory", + recallCount: 8, + dailyCount: 3, + promotedAt: recentIso, }, - }, - null, - 2, - )}\n`, - "utf-8", - ); + }; + } + if (namespace === SHORT_TERM_PHASE_SIGNAL_NAMESPACE && workspaceDir === mainWorkspaceDir) { + return { + "memory:memory/2026-04-03.md:1:2": { + lightHits: 2, + remHits: 3, + }, + "memory:memory/2026-04-02.md:1:2": { + lightHits: 9, + remHits: 9, + }, + }; + } + if (namespace === SHORT_TERM_PHASE_SIGNAL_NAMESPACE && workspaceDir === alphaWorkspaceDir) { + return { + "memory:memory/2026-04-01.md:1:2": { + lightHits: 5, + remHits: 5, + }, + }; + } + return {}; + }); getRuntimeConfig.mockReturnValue({ agents: { @@ -607,27 +558,18 @@ describe("doctor.memory.status", () => { it("falls back to the manager workspace when no configured dreaming workspaces resolve", async () => { const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-fallback-")); - const storePath = path.join(workspaceDir, "memory", ".dreams", "short-term-recall.json"); - await fs.mkdir(path.dirname(storePath), { recursive: true }); - await fs.writeFile( - storePath, - `${JSON.stringify( - { - version: 1, - updatedAt: "2026-04-04T00:00:00.000Z", - entries: { - "memory:memory/2026-04-03.md:1:2": { - path: "memory/2026-04-03.md", - source: "memory", - promotedAt: "2026-04-04T00:00:00.000Z", - }, - }, + readDreamingWorkspaceMap.mockImplementation(async (namespace: string, candidate: string) => { + if (namespace !== SHORT_TERM_RECALL_NAMESPACE || candidate !== workspaceDir) { + return {}; + } + return { + "memory:memory/2026-04-03.md:1:2": { + path: "memory/2026-04-03.md", + source: "memory", + promotedAt: "2026-04-04T00:00:00.000Z", }, - null, - 2, - )}\n`, - "utf-8", - ); + }; + }); resolveMemorySearchConfig.mockReturnValue(null); getRuntimeConfig.mockReturnValue({ plugins: { @@ -720,27 +662,12 @@ describe("doctor.memory.status", () => { const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-error-")); const mainWorkspaceDir = path.join(workspaceRoot, "main"); const alphaWorkspaceDir = path.join(workspaceRoot, "alpha"); - const alphaStorePath = path.join( - alphaWorkspaceDir, - "memory", - ".dreams", - "short-term-recall.json", - ); - await fs.mkdir(path.dirname(alphaStorePath), { recursive: true }); - await fs.writeFile( - alphaStorePath, - `${JSON.stringify( - { - version: 1, - updatedAt: "2026-04-04T00:00:00.000Z", - entries: {}, - }, - null, - 2, - )}\n`, - "utf-8", - ); - await fs.mkdir(path.join(mainWorkspaceDir, "memory", ".dreams"), { recursive: true }); + readDreamingWorkspaceMap.mockImplementation(async (namespace: string) => { + if (namespace === SHORT_TERM_RECALL_NAMESPACE) { + throw Object.assign(new Error("denied"), { code: "EACCES" }); + } + return {}; + }); getRuntimeConfig.mockReturnValue({ agents: { @@ -768,27 +695,6 @@ describe("doctor.memory.status", () => { agentId === "alpha" ? alphaWorkspaceDir : mainWorkspaceDir, ); - const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation(async (target, options) => { - const targetPath = - typeof target === "string" - ? target - : Buffer.isBuffer(target) - ? target.toString("utf-8") - : target instanceof URL - ? target.pathname - : ""; - if ( - targetPath === path.join(mainWorkspaceDir, "memory", ".dreams", "short-term-recall.json") || - targetPath === alphaStorePath - ) { - const error = Object.assign(new Error("denied"), { code: "EACCES" }); - throw error; - } - return await vi - .importActual("node:fs/promises") - .then((actual) => actual.readFile(target, options as never)); - }); - const close = vi.fn().mockResolvedValue(undefined); getMemorySearchManager.mockResolvedValue({ manager: { @@ -808,7 +714,6 @@ describe("doctor.memory.status", () => { storeError: "2 dreaming stores had read errors.", }); } finally { - readFileSpy.mockRestore(); await fs.rm(workspaceRoot, { recursive: true, force: true }); } }); @@ -819,7 +724,7 @@ describe("doctor.memory dream actions", () => { resolveAgentWorkspaceDir.mockReturnValue("/tmp/openclaw"); removeGroundedShortTermCandidates.mockResolvedValue({ removed: 3, - storePath: "/tmp/openclaw/memory/.dreams/short-term-recall.json", + storeLabel: "sqlite:plugin_state_entries/memory-core/dreaming.short-term-recall", }); const respond = vi.fn(); diff --git a/src/gateway/server-methods/doctor.ts b/src/gateway/server-methods/doctor.ts index ab66aa4fc08..3e58e4c0dfb 100644 --- a/src/gateway/server-methods/doctor.ts +++ b/src/gateway/server-methods/doctor.ts @@ -2,6 +2,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { + MEMORY_CORE_SHORT_TERM_PHASE_SIGNAL_NAMESPACE, + MEMORY_CORE_SHORT_TERM_RECALL_NAMESPACE, + readDreamingWorkspaceMap, +} from "../../memory-host-sdk/dreaming-state-store.js"; import { isSameMemoryDreamingDay, resolveMemoryDeepDreamingConfig, @@ -25,8 +30,6 @@ import { import { asRecord, normalizeTrimmedString } from "./record-shared.js"; import type { GatewayRequestHandlers } from "./types.js"; -const SHORT_TERM_STORE_RELATIVE_PATH = path.join("memory", ".dreams", "short-term-recall.json"); -const SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH = path.join("memory", ".dreams", "phase-signals.json"); const MANAGED_DEEP_SLEEP_CRON_NAME = "Memory Dreaming Promotion"; const MANAGED_DEEP_SLEEP_CRON_TAG = "[managed-by=memory-core.short-term-promotion]"; const DEEP_SLEEP_SYSTEM_EVENT_TEXT = "__openclaw_memory_core_short_term_promotion_dream__"; @@ -96,8 +99,8 @@ type DoctorMemoryDreamingPayload = { remPhaseHitCount: number; promotedTotal: number; promotedToday: number; - storePath?: string; - phaseSignalPath?: string; + storeLabel?: string; + phaseSignalLabel?: string; lastPromotedAt?: string; storeError?: string; phaseSignalError?: string; @@ -269,8 +272,8 @@ function resolveDreamingConfig( | "remPhaseHitCount" | "promotedTotal" | "promotedToday" - | "storePath" - | "phaseSignalPath" + | "storeLabel" + | "phaseSignalLabel" | "lastPromotedAt" | "storeError" | "phaseSignalError" @@ -371,8 +374,8 @@ type DreamingStoreStats = Pick< | "remPhaseHitCount" | "promotedTotal" | "promotedToday" - | "storePath" - | "phaseSignalPath" + | "storeLabel" + | "phaseSignalLabel" | "lastPromotedAt" | "storeError" | "phaseSignalError" @@ -480,18 +483,22 @@ function trimDreamingEntries( return selected; } +function resolveDreamingStoreLabel(namespace: string): string { + return `sqlite:plugin_state_entries/memory-core/${namespace}`; +} + async function loadDreamingStoreStats( workspaceDir: string, nowMs: number, timezone?: string, ): Promise { - const storePath = path.join(workspaceDir, SHORT_TERM_STORE_RELATIVE_PATH); - const phaseSignalPath = path.join(workspaceDir, SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH); + const storeLabel = resolveDreamingStoreLabel(MEMORY_CORE_SHORT_TERM_RECALL_NAMESPACE); + const phaseSignalLabel = resolveDreamingStoreLabel(MEMORY_CORE_SHORT_TERM_PHASE_SIGNAL_NAMESPACE); try { - const raw = await fs.readFile(storePath, "utf-8"); - const parsed = JSON.parse(raw) as unknown; - const store = asRecord(parsed); - const entries = asRecord(store?.entries) ?? {}; + const entries = await readDreamingWorkspaceMap( + MEMORY_CORE_SHORT_TERM_RECALL_NAMESPACE, + workspaceDir, + ); let shortTermCount = 0; let recallSignalCount = 0; let dailySignalCount = 0; @@ -574,10 +581,10 @@ async function loadDreamingStoreStats( let phaseSignalError: string | undefined; try { - const phaseRaw = await fs.readFile(phaseSignalPath, "utf-8"); - const parsedPhase = JSON.parse(phaseRaw) as unknown; - const phaseStore = asRecord(parsedPhase); - const phaseEntries = asRecord(phaseStore?.entries) ?? {}; + const phaseEntries = await readDreamingWorkspaceMap( + MEMORY_CORE_SHORT_TERM_PHASE_SIGNAL_NAMESPACE, + workspaceDir, + ); for (const [key, value] of Object.entries(phaseEntries)) { if (!activeKeys.has(key)) { continue; @@ -613,8 +620,8 @@ async function loadDreamingStoreStats( remPhaseHitCount, promotedTotal, promotedToday, - storePath, - phaseSignalPath, + storeLabel, + phaseSignalLabel, shortTermEntries: trimDreamingEntries(shortTermEntries, compareDreamingEntryByRecency), signalEntries: trimDreamingEntries(shortTermEntries, compareDreamingEntryBySignals), promotedEntries: trimDreamingEntries(promotedEntries, compareDreamingEntryByPromotion), @@ -622,26 +629,6 @@ async function loadDreamingStoreStats( ...(phaseSignalError ? { phaseSignalError } : {}), }; } catch (err) { - const code = (err as NodeJS.ErrnoException | undefined)?.code; - if (code === "ENOENT") { - return { - shortTermCount: 0, - recallSignalCount: 0, - dailySignalCount: 0, - groundedSignalCount: 0, - totalSignalCount: 0, - phaseSignalCount: 0, - lightPhaseHitCount: 0, - remPhaseHitCount: 0, - promotedTotal: 0, - promotedToday: 0, - storePath, - phaseSignalPath, - shortTermEntries: [], - signalEntries: [], - promotedEntries: [], - }; - } return { shortTermCount: 0, recallSignalCount: 0, @@ -653,8 +640,8 @@ async function loadDreamingStoreStats( remPhaseHitCount: 0, promotedTotal: 0, promotedToday: 0, - storePath, - phaseSignalPath, + storeLabel, + phaseSignalLabel, shortTermEntries: [], signalEntries: [], promotedEntries: [], @@ -676,8 +663,8 @@ function mergeDreamingStoreStats(stats: DreamingStoreStats[]): DreamingStoreStat let promotedToday = 0; let latestPromotedAtMs = Number.NEGATIVE_INFINITY; let lastPromotedAt: string | undefined; - const storePaths = new Set(); - const phaseSignalPaths = new Set(); + const storeLabels = new Set(); + const phaseSignalLabels = new Set(); const storeErrors: string[] = []; const phaseSignalErrors: string[] = []; const shortTermEntries: DoctorMemoryDreamingEntryPayload[] = []; @@ -695,11 +682,11 @@ function mergeDreamingStoreStats(stats: DreamingStoreStats[]): DreamingStoreStat remPhaseHitCount += stat.remPhaseHitCount; promotedTotal += stat.promotedTotal; promotedToday += stat.promotedToday; - if (stat.storePath) { - storePaths.add(stat.storePath); + if (stat.storeLabel) { + storeLabels.add(stat.storeLabel); } - if (stat.phaseSignalPath) { - phaseSignalPaths.add(stat.phaseSignalPath); + if (stat.phaseSignalLabel) { + phaseSignalLabels.add(stat.phaseSignalLabel); } if (stat.storeError) { storeErrors.push(stat.storeError); @@ -731,8 +718,8 @@ function mergeDreamingStoreStats(stats: DreamingStoreStats[]): DreamingStoreStat shortTermEntries: trimDreamingEntries(shortTermEntries, compareDreamingEntryByRecency), signalEntries: trimDreamingEntries(signalEntries, compareDreamingEntryBySignals), promotedEntries: trimDreamingEntries(promotedEntries, compareDreamingEntryByPromotion), - ...(storePaths.size === 1 ? { storePath: [...storePaths][0] } : {}), - ...(phaseSignalPaths.size === 1 ? { phaseSignalPath: [...phaseSignalPaths][0] } : {}), + ...(storeLabels.size === 1 ? { storeLabel: [...storeLabels][0] } : {}), + ...(phaseSignalLabels.size === 1 ? { phaseSignalLabel: [...phaseSignalLabels][0] } : {}), ...(lastPromotedAt ? { lastPromotedAt } : {}), ...(storeErrors.length === 1 ? { storeError: storeErrors[0] } diff --git a/src/plugin-sdk/memory-core-bundled-runtime.ts b/src/plugin-sdk/memory-core-bundled-runtime.ts index 17688184bb8..40b9c598334 100644 --- a/src/plugin-sdk/memory-core-bundled-runtime.ts +++ b/src/plugin-sdk/memory-core-bundled-runtime.ts @@ -28,7 +28,7 @@ type RuntimeFacadeModule = { }) => void; removeGroundedShortTermCandidates: (params: { workspaceDir: string; - }) => Promise<{ removed: number; storePath: string }>; + }) => Promise<{ removed: number; storeLabel: string }>; repairDreamingArtifacts: (params: { workspaceDir: string; archiveDiary?: boolean; diff --git a/src/plugin-sdk/memory-core-engine-runtime.ts b/src/plugin-sdk/memory-core-engine-runtime.ts index 03a9fbada11..b6082610e94 100644 --- a/src/plugin-sdk/memory-core-engine-runtime.ts +++ b/src/plugin-sdk/memory-core-engine-runtime.ts @@ -63,7 +63,7 @@ export type ShortTermAuditIssue = { }; export type ShortTermAuditSummary = { - storePath: string; + storeLabel: string; updatedAt?: string; exists: boolean; entryCount: number;