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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user