Skip to content

エントリと起動

源码版本v2026.6.11

責務

openclaw コマンドを入力して Enter を押してから,最初のビジネスコマンドが本当に実行を開始するまでの間のすべてを「エントリ (entry)」が管轄します:Node バージョンチェック、compile cache パス選択、help/version 高速パス、シグナル転送、最後に制御を dist/entry.js に渡します。entry.js は引き継いだ後にもう一度環境の正規化、profile/container オプション解析、respawn 判定を行い,runMainOrRootHelp で argv を CLI 振り分け に渡します。

エントリが 2 層に分かれているのには理由があります:openclaw.mjs は純 Node ESM で,TypeScript 依存がなく,単独実行可能です。これが解決するのは「TS コンパイル産物がロードされる前に決めるべきこと」です。src/entry.ts は TS コンパイル産物のエントリで,dist/ が既に存在することを前提に,より複雑な dynamic import と trace ができます。

設計動機

なぜエントリを 2 層に分け,さらに第一層にこんなに多くの fast path を詰め込むのか?根本原因はコールドスタート遅延です。OpenClaw は CLI で,openclaw --version のようなコマンドに対するユーザの許容遅延は数百ミリ秒級ですが,Commander + プラグイン + 設定を完全ロードするには数百ミリ秒から秒単位でかかります。毎回 --version で完全起動リンクを走らせると体験が悪くなります。

エントリ層の戦略は:読み込まなくていいなら読み込まない。バージョン番号は package.json から直接読み,help テキストは事前コンパイル済みの cli-startup-metadata.json から読み,本当にコマンドを実行するときだけ dist/entry.js を dynamic import します。これで openclaw --version は数十ミリ秒で出力できます。

第二層 entry.ts にも独自の fast path があります:まず tryHandleRootHelpFastPathtryHandlePrecomputedCommandHelpFastPath を試し,両方ともヒットしなければ ./cli/run-main.js を dynamic import します。この import 連鎖は各ステップで await するので,前の fast path がヒットしたとき背後のコードは読み込まれません。

もう一つの動機はcompile cache 隔離です。Node 22+ の module.enableCompileCache() は ESM 起動を著しく加速しますが,ソース checkout と Windows の Node 24 早期バージョンにはデッドロックバグがあります。エントリ層には専用の respawn ロジックがあります:現在のプロセスがソース checkout(isSourceCheckoutLauncher)か,影響を受ける Node 24 バージョンかを判定し,該当すれば compile cache を無効化した子プロセスを respawn し,env に NODE_DISABLE_COMPILE_CACHE=1 を設定します。この判定は TS エントリ読み込み前に完了する必要があります,さもなくばキャッシュが既に汚染されています。

主要ファイル

データフロー

第一層 openclaw.mjs 先頭のバージョン守衛(openclaw.mjs:L38):

typescript
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=22MIN_NODE_MINOR=19 は硬いしきい値で,下回れば即退出。バージョン守衛の後は compile cache の respawn 決策(openclaw.mjs:L207):

typescript
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,
  );
};

COMPILE_CACHE_DISABLED_RESPAWNED_ENV という自己循環守衛に注意:respawn された子プロセスはもう一度このロジックを走りますが,環境変数が既に "1" になっているので,最大 1 回しか respawn せず,無限再帰しません。

第一層が dist/entry.js にフォールバック(openclaw.mjs:L765):fast path が全ヒットしないときだけ tryImport("./dist/entry.js") し,失敗したら .mjs を試し,両方失敗してようやく buildMissingEntryErrorMessage で「これは未ビルドのソースツリー,pnpm install && pnpm build を実行」というヒントを出します。tryImport は「直接的な module not found」だけを飲み込み,他のエラーはそのままスローします——これで dist/entry.js が存在しても内部のどこかの import が失敗したとき,エラーが飲み込まれて「missing dist」に偽装されません。

第二層 entry.ts のエントリ振り分け核心(entry.ts:L130):

