10のエージェントが同時に同じ型チェックを実行した。解決策はディレクトリだった。

同じチェックアウトに17のコーディングエージェント、同時に走る10のtsc、load average 37、空きRAMは87 MB。90秒の型チェックが7分36かかりました。計測した内容、マシンが計算していなかった理由、そして問題を解決した小さな共有ロックを紹介します。どんなリポジトリにもコピーできます。

9月7日、開発マシンがまともに応答しなくなりました。クラッシュでもフリーズでもありません。ただ、すべてが10倍の時間をかけるようになったのです。コードとは何の関係もないものまで含めて。

分かりやすい容疑者はどれも外れでした。ノートPCは熱くなっていません。スロットリングの記録はなく、バッテリーは30.6 C。CPUを食い尽くす暴走プロセスもありません。何もデプロイされていません。唯一いつもと違ったのは、同じリポジトリで17のエージェントCLIが生きていたことで、これはうちでは普通の1日です。

実際に何が起きていたのか、推測ではなく計測した結果です。

計測

16 GB、8コアのマシン、起動から5時間半、17のエージェントが作業中。

計測した項目
生きているエージェントCLI17
同時に走る tsc --noEmit 、2分のあいだに確認3、その後10
Load average37から41
空きRAM / メモリ圧縮87 MB / 7.2 GB
デスクトップアプリの型チェック1回、飽和したマシンCPU 26 秒に対して実時間7分36
同じ型チェック、静かなマシン33 秒

決定的な行は下から2番目です。CPUの26秒が7分半に引き伸ばされている、つまり使用率10パーセント。型チェックは計算していませんでした。メモリを待っていたのです。

そしてそのうち1プロセスは途中でOSに殺されました。殺された tsc はゼロ以外のコードと空の出力で終了しますが、これは本物の型エラーと見分けがつきません。つまりマシンは遅いだけでなく、誰も信用できない判定を出していました。

誰も間違ったことはしていない

ここは立ち止まる価値のある部分です。この障害が来るのを見えにくくしているのが、まさにこれだからです。

どのエージェントもルールに従っていました。どれもTypeScriptを編集していました。どれも手を戻す前に型を確認するよう指示されていました。どれも tsc --noEmit を実行しました。どれも他のエージェントを見ていませんでした。 「いま高価な処理をしているから待って」とエージェントが書き込む共通の黒板は存在しません。

そして自己増幅します。マシンが飽和しているので型チェックが遅くなる。それを見張っているエージェントは、止まっていると判断する。だから殺して、もう1つ起動する。この反射は単独では正しく、集団では破滅的です。そしてこれは、エージェントが詰まった検索プロセスを残していったときに1か月前に記録したのと同じ障害の系統です。Process Guard は起動してしまったものを見つける安全網で、こちらは起動そのものを止める手当てです。

採らなかった3つの答え

エージェントを減らす。 これは症状を半分にして、バグを残します。負荷のかかったマシンで型チェックが2つ同時に走れば、やはり1つより遅い。そして台数を減らすことは、仕事を速くしているものを差し出して問題を払うことです。

最後に型チェックを1回だけ。 魅力的ですが、性能とは何の関係もない理由で間違っています。10チケット後に見つかった型エラーは孤児です。それを書いたエージェントは閉じられ、コンテキストは失われ、1行を直すために人間が話題全体を開き直すことになります。検証を先送りしたくはありませんでした。

インクリメンタルコンパイル。 試して外しました。 --noEmit モードでは利得が怪しく、同時に走るプロセスは共有の .tsbuildinfo を壊します。問題の半分を、もう半分を悪化させながら解決する方法です。

代わりにやったこと、1回のチェックを共有する

ルールは「確認する回数を減らす」ではありません。プロジェクトごとに、全員のために、一度に1つの型チェックです。ラッパースクリプトがN回の確認を1回に置き換え、3つの場合に答えます。

  1. 前回の実行から何も変わっていないので、その結果を返す。
  2. 実行がすでに進行中なので、それを待って結果を受け取る。
  3. それ以外なら、ロックを取ってマシン上で唯一の tsc になる。

エージェントの視点では何も変わりません。 yarn typecheck と打ち、型エラーを受け取る。以前より長く待つこともありません。自分が待つ実行は、自分より先に始まった実行だからです。マシンは10回分ではなく1回分を払います。

これがすべてです。面白いのは、これに必要な2つの仕組みが、思うよりずっと小さいことです。

ロックはディレクトリ

ファイルでもデータベースでもデーモンでもありません。ディレクトリです。

try {
  mkdirSync(lockDir);       // 成功: ロックは自分のもの
} catch (err) {
  if (err.code === 'EEXIST') { /* 誰かが持っている、待つ */ }
}

mkdir はディレクトリを作るか EEXIST で失敗するかのどちらかで、それをmacOS、Windows、Linuxでアトミックに、依存もネイティブ呼び出しもなく行います。ファイルを書いてから存在を確認するのは2操作になり、2操作こそが2つ目のエージェントが割り込む場所です。

ディレクトリの中には、pid、ホスト名、開始時刻を入れた owner.json を置きます。このファイルは診断と、死んだロックの検出のためのものです。排他をしているのは決してこのファイルではありません。

死んだロックは2つの場合に自動で引き継がれます。所有プロセスが消えている場合、これはホスト名が一致するときだけ確認します。pidはマシンをまたぐと意味がないからです。もう1つは、ロックが15分より古い場合です。

