|
|
|
|
@@ -10,12 +10,98 @@ import {
|
|
|
|
|
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',
|
|
|
|
|
@@ -65,7 +151,6 @@ const OPENCLAW_BUNDLED_RUNTIME_DEPENDENCY_NAMES = [
|
|
|
|
|
'@slack/bolt',
|
|
|
|
|
'@slack/web-api',
|
|
|
|
|
'@tencent-connect/qqbot-connector',
|
|
|
|
|
'@tloncorp/tlon-skill',
|
|
|
|
|
'@twurple/api',
|
|
|
|
|
'@twurple/auth',
|
|
|
|
|
'@twurple/chat',
|
|
|
|
|
@@ -79,6 +164,7 @@ const OPENCLAW_BUNDLED_RUNTIME_DEPENDENCY_NAMES = [
|
|
|
|
|
'croner',
|
|
|
|
|
'discord-api-types',
|
|
|
|
|
'dotenv',
|
|
|
|
|
'docx',
|
|
|
|
|
'express',
|
|
|
|
|
'fake-indexeddb',
|
|
|
|
|
'gaxios',
|
|
|
|
|
@@ -171,10 +257,11 @@ async function runRuntimeDepsRepair(
|
|
|
|
|
|
|
|
|
|
const removedStaleRoot = maybeRemoveStaleInstallRoot(launchContext, inspected.report);
|
|
|
|
|
const missingBefore = Array.isArray(inspected.report.missing) ? inspected.report.missing.length : 0;
|
|
|
|
|
const shouldRepair = removedStaleRoot || missingBefore > 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, [
|
|
|
|
|
@@ -186,7 +273,14 @@ async function runRuntimeDepsRepair(
|
|
|
|
|
], timeoutMs)).report;
|
|
|
|
|
|
|
|
|
|
const missingAfterLocal = Array.isArray(finalReport.missing) ? finalReport.missing.length : 0;
|
|
|
|
|
if (missingAfterLocal > 0 || !localMaterialized) {
|
|
|
|
|
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',
|
|
|
|
|
@@ -195,6 +289,7 @@ async function runRuntimeDepsRepair(
|
|
|
|
|
'--package-root',
|
|
|
|
|
launchContext.openclawDir,
|
|
|
|
|
], timeoutMs)).report;
|
|
|
|
|
npmRepaired = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -204,7 +299,7 @@ async function runRuntimeDepsRepair(
|
|
|
|
|
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'})`);
|
|
|
|
|
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(
|
|
|
|
|
@@ -294,8 +389,8 @@ function maybeRemoveStaleInstallRoot(
|
|
|
|
|
const resolvedOpenClawDir = path.resolve(launchContext.openclawDir);
|
|
|
|
|
if (resolvedInstallRoot === resolvedOpenClawDir) return false;
|
|
|
|
|
|
|
|
|
|
const desiredDeps = createDesiredDependencyMap(report, launchContext);
|
|
|
|
|
if (desiredDeps.size === 0) 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;
|
|
|
|
|
@@ -318,13 +413,41 @@ function maybeRemoveStaleInstallRoot(
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dependencyMapContainsAll(currentDeps, desiredDeps)) return false;
|
|
|
|
|
if (dependencyMapMatches(currentDeps, requiredDeps)) return false;
|
|
|
|
|
|
|
|
|
|
logger.warn(`[plugins] Resetting stale OpenClaw runtime dependency root (expected=${desiredDeps.size}, current=${Object.keys(currentDeps).length}): ${installRoot}`);
|
|
|
|
|
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,
|
|
|
|
|
@@ -408,8 +531,9 @@ function collectLocalRuntimePackageDirs(
|
|
|
|
|
const packageDirs = new Map<string, string>();
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
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) => ({ name })),
|
|
|
|
|
...rootNames.map((name) => preferredByName.get(name) ?? { name }),
|
|
|
|
|
...preferredItems.filter((item) => !rootSet.has(item.name)),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
@@ -492,7 +616,8 @@ function resolvePackageDirFromPackage(parentPackageDir: string, packageName: str
|
|
|
|
|
try {
|
|
|
|
|
const req = createRequire(path.join(parentPackageDir, 'package.json'));
|
|
|
|
|
try {
|
|
|
|
|
return path.dirname(req.resolve(`${packageName}/package.json`));
|
|
|
|
|
const manifestPath = req.resolve(`${packageName}/package.json`);
|
|
|
|
|
return findPackageRootForEntry(manifestPath, packageName) ?? path.dirname(manifestPath);
|
|
|
|
|
} catch {
|
|
|
|
|
const entryPath = req.resolve(packageName);
|
|
|
|
|
return findPackageRootForEntry(entryPath, packageName);
|
|
|
|
|
@@ -558,25 +683,46 @@ function createDesiredDependencyMap(
|
|
|
|
|
report: OpenClawRuntimeDepsReport,
|
|
|
|
|
launchContext: GatewayLaunchContext,
|
|
|
|
|
): Map<string, string> {
|
|
|
|
|
const desired = new Map<string, string>();
|
|
|
|
|
const desired = createReportedDependencyMap(report);
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(report.deps)) {
|
|
|
|
|
for (const dep of report.deps) {
|
|
|
|
|
if (!dep || typeof dep.name !== 'string' || typeof dep.version !== 'string') continue;
|
|
|
|
|
desired.set(dep.name, dep.version);
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
if (desired.has(depName)) continue;
|
|
|
|
|
const spec = readLocalDependencySpec(launchContext, depName) ?? readLocalPackageVersionSpec(launchContext, depName);
|
|
|
|
|
if (spec) {
|
|
|
|
|
desired.set(depName, spec);
|
|
|
|
|
}
|
|
|
|
|
addLocalDependencySpec(desired, launchContext, depName);
|
|
|
|
|
}
|
|
|
|
|
return desired;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addLocalDependencySpec(
|
|
|
|
|
desired: Map<string, string>,
|
|
|
|
|
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<string, string> {
|
|
|
|
|
const desired = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
@@ -622,7 +768,8 @@ function isPlainStringRecord(value: unknown): value is Record<string, string> {
|
|
|
|
|
return Object.values(value).every((entry) => typeof entry === 'string');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dependencyMapContainsAll(current: Record<string, string>, desired: Map<string, string>): boolean {
|
|
|
|
|
function dependencyMapMatches(current: Record<string, string>, desired: Map<string, string>): boolean {
|
|
|
|
|
if (Object.keys(current).length !== desired.size) return false;
|
|
|
|
|
for (const [key, version] of desired) {
|
|
|
|
|
if (current[key] !== version) {
|
|
|
|
|
return false;
|
|
|
|
|
@@ -631,6 +778,202 @@ function dependencyMapContainsAll(current: Record<string, string>, desired: Map<
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isDesktopMainSession(sessionKey: string, entry: Record<string, unknown>): 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<string, unknown>).channel
|
|
|
|
|
: undefined;
|
|
|
|
|
const originProvider = origin && typeof origin === 'object' && !Array.isArray(origin)
|
|
|
|
|
? (origin as Record<string, unknown>).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<string, Record<string, unknown>>;
|
|
|
|
|
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<void> {
|
|
|
|
|
@@ -639,6 +982,12 @@ export async function ensureOpenClawRuntimeDepsReady(
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
patchOpenClawRuntimeDepsLoadRoot(launchContext);
|
|
|
|
|
patchOpenClawAgentsSkillsDiscovery(launchContext);
|
|
|
|
|
patchOpenClawMainSessionRestartRecovery(launchContext);
|
|
|
|
|
patchOpenClawStuckSessionRecovery(launchContext);
|
|
|
|
|
markOrphanedDesktopMainSessionsFailed();
|
|
|
|
|
|
|
|
|
|
const key = `${launchContext.entryScript}::${launchContext.openclawDir}`;
|
|
|
|
|
const existing = preflightPromises.get(key);
|
|
|
|
|
if (existing) {
|
|
|
|
|
|