typescript
if (!tryHandleRootVersionFastPath(process.argv)) {
  await runMainOrRootHelp(process.argv);
}

runMainOrRootHelp 自身もまず help fast path を試します(entry.ts:L271):

typescript
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);
  }
}

gatewayEntryStartupTrace.measure("run-main-import", ...) に注意——run-main.js のこの一度の dynamic import は明示的に計測されます,コールドスタートで最も重いステップ(数百ミリ秒)だからです。runCli がエラーをスローしたとき直接 console.error(error.stack) するのではなく,formatCliFailureLines でユーザフレンドリーなタイトル + trace を出してから process.exit(1) します。

entry.ts 先頭にはもう一つ重要な isMainModule 守衛があります(entry.ts:L56):

typescript
if (
  !isMainModule({
    currentFile: fileURLToPath(import.meta.url),
    wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
  })
) {
  // Imported as a dependency — skip all entry-point side effects.
} else {
  // ... 完全起動ロジック
}

コメントが理由を言っています:bundler が entry.js を共有依存として dist/index.js から import する可能性があり,この守衛がないと2 つの gateway が起動し,2 番目がポートを奪い合って失敗しクラッシュします。

境界と失敗

  • respawn シグナル転送:runRespawnedChild(openclaw.mjs:L102)は子プロセスに SIGTERM/SIGINT/SIGHUP/SIGQUIT リスナを取り付け,シグナル受信時に子プロセスへ転送し,1 秒後に兜底で process.exit します。子プロセスがシグナルを無視して launcher が永遠に hang するのを防ぎます。Windows プラットフォームのシグナルセットは異なります(只有 SIGTERM/SIGINT/SIGBREAK)。
  • compile cache パス汚染:NODE_COMPILE_CACHE 環境変数が書き込み不可ディレクトリを指すと,module.enableCompileCache は静かに失敗し(openclaw.mjs:L267) try/catch がエラーを飲み込みます——これは意図的で,キャッシュ失敗が起動を阻止すべきではありません。
  • ソース checkout 検出:isSourceCheckoutLauncher().git または src/entry.ts の存在で判定し,ソースツリーなら compile cache を強制無効化します(ソースはいつでも変更される可能性があり,キャッシュされたコンパイル産物が期限切れになるため)。
  • missing dist エラー:buildMissingEntryErrorMessage(openclaw.mjs:L354)は src/entry.ts の存在を検出し,存在すれば「これは未ビルドのソースツリー,pnpm install && pnpm build を実行」というヒントを出し,単に「file not found」と言いません——この区別でユーザの数分のトラブルシュート時間を節約できます。
  • isMainModule 二重起動:entry.ts 先頭の守衛(entry.ts:L56)のコメントが指摘するように,守衛がないと bundler が entry.js を共有依存として扱い,runCli が 2 回呼ばれて 2 番目のインスタンスが gateway ロック/ポートを奪い合ってクラッシュします。
  • respawn ループ防護:COMPILE_CACHE_DISABLED_RESPAWNED_ENVOPENCLAW_PACKAGED_COMPILE_CACHE_RESPAWNED の 2 つの環境変数がそれぞれ 2 種の respawn を守り,最大 1 回しか respawn しないことを保証します。

まとめ

エントリ層が行うのは「TS 読み込み前に解決できること」:バージョン守衛、compile cache 決策、help/version fast path、シグナル転送。openclaw.mjs は純 Node スクリプトで,コンパイル産物に依存しません。entry.ts は TS コンパイル産物のエントリで,respawn 計画、profile 解析、help fast path 兜底を補い,最後に argv を runCli に渡して本当の CLI コマンド振り分け に入ります。振り分けリンクがどう argv を具体的な subcli にルーティングするか,どう gateway hot path を走らせるか,どうコマンドを lazy-register するかは次のページ,gateway 自身の起動シーケンスは ゲートウェイコア を参照。

公式資料:OpenClaw ドキュメント · README