# Claude Code の長いテストログを圧縮すると、エラーはどこまで残る？Context Diet の境界を検証

> Canonical: https://vxi.me/ja/posts/claude-code-context-diet-log-compression/
> Locale: ja-jp
> Published: 2026-10-11
> Updated: 2026-10-11
> Markdown: https://vxi.me/ja/posts/claude-code-context-diet-log-compression.md

Claude Code で `npm test` や `pytest` を実行すると、何百行もの `PASS` が流れる一方で、本当に直したい失敗は一か所だけ、ということがある。Agent は原因を探すためにコマンドを再実行したり、ログ全体を読み直したりする。最後の数十行だけを渡す方法も安全とは限らない。重要な例外が途中に出ているかもしれないからだ。

[Hacker News の Show HN](https://news.ycombinator.com/item?id=50024608) で見つけた [Context Diet](https://github.com/drkokorev/context-diet) は、この問題に取り組む Claude Code 用の Mod だ。ツール出力を短い要約に置き換えつつ、全文を読み返せるファイルパスを残す。最初は合理的だと思った。ただ README にある「every failure」という説明は気になった。表示できる文字数が限られるのに、異なるエラーが大量に発生したら本当にすべて残せるのだろうか。

そこで対象を [v0.3.0、commit `689284a`](https://github.com/drkokorev/context-diet/tree/689284a893f1aa509b9c6c1c06b91ef1ca52279c) に固定し、Hook、圧縮ロジック、テストを読んだ。そのうえで汎用ログ圧縮部分を手作業で JavaScript に移し、人工的に作ったログで確認した。

最初に範囲を明確にしておく。**実際の Claude Code に Context Diet Mod をインストールして実行したわけではない。Claude モデルの A/B テストもしていない。** 以降の数値は、ソース由来の圧縮アルゴリズムと人工ログに対するテスト結果であり、実環境でのエラー欠落率や Token 削減率ではない。

## モデルに入るツール出力を短くする仕組み

固定したバージョンの [`session.append` Hook](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/plugins/context-diet/hooks/register.tsx) は、一定以上の長さになった Bash、Grep、Glob、JSON 形式の MCP 出力などを対象とする。Read、Edit、Write、WebFetch、JSON ではない MCP の説明文は、この経路では圧縮しない。既定のしきい値は **8,000 文字**で、8,000 Token ではない。

すべてに単純な `tail` を使うわけでもない。[パーサーのコード](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/plugins/context-diet/hooks/tools.ts) には Jest、Vitest、pytest、Go test、Cargo、TypeScript チェッカーなどを認識し、成功行より失敗箇所や位置情報を優先する処理がある。JSON は構造を残し、Diff、Grep、ファイル一覧にも別の要約方式がある。ただし、これは**ソース上で確認した処理方針**であり、私が各テストフレームワークを Claude Code 上で動かした結果ではない。

重要なのは元データへの戻り道だ。Hook は要約へ置き換える前に全文を `.context-diet/out-NNN.txt` に保存しようとする。ファイル名は **300 個のスロット**で循環する。保存に失敗した場合は、存在しないファイルへのリンクを返さず、元のツール結果をそのまま通す。

もっとも、ローカル保存は永続保管ではない。古いスロットは再利用されるし、ログ内の秘密情報や環境変数、プロジェクトのパスがディスクに残る可能性もある。`.gitignore` は誤って Git に追加するリスクを下げるが、暗号化でもバックアップでもない。本番インシデントで必要な原本は別に保管するべきだ。

## エラー優先でも、予算を超えれば省略される

[`diet.ts` の汎用ログ圧縮処理](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/plugins/context-diet/hooks/diet.ts) を読むと、重複する行をまとめ、冒頭と末尾に文字数を割り当て、後方からエラーを探して前後の文脈を残そうとしている。

決定的なのは次の条件だ。

```ts
const cost = window.reduce((n, i) => n + (folded[i]?.text.length ?? 0) + 1, 0)
if (used + cost > target) continue
```

あるエラーの周辺情報が残りの予算に収まらなければ、その部分は飛ばされる。エラーが一つなら効果的なこともある。しかし、異なる失敗が数十件あって長いスタックトレースまで付けば、有限の要約にすべてを入れることはできない。

Jest や pytest として認識される出力には専用パーサーがあり、別の優先順位付けを行う。**以下の検証は汎用ログのフォールバック処理を対象にしており、Jest/pytest 専用パーサーの欠落率を測っていない。** 「Jest のエラーが 95% 消える」などと読み替えるのは誤りだ。

README の「every failure」は、実装上は「失敗を優先し、全文に戻れるようにする」と捉えるのが妥当だと思う。「どんな入力でもすべての失敗行を短い要約に表示する」という保証にはならない。

## エラーが一件の場合と、145 件ある場合

完全な Claude Code 環境を構築する代わりに、固定ソースから汎用 `dietLog` と関連テキスト関数を JavaScript に手動移植した。Node.js v22.16.0 の隔離環境で実行した**合成テストは 4/4 成功、終了コードは 0**。公式 Mod の統合テストではない。

| 人工的な入力 | 原文の長さ | 要約の長さ | 観察結果 |
| --- | ---: | ---: | --- |
| 長いログの中央に重要なエラー一件 | 32,087 文字 | 1,175 文字 | 1/1 件のマーカーが残った |
| 145 種類のエラー、要約予算 1,450 文字 | 23,146 文字 | 1,419 文字 | 元のエラー行は 7/145 行だけ表示 |
| 短い出力 | 45 文字 | 45 文字 | 変更なし |
| `PASS errorHandler.test.ts` という分類確認 | 52 文字 | 52 文字 | PASS をエラーと誤判定しない |

二番目のケースでは、元のエラー行 **138 行が表示用の要約には含まれなかった**。以下は実際に記録した*人工ログの要約*から抜き出したものだ。実プロジェクトで起きた障害ではない。

```text
… [225 lines cut] …
ERROR CASEei unhandled category_ts in /app/modules/amy/workflow.ts:40 expected stable result bci
ERROR CASEej unhandled category_tt in /app/modules/amz/workflow.ts:40 expected stable result bcj
ERROR CASEek unhandled category_tu in /app/modules/ana/workflow.ts:40 expected stable result bck
…
… [79 lines cut] …
```

ここで小さな罠がある。圧縮器自身の表示は「5 of 145 error lines with context」だったのに、最終的な要約に存在する元のエラー行を数えると **7 行**だった。冒頭・末尾として保持された行にもエラーが含まれ得るからだ。「明示的に選んだエラー周辺の数」と「目に見えるエラー行数」は同じ指標ではない。

**1,450 文字という要約予算は、境界を見るため意図的に小さくした人工的な条件であり、既定値ではない。** v0.3.0 の既定しきい値は 8,000 文字。通常の長い出力は `Math.max(2_000, Math.round(threshold / 2))` により要約目標 **4,000 文字**、Claude Code がすでにファイルへ保存した出力なら **3,000 文字**を使う。予算が増えれば表示できる情報も増え得るが、この数字から実際のプラグインのエラー欠落率は計算できない。

さらに、文字数の選択部分だけを切り出した **Node.js 標準ライブラリのみの二件のテスト**も実行した。こちらは人工エラー行が少し長いため、同じ 1,450 文字で **8/145 行**が表示された。**2/2 成功**しているが、先ほどの 7/145 と同じ入力ではない。二つを性能比較のように扱うべきではない。

次はその最小再現コードの全文だ。`budget-replay.test.mjs` として保存し、`node --test budget-replay.test.mjs` で実行できる。

```js
/**
 * First-party negative control for Context Diet v0.3.0's generic dietLog budget path.
 * Pin: drkokorev/context-diet @ 689284a893f1aa509b9c6c1c06b91ef1ca52279c.
 * Only the error-window budget logic is mirrored, for controlled ASCII log lines.
 * This is NOT an upstream Mod invocation, a benchmark, or a production loss rate.
 * The full adapted dietLog fixture and observed digests are in the separate evidence bundle.
 */
import assert from 'node:assert/strict';
import test from 'node:test';

const id = i => { let s='',n=i+1; while(n){ n--;s=String.fromCharCode(97+n%26)+s;n=Math.floor(n/26); } return s; };
const noise = n => Array.from({length:n},(_,i)=>'step '+id(i)+' completed '+id(i+1200)+' '+i);
const critical = line => /^ERROR\b|^\s+(Expected|Received):/.test(line);
function select(raw,target){
  const folded=raw.split('\n').map(text=>({text,level:critical(text)?2:0}));
  const errorHits=folded.map((l,i)=>l.level===2?i:-1).filter(i=>i>=0);
  const keep=new Set();let used=0;
  const headBudget=target*(errorHits.length?0.1:0.2);
  const tailBudget=target*(errorHits.length?0.25:0.35);
  for(let i=0;i<folded.length&&used<headBudget;i++){keep.add(i);used+=folded[i].text.length+1;}
  let tailUsed=0;
  for(let i=folded.length-1;i>=0&&tailUsed<tailBudget;i--){
    if(!keep.has(i)){keep.add(i);tailUsed+=folded[i].text.length+1;}
  }
  used+=tailUsed;
  let windowsShown=0;
  for(let h=errorHits.length-1;h>=0;h--){
    const at=errorHits[h];const window=[];
    for(let i=at-2;i<=at+3;i++)if(i>=0&&i<folded.length&&!keep.has(i))window.push(i);
    const cost=window.reduce((n,i)=>n+folded[i].text.length+1,0);
    if(used+cost>target)continue; // EXACT budget guard from upstream dietLog.take()
    window.forEach(i=>keep.add(i));used+=cost;windowsShown++;
  }
  return {visible:folded.filter((_,i)=>keep.has(i)).map(l=>l.text).join('\n'),totalCritical:errorHits.length,windowsShown};
}
test('single late/central failure remains visible',()=>{
  const text=[...noise(310),'ERROR ECASEONE impossible transaction transition','  Expected: completed','  Received: pending',...noise(310)].join('\n');
  const out=select(text,2000);
  assert.ok(out.visible.includes('ECASEONE'));
});
test('many distinct failures cannot all fit finite digest budget',()=>{
  const failures=Array.from({length:145},(_,i)=>'ERROR CASE'+id(i)+' unhandled category_'+id(i+400)+' in /app/modules/'+id(i+900)+'/workflow.ts:40 expected stable result '+id(i+1300));
  const raw=[...noise(90),...failures,...noise(90)].join('\n');
  const out=select(raw,1450);
  const retained=failures.filter(line=>out.visible.includes(line)).length;
  assert.ok(retained>0&&retained<failures.length);
  process.stdout.write(JSON.stringify({failure_lines:failures.length,retained,missing:failures.length-retained,digest_budget_chars:1450,mode:'source-budget-window-control-not-plugin-runtime'})+'\n');
});
```

これは汎用ログの予算判定の再現であり、Claude Code の Mod API は呼んでいない。テストが通っても、実際の Agent が正しく問題を解決した証拠にはならない。

私が重視したいのは、圧縮の是非よりも**要約は証拠の代わりではなく、原文に戻るための索引である**という点だ。

## まず dry を使い、capture は別に判断する

Mod に対応した Claude Code を使っているなら、[固定した版の README](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/README.md) と現在のクライアント文書を確認してほしい。当時の指定は Claude Code **2.1.287 以降**。私は実際のクライアントでインストールを試していない。

```text
/plugin marketplace add drkokorev/context-diet
/plugin install context-diet@context-diet
```

まず `/diet dry` で日頃のテストを流し、`/diet report` でどれだけ省略される予定かを確認するのがよさそうだ。dry は元のツール結果を置き換えず、削減量だけを記録する設計になっている。完全なログが必要なコマンドには `/diet keep <コマンドの一部>`、あるいは `/diet off` を使う。

一方 `/diet capture on` は別の問題に対応する。Claude Code 自身が長い出力を先に切ってしまう場合、対象の単純なテスト・ビルドコマンドをラップし、stdout と stderr をまとめて `.context-diet/run-NN.log` に記録してから読み返し、終了コードを維持しようとする。**既定では無効**だ。

この書き換えにより、以前許可した `Bash(pytest:*)` などの権限パターンに一致しなくなり、追加の確認が必要になる場合がある。出力の結合は表示順にも影響し得る。パイプ、複数コマンド、リダイレクト、watch やバックグラウンド実行などは対象外だ。厳密な権限管理やリアルタイムの出力に依存する作業では、無条件に有効化しない方がいい。無効化するには `/diet capture off` を使う。

[README に載る原作者の結果](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/README.md)では、七つの質問は両条件で全問正解、ツール呼び出し総数は **13 回から 8 回**に減ったという。また **18 回の Agent 修正試行**で平均呼び出し回数は、未使用時 **8.0**、使用時 **6.0**、capture 併用時 **5.8** と報告されている。これは原作者による小規模な集計で、私は実行履歴やコストを独立に検証していない。

成功ログが大量に出る作業なら Context Diet は評価する価値がある。ただ、障害調査や複数エラーが絡むケース、自動 PR マージの判断では、要約だけを根拠にせず全文を読むべきだ。Agent が実際に全文を再読するのか、見えた数件だけ直して PR を出してしまうのかは、今回まだ検証していない振る舞いの問題である。

以前取り上げた [CLAUDE.md / AGENTS.md / Skills の指示分割](https://aitokenhacks.com/posts/claude-md-agents-md-skills-ci/)や [Agent 間の状態引き継ぎ](https://aitokenhacks.com/posts/ai-agent-orchestration-after-assistants/)は、「何を読み込ませるか」「誰が担当するか」の話だった。今回はもっと身近な問いだ。目の前のツール結果は完全な証拠なのか。それとも、証拠へ戻るためのメモなのか。
