import { spawn } from 'node:child_process'; import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs'; import { createRequire } from 'node:module'; import { homedir } from 'node:os'; import path from 'node:path'; import type { GatewayLaunchContext } from './config-sync'; import { logger } from '../utils/logger'; const DEFAULT_RUNTIME_DEPS_PREFLIGHT_TIMEOUT_MS = 10 * 60_000; const MAX_CAPTURED_OUTPUT_LENGTH = 16_000; const ALLOW_NPM_RUNTIME_DEPS_REPAIR_ENV = 'YINIAN_ALLOW_OPENCLAW_RUNTIME_DEPS_NPM_REPAIR'; const EXPAND_RUNTIME_DEPS_FALLBACK_ENV = 'YINIAN_EXPAND_OPENCLAW_RUNTIME_DEPS'; const OPENCLAW_RUNTIME_DEPS_LOADROOT_PATCH_SEARCH = `const packagePlan = collectBundledPluginRuntimeDeps({ \t\t\textensionsDir, \t\t\t...params.config ? { config: params.config } : {}, \t\t\tmanifestCache, \t\t\t...normalizePluginId ? { normalizePluginId } : {} \t\t});`; const OPENCLAW_RUNTIME_DEPS_LOADROOT_PATCH_REPLACE = `const packagePlan = collectBundledPluginRuntimeDeps({ \t\t\textensionsDir, \t\t\t...params.config ? { config: params.config } : { selectedPluginIds: new Set([params.pluginId]) }, \t\t\tmanifestCache, \t\t\t...normalizePluginId ? { normalizePluginId } : {} \t\t});`; const OPENCLAW_AGENTS_SKILLS_PATCH_SEARCH = `\tconst osHomeDir = resolveUserHomeDir(); \tconst personalAgentsSkills = loadSkills({ \t\tdir: osHomeDir ? path.resolve(osHomeDir, ".agents", "skills") : path.resolve(".agents", "skills"), \t\tsource: "agents-skills-personal" \t}); \tconst projectAgentsSkills = loadSkills({ \t\tdir: path.resolve(workspaceDir, ".agents", "skills"), \t\tsource: "agents-skills-project" \t});`; const OPENCLAW_AGENTS_SKILLS_PATCH_REPLACE = `\tconst openClawAgentsSkillsDisabled = process.env.OPENCLAW_DISABLE_AGENTS_SKILLS === "1"; \tconst osHomeDir = openClawAgentsSkillsDisabled ? void 0 : resolveUserHomeDir(); \tconst personalAgentsSkills = openClawAgentsSkillsDisabled ? [] : loadSkills({ \t\tdir: osHomeDir ? path.resolve(osHomeDir, ".agents", "skills") : path.resolve(".agents", "skills"), \t\tsource: "agents-skills-personal" \t}); \tconst projectAgentsSkills = openClawAgentsSkillsDisabled ? [] : loadSkills({ \t\tdir: path.resolve(workspaceDir, ".agents", "skills"), \t\tsource: "agents-skills-project" \t});`; const OPENCLAW_MAIN_SESSION_RECOVERY_MARK_SEARCH = `\t\t\tif (!interruptedSessionIds.has(entry.sessionId)) continue; \t\t\tentry.abortedLastRun = true; \t\t\tstore[sessionKey] = entry; \t\t\tresult.marked++;`; const OPENCLAW_MAIN_SESSION_RECOVERY_MARK_REPLACE = `\t\t\tif (!interruptedSessionIds.has(entry.sessionId)) continue; \t\t\tentry.abortedLastRun = true; \t\t\tif (process.env.OPENCLAW_DISABLE_MAIN_SESSION_RESTART_RECOVERY === "1") { \t\t\t\tentry.status = "failed"; \t\t\t\tentry.endedAt = Date.now(); \t\t\t\tentry.updatedAt = entry.endedAt; \t\t\t} else { \t\t\t\tentry.updatedAt = Date.now(); \t\t\t} \t\t\tstore[sessionKey] = entry; \t\t\tresult.marked++;`; const OPENCLAW_MAIN_SESSION_RECOVERY_SCHEDULE_SEARCH = `function scheduleRestartAbortedMainSessionRecovery(params = {}) { \tconst initialDelay = params.delayMs ?? DEFAULT_RECOVERY_DELAY_MS;`; const OPENCLAW_MAIN_SESSION_RECOVERY_SCHEDULE_REPLACE = `function scheduleRestartAbortedMainSessionRecovery(params = {}) { \tif (process.env.OPENCLAW_DISABLE_MAIN_SESSION_RESTART_RECOVERY === "1") { \t\tlog.info("main-session restart recovery disabled by OPENCLAW_DISABLE_MAIN_SESSION_RESTART_RECOVERY"); \t\treturn; \t} \tconst initialDelay = params.delayMs ?? DEFAULT_RECOVERY_DELAY_MS;`; const OPENCLAW_STUCK_ACTIVE_ABORT_SEARCH = `\t\t\t\t(opts?.recoverStuckSession ?? recoverStuckSession)({ \t\t\t\t\tsessionId: state.sessionId, \t\t\t\t\tsessionKey: state.sessionKey, \t\t\t\t\tageMs, \t\t\t\t\tqueueDepth: state.queueDepth \t\t\t\t});`; const OPENCLAW_STUCK_ACTIVE_ABORT_REPLACE = `\t\t\t\t(opts?.recoverStuckSession ?? recoverStuckSession)({ \t\t\t\t\tsessionId: state.sessionId, \t\t\t\t\tsessionKey: state.sessionKey, \t\t\t\t\tageMs, \t\t\t\t\tqueueDepth: state.queueDepth, \t\t\t\t\tallowActiveAbort: (() => { \t\t\t\t\t\tconst thresholdMs = Number(process.env.YINIAN_OPENCLAW_STUCK_ACTIVE_ABORT_MS || "900000"); \t\t\t\t\t\treturn Number.isFinite(thresholdMs) && thresholdMs > 0 && ageMs >= thresholdMs; \t\t\t\t\t})() \t\t\t\t});`; const OPENCLAW_DESKTOP_RUNTIME_DEPENDENCY_NAMES = [ '@anthropic-ai/sdk', '@anthropic-ai/vertex-sdk', '@aws-sdk/client-bedrock', '@aws-sdk/client-bedrock-runtime', '@aws-sdk/credential-provider-node', '@aws/bedrock-token-generator', '@google/genai', '@openai/codex', 'express', 'openai', 'playwright-core', ] as const; const OPENCLAW_BUNDLED_RUNTIME_DEPENDENCY_NAMES = [ '@agentclientprotocol/claude-agent-acp', '@agentclientprotocol/sdk', '@anthropic-ai/sdk', '@anthropic-ai/vertex-sdk', '@aws-sdk/client-bedrock', '@aws-sdk/client-bedrock-runtime', '@aws-sdk/client-s3', '@aws-sdk/credential-provider-node', '@aws-sdk/s3-request-presigner', '@aws/bedrock-token-generator', '@azure/identity', '@clack/prompts', '@clawdbot/lobster', '@discordjs/voice', '@google/genai', '@grammyjs/runner', '@grammyjs/transformer-throttler', '@homebridge/ciao', '@lancedb/lancedb', '@larksuiteoapi/node-sdk', '@line/bot-sdk', '@lydell/node-pty', '@mariozechner/pi-agent-core', '@mariozechner/pi-ai', '@mariozechner/pi-coding-agent', '@matrix-org/matrix-sdk-crypto-nodejs', '@matrix-org/matrix-sdk-crypto-wasm', '@microsoft/teams.api', '@microsoft/teams.apps', '@modelcontextprotocol/sdk', '@mozilla/readability', '@openai/codex', '@opentelemetry/api', '@opentelemetry/api-logs', '@opentelemetry/exporter-logs-otlp-proto', '@opentelemetry/exporter-metrics-otlp-proto', '@opentelemetry/exporter-trace-otlp-proto', '@opentelemetry/resources', '@opentelemetry/sdk-logs', '@opentelemetry/sdk-metrics', '@opentelemetry/sdk-node', '@opentelemetry/sdk-trace-base', '@opentelemetry/semantic-conventions', '@pierre/diffs', '@pierre/theme', '@slack/bolt', '@slack/web-api', '@tencent-connect/qqbot-connector', '@twurple/api', '@twurple/auth', '@twurple/chat', '@urbit/aura', '@whiskeysockets/baileys', '@zed-industries/codex-acp', 'acpx', 'ajv', 'chokidar', 'commander', 'croner', 'discord-api-types', 'dotenv', 'docx', 'express', 'fake-indexeddb', 'gaxios', 'global-agent', 'google-auth-library', 'grammy', 'https-proxy-agent', 'jimp', 'jiti', 'json5', 'jsonwebtoken', 'jszip', 'jwks-rsa', 'linkedom', 'markdown-it', 'matrix-js-sdk', 'minimatch', 'mpg123-decoder', 'music-metadata', 'node-edge-tts', 'nostr-tools', 'openai', 'openshell', 'opusscript', 'pdfjs-dist', 'playwright-core', 'semver', 'sharp', 'silk-wasm', 'sqlite-vec', 'tar', 'tokenjuice', 'tslog', 'typebox', 'undici', 'web-push', 'ws', 'yaml', 'zca-js', 'zod', ] as const; const preflightPromises = new Map>(); interface OpenClawRuntimeDepsReport { installRoot?: string; deps?: Array<{ name?: unknown; version?: unknown }>; missing?: unknown[]; repairedSpecs?: unknown[]; } function appendOutput(current: string, chunk: Buffer): string { const next = current + chunk.toString('utf8'); if (next.length <= MAX_CAPTURED_OUTPUT_LENGTH) return next; return next.slice(next.length - MAX_CAPTURED_OUTPUT_LENGTH); } function resolveNodeRunner(launchContext: GatewayLaunchContext): { command: string; env: Record; } { const bundledNodePath = launchContext.forkEnv.YINIAN_NODE_EXEC_PATH; if (bundledNodePath && existsSync(bundledNodePath)) { return { command: bundledNodePath, env: { ...launchContext.forkEnv }, }; } return { command: process.execPath, env: { ...launchContext.forkEnv, ELECTRON_RUN_AS_NODE: '1', }, }; } async function runRuntimeDepsRepair( launchContext: GatewayLaunchContext, timeoutMs = DEFAULT_RUNTIME_DEPS_PREFLIGHT_TIMEOUT_MS, ): Promise { const inspected = await runPluginsDepsCommand(launchContext, [ 'plugins', 'deps', '--json', '--package-root', launchContext.openclawDir, ], timeoutMs); const removedStaleRoot = maybeRemoveStaleInstallRoot(launchContext, inspected.report); const missingBefore = Array.isArray(inspected.report.missing) ? inspected.report.missing.length : 0; const shouldRepair = removedStaleRoot || missingBefore > 0 || shouldMaterializeExternalInstallRoot(launchContext, inspected.report); let finalReport = inspected.report; let localMaterialized = false; let npmRepaired = false; if (shouldRepair) { localMaterialized = tryMaterializeRuntimeDepsFromLocalPackages(launchContext, inspected.report); finalReport = (await runPluginsDepsCommand(launchContext, [ 'plugins', 'deps', '--json', '--package-root', launchContext.openclawDir, ], timeoutMs)).report; const missingAfterLocal = Array.isArray(finalReport.missing) ? finalReport.missing.length : 0; if (missingAfterLocal > 0) { if (process.env[ALLOW_NPM_RUNTIME_DEPS_REPAIR_ENV] !== '1') { throw new Error([ `OpenClaw runtime dependency preflight still has ${missingAfterLocal} missing dependency/dependencies after local materialization.`, `NPM/PNPM repair is disabled during normal desktop startup to avoid blocking conversations.`, `Run the administrator repair flow or set ${ALLOW_NPM_RUNTIME_DEPS_REPAIR_ENV}=1 for a one-time repair.`, ].join(' ')); } finalReport = (await runPluginsDepsCommand(launchContext, [ 'plugins', 'deps', '--repair', '--json', '--package-root', launchContext.openclawDir, ], timeoutMs)).report; npmRepaired = true; } } const missingAfter = Array.isArray(finalReport.missing) ? finalReport.missing.length : 0; const repairedCount = Array.isArray(finalReport.repairedSpecs) ? finalReport.repairedSpecs.length : 0; if (missingAfter > 0) { throw new Error(`OpenClaw runtime dependency preflight still has ${missingAfter} missing dependency/dependencies after repair`); } logger.info(`[plugins] OpenClaw runtime dependency preflight complete (missing=${missingAfter}, repaired=${repairedCount}, reset=${removedStaleRoot ? 'yes' : 'no'}, local=${localMaterialized ? 'yes' : 'no'}, npm=${npmRepaired ? 'yes' : 'no'})`); } function runPluginsDepsCommand( launchContext: GatewayLaunchContext, args: string[], timeoutMs: number, ): Promise<{ report: OpenClawRuntimeDepsReport; stdout: string; stderr: string }> { const runner = resolveNodeRunner(launchContext); const commandArgs = [launchContext.entryScript, ...args]; logger.info(`[plugins] Checking OpenClaw runtime dependencies before Gateway start (packageRoot=${launchContext.openclawDir})`); return new Promise((resolve, reject) => { const child = spawn(runner.command, commandArgs, { cwd: launchContext.openclawDir, env: { ...runner.env, OPENCLAW_NO_RESPAWN: '1', }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }); let stdout = ''; let stderr = ''; let settled = false; const timeout = setTimeout(() => { if (settled) return; settled = true; child.kill('SIGTERM'); reject(new Error(`OpenClaw runtime dependency preflight timed out after ${timeoutMs}ms`)); }, timeoutMs); timeout.unref?.(); child.stdout?.on('data', (chunk: Buffer) => { stdout = appendOutput(stdout, chunk); }); child.stderr?.on('data', (chunk: Buffer) => { stderr = appendOutput(stderr, chunk); }); child.on('error', (error) => { if (settled) return; settled = true; clearTimeout(timeout); reject(error); }); child.on('close', (code, signal) => { if (settled) return; settled = true; clearTimeout(timeout); if (code === 0 && !signal) { try { const parsed = JSON.parse(stdout) as OpenClawRuntimeDepsReport; resolve({ report: parsed, stdout, stderr }); } catch (error) { reject(new Error([ 'OpenClaw runtime dependency preflight returned invalid JSON', error instanceof Error ? error.message : String(error), stderr.trim(), stdout.trim(), ].filter(Boolean).join('\n'))); } return; } reject(new Error([ `OpenClaw runtime dependency preflight failed (code=${code ?? 'null'}, signal=${signal ?? 'none'})`, stderr.trim(), stdout.trim(), ].filter(Boolean).join('\n'))); }); }); } function maybeRemoveStaleInstallRoot( launchContext: GatewayLaunchContext, report: OpenClawRuntimeDepsReport, ): boolean { const installRoot = typeof report.installRoot === 'string' ? report.installRoot : ''; if (!installRoot) return false; const resolvedInstallRoot = path.resolve(installRoot); const resolvedOpenClawDir = path.resolve(launchContext.openclawDir); if (resolvedInstallRoot === resolvedOpenClawDir) return false; const requiredDeps = createDesiredDependencyMap(report, launchContext); if (requiredDeps.size === 0) return false; const manifestPath = path.join(installRoot, 'package.json'); if (!existsSync(manifestPath)) return false; let currentDeps: Record; try { const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: unknown; dependencies?: unknown; }; if (parsed.name !== 'openclaw-runtime-deps-install') { logger.warn(`[plugins] Removing stale OpenClaw runtime dependency root with unexpected manifest: ${installRoot}`); rmSync(installRoot, { recursive: true, force: true }); return true; } currentDeps = isPlainStringRecord(parsed.dependencies) ? parsed.dependencies : {}; } catch (error) { logger.warn(`[plugins] Removing unreadable OpenClaw runtime dependency root: ${installRoot}`, error); rmSync(installRoot, { recursive: true, force: true }); return true; } if (dependencyMapMatches(currentDeps, requiredDeps)) return false; logger.warn(`[plugins] Resetting stale OpenClaw runtime dependency root (expected=${requiredDeps.size}, current=${Object.keys(currentDeps).length}): ${installRoot}`); rmSync(installRoot, { recursive: true, force: true }); return true; } function shouldMaterializeExternalInstallRoot( launchContext: GatewayLaunchContext, report: OpenClawRuntimeDepsReport, ): boolean { const installRoot = typeof report.installRoot === 'string' ? report.installRoot : ''; if (!installRoot) return false; const resolvedInstallRoot = path.resolve(installRoot); const resolvedOpenClawDir = path.resolve(launchContext.openclawDir); if (resolvedInstallRoot === resolvedOpenClawDir) return false; const requiredDeps = createDesiredDependencyMap(report, launchContext); if (requiredDeps.size === 0) return false; const manifestPath = path.join(installRoot, 'package.json'); if (!existsSync(manifestPath)) return true; try { const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as { dependencies?: unknown; }; const currentDeps = isPlainStringRecord(parsed.dependencies) ? parsed.dependencies : {}; return !dependencyMapMatches(currentDeps, requiredDeps); } catch { return true; } } function tryMaterializeRuntimeDepsFromLocalPackages( launchContext: GatewayLaunchContext, report: OpenClawRuntimeDepsReport, ): boolean { const installRoot = typeof report.installRoot === 'string' ? report.installRoot : ''; if (!installRoot) return false; const resolvedInstallRoot = path.resolve(installRoot); const resolvedOpenClawDir = path.resolve(launchContext.openclawDir); if (resolvedInstallRoot === resolvedOpenClawDir) return false; const desiredDeps = createDesiredDependencyMap(report, launchContext); if (desiredDeps.size === 0) return false; const tempRoot = `${installRoot}.tmp-${process.pid}-${Date.now()}`; try { rmSync(tempRoot, { recursive: true, force: true }); mkdirSync(path.join(tempRoot, 'node_modules'), { recursive: true }); const localPackages = collectLocalRuntimePackageDirs( launchContext, Array.from(desiredDeps.keys()), [ ...readLocalPackageDependencyNames(launchContext.openclawDir).map((name) => ({ name, parentDir: launchContext.openclawDir, })), ...readLocalPackageDependencyNames(process.cwd()).map((name) => ({ name, parentDir: process.cwd(), })), ...listNodeModulesPackageNames(path.join(launchContext.openclawDir, 'node_modules')).map((name) => ({ name, })), ], ); if (localPackages.missingRootName) { logger.warn(`[plugins] Local OpenClaw runtime dependency not found, falling back to npm repair: ${localPackages.missingRootName}`); rmSync(tempRoot, { recursive: true, force: true }); return false; } for (const [depName, packageDir] of localPackages.packageDirs) { const destination = path.join(tempRoot, 'node_modules', ...depName.split('/')); mkdirSync(path.dirname(destination), { recursive: true }); symlinkSync(packageDir, destination, process.platform === 'win32' ? 'junction' : 'dir'); } writeFileSync( path.join(tempRoot, 'package.json'), `${JSON.stringify({ name: 'openclaw-runtime-deps-install', private: true, dependencies: Object.fromEntries( Array.from(desiredDeps.entries()).sort(([left], [right]) => left.localeCompare(right)), ), }, null, 2)}\n`, 'utf8', ); rmSync(installRoot, { recursive: true, force: true }); mkdirSync(path.dirname(installRoot), { recursive: true }); renameSync(tempRoot, installRoot); logger.info(`[plugins] Materialized OpenClaw runtime dependencies from bundled/local packages (${desiredDeps.size} direct deps, ${localPackages.packageDirs.size} packages): ${installRoot}`); return true; } catch (error) { rmSync(tempRoot, { recursive: true, force: true }); logger.warn('[plugins] Failed to materialize OpenClaw runtime dependencies from local packages; falling back to npm repair:', error); return false; } } function collectLocalRuntimePackageDirs( launchContext: GatewayLaunchContext, rootNames: string[], preferredItems: Array<{ name: string; parentDir?: string }> = [], ): { packageDirs: Map; missingRootName: string | null; } { const packageDirs = new Map(); const seen = new Set(); const rootSet = new Set(rootNames); const preferredByName = new Map(preferredItems.map((item) => [item.name, item])); const queue: Array<{ name: string; parentDir?: string }> = [ ...rootNames.map((name) => preferredByName.get(name) ?? { name }), ...preferredItems.filter((item) => !rootSet.has(item.name)), ]; for (let index = 0; index < queue.length; index += 1) { const item = queue[index]; if (seen.has(item.name) || isOptionalNativeClipboardPackage(item.name)) continue; const packageDir = item.parentDir ? resolvePackageDirFromPackage(item.parentDir, item.name) ?? resolveLocalPackageDir(launchContext, item.name) : resolveLocalPackageDir(launchContext, item.name); if (!packageDir) { if (rootSet.has(item.name)) return { packageDirs, missingRootName: item.name }; continue; } seen.add(item.name); packageDirs.set(item.name, packageDir); for (const depName of readLocalPackageDependencyNames(packageDir)) { if (!seen.has(depName) && !isOptionalNativeClipboardPackage(depName)) { queue.push({ name: depName, parentDir: packageDir }); } } } return { packageDirs, missingRootName: null }; } function listNodeModulesPackageNames(nodeModulesDir: string): string[] { if (!existsSync(nodeModulesDir)) return []; const names: string[] = []; try { for (const entry of readdirSafe(nodeModulesDir)) { if (entry === '.bin') continue; const entryPath = path.join(nodeModulesDir, entry); if (entry.startsWith('@')) { for (const scopedEntry of readdirSafe(entryPath)) { names.push(`${entry}/${scopedEntry}`); } } else { names.push(entry); } } } catch { return []; } return Array.from(new Set(names)).sort((left, right) => left.localeCompare(right)); } function readdirSafe(dir: string): string[] { try { return existsSync(dir) ? readdirSync(dir) : []; } catch { return []; } } function isOptionalNativeClipboardPackage(name: string): boolean { return name === '@mariozechner/clipboard' || name.startsWith('@mariozechner/clipboard-'); } function readLocalPackageDependencyNames(packageDir: string): string[] { try { const parsed = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as { dependencies?: unknown; optionalDependencies?: unknown; }; return Array.from(new Set([ ...Object.keys(isPlainStringRecord(parsed.dependencies) ? parsed.dependencies : {}), ...Object.keys(isPlainStringRecord(parsed.optionalDependencies) ? parsed.optionalDependencies : {}), ])).sort((left, right) => left.localeCompare(right)); } catch { return []; } } function resolvePackageDirFromPackage(parentPackageDir: string, packageName: string): string | null { try { const req = createRequire(path.join(parentPackageDir, 'package.json')); try { const manifestPath = req.resolve(`${packageName}/package.json`); return findPackageRootForEntry(manifestPath, packageName) ?? path.dirname(manifestPath); } catch { const entryPath = req.resolve(packageName); return findPackageRootForEntry(entryPath, packageName); } } catch { return null; } } function resolveLocalPackageDir( launchContext: GatewayLaunchContext, packageName: string, ): string | null { const directCandidates = [ path.join(launchContext.openclawDir, 'node_modules', ...packageName.split('/')), path.join(process.cwd(), 'node_modules', ...packageName.split('/')), ]; for (const candidate of directCandidates) { if (existsSync(path.join(candidate, 'package.json'))) return candidate; } const requireRoots = [ path.join(launchContext.openclawDir, 'package.json'), path.join(process.cwd(), 'package.json'), ]; for (const root of requireRoots) { try { const req = createRequire(root); try { return path.dirname(req.resolve(`${packageName}/package.json`)); } catch { const entryPath = req.resolve(packageName); const packageDir = findPackageRootForEntry(entryPath, packageName); if (packageDir) return packageDir; } } catch { // Try the next require root. } } return null; } function findPackageRootForEntry(entryPath: string, packageName: string): string | null { let current = path.dirname(entryPath); while (current && current !== path.dirname(current)) { const manifestPath = path.join(current, 'package.json'); if (existsSync(manifestPath)) { try { const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: unknown }; if (parsed.name === packageName) return current; } catch { // Keep walking. } } current = path.dirname(current); } return null; } function createDesiredDependencyMap( report: OpenClawRuntimeDepsReport, launchContext: GatewayLaunchContext, ): Map { const desired = createReportedDependencyMap(report); for (const depName of OPENCLAW_DESKTOP_RUNTIME_DEPENDENCY_NAMES) { addLocalDependencySpec(desired, launchContext, depName); } if (process.env[EXPAND_RUNTIME_DEPS_FALLBACK_ENV] !== '1') { return desired; } for (const depName of OPENCLAW_BUNDLED_RUNTIME_DEPENDENCY_NAMES) { addLocalDependencySpec(desired, launchContext, depName); } return desired; } function addLocalDependencySpec( desired: Map, launchContext: GatewayLaunchContext, depName: string, ): void { if (desired.has(depName)) return; const spec = readLocalDependencySpec(launchContext, depName) ?? readLocalPackageVersionSpec(launchContext, depName); if (spec) { desired.set(depName, spec); } } function createReportedDependencyMap(report: OpenClawRuntimeDepsReport): Map { const desired = new Map(); if (!Array.isArray(report.deps)) return desired; for (const dep of report.deps) { if (!dep || typeof dep.name !== 'string' || typeof dep.version !== 'string') continue; desired.set(dep.name, dep.version); } return desired; } function readLocalDependencySpec( launchContext: GatewayLaunchContext, packageName: string, ): string | null { const roots = [process.cwd(), launchContext.openclawDir]; for (const root of roots) { try { const parsed = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) as { dependencies?: unknown; devDependencies?: unknown; optionalDependencies?: unknown; }; for (const entries of [parsed.dependencies, parsed.devDependencies, parsed.optionalDependencies]) { if (isPlainStringRecord(entries) && entries[packageName]) { return entries[packageName]; } } } catch { // Try the next manifest. } } return null; } function readLocalPackageVersionSpec( launchContext: GatewayLaunchContext, packageName: string, ): string | null { const packageDir = resolveLocalPackageDir(launchContext, packageName); if (!packageDir) return null; try { const parsed = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as { version?: unknown; }; return typeof parsed.version === 'string' && parsed.version.trim() ? parsed.version.trim() : null; } catch { return null; } } function isPlainStringRecord(value: unknown): value is Record { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; return Object.values(value).every((entry) => typeof entry === 'string'); } function dependencyMapMatches(current: Record, desired: Map): boolean { if (Object.keys(current).length !== desired.size) return false; for (const [key, version] of desired) { if (current[key] !== version) { return false; } } return true; } function isDesktopMainSession(sessionKey: string, entry: Record): boolean { if (!sessionKey.startsWith('agent:main:')) return false; if (sessionKey.includes(':subagent:') || sessionKey.includes(':cron:') || sessionKey.includes(':acp:')) return false; const deliveryContext = entry.deliveryContext; const origin = entry.origin; const deliveryChannel = deliveryContext && typeof deliveryContext === 'object' && !Array.isArray(deliveryContext) ? (deliveryContext as Record).channel : undefined; const originProvider = origin && typeof origin === 'object' && !Array.isArray(origin) ? (origin as Record).provider : undefined; return entry.lastChannel === 'webchat' || deliveryChannel === 'webchat' || originProvider === 'webchat'; } function markOrphanedDesktopMainSessionsFailed(): void { const agentsRoot = path.join(homedir(), '.openclaw', 'agents'); if (!existsSync(agentsRoot)) return; let agentDirs; try { agentDirs = readdirSync(agentsRoot, { withFileTypes: true }); } catch { return; } for (const agentDir of agentDirs) { if (!agentDir.isDirectory()) continue; const sessionsDir = path.join(agentsRoot, agentDir.name, 'sessions'); const storePath = path.join(sessionsDir, 'sessions.json'); if (!existsSync(storePath)) continue; try { const raw = readFileSync(storePath, 'utf8'); const store = JSON.parse(raw) as Record>; let changed = 0; const now = Date.now(); for (const [sessionKey, entry] of Object.entries(store)) { if (!entry || entry.status !== 'running' || !isDesktopMainSession(sessionKey, entry)) continue; const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId.trim() : ''; if (!sessionId) continue; if (existsSync(path.join(sessionsDir, `${sessionId}.jsonl.lock`))) continue; entry.status = 'failed'; entry.abortedLastRun = true; entry.endedAt = now; entry.updatedAt = now; store[sessionKey] = entry; changed += 1; } if (changed > 0) { writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`, 'utf8'); logger.warn(`[sessions] Marked ${changed} orphaned desktop main session(s) failed before Gateway start`); } } catch (error) { logger.warn(`[sessions] Failed to clean orphaned desktop main sessions: ${storePath}`, error); } } } function patchOpenClawRuntimeDepsLoadRoot(launchContext: GatewayLaunchContext): void { const distDir = path.join(launchContext.openclawDir, 'dist'); if (!existsSync(distDir)) return; let entries; try { entries = readdirSync(distDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (!entry.isFile() || !/^bundled-runtime-deps-.*\.js$/.test(entry.name)) continue; const filePath = path.join(distDir, entry.name); try { const current = readFileSync(filePath, 'utf8'); if (current.includes(OPENCLAW_RUNTIME_DEPS_LOADROOT_PATCH_REPLACE)) return; if (!current.includes(OPENCLAW_RUNTIME_DEPS_LOADROOT_PATCH_SEARCH)) continue; writeFileSync(filePath, current.replace( OPENCLAW_RUNTIME_DEPS_LOADROOT_PATCH_SEARCH, OPENCLAW_RUNTIME_DEPS_LOADROOT_PATCH_REPLACE, ), 'utf8'); logger.info(`[plugins] Patched OpenClaw bundled runtime dependency loader: ${filePath}`); return; } catch (error) { logger.warn('[plugins] Failed to patch OpenClaw bundled runtime dependency loader', error); return; } } } function patchOpenClawAgentsSkillsDiscovery(launchContext: GatewayLaunchContext): void { const distDir = path.join(launchContext.openclawDir, 'dist'); if (!existsSync(distDir)) return; let entries; try { entries = readdirSync(distDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (!entry.isFile() || !/^workspace-.*\.js$/.test(entry.name)) continue; const filePath = path.join(distDir, entry.name); try { const current = readFileSync(filePath, 'utf8'); if (current.includes(OPENCLAW_AGENTS_SKILLS_PATCH_REPLACE)) return; if (!current.includes(OPENCLAW_AGENTS_SKILLS_PATCH_SEARCH)) continue; writeFileSync(filePath, current.replace( OPENCLAW_AGENTS_SKILLS_PATCH_SEARCH, OPENCLAW_AGENTS_SKILLS_PATCH_REPLACE, ), 'utf8'); logger.info(`[skills] Patched OpenClaw .agents skill discovery boundary: ${filePath}`); return; } catch (error) { logger.warn('[skills] Failed to patch OpenClaw .agents skill discovery boundary', error); return; } } } function patchOpenClawMainSessionRestartRecovery(launchContext: GatewayLaunchContext): void { const distDir = path.join(launchContext.openclawDir, 'dist'); if (!existsSync(distDir)) return; let entries; try { entries = readdirSync(distDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (!entry.isFile() || !/^main-session-restart-recovery-.*\.js$/.test(entry.name)) continue; const filePath = path.join(distDir, entry.name); try { let current = readFileSync(filePath, 'utf8'); let next = current; if (!next.includes(OPENCLAW_MAIN_SESSION_RECOVERY_MARK_REPLACE)) { if (!next.includes(OPENCLAW_MAIN_SESSION_RECOVERY_MARK_SEARCH)) continue; next = next.replace( OPENCLAW_MAIN_SESSION_RECOVERY_MARK_SEARCH, OPENCLAW_MAIN_SESSION_RECOVERY_MARK_REPLACE, ); } if (!next.includes(OPENCLAW_MAIN_SESSION_RECOVERY_SCHEDULE_REPLACE)) { if (!next.includes(OPENCLAW_MAIN_SESSION_RECOVERY_SCHEDULE_SEARCH)) continue; next = next.replace( OPENCLAW_MAIN_SESSION_RECOVERY_SCHEDULE_SEARCH, OPENCLAW_MAIN_SESSION_RECOVERY_SCHEDULE_REPLACE, ); } if (next !== current) { writeFileSync(filePath, next, 'utf8'); logger.info(`[sessions] Patched OpenClaw main session restart recovery boundary: ${filePath}`); } return; } catch (error) { logger.warn('[sessions] Failed to patch OpenClaw main session restart recovery boundary', error); return; } } } function patchOpenClawStuckSessionRecovery(launchContext: GatewayLaunchContext): void { const distDir = path.join(launchContext.openclawDir, 'dist'); if (!existsSync(distDir)) return; let entries; try { entries = readdirSync(distDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (!entry.isFile() || !/^diagnostic-.*\.js$/.test(entry.name)) continue; const filePath = path.join(distDir, entry.name); try { const current = readFileSync(filePath, 'utf8'); if (current.includes(OPENCLAW_STUCK_ACTIVE_ABORT_REPLACE)) return; if (!current.includes(OPENCLAW_STUCK_ACTIVE_ABORT_SEARCH)) continue; writeFileSync(filePath, current.replace( OPENCLAW_STUCK_ACTIVE_ABORT_SEARCH, OPENCLAW_STUCK_ACTIVE_ABORT_REPLACE, ), 'utf8'); logger.info(`[sessions] Patched OpenClaw stuck session active-run recovery: ${filePath}`); return; } catch (error) { logger.warn('[sessions] Failed to patch OpenClaw stuck session active-run recovery', error); return; } } } export async function ensureOpenClawRuntimeDepsReady( launchContext: GatewayLaunchContext, ): Promise { if (process.env.YINIAN_SKIP_OPENCLAW_RUNTIME_DEPS_PREFLIGHT === '1') { logger.warn('[plugins] Skipping OpenClaw runtime dependency preflight via YINIAN_SKIP_OPENCLAW_RUNTIME_DEPS_PREFLIGHT=1'); return; } patchOpenClawRuntimeDepsLoadRoot(launchContext); patchOpenClawAgentsSkillsDiscovery(launchContext); patchOpenClawMainSessionRestartRecovery(launchContext); patchOpenClawStuckSessionRecovery(launchContext); markOrphanedDesktopMainSessionsFailed(); const key = `${launchContext.entryScript}::${launchContext.openclawDir}`; const existing = preflightPromises.get(key); if (existing) { await existing; return; } const promise = runRuntimeDepsRepair(launchContext); preflightPromises.set(key, promise); try { await promise; } catch (error) { preflightPromises.delete(key); throw error; } finally { preflightPromises.delete(key); } }