refactor(memory-core): rename sleep surface back to dreaming

This commit is contained in:
Vincent Koc
2026-04-05 18:13:49 +01:00
parent 848cc5e0ce
commit 8ff41a6bc4
27 changed files with 258 additions and 2251 deletions

View File

@@ -1,6 +1,7 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { registerMemoryCli } from "./src/cli.js";
import { registerSleepCommand } from "./src/dreaming-command.js";
import { registerDreamingCommand } from "./src/dreaming-command.js";
import { registerMemoryDreamingPhases } from "./src/dreaming-phases.js";
import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
import {
buildMemoryFlushPlan,
@@ -11,7 +12,6 @@ import {
import { registerBuiltInMemoryEmbeddingProviders } from "./src/memory/provider-adapters.js";
import { buildPromptSection } from "./src/prompt-section.js";
import { memoryRuntime } from "./src/runtime-provider.js";
import { registerMemorySleepPhases } from "./src/sleep.js";
import { createMemoryGetTool, createMemorySearchTool } from "./src/tools.js";
export {
buildMemoryFlushPlan,
@@ -29,8 +29,8 @@ export default definePluginEntry({
register(api) {
registerBuiltInMemoryEmbeddingProviders(api);
registerShortTermPromotionDreaming(api);
registerMemorySleepPhases(api);
registerSleepCommand(api);
registerMemoryDreamingPhases(api);
registerDreamingCommand(api);
api.registerMemoryPromptSection(buildPromptSection);
api.registerMemoryFlushPlan(buildMemoryFlushPlan);
api.registerMemoryRuntime(memoryRuntime);

View File

@@ -2,82 +2,82 @@
"id": "memory-core",
"kind": "memory",
"uiHints": {
"sleep.timezone": {
"label": "Sleep Timezone",
"dreaming.timezone": {
"label": "Dreaming Timezone",
"placeholder": "America/Los_Angeles",
"help": "IANA timezone used for sleep schedules and day bucketing."
"help": "IANA timezone used for dreaming schedules and day bucketing."
},
"sleep.verboseLogging": {
"label": "Sleep Verbose Logging",
"dreaming.verboseLogging": {
"label": "Dreaming Verbose Logging",
"placeholder": "false",
"help": "Emit detailed per-run sleep logs for phase scheduling, ranking, and writes."
"help": "Emit detailed per-run dreaming logs for phase scheduling, ranking, and writes."
},
"sleep.storage.mode": {
"label": "Sleep Storage Mode",
"dreaming.storage.mode": {
"label": "Dreaming Storage Mode",
"placeholder": "inline",
"help": "Write inline to MEMORY.md and daily notes, to separate reports, or both."
},
"sleep.phases.light.cron": {
"dreaming.phases.light.cron": {
"label": "Light Sleep Cron",
"placeholder": "0 3 * * *",
"help": "Cron cadence for light sleep sorting runs."
},
"sleep.phases.light.lookbackDays": {
"dreaming.phases.light.lookbackDays": {
"label": "Light Sleep Lookback Days",
"placeholder": "2",
"help": "How many prior days light sleep may inspect."
},
"sleep.phases.light.limit": {
"dreaming.phases.light.limit": {
"label": "Light Sleep Limit",
"placeholder": "100",
"help": "Maximum staged light-sleep candidates per run."
},
"sleep.phases.deep.cron": {
"dreaming.phases.deep.cron": {
"label": "Deep Sleep Cron",
"placeholder": "0 3 * * *",
"help": "Cron cadence for deep sleep promotion and recovery runs."
},
"sleep.phases.deep.limit": {
"dreaming.phases.deep.limit": {
"label": "Deep Sleep Limit",
"placeholder": "10",
"help": "Maximum candidates promoted into MEMORY.md per deep-sleep run."
},
"sleep.phases.deep.minScore": {
"dreaming.phases.deep.minScore": {
"label": "Deep Sleep Min Score",
"placeholder": "0.8",
"help": "Minimum weighted rank required for durable promotion."
},
"sleep.phases.deep.minRecallCount": {
"dreaming.phases.deep.minRecallCount": {
"label": "Deep Sleep Min Recalls",
"placeholder": "3",
"help": "Minimum recall count required for durable promotion."
},
"sleep.phases.deep.minUniqueQueries": {
"dreaming.phases.deep.minUniqueQueries": {
"label": "Deep Sleep Min Queries",
"placeholder": "3",
"help": "Minimum unique query count required for durable promotion."
},
"sleep.phases.deep.recencyHalfLifeDays": {
"dreaming.phases.deep.recencyHalfLifeDays": {
"label": "Deep Sleep Recency Half-Life Days",
"placeholder": "14",
"help": "Days for the recency score to decay by half during deep-sleep ranking."
},
"sleep.phases.deep.maxAgeDays": {
"dreaming.phases.deep.maxAgeDays": {
"label": "Deep Sleep Max Age Days",
"placeholder": "30",
"help": "Optional maximum candidate age in days for deep-sleep promotion."
},
"sleep.phases.deep.recovery.lookbackDays": {
"dreaming.phases.deep.recovery.lookbackDays": {
"label": "Recovery Lookback Days",
"placeholder": "30",
"help": "How many prior days deep sleep may scan when memory recovery is triggered."
},
"sleep.phases.rem.cron": {
"dreaming.phases.rem.cron": {
"label": "REM Sleep Cron",
"placeholder": "0 5 * * 0",
"help": "Cron cadence for REM sleep reflection runs."
},
"sleep.phases.rem.lookbackDays": {
"dreaming.phases.rem.lookbackDays": {
"label": "REM Sleep Lookback Days",
"placeholder": "7",
"help": "How many prior days REM sleep may inspect for patterns."
@@ -87,7 +87,7 @@
"type": "object",
"additionalProperties": false,
"properties": {
"sleep": {
"dreaming": {
"type": "object",
"additionalProperties": false,
"properties": {

View File

@@ -114,7 +114,7 @@ function resolveMemoryPluginConfig(cfg: OpenClawConfig): Record<string, unknown>
return asRecord(entry?.config) ?? {};
}
function formatSleepSummary(cfg: OpenClawConfig): string {
function formatDreamingSummary(cfg: OpenClawConfig): string {
const pluginConfig = resolveMemoryPluginConfig(cfg);
const dreaming = resolveShortTermPromotionDreamingConfig({ pluginConfig, cfg });
if (!dreaming.enabled) {
@@ -577,7 +577,7 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
`${label("Dirty")} ${status.dirty ? warn("yes") : muted("no")}`,
`${label("Store")} ${info(storePath)}`,
`${label("Workspace")} ${info(workspacePath)}`,
`${label("Sleep")} ${info(formatSleepSummary(cfg))}`,
`${label("Dreaming")} ${info(formatDreamingSummary(cfg))}`,
].filter(Boolean) as string[];
if (embeddingProbe) {
const state = embeddingProbe.ok ? "ready" : "unavailable";

View File

@@ -367,7 +367,7 @@ describe("memory cli", () => {
await runMemoryCli(["status"]);
expect(log).toHaveBeenCalledWith(expect.stringContaining("Recall store: 1 entries"));
expect(log).toHaveBeenCalledWith(expect.stringContaining("Sleep: 0 3 * * *"));
expect(log).toHaveBeenCalledWith(expect.stringContaining("Dreaming: 0 3 * * *"));
expect(close).toHaveBeenCalled();
});
});

View File

@@ -4,7 +4,7 @@ import type {
} from "openclaw/plugin-sdk/core";
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import { describe, expect, it, vi } from "vitest";
import { registerSleepCommand } from "./dreaming-command.js";
import { registerDreamingCommand } from "./dreaming-command.js";
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -13,10 +13,10 @@ function asRecord(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function resolveStoredSleep(config: OpenClawConfig): Record<string, unknown> {
function resolveStoredDreaming(config: OpenClawConfig): Record<string, unknown> {
const entry = asRecord(config.plugins?.entries?.["memory-core"]);
const pluginConfig = asRecord(entry?.config);
return asRecord(pluginConfig?.sleep) ?? {};
return asRecord(pluginConfig?.dreaming) ?? {};
}
function createHarness(initialConfig: OpenClawConfig = {}) {
@@ -39,10 +39,10 @@ function createHarness(initialConfig: OpenClawConfig = {}) {
}),
} as unknown as OpenClawPluginApi;
registerSleepCommand(api);
registerDreamingCommand(api);
if (!command) {
throw new Error("memory-core did not register /sleep");
throw new Error("memory-core did not register /dreaming");
}
return {
@@ -56,7 +56,7 @@ function createCommandContext(args?: string): PluginCommandContext {
return {
channel: "webchat",
isAuthorizedSender: true,
commandBody: args ? `/sleep ${args}` : "/sleep",
commandBody: args ? `/dreaming ${args}` : "/dreaming",
args,
config: {},
requestConversationBinding: async () => ({ status: "error", message: "unsupported" }),
@@ -65,20 +65,20 @@ function createCommandContext(args?: string): PluginCommandContext {
};
}
describe("memory-core /sleep command", () => {
describe("memory-core /dreaming command", () => {
it("registers with a phase-oriented description", () => {
const { command } = createHarness();
expect(command.name).toBe("sleep");
expect(command.name).toBe("dreaming");
expect(command.acceptsArgs).toBe(true);
expect(command.description).toContain("sleep phases");
expect(command.description).toContain("dreaming phases");
});
it("shows phase explanations when invoked without args", async () => {
const { command } = createHarness();
const result = await command.handler(createCommandContext());
expect(result.text).toContain("Usage: /sleep status");
expect(result.text).toContain("Sleep status:");
expect(result.text).toContain("Usage: /dreaming status");
expect(result.text).toContain("Dreaming status:");
expect(result.text).toContain("- light: sorts recent memory traces into the daily note.");
expect(result.text).toContain(
"- deep: promotes durable memories into MEMORY.md and handles recovery when memory is thin.",
@@ -88,13 +88,13 @@ describe("memory-core /sleep command", () => {
);
});
it("persists global enablement under plugins.entries.memory-core.config.sleep.enabled", async () => {
it("persists global enablement under plugins.entries.memory-core.config.dreaming.enabled", async () => {
const { command, runtime, getRuntimeConfig } = createHarness({
plugins: {
entries: {
"memory-core": {
config: {
sleep: {
dreaming: {
phases: {
deep: {
minScore: 0.9,
@@ -110,7 +110,7 @@ describe("memory-core /sleep command", () => {
const result = await command.handler(createCommandContext("off"));
expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
expect(resolveStoredSleep(getRuntimeConfig())).toMatchObject({
expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({
enabled: false,
phases: {
deep: {
@@ -118,23 +118,23 @@ describe("memory-core /sleep command", () => {
},
},
});
expect(result.text).toContain("Sleep disabled.");
expect(result.text).toContain("Dreaming disabled.");
});
it("persists phase changes under plugins.entries.memory-core.config.sleep.phases", async () => {
it("persists phase changes under plugins.entries.memory-core.config.dreaming.phases", async () => {
const { command, runtime, getRuntimeConfig } = createHarness();
const result = await command.handler(createCommandContext("disable rem"));
expect(runtime.config.writeConfigFile).toHaveBeenCalledTimes(1);
expect(resolveStoredSleep(getRuntimeConfig())).toMatchObject({
expect(resolveStoredDreaming(getRuntimeConfig())).toMatchObject({
phases: {
rem: {
enabled: false,
},
},
});
expect(result.text).toContain("REM sleep disabled.");
expect(result.text).toContain("REM phase disabled.");
});
it("returns status without mutating config", async () => {
@@ -143,7 +143,7 @@ describe("memory-core /sleep command", () => {
entries: {
"memory-core": {
config: {
sleep: {
dreaming: {
timezone: "America/Los_Angeles",
storage: {
mode: "both",
@@ -164,7 +164,7 @@ describe("memory-core /sleep command", () => {
const result = await command.handler(createCommandContext("status"));
expect(result.text).toContain("Sleep status:");
expect(result.text).toContain("Dreaming status:");
expect(result.text).toContain("- enabled: on (America/Los_Angeles)");
expect(result.text).toContain("- storage: both + reports");
expect(result.text).toContain("recencyHalfLifeDays=21");
@@ -176,7 +176,7 @@ describe("memory-core /sleep command", () => {
const { command, runtime } = createHarness();
const result = await command.handler(createCommandContext("unknown-mode"));
expect(result.text).toContain("Usage: /sleep status");
expect(result.text).toContain("Usage: /dreaming status");
expect(runtime.config.writeConfigFile).not.toHaveBeenCalled();
});
});

View File

@@ -1,12 +1,12 @@
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import {
resolveMemoryLightSleepConfig,
resolveMemoryRemSleepConfig,
resolveMemorySleepConfig,
resolveMemoryLightDreamingConfig,
resolveMemoryRemDreamingConfig,
resolveMemoryDreamingConfig,
} from "openclaw/plugin-sdk/memory-core-host-status";
import { resolveShortTermPromotionDreamingConfig } from "./dreaming.js";
type SleepPhaseName = "light" | "deep" | "rem";
type DreamingPhaseName = "light" | "deep" | "rem";
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -15,7 +15,7 @@ function asRecord(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function normalizeSleepPhase(value: unknown): SleepPhaseName | null {
function normalizeDreamingPhase(value: unknown): DreamingPhaseName | null {
if (typeof value !== "string") {
return null;
}
@@ -31,16 +31,16 @@ function resolveMemoryCorePluginConfig(cfg: OpenClawConfig): Record<string, unkn
return asRecord(entry?.config) ?? {};
}
function updateSleepEnabledInConfig(cfg: OpenClawConfig, enabled: boolean): OpenClawConfig {
function updateDreamingEnabledInConfig(cfg: OpenClawConfig, enabled: boolean): OpenClawConfig {
const entries = { ...(cfg.plugins?.entries ?? {}) };
const existingEntry = asRecord(entries["memory-core"]) ?? {};
const existingConfig = asRecord(existingEntry.config) ?? {};
const existingSleep = asRecord(existingConfig.sleep) ?? {};
const existingSleep = asRecord(existingConfig.dreaming) ?? {};
entries["memory-core"] = {
...existingEntry,
config: {
...existingConfig,
sleep: {
dreaming: {
...existingSleep,
enabled,
},
@@ -56,22 +56,22 @@ function updateSleepEnabledInConfig(cfg: OpenClawConfig, enabled: boolean): Open
};
}
function updateSleepPhaseEnabledInConfig(
function updateDreamingPhaseEnabledInConfig(
cfg: OpenClawConfig,
phase: SleepPhaseName,
phase: DreamingPhaseName,
enabled: boolean,
): OpenClawConfig {
const entries = { ...(cfg.plugins?.entries ?? {}) };
const existingEntry = asRecord(entries["memory-core"]) ?? {};
const existingConfig = asRecord(existingEntry.config) ?? {};
const existingSleep = asRecord(existingConfig.sleep) ?? {};
const existingSleep = asRecord(existingConfig.dreaming) ?? {};
const existingPhases = asRecord(existingSleep.phases) ?? {};
const existingPhase = asRecord(existingPhases[phase]) ?? {};
entries["memory-core"] = {
...existingEntry,
config: {
...existingConfig,
sleep: {
dreaming: {
...existingSleep,
phases: {
...existingPhases,
@@ -107,20 +107,20 @@ function formatPhaseGuide(): string {
function formatStatus(cfg: OpenClawConfig): string {
const pluginConfig = resolveMemoryCorePluginConfig(cfg);
const sleep = resolveMemorySleepConfig({
const dreaming = resolveMemoryDreamingConfig({
pluginConfig,
cfg,
});
const deep = resolveShortTermPromotionDreamingConfig({ pluginConfig, cfg });
const light = resolveMemoryLightSleepConfig({ pluginConfig, cfg });
const rem = resolveMemoryRemSleepConfig({ pluginConfig, cfg });
const timezone = sleep.timezone ? ` (${sleep.timezone})` : "";
const light = resolveMemoryLightDreamingConfig({ pluginConfig, cfg });
const rem = resolveMemoryRemDreamingConfig({ pluginConfig, cfg });
const timezone = dreaming.timezone ? ` (${dreaming.timezone})` : "";
return [
"Sleep status:",
`- enabled: ${formatEnabled(sleep.enabled)}${timezone}`,
`- storage: ${sleep.storage.mode}${sleep.storage.separateReports ? " + reports" : ""}`,
`- verboseLogging: ${formatEnabled(sleep.verboseLogging)}`,
"Dreaming status:",
`- enabled: ${formatEnabled(dreaming.enabled)}${timezone}`,
`- storage: ${dreaming.storage.mode}${dreaming.storage.separateReports ? " + reports" : ""}`,
`- verboseLogging: ${formatEnabled(dreaming.verboseLogging)}`,
`- light: ${formatEnabled(light.enabled)} · cadence=${light.enabled ? light.cron : "disabled"} · lookbackDays=${light.lookbackDays} · limit=${light.limit}`,
`- deep: ${formatEnabled(deep.enabled)} · cadence=${deep.enabled ? deep.cron : "disabled"} · limit=${deep.limit} · minScore=${deep.minScore} · minRecallCount=${deep.minRecallCount} · minUniqueQueries=${deep.minUniqueQueries} · recencyHalfLifeDays=${deep.recencyHalfLifeDays} · maxAgeDays=${deep.maxAgeDays ?? "none"}`,
`- rem: ${formatEnabled(rem.enabled)} · cadence=${rem.enabled ? rem.cron : "disabled"} · lookbackDays=${rem.lookbackDays} · limit=${rem.limit} · minPatternStrength=${rem.minPatternStrength}`,
@@ -129,10 +129,10 @@ function formatStatus(cfg: OpenClawConfig): string {
function formatUsage(includeStatus: string): string {
return [
"Usage: /sleep status",
"Usage: /sleep on|off",
"Usage: /sleep enable light|deep|rem",
"Usage: /sleep disable light|deep|rem",
"Usage: /dreaming status",
"Usage: /dreaming on|off",
"Usage: /dreaming enable light|deep|rem",
"Usage: /dreaming disable light|deep|rem",
"",
includeStatus,
"",
@@ -141,10 +141,10 @@ function formatUsage(includeStatus: string): string {
].join("\n");
}
export function registerSleepCommand(api: OpenClawPluginApi): void {
export function registerDreamingCommand(api: OpenClawPluginApi): void {
api.registerCommand({
name: "sleep",
description: "Configure memory sleep phases and durable promotion behavior.",
name: "dreaming",
description: "Configure memory dreaming phases and durable promotion behavior.",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
@@ -169,23 +169,25 @@ export function registerSleepCommand(api: OpenClawPluginApi): void {
if (firstToken === "on" || firstToken === "off") {
const enabled = firstToken === "on";
const nextConfig = updateSleepEnabledInConfig(currentConfig, enabled);
await api.runtime.config.writeConfigFile(nextConfig);
return {
text: [`Sleep ${enabled ? "enabled" : "disabled"}.`, "", formatStatus(nextConfig)].join(
"\n",
),
};
}
const phase = normalizeSleepPhase(secondToken);
if ((firstToken === "enable" || firstToken === "disable") && phase) {
const enabled = firstToken === "enable";
const nextConfig = updateSleepPhaseEnabledInConfig(currentConfig, phase, enabled);
const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled);
await api.runtime.config.writeConfigFile(nextConfig);
return {
text: [
`${phase.toUpperCase()} sleep ${enabled ? "enabled" : "disabled"}.`,
`Dreaming ${enabled ? "enabled" : "disabled"}.`,
"",
formatStatus(nextConfig),
].join("\n"),
};
}
const phase = normalizeDreamingPhase(secondToken);
if ((firstToken === "enable" || firstToken === "disable") && phase) {
const enabled = firstToken === "enable";
const nextConfig = updateDreamingPhaseEnabledInConfig(currentConfig, phase, enabled);
await api.runtime.config.writeConfigFile(nextConfig);
return {
text: [
`${phase.toUpperCase()} phase ${enabled ? "enabled" : "disabled"}.`,
"",
formatStatus(nextConfig),
].join("\n"),
@@ -196,5 +198,3 @@ export function registerSleepCommand(api: OpenClawPluginApi): void {
},
});
}
export const registerDreamingCommand = registerSleepCommand;

View File

@@ -140,7 +140,7 @@ describe("short-term dreaming config", () => {
it("reads explicit dreaming config values", () => {
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
timezone: "UTC",
verboseLogging: true,
phases: {
@@ -178,7 +178,7 @@ describe("short-term dreaming config", () => {
it("accepts cron alias and numeric string thresholds", () => {
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
phases: {
deep: {
cron: "5 1 * * *",
@@ -213,7 +213,7 @@ describe("short-term dreaming config", () => {
it("treats blank numeric strings as unset and keeps preset defaults", () => {
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
phases: {
deep: {
limit: " ",
@@ -247,7 +247,7 @@ describe("short-term dreaming config", () => {
it("accepts limit=0 as an explicit no-op promotion cap", () => {
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
phases: {
deep: {
limit: 0,
@@ -262,14 +262,14 @@ describe("short-term dreaming config", () => {
it("accepts verboseLogging as a boolean or boolean string", () => {
const enabled = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
verboseLogging: true,
},
},
});
const disabled = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
verboseLogging: "false",
},
},
@@ -282,7 +282,7 @@ describe("short-term dreaming config", () => {
it("falls back to defaults when thresholds are negative", () => {
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
phases: {
deep: {
minScore: -0.2,
@@ -308,7 +308,7 @@ describe("short-term dreaming config", () => {
it("keeps deep sleep disabled when the phase is off", () => {
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
sleep: {
dreaming: {
phases: {
deep: {
enabled: false,

View File

@@ -1,21 +1,21 @@
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import {
DEFAULT_MEMORY_DEEP_SLEEP_CRON_EXPR as DEFAULT_MEMORY_DREAMING_CRON_EXPR,
DEFAULT_MEMORY_DEEP_SLEEP_LIMIT as DEFAULT_MEMORY_DREAMING_LIMIT,
DEFAULT_MEMORY_DEEP_SLEEP_MIN_RECALL_COUNT as DEFAULT_MEMORY_DREAMING_MIN_RECALL_COUNT,
DEFAULT_MEMORY_DEEP_SLEEP_MIN_SCORE as DEFAULT_MEMORY_DREAMING_MIN_SCORE,
DEFAULT_MEMORY_DEEP_SLEEP_MIN_UNIQUE_QUERIES as DEFAULT_MEMORY_DREAMING_MIN_UNIQUE_QUERIES,
DEFAULT_MEMORY_DEEP_SLEEP_RECENCY_HALF_LIFE_DAYS as DEFAULT_MEMORY_DREAMING_RECENCY_HALF_LIFE_DAYS,
DEFAULT_MEMORY_DEEP_DREAMING_CRON_EXPR as DEFAULT_MEMORY_DREAMING_CRON_EXPR,
DEFAULT_MEMORY_DEEP_DREAMING_LIMIT as DEFAULT_MEMORY_DREAMING_LIMIT,
DEFAULT_MEMORY_DEEP_DREAMING_MIN_RECALL_COUNT as DEFAULT_MEMORY_DREAMING_MIN_RECALL_COUNT,
DEFAULT_MEMORY_DEEP_DREAMING_MIN_SCORE as DEFAULT_MEMORY_DREAMING_MIN_SCORE,
DEFAULT_MEMORY_DEEP_DREAMING_MIN_UNIQUE_QUERIES as DEFAULT_MEMORY_DREAMING_MIN_UNIQUE_QUERIES,
DEFAULT_MEMORY_DEEP_DREAMING_RECENCY_HALF_LIFE_DAYS as DEFAULT_MEMORY_DREAMING_RECENCY_HALF_LIFE_DAYS,
resolveMemoryCorePluginConfig,
resolveMemoryDeepSleepConfig,
resolveMemorySleepWorkspaces,
resolveMemoryDeepDreamingConfig,
resolveMemoryDreamingWorkspaces,
} from "openclaw/plugin-sdk/memory-core-host-status";
import { writeDeepDreamingReport } from "./dreaming-markdown.js";
import {
applyShortTermPromotions,
repairShortTermPromotionArtifacts,
rankShortTermPromotionCandidates,
} from "./short-term-promotion.js";
import { writeDeepSleepReport } from "./sleep-markdown.js";
const MANAGED_DREAMING_CRON_NAME = "Memory Dreaming Promotion";
const MANAGED_DREAMING_CRON_TAG = "[managed-by=memory-core.short-term-promotion]";
@@ -267,7 +267,7 @@ export function resolveShortTermPromotionDreamingConfig(params: {
pluginConfig?: Record<string, unknown>;
cfg?: OpenClawConfig;
}): ShortTermPromotionDreamingConfig {
const resolved = resolveMemoryDeepSleepConfig(params);
const resolved = resolveMemoryDeepDreamingConfig(params);
return {
enabled: resolved.enabled,
cron: resolved.cron,
@@ -372,7 +372,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
const recencyHalfLifeDays =
params.config.recencyHalfLifeDays ?? DEFAULT_MEMORY_DREAMING_RECENCY_HALF_LIFE_DAYS;
const workspaceCandidates = params.cfg
? resolveMemorySleepWorkspaces(params.cfg).map((entry) => entry.workspaceDir)
? resolveMemoryDreamingWorkspaces(params.cfg).map((entry) => entry.workspaceDir)
: [];
const seenWorkspaces = new Set<string>();
const workspaces = workspaceCandidates.filter((workspaceDir) => {
@@ -467,7 +467,7 @@ export async function runShortTermDreamingPromotionIfTriggered(params: {
`memory-core: dreaming applied details [workspace=${workspaceDir}] ${appliedSummary}`,
);
}
await writeDeepSleepReport({
await writeDeepDreamingReport({
workspaceDir,
bodyLines: reportLines,
timezone: params.config.timezone,

View File

@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import { formatMemorySleepDay } from "openclaw/plugin-sdk/memory-core-host-status";
import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-core-host-status";
import {
deriveConceptTags,
MAX_CONCEPT_TAGS,
@@ -605,7 +605,7 @@ export async function recordShortTermRecalls(params: {
const queryHashes = mergeQueryHashes(existing?.queryHashes ?? [], queryHash);
const recallDays = mergeRecentDistinct(
existing?.recallDays ?? [],
formatMemorySleepDay(nowMs, params.timezone),
formatMemoryDreamingDay(nowMs, params.timezone),
MAX_RECALL_DAYS,
);
const conceptTags = deriveConceptTags({ path: normalizedPath, snippet });
@@ -929,7 +929,7 @@ function buildPromotionSection(
nowMs: number,
timezone?: string,
): string {
const sectionDate = formatMemorySleepDay(nowMs, timezone);
const sectionDate = formatMemoryDreamingDay(nowMs, timezone);
const lines = ["", `## Promoted From Short-Term Memory (${sectionDate})`, ""];
for (const candidate of candidates) {

View File

@@ -1,155 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
import {
formatMemorySleepDay,
type MemorySleepPhaseName,
type MemorySleepStorageConfig,
} from "openclaw/plugin-sdk/memory-core-host-status";
const DAILY_PHASE_HEADINGS: Record<Exclude<MemorySleepPhaseName, "deep">, string> = {
light: "## Light Sleep",
rem: "## REM Sleep",
};
const DAILY_PHASE_LABELS: Record<Exclude<MemorySleepPhaseName, "deep">, string> = {
light: "light",
rem: "rem",
};
function resolvePhaseMarkers(phase: Exclude<MemorySleepPhaseName, "deep">): {
start: string;
end: string;
} {
const label = DAILY_PHASE_LABELS[phase];
return {
start: `<!-- openclaw:sleep:${label}:start -->`,
end: `<!-- openclaw:sleep:${label}:end -->`,
};
}
function withTrailingNewline(content: string): string {
return content.endsWith("\n") ? content : `${content}\n`;
}
function replaceManagedBlock(params: {
original: string;
heading: string;
startMarker: string;
endMarker: string;
body: string;
}): string {
const managedBlock = `${params.heading}\n${params.startMarker}\n${params.body}\n${params.endMarker}`;
const existingPattern = new RegExp(
`${escapeRegex(params.heading)}\\n${escapeRegex(params.startMarker)}[\\s\\S]*?${escapeRegex(params.endMarker)}`,
"m",
);
if (existingPattern.test(params.original)) {
return params.original.replace(existingPattern, managedBlock);
}
const trimmed = params.original.trimEnd();
if (trimmed.length === 0) {
return `${managedBlock}\n`;
}
return `${trimmed}\n\n${managedBlock}\n`;
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function resolveDailyMemoryPath(workspaceDir: string, epochMs: number, timezone?: string): string {
const isoDay = formatMemorySleepDay(epochMs, timezone);
return path.join(workspaceDir, "memory", `${isoDay}.md`);
}
function resolveSeparateReportPath(
workspaceDir: string,
phase: MemorySleepPhaseName,
epochMs: number,
timezone?: string,
): string {
const isoDay = formatMemorySleepDay(epochMs, timezone);
return path.join(workspaceDir, "memory", "sleep", phase, `${isoDay}.md`);
}
function shouldWriteInline(storage: MemorySleepStorageConfig): boolean {
return storage.mode === "inline" || storage.mode === "both";
}
function shouldWriteSeparate(storage: MemorySleepStorageConfig): boolean {
return storage.mode === "separate" || storage.mode === "both" || storage.separateReports;
}
export async function writeDailySleepPhaseBlock(params: {
workspaceDir: string;
phase: Exclude<MemorySleepPhaseName, "deep">;
bodyLines: string[];
nowMs?: number;
timezone?: string;
storage: MemorySleepStorageConfig;
}): Promise<{ inlinePath?: string; reportPath?: string }> {
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No notable updates.";
let inlinePath: string | undefined;
let reportPath: string | undefined;
if (shouldWriteInline(params.storage)) {
inlinePath = resolveDailyMemoryPath(params.workspaceDir, nowMs, params.timezone);
await fs.mkdir(path.dirname(inlinePath), { recursive: true });
const original = await fs.readFile(inlinePath, "utf-8").catch((err: unknown) => {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
return "";
}
throw err;
});
const markers = resolvePhaseMarkers(params.phase);
const updated = replaceManagedBlock({
original,
heading: DAILY_PHASE_HEADINGS[params.phase],
startMarker: markers.start,
endMarker: markers.end,
body,
});
await fs.writeFile(inlinePath, withTrailingNewline(updated), "utf-8");
}
if (shouldWriteSeparate(params.storage)) {
reportPath = resolveSeparateReportPath(
params.workspaceDir,
params.phase,
nowMs,
params.timezone,
);
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const report = [
`# ${params.phase === "light" ? "Light Sleep" : "REM Sleep"}`,
"",
body,
"",
].join("\n");
await fs.writeFile(reportPath, report, "utf-8");
}
return {
...(inlinePath ? { inlinePath } : {}),
...(reportPath ? { reportPath } : {}),
};
}
export async function writeDeepSleepReport(params: {
workspaceDir: string;
bodyLines: string[];
nowMs?: number;
timezone?: string;
storage: MemorySleepStorageConfig;
}): Promise<string | undefined> {
if (!shouldWriteSeparate(params.storage)) {
return undefined;
}
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const reportPath = resolveSeparateReportPath(params.workspaceDir, "deep", nowMs, params.timezone);
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No durable changes.";
await fs.writeFile(reportPath, `# Deep Sleep\n\n${body}\n`, "utf-8");
return reportPath;
}

View File

@@ -1,657 +0,0 @@
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import {
resolveMemoryCorePluginConfig,
resolveMemoryLightSleepConfig,
resolveMemoryRemSleepConfig,
resolveMemorySleepWorkspaces,
type MemoryLightSleepConfig,
type MemoryRemSleepConfig,
type MemorySleepPhaseName,
} from "openclaw/plugin-sdk/memory-core-host-status";
import { readShortTermRecallEntries, type ShortTermRecallEntry } from "./short-term-promotion.js";
import { writeDailySleepPhaseBlock } from "./sleep-markdown.js";
type Logger = Pick<OpenClawPluginApi["logger"], "info" | "warn" | "error">;
type CronSchedule = { kind: "cron"; expr: string; tz?: string };
type CronPayload = { kind: "systemEvent"; text: string };
type ManagedCronJobCreate = {
name: string;
description: string;
enabled: boolean;
schedule: CronSchedule;
sessionTarget: "main";
wakeMode: "next-heartbeat";
payload: CronPayload;
};
type ManagedCronJobPatch = {
name?: string;
description?: string;
enabled?: boolean;
schedule?: CronSchedule;
sessionTarget?: "main";
wakeMode?: "next-heartbeat";
payload?: CronPayload;
};
type ManagedCronJobLike = {
id: string;
name?: string;
description?: string;
enabled?: boolean;
schedule?: {
kind?: string;
expr?: string;
tz?: string;
};
sessionTarget?: string;
wakeMode?: string;
payload?: {
kind?: string;
text?: string;
};
createdAtMs?: number;
};
type CronServiceLike = {
list: (opts?: { includeDisabled?: boolean }) => Promise<ManagedCronJobLike[]>;
add: (input: ManagedCronJobCreate) => Promise<unknown>;
update: (id: string, patch: ManagedCronJobPatch) => Promise<unknown>;
remove: (id: string) => Promise<{ removed?: boolean }>;
};
const LIGHT_SLEEP_CRON_NAME = "Memory Light Sleep";
const LIGHT_SLEEP_CRON_TAG = "[managed-by=memory-core.sleep.light]";
const LIGHT_SLEEP_EVENT_TEXT = "__openclaw_memory_core_light_sleep__";
const REM_SLEEP_CRON_NAME = "Memory REM Sleep";
const REM_SLEEP_CRON_TAG = "[managed-by=memory-core.sleep.rem]";
const REM_SLEEP_EVENT_TEXT = "__openclaw_memory_core_rem_sleep__";
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 formatErrorMessage(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
function buildCronDescription(params: {
tag: string;
phase: "light" | "rem";
cron: string;
limit: number;
lookbackDays: number;
}): string {
return `${params.tag} Run ${params.phase} sleep (cron=${params.cron}, limit=${params.limit}, lookbackDays=${params.lookbackDays}).`;
}
function buildManagedCronJob(params: {
name: string;
tag: string;
payloadText: string;
cron: string;
timezone?: string;
phase: "light" | "rem";
limit: number;
lookbackDays: number;
}): ManagedCronJobCreate {
return {
name: params.name,
description: buildCronDescription({
tag: params.tag,
phase: params.phase,
cron: params.cron,
limit: params.limit,
lookbackDays: params.lookbackDays,
}),
enabled: true,
schedule: {
kind: "cron",
expr: params.cron,
...(params.timezone ? { tz: params.timezone } : {}),
},
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: {
kind: "systemEvent",
text: params.payloadText,
},
};
}
function isManagedPhaseJob(
job: ManagedCronJobLike,
params: {
name: string;
tag: string;
payloadText: string;
},
): boolean {
const description = normalizeTrimmedString(job.description);
if (description?.includes(params.tag)) {
return true;
}
const name = normalizeTrimmedString(job.name);
const payloadText = normalizeTrimmedString(job.payload?.text);
return name === params.name && payloadText === params.payloadText;
}
function buildManagedPhasePatch(
job: ManagedCronJobLike,
desired: ManagedCronJobCreate,
): ManagedCronJobPatch | null {
const patch: ManagedCronJobPatch = {};
const scheduleKind = normalizeTrimmedString(job.schedule?.kind)?.toLowerCase();
const scheduleExpr = normalizeTrimmedString(job.schedule?.expr);
const scheduleTz = normalizeTrimmedString(job.schedule?.tz);
if (normalizeTrimmedString(job.name) !== desired.name) {
patch.name = desired.name;
}
if (normalizeTrimmedString(job.description) !== desired.description) {
patch.description = desired.description;
}
if (job.enabled !== true) {
patch.enabled = true;
}
if (
scheduleKind !== "cron" ||
scheduleExpr !== desired.schedule.expr ||
scheduleTz !== desired.schedule.tz
) {
patch.schedule = desired.schedule;
}
if (normalizeTrimmedString(job.sessionTarget)?.toLowerCase() !== "main") {
patch.sessionTarget = "main";
}
if (normalizeTrimmedString(job.wakeMode)?.toLowerCase() !== "next-heartbeat") {
patch.wakeMode = "next-heartbeat";
}
const payloadKind = normalizeTrimmedString(job.payload?.kind)?.toLowerCase();
const payloadText = normalizeTrimmedString(job.payload?.text);
if (payloadKind !== "systemevent" || payloadText !== desired.payload.text) {
patch.payload = desired.payload;
}
return Object.keys(patch).length > 0 ? patch : null;
}
function sortManagedJobs(managed: ManagedCronJobLike[]): ManagedCronJobLike[] {
return managed.toSorted((a, b) => {
const aCreated =
typeof a.createdAtMs === "number" && Number.isFinite(a.createdAtMs)
? a.createdAtMs
: Number.MAX_SAFE_INTEGER;
const bCreated =
typeof b.createdAtMs === "number" && Number.isFinite(b.createdAtMs)
? b.createdAtMs
: Number.MAX_SAFE_INTEGER;
if (aCreated !== bCreated) {
return aCreated - bCreated;
}
return a.id.localeCompare(b.id);
});
}
function resolveCronServiceFromStartupEvent(event: unknown): CronServiceLike | null {
const payload = asRecord(event);
if (!payload || payload.type !== "gateway" || payload.action !== "startup") {
return null;
}
const context = asRecord(payload.context);
const deps = asRecord(context?.deps);
const cronCandidate = context?.cron ?? deps?.cron;
if (!cronCandidate || typeof cronCandidate !== "object") {
return null;
}
const cron = cronCandidate as Partial<CronServiceLike>;
if (
typeof cron.list !== "function" ||
typeof cron.add !== "function" ||
typeof cron.update !== "function" ||
typeof cron.remove !== "function"
) {
return null;
}
return cron as CronServiceLike;
}
async function reconcileManagedPhaseCronJob(params: {
cron: CronServiceLike | null;
desired: ManagedCronJobCreate;
match: { name: string; tag: string; payloadText: string };
enabled: boolean;
logger: Logger;
}): Promise<void> {
const cron = params.cron;
if (!cron) {
return;
}
const allJobs = await cron.list({ includeDisabled: true });
const managed = allJobs.filter((job) => isManagedPhaseJob(job, params.match));
if (!params.enabled) {
for (const job of managed) {
try {
await cron.remove(job.id);
} catch (err) {
params.logger.warn(
`memory-core: failed to remove managed ${params.match.name} cron job ${job.id}: ${formatErrorMessage(err)}`,
);
}
}
return;
}
if (managed.length === 0) {
await cron.add(params.desired);
return;
}
const [primary, ...duplicates] = sortManagedJobs(managed);
for (const duplicate of duplicates) {
try {
await cron.remove(duplicate.id);
} catch (err) {
params.logger.warn(
`memory-core: failed to prune duplicate managed ${params.match.name} cron job ${duplicate.id}: ${formatErrorMessage(err)}`,
);
}
}
const patch = buildManagedPhasePatch(primary, params.desired);
if (patch) {
await cron.update(primary.id, patch);
}
}
function resolveWorkspaces(params: {
cfg?: OpenClawConfig;
fallbackWorkspaceDir?: string;
}): string[] {
const workspaceCandidates = params.cfg
? resolveMemorySleepWorkspaces(params.cfg).map((entry) => entry.workspaceDir)
: [];
const seen = new Set<string>();
const workspaces = workspaceCandidates.filter((workspaceDir) => {
if (seen.has(workspaceDir)) {
return false;
}
seen.add(workspaceDir);
return true;
});
const fallbackWorkspaceDir = normalizeTrimmedString(params.fallbackWorkspaceDir);
if (workspaces.length === 0 && fallbackWorkspaceDir) {
workspaces.push(fallbackWorkspaceDir);
}
return workspaces;
}
function calculateLookbackCutoffMs(nowMs: number, lookbackDays: number): number {
return nowMs - Math.max(0, lookbackDays) * 24 * 60 * 60 * 1000;
}
function entryAverageScore(entry: ShortTermRecallEntry): number {
return entry.recallCount > 0 ? Math.max(0, Math.min(1, entry.totalScore / entry.recallCount)) : 0;
}
function tokenizeSnippet(snippet: string): Set<string> {
return new Set(
snippet
.toLowerCase()
.split(/[^a-z0-9]+/i)
.map((token) => token.trim())
.filter(Boolean),
);
}
function jaccardSimilarity(left: string, right: string): number {
const leftTokens = tokenizeSnippet(left);
const rightTokens = tokenizeSnippet(right);
if (leftTokens.size === 0 || rightTokens.size === 0) {
return left.trim().toLowerCase() === right.trim().toLowerCase() ? 1 : 0;
}
let intersection = 0;
for (const token of leftTokens) {
if (rightTokens.has(token)) {
intersection += 1;
}
}
const union = new Set([...leftTokens, ...rightTokens]).size;
return union > 0 ? intersection / union : 0;
}
function dedupeEntries(entries: ShortTermRecallEntry[], threshold: number): ShortTermRecallEntry[] {
const deduped: ShortTermRecallEntry[] = [];
for (const entry of entries) {
const duplicate = deduped.find(
(candidate) =>
candidate.path === entry.path &&
jaccardSimilarity(candidate.snippet, entry.snippet) >= threshold,
);
if (duplicate) {
if (entry.recallCount > duplicate.recallCount) {
duplicate.recallCount = entry.recallCount;
}
duplicate.totalScore = Math.max(duplicate.totalScore, entry.totalScore);
duplicate.maxScore = Math.max(duplicate.maxScore, entry.maxScore);
duplicate.queryHashes = [...new Set([...duplicate.queryHashes, ...entry.queryHashes])];
duplicate.recallDays = [
...new Set([...duplicate.recallDays, ...entry.recallDays]),
].toSorted();
duplicate.conceptTags = [...new Set([...duplicate.conceptTags, ...entry.conceptTags])];
duplicate.lastRecalledAt =
Date.parse(entry.lastRecalledAt) > Date.parse(duplicate.lastRecalledAt)
? entry.lastRecalledAt
: duplicate.lastRecalledAt;
continue;
}
deduped.push({ ...entry });
}
return deduped;
}
function buildLightSleepBody(entries: ShortTermRecallEntry[]): string[] {
if (entries.length === 0) {
return ["- No notable updates."];
}
const lines: string[] = [];
for (const entry of entries) {
const snippet = entry.snippet || "(no snippet captured)";
lines.push(`- Candidate: ${snippet}`);
lines.push(` - confidence: ${entryAverageScore(entry).toFixed(2)}`);
lines.push(` - evidence: ${entry.path}:${entry.startLine}-${entry.endLine}`);
lines.push(` - recalls: ${entry.recallCount}`);
lines.push(` - status: staged`);
}
return lines;
}
function buildRemSleepBody(
entries: ShortTermRecallEntry[],
limit: number,
minPatternStrength: number,
): string[] {
const tagStats = new Map<string, { count: number; evidence: Set<string> }>();
for (const entry of entries) {
for (const tag of entry.conceptTags) {
if (!tag) {
continue;
}
const stat = tagStats.get(tag) ?? { count: 0, evidence: new Set<string>() };
stat.count += 1;
stat.evidence.add(`${entry.path}:${entry.startLine}-${entry.endLine}`);
tagStats.set(tag, stat);
}
}
const ranked = [...tagStats.entries()]
.map(([tag, stat]) => {
const strength = Math.min(1, (stat.count / Math.max(1, entries.length)) * 2);
return { tag, strength, stat };
})
.filter((entry) => entry.strength >= minPatternStrength)
.toSorted(
(a, b) =>
b.strength - a.strength || b.stat.count - a.stat.count || a.tag.localeCompare(b.tag),
)
.slice(0, limit);
if (ranked.length === 0) {
return ["- No strong patterns surfaced."];
}
const lines: string[] = [];
for (const entry of ranked) {
lines.push(`- Theme: \`${entry.tag}\` kept surfacing across ${entry.stat.count} memories.`);
lines.push(` - confidence: ${entry.strength.toFixed(2)}`);
lines.push(` - evidence: ${[...entry.stat.evidence].slice(0, 3).join(", ")}`);
lines.push(` - note: reflection`);
}
return lines;
}
async function runLightSleep(params: {
workspaceDir: string;
config: MemoryLightSleepConfig & {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
};
logger: Logger;
nowMs?: number;
}): Promise<void> {
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const cutoffMs = calculateLookbackCutoffMs(nowMs, params.config.lookbackDays);
const entries = dedupeEntries(
(await readShortTermRecallEntries({ workspaceDir: params.workspaceDir, nowMs }))
.filter((entry) => Date.parse(entry.lastRecalledAt) >= cutoffMs)
.toSorted((a, b) => {
const byTime = Date.parse(b.lastRecalledAt) - Date.parse(a.lastRecalledAt);
if (byTime !== 0) {
return byTime;
}
return b.recallCount - a.recallCount;
})
.slice(0, params.config.limit),
params.config.dedupeSimilarity,
);
const bodyLines = buildLightSleepBody(entries.slice(0, params.config.limit));
await writeDailySleepPhaseBlock({
workspaceDir: params.workspaceDir,
phase: "light",
bodyLines,
nowMs,
timezone: params.config.timezone,
storage: params.config.storage,
});
if (params.config.enabled && entries.length > 0 && params.config.storage.mode !== "separate") {
params.logger.info(
`memory-core: light sleep staged ${Math.min(entries.length, params.config.limit)} candidate(s) [workspace=${params.workspaceDir}].`,
);
}
}
async function runRemSleep(params: {
workspaceDir: string;
config: MemoryRemSleepConfig & {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
};
logger: Logger;
nowMs?: number;
}): Promise<void> {
const nowMs = Number.isFinite(params.nowMs) ? (params.nowMs as number) : Date.now();
const cutoffMs = calculateLookbackCutoffMs(nowMs, params.config.lookbackDays);
const entries = (
await readShortTermRecallEntries({ workspaceDir: params.workspaceDir, nowMs })
).filter((entry) => Date.parse(entry.lastRecalledAt) >= cutoffMs);
const bodyLines = buildRemSleepBody(
entries,
params.config.limit,
params.config.minPatternStrength,
);
await writeDailySleepPhaseBlock({
workspaceDir: params.workspaceDir,
phase: "rem",
bodyLines,
nowMs,
timezone: params.config.timezone,
storage: params.config.storage,
});
if (params.config.enabled && entries.length > 0 && params.config.storage.mode !== "separate") {
params.logger.info(
`memory-core: REM sleep wrote reflections from ${entries.length} recent memory trace(s) [workspace=${params.workspaceDir}].`,
);
}
}
async function runPhaseIfTriggered(params: {
cleanedBody: string;
trigger?: string;
workspaceDir?: string;
cfg?: OpenClawConfig;
logger: Logger;
phase: "light" | "rem";
eventText: string;
config:
| (MemoryLightSleepConfig & {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
})
| (MemoryRemSleepConfig & {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
});
}): Promise<{ handled: true; reason: string } | undefined> {
if (params.trigger !== "heartbeat" || params.cleanedBody.trim() !== params.eventText) {
return undefined;
}
if (!params.config.enabled) {
return { handled: true, reason: `memory-core: ${params.phase} sleep disabled` };
}
const workspaces = resolveWorkspaces({
cfg: params.cfg,
fallbackWorkspaceDir: params.workspaceDir,
});
if (workspaces.length === 0) {
params.logger.warn(
`memory-core: ${params.phase} sleep skipped because no memory workspace is available.`,
);
return { handled: true, reason: `memory-core: ${params.phase} sleep missing workspace` };
}
if (params.config.limit === 0) {
params.logger.info(`memory-core: ${params.phase} sleep skipped because limit=0.`);
return { handled: true, reason: `memory-core: ${params.phase} sleep disabled by limit` };
}
for (const workspaceDir of workspaces) {
try {
if (params.phase === "light") {
await runLightSleep({
workspaceDir,
config: params.config as MemoryLightSleepConfig & {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
},
logger: params.logger,
});
} else {
await runRemSleep({
workspaceDir,
config: params.config as MemoryRemSleepConfig & {
timezone?: string;
storage: { mode: "inline" | "separate" | "both"; separateReports: boolean };
},
logger: params.logger,
});
}
} catch (err) {
params.logger.error(
`memory-core: ${params.phase} sleep failed for workspace ${workspaceDir}: ${formatErrorMessage(err)}`,
);
}
}
return { handled: true, reason: `memory-core: ${params.phase} sleep processed` };
}
export function registerMemorySleepPhases(api: OpenClawPluginApi): void {
api.registerHook(
"gateway:startup",
async (event: unknown) => {
const cron = resolveCronServiceFromStartupEvent(event);
const pluginConfig = resolveMemoryCorePluginConfig(api.config) ?? api.pluginConfig;
const light = resolveMemoryLightSleepConfig({ pluginConfig, cfg: api.config });
const rem = resolveMemoryRemSleepConfig({ pluginConfig, cfg: api.config });
const lightDesired = buildManagedCronJob({
name: LIGHT_SLEEP_CRON_NAME,
tag: LIGHT_SLEEP_CRON_TAG,
payloadText: LIGHT_SLEEP_EVENT_TEXT,
cron: light.cron,
timezone: light.timezone,
phase: "light",
limit: light.limit,
lookbackDays: light.lookbackDays,
});
const remDesired = buildManagedCronJob({
name: REM_SLEEP_CRON_NAME,
tag: REM_SLEEP_CRON_TAG,
payloadText: REM_SLEEP_EVENT_TEXT,
cron: rem.cron,
timezone: rem.timezone,
phase: "rem",
limit: rem.limit,
lookbackDays: rem.lookbackDays,
});
try {
await reconcileManagedPhaseCronJob({
cron,
desired: lightDesired,
match: {
name: LIGHT_SLEEP_CRON_NAME,
tag: LIGHT_SLEEP_CRON_TAG,
payloadText: LIGHT_SLEEP_EVENT_TEXT,
},
enabled: light.enabled,
logger: api.logger,
});
await reconcileManagedPhaseCronJob({
cron,
desired: remDesired,
match: {
name: REM_SLEEP_CRON_NAME,
tag: REM_SLEEP_CRON_TAG,
payloadText: REM_SLEEP_EVENT_TEXT,
},
enabled: rem.enabled,
logger: api.logger,
});
} catch (err) {
api.logger.error(
`memory-core: sleep startup reconciliation failed: ${formatErrorMessage(err)}`,
);
}
},
{ name: "memory-core-sleep-phase-cron" },
);
api.on("before_agent_reply", async (event, ctx) => {
const pluginConfig = resolveMemoryCorePluginConfig(api.config) ?? api.pluginConfig;
const light = resolveMemoryLightSleepConfig({ pluginConfig, cfg: api.config });
const lightResult = await runPhaseIfTriggered({
cleanedBody: event.cleanedBody,
trigger: ctx.trigger,
workspaceDir: ctx.workspaceDir,
cfg: api.config,
logger: api.logger,
phase: "light",
eventText: LIGHT_SLEEP_EVENT_TEXT,
config: light,
});
if (lightResult) {
return lightResult;
}
const rem = resolveMemoryRemSleepConfig({ pluginConfig, cfg: api.config });
return await runPhaseIfTriggered({
cleanedBody: event.cleanedBody,
trigger: ctx.trigger,
workspaceDir: ctx.workspaceDir,
cfg: api.config,
logger: api.logger,
phase: "rem",
eventText: REM_SLEEP_EVENT_TEXT,
config: rem,
});
});
}

View File

@@ -137,7 +137,7 @@ describe("memory_search recall tracking", () => {
}
});
it("passes the resolved sleep timezone into recall tracking", async () => {
it("passes the resolved dreaming timezone into recall tracking", async () => {
setMemorySearchImpl(async () => [
{
path: "memory/2026-04-03.md",
@@ -161,7 +161,7 @@ describe("memory_search recall tracking", () => {
entries: {
"memory-core": {
config: {
sleep: {
dreaming: {
timezone: "Europe/London",
},
},

View File

@@ -8,7 +8,7 @@ import {
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import {
resolveMemoryCorePluginConfig,
resolveMemoryDeepSleepConfig,
resolveMemoryDeepDreamingConfig,
} from "openclaw/plugin-sdk/memory-core-host-status";
import { recordShortTermRecalls } from "./short-term-promotion.js";
import {
@@ -108,7 +108,7 @@ export function createMemorySearchTool(options: {
status.backend === "qmd"
? clampResultsByInjectedChars(decorated, resolved.qmd?.limits.maxInjectedChars)
: decorated;
const sleepTimezone = resolveMemoryDeepSleepConfig({
const sleepTimezone = resolveMemoryDeepDreamingConfig({
pluginConfig: resolveMemoryCorePluginConfig(cfg),
cfg,
}).timezone;