UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

232 lines (231 loc) • 10.3 kB
import { o as safeRealpathSync$1 } from "./boundary-path-CAv_plX7.mjs"; import { p as shortenHomePath } from "./utils-CitNzLyF.mjs"; import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import { t as findGitRoot } from "./git-root-ChTibfp1.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-Cs7BM4-K.mjs"; import { t as DEFAULT_AGENTS_FILENAME } from "./workspace-bootstrap-policy-CL8uSPRO.mjs"; import "./workspace-C4gwB73P.mjs"; import { t as note } from "./note-Bi0VsI6R.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"; import { isUtf8 } from "node:buffer"; //#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"; } 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); return stat.isFile() ? { exists: true, bytes: stat.size } : { exists: false }; } 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; } } async function detectRootMemoryFiles(workspaceDir) { const resolvedWorkspace = path.resolve(workspaceDir); const canonicalPath = resolveCanonicalRootMemoryPath(resolvedWorkspace); const legacyPath = resolveLegacyRootMemoryPath(resolvedWorkspace); const entries = await listWorkspaceEntries(resolvedWorkspace); const inspect = (filePath) => entries.has(path.basename(filePath)) ? statIfExists(filePath) : Promise.resolve({ exists: false }); const [canonical, legacy] = await Promise.all([inspect(canonicalPath), inspect(legacyPath)]); 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"; } 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 message = err instanceof Error ? err.message : ""; return { ...unchanged, skipReason: message.startsWith("File exceeds") ? "a file exceeded the safe read limit" : message.startsWith("File is not valid UTF-8:") ? message : "a file could not be read" }; }; const readMemoryFile = async (filePath) => { const { buffer } = await readRegularFile({ filePath, maxBytes: ROOT_MEMORY_FILE_MAX_BYTES }); if (!isUtf8(buffer)) throw new Error(`File is not valid UTF-8: ${filePath}; preserve its bytes and convert a copy to UTF-8 before retrying`); return buffer.toString("utf-8"); }; try { await Promise.all([readMemoryFile(detection.canonicalPath), readMemoryFile(detection.legacyPath)]); } catch (err) { return skippedForReadFailure(err); } let archivedLegacyPath; try { archivedLegacyPath = await moveLegacyRootMemoryFileToArchive({ workspaceDir: detection.workspaceDir, legacyPath: detection.legacyPath }); } catch { return { ...unchanged, skipReason: "legacy memory could not be archived atomically" }; } let canonicalText; let legacyText; try { [canonicalText, legacyText] = await Promise.all([readMemoryFile(detection.canonicalPath), readMemoryFile(archivedLegacyPath)]); } 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 { ...unchanged, changed: true, mergedLegacy: canonicalText !== legacyText, archivedLegacyPath }; } async function noteWorkspaceMemoryHealth(scope) { try { const detection = await detectRootMemoryFiles(scope.workspaceDir); if (detection.canonicalExists && detection.legacyExists) { const rootMemoryWarning = [ "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"); note(`${scope.labelAgent ? `Agent "${scope.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 prefix = params.scope.labelAgent ? `Agent "${params.scope.agentId}": ` : ""; const rootMemoryFiles = await detectRootMemoryFiles(params.scope.workspaceDir); 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(params.scope.workspaceDir); const reason = migration.skipReason; if (reason) { 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.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 };