UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

367 lines (366 loc) 18.9 kB
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 };