開発claude-code-tools

セッションが何度も落ちた。The model's tool call could not be parsed (retry also failed). という同じエラーで、ファイルを探している最中や計画をレビューしている途中に、リトライまで失敗してセッションごと沈んだ。今日はこのエラーの原因を腰を据えて潰しにかかった一日になった。

落ちる現象には前から心当たりがあった。「汚染は大体、数十MBの巨大ファイルだ」という経験則を自分の中に持っていて、巨大なログがコンテキストに居座ると tool call が壊れる、と信じ込んでいた。だから今回も「どこかに肥大化したセッションがあるはずだ」という前提で調査を始めた。結論を先に言うと、この思い込みは計測で完全に覆った。

巨大ファイル犯人説で調査を始めた

まず別セッションを立てて、原因の調査をさせることにした。ところがその調査役のセッションも、ログを読み込んでいる途中で同じ malformed を拾って止まった。「調査役にも汚染が伝染する」という経験則どおりの光景で、最初はこれが犯人説を補強する材料に見えた。

セッションログは C:\Users\numbe\.claude\projects\ 配下に .jsonl でずらりと並んでいる。中身を全部 Read したら調査役自身がまた汚染を拾うので、各行の文字数だけを測って巨大な行を炙り出した。

awk '{print length($0)"\t"NR}' SESSION.jsonl | sort -rn | head

最大のセッションは48.6MB、次が17.2MBだった。巨大行の正体を、内容本体を出さずにメタ情報だけ抜いて確かめると、すべて画像のbase64だった。8MB級の行は画像ファイルの Read 結果、3MB級が多数あるのは Chrome DevTools の take_screenshot(fullPage含む)。コンテキストを膨らませているのは画像で間違いなかった。ここまでは犯人説どおりに進んでいた。

計測したら逆だった

決定打が出たのは、parse失敗の回数をセッションごとに並べたときだ。

セッション内容サイズ画像base64parse失敗結末
deb5e53dlogic-flow-v2.html48.6MB14行0完走
ce3171c1/make-diary17.2MB31行0完走
7dafc090make-diary-parallel3.0MB6行0完走
58c18c40整合性確認0.1MB0あり落ちた

48.6MBの画像だらけのセッションは parse失敗0回で最後まで走り切っていた。一方で落ちたのは画像ゼロの0.1MBという極小セッションだった。巨大ファイルと parse失敗は、相関がないどころかほぼ逆相関だった。「数十MBが犯人」という経験則は、少なくともこの「落ちる」現象には当てはまらなかった。

巨大セッションログは --resume で再開しない限り新しいセッションのコンテキストに一切読み込まれない。ディスクに置いてあるだけのファイルで、放置しても今動いているセッションが重くなることも、parse失敗が起きることもない。「掃除しないと落ちる」という因果は、もともと存在しなかった。

真因はツール呼び出しの引数破綻だった

落ちた唯一の本物のエラーセッション(58c18c40)で、malformed が出る直前のシーケンスを追った。複雑なjqを避けて、行の種類だけを素朴に並べた。

35: assistant tool_use Bash      ← Bashを呼ぶ
38: user tool_result             ← Bashの結果が返る
40: assistant thinking           ← 次の手を考える
41: user "malformed... retry"    ← 次のツール呼び出しの生成が壊れた
43: assistant "retry also failed"

直前の Bash は find ~/.claude -iname "*make-diary*" というファイル探索で、その結果はわずか3.8KBだった。巨大データはどこにもない。壊れていたのは入力データではなく、ツール結果を受け取った直後に次のツール呼び出しを生成する段階だった。モデルが出力する tool_use の引数の構造化に失敗していた。これで巨大データ原因説は確定的に否定された。

落ちたセッションの共通項は「make-diary系の定義・コマンド・スキルを多数突き合わせる整合性確認」だった。記号だらけのテキストの断片を引数に何度も詰め込んで照合する作業で、引数が壊れやすかったと見ている。

誘発条件として固まったのは次の3つだ。

  • 多数のファイルパス・コード/markdown断片・特殊文字を、1回のツール呼び出しの引数に大量に詰める
  • 複雑なjqワンライナー(ネスト引用符・gsub・文字列スライスの多用)を生成する
  • コンテキストにエラー文字列やログJSONが溜まり、tool_use の生成が連鎖的に不安定化する

