From e64e174013282820f24d23a24f169fa3d4de726d Mon Sep 17 00:00:00 2001 From: Rui <1685901819@qq.com> Date: Tue, 11 Aug 2026 06:19:05 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20artifacts=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../artifacts/__tests__/scanner.test.ts | 40 ++++++++++++++ src/commands/artifacts/scanner.ts | 52 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/commands/artifacts/__tests__/scanner.test.ts b/src/commands/artifacts/__tests__/scanner.test.ts index 8d62724836..ee78328c86 100644 --- a/src/commands/artifacts/__tests__/scanner.test.ts +++ b/src/commands/artifacts/__tests__/scanner.test.ts @@ -155,4 +155,44 @@ describe('extractArtifacts', () => { expect(result.map(r => r.basename)).toEqual(['b.html', 'a.html']) }) + + test('indexes multiple artifacts while preserving first-result-wins semantics', () => { + const messages: Message[] = [ + assistantToolUse('tu1', { file_path: '/tmp/a.html' }), + assistantToolUse('tu2', { file_path: '/tmp/b.html' }), + userToolResult( + 'tu1', + 'https://x.test/first.html (id: first, expires: 2026-06-27T10:00:00.000Z)', + ), + userToolResult( + 'tu1', + 'https://x.test/duplicate.html (id: duplicate, expires: 2026-06-28T10:00:00.000Z)', + ), + userToolResult( + 'tu2', + 'https://x.test/second.html (id: second, expires: 2026-06-29T10:00:00.000Z)', + ), + ] + + const result = extractArtifacts(messages) + + expect(result.map(r => r.hash)).toEqual(['second', 'first']) + }) + + test('still pairs a tool_result that appears before its artifact tool_use', () => { + const messages: Message[] = [ + userToolResult( + 'tu1', + 'https://x.test/a.html (id: a, expires: 2026-06-27T10:00:00.000Z)', + ), + userToolResult( + 'tu2', + 'https://x.test/b.html (id: b, expires: 2026-06-28T10:00:00.000Z)', + ), + assistantToolUse('tu1', { file_path: '/tmp/a.html' }), + assistantToolUse('tu2', { file_path: '/tmp/b.html' }), + ] + + expect(extractArtifacts(messages).map(r => r.hash)).toEqual(['b', 'a']) + }) }) diff --git a/src/commands/artifacts/scanner.ts b/src/commands/artifacts/scanner.ts index e6ea68d328..70ef5c60e8 100644 --- a/src/commands/artifacts/scanner.ts +++ b/src/commands/artifacts/scanner.ts @@ -16,8 +16,15 @@ const URL_REGEX = /https?:\/\/[^\s)"',]+\.html\b/ const ID_REGEX = /\bid:\s*([A-Za-z0-9_-]+)/ const EXPIRES_REGEX = /\bexpires:\s*([0-9T:.Z+-]+)/ +type ArtifactToolResult = { + content: unknown + is_error?: boolean +} + export function extractArtifacts(messages: Message[]): ArtifactInfo[] { const results: ArtifactInfo[] = [] + let artifactUseCount = 0 + let indexedResults: Map | null = null for (const message of messages) { if (message.type !== 'assistant') continue @@ -35,7 +42,19 @@ export function extractArtifacts(messages: Message[]): ArtifactInfo[] { const input = b.input as { file_path?: string } | undefined const filePath = input?.file_path ?? '' - const resultBlock = findToolResult(messages, toolUseId) + artifactUseCount++ + if (artifactUseCount === 2) { + // One direct lookup is already linear and avoids allocating an index + // for the common single-artifact case. Starting with the second use, + // index tool results once instead of rescanning the transcript for + // every artifact (which becomes quadratic in artifact-heavy sessions). + indexedResults = indexToolResults(messages) + } + + const resultBlock = indexedResults + ? (indexedResults.get(toolUseId) ?? null) + : findToolResult(messages, toolUseId) + if (!resultBlock) continue const rawContent = @@ -78,7 +97,7 @@ export function extractArtifacts(messages: Message[]): ArtifactInfo[] { function findToolResult( messages: Message[], toolUseId: string, -): { content: unknown; is_error?: boolean } | null { +): ArtifactToolResult | null { for (const message of messages) { if (message.type !== 'user') continue const content = message.message?.content @@ -95,3 +114,32 @@ function findToolResult( } return null } + +function indexToolResults( + messages: Message[], +): Map { + const results = new Map() + + for (const message of messages) { + if (message.type !== 'user') continue + const content = message.message?.content + if (!Array.isArray(content)) continue + + for (const block of content) { + if (typeof block !== 'object' || block === null) continue + if (!('type' in block)) continue + const b = block as unknown as Record + if (b.type !== 'tool_result') continue + const toolUseId = b.tool_use_id as string + if (results.has(toolUseId)) continue + + // Match findToolResult's first-result-wins behavior for duplicate IDs. + results.set(toolUseId, { + content: b.content, + is_error: b.is_error as boolean | undefined, + }) + } + } + + return results +}