Entrada y arranque
Responsabilidad
Todo lo que ocurre tras pulsar Enter en el comando openclaw, hasta que el primer comando de negocio realmente empieza a ejecutarse, lo gestiona la «entrada (entry)»: comprobación de la versión de Node, selección de la ruta de la compile cache, fast path de help/version, reenvío de señales y, finalmente, ceder el control a dist/entry.js. Cuando entry.js toma el relevo, vuelve a normalizar el entorno, parsea las opciones de profile/container, decide si necesita respawn y luego llama a runMainOrRootHelp para pasar argv al despacho CLI.
La entrada se divide en dos capas por una razón: openclaw.mjs es ESM de Node puro, sin ninguna dependencia de TypeScript, ejecutable de forma autónoma; resuelve «lo que hay que decidir antes de que el producto compilado de TS se cargue». src/entry.ts es la entrada del producto compilado de TS: asume que dist/ ya existe y puede hacer dynamic imports más complejos y traza.
Motivación de diseño
¿Por qué dividir la entrada en dos capas y además meter tantos fast path en la primera? La razón de fondo es la latencia de arranque en frío (cold startup). OpenClaw es un CLI; la tolerancia del usuario a la latencia de comandos como openclaw --version es del orden de centenas de milisegundos, pero cargar Commander + plugins + configuración al completo cuesta cientos de milisegundos o incluso un segundo. Si cada --version recorriera la cadena completa de arranque, la experiencia sería pésima.
La estrategia de la capa de entrada es: si no hace falta cargar, no se carga. La versión se lee directamente de package.json; el texto de help se lee de un cli-startup-metadata.json precompilado; solo cuando de verdad hay que ejecutar un comando se hace dynamic import de dist/entry.js. Esto permite que openclaw --version produzca salida en unas decenas de milisegundos.
La segunda capa, entry.ts, también tiene sus propios fast path: primero prueba tryHandleRootHelpFastPath y tryHandlePrecomputedCommandHelpFastPath; solo si ninguno coincide hace dynamic import de ./cli/run-main.js. Esta cadena de imports hace await en cada paso, de modo que cuando un fast path anterior acierta, el código posterior ni se carga.
Otra motivación es el aislamiento de la compile cache (compile cache). module.enableCompileCache() de Node 22+ acelera significativamente el arranque ESM, pero hay un bug de deadlock en checkout de código fuente y en versiones tempranas de Node 24 sobre Windows. La capa de entrada tiene un bloque de lógica de respawn (respawn): primero determina si el proceso actual es un launcher de checkout de código fuente (isSourceCheckoutLauncher) o una versión afectada de Node 24; si es así, respawnea un subproceso con la compile cache deshabilitada y mete NODE_DISABLE_COMPILE_CACHE=1 en env. Esta decisión debe completarse antes de que la entrada de TS se cargue, o la cache ya estará contaminada.
Archivos clave
openclaw.mjs guardián de versión:L11-L53—MIN_NODE_MAJOR=22/MIN_NODE_MINOR=19; por debajo,process.exit(1).openclaw.mjs compile cache respawn:L207-L264—respawnWithoutCompileCacheIfNeeded+respawnWithPackagedCompileCacheIfNeededdeciden si hace falta reiniciar el subproceso.openclaw.mjs help fast path:L717-L763—tryOutputBareRootHelpytryOutputPrecomputedCommandHelpdevuelven directamente el texto precompilado.openclaw.mjs fallback a entry.js:L765-L780— solo si ningún fast path acierta se hace dynamic import de./dist/entry.js.entry.ts guardián isMainModule:L56-L79— evita que el bundler, al tratarentry.jscomo dependencia compartida, arranque el gateway dos veces por un segundo import.entry.ts plan de respawn:L90-L103—buildCliRespawnPlan+runCliRespawnPlantratan los escenarios que necesitan cambiar de runtime.entry.ts fast path de versión y llamada a runMainOrRootHelp:L130-L131— solo sitryHandleRootVersionFastPathno acierta se va al despacho principal.entry.ts runMainOrRootHelp:L271-L295— fast path de help + dynamic import de run-main.entry.ts help de comando precompilado:L208-L269—tryHandlePrecomputedCommandHelpFastPathtratabrowser/secrets/nodes+ help de subcomando.
Flujo de datos
El guardián de versión en la parte superior de la primera capa openclaw.mjs (openclaw.mjs:L38):
const ensureSupportedNodeVersion = () => {
if (isSupportedNodeVersion(parseNodeVersion(process.versions.node))) {
return;
}
process.stderr.write(
`openclaw: Node.js v${MIN_NODE_VERSION}+ is required (current: v${process.versions.node}).\n` +
"If you use nvm, run:\n" +
` nvm install ${MIN_NODE_MAJOR}\n` +
` nvm use ${MIN_NODE_MAJOR}\n` +
` nvm alias default ${MIN_NODE_MAJOR}\n`,
);
process.exit(1);
};MIN_NODE_MAJOR=22 y MIN_NODE_MINOR=19 son un umbral estricto: por debajo, se sale directamente. Tras el guardián de versión viene la decisión de respawn de la compile cache (openclaw.mjs:L207):
const respawnWithoutCompileCacheIfNeeded = () => {
const needsDisabledCompileCacheRespawn =
isSourceCheckoutLauncher() || shouldSkipCompileCacheForWindowsNode24();
if (!needsDisabledCompileCacheRespawn) {
return false;
}
if (process.env[COMPILE_CACHE_DISABLED_RESPAWNED_ENV] === "1") {
return false;
}
// ...
return runRespawnedChild(
process.execPath,
[...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
env,
);
};Nótese el guardián de auto-recurrencia COMPILE_CACHE_DISABLED_RESPAWNED_ENV: el subproceso respawneado vuelve a ejecutar este mismo bloque, pero la variable de entorno ya vale "1", así que como máximo se respawnea una vez: no hay recursión infinita.
El fallback de la primera capa a dist/entry.js (openclaw.mjs:L765): solo si ningún fast path acierta se llama tryImport("./dist/entry.js"); si falla, se prueba con .mjs; solo si ambos fallan se construye el mensaje de buildMissingEntryErrorMessage que sugiere «esto es un árbol de código fuente sin construir, ejecuta pnpm install && pnpm build». tryImport solo traga el error directo de «module not found»; el resto de errores se propaga tal cual. Así, si dist/entry.js existe pero un import interno suyo falla, el error no se camufla como «missing dist».
El núcleo del despacho de la segunda capa entry.ts (entry.ts:L130):
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
}runMainOrRootHelp también prueba primero el fast path de help (entry.ts:L271):
async function runMainOrRootHelp(argv: string[]): Promise<void> {
if (await tryHandleRootHelpFastPath(argv)) {
return;
}
if (await tryHandlePrecomputedCommandHelpFastPath(argv)) {
return;
}
try {
const { runCli } = await gatewayEntryStartupTrace.measure(
"run-main-import",
() => import("./cli/run-main.js"),
);
await runCli(argv);
} catch (error) {
const { formatCliFailureLines } = await import("./cli/failure-output.js");
for (const line of formatCliFailureLines({
title: "Could not start the CLI.",
error,
argv,
})) {
console.error(line);
}
process.exit(1);
}
}Nótese gatewayEntryStartupTrace.measure("run-main-import", ...) — este dynamic import de run-main.js se cronometra explícitamente porque es el paso más pesado del arranque en frío (cientos de milisegundos). Cuando runCli lanza un error, no se hace directamente console.error(error.stack), sino que se enruta por formatCliFailureLines para producir un título amigable + traza, y entonces process.exit(1).
En la parte superior de entry.ts hay además un guardián clave isMainModule (entry.ts:L56):
if (
!isMainModule({
currentFile: fileURLToPath(import.meta.url),
wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
})
) {
// Imported as a dependency — skip all entry-point side effects.
} else {
// ... lógica completa de arranque
}El comentario explica la razón: el bundler puede importar entry.js como dependencia compartida desde dist/index.js; sin este guardián, se arrancarían dos gateways, y el segundo fallaría al tomar el puerto y colisionar.
Límites y fallos
- Reenvío de señales en respawn:
runRespawnedChild(openclaw.mjs:L102) instala listeners deSIGTERM/SIGINT/SIGHUP/SIGQUITen el subproceso; al recibir una señal, la reenvía al subproceso y, tras 1 segundo, haceprocess.exitcomo red de seguridad. Así evita que un subproceso que ignore señales deje al launcher colgado para siempre. En Windows el conjunto de señales es distinto (soloSIGTERM/SIGINT/SIGBREAK). - Contaminación de la ruta de compile cache: si la variable de entorno
NODE_COMPILE_CACHEapunta a un directorio no escribible,module.enableCompileCachefalla silenciosamente (openclaw.mjs:L267); eltry/catchtraga el error — es intencional: un fallo de cache no debe bloquear el arranque. - Detección de checkout de código fuente:
isSourceCheckoutLauncher()comprueba si existen.gitosrc/entry.ts; en un árbol de código fuente se fuerza la deshabilitación de la compile cache (porque el código fuente puede cambiar en cualquier momento y el producto compilado en cache quedaría caduco). - Mensaje de error por missing dist:
buildMissingEntryErrorMessage(openclaw.mjs:L354) detecta si existesrc/entry.ts; si existe, sugiere «esto es un árbol de código fuente sin construir, ejecutapnpm install && pnpm build» en vez de limitarse a decir «file not found» — esta distinción ahorra al usuario minutos de investigación. - Doble arranque por isMainModule: el guardián superior de
entry.ts(entry.ts:L56) señalado en el comentario: sin él, el bundler trataríaentry.jscomo dependencia compartida, llamaríarunClidos veces y la segunda instancia fallaría al tomar el lock/puerto del gateway y colisionar. - Protección de bucle de respawn: dos variables de entorno,
COMPILE_CACHE_DISABLED_RESPAWNED_ENVyOPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED, guardián de los dos tipos de respawn; garantizan que como máximo se respawnea una vez.
Resumen
La capa de entrada se ocupa de «lo que se puede resolver antes de cargar TS»: guardián de versión, decisión sobre compile cache, fast path de help/version, reenvío de señales. openclaw.mjs es un script de Node puro, sin dependencias de ningún producto compilado; entry.ts es la entrada del producto compilado de TS y añade el plan de respawn, el parseo de profile y un fast path de help adicional como red de seguridad, hasta que finalmente pasa argv a runCli y entra de verdad en el despacho de comandos CLI. Cómo la cadena de despacho enruta argv a un subcli concreto, cómo recorre el hot path del gateway y cómo registra comandos de forma perezosa se explica en la próxima página; la secuencia de arranque del gateway en sí se ve en Núcleo del gateway.
Referencias oficiales: Documentación de OpenClaw · README.