openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
221 lines (220 loc) • 10.1 kB
JavaScript
import { r as resolveOpenClawPackageRootSync } from "./openclaw-root-BGdnWXIW.mjs";
import { r as resolveOsHomeDir } from "./home-dir-D0b-ywue.mjs";
import { t as expandHomePrefix } from "./home-dir-Dc6nN6HB.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { g as resolveConfigDir, p as shortenHomePath } from "./utils-CitNzLyF.mjs";
import { o as resolveAllAgentSessionStoreTargetsSync } from "./targets-kx08xomQ.mjs";
import { r as hydrateSessionStoreSkillPromptRefs } from "./skill-prompt-blobs-Ah1d_QTY.mjs";
import { t as decodeXml } from "./xml-mfWEXuUN.mjs";
import { t as note } from "./note-Bi0VsI6R.mjs";
import { t as resolveBundledSkillsDir } from "./bundled-dir-CD5eUM41.mjs";
import fs from "node:fs";
import path from "node:path";
//#region src/commands/doctor-session-snapshots.ts
/** Advisory inspection of runtime snapshot paths retained in legacy session stores. */
const SESSION_SNAPSHOTS_CHECK_ID = "core/doctor/session-snapshots";
function resolveSessionSnapshotBundledSkillsDir() {
const resolved = resolveBundledSkillsDir();
if (resolved) return resolved;
const packageRoot = resolveOpenClawPackageRootSync({
argv1: process.argv[1],
moduleUrl: import.meta.url,
cwd: process.cwd()
});
return packageRoot ? path.join(packageRoot, "skills") : void 0;
}
function extractSkillLocations(prompt) {
return typeof prompt === "string" ? [...prompt.matchAll(/<location>([\s\S]*?)<\/location>/g)].flatMap((match) => {
const raw = match[1]?.trim();
return raw ? [decodeXml(raw)] : [];
}) : [];
}
function collectResolvedSkillPaths(value) {
if (!Array.isArray(value)) return [];
const paths = [];
for (const skill of value) {
if (!isRecord(skill)) continue;
const sourceInfo = isRecord(skill.sourceInfo) ? skill.sourceInfo : void 0;
for (const [filePath, baseDir] of [[skill.filePath, skill.baseDir], [sourceInfo?.path, sourceInfo?.baseDir]]) {
if (typeof filePath === "string" && filePath.trim()) paths.push(filePath.trim());
if (typeof baseDir === "string" && baseDir.trim()) paths.push(path.join(baseDir.trim(), "SKILL.md"));
}
}
return paths;
}
function collectInjectedWorkspaceFilePaths(value) {
if (!Array.isArray(value)) return [];
return value.map((entry) => isRecord(entry) && typeof entry.path === "string" ? entry.path.trim() : "").filter(Boolean);
}
function collectCachedSnapshotPaths(entry) {
const snapshot = entry.skillsSnapshot;
const report = entry.systemPromptReport;
return [
["skillsSnapshot.prompt", extractSkillLocations(snapshot?.prompt)],
["skillsSnapshot.resolvedSkills", collectResolvedSkillPaths(snapshot?.resolvedSkills)],
["systemPromptReport.injectedWorkspaceFiles", isRecord(report) ? collectInjectedWorkspaceFilePaths(report.injectedWorkspaceFiles) : []]
].flatMap(([field, paths]) => paths.map((location) => ({
field,
path: location
})));
}
function isAbsolutePathLike(value) {
return path.isAbsolute(value) || path.win32.isAbsolute(value);
}
function splitPathSegments(value) {
return value.replace(/^[a-z]:/i, "").replaceAll("\\", "/").split("/").filter(Boolean);
}
function isWindowsAbsolutePath(value) {
return /^[a-z]:/i.test(value) && ["/", "\\"].includes(value.slice(2, 3)) || value.startsWith("\\\\");
}
function isBundledRuntimeSkillsPath(beforeSkillRoot) {
const lower = beforeSkillRoot.map((segment) => segment.toLowerCase());
const openclawIndex = lower.lastIndexOf("openclaw");
return lower.some((segment) => segment === "dist-runtime" || segment === "node_modules" || segment.startsWith("openclaw@")) || openclawIndex > 0 && ["tmp", "temp"].includes(lower[openclawIndex - 1]);
}
function extractBundledSkillRelativeSegments(cachedPath) {
const segments = splitPathSegments(cachedPath);
const skillRootIndex = segments.lastIndexOf("skills");
if (skillRootIndex < 0 || !isBundledRuntimeSkillsPath(segments.slice(0, skillRootIndex))) return;
const relativeSegments = segments.slice(skillRootIndex + 1);
if (relativeSegments.length < 2 || relativeSegments.at(-1) !== "SKILL.md") return;
return relativeSegments;
}
function isInsidePath(baseDir, candidatePath) {
const baseIsWindows = isWindowsAbsolutePath(baseDir);
if (baseIsWindows !== isWindowsAbsolutePath(candidatePath)) return false;
const pathApi = baseIsWindows ? path.win32 : path;
const relative = pathApi.relative(pathApi.resolve(baseDir), pathApi.resolve(candidatePath));
return relative === "" || !relative.startsWith("..") && !pathApi.isAbsolute(relative);
}
function joinPathForRoot(root, ...segments) {
return isWindowsAbsolutePath(root) ? path.win32.join(root, ...segments) : path.join(root, ...segments);
}
function resolveExpectedBundledSkillPath(params) {
const osHomeDir = resolveOsHomeDir(params.env);
const expandedCachedPath = osHomeDir ? expandHomePrefix(params.cachedPath, { home: osHomeDir }) : params.cachedPath;
if (!isAbsolutePathLike(expandedCachedPath)) return;
const relativeSegments = extractBundledSkillRelativeSegments(expandedCachedPath);
if (!relativeSegments) return;
if (relativeSegments.join("/") === "imsg/SKILL.md") {
const movedPath = path.join(resolveConfigDir(params.env), "plugin-skills", "imsg", "SKILL.md");
if (fs.existsSync(movedPath)) return movedPath;
}
if (isInsidePath(params.bundledSkillsDir, expandedCachedPath)) return;
const expectedPath = joinPathForRoot(params.bundledSkillsDir, ...relativeSegments);
return fs.existsSync(expectedPath) ? expectedPath : void 0;
}
/** Finds cached bundled-skill paths that point at old runtime/temp package roots. */
function scanSessionStoreForStaleRuntimeSnapshotPaths(params) {
const bundledSkillsDir = params.bundledSkillsDir?.trim();
if (!bundledSkillsDir) return [];
const findings = [];
const seen = /* @__PURE__ */ new Set();
for (const [sessionKey, entry] of Object.entries(params.store)) {
if (!entry || typeof entry !== "object") continue;
for (const cached of collectCachedSnapshotPaths(entry)) {
const expectedPath = resolveExpectedBundledSkillPath({
cachedPath: cached.path,
bundledSkillsDir,
env: params.env
});
if (!expectedPath) continue;
const key = `${sessionKey}\0${cached.field}\0${cached.path}`;
if (seen.has(key)) continue;
seen.add(key);
findings.push({
sessionKey,
field: cached.field,
cachedPath: cached.path,
expectedPath
});
}
}
return findings;
}
function loadSessionStoreForSnapshotScan(storePath) {
const parsed = JSON.parse(fs.readFileSync(storePath, "utf-8"));
if (!isRecord(parsed)) return {};
const store = parsed;
hydrateSessionStoreSkillPromptRefs({
storePath,
store
});
return store;
}
async function scanSessionSnapshotHealth(params, onError) {
const bundledSkillsDir = resolveSessionSnapshotBundledSkillsDir();
const stores = [];
if (bundledSkillsDir) {
const storePaths = resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env }).map((target) => target.storePath).filter((storePath) => fs.existsSync(storePath)).toSorted((a, b) => a.localeCompare(b));
for (const storePath of storePaths) {
let store;
try {
store = loadSessionStoreForSnapshotScan(storePath);
} catch (error) {
onError?.(storePath, error);
continue;
}
const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
store,
bundledSkillsDir,
env: params.env
});
if (findings.length > 0) stores.push({
storePath,
findings
});
}
}
return {
bundledSkillsDir,
stores
};
}
async function detectSessionSnapshotHealthIssues(params) {
const { stores } = await scanSessionSnapshotHealth(params);
return stores.flatMap(({ storePath, findings }) => findings.map((finding) => ({
...finding,
storePath
})));
}
function sessionSnapshotIssueToHealthFinding(issue) {
return {
checkId: SESSION_SNAPSHOTS_CHECK_ID,
severity: "info",
message: `${issue.sessionKey} historical session metadata references an inactive runtime root.`,
path: issue.storePath,
target: issue.cachedPath,
requirement: `Current bundled skill path: ${issue.expectedPath}`,
fixHint: "No repair is needed for this historical metadata. Doctor preserves migration originals; active sessions use canonical SQLite state and the current runtime skill catalog."
};
}
/** Reports historical snapshot paths without rewriting migration source bytes. */
async function noteSessionSnapshotHealth(params) {
const { bundledSkillsDir, stores } = await scanSessionSnapshotHealth(params, (storePath, error) => {
note(`- Failed to inspect session snapshot metadata in ${shortenHomePath(storePath)}: ${String(error)}`, "Session snapshots");
});
if (!bundledSkillsDir) return;
const findingsByStore = new Map(stores.map(({ storePath, findings }) => [storePath, findings]));
const totalFindings = [...findingsByStore.values()].reduce((total, findings) => total + findings.length, 0);
if (totalFindings === 0) return;
const affectedSessions = new Set([...findingsByStore.values()].flatMap((findings) => findings.map((finding) => finding.sessionKey)));
const lines = [
`- Found ${affectedSessions.size} session${affectedSessions.size === 1 ? "" : "s"} with stale cached session metadata paths.`,
` Live bundled skills root is healthy: ${shortenHomePath(bundledSkillsDir)}`,
" Historical metadata references an inactive runtime root. Originals are preserved; active sessions use canonical SQLite state and the current runtime skill catalog. No cleanup or session reset is needed."
];
let shown = 0;
for (const [storePath, findings] of findingsByStore) {
lines.push(` Store: ${shortenHomePath(storePath)}`);
for (const finding of findings.slice(0, Math.max(0, 10 - shown))) {
lines.push(` - ${finding.sessionKey} ${finding.field}: ${shortenHomePath(finding.cachedPath)} -> ${shortenHomePath(finding.expectedPath)}`);
shown += 1;
}
if (shown >= 10) break;
}
if (totalFindings > shown) lines.push(` ...and ${totalFindings - shown} more stale cached path${totalFindings - shown === 1 ? "" : "s"}.`);
note(lines.join("\n"), "Session snapshots");
}
//#endregion
export { detectSessionSnapshotHealthIssues, noteSessionSnapshotHealth, sessionSnapshotIssueToHealthFinding };