UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

258 lines (257 loc) • 11.7 kB
import { p as shortenHomePath } from "./utils-DUAwk2p5.mjs"; import { o as safeRealpathSync$1 } from "./boundary-path-CAv_plX7.mjs"; import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs"; import { t as findGitRoot } from "./git-root-Bp6q-ESK.mjs"; import { d as resolveAgentWorkspaceDir } from "./agent-scope-config-BU4qc9bA.mjs"; import { o as tryResolveDefaultAgentId } from "./agent-roster-B45JERT5.mjs"; import "./agent-scope-DWB4x52T.mjs"; 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-BUq0D-EI.mjs"; import { t as DEFAULT_AGENTS_FILENAME } from "./workspace-bootstrap-policy-DaS4N8nC.mjs"; import "./workspace-BUeMloSh.mjs"; import { t as note } from "./note-BpWs6H5N.mjs"; import fs from "node:fs"; import path from "node:path"; import { readRegularFile } from "@openclaw/fs-safe/advanced"; import { safeStatSync } from "@openclaw/fs-safe/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 the workspace git-backup tip when the workspace exists but is not a git repo. */ function collectWorkspaceBackupTip(workspaceDir) { if (!safeStatSync(workspaceDir)?.isDirectory()) return null; const resolvedWorkspaceDir = safeRealpathSync$1(workspaceDir); if (!resolvedWorkspaceDir || findGitRoot(resolvedWorkspaceDir)) return null; return "- Tip: back up the agent workspace in a private git repo; keep ~/.openclaw out of git (credentials, sessions). Details: /concepts/agent-workspace#git-backup-recommended-private"; } /** 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); const unchanged = { changed: false, canonicalPath: detection.canonicalPath, legacyPath: detection.legacyPath, mergedLegacy: false }; if (!detection.canonicalExists || !detection.legacyExists) return unchanged; const skippedForReadFailure = (err) => { const isTooLarge = typeof err === "object" && err !== null && "message" in err && typeof err.message === "string" && err.message.startsWith("File exceeds"); return { ...unchanged, 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 { ...unchanged, 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, 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, mergedLegacy: canonicalText !== legacyText, archivedLegacyPath }; } /** 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 || migration.readError) { const reason = migration.readLimitExceeded ? "a file exceeded the safe read limit" : "a file could not be read"; note([ `${prefix}Workspace memory root repair skipped (${reason}):`, `- 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, `- removed legacy file: ${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 { shouldSuggestMemorySystem as a, noteWorkspaceMemoryHealth as i, collectWorkspaceBackupTip as n, maybeRepairWorkspaceMemoryHealth as r, MEMORY_SYSTEM_PROMPT as t };