Part of #94.
⚠️ Выводы не проверены человеком. Ишью заведено по аудиту, который сделал Claude Code. Перед исправлением достаточно один раз выполнить блок «Как проверить» и сравнить вывод с ожидаемым.
Что не так
Один пользовательский ход, посреди которого случился компакт:
|
analyze:session |
панель |
analyze:sessions |
| wait-loop (3 тихих раунда + компакт + 3 тихих раунда, порог — 5 за ход) |
найден («6 quiet rounds») |
нет |
— |
| самый длинный ход (6 раундов по 4 мин) |
21 мин |
— |
8 мин |
После #69 analyze:session не начинает новый ход на компакте. Инвентарь считает сводку компакта границей хода, а панель работает по AI-группам, которые компакт разрезает.
До #69 обе CLI и панель совпадали — проверено на коммитах мержа #23, #26, #27, #33, #42, #44, #46, #68. Обещания, которые это нарушает:
Как проверить (один раз, ~1 мин)
Из корня репозитория:
cat > /tmp/repro-compact.mts <<'EOF'
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const R = process.cwd(); // запускать из корня репозитория
const load = (p: string) => import(`${R}/${p}`);
let t = Date.parse('2026-10-01T07:00:00Z'), n = 0;
const at = (ms = 20_000) => new Date((t += ms)).toISOString();
const base = (x = {}) => ({ uuid: `u${++n}`, parentUuid: null, sessionId: 's', cwd: '/p', isSidechain: false, ...x });
const user = (content: unknown, x = {}, gap?: number) => ({ ...base(x), type: 'user', timestamp: at(gap), message: { role: 'user', content } });
const ai = (block: object, ctx: number, out: number, ids: { rid?: string; mid: string }, x = {}, gap?: number) => ({ ...base(x), type: 'assistant', timestamp: at(gap), ...(ids.rid ? { requestId: ids.rid } : {}), message: { id: ids.mid, role: 'assistant', model: 'claude-x', content: [block], usage: { input_tokens: 0, cache_read_input_tokens: ctx, cache_creation_input_tokens: 0, output_tokens: out } } });
const result = (id: string, gap?: number) => ({ ...base(), type: 'user', timestamp: at(gap), message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: 'ok' }] } });
const text = (s: string) => ({ type: 'text', text: s });
const tool = (id: string, name: string, input: object) => ({ type: 'tool_use', id, name, input });
const write = (lines: object[]) => { const f = join(mkdtempSync(join(tmpdir(), 'repro-')), 's.jsonl'); writeFileSync(f, lines.map((l) => JSON.stringify(l)).join('\n') + '\n'); return f; };
// один пользовательский ход, посреди него — компакт
const jsonl = await load('src/main/utils/jsonl.ts');
const { ChunkBuilder } = await load('src/main/services/analysis/ChunkBuilder.ts');
const { transformChunksToConversation } = await load('src/renderer/utils/groupTransformer.ts');
const { processSessionContextWithPhases } = await load('src/renderer/utils/contextTracker.ts');
const cli = await load('src/cli/analyzeSession.ts');
const inv = await load('src/cli/sessionInventory.ts');
const compact = (gap?: number) => user('This session is being continued from a previous conversation...', { isCompactSummary: true }, gap);
// A: 3 тихих тика + компакт + 3 тихих тика (порог wait-loop = 5 в одном ходу)
const a: object[] = [user('watch the deploy')];
for (let i = 0; i < 3; i++) a.push(ai(text('still waiting'), 80_000, 20, { rid: `a${i}`, mid: `ma${i}` }));
a.push(compact());
for (let i = 0; i < 3; i++) a.push(ai(text('still waiting'), 80_000, 20, { rid: `b${i}`, mid: `mb${i}` }));
const ma = await jsonl.parseJsonlFile(write(a));
const conv = transformChunksToConversation(new ChunkBuilder().buildChunks(ma, []), [], false);
let panelWait = false;
for (const s of processSessionContextWithPhases(conv.items, '/p', {}, new Map(), new Map()).statsMap.values())
if (s.newInjections.some((i: { category: string }) => i.category === 'wait-loop')) panelWait = true;
const cliWait = cli.computeFindings(ma, cli.buildLedger(ma)).some((x: { type: string }) => x.type === 'wait_loop');
// B: 3 раунда по 4 мин + компакт + 3 раунда по 4 мин
const b: object[] = [user('long task')];
for (let i = 0; i < 6; i++) {
if (i === 3) b.push(compact(60_000));
b.push(ai(tool(`c${i}`, 'Bash', { command: `step ${i}` }), 100_000, 50, { rid: `c${i}`, mid: `mc${i}` }, {}, 240_000), result(`c${i}`, 1_000));
}
const fb = write(b);
const ledger = cli.buildLedger(await jsonl.parseJsonlFile(fb));
console.table({
'A: wait-loop найден': { 'analyze:session': cliWait, 'панель': panelWait, 'analyze:sessions': '—' },
'B: самый длинный ход, мин': { 'analyze:session': ledger.totals.longestTurn?.activeMinutes, 'панель': '—', 'analyze:sessions': +((await inv.scanSessionFile(fb)).longestTurnMs / 60000).toFixed(1) },
});
console.log(cliWait !== panelWait ? 'ВОСПРОИЗВЕДЕНО: компакт режет ход в панели и инвентаре, но не в analyze:session' : 'OK: компакт обрабатывается одинаково');
EOF
pnpm exec tsx /tmp/repro-compact.mts
Правильно: в строке A оба значения одинаковы, в строке B — одинаковые минуты. Сейчас: A — true / false, B — 21 / 8, строка ВОСПРОИЗВЕДЕНО.
Где в коде (гипотеза)
Что сделать
- Решить в каноне (
scripts/turn-accounting.mjs), режет ли компакт ход для времени и потерь.
- Применить это решение во всех трёх местах: ledger, инвентарь и порог wait-loop в панели (считать тики по всему ходу или по группе).
Связано: #91 — вторая причина расхождения активного времени между двумя CLI.
Acceptance
Part of #94.
Что не так
Один пользовательский ход, посреди которого случился компакт:
analyze:sessionanalyze:sessionsПосле #69
analyze:sessionне начинает новый ход на компакте. Инвентарь считает сводку компакта границей хода, а панель работает по AI-группам, которые компакт разрезает.До #69 обе CLI и панель совпадали — проверено на коммитах мержа #23, #26, #27, #33, #42, #44, #46, #68. Обещания, которые это нарушает:
Как проверить (один раз, ~1 мин)
Из корня репозитория:
Правильно: в строке A оба значения одинаковы, в строке B — одинаковые минуты. Сейчас: A —
true/false, B — 21 / 8, строкаВОСПРОИЗВЕДЕНО.Где в коде (гипотеза)
analyze:session:src/cli/analyzeSession.ts:432— сводка компакта пропускается, ход продолжается (с fix(turn): compaction is not a turn — one numbering everywhere #69).src/cli/sessionInventory.ts:202— вisParsedUserChunkMessageне передаётсяisCompactSummary, сводка считается началом хода.ChunkBuilderна компакте начинает новую группу.Что сделать
scripts/turn-accounting.mjs), режет ли компакт ход для времени и потерь.Связано: #91 — вторая причина расхождения активного времени между двумя CLI.
Acceptance