Repository navigation
Ledger drops content blocks: keep-last dedup treats split response lines as snapshots #88
Description
⚠️ Выводы не проверены человеком — доверия к ним нет. Ишью заведено по аудиту, который сделал Claude Code. Автор репозитория просил считать эти выводы непроверенными: прежде чем чинить, дважды воспроизведите каждый пункт по шагам ниже — на своих сессиях, а не по цифрам отсюда — и отметьте результат в разделе «Перепроверка». Claude перепроверил их один раз (код + сырой JSONL одной сессии Claude Code 2.1.296); это не засчитывается.
Суть (гипотеза)
deduplicateByRequestId (src/main/utils/jsonl.ts:256) оставляет последнюю строку на requestId, исходя из того, что строки одного запроса — это снимки стрима и последняя содержит всё (так написано в комментарии к функции и к mergeAssistantFragments). В проверенной сессии Claude Code пишет один блок ответа на строку (thinking, text, каждый tool_use — отдельной строкой) с одинаковым usage. Значит, строки не снимки, а части ответа, и keep-last выбрасывает все блоки, кроме последнего.
Для подсчёта токенов это безвредно (usage во всех строках одинаков). Но CLI берёт из оставшейся строки и содержимое:
buildLedger→RoundRow.tools(src/cli/analyzeSession.ts:410): колонка tools в BY TURN / ROUNDS и текстcontext_spike;detectCycleFindings(:805) →cycle_motif/probe_no_progressвидят только последний вызов раунда;thinkingTokensOf— thinking-блок всегда в первой строке ответа, поэтому выпадает всегда (связано с thinking_heavy measures returned thinking text, not billed thinking tokens #83).
Приложение не затронуто: calculateMetrics использует дедупликацию только для usage.
Наблюдение (одна сессия, Claude Code 2.1.296)
| значение | |
|---|---|
| запросов в главной цепочке | 72 |
| из них записаны несколькими строками | 55 |
| блоков на строку | всегда 1 (160 строк) |
| запросов с разным usage между строками | 0 |
| tool_use блоков в сыром JSONL | 96 |
вызовов в ledger.rounds[].tools |
64 (потеряна ~⅓) |
| thinking-блоков | 48, ни один не попал в оставленные строки |
Пример: запрос с 6 параллельными sub_issue_write записан 7 строками (text + 6 tool_use); в ledger раунд показывает 1 вызов.
Как воспроизвести
# python3 repro.py ~/.claude/projects/<dir>/<session>.jsonl
import json, sys, collections
by = collections.defaultdict(list)
for line in open(sys.argv[1]):
e = json.loads(line)
if e.get("type") == "assistant" and e.get("requestId") and not e.get("isSidechain"):
by[e["requestId"]].append(e)
blocks = lambda e: e["message"].get("content", [])
print("requests", len(by), "multi-line", sum(len(v) > 1 for v in by.values()))
print("tool_use all lines", sum(b.get("type") == "tool_use" for v in by.values() for e in v for b in blocks(e)))
print("tool_use last line only", sum(b.get("type") == "tool_use" for v in by.values() for b in blocks(v[-1])))
print("usage identical across lines", sum(len({json.dumps(e["message"].get("usage"), sort_keys=True) for e in v}) == 1 for v in by.values()))Сравнить «last line only» с pnpm analyze:session <file> --json | jq '[.ledger.rounds[].tools | length] | add'.
Что сделать
- Для строк с
requestIdсклеивать содержимое (какmergeAssistantFragmentsуже делает для строк безrequestId), usage брать из последней строки. - Учесть старые версии Claude Code, где строки могли быть настоящими снимками с накапливающимся содержимым: дедуплицировать блоки по
tool_use.id/ идентичности, чтобы не удвоить. - Обновить комментарии к
deduplicateByRequestIdиmergeAssistantFragments.
Перепроверка (каждый пункт — дважды, независимо)
- Claude Code пишет один блок на строку с одинаковым usage — проверка №1
- то же — проверка №2 (другая сессия / другая версия Claude Code)
-
ledger.rounds[].toolsтеряет вызовы по сравнению с сырым JSONL — проверка №1 - то же — проверка №2
- Найдены (или исключены) старые сессии, где строки — накапливающиеся снимки
Acceptance
- Число вызовов в ledger = числу
tool_useв сыром JSONL главной цепочки (тест на фикстуре из нескольких строк на запрос) - Thinking-блоки доходят до
thinkingTokensOf(или он переведён на usage, см. thinking_heavy measures returned thinking text, not billed thinking tokens #83) - Тест на старый формат снимков не удваивает блоки
-
pnpm typecheck && pnpm testзелёные
Activity
axisrow commentedon Oct 11, 2026
Дополнение по второй сессии: тот же Claude Code 2.1.296, другая сессия (аудит 2026-10-11). Это проверка Claude — она не засчитывается.
tool_useв сыром JSONLledger.rounds[].toolse4e9d97), то есть потеря есть как минимум с feat(cli): wait_loop finding, API-time metrics, longest-turn inventory #23.tool_useвсегда стоит в последней строке запроса. Поэтому ложных «тихих тиков» нет: критерий требует отсутствия вызовов, и wait-loop не задет.Уточнение к «Приложение не затронуто»
Для итогов токенов это так. Но в рендерере есть то же правило «последняя строка запроса»:
classifyRoundsберёт вызовы раунда только из последней строки. Если повтор стоит не последним среди параллельных вызовов запроса, категория Loop в панели его не видит:Как проверить (один раз, ~1 мин, из корня репозитория)
Правильно: Loop есть в обоих вариантах. Сейчас: во втором варианте
НЕТ.Склейку содержимого из п. 1 «Что сделать» стоит применить и к
classifyRounds, иначе в панели останется та же потеря. Сценарий «многострочный запрос с параллельнымиtool_use» есть в таблице теста паритета #103 (эпик #94).Generated by Claude Code
fix(lint): sonarjs/function-return-type on mergeResponseContent (#88)
fix(ledger): keep all content blocks of requestId split responses (#88)…
test(parity): one edge-case fixture through every consumer (#103) (#132)