yoi/tickets/compact-retained-split-usage-records.md

5.2 KiB

Compact: retained split が pruning 後 usage record に引きずられて巨大 tail を残す

背景

2026-05-26、起動中の insomnia Pod で compact 後の継続 turn が実質失敗した。compact 自体は summary を生成して新 segment を作成しており、処理としては成功扱いだった。しかし post-compact SegmentStart.history が 1957 items / 約 3.7MB と巨大なまま残り、その後の新 segment 上の turn は assistant output / llm_usage を残さず run_completed finished になった。

関連する過去事例として docs/report/2026-05-24-compact-retained-tail-oversize.md がある。今回の調査では、前回疑っていた「retained split が request-time pruning / projection 後の usage record と persisted history の実サイズのズレに引きずられる」問題が、より具体的に再現した。

対象観測:

session: 019e5d30-f7ad-7fb1-bdec-c592e888e290
old segment: 019e5d30-f7ad-7fb1-bdec-c5a41394e6b1
new segment: 019e62d3-5cbf-7020-b696-8d661b5e1026

pre-compact history は 2192 items / 約 4.5MB。compact 後の SegmentStart.history は 1957 items / 約 3.7MB だった。

原因の見立て

llm_usage.input_total_tokens は「その LLM request に実際に投げた prompt の占有量」としては正しいが、split_for_retained はそれを persisted/raw history の prefix token 数のように扱っている。

現在の retained split は概ね以下の前提で動く。

current = tokens_at(history.len())
target = current - retained_tokens
先頭から tokens_at(idx) >= target になる最初の idx を cut にする

この探索は tokens_at(idx) が prefix 長に対して単調増加することを前提にしている。しかし実際の usage record は pruning / projection 後の request 実測であり、raw persisted history の累積 token 数ではないため、大きく上下する。

今回の直接原因になった周辺 record:

history_len=237 input_total_tokens=93358
history_len=240 input_total_tokens=20532
history_len=242 input_total_tokens=166263  # target を超えて cut に選ばれた
history_len=245 input_total_tokens=27680
history_len=247 input_total_tokens=85797

compact 直前の推定は以下だった。

current estimate: 173310 tokens
retained target: 8000 tokens
split target: 165310 tokens
initial cut: 242
balanced cut: 242
retained items: 1950

balance_to_pair_boundary による引き戻しは今回発生しておらず、問題は tool call/result boundary 保護ではない。最初から usage record の外れ値によって cut が早すぎる位置に決まっている。

要件

  • Compact の retained split が request-time pruning / projection 後の llm_usage.input_total_tokens を raw persisted history prefix として誤用しない。
    • llm_usage.input_total_tokens は request occupancy としては維持してよい。
    • retained split 用の推定は、persisted/raw history の実サイズに近い基準で行う。
    • 少なくとも usage record が非単調な場合に、古い外れ値で cut が極端に早くならない。
  • Compact 成功後に post-compact context の検証を行う。
    • retained item count
    • retained byte size / serialized history size
    • post-compact estimated tokens
    • threshold / retained budget に対する比率
  • post-compact history が明らかに過大な場合、単純な成功扱いにしない。
    • CompactFailed 相当として扱う、または just_compacted を立てず次の safety net を有効にする。
    • infinite compact loop / thrash を避けるため、失敗理由は明示する。
  • compact 後の次 turn が assistant output / llm_usage 無しに run_completed finished になる経路を潰す。
    • context length / request build / upstream error が発生した場合は run_errored または rollback として観測できる。
    • 少なくとも空 finished turn を正常完了として扱わない。
  • compact metrics を session log か metrics extension で追えるようにする。
    • source: manual / pre_run / between_requests
    • old_segment_id / new_segment_id
    • old_history_len / new_history_len
    • retained_from index
    • retained_items / retained bytes
    • estimate source / post-condition result

完了条件

  • 非単調な usage record が存在しても、retained split が巨大 tail を残さないことを確認する test がある。
  • pruning / projection 後 usage と raw persisted history size が乖離するケースの regression test がある。
  • post-compact context 検証が実装され、過大 tail の成功扱いを防ぐ test がある。
  • compact 後の空 turn が run_completed finished にならないことを確認する test がある。
  • cargo fmt --check と関連 crate の test が通る。

範囲外

  • ローカルトークナイザの導入。
  • 既存 session log の migration。
  • compact summary prompt の大幅な再設計。
  • TUI 表示だけで問題を隠す対応。

参考

  • docs/report/2026-05-24-compact-retained-tail-oversize.md
  • crates/pod/src/compact/token_counter.rs
  • crates/llm-worker/src/token_counter.rs