Files
openclaw/extensions/memory-core/src/dreaming-command.ts
Vignesh 4c1022c73b feat(memory-core): add dreaming promotion with weighted recall thresholds (#60569)
* memory-core: add dreaming promotion flow with weighted thresholds

* docs(memory): mark dreaming as experimental

* memory-core: address dreaming promotion review feedback

* memory-core: harden short-term promotion concurrency

* acpx: make abort-process test timer-independent

* memory-core: simplify dreaming config with mode presets

* memory-core: add /dreaming command and tighten recall tracking

* ui: add Dreams tab with sleeping lobster animation

Adds a new Dreams tab to the gateway UI under the Agent group.
The tab is gated behind the memory-core dreaming config — it only
appears in the sidebar when dreaming.mode is not 'off'.

Features:
- Sleeping vector lobster with breathing animation
- Floating Z's, twinkling starfield, moon glow
- Rotating dream phrase bubble (17 whimsical phrases)
- Memory stats bar (short-term, long-term, promoted)
- Active/idle visual states
- 14 unit tests

* plugins: fix --json stdout pollution from hook runner log

The hook runner initialization message was using log.info() which
writes to stdout via console.log, breaking JSON.parse() in the
Docker smoke test for 'openclaw plugins list --json'. Downgrade to
log.debug() so it only appears when debugging is enabled.

* ui: keep Dreams tab visible when dreaming is off

* tests: fix contracts and stabilize extension shards

* memory-core: harden dreaming recall persistence and locking

* fix: stabilize dreaming PR gates (#60569) (thanks @vignesh07)

* test: fix rebase drift in telegram and plugin guards
2026-04-03 20:26:53 -07:00

169 lines
4.7 KiB
TypeScript

import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import { resolveShortTermPromotionDreamingConfig } from "./dreaming.js";
type DreamingMode = "off" | "core" | "rem" | "deep";
const DREAMING_MODE_LIST = [
"off",
"core",
"rem",
"deep",
] as const satisfies readonly DreamingMode[];
const DEFAULT_DREAMING_MODE: DreamingMode = "off";
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 normalizeDreamingMode(value: unknown): DreamingMode | null {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim().toLowerCase();
if (
normalized === "off" ||
normalized === "core" ||
normalized === "rem" ||
normalized === "deep"
) {
return normalized;
}
return null;
}
function resolveMemoryCorePluginConfig(cfg: OpenClawConfig): Record<string, unknown> {
const entry = asRecord(cfg.plugins?.entries?.["memory-core"]);
return asRecord(entry?.config) ?? {};
}
function resolveDreamingModeFromConfig(pluginConfig: Record<string, unknown>): DreamingMode {
const dreaming = asRecord(pluginConfig.dreaming);
return normalizeDreamingMode(dreaming?.mode) ?? DEFAULT_DREAMING_MODE;
}
function updateDreamingModeInConfig(cfg: OpenClawConfig, mode: DreamingMode): OpenClawConfig {
const entries = { ...(cfg.plugins?.entries ?? {}) };
const existingEntry = asRecord(entries["memory-core"]) ?? {};
const existingConfig = asRecord(existingEntry.config) ?? {};
const existingDreaming = asRecord(existingConfig.dreaming) ?? {};
entries["memory-core"] = {
...existingEntry,
config: {
...existingConfig,
dreaming: {
...existingDreaming,
mode,
},
},
};
return {
...cfg,
plugins: {
...cfg.plugins,
entries,
},
};
}
function formatModeGuideLine(mode: DreamingMode): string {
if (mode === "off") {
return "- off: disable automatic short-term to long-term promotion.";
}
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig: {
dreaming: {
mode,
},
},
});
return (
`- ${mode}: cadence=${resolved.cron}; ` +
`minScore=${resolved.minScore}, minRecallCount=${resolved.minRecallCount}, ` +
`minUniqueQueries=${resolved.minUniqueQueries}.`
);
}
function formatModeGuide(): string {
return DREAMING_MODE_LIST.map((mode) => formatModeGuideLine(mode)).join("\n");
}
function formatStatus(cfg: OpenClawConfig): string {
const pluginConfig = resolveMemoryCorePluginConfig(cfg);
const mode = resolveDreamingModeFromConfig(pluginConfig);
const resolved = resolveShortTermPromotionDreamingConfig({
pluginConfig,
cfg,
});
const cadence = resolved.enabled ? resolved.cron : "disabled";
const timezone = resolved.enabled && resolved.timezone ? ` (${resolved.timezone})` : "";
return [
"Dreaming status:",
`- mode: ${mode}`,
`- cadence: ${cadence}${timezone}`,
`- limit: ${resolved.limit}`,
`- thresholds: minScore=${resolved.minScore}, minRecallCount=${resolved.minRecallCount}, minUniqueQueries=${resolved.minUniqueQueries}`,
].join("\n");
}
function formatUsage(includeStatus: string): string {
return [
"Usage: /dreaming off|core|rem|deep",
"Use /dreaming status for current settings.",
"",
includeStatus,
"",
"Modes:",
formatModeGuide(),
].join("\n");
}
export function registerDreamingCommand(api: OpenClawPluginApi): void {
api.registerCommand({
name: "dreaming",
description: "Configure memory dreaming mode (off|core|rem|deep).",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const firstToken = args.split(/\s+/).filter(Boolean)[0]?.toLowerCase() ?? "";
const currentConfig = api.runtime.config.loadConfig();
if (
!firstToken ||
firstToken === "help" ||
firstToken === "options" ||
firstToken === "modes"
) {
return { text: formatUsage(formatStatus(currentConfig)) };
}
if (firstToken === "status") {
return { text: formatStatus(currentConfig) };
}
const requestedMode = normalizeDreamingMode(firstToken);
if (!requestedMode) {
return { text: formatUsage(formatStatus(currentConfig)) };
}
const nextConfig = updateDreamingModeInConfig(currentConfig, requestedMode);
await api.runtime.config.writeConfigFile(nextConfig);
return {
text: [
`Dreaming mode set to ${requestedMode}.`,
"",
formatStatus(nextConfig),
"",
"Modes:",
formatModeGuide(),
].join("\n"),
};
},
});
}