fix(observer): uuid-dedup in parseLines (C-1 root fix for quirk #101)

Bug: Claude Code's transcript JSONL file accumulates duplicated context-
rebuild snapshots — the same entry re-printed with the SAME `uuid`. Without
dedup, session_turn / task_size / events double-count, and session_turn
becomes non-monotonic across episodes parsed at different file-growth
states. Live evidence: episodes-2026-05.jsonl lines 14/15/16 of the same
session showed session_turn 139 → 140 → 91 (backwards in time). Probe
on transcript 553717ec: 22400 entries, only 6074 unique uuid (68% dup
rate); real user prompts 264 total vs 92 unique-uuid.

Fix: parseLines now tracks a `seenUuid` Set and skips entries whose uuid
has already been encountered (keep-first). Entries without `uuid`
(synthetic test fixtures) pass through unchanged. All downstream functions
(findTurnStart, extractEnvironment, extractTaskSize, etc.) operate on the
deduped entries array, so the fix is single-point and total.

Tests: new `parseTranscript — uuid-dedup` describe block covers
(1) duplicated-uuid prompts collapse → session_turn counts once,
(2) distinct-uuid entries preserved (no over-dedup),
(3) no-uuid entries pass through (synthetic-fixture safety),
(4) duplicated-uuid assistant turns → tool_calls / files_touched counted once.
110/110 parser tests green (was 106).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-20 11:00:50 +03:00
parent 1ba8b6e590
commit 94f831f7d1
2 changed files with 117 additions and 1 deletions
+102
View File
@@ -670,6 +670,108 @@ describe('parseTranscript — AskUserQuestion in-turn choice', () => {
});
});
describe('parseTranscript — uuid-dedup for duplicated snapshots (quirk #101)', () => {
// Claude Code's transcript file accumulates duplicated context-rebuild
// snapshots; the same entry is re-printed with the SAME `uuid`. Without
// dedup, session_turn / task_size / events double-count and become
// non-monotonic across episodes parsed at different file-growth states.
// Root fix: dedup by uuid in parseLines.
it('collapses duplicated-uuid user prompts (session_turn counts once)', () => {
const dup = 'aaaa-bbbb-cccc';
const lines = [
JSON.stringify({
uuid: dup,
type: 'user',
message: { role: 'user', content: 'hi' },
timestamp: '2026-05-19T10:00:00Z',
sessionId: 's1',
}),
JSON.stringify({
uuid: dup,
type: 'user',
message: { role: 'user', content: 'hi' },
timestamp: '2026-05-19T10:00:00Z',
sessionId: 's1',
}),
].join('\n');
const ep = parseTranscript(lines, 'fallback');
expect(ep.environment.session_turn).toBe(1);
});
it('preserves distinct-uuid entries (no over-dedup)', () => {
const lines = [
JSON.stringify({
uuid: 'one',
type: 'user',
message: { role: 'user', content: 'first' },
timestamp: '2026-05-19T09:00:00Z',
sessionId: 's1',
}),
JSON.stringify({
uuid: 'two',
type: 'user',
message: { role: 'user', content: 'second' },
timestamp: '2026-05-19T10:00:00Z',
sessionId: 's1',
}),
].join('\n');
const ep = parseTranscript(lines, 'fallback');
expect(ep.environment.session_turn).toBe(2);
});
it('entries without uuid pass through unchanged (synthetic fixtures unaffected)', () => {
// Existing tests build entries without `uuid` — must still work.
const t = jsonl([
userPrompt('one', '2026-05-19T09:00:00Z'),
userPrompt('two', '2026-05-19T10:00:00Z'),
]);
expect(parseTranscript(t).environment.session_turn).toBe(2);
});
it('collapses duplicated-uuid assistant turns (task_size counted once)', () => {
const lines = [
JSON.stringify({
uuid: 'u-prompt',
type: 'user',
message: { role: 'user', content: 'go' },
timestamp: '2026-05-19T10:00:00Z',
sessionId: 's1',
}),
JSON.stringify({
uuid: 'u-asst',
type: 'assistant',
message: {
role: 'assistant',
content: [
{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/x.js' } },
{ type: 'tool_use', id: 't2', name: 'Edit', input: { file_path: '/x.js' } },
],
},
timestamp: '2026-05-19T10:01:00Z',
sessionId: 's1',
}),
// EXACT duplicate of the assistant entry — snapshot rebuild artifact
JSON.stringify({
uuid: 'u-asst',
type: 'assistant',
message: {
role: 'assistant',
content: [
{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/x.js' } },
{ type: 'tool_use', id: 't2', name: 'Edit', input: { file_path: '/x.js' } },
],
},
timestamp: '2026-05-19T10:01:00Z',
sessionId: 's1',
}),
].join('\n');
const ep = parseTranscript(lines, 'fallback');
expect(ep.task_size.tool_calls).toBe(2);
expect(ep.task_size.files_touched).toBe(1);
});
});
describe('extractEnvironment — parallel_session narrowed to tool_result evidence', () => {
// Collision detection scans ONLY tool_result content (real command output).
// Prose mentions in user prompts / assistant text — including analysis text