UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

508 lines (507 loc) • 19.5 kB
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 };