Einstieg und Start
Verantwortung
Nachdem man den openclaw-Befehl abgeschickt hat, gehört alles bis zum tatsächlichen Beginn des ersten Geschäftskommandos zum „Einstieg (entry)": Node-Versionsprüfung, Compile-Cache-Pfadauswahl, Help/Version-Schnellpfade, Signalweiterleitung und schließlich die Übergabe an dist/entry.js. Nachdem entry.js übernimmt, erfolgt eine erneute Environment-Normalisierung, Auflösung von Profile/Container-Optionen, Respawn-Entscheidung und dann der Aufruf von runMainOrRootHelp, der argv an die CLI-Verteilung übergibt.
Dass der Einstieg in zwei Schichten geteilt ist, hat einen Grund: openclaw.mjs ist reines Node-ESM, ohne TypeScript-Abhängigkeiten, eigenständig ausführbar; es löst die Dinge, „die entschieden werden müssen, bevor das TS-Kompilat geladen ist". src/entry.ts ist der Einstieg des TS-Kompilats und geht davon aus, dass dist/ bereits existiert; hier sind aufwendigere dynamische Imports und Traces möglich.
Designmotivation
Warum den Einstieg in zwei Schichten aufteilen und so viele Fast-Paths in die erste Schicht legen? Die Wurzel ist die Kaltstartlatenz (cold-start latency). OpenClaw ist ein CLI; die Latenztoleranz für openclaw --version liegt im Bereich hunderter Millisekunden, das vollständige Laden von Commander + Plugins + Konfiguration dauert aber mehrere hundert Millisekunden bis über eine Sekunde. Würde jeder --version-Aufruf die volle Startkette durchlaufen, wäre die Erfahrung miserabel.
Die Strategie der Einstiegsschicht: Was nicht geladen werden muss, wird nicht geladen. Die Versionsnummer wird direkt aus package.json gelesen, der Help-Text aus einem vorkompilierten cli-startup-metadata.json, erst wenn wirklich ein Kommando ausgeführt werden soll, erfolgt ein dynamischer Import von dist/entry.js. So kann openclaw --version in wenigen Dutzend Millisekunden antworten.
Die zweite Schicht entry.ts hat ebenfalls eigene Fast-Paths: Sie versucht zunächst tryHandleRootHelpFastPath und tryHandlePrecomputedCommandHelpFastPath; erst wenn beide nicht zuschlagen, wird ./cli/run-main.js dynamisch importiert. Jeder Schritt dieser Importkette ist awaited, sodass bei Fast-Path-Treffer der folgende Code gar nicht geladen wird.
Ein weiteres Motiv ist die Isolierung des Compile-Caches (compile cache isolation). module.enableCompileCache() in Node 22+ beschleunigt den ESM-Start erheblich, aber Source-Checkout und frühe Node-24-Versionen auf Windows haben einen Deadlock-Bug. Der Einstieg enthält deshalb eine Respawn-Logik: Zuerst wird geprüft, ob der aktuelle Prozess ein Source-Checkout (isSourceCheckoutLauncher) oder eine betroffene Node-24-Version ist; wenn ja, wird ein Kindprozess ohne Compile-Cache gestartet und NODE_DISABLE_COMPILE_CACHE=1 in die env gesetzt. Diese Entscheidung muss vor dem Laden des TS-Einstiegs getroffen werden, sonst ist der Cache bereits kontaminiert.
Schlüsseldateien
openclaw.mjs Versionswächter:L11-L53—MIN_NODE_MAJOR=22/MIN_NODE_MINOR=19; bei Unterschreitungprocess.exit(1).openclaw.mjs Compile-Cache-Respawn:L207-L264—respawnWithoutCompileCacheIfNeeded+respawnWithPackagedCompileCacheIfNeededentscheiden über einen Kindprozess-Neustart.openclaw.mjs Help-Fast-Path:L717-L763—tryOutputBareRootHelpundtryOutputPrecomputedCommandHelpgeben vorkompilierten Help-Text direkt aus.openclaw.mjs Fallback zu entry.js:L765-L780— Erst wenn alle Fast-Paths verfebht, dynamischer Import von./dist/entry.js.entry.ts isMainModule-Wächter:L56-L79— Verhindert doppelten Gateway-Start, wenn der Bundlerentry.jsals shared Dependency erneut importiert.entry.ts Respawn-Plan:L90-L103—buildCliRespawnPlan+runCliRespawnPlanfür Runtime-Wechsel-Szenarien.entry.ts Versions-Fast-Path und runMainOrRootHelp-Aufruf:L130-L131—tryHandleRootVersionFastPathverfehlt, dann Hauptverteilung.entry.ts runMainOrRootHelp:L271-L295— Help-Fast-Path + dynamischer Import von run-main.entry.ts vorkompilierter Befehlshelp:L208-L269—tryHandlePrecomputedCommandHelpFastPathbehandeltbrowser/secrets/nodes+ Subkommando-Help.
Datenfluss
Der Versionswächter ganz oben in 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, MIN_NODE_MINOR=19 sind harte Schwellen; bei Unterschreitung sofort Exit. Nach dem Versionswächter folgt die Compile-Cache-Respawn-Entscheidung (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,
);
};Beachten Sie die Selbstschutz-Variable COMPILE_CACHE_DISABLED_RESPAWNED_ENV: Der respawnete Kindprozess durchläuft diese Logik erneut, aber die env-Variable ist bereits "1", also erfolgt maximal ein Respawn — keine endlose Rekursion.
Erste Schicht fällt auf dist/entry.js zurück (openclaw.mjs:L765): Erst wenn alle Fast-Paths verfehlt haben, erfolgt tryImport("./dist/entry.js"); bei Misserfolg wird .mjs versucht, und erst wenn beides scheitert, liefert buildMissingEntryErrorMessage den Hinweis „Dies ist ein ungebauter Source-Baum, führe pnpm install && pnpm build aus". tryImport schluckt nur „direkte module-not-found"-Fehler, andere Fehler werden weitergeworfen — so wird ein Fehler innerhalb von dist/entry.js nicht als „missing dist" verschleiert.
Kern der Einstiegsverteilung der zweiten Schicht entry.ts (entry.ts:L130):
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
}runMainOrRootHelp selbst probiert zuerst einen Help-Fast-Path (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);
}
}Beachten Sie gatewayEntryStartupTrace.measure("run-main-import", ...) — dieser eine dynamische Import von run-main.js wird explizit gemessen, da er der schwerste Schritt im Kaltstart ist (mehrere hundert Millisekunden). Wenn runCli wirft, wird nicht einfach console.error(error.stack) verwendet, sondern einheitlich über formatCliFailureLines ein benutzerfreundlicher Titel + Trace ausgegeben, dann process.exit(1).
Oben in entry.ts gibt es zudem einen zentralen isMainModule-Wächter (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 {
// ... vollständige Startlogik
}Der Kommentar nennt den Grund: Ein Bundler könnte entry.js als shared Dependency von dist/index.js importieren; ohne diesen Wächter würde das Gateway zweimal starten, der zweite Instanz würde den Port streitig machen und abstürzen.
Grenzen und Fehler
- Respawn-Signalweiterleitung:
runRespawnedChild(openclaw.mjs:L102) registriert am KindprozessSIGTERM/SIGINT/SIGHUP/SIGQUIT-Listener, leitet empfangene Signale an den Kindprozess weiter und hat nach 1 Sekunde einen Fallbackprocess.exit. So wird verhindert, dass der Launcher hängen bleibt, wenn der Kindprozess Signale ignoriert. Windows hat eine andere Signalmenge (nurSIGTERM/SIGINT/SIGBREAK). - Compile-Cache-Pfadkontamination: Wenn die Umgebungsvariable
NODE_COMPILE_CACHEauf ein nicht beschreibbares Verzeichnis zeigt, schlägtmodule.enableCompileCachestillschweigend fehl (openclaw.mjs:L267);try/catchschluckt den Fehler — bewusst, ein Cache-Fehler darf den Start nicht blockieren. - Source-Checkout-Erkennung:
isSourceCheckoutLauncher()prüft das Vorhandensein von.gitodersrc/entry.ts; ist es ein Source-Baum, wird der Compile-Cache zwangsweise deaktiviert (Source kann sich ändern, gecachte Kompilate wären veraltet). - Missing-dist-Fehler:
buildMissingEntryErrorMessage(openclaw.mjs:L354) prüft, obsrc/entry.tsexistiert; wenn ja, folgt der Hinweis „Dies ist ein ungebauter Source-Baum, führepnpm install && pnpm buildaus" statt eines schlichten „file not found" — diese Unterscheidung spart dem Nutzer Minuten der Fehlersuche. - isMainModule-Doppelstart: Der Kommentar des Wächters oben in
entry.ts(entry.ts:L56) erklärt, dass ein Bundler ohne ihnentry.jsals shared Dependency nutzen würde, sodassrunClizweimal aufgerufen würde und die zweite Instanz an der Gateway-Sperre/dem Port scheitert. - Respawn-Schleifenschutz: Die beiden Umgebungsvariablen
COMPILE_CACHE_DISABLED_RESPAWNED_ENVundOPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNEDbewachen jeweils zwei Respawn-Arten und stellen sicher, dass maximal einmal respawned wird.
Zusammenfassung
Die Einstiegsschicht erledigt, „was vor dem Laden von TS gelöst werden kann": Versionswächter, Compile-Cache-Entscheidung, Help/Version-Fast-Path, Signalweiterleitung. openclaw.mjs ist ein reines Node-Skript ohne Abhängigkeit von Kompilaten; entry.ts ist der Einstieg des TS-Kompilats und ergänzt Respawn-Plan, Profile-Auflösung, Help-Fast-Path-Fallback und reicht argv schließlich an runCli für die eigentliche CLI-Befehlsverteilung. Wie die Verteilungskette argv an SubCLIs weiterreicht, den Gateway-Hot-Path nutzt und Befehle lazy registriert, siehe nächste Seite; die Startsequenz des Gateways selbst siehe Gateway-Kern.
Vergleich mit offiziellen Ressourcen: OpenClaw-Doku · README.