openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
554 lines (553 loc) • 20.2 kB
JavaScript
import { y as parseDateStringTimestampMs } from "./number-coercion-CLj0HTDM.js";
import "./src-vebZIeLe.js";
import { t as expectDefined } from "./expect-CyE8FADM.js";
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { m as resolveAgentWorkspaceDir, o as listAgentIds, t as AgentSelectionRequiredError, w as tryResolveAmbientOwnerAgentId, y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import "./agent-scope-DbtJyKUL.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { C as resolveMemoryDreamingPluginConfig, D as resolveMemoryRemDreamingConfig, E as resolveMemoryLightDreamingConfig, S as resolveMemoryDreamingConfig, T as resolveMemoryDreamingWorkspaces, x as resolveMemoryDeepDreamingConfig } from "./dreaming-DmJZL5-O.js";
import { a as getActiveMemorySearchManagerCore } from "./memory-runtime-DgrGeT4a.js";
import "./server-utils-BUZaV07G.js";
import { a as memory_core_bundled_runtime_exports } from "./memory-core-bundled-runtime-CacBOGXk.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/gateway/server-methods/doctor.ts
const MANAGED_DEEP_SLEEP_CRON_NAME = "Memory Dreaming Promotion";
const MANAGED_DEEP_SLEEP_CRON_TAG = "[managed-by=memory-core.short-term-promotion]";
const DEEP_SLEEP_SYSTEM_EVENT_TEXT = "__openclaw_memory_core_short_term_promotion_dream__";
const DREAM_DIARY_FILE_NAMES = ["DREAMS.md", "dreams.md"];
function extractIsoDayFromPath(filePath) {
return filePath.replaceAll("\\", "/").match(/(\d{4}-\d{2}-\d{2})(?:-[^/]+)?\.md$/i)?.[1] ?? null;
}
function groundedMarkdownToDiaryLines(markdown) {
return markdown.split("\n").map((line) => line.replace(/^##\s+/, "").trimEnd()).filter((line, index, lines) => line.length > 0 || index > 0 && expectDefined(lines[index - 1], "lines entry at index 1")?.length > 0);
}
async function listWorkspaceDailyFiles(memoryDir) {
let entries;
try {
entries = await fs.readdir(memoryDir);
} catch (err) {
if (err?.code === "ENOENT") return [];
throw err;
}
return entries.filter((name) => /^\d{4}-\d{2}-\d{2}(?:-[^/]+)?\.md$/i.test(name)).map((name) => path.join(memoryDir, name)).toSorted((left, right) => left.localeCompare(right));
}
function resolveDreamingConfig(cfg) {
const resolved = resolveMemoryDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
cfg
});
const light = resolveMemoryLightDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
cfg
});
const deep = resolveMemoryDeepDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
cfg
});
const rem = resolveMemoryRemDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
cfg
});
return {
enabled: resolved.enabled,
...resolved.timezone ? { timezone: resolved.timezone } : {},
verboseLogging: resolved.verboseLogging,
storageMode: resolved.storage.mode,
separateReports: resolved.storage.separateReports,
shortTermEntries: [],
signalEntries: [],
promotedEntries: [],
phases: {
light: {
enabled: light.enabled,
cron: light.cron,
lookbackDays: light.lookbackDays,
limit: light.limit,
managedCronPresent: false
},
deep: {
enabled: deep.enabled,
cron: deep.cron,
limit: deep.limit,
minScore: deep.minScore,
minRecallCount: deep.minRecallCount,
minUniqueQueries: deep.minUniqueQueries,
recencyHalfLifeDays: deep.recencyHalfLifeDays,
managedCronPresent: false,
...typeof deep.maxAgeDays === "number" ? { maxAgeDays: deep.maxAgeDays } : {}
},
rem: {
enabled: rem.enabled,
cron: rem.cron,
lookbackDays: rem.lookbackDays,
limit: rem.limit,
minPatternStrength: rem.minPatternStrength,
managedCronPresent: false
}
}
};
}
const DREAMING_ENTRY_LIST_LIMIT = 8;
function parseDreamingTimestampMs(value) {
return parseDateStringTimestampMs(value) ?? Number.NEGATIVE_INFINITY;
}
function compareDreamingEntryByRecency(a, b) {
const aMs = parseDreamingTimestampMs(a.lastRecalledAt);
const bMs = parseDreamingTimestampMs(b.lastRecalledAt);
if (bMs !== aMs) return bMs > aMs ? 1 : -1;
if (b.totalSignalCount !== a.totalSignalCount) return b.totalSignalCount - a.totalSignalCount;
return a.path.localeCompare(b.path);
}
function compareDreamingEntryBySignals(a, b) {
if (b.totalSignalCount !== a.totalSignalCount) return b.totalSignalCount - a.totalSignalCount;
if (b.phaseHitCount !== a.phaseHitCount) return b.phaseHitCount - a.phaseHitCount;
return compareDreamingEntryByRecency(a, b);
}
function compareDreamingEntryByPromotion(a, b) {
const aMs = parseDreamingTimestampMs(a.promotedAt);
const bMs = parseDreamingTimestampMs(b.promotedAt);
if (bMs !== aMs) return bMs > aMs ? 1 : -1;
return compareDreamingEntryBySignals(a, b);
}
function trimDreamingEntries(entries, compare) {
const selected = [];
for (const entry of entries) {
let insertAt = selected.length;
for (let index = 0; index < selected.length; index += 1) if (compare(entry, expectDefined(selected[index], "selected entry at index")) < 0) {
insertAt = index;
break;
}
if (insertAt < DREAMING_ENTRY_LIST_LIMIT) {
selected.splice(insertAt, 0, entry);
if (selected.length > DREAMING_ENTRY_LIST_LIMIT) selected.pop();
} else if (selected.length < DREAMING_ENTRY_LIST_LIMIT) selected.push(entry);
}
return selected;
}
async function loadDreamingStoreStats(workspaceDir, nowMs, loadShortTermPromotionDreamingStats, timezone) {
try {
return await loadShortTermPromotionDreamingStats({
workspaceDir,
nowMs,
timezone
});
} catch (err) {
return {
shortTermCount: 0,
recallSignalCount: 0,
dailySignalCount: 0,
groundedSignalCount: 0,
totalSignalCount: 0,
phaseSignalCount: 0,
lightPhaseHitCount: 0,
remPhaseHitCount: 0,
promotedTotal: 0,
promotedToday: 0,
shortTermEntries: [],
signalEntries: [],
promotedEntries: [],
storeError: formatErrorMessage(err)
};
}
}
function mergeDreamingStoreStats(stats) {
let shortTermCount = 0;
let recallSignalCount = 0;
let dailySignalCount = 0;
let groundedSignalCount = 0;
let totalSignalCount = 0;
let phaseSignalCount = 0;
let lightPhaseHitCount = 0;
let remPhaseHitCount = 0;
let promotedTotal = 0;
let promotedToday = 0;
let latestPromotedAtMs = Number.NEGATIVE_INFINITY;
let lastPromotedAt;
const storePaths = /* @__PURE__ */ new Set();
const phaseSignalPaths = /* @__PURE__ */ new Set();
const storeErrors = [];
const phaseSignalErrors = [];
const shortTermEntries = [];
const signalEntries = [];
const promotedEntries = [];
for (const stat of stats) {
shortTermCount += stat.shortTermCount;
recallSignalCount += stat.recallSignalCount;
dailySignalCount += stat.dailySignalCount;
groundedSignalCount += stat.groundedSignalCount;
totalSignalCount += stat.totalSignalCount;
phaseSignalCount += stat.phaseSignalCount;
lightPhaseHitCount += stat.lightPhaseHitCount;
remPhaseHitCount += stat.remPhaseHitCount;
promotedTotal += stat.promotedTotal;
promotedToday += stat.promotedToday;
if (stat.storePath) storePaths.add(stat.storePath);
if (stat.phaseSignalPath) phaseSignalPaths.add(stat.phaseSignalPath);
if (stat.storeError) storeErrors.push(stat.storeError);
if (stat.phaseSignalError) phaseSignalErrors.push(stat.phaseSignalError);
shortTermEntries.push(...stat.shortTermEntries);
signalEntries.push(...stat.signalEntries);
promotedEntries.push(...stat.promotedEntries);
const promotedAtMs = stat.lastPromotedAt ? Date.parse(stat.lastPromotedAt) : NaN;
if (Number.isFinite(promotedAtMs) && promotedAtMs > latestPromotedAtMs) {
latestPromotedAtMs = promotedAtMs;
lastPromotedAt = stat.lastPromotedAt;
}
}
return {
shortTermCount,
recallSignalCount,
dailySignalCount,
groundedSignalCount,
totalSignalCount,
phaseSignalCount,
lightPhaseHitCount,
remPhaseHitCount,
promotedTotal,
promotedToday,
shortTermEntries: trimDreamingEntries(shortTermEntries, compareDreamingEntryByRecency),
signalEntries: trimDreamingEntries(signalEntries, compareDreamingEntryBySignals),
promotedEntries: trimDreamingEntries(promotedEntries, compareDreamingEntryByPromotion),
...storePaths.size === 1 ? { storePath: [...storePaths][0] } : {},
...phaseSignalPaths.size === 1 ? { phaseSignalPath: [...phaseSignalPaths][0] } : {},
...lastPromotedAt ? { lastPromotedAt } : {},
...storeErrors.length === 1 ? { storeError: storeErrors[0] } : storeErrors.length > 1 ? { storeError: `${storeErrors.length} dreaming stores had read errors.` } : {},
...phaseSignalErrors.length === 1 ? { phaseSignalError: phaseSignalErrors[0] } : phaseSignalErrors.length > 1 ? { phaseSignalError: `${phaseSignalErrors.length} phase signal stores had read errors.` } : {}
};
}
function isManagedDreamingJob(job, params) {
if (normalizeOptionalString(job.description)?.includes(params.tag)) return true;
const name = normalizeOptionalString(job.name);
const payloadKind = normalizeOptionalString(job.payload?.kind)?.toLowerCase();
const payloadText = normalizeOptionalString(job.payload?.text);
return name === params.name && payloadKind === "systemevent" && payloadText === params.payloadText;
}
async function resolveManagedDreamingCronStatus(params) {
if (!params.context.cron || typeof params.context.cron.list !== "function") return { managedCronPresent: false };
try {
const managed = (await params.context.cron.list({ includeDisabled: true })).filter((job) => typeof job === "object" && job !== null).filter((job) => isManagedDreamingJob(job, params.match));
let nextRunAtMs;
for (const job of managed) {
if (job.enabled !== true) continue;
const candidate = job.state?.nextRunAtMs;
if (typeof candidate !== "number" || !Number.isFinite(candidate)) continue;
if (nextRunAtMs === void 0 || candidate < nextRunAtMs) nextRunAtMs = candidate;
}
return {
managedCronPresent: managed.length > 0,
...nextRunAtMs !== void 0 ? { nextRunAtMs } : {}
};
} catch {
return { managedCronPresent: false };
}
}
async function resolveAllManagedDreamingCronStatuses(context) {
const sweepStatus = await resolveManagedDreamingCronStatus({
context,
match: {
name: MANAGED_DEEP_SLEEP_CRON_NAME,
tag: MANAGED_DEEP_SLEEP_CRON_TAG,
payloadText: DEEP_SLEEP_SYSTEM_EVENT_TEXT
}
});
return {
light: sweepStatus,
deep: sweepStatus,
rem: sweepStatus
};
}
async function readDreamDiary(workspaceDir) {
for (const name of DREAM_DIARY_FILE_NAMES) {
const filePath = path.join(workspaceDir, name);
let stat;
try {
stat = await fs.lstat(filePath);
} catch (err) {
if (err?.code === "ENOENT") continue;
return {
found: false,
path: name
};
}
if (stat.isSymbolicLink() || !stat.isFile()) continue;
try {
return {
found: true,
path: name,
content: await fs.readFile(filePath, "utf-8"),
updatedAtMs: Math.floor(stat.mtimeMs)
};
} catch {
return {
found: false,
path: name
};
}
}
return {
found: false,
path: DREAM_DIARY_FILE_NAMES[0]
};
}
function shouldProbeMemoryEmbeddings(params) {
if (!params || typeof params !== "object") return false;
const record = params;
return record.probe === true || record.deep === true;
}
function resolveDoctorMemoryAgent(context, params, respond, omittedAgentId) {
const cfg = context.getRuntimeConfig();
const rawAgentId = asOptionalRecord(params)?.agentId;
if (rawAgentId !== void 0 && typeof rawAgentId !== "string") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "agentId must be a string"));
return null;
}
const requestedAgentId = typeof rawAgentId === "string" ? normalizeAgentId(rawAgentId) : void 0;
let agentId = requestedAgentId ?? omittedAgentId;
if (!agentId) try {
agentId = resolveDefaultAgentId(cfg, {
surface: "doctor memory",
hint: "Pass agentId to select a configured agent."
});
} catch (error) {
if (!(error instanceof AgentSelectionRequiredError)) throw error;
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message));
return null;
}
if (requestedAgentId && !listAgentIds(cfg).includes(agentId)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown agent id "${requestedAgentId}"`));
return null;
}
return {
cfg,
agentId,
...requestedAgentId ? { requestedAgentId } : {}
};
}
function resolveDoctorMemoryTarget(context, params, respond) {
const resolved = resolveDoctorMemoryAgent(context, params, respond);
if (!resolved) return null;
return {
cfg: resolved.cfg,
agentId: resolved.agentId,
workspaceDir: resolveAgentWorkspaceDir(resolved.cfg, resolved.agentId)
};
}
const SKIPPED_MEMORY_EMBEDDING_PROBE = {
ok: false,
checked: false,
error: "memory embedding readiness not checked; run `openclaw memory status --deep` to probe"
};
const createDoctorHandlers = (memoryCoreRuntime = memory_core_bundled_runtime_exports) => ({
"doctor.memory.status": async ({ respond, context, params }) => {
const resolved = resolveDoctorMemoryAgent(context, params, respond, tryResolveAmbientOwnerAgentId(context.getRuntimeConfig()));
if (!resolved) return;
const { cfg, agentId, requestedAgentId } = resolved;
const { manager, error } = await getActiveMemorySearchManagerCore({
cfg,
agentId,
purpose: "status"
});
if (!manager) {
respond(true, {
agentId,
embedding: {
ok: false,
error: error ?? "memory search unavailable"
}
}, void 0);
return;
}
try {
let status = manager.status();
const shouldProbe = shouldProbeMemoryEmbeddings(params);
let embedding = shouldProbe ? await manager.probeEmbeddingAvailability() : manager.getCachedEmbeddingAvailability?.() ?? SKIPPED_MEMORY_EMBEDDING_PROBE;
if (shouldProbe) status = manager.status();
if (!embedding.ok && !embedding.error) embedding = {
ok: false,
error: "memory embeddings unavailable"
};
const nowMs = Date.now();
const dreamingConfig = resolveDreamingConfig(cfg);
const workspaceDir = normalizeOptionalString(status.workspaceDir);
const configuredWorkspaces = requestedAgentId ? workspaceDir ? [workspaceDir] : [] : resolveMemoryDreamingWorkspaces(cfg, {
primaryWorkspaceDir: workspaceDir,
primaryAgentId: agentId
}).map((entry) => entry.workspaceDir);
const allWorkspaces = configuredWorkspaces.length > 0 ? configuredWorkspaces : workspaceDir ? [workspaceDir] : [];
const storeStats = allWorkspaces.length > 0 ? mergeDreamingStoreStats(await Promise.all(allWorkspaces.map((entry) => loadDreamingStoreStats(entry, nowMs, memoryCoreRuntime.loadShortTermPromotionDreamingStats, dreamingConfig.timezone)))) : {
shortTermCount: 0,
recallSignalCount: 0,
dailySignalCount: 0,
groundedSignalCount: 0,
totalSignalCount: 0,
phaseSignalCount: 0,
lightPhaseHitCount: 0,
remPhaseHitCount: 0,
promotedTotal: 0,
promotedToday: 0
};
const cronStatuses = await resolveAllManagedDreamingCronStatuses(context);
respond(true, {
agentId,
provider: status.provider,
embedding,
embeddingRuntime: (() => {
const runtime = asOptionalRecord(asOptionalRecord(status.custom)?.llamaCppRuntime);
return runtime?.engine === "llama.cpp" ? runtime : void 0;
})(),
dreaming: {
...dreamingConfig,
...storeStats,
phases: {
light: {
...dreamingConfig.phases.light,
...cronStatuses.light
},
deep: {
...dreamingConfig.phases.deep,
...cronStatuses.deep
},
rem: {
...dreamingConfig.phases.rem,
...cronStatuses.rem
}
}
}
}, void 0);
} catch (err) {
respond(true, {
agentId,
embedding: {
ok: false,
error: `gateway memory probe failed: ${formatErrorMessage(err)}`
}
}, void 0);
} finally {
await manager.close?.().catch(() => {});
}
},
"doctor.memory.dreamDiary": async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
const { agentId, workspaceDir } = target;
respond(true, {
agentId,
...await readDreamDiary(workspaceDir)
}, void 0);
},
"doctor.memory.backfillDreamDiary": async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
const { cfg, agentId, workspaceDir } = target;
const sourceFiles = await listWorkspaceDailyFiles(path.join(workspaceDir, "memory"));
if (sourceFiles.length === 0) {
const dreamDiary = await readDreamDiary(workspaceDir);
respond(true, {
agentId,
path: dreamDiary.path,
action: "backfill",
found: dreamDiary.found,
scannedFiles: 0,
written: 0,
replaced: 0
}, void 0);
return;
}
const grounded = await memoryCoreRuntime.previewGroundedRemMarkdown({
workspaceDir,
inputPaths: sourceFiles
});
const remConfig = resolveMemoryRemDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
cfg
});
const entries = grounded.files.map((file) => {
const isoDay = extractIsoDayFromPath(file.path);
if (!isoDay) return null;
return {
isoDay,
sourcePath: file.path,
bodyLines: groundedMarkdownToDiaryLines(file.renderedMarkdown)
};
}).filter((entry) => entry !== null);
const written = await memoryCoreRuntime.writeBackfillDiaryEntries({
workspaceDir,
entries,
timezone: remConfig.timezone
});
const dreamDiary = await readDreamDiary(workspaceDir);
respond(true, {
agentId,
path: dreamDiary.path,
action: "backfill",
found: dreamDiary.found,
scannedFiles: grounded.scannedFiles,
written: written.written,
replaced: written.replaced
}, void 0);
},
"doctor.memory.resetDreamDiary": async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
const { agentId, workspaceDir } = target;
const removed = await memoryCoreRuntime.removeBackfillDiaryEntries({ workspaceDir });
const dreamDiary = await readDreamDiary(workspaceDir);
respond(true, {
agentId,
path: dreamDiary.path,
action: "reset",
found: dreamDiary.found,
removedEntries: removed.removed
}, void 0);
},
"doctor.memory.resetGroundedShortTerm": async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
const { agentId, workspaceDir } = target;
respond(true, {
agentId,
action: "resetGroundedShortTerm",
removedShortTermEntries: (await memoryCoreRuntime.removeGroundedShortTermCandidates({ workspaceDir })).removed
}, void 0);
},
"doctor.memory.repairDreamingArtifacts": async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
const { agentId, workspaceDir } = target;
const repair = await memoryCoreRuntime.repairDreamingArtifacts({ workspaceDir });
respond(true, {
agentId,
action: "repairDreamingArtifacts",
changed: repair.changed,
archiveDir: repair.archiveDir,
archivedDreamsDiary: repair.archivedDreamsDiary,
archivedSessionCorpus: repair.archivedSessionCorpus,
archivedSessionIngestion: repair.archivedSessionIngestion,
warnings: repair.warnings
}, void 0);
},
"doctor.memory.dedupeDreamDiary": async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
const { agentId, workspaceDir } = target;
const dedupe = await memoryCoreRuntime.dedupeDreamDiaryEntries({ workspaceDir });
const dreamDiary = await readDreamDiary(workspaceDir);
respond(true, {
agentId,
action: "dedupeDreamDiary",
path: dreamDiary.path,
found: dreamDiary.found,
removedEntries: dedupe.removed,
dedupedEntries: dedupe.removed,
keptEntries: dedupe.kept
}, void 0);
}
});
//#endregion
export { createDoctorHandlers };