openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
367 lines (366 loc) • 18.9 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { t as exitCliAfterOutput } from "./one-shot-exit-BkFPnjbM.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import "./session-key-BnWWjqNc.js";
import { n as formatConsoleDiagnosticLine } from "./json-console-line-C0Xvs26k.js";
import { s as resolvePackageExtensionEntries } from "./manifest-ByRdkf9X.js";
import { t as VERSION } from "./version-v1kuAkGj.js";
import { dt as resolveSqliteDatabaseFilePaths } from "./openclaw-state-db-BRTnL-D8.js";
import { r as validatePackageExtensionEntriesForInstall } from "./package-entry-resolution-DZYcOePc.js";
import { r as resolveInstalledPluginIndexInstallOwner } from "./installed-plugin-index-install-owner-Bd-Byre8.js";
import { a as readPersistedInstalledPluginIndex } from "./installed-plugin-index-store-MVV6aa7C.js";
import { i as resolvePluginVersionDriftUpdateCommand, r as resolvePluginVersionDriftTargets, t as detectPluginVersionDrift } from "./plugin-version-drift-Clfu4emY.js";
import { r as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dp-kgpcT.js";
import { u as resolveSessionStoreTargets } from "./targets-Cknmo7YZ.js";
import { i as withDoctorSqliteMaintenanceLock, r as isDestructiveDoctorSessionSqliteMode } from "./doctor-sqlite-maintenance-lock-BDyGk9pJ.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import crypto from "node:crypto";
//#region src/commands/doctor-post-upgrade.types.ts
/** Probe codes emitted by post-upgrade validation. */
const POST_UPGRADE_PROBE_CODES = [
"plugin.index_unavailable",
"plugin.entry_unresolved",
"plugin.manifest_drift",
"plugin.version_drift"
];
//#endregion
//#region src/commands/doctor-post-upgrade.ts
/** Post-upgrade validation probes for persisted plugin index and package extension entries. */
function buildReport(findings) {
return {
probesRun: [...POST_UPGRADE_PROBE_CODES],
findings
};
}
function isSourceCheckoutPluginRecord(record) {
if (record.origin === "workspace" || record.origin === "config") return true;
return record.origin === "bundled" && isBundledSourceCheckoutPluginRoot(record.rootDir);
}
function isBundledSourceCheckoutPluginRoot(pluginRootDir) {
let current = path.resolve(pluginRootDir);
while (true) {
const extensionsDir = path.dirname(current);
if (path.basename(extensionsDir) === "extensions") {
const packageRoot = path.dirname(extensionsDir);
return fs.existsSync(path.join(packageRoot, ".git")) && fs.existsSync(path.join(packageRoot, "pnpm-workspace.yaml")) && fs.existsSync(path.join(packageRoot, "src"));
}
const next = path.dirname(current);
if (next === current) return false;
current = next;
}
}
async function readInstalledPackageJson(rootDir, packageJsonRelPath) {
const absPath = path.join(rootDir, packageJsonRelPath);
const raw = await fs$1.readFile(absPath, "utf-8");
const parsed = JSON.parse(raw);
if (!isRecord(parsed)) throw new Error("package.json must contain a JSON object");
return parsed;
}
async function resolvePackageJsonRelPath(record) {
if (record.packageJson) return record.packageJson.path;
try {
await fs$1.access(path.join(record.rootDir, "package.json"));
return "package.json";
} catch {
return;
}
}
async function sha256OfFile(absPath) {
try {
const raw = await fs$1.readFile(absPath);
return crypto.createHash("sha256").update(raw).digest("hex");
} catch {
return null;
}
}
/** Runs post-upgrade plugin probes and returns structured findings for the caller to render. */
async function runPostUpgradeProbes(params) {
const findings = [];
const installs = await readPersistedInstalledPluginIndex(params);
if (!installs) {
findings.push({
level: "error",
code: "plugin.index_unavailable",
message: "Installed plugin index is missing, unreadable, or malformed. Run `openclaw plugins registry --refresh` to rebuild it before post-upgrade validation."
});
return buildReport(findings);
}
const enabledPlugins = installs.plugins.filter((record) => record.enabled);
const installRecords = Object.fromEntries(Object.entries(installs.installRecords).filter(([id]) => enabledPlugins.some((record) => record.pluginId === id || resolveInstalledPluginIndexInstallOwner(record) === id)));
const drift = await resolvePluginVersionDriftTargets(detectPluginVersionDrift({
gatewayVersion: VERSION,
installRecords
}));
for (const entry of drift.drifts) {
const updateCommand = resolvePluginVersionDriftUpdateCommand(entry);
findings.push({
level: "warn",
code: "plugin.version_drift",
plugin: entry.pluginId,
message: `Plugin ${entry.pluginId} is ${entry.installedVersion}, but OpenClaw is ${VERSION}. ${updateCommand ? `Run \`${updateCommand}\`, then restart the Gateway.` : "No confirmed repair target is available; check registry availability and rerun this command."}`
});
}
for (const record of enabledPlugins) {
const pkgRelPath = await resolvePackageJsonRelPath(record);
if (pkgRelPath) {
let pkg;
try {
pkg = await readInstalledPackageJson(record.rootDir, pkgRelPath);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
const message = `[doctor-post-upgrade] could not read package.json for ${record.pluginId} at ${record.rootDir}: ${reason}`;
process.stderr.write(`${formatConsoleDiagnosticLine({
level: "warn",
message
})}\n`);
findings.push({
level: "error",
code: "plugin.entry_unresolved",
message: `Plugin ${record.pluginId}: could not read package.json (${pkgRelPath}): ${reason}. Reinstall the plugin or run \`openclaw plugins registry --refresh\`.`,
plugin: record.pluginId,
entry: pkgRelPath
});
continue;
}
const resolvedEntries = resolvePackageExtensionEntries(pkg);
if (resolvedEntries.status === "invalid") findings.push({
level: "error",
code: "plugin.entry_unresolved",
message: `Plugin ${record.pluginId}: ${resolvedEntries.error}. Reinstall the plugin or run \`openclaw plugins registry --refresh\`.`,
plugin: record.pluginId,
entry: pkgRelPath
});
else if (resolvedEntries.status === "ok") {
const entries = resolvedEntries.entries;
const validation = await validatePackageExtensionEntriesForInstall({
packageDir: record.rootDir,
extensions: [...entries],
manifest: pkg,
allowSourceTypeScriptEntries: isSourceCheckoutPluginRecord(record)
});
if (!validation.ok) {
const offendingEntry = entries.find((entry) => validation.error.includes(entry));
findings.push({
level: "error",
code: "plugin.entry_unresolved",
message: `Plugin ${record.pluginId}: ${validation.error}`,
plugin: record.pluginId,
...offendingEntry ? { entry: offendingEntry } : {}
});
}
}
}
if (record.manifestPath && record.manifestHash) {
const currentHash = await sha256OfFile(record.manifestPath);
if (currentHash && currentHash !== record.manifestHash) findings.push({
level: "warn",
code: "plugin.manifest_drift",
message: `Plugin ${record.pluginId} manifest hash drifted from installs.json snapshot. Run \`openclaw plugins registry --refresh\` to re-sync.`,
plugin: record.pluginId
});
}
}
return buildReport(findings);
}
//#endregion
//#region src/commands/doctor.ts
/** Top-level doctor command wrapper, including post-upgrade probe mode. */
function resolveExplicitSessionSqliteMaintenancePaths(options) {
if (!options.sessionSqliteStore) return [];
const requestedAgentId = normalizeAgentId(options.sessionSqliteAgent ?? "main");
const targets = resolveSessionStoreTargets({ agents: { entries: { [requestedAgentId]: { default: true } } } }, {
store: options.sessionSqliteStore,
...options.sessionSqliteAgent ? { agent: options.sessionSqliteAgent } : {},
...options.sessionSqliteAllAgents ? { allAgents: true } : {}
}, { env: process.env });
const protectedPaths = /* @__PURE__ */ new Set();
for (const target of targets) {
protectedPaths.add(target.storePath);
const sqlitePath = resolveSqliteTargetFromSessionStorePath(target.storePath, { agentId: target.agentId }).path;
if (sqlitePath) for (const databasePath of resolveSqliteDatabaseFilePaths(sqlitePath)) protectedPaths.add(databasePath);
}
return [...protectedPaths];
}
/** Runs doctor or the post-upgrade probe submode using the provided runtime. */
async function doctorCommand(runtime, options) {
const outputRuntime = runtime ?? defaultRuntime;
if (options?.stateSqlite) {
const { runDoctorStateSqliteCompact } = await import("./doctor-state-sqlite-compact-BmQmN-on.js");
const report = await runDoctorStateSqliteCompact();
if (options.json) writeRuntimeJson(outputRuntime, report);
else if (report.skipped) outputRuntime.log(`state-sqlite compact: skipped; database missing at ${report.path}`);
else {
outputRuntime.log(`state-sqlite compact: reclaimed=${report.reclaimedBytes} bytes, db=${report.before.dbSizeBytes}->${report.after.dbSizeBytes} bytes, wal=${report.before.walSizeBytes}->${report.after.walSizeBytes} bytes`);
outputRuntime.log(`- freelist=${report.before.freelistPages}->${report.after.freelistPages} pages, page-size=${report.after.pageSizeBytes} bytes, auto-vacuum=${report.before.autoVacuum}->${report.after.autoVacuum}`);
outputRuntime.log(`- integrity-check=${report.integrityCheck}, path=${report.path}`);
}
exitCliAfterOutput(outputRuntime, 0);
}
if (options?.sessionSqlite) {
const sessionSqliteMode = options.sessionSqlite;
const { runDoctorSessionSqlite, reconcileDoctorSessionSqlitePublication } = await import("./doctor-session-sqlite-DM2cPm22.js");
const sessionSqliteOptions = {
mode: sessionSqliteMode,
...options.sessionSqliteStore ? { store: options.sessionSqliteStore } : {},
...options.sessionSqliteAgent ? { agent: options.sessionSqliteAgent } : {},
...options.sessionSqliteAllAgents ? { allAgents: true } : {}
};
const runSessionSqlite = async () => await runDoctorSessionSqlite(sessionSqliteOptions);
const reconcileHardlink = (filePath) => reconcileDoctorSessionSqlitePublication(sessionSqliteOptions, filePath);
const report = isDestructiveDoctorSessionSqliteMode(sessionSqliteMode) ? await withDoctorSqliteMaintenanceLock({
env: process.env,
operation: `session SQLite ${sessionSqliteMode}`,
...options.sessionSqliteStore ? { protectedPaths: resolveExplicitSessionSqliteMaintenancePaths(options) } : {},
...sessionSqliteMode !== "compact" ? { reconcileHardlink } : {},
run: runSessionSqlite
}) : await runSessionSqlite();
if (sessionSqliteMode === "recover" && options.sessionSqliteGithubIssue === true) await maybeCreateSessionSqliteGithubIssue(outputRuntime, report, options);
if (options.json) writeRuntimeJson(outputRuntime, report);
else {
outputRuntime.log(`session-sqlite ${report.mode}: ${report.totals.targets} target(s), ${report.totals.legacyEntries} legacy entries, ${report.totals.sqliteEntries} sqlite entries, ${report.totals.issues} issue(s)`);
if (report.migrationRun) {
outputRuntime.log(`- migration-run=${report.migrationRun.runId}`);
outputRuntime.log(`- manifest=${report.migrationRun.manifestPath}`);
if (report.migrationRun.failureReportMarkdownPath) outputRuntime.log(`- failure-report=${report.migrationRun.failureReportMarkdownPath}`);
}
if (report.supportIssue) outputRuntime.log(`- support-issue-report=${report.supportIssue.bodyPath ?? "inline"}`);
for (const target of report.targets) {
outputRuntime.log(`- ${target.agentId}: imported=${target.importedEntries}/${target.importedTranscriptEvents} events, validated=${target.validatedEntries}/${target.validatedTranscriptEvents} events, archived-unreferenced-jsonl=${target.archivedUnreferencedJsonlFiles.length}, unreferenced-jsonl=${target.unreferencedJsonlFiles.length}`);
if (target.restore) outputRuntime.log(` restored=${target.restore.restoredFiles.length}, skipped=${target.restore.skippedFiles.length}, conflicts=${target.restore.conflicts.length}, manifests=${target.restore.manifestPaths.length}`);
if (target.compact) outputRuntime.log(` compact reclaimed=${target.compact.reclaimedBytes} bytes, db=${target.compact.dbSizeBeforeBytes}->${target.compact.dbSizeAfterBytes} bytes, wal=${target.compact.walSizeBeforeBytes}->${target.compact.walSizeAfterBytes} bytes`);
if (target.corruptRecovery) outputRuntime.log(` corrupt-db-recovery moved=${target.corruptRecovery.movedFiles.length}, skipped=${target.corruptRecovery.skippedFiles.length}`);
for (const issue of target.issues.slice(0, 10)) outputRuntime.log(` [${issue.code}]${issue.sessionKey ? ` ${issue.sessionKey}:` : ""} ${issue.message}`);
if (target.issues.length > 10) outputRuntime.log(` ...and ${target.issues.length - 10} more issue(s)`);
}
}
exitCliAfterOutput(outputRuntime, report.totals.issues > 0 ? 1 : 0);
}
if (options?.postUpgrade) {
const report = await runPostUpgradeProbes({});
if (options.json) writeRuntimeJson(outputRuntime, report);
else {
for (const f of report.findings) outputRuntime.log(`[${f.level}] ${f.code}: ${f.message}`);
if (report.findings.length === 0) outputRuntime.log("post-upgrade: no findings");
}
const hasError = report.findings.some((f) => f.level === "error");
exitCliAfterOutput(outputRuntime, hasError ? 1 : 0);
}
await (await import("./doctor-health-BKXZX_tC.js")).runDoctorHealthFlow(runtime, options);
}
async function maybeCreateSessionSqliteGithubIssue(runtime, report, options) {
const shouldLog = options.json !== true;
const supportIssue = report.supportIssue;
if (!supportIssue) {
if (shouldLog) runtime.log("session-sqlite recover: no support issue payload was generated");
return;
}
let approved = options.yes === true;
if (!approved && options.nonInteractive !== true && options.json !== true) {
const { promptYesNo } = await import("./prompt-BMrl28g7.js");
approved = await promptYesNo("Create a GitHub issue in openclaw/openclaw with the sanitized recovery report?", false);
}
if (!approved) {
supportIssue.github = { status: "skipped" };
if (shouldLog) runtime.log("session-sqlite recover: GitHub issue creation skipped");
return;
}
const manifestPath = report.migrationRun?.manifestPath;
if (!manifestPath) {
setSessionSqliteGithubIssueFailure(runtime, supportIssue, shouldLog, "GitHub issue creation is unavailable because its private retry receipt could not be prepared.");
return;
}
const { prepareGithubIssue, reconcileGithubIssue, submitGithubIssue } = await import("./github-issue-5P4e-xwX.js");
const { claimSessionSqliteMigrationGithubIssue, clearSessionSqliteMigrationGithubIssueClaim } = await import("./doctor-session-sqlite-failure-CASyFqCM.js");
const prepared = prepareGithubIssue({
body: supportIssue.body,
title: supportIssue.title
});
let claim;
try {
claim = await withSessionSqliteGithubIssueReceipt(manifestPath, (authority) => claimSessionSqliteMigrationGithubIssue(manifestPath, {
marker: prepared.marker,
title: prepared.title
}, authority));
} catch {
claim = void 0;
}
if (!claim) {
setSessionSqliteGithubIssueFailure(runtime, supportIssue, shouldLog, "GitHub issue creation is unavailable because its private retry receipt could not be saved.");
return;
}
supportIssue.title = claim.issue.title;
const claimedIssue = prepareGithubIssue({
body: supportIssue.body,
title: claim.issue.title
});
if (claimedIssue.marker !== claim.issue.marker) {
setSessionSqliteGithubIssueFailure(runtime, supportIssue, shouldLog, "GitHub issue creation is unavailable because its private retry receipt is inconsistent.");
return;
}
if (claim.status === "existing") {
const reconciled = await reconcileGithubIssue(claimedIssue).catch(() => ({ status: "unavailable" }));
if (reconciled.status === "created") {
setSessionSqliteGithubIssueCreated(runtime, supportIssue, shouldLog, reconciled.url);
return;
}
setSessionSqliteGithubIssueFailure(runtime, supportIssue, shouldLog, "A prior GitHub issue handoff may already have created this report; no duplicate was opened.");
return;
}
const created = await submitGithubIssue(claimedIssue).catch(() => ({
reason: "creation-outcome-unknown",
status: "outcome-unknown"
}));
if (created.status === "created") {
setSessionSqliteGithubIssueCreated(runtime, supportIssue, shouldLog, created.url);
return;
}
if (created.status === "outcome-unknown") {
setSessionSqliteGithubIssueFailure(runtime, supportIssue, shouldLog, "GitHub issue creation outcome is unknown; no duplicate was opened.");
return;
}
if (created.status === "fallback-unavailable") {
await withSessionSqliteGithubIssueReceipt(manifestPath, (authority) => clearSessionSqliteMigrationGithubIssueClaim(manifestPath, claimedIssue.marker, authority)).catch(() => false);
setSessionSqliteGithubIssueFailure(runtime, supportIssue, shouldLog, "GitHub issue creation is unavailable, and this report is too large for a safe browser fallback.");
return;
}
const message = created.reason === "cli-unavailable" ? "GitHub CLI is unavailable." : created.reason === "authentication-unavailable" ? "GitHub authentication is unavailable." : "GitHub issue creation is unavailable.";
const { detectBrowserOpenSupport, openUrl } = await import("./browser-open-0wEwDSeV.js");
const browserSupport = await detectBrowserOpenSupport().catch(() => ({ ok: false }));
const opened = browserSupport.ok ? await openUrl(created.url).catch(() => false) : false;
if (!browserSupport.ok) await withSessionSqliteGithubIssueReceipt(manifestPath, (authority) => clearSessionSqliteMigrationGithubIssueClaim(manifestPath, claimedIssue.marker, authority)).catch(() => false);
supportIssue.github = {
message,
status: "failed"
};
if (shouldLog) {
runtime.log(`session-sqlite recover: ${message}`);
runtime.log(opened ? "session-sqlite recover: opened the sanitized fallback in your browser" : "session-sqlite recover: browser handoff unavailable; the sanitized report remains available in the recovery result");
}
}
async function withSessionSqliteGithubIssueReceipt(manifestPath, run) {
return await withDoctorSqliteMaintenanceLock({
env: process.env,
operation: "session SQLite GitHub issue receipt",
protectedPaths: [manifestPath],
run
});
}
function setSessionSqliteGithubIssueCreated(runtime, issue, shouldLog, url) {
issue.github = {
status: "created",
url
};
if (shouldLog) runtime.log(`session-sqlite recover: created GitHub issue ${url}`);
}
function setSessionSqliteGithubIssueFailure(runtime, issue, shouldLog, message) {
issue.github = {
message,
status: "failed"
};
if (shouldLog) runtime.log(`session-sqlite recover: ${message}`);
}
//#endregion
export { doctorCommand };