UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

265 lines (264 loc) 11.7 kB
import { m as shortenHomePath } from "./utils-P__uGsPB.js"; import { E as tryResolveDefaultAgentId, m as resolveAgentWorkspaceDir } from "./agent-scope-config-DcbEhP0R.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { r as readRegularFile } from "./regular-file-Equ4rrfE.js"; import "./agent-scope-DbtJyKUL.js"; import { a as resolveCanonicalRootMemoryPath, n as LEGACY_ROOT_MEMORY_FILENAME, o as resolveLegacyRootMemoryPath, s as resolveRootMemoryRepairDir, t as CANONICAL_ROOT_MEMORY_FILENAME } from "./root-memory-files-IL5Gznz4.js"; import { t as DEFAULT_AGENTS_FILENAME } from "./workspace-ConDEamr.js"; import { t as note } from "./note-DqHk3fA1.js"; import fs from "node:fs"; import path from "node:path"; //#region src/commands/doctor-workspace.ts /** Doctor checks and repairs for workspace memory files and legacy workspace hints. */ const AGENTS_MD_MAX_BYTES = 1048576; const ROOT_MEMORY_FILE_MAX_BYTES = 8388608; const MEMORY_SYSTEM_PROMPT = [ "Memory system not found in workspace.", "Paste this into your agent:", "", "Install the memory system by applying:", "https://github.com/openclaw/openclaw/commit/9ffea23f31ca1df5183b25668f8f814bee0fb34e", "https://github.com/openclaw/openclaw/commit/7d1fee70e76f2f634f1b41fca927ee663914183a" ].join("\n"); /** Returns true when the workspace appears to lack canonical memory guidance. */ async function shouldSuggestMemorySystem(workspaceDir) { if ((await listWorkspaceEntries(workspaceDir)).has("MEMORY.md")) try { if ((await fs.promises.stat(resolveCanonicalRootMemoryPath(workspaceDir))).isFile()) return false; } catch {} const agentsPath = path.join(workspaceDir, DEFAULT_AGENTS_FILENAME); try { const resolvedAgentsPath = await fs.promises.realpath(agentsPath); const { buffer } = await readRegularFile({ filePath: resolvedAgentsPath, maxBytes: AGENTS_MD_MAX_BYTES }); if (new RegExp(`\\b${"MEMORY.md".replace(".", "\\.")}\\b`).test(buffer.toString("utf-8"))) return false; } catch {} return true; } async function statIfExists(filePath) { try { const stat = await fs.promises.stat(filePath); if (!stat.isFile()) return { exists: false }; return { exists: true, bytes: stat.size }; } catch (err) { if (err?.code === "ENOENT") return { exists: false }; throw err; } } async function listWorkspaceEntries(workspaceDir) { try { return new Set(await fs.promises.readdir(workspaceDir)); } catch (err) { if (err?.code === "ENOENT") return /* @__PURE__ */ new Set(); throw err; } } /** Detects canonical and legacy root memory files in a workspace. */ async function detectRootMemoryFiles(workspaceDir) { const resolvedWorkspace = path.resolve(workspaceDir); const canonicalPath = resolveCanonicalRootMemoryPath(resolvedWorkspace); const legacyPath = resolveLegacyRootMemoryPath(resolvedWorkspace); const entries = await listWorkspaceEntries(resolvedWorkspace); const [canonical, legacy] = await Promise.all([entries.has("MEMORY.md") ? statIfExists(canonicalPath) : Promise.resolve({ exists: false }), entries.has("memory.md") ? statIfExists(legacyPath) : Promise.resolve({ exists: false })]); return { workspaceDir: resolvedWorkspace, canonicalPath, legacyPath, canonicalExists: canonical.exists, legacyExists: legacy.exists, ...typeof canonical.bytes === "number" ? { canonicalBytes: canonical.bytes } : {}, ...typeof legacy.bytes === "number" ? { legacyBytes: legacy.bytes } : {} }; } function formatBytes(bytes) { return typeof bytes === "number" ? `${bytes} bytes` : "size unknown"; } /** Formats the warning for split canonical/legacy root memory files. */ function formatRootMemoryFilesWarning(detection) { if (detection.canonicalExists && detection.legacyExists) return [ "Split root durable memory files detected:", `- canonical: ${shortenHomePath(detection.canonicalPath)} (${formatBytes(detection.canonicalBytes)})`, `- legacy: ${shortenHomePath(detection.legacyPath)} (${formatBytes(detection.legacyBytes)})`, `OpenClaw uses ${CANONICAL_ROOT_MEMORY_FILENAME} as the canonical durable memory file.`, `Dreaming writes durable promotions to ${CANONICAL_ROOT_MEMORY_FILENAME}, so older facts in ${LEGACY_ROOT_MEMORY_FILENAME} can be shadowed.`, `Run "openclaw doctor --fix" to merge the legacy file into ${CANONICAL_ROOT_MEMORY_FILENAME} with a backup.` ].join("\n"); return null; } async function moveLegacyRootMemoryFileToArchive(params) { const repairDir = resolveRootMemoryRepairDir(params.workspaceDir); await fs.promises.mkdir(repairDir, { recursive: true }); const archiveDir = path.join(repairDir, (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-").replaceAll(".", "-")); await fs.promises.mkdir(archiveDir, { recursive: true }); const archivePath = path.join(archiveDir, LEGACY_ROOT_MEMORY_FILENAME); await fs.promises.rename(params.legacyPath, archivePath); return archivePath; } function buildMergedLegacyRootMemorySection(params) { return [ "", `## Imported From Legacy Root ${LEGACY_ROOT_MEMORY_FILENAME}`, "", `<!-- openclaw-root-memory-merge source=${LEGACY_ROOT_MEMORY_FILENAME} archived=${params.archivedLegacyPath} -->`, `This content came from legacy root \`${LEGACY_ROOT_MEMORY_FILENAME}\`, which was shadowed by \`${CANONICAL_ROOT_MEMORY_FILENAME}\`.`, "", params.legacyText.trim(), "" ].join("\n"); } /** Archives and merges a legacy root memory file into canonical memory. */ async function migrateLegacyRootMemoryFile(workspaceDir) { const detection = await detectRootMemoryFiles(workspaceDir); if (!detection.canonicalExists || !detection.legacyExists) return { changed: false, canonicalPath: detection.canonicalPath, legacyPath: detection.legacyPath, removedLegacy: false, mergedLegacy: false }; const skippedForReadFailure = (err) => { const isTooLarge = typeof err === "object" && err !== null && "message" in err && typeof err.message === "string" && err.message.startsWith("File exceeds"); return { changed: false, canonicalPath: detection.canonicalPath, legacyPath: detection.legacyPath, removedLegacy: false, mergedLegacy: false, readLimitExceeded: isTooLarge, readError: !isTooLarge }; }; try { await Promise.all([readRegularFile({ filePath: detection.canonicalPath, maxBytes: ROOT_MEMORY_FILE_MAX_BYTES }), readRegularFile({ filePath: detection.legacyPath, maxBytes: ROOT_MEMORY_FILE_MAX_BYTES })]); } catch (err) { return skippedForReadFailure(err); } let archivedLegacyPath; try { archivedLegacyPath = await moveLegacyRootMemoryFileToArchive({ workspaceDir: detection.workspaceDir, legacyPath: detection.legacyPath }); } catch { return { changed: false, canonicalPath: detection.canonicalPath, legacyPath: detection.legacyPath, removedLegacy: false, mergedLegacy: false, archiveError: true }; } let canonicalText; let legacyText; try { [canonicalText, legacyText] = await Promise.all([readRegularFile({ filePath: detection.canonicalPath, maxBytes: ROOT_MEMORY_FILE_MAX_BYTES }).then(({ buffer }) => buffer.toString("utf-8")), readRegularFile({ filePath: archivedLegacyPath, maxBytes: ROOT_MEMORY_FILE_MAX_BYTES }).then(({ buffer }) => buffer.toString("utf-8"))]); } catch (err) { return { ...skippedForReadFailure(err), changed: true, removedLegacy: true, archivedLegacyPath }; } if (canonicalText !== legacyText) { const merged = `${canonicalText.trimEnd()}\n${buildMergedLegacyRootMemorySection({ legacyText, archivedLegacyPath: shortenHomePath(archivedLegacyPath) })}`; await fs.promises.writeFile(detection.canonicalPath, merged, "utf-8"); } return { changed: true, canonicalPath: detection.canonicalPath, legacyPath: detection.legacyPath, removedLegacy: true, mergedLegacy: canonicalText !== legacyText, archivedLegacyPath, ...typeof detection.legacyBytes === "number" ? { copiedBytes: detection.legacyBytes } : {} }; } /** Emits workspace root-memory health warnings. */ async function noteWorkspaceMemoryHealth(cfg, scope) { try { const agentId = scope?.agentId ?? tryResolveDefaultAgentId(cfg); if (!agentId) throw new Error("Cannot inspect workspace memory until the agent roster has one default"); const rootMemoryWarning = formatRootMemoryFilesWarning(await detectRootMemoryFiles(scope?.workspaceDir ?? resolveAgentWorkspaceDir(cfg, agentId))); if (rootMemoryWarning) note(`${scope?.labelAgent ? `Agent "${agentId}":\n` : ""}${rootMemoryWarning}`, "Workspace memory"); } catch (err) { const prefix = scope?.labelAgent ? `Agent "${scope.agentId}": ` : ""; note(`${prefix}Workspace memory audit could not be completed: ${formatErrorMessage(err)}`, "Doctor"); } } /** Prompts to merge legacy root memory into canonical memory when both files exist. */ async function maybeRepairWorkspaceMemoryHealth(params) { try { const agentId = params.scope?.agentId ?? tryResolveDefaultAgentId(params.cfg); if (!agentId) throw new Error("Cannot repair workspace memory until the agent roster has one default"); const configuredWorkspaceDir = params.scope?.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId); const prefix = params.scope?.labelAgent ? `Agent "${agentId}": ` : ""; const rootMemoryFiles = await detectRootMemoryFiles(configuredWorkspaceDir); if (!rootMemoryFiles.canonicalExists || !rootMemoryFiles.legacyExists) return; if (!await params.prompter.confirmRuntimeRepair({ message: `${prefix}Merge legacy root memory.md into canonical MEMORY.md and remove the shadowed file?`, initialValue: true })) return; const migration = await migrateLegacyRootMemoryFile(configuredWorkspaceDir); if (migration.readLimitExceeded) { note([ `${prefix}Workspace memory root repair skipped (a file exceeded the safe read limit):`, `- canonical: ${migration.canonicalPath}`, `- legacy: ${migration.legacyPath}`, migration.archivedLegacyPath ? `- preserved archive: ${migration.archivedLegacyPath}` : null ].filter((line) => Boolean(line)).join("\n"), "Doctor changes"); return; } if (migration.readError) { note([ `${prefix}Workspace memory root repair skipped (a file could not be read):`, `- canonical: ${migration.canonicalPath}`, `- legacy: ${migration.legacyPath}`, migration.archivedLegacyPath ? `- preserved archive: ${migration.archivedLegacyPath}` : null ].filter((line) => Boolean(line)).join("\n"), "Doctor changes"); return; } if (migration.archiveError) { note([ `${prefix}Workspace memory root repair skipped (legacy memory could not be archived atomically):`, `- canonical: ${migration.canonicalPath}`, `- legacy: ${migration.legacyPath}` ].join("\n"), "Doctor changes"); return; } if (!migration.changed) return; const lines = [ `${prefix}Workspace memory root merged:`, `- canonical: ${migration.canonicalPath}`, migration.archivedLegacyPath ? `- backup: ${migration.archivedLegacyPath}` : null, migration.mergedLegacy ? `- merged legacy content from: ${migration.legacyPath}` : null, migration.removedLegacy ? `- removed legacy file: ${migration.legacyPath}` : `- legacy file still present: ${migration.legacyPath}` ].filter(Boolean); note(lines.join("\n"), "Doctor changes"); } catch (err) { const prefix = params.scope?.labelAgent ? `Agent "${params.scope.agentId}": ` : ""; note(`${prefix}Workspace memory repair could not be completed: ${formatErrorMessage(err)}`, "Doctor"); } } //#endregion export { migrateLegacyRootMemoryFile as a, maybeRepairWorkspaceMemoryHealth as i, detectRootMemoryFiles as n, noteWorkspaceMemoryHealth as o, formatRootMemoryFilesWarning as r, shouldSuggestMemorySystem as s, MEMORY_SYSTEM_PROMPT as t };