2つ目は身をもって実証した。調査セッション自身が、複雑なjqワンライナーを組み立てた直後に1回 malformed を出した。原因が「引数の質」であることを、自分のセッションが落ちかけて証明してくれた。以降はシンプルなコマンドに切り替えたら安定した。

計測の落とし穴—単純なgrepは過大カウントする

調査の最後で、自分の計測そのものに欠陥があったことに気づいた。parse失敗の回数を grep -c "could not be parsed" で数えていたが、これは本物のエラーだけでなく、ハーネスのリトライ指示文、ユーザー発言内の引用、そして自分が調査議論で書いた同じ文字列まで全部拾う。

isApiErrorMessage:true で本物のエラーだけに絞り直したら、数字が一変した。

セッション単純grep本物のエラー
58c18c4052(唯一の本物)
44b21973(調査セッション自身)320
6bbbd8f510

調査セッション自身は単純grepで32ヒットしたのに、本物のエラーは0回だった。32回は全部、自分が調査の議論の中で could not be parsed という文字列を繰り返し書いたものだった。本物の malformed で落ちたのは58c18c40のただ1セッション、回数にして2回だけ。これを取り違えていたら「あちこちで落ちている」という誤った深刻さで原因を見誤るところだった。本物だけ数えるには、こう絞る。

grep '"isApiErrorMessage":true' SESSION.jsonl | grep -c "could not be parsed"

ついでにセッションIDの取り違えもあった。「現在動いているセッションのID」は、バックグラウンドタスクの出力パス(...\Temp\claude\<project>\<SESSION-ID>\tasks\...)で確認できる。これを見ずに進めて、調査セッション自身を「過去に落ちた別の調査役の墓場」だと誤認していた。ログ調査では、まず自分がどのセッションにいるのかを確定させる。

小さく単純に保つルールに切り替えた

調査結果を ~/.claude/rules/tool-call-malformed.md に落とし込んで、運用ルールを書き換えた。要点はこうだ。

  • could not be parsed が出たら、ファイルサイズより先に直近のツール呼び出しの引数を疑う
  • コード/markdown断片・特殊文字を1回のツール引数に大量に詰めない。突き合わせ作業は小分けにする
  • ログ調査では中身を全Readせず、複雑なjqを避けて素朴な抽出に留める
  • parse失敗を数えるときは必ず isApiErrorMessage:true で絞る

「汚染」という一語で混ぜていた2つの問題を、別々に扱うことにした。

種別実体影響対処
(A) 肥大化画像base64ディスク・トークン浪費、cache missスクショ/画像Readを控える、古いセッションを掃除
(B) parse失敗で落ちるmalformed tool callセッションが復帰不能引数を小さく単純に保つ

(A)は落ちる原因ではないが、ディスクとトークンは確かに食う。掃除する価値はあるので、別の話として片付けた。

session-backupで肥大化したセッションを掃除した

(B)の対策を固めたあと、ついでに(A)の掃除に回った。session-backup スキルで、前回バックアップ(2026-03-08)以降のセッションを外付けSSDにまとめ、画像で肥大化した古い巨大セッションを削除した。

ここでも一度つまずいた。差分バックアップで tar -T にファイルリストを渡したら、全行が unrecognized option で拒否され、Exiting with failure status で終わっていた。にもかかわらず「圧縮完了 2.21GB」と表示が出ていた。tar の終了コードを確認せず無条件にメッセージを出していたための誤報だった。

破壊的な削除の前は、バックアップの成功を必ず確認する。tar の終了コードを見て、アーカイブの件数と削除対象の件数を厳密に照合した。照合で未カバー0件を確認してから、1ヶ月超の3,587件を削除した。本体のセッションログは6.4GB から1.5GB に軽くなった。終了コードを見ずに「完了」と出すのは、調査で計測の落とし穴に落ちたのと同じ構図だった。

この日の収穫

経験則は調査の出発点としては役に立つが、検証なしに結論にすると判断を歪める。「数十MBが犯人」という思い込みのまま掃除を進めていたら、肥大化は片付いても落ちる現象は再発し続けたはずだ。計測して初めて、48MBは無傷で0.1MBが落ちているという逆の絵が見えた。

数えるときは何を数えているのかを疑う。単純なgrepが32ヒットしても、本物は0だった。終了コードを見ずに「完了」と出すのも、自分の書いた文字列を本物のエラーと数えるのも、根は同じ「確かめずに信じた」ことだった。ツールの引数は小さく単純に保つ。これが今日いちばんの収穫だった。