『稼働コスト表示』が実は59%過少だったバグ──累積カウンタの復元をポストモーテムで読む
マルチエージェントharness「Munder Difflin」は、自社のエージェント稼働コスト表示が実は59%過少だったと、開発者本人が技術ポストモーテムで公開した。原因はアプリ再起動のたびにゼロへ戻る累積カウンタ。append-onlyログの単調性だけを頼りに過去の実額を復元した手順を、一次ソースの本文からたどる。

目次
複数のAIコーディングエージェントを自宅PCで束ねて動かすharness「Munder Difflin」(既報)が、自社ブログで珍しい種類のバグ報告を公開した。エージェントごとの稼働コストを表示する数字が、実際には59%過小に表示され続けていたという内容だ。金額そのものの大きさより、「なぜ誰も気づけなかったか」と「壊れたカウンタからどうやって正しい過去の数字を復元したか」の技術的な筋道が読みどころだった。
3行まとめ
- Munder Difflinのエージェントごとの支出額
usdは、実際には「アプリのプロセス開始時点からの累積」だった。ラベルは「生涯合計(lifetime)」を意味していたが、実装は「直近の再起動からの合計」を計測していた- 自社のフロアで実測したところ、表示コストは実際の59%しかなかった。原因はアプリ再起動のたびにメモリ上のアキュムレータがリセットされる一方、エージェント自身は同じセッションIDで作業を継続していたこと
- 復元の鍵は「累積カウンタは上昇しかしない」という不変条件(invariant)1つだけ。append-onlyのログに残っていた過去の「山(peak)」を、値が下がった瞬間=リスタートとみなして合算するアルゴリズムで、マイグレーションなしに正しい生涯合計を再構築した
「ラベルが実装より長生きを約束していた」というバグ
ブログ本文は、バグの本質をこう総括している。
You can build an agent harness that does everything right and still hand people a number that lies. Ours did. […] It was under reporting by more than half.
(すべてを正しく作ったつもりのエージェントharnessでも、人に嘘をつく数字を渡してしまうことがある。私たちのがそうだった。〔中略〕実際のコストの半分以上を過小報告していた)
各エージェントの使用量サンプルはAgentUsageSample.usdというフィールドに集計され、fleet.jsonに書き出されて、オーケストレーター(Michael)や各エージェント自身が参照する「LIVE ROSTER」の行にも表示される。全員がこの数字を見て「もう1人雇うべきか、それとも解散すべきか」を判断していた、とブログは書く。
問題は、このフィールドが実際には何を測っていたかだった。
AgentUsageSample.usd is cumulative since process start. Not since the agent started. Since the app started.
(AgentUsageSample.usdは「プロセス開始からの累積」であって、「エージェント開始からの累積」ではない。「アプリ開始からの累積」だった)
コレクタはメモリ上のマップに値を積算しており、アプリの再起動はそのマップを空の状態から作り直す。ところがエージェント自身は同じワーカー・同じ作業・同じsession_idで再開するため、「エージェントは続いているのに、コスト集計だけがゼロに戻る」という食い違いが生じていた。エージェントが長く生き続け、再起動を多く経験するほど、そのエージェントのコスト数字はより大きく実態から乖離する、という性質を持っていたことになる。
「一度も嘘には見えなかった」という発見の難しさ
このバグが厄介だったのは、単体で見るとどの数値も「正常」に見える点だったとブログは説明する。
Within any single run of the app the counter is perfectly monotonic, which is exactly how a lifetime total behaves, so a person watching it for an hour sees nothing suspicious. […] The signature only exists in the history. You cannot see it in a value. You can only see it in a sequence, and only if you look at the whole sequence at once.
(アプリの1回の実行の中では、カウンタは完璧に単調増加する。これはまさに「生涯合計」らしい振る舞いなので、1時間眺めていた人には何もおかしく見えない。〔中略〕この不具合の痕跡は履歴の中にしか存在しない。1つの値を見ても分からない。シーケンス全体を一度に見て初めて分かる)
負の値にもならず、NaNにもならず、何も例外を投げない。「アプリを再起動する」というごく普通の操作の後で、それ以前に画面上で見ていた数字と再照合する人はいない──この2点が、バグを「発見できるが、発見しにくい」ものにしていた。
復元の鍵はたった1つの不変条件
このバグを直せたのは、コストのログがcost-ledger.jsonlというappend-only(追記専用)ファイルに、これまで発行されたすべての使用量サンプルが行単位で残っていたからだ。同社が別記事「Append-Only Event Logs for Agents」(2026年6月1日公開)で説明している設計思想を読むと、この会社はhive内で起きた出来事を「1イベント1行のタイムスタンプ付きJSON、書いたら二度と変更・削除しない」という形で記録する方針を、コスト集計に限らず全般的に採用していることが分かる。この一般方針が、今回のコスト集計バグを「書き込み側でなく読み取り側のバグ」として復元可能にした前提になっている。ブログはこう書く。
A bug in a derived read is recoverable. The same bug in the write path would have meant a number that starts being right today and is wrong forever behind you.
(読み取り側の派生ロジックのバグは回復可能だ。同じバグが書き込み側にあったら、今日から正しくなるが、それより前はずっと間違ったままの数字になっていただろう)
復元アルゴリズムの発想はシンプルだ。1つのエージェント・1つのセッションの中では、カウンタは上がることしかできない。つまり「値が下がった」という事象は、それ自体が「リスタートが起きた」ことの動かぬ証拠になる。
A decrease is a restart, and it is the only thing a decrease can be.
(値の減少はリスタートである。減少が意味しうることは、それ以外にない)
ログを1行ずつたどり、エージェント・セッションごとに「現在開いているセグメントの高値(peak)」と「すでに閉じたセグメントの高値の合計(committed)」の2つの数字を保持する。値が下がったら、直前のセグメントはそのピークで終わったとみなしてcommittedに加算し、新しいセグメントを開く。生涯合計は最終的にcommitted + peakをすべてのセッションで合算した値になる。
「10セント未満は誤差として無視」を選ばなかった理由
このアルゴリズムには、一見常識的に見える「しきい値」を意図的に入れなかったという判断がある。
We looked at our own ledger and found real restarts falling from peaks under a dollar straight to zero. A one dollar threshold misses those completely, and it misses them silently, which is the same failure we were already fixing wearing a different hat.
(自分たちのログを見たところ、1ドル未満のピークからそのままゼロへ落ちる、本物のリスタートが実際に見つかった。1ドルのしきい値ではそれらを完全に、しかも静かに見逃してしまう。これは、今まさに直そうとしていたのと同じ種類の失敗を、別の顔でもう一度やることになる)
累積カウンタが下がる正当な理由は存在しない以上、「多分大丈夫」というグレーゾーンを切り出す必要がない、という判断だ。しきい値は浮動小数点誤差を吸収する程度の微小な値にとどめられた。
「自信満々のゼロ」より「分からない、と正直に言う」ほうがまし
もう一つの設計判断が、初回のログ全読み込みが終わるまでの扱いだ。
Zero is a lie with a confident face. […] So usdFor returns null until a pass has actually reached the end of the file, and there is an explicit warm flag behind it so a caller can tell "no spend" apart from "not read yet".
(ゼロは、自信満々の顔をした嘘だ。〔中略〕そこでusdForは、ログの終端まで実際に読み終えるまでnullを返す。「まだ読んでいない」ことを呼び出し元が区別できるよう、明示的なwarmフラグも用意した)
fleet.jsonは、この集計処理がまだ「ウォーム」でない間は旧来のセッション単位の数字にフォールバックし、ウォームになった瞬間に生涯合計へ切り替える設計になった。旧来のセッション単位の数字自体もsessionUsdとして残された。「アプリを開いてから今のセッションでいくら使ったか」もまた実際に意味のある問いだから、というのがその理由だ。
検証不可能な推測値を、2つ目の独立実装で照合する
このバグ修正が扱っているのは「正解となる請求書が存在しない、推測によって導いた数値」だ。ブログはここで、テストが通ること自体は「コードが前提と一致していること」の証明にしかならず、「前提そのものが正しいこと」の証明にはならないと釘を刺す。そこで、まったく別の人間が独立に書いたもう1つの実装でログを畳み込み、両者の結果を突き合わせた。
They agree to the cent, with no per-agent disagreement anywhere in the set. That is not proof, but two independent implementations agreeing on every agent is a very different level of confidence than one implementation agreeing with itself.
(両者はセント単位まで一致し、どのエージェントについても不一致はなかった。これは証明ではないが、独立した2つの実装がすべてのエージェントについて一致することは、1つの実装が自分自身と一致することとはまったく違う水準の確信を与える)
あえて直さなかったもの
このポストモーテムがもう一つ誠実だと感じたのは、「直せるのに直さなかった箇所」を明記している点だ。フロア全体の支出上限(サーキットブレーカー)は、同じリセット型カウンタを参照しているため、実質的に「アプリの1セッション分」しか計測していない。ブログはこれを単純なバグではなく「支出上限とは『今日これだけ使ったら止める』なのか、『生涯でこれだけ使ったら止める』なのか、という未回答の設計判断だ」と位置づけ、あえてパッチを当てずに文書化にとどめたと書いている。
A fix that silently rewrites a policy is not a fix, it is a decision taken by whoever happened to be in the file.
(ポリシーを黙って書き換える修正は、修正ではない。たまたまそのファイルを触っていた人間が下した決定にすぎない)
「なぜ気づけなかったか」のパターンは他のポストモーテムとも重なる
このバグ修正はv0.4.5に含まれ、同じリリースには「Apple SiliconでCoreMLが量子化embeddingグラフをオーバーフローさせ、意味記憶が一度も機能していなかった」バグと、「エージェント同士のメッセージが確実に届いていなかった」バグの修正も同居していたとブログは明かしている。この記事では、同社ブログが公開している他の2本のポストモーテム記事も直接読み、共通するパターンを確認した。
| 記事 | 公開日 | 症状 | 直接原因 |
|---|---|---|---|
| The Spend Counter That Went Back to Zero | ― | コスト表示が実額の59%しかない | プロセス再起動でメモリ上の累積カウンタがゼロに戻る |
| The Newline That Silenced Windows Agents | 2026-08-19 | Windows版でエージェント同士が一度もメッセージを送受信できていなかった。エラーは一切出ない | npmの.cmdシムがcmd.exe経由の起動を強制し、cmd.exeが引数を最初の改行で打ち切る。複数行の起動プロトコルが1行しか届かない |
| Why Our Auto-Update Never Ran | 2026-08-08 | v0.3.4以降、3リリースにわたって自動更新が一度も作動していなかった | CommonJS/ESM境界をまたぐ分割代入がundefinedを生み、catchブロックがエラーメッセージを握りつぶしていた |
「The Newline That Silenced Windows Agents」(著者Chaitanya Giri氏、2026年8月19日公開)を実際に読むと、原因は3層に分かれた「合わせ技」だと説明されている。(1) Windows上のnpm install -gは実行ファイルではなく.cmdバッチのシムをPATHに置く、(2) .cmdはPE実行ファイルではないためNode.jsのchild_processは起動コマンドをcmd.exe /d /s /c "..."へ静かに書き換える、(3) cmd.exeはDOS時代からほぼ変わらない構文解析規則で、コマンド文字列を最初の改行で読み捨てる。Munder Difflinの起動プロトコルは複数行のブロックだったため、Windows環境ではエージェントが1行目(「あなたは協調するhive内の自律エージェントです」に相当する文言)しか受け取れず、それでいてターミナル描画やプロンプト応答は正常に見えるため、「エージェントは静かなだけ」で「エージェントは壊れている」ことと外形上見分けがつかなかったという。修正は.cmdシムを経由させず、シムが指す実体(node.exe本体など)を直接argv配列で起動する方式に変更したが、OpenCodeのnpmパッケージがコンパイル済みバイナリを同梱する形式だったため、この修正だけでは救えないケースが0.4.4で追加対応されている。記事の結論は「エラーが出ないことは証拠ではなく、単なる一つの主張にすぎない」という一文で、コスト表示バグの教訓と同じ構図であることが分かる。
数字はすべて公開ブログの記述、自分たちのフロアで検証したものではない
この記事はMunder Difflin公式ブログ計4本の記事本文を一次ソースとしている。実際にMunder Difflinをインストールし、複数エージェントを走らせてコスト表示やWindows版の通信、自動更新の挙動を自分の環境で確認する検証は行っていない。59%という過少表示の割合は、あくまでMunder Difflin社自身のフロアでの実測値であり、他の利用者の環境でも同じ割合になるとは限らない。復元アルゴリズムのコード全体(ブログに掲載されている抜粋以外の部分)や、実際にリリースされたv0.4.5のソースコードを読んで検証したわけでもない。「append-only event log」を扱うブログ記事(2026年6月1日公開)は、この会社が採用しているログ設計思想の一般論であり、コスト集計バグの直接の記述ではない点も付け加えておく。Zenn検索では該当する記事は0件(2026年8月27日実測)。
関連記事: 「クローンだらけのオフィス」を自宅PCに──Munder Difflinはマルチエージェント運用をどう解決したか / AIに毎回同じ説明をするのが面倒──「朝会」方式で引き継ぎをなくす運用
感想・指摘はコメント欄へ。
出典・参照資料
AIニュースの解説を動画でも
YouTubeでは注目ニュースの背景を解説し、Xでは新着記事をお知らせしています。
コメント
まだコメントはありません。最初のコメントを書いてみませんか?
AIについて聞きたいことはありますか?
質問箱で無料で受け付けています。回答は公開され、他の方の参考にもなります。
質問箱を見る →新しい記事をメールで受け取る
AIの新しい発表を、出典付きで整理して届けます。