Files
openclaw/extensions/memory-core/src/tools.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

184 lines
6.6 KiB
TypeScript

import {
jsonResult,
readNumberParam,
readStringParam,
type AnyAgentTool,
type OpenClawConfig,
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import { recordShortTermRecalls } from "./short-term-promotion.js";
import {
clampResultsByInjectedChars,
decorateCitations,
resolveMemoryCitationsMode,
shouldIncludeCitations,
} from "./tools.citations.js";
import {
buildMemorySearchUnavailableResult,
createMemoryTool,
getMemoryManagerContext,
getMemoryManagerContextWithPurpose,
loadMemoryToolRuntime,
MemoryGetSchema,
MemorySearchSchema,
} from "./tools.shared.js";
function buildRecallKey(
result: Pick<MemorySearchResult, "source" | "path" | "startLine" | "endLine">,
): string {
return `${result.source}:${result.path}:${result.startLine}:${result.endLine}`;
}
function resolveRecallTrackingResults(
rawResults: MemorySearchResult[],
surfacedResults: MemorySearchResult[],
): MemorySearchResult[] {
if (surfacedResults.length === 0 || rawResults.length === 0) {
return surfacedResults;
}
const rawByKey = new Map<string, MemorySearchResult>();
for (const raw of rawResults) {
const key = buildRecallKey(raw);
if (!rawByKey.has(key)) {
rawByKey.set(key, raw);
}
}
return surfacedResults.map((surfaced) => rawByKey.get(buildRecallKey(surfaced)) ?? surfaced);
}
function queueShortTermRecallTracking(params: {
workspaceDir?: string;
query: string;
rawResults: MemorySearchResult[];
surfacedResults: MemorySearchResult[];
}): void {
const trackingResults = resolveRecallTrackingResults(params.rawResults, params.surfacedResults);
void recordShortTermRecalls({
workspaceDir: params.workspaceDir,
query: params.query,
results: trackingResults,
}).catch(() => {
// Recall tracking is best-effort and must never block memory recall.
});
}
export function createMemorySearchTool(options: {
config?: OpenClawConfig;
agentSessionKey?: string;
}): AnyAgentTool | null {
return createMemoryTool({
options,
label: "Memory Search",
name: "memory_search",
description:
"Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user.",
parameters: MemorySearchSchema,
execute:
({ cfg, agentId }) =>
async (_toolCallId, params) => {
const query = readStringParam(params, "query", { required: true });
const maxResults = readNumberParam(params, "maxResults");
const minScore = readNumberParam(params, "minScore");
const { resolveMemoryBackendConfig } = await loadMemoryToolRuntime();
const memory = await getMemoryManagerContext({ cfg, agentId });
if ("error" in memory) {
return jsonResult(buildMemorySearchUnavailableResult(memory.error));
}
try {
const citationsMode = resolveMemoryCitationsMode(cfg);
const includeCitations = shouldIncludeCitations({
mode: citationsMode,
sessionKey: options.agentSessionKey,
});
const rawResults = await memory.manager.search(query, {
maxResults,
minScore,
sessionKey: options.agentSessionKey,
});
const status = memory.manager.status();
const decorated = decorateCitations(rawResults, includeCitations);
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
const results =
status.backend === "qmd"
? clampResultsByInjectedChars(decorated, resolved.qmd?.limits.maxInjectedChars)
: decorated;
queueShortTermRecallTracking({
workspaceDir: status.workspaceDir,
query,
rawResults,
surfacedResults: results,
});
const searchMode = (status.custom as { searchMode?: string } | undefined)?.searchMode;
return jsonResult({
results,
provider: status.provider,
model: status.model,
fallback: status.fallback,
citations: citationsMode,
mode: searchMode,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return jsonResult(buildMemorySearchUnavailableResult(message));
}
},
});
}
export function createMemoryGetTool(options: {
config?: OpenClawConfig;
agentSessionKey?: string;
}): AnyAgentTool | null {
return createMemoryTool({
options,
label: "Memory Get",
name: "memory_get",
description:
"Safe snippet read from MEMORY.md or memory/*.md with optional from/lines; use after memory_search to pull only the needed lines and keep context small.",
parameters: MemoryGetSchema,
execute:
({ cfg, agentId }) =>
async (_toolCallId, params) => {
const relPath = readStringParam(params, "path", { required: true });
const from = readNumberParam(params, "from", { integer: true });
const lines = readNumberParam(params, "lines", { integer: true });
const { readAgentMemoryFile, resolveMemoryBackendConfig } = await loadMemoryToolRuntime();
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
if (resolved.backend === "builtin") {
try {
const result = await readAgentMemoryFile({
cfg,
agentId,
relPath,
from: from ?? undefined,
lines: lines ?? undefined,
});
return jsonResult(result);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return jsonResult({ path: relPath, text: "", disabled: true, error: message });
}
}
const memory = await getMemoryManagerContextWithPurpose({
cfg,
agentId,
purpose: "status",
});
if ("error" in memory) {
return jsonResult({ path: relPath, text: "", disabled: true, error: memory.error });
}
try {
const result = await memory.manager.readFile({
relPath,
from: from ?? undefined,
lines: lines ?? undefined,
});
return jsonResult(result);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return jsonResult({ path: relPath, text: "", disabled: true, error: message });
}
},
});
}