【#2】 OpenClaw を読み解く — openclaw と打ってから、起動まで
本記事のコード参照は OpenClaw
mainのcee2aca409(version2026.6.10)時点。行番号は更新でズレ得ます。
連載「OpenClaw を読み解く」
openclaw とターミナルに打ち込んでから Gateway が応答するまで、何が起きているのか。今回は src/entry.ts(全 295 行)を起点に、CLI プロセスの起動の一本道を読み解きます。巨大アプリの起動コードは「いかに重い処理を遅延させ、地雷を踏まないか」の工夫が詰まった場所です。
二段構えの入口 — openclaw.mjs から entry.ts へ
package.json の bin はこうです。
"bin": { "openclaw": "openclaw.mjs" }
ルートの openclaw.mjs が薄いラッパーで、実体は dist/.../entry.js(ソースは src/entry.ts)。この2段構えには理由があります。
地雷その一 — 自分が主役かを確かめる main-module ガード
entry.ts:57 の isMainModule ガードは、このファイルの最重要コメントが付いています。
// Guard: only run entry-point logic when this file is the main module.
// The bundler may import entry.js as a shared dependency when dist/index.js
// is the actual entry point; without this guard the top-level code below
// would call runCli a second time, starting a duplicate gateway that fails
// on the lock / port and crashes the process.
if (!isMainModule({ currentFile: fileURLToPath(import.meta.url),
wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS] })) {
// Imported as a dependency — skip all entry-point side effects.
} else {
// ... 起動処理本体 ...
}
dist/index.js(ライブラリ用エントリ)が entry.js を依存として読み込むことがあり、その際にトップレベルの起動処理がもう一度走ると、Gateway が二重起動してロック/ポート衝突でクラッシュします。ENTRY_WRAPPER_PAIRS(entry.ts:28)で openclaw.mjs / openclaw.js というラッパー名を許容しつつ、本当に自分がプロセスの主役のときだけ副作用を実行する、という防御です。
このコメントは AGENTS.md の「lifecycle ordering / ownership boundary な非自明分岐にはインラインコメントを必須とする」という規約の好例でもあります。
地雷その二 — キャッシュのために、自らを起動し直す
main モジュールだと確定すると、まず実行ルートを解決し、必要なら**自分自身を再起動(respawn)**します。
const installRoot = resolveEntryInstallRoot(entryFile);
const waitingForCompileCacheRespawn = respawnWithoutOpenClawCompileCacheIfNeeded({
currentFile: entryFile, installRoot,
});
if (!waitingForCompileCacheRespawn) {
process.title = "openclaw";
ensureOpenClawExecMarkerOnProcess();
installProcessWarningFilter();
normalizeEnv();
enableOpenClawCompileCache({ installRoot });
...
}
Node の V8 コンパイルキャッシュを使うと起動が速くなりますが、状況によっては無効化して再起動したほうが安全です。respawnWithoutOpenClawCompileCacheIfNeeded がその判断を担い、再起動を選んだ場合は親プロセスはここで止まります(waitingForCompileCacheRespawn === true)。
その後の初期化も順序が意味を持ちます。
-
process.title = "openclaw"—psでの見分けやすさ -
ensureOpenClawExecMarkerOnProcess()— 自分が openclaw 実行であるマーカー(子プロセス判定用) -
installProcessWarningFilter()— Node の警告ノイズを抑制 -
normalizeEnv()— 環境変数の正規化
地雷その三 — 引数を整え、形を変えて生まれ直す
process.argv = normalizeWindowsArgv(process.argv);
if (!ensureCliRespawnReady()) {
const parsedContainer = parseCliContainerArgs(process.argv); // --container
const parsed = parseCliProfileArgs(parsedContainer.argv); // --profile / --dev
...
}
ensureCliRespawnReady()(entry.ts:90)は buildCliRespawnPlan() を呼び、必要なら runCliRespawnPlan(plan) で再実行して true を返します。true のときは「親はもう CLI を続行してはいけない」。Node のバージョンやフラグの都合で、適切な実行形態に自分を起動し直す仕組みです。
--container(コンテナターゲット指定)と --profile/--dev(プロファイル切替)は早い段階でパースされ、両者の同時指定はここで弾かれます。
if (containerTargetName && parsed.profile) {
console.error("[openclaw] --container cannot be combined with --profile/--dev");
process.exit(2);
}
プロファイル指定があれば applyCliProfileEnv で環境を切り替え、Commander とアドホックな argv チェックの両方が一貫するよう process.argv を書き戻します。
速さの肝 — 「必要になるまで読まない」fast-path
起動コードを読むと、tryHandle...FastPath という関数が複数あることに気づきます。
if (!tryHandleRootVersionFastPath(process.argv)) {
await runMainOrRootHelp(process.argv);
}
-
tryHandleRootVersionFastPath(entry.ts:22import) —openclaw --versionを、重いモジュールを一切読まずに即答する。 -
tryHandleRootHelpFastPath(entry.ts:137) —openclaw --helpのヘルプ表示。ここが秀逸で、まず 設定に敏感なプラグインがあるかをloadRootHelpRenderOptionsForConfigSensitivePluginsで確認し、なければ事前計算済みのヘルプ文字列(outputPrecomputedRootHelpText)をそのまま出します。プラグインの状態次第でヘルプ内容が変わるので、変わらないと分かるときだけ事前計算をショートカットする、という二段構えです。 -
tryHandlePrecomputedCommandHelpFastPath(entry.ts:208) —browser/secrets/nodesの各サブコマンドの--helpも同様に事前計算。
なぜここまでするのか。--version や --help のためにプラグインローダーや設定読み込みまで走らせるのは無駄で、体感も遅くなるからです。「重い初期化は本当に必要になるまで遅延する」という設計が、起動コードのいたるところに見えます。OPENCLAW_GATEWAY_STARTUP_TRACE を立てると、entry.bootstrap / entry.argv などの計測トレースが出る仕掛けもあります(createGatewayStartupTrace, src/cli/startup-trace.ts)。
起動の全景 — 一本道を俯瞰する
openclaw.mjs
└─ entry.ts (main module 判定)
├─ compile cache respawn 判定 ── 必要なら再起動して親終了
├─ process 初期化 (title / marker / warning filter / env)
├─ Windows argv 正規化
├─ CLI respawn 判定 ── 必要なら再実行して親終了
├─ --container / --profile パース
├─ fast-path: --version / --help は即応答
└─ runMain (Commander 本体) ──→ 各サブコマンドへ
常駐するということ — OS の差異を吸う daemon 層
openclaw gateway で起動した Gateway を常駐サービスにするのが src/daemon/ です。README.md の推奨セットアップ openclaw onboard --install-daemon がここを使います。
ディレクトリを見ると、OS ごとのサービスマネージャを抽象化しているのがわかります。
-
launchd.ts/launchd-plist.ts— macOS(launchd user service) -
systemd.ts/systemd-unit.ts/systemd-linger.ts— Linux(systemd user service) -
schtasks.ts/schtasks-exec.ts— Windows(タスクスケジューラ) -
service.ts/service-env.ts/service-layout.ts— 3 OS 共通のサービス抽象
つまり「OS ごとのサービス登録の差異」を daemon/ が吸収し、上位は openclaw gateway restart/status --deep(AGENTS.md の Platform/Ops 節)という統一インターフェイスで扱える、という設計です。bootstrap/ は対照的に薄く、node-extra-ca-certs(追加 CA 証明書)と node-startup-env(起動時の環境)の調整だけを担います。
読みどころ — 起動コードが教える三つの定石
今回押さえたいのは次の3点です。
- 二重起動を防ぐ main-module ガード — バンドル時の共有依存に強い。
- respawn による自己再起動 — コンパイルキャッシュ/Node 実行形態を最適化するため、エントリは自分を起動し直すことを厭わない。
-
fast-path による遅延初期化 —
--version/--helpは重い経路を踏まずに即応答。「必要になるまで読まない」が徹底されている。
これらはすべて「起動が速く、壊れにくい CLI」を作るための定石であり、巨大アプリほど効いてきます。
次回予告 — 制御プレーンの会話へ
#03 は、起動後に立ち上がる Gateway とその通信規約 Gateway プロトコル(packages/gateway-protocol)を読み解きます。チャネル・ツール・イベントを束ねる「制御プレーン」が、クライアント(CLI / Web UI / モバイルノード)とどう会話するのか。プロトコルのバージョニング規律(AGENTS.md: 「additive first」)にも踏み込みます。
参考: src/entry.ts(特に :28 / :49 / :57 / :90 / :130 / :137 / :208), src/cli/startup-trace.ts, src/daemon/, src/bootstrap/, README.md(daemon セットアップ)
