Repository navigation
Description
依頼(原文)
何を見つけたか
スプリント PR 1973(#1953 の帳簿)の cross-review のラウンド 1 で、codex が重大(major)の指摘を 5 件出した。そのうち次の 4 件は帳簿の数え方の誤りで、修正も起票もされないまま develop へマージされ、10.17.74 で出た。今の develop(425820c72、v10.18.0 の後)にもそのまま残っている(2026-10-17 に指摘 1 を再現: 同じ
message.idの 2 行で output_tokens が 1 → 50 の記録をtranscript_usageが 1 と数える)。
番号 箇所 指摘 1 plugins/ndf/scripts/lib/usage_capture.py:195(transcript_usage)同じ message.idの 2 行目以降を捨てるため、ストリーミングで更新された最後の usage が帳簿に入らず、少なく数える。scripts/token-usage.pyのscan_fileは「message.id で重複を除き、最後の usage を取る」2 plugins/ndf/scripts/lib/usage_capture.py:306-322(record_subagent)SendMessage で続けたサブエージェントがもう一度止まると、会話の記録全体の累計を再び書き足すため、続ける前の分を 2 回数える 3 plugins/ndf/scripts/lib/usage_aggregate.py:188(transcripts)期間を会話の最初の timestampだけで決め、採った会話は全体を足す。日をまたぐ会話で対象日の分が落ちるか、期間外の分が混じる4 plugins/ndf/scripts/lib/usage_aggregate.py:343(step_totals)runtime: claudeの work が書くsource=superviseの Claude の行を除くため、その使用量が報告の両方の欄から消える(codex の指摘。再現は未確認)残る 1 件(
issues/issue-1953-design.md:395の「実装で確かめる」2 項目の証跡)は、kiro が同じラウンドのレビュー本文で確かめた記録がある。どこで見つけたか
- レビュー: スプリント m1953 #1973 (review) (「差分の外を指す指摘」の節。インラインのスレッドは無い)
- 修正の worker の報告:
~/.local/state/ndf/sv/sprint-m1953/6-check-state/05-review.outの「codex の本文にだけある 5 件」。対応の対象を未解決のスレッドの数(0 件)で決めたため「今回の対象外とし、取り込み側へ渡す」とした- 取り込み側(cross-review の drive)は 5 件を次のラウンドへ渡さず、検査の件数は
findings 3 / fixed 3 / unresolved 0で終わった(run-6-review.log)- スプリントの振り返りで、範囲外として扱った指摘に番号が無いことから拾った
なぜ今直さなかったか
指摘が出た時点では修正の対象から外れ、範囲外の起票もされなかった(取りこぼし)。仕組み側の原因は別の課題(cross-review の本文だけの指摘が修正の対象から落ちる)に分ける。
受け入れ条件
- 1. 同じ
message.idが複数行に現れる会話の記録で、帳簿の値がその id の最後の usage になる(token-usage.pyと一致する)- 2. 同じ
agent_idのサブエージェントが 2 回止まっても、帳簿の合計が会話の記録の合計と一致する(2 回数えない)- 3. 期間の境界をまたぐ会話で、期間内の応答の分だけが集計に入る
- 4. 4 の指摘を再現で確かめ、当たるなら
runtime: claudeの work の使用量が報告のどちらかの欄に 1 回だけ載る。当たらないなら理由を課題に残す- 5. 1〜3(と 4 が当たるなら 4)を再現するテストがある
関連
- 由来: PR スプリント m1953 #1973(スプリント m1953 の検査)、issue トークン消費を数える範囲がサブエージェントだけで、claude -p・codex・kiro・agy の呼び出しが計測から漏れる → 経路を問わずすべての呼び出しをランタイムごとの帳簿で数える #1953
- 仕組み側の原因: cross-review の本文にだけ書かれた指摘(差分の外・422 で総評へ移したもの)が修正の対象にも件数にも入らず、未対応のまま収束する #1980(cross-review の本文だけの指摘が修正の対象から落ちる。10.17.75 で実装済み。閉じている)
(書き手: 起票者(スプリント m1953 の振り返りで AI が起票)、日付: 2026-10-13。利用者の指示の原文は無い)
起票者の推定
課題の本文は、スプリントの振り返りで AI(起票者)が書いた。利用者の指示の原文は無く、下の条件はすべて起票者が推して足したものである。承認ゲート 1 で利用者が確かめるまで、利用者の決定として扱わない。
| # | 条件 | 推した出所 | この要求での扱い |
|---|---|---|---|
| S1 | 同じ message.id の応答は、最後の usage を帳簿の値にする(scripts/token-usage.py と一致させる) |
本文の受け入れ条件 1。scripts/token-usage.py:224 の「message.id で重複を除き、最後の usage を取る」 |
受け入れ条件 1 |
| S2 | 同じサブエージェントが 2 回止まっても、帳簿の合計を会話の記録の合計と一致させる | 本文の受け入れ条件 2。PR 1973 のレビューの codex の指摘 | 受け入れ条件 2・3 |
| S3 | 期間の境界をまたぐ会話は、期間内の応答の分だけを集計に入れる | 本文の受け入れ条件 3 | 受け入れ条件 4・5 |
| S4 | runtime: claude の work の使用量は、報告のどちらかの欄に 1 回だけ載せる |
本文の受け入れ条件 4(再現が未確認と書かれていた) | 受け入れ条件 6・7。載せる欄は前提 3 |
| S5 | 1〜4 を再現するテストを置く | 本文の受け入れ条件 5 | 受け入れ条件 8 |
| S6 | 本文だけの指摘が修正の対象から落ちる仕組み側の原因は、別の課題で扱う | 本文の「なぜ今直さなかったか」と #1980(閉じている) | 含まない |
目的
- 使用量の帳簿と、その集計(
usage-report.py・supervise の報告)の Claude の値を、Claude Code の会話の記録の値と一致させる。トークン削減の効果を、少なく数えた値・2 回数えた値・期間外の値で測らないようにする
前提
- 前提 1: 4 件の指摘は、今の develop(d51159151)で再現した(2026-11-05)。手順と結果は下の「再現の記録」にある
- 前提 2: 既に帳簿へ書かれた行(2 回数えた行を含む)は直さない。値が変わるのは、この変更が配布された後に書かれる行と、その後に行う集計だけである
- 前提 3: supervise の work(
runtime: claude)で起動した claude の使用量は、報告の「CLI(drive・work の中)」の欄に載せる。理由: この起動は external-ai.py と起動の共有処理(launch-cli.sh)を通り、codex・kiro・agy の work と同じ経路で帳簿へ書かれる。「LLM の使用量」の欄は supervise 自身が受け取ったclaude -pの結果の使用量だけを持つ(runtimeを持つ work の結果の使用量は空) - 前提 4: 帳簿の行(サブエージェントの行を含む)は、行の時刻(
at)で期間に入れる。1 行の中の応答を期間で分けない。期間で分けるのは、会話の記録から足す分(会話の記録だけの呼び出し)だけである - 前提 5: サブエージェントの会話の記録は、同じ
agent_idで続けても 1 本のファイルに追記される(SendMessage で続けた後の hook の入力のagent_transcript_pathは同じファイルを指す)
対象範囲
含む:
- サブエージェントの会話の記録 1 本から使用量を読む処理(同じ
message.idの扱い) - SubagentStop の hook が帳簿へ書く値(同じ
agent_idの 2 回目以降の停止) usage-report.pyの Claude の帳簿の 3 つの値で、会話の記録から足す分の期間の絞り方- supervise のステップの帳簿の行の集め直し(
step_totals)での、runtime: claudeの work の行の扱い - 確定仕様
docs/specifications/ndf-usage-ledger-by-runtime.mdの、上の 4 つに当たる記述
含まない:
- 既に書かれた帳簿の行の書き直しと、過去の集計のやり直し(前提 2)
- 帳簿の行を応答ごとに分けて期間で切ること(前提 4)
- cross-review の本文だけの指摘が修正の対象から落ちる仕組み(cross-review の本文にだけ書かれた指摘(差分の外・422 で総評へ移したもの)が修正の対象にも件数にも入らず、未対応のまま収束する #1980。閉じている)
scripts/token-usage.pyの変更(一致させる相手として読むだけ)- codex・kiro・agy・jev の帳簿の取り出し方
再現の記録
一時ディレクトリに会話の記録と帳簿を作り、今のコードの関数を呼んだ(2026-11-05、d51159151)。
| 指摘 | 入力 | 今の値 | 期待する値 |
|---|---|---|---|
| 1 | 同じ message.id の 2 行(output_tokens が 1 → 50) |
transcript_usage の output_tokens = 1 |
50 |
| 2 | 1 回目の停止で応答 1 件(出力 5)、続けた後の 2 回目の停止で応答 2 件(出力 5・7) | 帳簿の出力の合計 = 17 | 12 |
| 3 | entrypoint=sdk-cli の会話で、応答が JST 11-01 23:50(出力 100)と 11-02 00:10(出力 7) |
期間 11-01 で 107、期間 11-02 で 0 | 11-01 で 100、11-02 で 7 |
| 4 | NDF_USAGE_SOURCE=supervise・NDF_USAGE_KIND=work で record_output("claude", …) を書いた後の step_totals |
{}(行が除かれる) |
Claude の行が 1 回載る |
指摘 4 は、supervise 自身の claude -p の行も source=supervise・kind=work で書かれる(supervise_lib/claude.py の claude_kind)。今の帳簿の行の source と kind だけでは 2 つを見分けられない。
ドメインイベント
| # | イベント | 引き金 | 失敗したとき | 順序の前提 |
|---|---|---|---|---|
| E1 | Claude Code が会話の記録へ応答の行を書いた。ストリーミングの途中と最後で、同じ message.id の行が複数書かれる |
応答の生成 | NDF の外。読む側は読めた行だけを数える | — |
| E2 | サブエージェントが止まり、SubagentStop の hook が使用量を帳簿へ 1 行書いた | サブエージェントの停止 | 帳簿へ書けなくても hook は落ちない(帳簿の契約の I5)。その回の分は帳簿に残らない | E1 |
| E3 | 同じサブエージェントを SendMessage で続けた | 親の会話の SendMessage | NDF の外 | E2 |
| E4 | 続けたサブエージェントがもう一度止まり、hook が再び 1 行書いた | サブエージェントの停止 | E2 と同じ | E2・E3。E2 の行が書けなかったときも成り立つ必要がある(受け入れ条件 3) |
| E5 | supervise の work(runtime: claude)が external-ai.py で claude を起動し、起動の共有処理が帳簿へ 1 行書いた |
work のステップ | 帳簿へ書けなくても起動は落ちない | — |
| E6 | supervise がステップの終わりに、そのステップの帳簿の行をランタイムごとに集めた | ステップの終わり | 帳簿が読めなければ集めた値は空 | E5 |
| E7 | supervise がフェーズレポートを書いた | フェーズの終わり | — | E6 |
| E8 | usage-report.py が期間を受け、帳簿の行と会話の記録を読んで Claude の 3 つの値を出した |
利用者・振り返りの実行 | 会話の記録が読めなければ、その分を足さない | E1・E2・E4 |
用語
| 用語 | 意味 |
|---|---|
| 使用量の帳簿 | NDF が起動したエージェントの呼び出し 1 回ごとに、呼び出し元・モデル・使用量を 1 行で追記する jsonl(用語集のとおり) |
| 会話の記録だけの呼び出し | 帳簿に行が無く、Claude Code の会話の記録にだけ残る claude -p の会話とサブエージェント(用語集のとおり) |
| Claude の帳簿の全体 | 用語集のとおり。受け入れ条件 3・5 で一致を見る値 |
| 呼び出し元 | 帳簿の行の source と kind の組(用語集のとおり) |
| 起動の共有処理 | 起動の後に帳簿へ 1 行を書く 4 か所(用語集のとおり) |
受け入れ条件
- 1. 同じ
message.idの assistant の行が 2 行以上ある会話の記録で、帳簿へ書く使用量の 4 つの値(入力・cache read・cache write・出力)が、その id の最後の行の usage になる。同じ記録をscripts/token-usage.pyのscan_fileで読んだ合計と一致する - 2. 前提: 同じ
agent_idのサブエージェントが 2 回止まり、hook が 2 回動いた。結果: そのagent_idの帳簿の行の使用量の和が、2 回目の停止の時点の会話の記録の合計と一致する - 3. 前提: 受け入れ条件 2 で、1 回目の行が帳簿に無い(書けなかった)。結果: 2 回目の停止の後の帳簿の行の和が、会話の記録の合計と一致する
- 4.
usage-report.py --ledger claudeの期間(JST の日付・両端を含む)を指定したとき、会話の記録から足す分(会話の記録だけのclaude -pの会話とサブエージェント)には、応答の時刻が期間内の応答の使用量だけが入る。最初の応答が期間より前で、後の応答が期間内の会話も、期間内の分が入る - 5. 期間を指定しないときの Claude の帳簿の全体と会話の記録だけの値は、受け入れ条件 1 の数え方を除いて変わらない
- 6. supervise の work(
runtime: claude)で起動した claude の使用量が、フェーズレポートの「CLI(drive・work の中)」の欄に 1 回だけ載り、「LLM の使用量」の欄には載らない - 7. supervise 自身の
claude -p(runtimeを持たない work・判断・PR の呼び出し)の使用量は、「CLI(drive・work の中)」の欄に載らない(今と変わらない) - 8. 受け入れ条件 1・2・3・4・6・7 を、それぞれ再現するテストがある。各テストは今の develop(d51159151)のコードで失敗する(7 は退行を防ぐテストで、今のコードでも通る)
- 9. 確定仕様
docs/specifications/ndf-usage-ledger-by-runtime.mdのサブエージェントの取り出し・期間・ステップの集め直しの記述が、受け入れ条件 1〜7 の振る舞いと一致する
非機能の条件
| 大項目 | 条件 |
|---|---|
| 運用・保守性 | hook と起動の共有処理は、帳簿へ書けないとき・会話の記録を読めないときに落ちない(帳簿の契約の I5 を保つ) |
| 移行性 | 帳簿の行の既存の項目を消さず、意味を変えない。項目を足すときは、足す前の行を読んでも集計が落ちない |
影響
| 対象 | 影響 |
|---|---|
| 公開インタフェース | 変わらない。usage-report.py の引数と出力の形、帳簿の行の既存の項目は保つ |
| データ | 既存の帳簿の行は書き換えない。サブエージェントの行の使用量が、同じ agent_id の 2 回目以降は前回からの増分になる(行の項目を足すかは設計が決める) |
| 既存の振る舞い | サブエージェントの使用量が増える方向に変わる(最後の usage を取る)。期間を指定した集計の会話の記録の分が変わる。supervise の報告の「CLI(drive・work の中)」に claude が載る |
検証手段
| 項目 | 手段 |
|---|---|
| テスト | uv run --frozen --project . --all-extras pytest plugins/ndf/scripts/tests/test_usage_capture.py plugins/ndf/scripts/tests/test_usage_aggregate.py plugins/ndf/scripts/tests/test_usage_ledger.py -q |
| テスト(全体) | uv run --frozen --project . --all-extras pytest . -q -n 4 |
| 静的解析 | ruff format --check と ruff check(CI の lint.yml と同じ) |
| 手動確認 | 配布後に、SendMessage で続けたサブエージェントを 1 回動かし、その agent_id の帳簿の行の和と scripts/token-usage.py の値を比べる(リリース後テスト) |
前提とする取り決め
| 項目 | 参照先 / 決めたこと |
|---|---|
| プロジェクト構造 | 取り出しと書き込みは plugins/ndf/scripts/lib/usage_capture.py、集計は plugins/ndf/scripts/lib/usage_aggregate.py に置く(確定仕様の「置き場」)。scripts/token-usage.py はリポジトリ固有で配布物から参照しない |
| コーディング規約 | AGENTS.md。ライブラリは標準ライブラリとライブラリだけを import する(帳簿の契約の I1) |
| テスト戦略 | 関数の単体テストで、一時ディレクトリの会話の記録と帳簿(NDF_USAGE_DIR)を使う。.md の文言を照合するテストは書かない |
境界
| 区分 | 内容 |
|---|---|
| 常に行う | 既存の使用量のテストの実行、リンタの適用 |
| 確認してから行う | 帳簿の行に項目を足すこと(承認ゲート 1 で設計を見る) |
| 行わない | 既存の帳簿の行の書き換え、scripts/token-usage.py の変更、codex・kiro・agy・jev の取り出しの変更 |
未決
| 項目 | 誰が決めるか | 期限 |
|---|---|---|
supervise の work(runtime: claude)の行と supervise 自身の claude -p の行を見分ける方法(呼び出し元の kind を分けるか、別の項目で見分けるか) |
設計(design)。承認ゲート 1 で利用者が見る |
設計 PR |
同じ agent_id の前回の値を、帳簿の既存の行から求めるか、別の控えから求めるか |
設計(design) |
設計 PR |
関連
- サブエージェントの停止の hook が最後の応答を読む前に帳簿を書き、使用量を最後の応答 1 件分少なく数える #2576 : サブエージェントの停止の hook が最後の応答を書かれる前に会話の記録を読み、最後の応答 1 件分少なく数える(この課題の一致の範囲の外)
- 使用量の帳簿が書き手ごとに別の置き場へ書かれ、サブエージェントの行と claude -p の行を同じ集計で読めない #2575 : 使用量の帳簿が書き手ごとに 2 つの置き場へ分かれ、集計で片方が抜ける。下の承認ゲート 1 の追記が決定 6 として残すとした件と同じ現象で、どちらの課題で直すかは出し直す設計で決める
進行
モード: —
- 要求と受け入れ条件 — 2026-11-05 15:30
- 作業場所の用意
- 設計 — 2026-11-05 15:38
- 素材の収集と出典の確定
- ドキュメント再構成 — 2026-11-05 15:45
- ドキュメントレビュー
- 計画
- 実装
- 構造改善
- 実装レビュー
- 完了判定
- Pull Request
- 確定仕様化
- 後片付け
- 配布
- 体裁レビュー
- リリース後テスト
- 振り返り
- 棚卸し
設計の承認ゲート 1 での利用者の指摘(直して出し直す)
(書き手: 利用者の答えをオーガナイザーが写した、日付: 2026-11-06。出し直しの設計はこの節を満たすように直す。この節は上書きしない)
利用者の判断(2026-11-06): 直して出し直す。範囲を、実際に起きている 3 つの数え違いに絞る(同じ応答を 2 回数える → 最後の行を採る・続けたサブエージェントの二重計上 → 差だけを書く・期間外の応答を数える → 応答ごとの時刻で絞る)。
外すもの: supervise の外部 AI(runtime: claude の work)の行を supervise 自身の行と分ける部分(受け入れ条件 6・7、決定 4、処理の流れ F4)。実際の帳簿に該当する行は 0 行で、起きた不具合の裏付けが無い。決定 11(前提 5 が外れたときの姿)は未確認のまま残す扱いで足り、決定 6(帳簿の置き場が 2 つある件)も要るか見直す。
- 追記(2026-11-06、オーガナイザーの実測): 帳簿が 2 か所に分かれ、集計で片方が抜ける。決定 6(置き場が 2 つある件)は外さずに残す。
ledger_dir()はCLAUDE_PLUGIN_DATAがあればプラグインのデータ置き場、無ければ~/.local/state/ndf/usageを使う。hook(サブエージェント)は前者、Bash から動くスクリプト(supervise・cross-review・cross-refactoring・mvv-gate など)は後者に書く。このリポジトリの分で、前者/persistent/ai/.claude/plugins/data/ndf-ai-plugins/usage/claude/devbasex__ai-plugins.jsonlは 1021 行(source はすべて subagent)、後者/home/ubuntu/.local/state/ndf/usage/claude/devbasex__ai-plugins.jsonlは 1534 行(supervise 937・cross-refactoring 230・cross-review 195 ほか)。Bash から打つusage-report.pyは後者だけを読み、サブエージェントの 1021 行が丸ごと抜ける。ループの費用の予算にも、conductor がサブエージェントに任せた分が入らない(開発ループ: レーンを増やすと当たる回数・時間の上限を撤廃する(relay の 1 日の起動回数・周回の予算など) #2505 の「費用の予算が少なく数えられている疑い」の中身とみられる)。読む側が両方の置き場を合わせて読むか、書く側の置き場を 1 つにそろえる
人の判断待ち(開発ループ lroot3)
- 判定: unknown(承認ゲート 1)
- 理由: 判定を読めない
- 根拠の項目: —
- 設計 Pull Request: 使用量の帳簿: Claude の値を会話の記録の合計と一致させる(#1979) #2577
- 答え: revise
- 答え方:
python3 /persistent/ai/.claude/plugins/cache/ai-plugins/ndf/10.27.21/scripts/sprint-state.py loop answer lroot3 1979 --approved|--rejected|--revise --note <指摘ファイル>
直して出し直す指摘
利用者の判断(2026-11-06): 直して出し直す。範囲を、実際に起きている 3 つの数え違いに絞る(同じ応答を 2 回数える → 最後の行を採る・続けたサブエージェントの二重計上 → 差だけを書く・期間外の応答を数える → 応答ごとの時刻で絞る)。
外すもの: supervise の外部 AI(runtime: claude の work)の行を supervise 自身の行と分ける部分(受け入れ条件 6・7、決定 4、処理の流れ F4)。実際の帳簿に該当する行は 0 行で、起きた不具合の裏付けが無い。決定 11(前提 5 が外れたときの姿)は未確認のまま残す扱いで足り、決定 6(帳簿の置き場が 2 つある件)も要るか見直す。
Activity
Docs: #1979 の設計で、帳簿にある claude -p の残りから引くサブエージェントを期間で絞らないと決める
Docs: #1979 の設計で、会話の記録の読み込みが受ける帳簿の claude -p の会話の集合と戻り値の 3 つ組を書き、更新時刻…