Part of #94.
⚠️ Выводы не проверены человеком. Ишью заведено по аудиту, который сделал Claude Code. Перед исправлением достаточно один раз выполнить блок «Как проверить» и сравнить вывод с ожидаемым.
Что не так
AI-группы до первого user-сообщения (или релея) нумеруются запасным способом — по порядковому номеру AI-группы (aiTurnIndex), а счётчик ходов в сайдбаре их не считает:
|
сайдбар: ходов |
чипы в чате |
| начинается со сводки компакта (продолженная сессия) |
1 |
Turn 1, Turn 1 |
| без user-ввода (ответ → компакт → ответ) |
0 |
Turn 1, Turn 2 |
В первом случае две разные группы получают «Turn 1». Во втором сайдбар прячет счётчик (при 0 он не показывается), а чат нумерует ходы. #55, закрытый в #58, починил сессии из одних релеев; случай «вообще без user-сообщений» из того же #55 остался.
Как проверить (один раз, ~1 мин)
Из корня репозитория:
cat > /tmp/repro-nouser.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 text = (s: string) => ({ type: 'text', text: s });
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 compact = () => user('This session is being continued from a previous conversation...', { isCompactSummary: true });
const cases = {
// продолженная сессия: сводка → ответ → вопрос → ответ
'начинается со сводки компакта': [compact(), ai(text('resuming'), 100_000, 50, { rid: 'q1', mid: 'm1' }), user('go on'), ai(text('ok'), 100_000, 50, { rid: 'q2', mid: 'm2' })],
// ни одного user-ввода: ответ → компакт → ответ
'без user-ввода': [ai(text('a1'), 100_000, 50, { rid: 'q3', mid: 'm3' }), compact(), ai(text('a2'), 100_000, 50, { rid: 'q4', mid: 'm4' })],
};
const rows: Record<string, object> = {};
for (const [name, lines] of Object.entries(cases)) {
const f = write(lines);
const msgs = await jsonl.parseJsonlFile(f);
const chips = transformChunksToConversation(new ChunkBuilder().buildChunks(msgs, []), [], false).items
.filter((i: { type: string }) => i.type === 'ai').map((i: { group: { turnIndex: number } }) => `Turn ${i.group.turnIndex + 1}`);
rows[name] = { 'сайдбар: ходов': (await jsonl.analyzeSessionFileMetadata(f)).turnCount, 'чипы в чате': chips.join(', ') };
}
console.table(rows);
const bad = Object.entries(rows).filter(([, r]) => { const c = String((r as Record<string, unknown>)['чипы в чате']).split(', '); return new Set(c).size < c.length || c.length > Number((r as Record<string, unknown>)['сайдбар: ходов']); });
console.log(bad.length ? `ВОСПРОИЗВЕДЕНО: чипы расходятся со счётчиком сайдбара (${bad.map(([k]) => k).join('; ')})` : 'OK');
EOF
pnpm exec tsx /tmp/repro-nouser.mts
Правильно: номера чипов не повторяются, и их не больше, чем ходов в сайдбаре. Сейчас: Turn 1, Turn 1 при одном ходе и Turn 1, Turn 2 при нуле ходов, строка ВОСПРОИЗВЕДЕНО.
Где в коде (гипотеза)
Что сделать
Одно правило для групп до первого ввода, общее для счётчика и чипов. Например: такие группы без номера (или «Turn 0»), либо сайдбар тоже их считает.
Acceptance
Part of #94.
Что не так
AI-группы до первого user-сообщения (или релея) нумеруются запасным способом — по порядковому номеру AI-группы (
aiTurnIndex), а счётчик ходов в сайдбаре их не считает:В первом случае две разные группы получают «Turn 1». Во втором сайдбар прячет счётчик (при 0 он не показывается), а чат нумерует ходы. #55, закрытый в #58, починил сессии из одних релеев; случай «вообще без user-сообщений» из того же #55 остался.
Как проверить (один раз, ~1 мин)
Из корня репозитория:
Правильно: номера чипов не повторяются, и их не больше, чем ходов в сайдбаре. Сейчас:
Turn 1, Turn 1при одном ходе иTurn 1, Turn 2при нуле ходов, строкаВОСПРОИЗВЕДЕНО.Где в коде (гипотеза)
aiTurnIndex—turnInputs > 0 ? turnInputs - 1 : aiCount: до первого ввода номер берётся из порядка AI-групп.src/main/utils/jsonl.ts:536–545— считаются только user- и relay-строки.Что сделать
Одно правило для групп до первого ввода, общее для счётчика и чипов. Например: такие группы без номера (или «Turn 0»), либо сайдбар тоже их считает.
Acceptance