openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
508 lines (507 loc) • 19.5 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-6kR-86XC.mjs";
import { y as parseDateStringTimestampMs } from "./number-coercion-Dq1l5muL.mjs";
import { i as extractErrorCode } from "./error-coercion-1P2One0X.mjs";
import "./src-SskUVPJG.mjs";
import { t as expectDefined } from "./expect-lbe3Hgrh.mjs";
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { C as tryResolveAmbientOwnerAgentId, d as resolveAgentWorkspaceDir, g as resolveDefaultAgentId, t as AgentSelectionRequiredError } from "./agent-scope-config-BU4qc9bA.mjs";
import "./session-key-BC4m_Ly5.mjs";
import { i as listAgentIds } from "./agent-roster-B45JERT5.mjs";
import "./agent-scope-DWB4x52T.mjs";
import { C as resolveMemoryDreamingPluginConfig, E as resolveMemoryDreamingWorkspaces, O as resolveMemoryRemDreamingConfig, S as resolveMemoryDreamingConfig } from "./dreaming-cjMW10lQ.mjs";
import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs";
import { f as errorShape } from "./error-codes-DI20vDEB.mjs";
import { i as getAgentWorkspaceAccess, t as WorkspaceAccessUnavailableError } from "./workspace-access-CCdQbCKD.mjs";
import { a as getActiveMemorySearchManagerCore } from "./memory-runtime-CmafEEmn.mjs";
import { s as memory_core_bundled_runtime_exports } from "./memory-core-bundled-runtime-muVM17iL.mjs";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/gateway/server-methods/doctor-memory-files.ts
const DREAM_DIARY_FILE_NAMES = ["DREAMS.md", "dreams.md"];
function getWorkspaceMemoryMaintenance(workspaceDir) {
const access = getAgentWorkspaceAccess(workspaceDir, "memoryFiles");
if (!access?.memoryFiles) return;
const files = access.memoryFiles.maintenance;
if (!files) throw new WorkspaceAccessUnavailableError("Remote Memory maintenance is unavailable");
return files;
}
async function listWorkspaceDailyFiles(workspaceDir) {
const memoryDir = path.join(workspaceDir, "memory");
const files = getWorkspaceMemoryMaintenance(workspaceDir);
let entries;
try {
entries = files ? (await files.listDirectory(memoryDir)).map((entry) => entry.name) : await fs.readdir(memoryDir);
} catch (err) {
if (extractErrorCode(err) === "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));
}
async function readDreamDiary(workspaceDir) {
const files = getWorkspaceMemoryMaintenance(workspaceDir);
for (const name of DREAM_DIARY_FILE_NAMES) {
const filePath = path.join(workspaceDir, name);
let stat;
try {
if (files) stat = await files.stat(filePath, false);
else {
const localStat = await fs.lstat(filePath);
stat = {
isSymbolicLink: localStat.isSymbolicLink(),
isFile: localStat.isFile(),
mtimeMs: localStat.mtimeMs
};
}
} catch (err) {
if (extractErrorCode(err) === "ENOENT") continue;
return {
found: false,
path: name
};
}
if (stat.isSymbolicLink || !stat.isFile) continue;
try {
return {
found: true,
path: name,
content: files ? (await files.readFile(filePath)).toString("utf-8") : 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]
};
}
//#endregion
//#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__";
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);
}
function resolveDreamingConfig(cfg) {
const resolved = resolveMemoryDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(cfg),
cfg
});
const { light, deep, rem } = resolved.phases;
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: resolved.enabled && light.enabled,
cron: light.cron,
lookbackDays: light.lookbackDays,
limit: light.limit,
managedCronPresent: false
},
deep: {
enabled: resolved.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: resolved.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 {
...mergeDreamingStoreStats([]),
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) {
if (normalizeOptionalString(job.description)?.includes(MANAGED_DEEP_SLEEP_CRON_TAG)) return true;
const name = normalizeOptionalString(job.name);
const payloadKind = normalizeOptionalString(job.payload?.kind)?.toLowerCase();
const payloadText = normalizeOptionalString(job.payload?.text);
return name === MANAGED_DEEP_SLEEP_CRON_NAME && payloadKind === "systemevent" && payloadText === DEEP_SLEEP_SYSTEM_EVENT_TEXT;
}
async function resolveManagedDreamingCronStatus(context) {
if (!context.cron || typeof context.cron.list !== "function") return { managedCronPresent: false };
try {
const managed = (await context.cron.list({ includeDisabled: true })).filter((job) => typeof job === "object" && job !== null).filter(isManagedDreamingJob);
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 };
}
}
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, tryResolveAmbientOwnerAgentId(context.getRuntimeConfig()));
if (!resolved) return null;
return {
cfg: resolved.cfg,
agentId: resolved.agentId,
workspaceDir: resolveAgentWorkspaceDir(resolved.cfg, resolved.agentId)
};
}
function memoryActionHandler(action, run) {
return async ({ respond, context, params }) => {
const target = resolveDoctorMemoryTarget(context, params, respond);
if (!target) return;
respond(true, {
agentId: target.agentId,
action,
...await run(target)
}, void 0);
};
}
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, searchRuntimeRegistered } = await getActiveMemorySearchManagerCore({
cfg,
agentId,
purpose: "status"
});
if (!manager) {
respond(true, {
agentId,
searchRuntimeRegistered,
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 allWorkspaces = requestedAgentId ? workspaceDir ? [workspaceDir] : [] : resolveMemoryDreamingWorkspaces(cfg, {
primaryWorkspaceDir: workspaceDir,
primaryAgentId: agentId
}).map((entry) => entry.workspaceDir);
const storeStats = mergeDreamingStoreStats(allWorkspaces.length === 0 ? [] : await Promise.all(allWorkspaces.map((entry) => loadDreamingStoreStats(entry, nowMs, memoryCoreRuntime.loadShortTermPromotionDreamingStats, dreamingConfig.timezone))));
const cronStatus = await resolveManagedDreamingCronStatus(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,
...cronStatus
},
deep: {
...dreamingConfig.phases.deep,
...cronStatus
},
rem: {
...dreamingConfig.phases.rem,
...cronStatus
}
}
}
}, 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": memoryActionHandler("backfill", async ({ cfg, workspaceDir }) => {
const sourceFiles = await listWorkspaceDailyFiles(workspaceDir);
if (sourceFiles.length === 0) {
const dreamDiary = await readDreamDiary(workspaceDir);
return {
path: dreamDiary.path,
found: dreamDiary.found,
scannedFiles: 0,
written: 0,
replaced: 0
};
}
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);
return {
path: dreamDiary.path,
found: dreamDiary.found,
scannedFiles: grounded.scannedFiles,
written: written.written,
replaced: written.replaced
};
}),
"doctor.memory.resetDreamDiary": memoryActionHandler("reset", async ({ workspaceDir }) => {
const removed = await memoryCoreRuntime.removeBackfillDiaryEntries({ workspaceDir });
const dreamDiary = await readDreamDiary(workspaceDir);
return {
path: dreamDiary.path,
found: dreamDiary.found,
removedEntries: removed.removed
};
}),
"doctor.memory.resetGroundedShortTerm": memoryActionHandler("resetGroundedShortTerm", async ({ workspaceDir }) => {
return { removedShortTermEntries: (await memoryCoreRuntime.removeGroundedShortTermCandidates({ workspaceDir })).removed };
}),
"doctor.memory.repairDreamingArtifacts": memoryActionHandler("repairDreamingArtifacts", async ({ workspaceDir }) => {
const repair = await memoryCoreRuntime.repairDreamingArtifacts({ workspaceDir });
return {
changed: repair.changed,
archiveDir: repair.archiveDir,
archivedDreamsDiary: repair.archivedDreamsDiary,
archivedSessionCorpus: repair.archivedSessionCorpus,
archivedSessionIngestion: repair.archivedSessionIngestion,
warnings: repair.warnings
};
}),
"doctor.memory.dedupeDreamDiary": memoryActionHandler("dedupeDreamDiary", async ({ workspaceDir }) => {
const dedupe = await memoryCoreRuntime.dedupeDreamDiaryEntries({ workspaceDir });
const dreamDiary = await readDreamDiary(workspaceDir);
return {
path: dreamDiary.path,
found: dreamDiary.found,
removedEntries: dedupe.removed,
dedupedEntries: dedupe.removed,
keptEntries: dedupe.kept
};
})
});
//#endregion
export { createDoctorHandlers };