バグ1つ分の代償を払った落とし穴。 mkdirowner.json の書き込みのあいだには、所有者が読めない窓があります。その窓でロックを死んだと宣言することは、いま取ったばかりの相手からそれを奪うことであり、このファイルが防ぐために存在するまさにその競合です。だから所有者が読めないときは、ディレクトリの古さで判断し、欠けているファイルでは判断しません。

指紋は日付と個数

ケース1は、前回の実行から何かが変わったかを知る必要があります。分かりやすい答えはソースファイルをハッシュすることです。私たちはそうしません。

指紋は、tsconfigの include から導いたルート群とtsconfig自身に対する <最新のmtime>:<ファイル数> です。

2300ファイルでは、全バイトを読むほうが、節約できる確認より高くつきます。日付だけでは削除を見逃します。個数だけでは編集を見逃します。合わせれば両方をカバーします。受け入れている偽陰性は、同じミリ秒に入った2つの編集で個数が変わらない場合であり、その最悪でも数秒古いキャッシュ結果にとどまり、静かな型エラーにはなりません。ブロックする検証はビルドのほうに残っているからです。

書かれたルールでは足りず、フックを追加した

指示は初日から AGENTS.md にありました。 tsc を直接実行しない、必ず共有スクリプトを使う。それでは足りませんでした。その理由は正直に言う価値があります。

編集のあとに型を確認するのは、深く染みついた反射です。追い込まれると、エージェントは指示を読み返さずに npx tsc --noEmit と打ちます。そして、ロックが防ぐために存在する群れを再現するには、ルールを外れるエージェントが1ついれば十分です。指示は交渉できます。フックはできません。

そこで、直接の tsc を拒否し、拒否のメッセージで正しいコマンドを名指しする PreToolUse フックを Bash ツールに付けました。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
        ]
      }
    ]
  }
}

フックはツール呼び出しをJSONとしてstdinから読み、拒否するときは理由をstderrに出して2で終了し、通すときは0で終了します。役に立つフックとうるさいフックを分けるのは2つの細部です。

文字列のどこかではなく、コマンドの位置にある tsc に一致させます。 3文字をどこでも探すと grep -rn tsc AGENTS.md まで拒否してしまいます。だからパターンは、行頭または ;&&|||( の直後に tsc があることを求め、その前にパッケージランナーやパスが付いてもよいことにしています。 tsc --version は通します。情報を出すだけのオプションを拒否する理由はありません。

予期していなかった2つ目のものも拒否します。 あるエージェントがロックの後ろで待ち、しばらくして死んでいるに違いないと判断し、自分の詰まりを解くためにロックディレクトリを削除するのを見ました。それは生きているプロセスの隣で2つ目の重いプロセスを始めることであり、ロックが守るすべての完璧な迂回です。だからロックやキャッシュのディレクトリを削除することも拒否し、死んだロックはひとりでに引き継がれると説明します。

この2つ目の拒否は、前もっては絶対に書けなかったものです。詰まったエージェントが実際に何をするかを見たところから来ています。それは、何をしそうか想像するよりも良いガードレールの出どころです。

どこで止まるか

フックはClaude Code固有です。フリートのほかのエージェントCLIは書かれたルールしか見ません。これは分かっている穴で、承知のうえです。すべてのCLIが読むフックの標準を待つあいだ、大半をカバーするガードレールは、ガードレールなしより勝ります。

共有スクリプトそのものはプロバイダー中立です。ただのコマンドだからです。 yarn typecheck を実行できるCLIなら、強制されていてもいなくてもロックの恩恵を受けます。

ここから持ち帰るもの

型チェックはいちばん音の大きい事例であって、特別な事例ではありませんでした。パターンは、高価で、短い時間枠のあいだ冪等で、同じ正当な理由から各エージェントが起動するあらゆるコマンドに当てはまります。依存関係のインストール、テストスイート全体の実行、本番向けビルド、固定ポートでの開発サーバーの起動。

この順番で3つの問い、それで設計は全部です。

  1. 最近の結果を再利用できるか。
  2. すでに進行中の実行に合流できるか。
  3. そうでなければ、自分が単独でそれを始める側か。

複数のエージェントがあなたのマシンを共有しているなら、計測する価値があるのは何台動いているかではありません。そのうち何台が同じ1分に同じコマンドを始めるかです。マシンが本当に感じているのはその数で、それを見るまでは、あなたは熱のせいにし続けます。

同じリポジトリで複数のエージェントを互いに踏ませずに動かす方法の全体像は 3から8のコーディングエージェントを並行して実行しつつ、管理を失わない方法 にあります。そして実際に起動してしまったプロセスの安全網は Process Guard です。共有スクリプトもフックも、どちらもAgentsRoomのリポジトリの中にあります。つまり、あの午後に17のエージェントが作業していた場所です。

AgentsRoomをダウンロード

すべてのAIエージェントを、すべてのプロジェクトで、ひとつのウィンドウから実行。

無料AgentsRoomをダウンロード

コンパニオンアプリ:外出先でもエージェントを確認

Claude、Codex、Antigravity CLI、またはその他の AI プロバイダーを使用します。

拡張機能を入手
Chrome Web Store

バグや要望を公開バックログに直接送信できます。

マルチプロジェクト
マルチプロバイダー
マルチエージェント
ライブステータス
ファイル差分
モバイルアプリ
ライブプレビュー
エージェントチーム
ブラウザテスト
バックログ駆動開発
プロンプトライブラリ
スキルライブラリ
すべての機能を見る

続きを読む