fix(memory-core): align dreaming promotion

This commit is contained in:
Peter Steinberger
2026-04-05 15:45:54 +01:00
parent 40ffada812
commit f7670bde7e
14 changed files with 1359 additions and 361 deletions

View File

@@ -6,6 +6,12 @@ import type { OpenClawConfig } from "../../config/config.js";
const loadConfig = vi.hoisted(() => vi.fn(() => ({}) as OpenClawConfig));
const resolveDefaultAgentId = vi.hoisted(() => vi.fn(() => "main"));
const resolveAgentWorkspaceDir = vi.hoisted(() =>
vi.fn((_cfg: OpenClawConfig, _agentId: string) => "/tmp/openclaw"),
);
const resolveMemorySearchConfig = vi.hoisted(() =>
vi.fn((_cfg: OpenClawConfig, _agentId: string) => ({ enabled: true })),
);
const getMemorySearchManager = vi.hoisted(() => vi.fn());
vi.mock("../../config/config.js", () => ({
@@ -14,6 +20,11 @@ vi.mock("../../config/config.js", () => ({
vi.mock("../../agents/agent-scope.js", () => ({
resolveDefaultAgentId,
resolveAgentWorkspaceDir,
}));
vi.mock("../../agents/memory-search.js", () => ({
resolveMemorySearchConfig,
}));
vi.mock("../../plugins/memory-runtime.js", () => ({
@@ -63,6 +74,8 @@ describe("doctor.memory.status", () => {
beforeEach(() => {
loadConfig.mockClear();
resolveDefaultAgentId.mockClear();
resolveAgentWorkspaceDir.mockReset().mockReturnValue("/tmp/openclaw");
resolveMemorySearchConfig.mockReset().mockReturnValue({ enabled: true });
getMemorySearchManager.mockReset();
});
@@ -134,18 +147,34 @@ describe("doctor.memory.status", () => {
});
it("includes dreaming counts and managed cron status when workspace data is available", async () => {
const now = Date.now();
const todayIso = new Date(now).toISOString();
const earlierIso = new Date(now - 2 * 24 * 60 * 60 * 1000).toISOString();
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "doctor-memory-status-"));
const storePath = path.join(workspaceDir, "memory", ".dreams", "short-term-recall.json");
await fs.mkdir(path.dirname(storePath), { recursive: true });
const now = Date.parse("2026-04-05T00:30:00.000Z");
vi.useFakeTimers();
vi.setSystemTime(now);
const recentIso = "2026-04-04T23:45:00.000Z";
const olderIso = "2026-04-02T10:00:00.000Z";
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",
);
await fs.mkdir(path.dirname(mainStorePath), { recursive: true });
await fs.mkdir(path.dirname(alphaStorePath), { recursive: true });
await fs.writeFile(
storePath,
mainStorePath,
`${JSON.stringify(
{
version: 1,
updatedAt: todayIso,
updatedAt: recentIso,
entries: {
"memory:memory/2026-04-03.md:1:2": {
path: "memory/2026-04-03.md",
@@ -155,16 +184,31 @@ describe("doctor.memory.status", () => {
"memory:memory/2026-04-02.md:1:2": {
path: "memory/2026-04-02.md",
source: "memory",
promotedAt: todayIso,
promotedAt: 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",
source: "memory",
promotedAt: earlierIso,
promotedAt: olderIso,
},
"memory:MEMORY.md:1:2": {
path: "MEMORY.md",
"memory:memory/2026-04-04.md:1:2": {
path: "memory/2026-04-04.md",
source: "memory",
promotedAt: recentIso,
},
},
},
@@ -175,24 +219,42 @@ describe("doctor.memory.status", () => {
);
loadConfig.mockReturnValue({
agents: {
defaults: {
userTimezone: "America/Los_Angeles",
memorySearch: {
enabled: true,
},
},
list: [
{ id: "main", workspace: mainWorkspaceDir },
{ id: "alpha", workspace: alphaWorkspaceDir },
],
},
plugins: {
entries: {
"memory-core": {
config: {
dreaming: {
mode: "rem",
frequency: "0 */4 * * *",
cron: "0 */4 * * *",
},
},
},
},
},
} as OpenClawConfig);
resolveAgentWorkspaceDir.mockImplementation((cfg: OpenClawConfig, agentId: string) => {
if (agentId === "alpha") {
return alphaWorkspaceDir;
}
return mainWorkspaceDir;
});
const close = vi.fn().mockResolvedValue(undefined);
getMemorySearchManager.mockResolvedValue({
manager: {
status: () => ({ provider: "gemini", workspaceDir }),
status: () => ({ provider: "gemini", workspaceDir: mainWorkspaceDir }),
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
close,
},
@@ -224,9 +286,10 @@ describe("doctor.memory.status", () => {
mode: "rem",
enabled: true,
frequency: "0 */4 * * *",
timezone: "America/Los_Angeles",
shortTermCount: 1,
promotedTotal: 2,
promotedToday: 1,
promotedTotal: 3,
promotedToday: 2,
managedCronPresent: true,
nextRunAtMs: now + 60_000,
}),
@@ -234,8 +297,180 @@ describe("doctor.memory.status", () => {
undefined,
);
expect(close).toHaveBeenCalled();
} finally {
vi.useRealTimers();
await fs.rm(workspaceRoot, { recursive: true, force: true });
}
});
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",
},
},
},
null,
2,
)}\n`,
"utf-8",
);
resolveMemorySearchConfig.mockReturnValue(null);
loadConfig.mockReturnValue({
plugins: {
entries: {
"memory-core": {
config: {
dreaming: {
mode: "core",
},
},
},
},
},
} as OpenClawConfig);
const close = vi.fn().mockResolvedValue(undefined);
getMemorySearchManager.mockResolvedValue({
manager: {
status: () => ({ provider: "gemini", workspaceDir }),
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
close,
},
});
const respond = vi.fn();
try {
await invokeDoctorMemoryStatus(respond);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
dreaming: expect.objectContaining({
shortTermCount: 0,
promotedTotal: 1,
managedCronPresent: false,
storePath,
}),
}),
undefined,
);
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("merges workspace store errors when multiple workspace stores are unreadable", async () => {
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 });
loadConfig.mockReturnValue({
agents: {
defaults: {
memorySearch: {
enabled: true,
},
},
list: [
{ id: "main", workspace: mainWorkspaceDir },
{ id: "alpha", workspace: alphaWorkspaceDir },
],
},
plugins: {
entries: {
"memory-core": {
config: {
dreaming: {
mode: "core",
},
},
},
},
},
} as OpenClawConfig);
resolveAgentWorkspaceDir.mockImplementation((_cfg: OpenClawConfig, agentId: string) =>
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<typeof import("node:fs/promises")>("node:fs/promises")
.then((actual) => actual.readFile(target, options as never));
});
const close = vi.fn().mockResolvedValue(undefined);
getMemorySearchManager.mockResolvedValue({
manager: {
status: () => ({ provider: "gemini", workspaceDir: mainWorkspaceDir }),
probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }),
close,
},
});
const respond = vi.fn();
try {
await invokeDoctorMemoryStatus(respond);
expect(respond).toHaveBeenCalledWith(
true,
expect.objectContaining({
dreaming: expect.objectContaining({
shortTermCount: 0,
promotedTotal: 0,
storeError: "2 dreaming stores had read errors.",
}),
}),
undefined,
);
} finally {
readFileSpy.mockRestore();
await fs.rm(workspaceRoot, { recursive: true, force: true });
}
});
});

View File

@@ -2,55 +2,25 @@ import fs from "node:fs/promises";
import path from "node:path";
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { loadConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
isSameMemoryDreamingDay,
resolveMemoryCorePluginConfig,
resolveMemoryDreamingConfig,
resolveMemoryDreamingWorkspaces,
type MemoryDreamingMode,
} from "../../memory-host-sdk/dreaming.js";
import { getActiveMemorySearchManager } from "../../plugins/memory-runtime.js";
import { formatError } from "../server-utils.js";
import type { GatewayRequestHandlers } from "./types.js";
const SHORT_TERM_STORE_RELATIVE_PATH = path.join("memory", ".dreams", "short-term-recall.json");
const SHORT_TERM_PATH_RE = /(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})\.md$/;
const SHORT_TERM_BASENAME_RE = /^(\d{4})-(\d{2})-(\d{2})\.md$/;
const MANAGED_DREAMING_CRON_NAME = "Memory Dreaming Promotion";
const MANAGED_DREAMING_CRON_TAG = "[managed-by=memory-core.short-term-promotion]";
const DREAMING_SYSTEM_EVENT_TEXT = "__openclaw_memory_core_short_term_promotion_dream__";
type DreamingMode = "off" | "core" | "rem" | "deep";
type DreamingPreset = Exclude<DreamingMode, "off">;
const DREAMING_PRESET_DEFAULTS: Record<
DreamingPreset,
{
frequency: string;
limit: number;
minScore: number;
minRecallCount: number;
minUniqueQueries: number;
}
> = {
core: {
frequency: "0 3 * * *",
limit: 10,
minScore: 0.75,
minRecallCount: 3,
minUniqueQueries: 2,
},
deep: {
frequency: "0 */12 * * *",
limit: 10,
minScore: 0.8,
minRecallCount: 3,
minUniqueQueries: 3,
},
rem: {
frequency: "0 */6 * * *",
limit: 10,
minScore: 0.85,
minRecallCount: 4,
minUniqueQueries: 3,
},
};
type DoctorMemoryDreamingPayload = {
mode: DreamingMode;
mode: MemoryDreamingMode;
enabled: boolean;
frequency: string;
timezone?: string;
@@ -93,39 +63,8 @@ function normalizeTrimmedString(value: unknown): string | undefined {
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeDreamingMode(value: unknown): DreamingMode {
const normalized = normalizeTrimmedString(value)?.toLowerCase();
if (
normalized === "off" ||
normalized === "core" ||
normalized === "rem" ||
normalized === "deep"
) {
return normalized;
}
return "off";
}
function normalizeNonNegativeInt(value: unknown, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return fallback;
}
const floored = Math.floor(value);
return floored < 0 ? fallback : floored;
}
function normalizeScore(value: unknown, fallback: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return fallback;
}
if (value < 0 || value > 1) {
return fallback;
}
return value;
}
function resolveDreamingConfig(
cfg: Record<string, unknown>,
cfg: OpenClawConfig,
): Omit<
DoctorMemoryDreamingPayload,
| "shortTermCount"
@@ -137,27 +76,19 @@ function resolveDreamingConfig(
| "managedCronPresent"
| "storeError"
> {
const plugins = asRecord(cfg.plugins);
const entries = asRecord(plugins?.entries);
const memoryCore = asRecord(entries?.["memory-core"]);
const pluginConfig = asRecord(memoryCore?.config);
const dreaming = asRecord(pluginConfig?.dreaming);
const mode = normalizeDreamingMode(dreaming?.mode);
const preset: DreamingPreset = mode === "off" ? "core" : mode;
const defaults = DREAMING_PRESET_DEFAULTS[preset];
const resolved = resolveMemoryDreamingConfig({
pluginConfig: resolveMemoryCorePluginConfig(cfg),
cfg,
});
return {
mode,
enabled: mode !== "off",
frequency: normalizeTrimmedString(dreaming?.frequency) ?? defaults.frequency,
timezone: normalizeTrimmedString(dreaming?.timezone),
limit: normalizeNonNegativeInt(dreaming?.limit, defaults.limit),
minScore: normalizeScore(dreaming?.minScore, defaults.minScore),
minRecallCount: normalizeNonNegativeInt(dreaming?.minRecallCount, defaults.minRecallCount),
minUniqueQueries: normalizeNonNegativeInt(
dreaming?.minUniqueQueries,
defaults.minUniqueQueries,
),
mode: resolved.mode,
enabled: resolved.enabled,
frequency: resolved.cron,
...(resolved.timezone ? { timezone: resolved.timezone } : {}),
limit: resolved.limit,
minScore: resolved.minScore,
minRecallCount: resolved.minRecallCount,
minUniqueQueries: resolved.minUniqueQueries,
};
}
@@ -167,20 +98,10 @@ function normalizeMemoryPath(rawPath: string): string {
function isShortTermMemoryPath(filePath: string): boolean {
const normalized = normalizeMemoryPath(filePath);
if (SHORT_TERM_PATH_RE.test(normalized)) {
if (/(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})\.md$/.test(normalized)) {
return true;
}
return SHORT_TERM_BASENAME_RE.test(normalized);
}
function isSameLocalDay(firstEpochMs: number, secondEpochMs: number): boolean {
const first = new Date(firstEpochMs);
const second = new Date(secondEpochMs);
return (
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate()
);
return /^(\d{4})-(\d{2})-(\d{2})\.md$/.test(normalized);
}
type DreamingStoreStats = Pick<
@@ -196,6 +117,7 @@ type DreamingStoreStats = Pick<
async function loadDreamingStoreStats(
workspaceDir: string,
nowMs: number,
timezone?: string,
): Promise<DreamingStoreStats> {
const storePath = path.join(workspaceDir, SHORT_TERM_STORE_RELATIVE_PATH);
try {
@@ -226,7 +148,7 @@ async function loadDreamingStoreStats(
}
promotedTotal += 1;
const promotedAtMs = Date.parse(promotedAt);
if (Number.isFinite(promotedAtMs) && isSameLocalDay(promotedAtMs, nowMs)) {
if (Number.isFinite(promotedAtMs) && isSameMemoryDreamingDay(promotedAtMs, nowMs, timezone)) {
promotedToday += 1;
}
if (Number.isFinite(promotedAtMs) && promotedAtMs > latestPromotedAtMs) {
@@ -262,6 +184,46 @@ async function loadDreamingStoreStats(
}
}
function mergeDreamingStoreStats(stats: DreamingStoreStats[]): DreamingStoreStats {
let shortTermCount = 0;
let promotedTotal = 0;
let promotedToday = 0;
let latestPromotedAtMs = Number.NEGATIVE_INFINITY;
let lastPromotedAt: string | undefined;
const storePaths = new Set<string>();
const storeErrors: string[] = [];
for (const stat of stats) {
shortTermCount += stat.shortTermCount;
promotedTotal += stat.promotedTotal;
promotedToday += stat.promotedToday;
if (stat.storePath) {
storePaths.add(stat.storePath);
}
if (stat.storeError) {
storeErrors.push(stat.storeError);
}
const promotedAtMs = stat.lastPromotedAt ? Date.parse(stat.lastPromotedAt) : Number.NaN;
if (Number.isFinite(promotedAtMs) && promotedAtMs > latestPromotedAtMs) {
latestPromotedAtMs = promotedAtMs;
lastPromotedAt = stat.lastPromotedAt;
}
}
return {
shortTermCount,
promotedTotal,
promotedToday,
...(storePaths.size === 1 ? { storePath: [...storePaths][0] } : {}),
...(lastPromotedAt ? { lastPromotedAt } : {}),
...(storeErrors.length === 1
? { storeError: storeErrors[0] }
: storeErrors.length > 1
? { storeError: `${storeErrors.length} dreaming stores had read errors.` }
: {}),
};
}
type ManagedDreamingCronStatus = {
managedCronPresent: boolean;
nextRunAtMs?: number;
@@ -351,15 +313,27 @@ export const doctorHandlers: GatewayRequestHandlers = {
embedding = { ok: false, error: "memory embeddings unavailable" };
}
const nowMs = Date.now();
const dreamingConfig = resolveDreamingConfig(cfg as Record<string, unknown>);
const dreamingConfig = resolveDreamingConfig(cfg);
const workspaceDir = normalizeTrimmedString((status as Record<string, unknown>).workspaceDir);
const storeStats = workspaceDir
? await loadDreamingStoreStats(workspaceDir, nowMs)
: {
shortTermCount: 0,
promotedTotal: 0,
promotedToday: 0,
};
const configuredWorkspaces = resolveMemoryDreamingWorkspaces(cfg).map(
(entry) => entry.workspaceDir,
);
const allWorkspaces =
configuredWorkspaces.length > 0 ? configuredWorkspaces : workspaceDir ? [workspaceDir] : [];
const storeStats =
allWorkspaces.length > 0
? mergeDreamingStoreStats(
await Promise.all(
allWorkspaces.map((entry) =>
loadDreamingStoreStats(entry, nowMs, dreamingConfig.timezone),
),
),
)
: {
shortTermCount: 0,
promotedTotal: 0,
promotedToday: 0,
};
const cronStatus = await resolveManagedDreamingCronStatus(context);
const payload: DoctorMemoryStatusPayload = {
agentId,

View File

@@ -0,0 +1,269 @@
import path from "node:path";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
import type { OpenClawConfig } from "../config/config.js";
export const DEFAULT_MEMORY_DREAMING_CRON_EXPR = "0 3 * * *";
export const DEFAULT_MEMORY_DREAMING_LIMIT = 10;
export const DEFAULT_MEMORY_DREAMING_MIN_SCORE = 0.75;
export const DEFAULT_MEMORY_DREAMING_MIN_RECALL_COUNT = 3;
export const DEFAULT_MEMORY_DREAMING_MIN_UNIQUE_QUERIES = 2;
export const DEFAULT_MEMORY_DREAMING_MODE = "off";
export const DEFAULT_MEMORY_DREAMING_PRESET = "core";
export type MemoryDreamingPreset = "core" | "deep" | "rem";
export type MemoryDreamingMode = MemoryDreamingPreset | "off";
export type MemoryDreamingConfig = {
mode: MemoryDreamingMode;
enabled: boolean;
cron: string;
timezone?: string;
limit: number;
minScore: number;
minRecallCount: number;
minUniqueQueries: number;
verboseLogging: boolean;
};
export type MemoryDreamingWorkspace = {
workspaceDir: string;
agentIds: string[];
};
export const MEMORY_DREAMING_PRESET_DEFAULTS: Record<
MemoryDreamingPreset,
{
cron: string;
limit: number;
minScore: number;
minRecallCount: number;
minUniqueQueries: number;
}
> = {
core: {
cron: DEFAULT_MEMORY_DREAMING_CRON_EXPR,
limit: DEFAULT_MEMORY_DREAMING_LIMIT,
minScore: DEFAULT_MEMORY_DREAMING_MIN_SCORE,
minRecallCount: DEFAULT_MEMORY_DREAMING_MIN_RECALL_COUNT,
minUniqueQueries: DEFAULT_MEMORY_DREAMING_MIN_UNIQUE_QUERIES,
},
deep: {
cron: "0 */12 * * *",
limit: DEFAULT_MEMORY_DREAMING_LIMIT,
minScore: 0.8,
minRecallCount: 3,
minUniqueQueries: 3,
},
rem: {
cron: "0 */6 * * *",
limit: DEFAULT_MEMORY_DREAMING_LIMIT,
minScore: 0.85,
minRecallCount: 4,
minUniqueQueries: 3,
},
};
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function normalizeTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeNonNegativeInt(value: unknown, fallback: number): number {
if (typeof value === "string" && value.trim().length === 0) {
return fallback;
}
const num = typeof value === "string" ? Number(value.trim()) : Number(value);
if (!Number.isFinite(num)) {
return fallback;
}
const floored = Math.floor(num);
if (floored < 0) {
return fallback;
}
return floored;
}
function normalizeScore(value: unknown, fallback: number): number {
if (typeof value === "string" && value.trim().length === 0) {
return fallback;
}
const num = typeof value === "string" ? Number(value.trim()) : Number(value);
if (!Number.isFinite(num) || num < 0 || num > 1) {
return fallback;
}
return num;
}
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "true") {
return true;
}
if (normalized === "false") {
return false;
}
}
return fallback;
}
function normalizePathForComparison(input: string): string {
const normalized = path.resolve(input);
if (process.platform === "win32") {
return normalized.toLowerCase();
}
return normalized;
}
function formatLocalIsoDay(epochMs: number): string {
const date = new Date(epochMs);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
export function normalizeMemoryDreamingMode(value: unknown): MemoryDreamingMode {
const normalized = normalizeTrimmedString(value)?.toLowerCase();
if (
normalized === "off" ||
normalized === "core" ||
normalized === "deep" ||
normalized === "rem"
) {
return normalized;
}
return DEFAULT_MEMORY_DREAMING_MODE;
}
export function resolveMemoryCorePluginConfig(
cfg: OpenClawConfig | Record<string, unknown> | undefined,
): Record<string, unknown> | undefined {
const root = asRecord(cfg);
const plugins = asRecord(root?.plugins);
const entries = asRecord(plugins?.entries);
const memoryCore = asRecord(entries?.["memory-core"]);
return asRecord(memoryCore?.config) ?? undefined;
}
export function resolveMemoryDreamingConfig(params: {
pluginConfig?: Record<string, unknown>;
cfg?: OpenClawConfig;
}): MemoryDreamingConfig {
const dreaming = asRecord(params.pluginConfig?.dreaming);
const mode = normalizeMemoryDreamingMode(dreaming?.mode);
const enabled = mode !== "off";
const preset: MemoryDreamingPreset = mode === "off" ? DEFAULT_MEMORY_DREAMING_PRESET : mode;
const defaults = MEMORY_DREAMING_PRESET_DEFAULTS[preset];
const timezone =
normalizeTrimmedString(dreaming?.timezone) ??
normalizeTrimmedString(params.cfg?.agents?.defaults?.userTimezone);
return {
mode,
enabled,
cron:
normalizeTrimmedString(dreaming?.cron) ??
normalizeTrimmedString(dreaming?.frequency) ??
defaults.cron,
...(timezone ? { timezone } : {}),
limit: normalizeNonNegativeInt(dreaming?.limit, defaults.limit),
minScore: normalizeScore(dreaming?.minScore, defaults.minScore),
minRecallCount: normalizeNonNegativeInt(dreaming?.minRecallCount, defaults.minRecallCount),
minUniqueQueries: normalizeNonNegativeInt(
dreaming?.minUniqueQueries,
defaults.minUniqueQueries,
),
verboseLogging: normalizeBoolean(dreaming?.verboseLogging, false),
};
}
export function formatMemoryDreamingDay(epochMs: number, timezone?: string): string {
if (!timezone) {
return formatLocalIsoDay(epochMs);
}
try {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(new Date(epochMs));
const values = new Map(parts.map((part) => [part.type, part.value]));
const year = values.get("year");
const month = values.get("month");
const day = values.get("day");
if (year && month && day) {
return `${year}-${month}-${day}`;
}
} catch {
// Fall back to host-local day for invalid or unsupported timezones.
}
return formatLocalIsoDay(epochMs);
}
export function isSameMemoryDreamingDay(
firstEpochMs: number,
secondEpochMs: number,
timezone?: string,
): boolean {
return (
formatMemoryDreamingDay(firstEpochMs, timezone) ===
formatMemoryDreamingDay(secondEpochMs, timezone)
);
}
export function resolveMemoryDreamingWorkspaces(cfg: OpenClawConfig): MemoryDreamingWorkspace[] {
const configured = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
const agentIds: string[] = [];
const seenAgents = new Set<string>();
for (const entry of configured) {
if (!entry || typeof entry !== "object" || typeof entry.id !== "string") {
continue;
}
const id = entry.id.trim().toLowerCase();
if (!id || seenAgents.has(id)) {
continue;
}
seenAgents.add(id);
agentIds.push(id);
}
if (agentIds.length === 0) {
agentIds.push(resolveDefaultAgentId(cfg));
}
const byWorkspace = new Map<string, MemoryDreamingWorkspace>();
for (const agentId of agentIds) {
if (!resolveMemorySearchConfig(cfg, agentId)) {
continue;
}
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId)?.trim();
if (!workspaceDir) {
continue;
}
const key = normalizePathForComparison(workspaceDir);
const existing = byWorkspace.get(key);
if (existing) {
existing.agentIds.push(agentId);
continue;
}
byWorkspace.set(key, {
workspaceDir,
agentIds: [agentId],
});
}
return [...byWorkspace.values()];
}

View File

@@ -1 +1,2 @@
export * from "../memory-host-sdk/status.js";
export * from "../memory-host-sdk/dreaming.js";

View File

@@ -46,6 +46,15 @@ export {
withProgress,
withProgressTotals,
} from "./memory-core-host-runtime-cli.js";
export {
formatMemoryDreamingDay,
isSameMemoryDreamingDay,
MEMORY_DREAMING_PRESET_DEFAULTS,
normalizeMemoryDreamingMode,
resolveMemoryCorePluginConfig,
resolveMemoryDreamingConfig,
resolveMemoryDreamingWorkspaces,
} from "./memory-core-host-status.js";
export {
listMemoryFiles,
normalizeExtraMemoryPaths,