Files
openclaw/extensions/memory-core/src/dreaming-command.ts
T
Peter Steinberger bc8e51ce37 refactor(memory-core): strengthen dreaming boundaries (#123138)
Replace assertion-driven cleanup with real runtime narrowing and typed owner contracts across memory-core dreaming.

Reject the unsound one-file lint ratchet and document repository-wide rules against checker-gaming and baseline-driven enforcement.

Co-authored-by: Amp <amp@ampcode.com>
2026-08-13 11:32:25 -07:00

138 lines
4.3 KiB
TypeScript

// Memory Core plugin module implements dreaming command behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolveMemoryDreamingConfig } from "openclaw/plugin-sdk/memory-core-host-status";
import type { OpenClawPluginApi, PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
import {
asNullableRecord,
normalizeLowercaseStringOrEmpty,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveShortTermPromotionDreamingConfig } from "./dreaming.js";
function resolveDreamingPluginConfig(cfg: OpenClawConfig): Record<string, unknown> {
const entry = asNullableRecord(cfg.plugins?.entries?.["memory-core"]);
return asNullableRecord(entry?.config) ?? {};
}
function updateDreamingEnabledInConfig(cfg: OpenClawConfig, enabled: boolean): OpenClawConfig {
const entries = { ...cfg.plugins?.entries };
const existingEntry = asNullableRecord(entries["memory-core"]) ?? {};
const existingConfig = asNullableRecord(existingEntry.config) ?? {};
const existingSleep = asNullableRecord(existingConfig.dreaming) ?? {};
entries["memory-core"] = {
...existingEntry,
config: {
...existingConfig,
dreaming: {
...existingSleep,
enabled,
},
},
};
return {
...cfg,
plugins: {
...cfg.plugins,
entries,
},
};
}
function formatEnabled(value: boolean): string {
return value ? "on" : "off";
}
function formatPhaseGuide(): string {
return [
"- implementation detail: each sweep runs light -> REM -> deep.",
"- deep is the only stage that writes durable entries to MEMORY.md.",
"- DREAMS.md is for human-readable dreaming summaries and diary entries.",
].join("\n");
}
function formatStatus(cfg: OpenClawConfig): string {
const pluginConfig = resolveDreamingPluginConfig(cfg);
const dreaming = resolveMemoryDreamingConfig({
pluginConfig,
cfg,
});
const deep = resolveShortTermPromotionDreamingConfig({ pluginConfig, cfg });
const timezone = dreaming.timezone ? ` (${dreaming.timezone})` : "";
return [
"Dreaming status:",
`- enabled: ${formatEnabled(dreaming.enabled)}${timezone}`,
`- sweep cadence: ${dreaming.frequency}`,
`- promotion policy: score>=${deep.minScore}, recalls>=${deep.minRecallCount}, uniqueQueries>=${deep.minUniqueQueries}`,
].join("\n");
}
function formatUsage(includeStatus: string): string {
return [
"Usage: /dreaming status",
"Usage: /dreaming on|off",
"",
includeStatus,
"",
"Phases:",
formatPhaseGuide(),
].join("\n");
}
function lacksAdminOrOwnerForDreamingMutation(params: {
gatewayClientScopes?: readonly string[];
senderIsOwner?: boolean;
}): boolean {
if (Array.isArray(params.gatewayClientScopes)) {
return !params.gatewayClientScopes.includes("operator.admin");
}
return params.senderIsOwner !== true;
}
export async function handleDreamingCommand(api: OpenClawPluginApi, ctx: PluginCommandContext) {
const args = ctx.args?.trim() ?? "";
const [firstToken = ""] = args
.split(/\s+/)
.filter(Boolean)
.map((token) => normalizeLowercaseStringOrEmpty(token));
const currentConfig = ctx.config;
if (!firstToken || firstToken === "help" || firstToken === "options" || firstToken === "phases") {
return { text: formatUsage(formatStatus(currentConfig)) };
}
if (firstToken === "status") {
return { text: formatStatus(currentConfig) };
}
if (firstToken === "on" || firstToken === "off") {
if (
lacksAdminOrOwnerForDreamingMutation({
gatewayClientScopes: ctx.gatewayClientScopes,
senderIsOwner: ctx.senderIsOwner,
})
) {
return {
text: "⚠️ /dreaming on|off requires owner status for channel callers or operator.admin for gateway clients.",
};
}
const enabled = firstToken === "on";
const committed = await api.runtime.config.mutateConfigFile({
afterWrite: { mode: "auto" },
mutate: (draft) => {
const nextConfig = updateDreamingEnabledInConfig(draft, enabled);
Object.assign(draft, nextConfig);
},
});
return {
text: [
`Dreaming ${enabled ? "enabled" : "disabled"}.`,
"",
formatStatus(committed.nextConfig),
].join("\n"),
};
}
return { text: formatUsage(formatStatus(currentConfig)) };
}