mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 02:22:25 +00:00
refactor(memory-core): rename sleep surface back to dreaming
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user