From c1850e3cc7472b76c7ffd4a710f7e8d40e18fdd3 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:33:39 -0700 Subject: [PATCH] perf(claude-usage): reject non-assistant transcript lines before parsing them (#18640) Only assistant records carry usage, but the parser ran JSON.parse on every line first and checked the type after. Claude transcripts interleave user and tool-result lines that routinely embed whole files or captured command output, so the scanner built and discarded a full object graph for each of them. A substring gate on the line rejects them first: median 50.5ms to 8.4ms over 20000 realistic lines. --- ...transcript-record-parser-prefilter.test.ts | 40 +++++++++++++++++++ .../claude-usage/transcript-record-parser.ts | 20 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/claude-usage/transcript-record-parser-prefilter.test.ts diff --git a/src/main/claude-usage/transcript-record-parser-prefilter.test.ts b/src/main/claude-usage/transcript-record-parser-prefilter.test.ts new file mode 100644 index 00000000000..992b4ba8fd3 --- /dev/null +++ b/src/main/claude-usage/transcript-record-parser-prefilter.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { parseClaudeUsageRecord } from './transcript-record-parser' + +function assistantLine(overrides: Record = {}): string { + return JSON.stringify({ + type: 'assistant', + sessionId: 'session-1', + timestamp: '2026-01-01T00:00:00.000Z', + message: { usage: { input_tokens: 3, output_tokens: 5 } }, + ...overrides + }) +} + +describe('assistant-record prefilter', () => { + it('still parses an ordinary assistant record', () => { + expect(parseClaudeUsageRecord(assistantLine())?.inputTokens).toBe(3) + }) + + it('rejects a user record that never mentions assistant', () => { + const userLine = JSON.stringify({ + type: 'user', + sessionId: 'session-1', + timestamp: '2026-01-01T00:00:00.000Z', + message: { content: 'x'.repeat(200) } + }) + + expect(parseClaudeUsageRecord(userLine)).toBeNull() + }) + + it('rejects a non-assistant record that happens to contain the word assistant', () => { + const userLine = JSON.stringify({ + type: 'user', + sessionId: 'session-1', + timestamp: '2026-01-01T00:00:00.000Z', + message: { content: 'ask the assistant about this' } + }) + + expect(parseClaudeUsageRecord(userLine)).toBeNull() + }) +}) diff --git a/src/main/claude-usage/transcript-record-parser.ts b/src/main/claude-usage/transcript-record-parser.ts index 59b0e75be39..2ad51f3ee90 100644 --- a/src/main/claude-usage/transcript-record-parser.ts +++ b/src/main/claude-usage/transcript-record-parser.ts @@ -84,10 +84,30 @@ function dedupeClaudeUsageTurns( return deduped } +/** + * Necessary condition for `JSON.parse(line).type === 'assistant'`, checked before the parse. + * + * Sound for any transcript written by a standard JSON serializer: `JSON.stringify` (which writes + * these files) escapes only quotes, backslashes and control characters, never ASCII letters, so + * the decoded value can only be `assistant` if the line spells it literally. The gate over-admits + * freely — the `parsed.type` check below stays authoritative. + * + * A `\u`-escape fallback was measured and rejected: it costs a second full-line scan and made + * transcripts whose tool results contain control characters 1.43x slower overall. + */ +function mayEncodeAssistantType(line: string): boolean { + return line.includes('assistant') +} + function parseClaudeUsageSourceRecord( line: string, fallbackSessionId: string | null = null ): ClaudeUsageParsedSourceTurn | null { + // Only assistant records carry usage, but transcripts interleave user/tool-result lines that + // routinely embed whole files. Reject those before paying for a full parse. + if (!mayEncodeAssistantType(line)) { + return null + } let parsed: ClaudeUsageSourceRecord try { parsed = JSON.parse(line) as ClaudeUsageSourceRecord