Skip to content

Commit 22cd50b

Browse files
Merge pull request #1349 from ai-yang/audit/perf-artifact-scan
perf: 为 tool_result 建立惰性索引,避免 /artifacts 二次方扫描
2 parents 44ff040 + e64e174 commit 22cd50b

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

src/commands/artifacts/__tests__/scanner.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,44 @@ describe('extractArtifacts', () => {
155155

156156
expect(result.map(r => r.basename)).toEqual(['b.html', 'a.html'])
157157
})
158+
159+
test('indexes multiple artifacts while preserving first-result-wins semantics', () => {
160+
const messages: Message[] = [
161+
assistantToolUse('tu1', { file_path: '/tmp/a.html' }),
162+
assistantToolUse('tu2', { file_path: '/tmp/b.html' }),
163+
userToolResult(
164+
'tu1',
165+
'https://x.test/first.html (id: first, expires: 2026-06-27T10:00:00.000Z)',
166+
),
167+
userToolResult(
168+
'tu1',
169+
'https://x.test/duplicate.html (id: duplicate, expires: 2026-06-28T10:00:00.000Z)',
170+
),
171+
userToolResult(
172+
'tu2',
173+
'https://x.test/second.html (id: second, expires: 2026-06-29T10:00:00.000Z)',
174+
),
175+
]
176+
177+
const result = extractArtifacts(messages)
178+
179+
expect(result.map(r => r.hash)).toEqual(['second', 'first'])
180+
})
181+
182+
test('still pairs a tool_result that appears before its artifact tool_use', () => {
183+
const messages: Message[] = [
184+
userToolResult(
185+
'tu1',
186+
'https://x.test/a.html (id: a, expires: 2026-06-27T10:00:00.000Z)',
187+
),
188+
userToolResult(
189+
'tu2',
190+
'https://x.test/b.html (id: b, expires: 2026-06-28T10:00:00.000Z)',
191+
),
192+
assistantToolUse('tu1', { file_path: '/tmp/a.html' }),
193+
assistantToolUse('tu2', { file_path: '/tmp/b.html' }),
194+
]
195+
196+
expect(extractArtifacts(messages).map(r => r.hash)).toEqual(['b', 'a'])
197+
})
158198
})

src/commands/artifacts/scanner.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ const URL_REGEX = /https?:\/\/[^\s)"',]+\.html\b/
1616
const ID_REGEX = /\bid:\s*([A-Za-z0-9_-]+)/
1717
const EXPIRES_REGEX = /\bexpires:\s*([0-9T:.Z+-]+)/
1818

19+
type ArtifactToolResult = {
20+
content: unknown
21+
is_error?: boolean
22+
}
23+
1924
export function extractArtifacts(messages: Message[]): ArtifactInfo[] {
2025
const results: ArtifactInfo[] = []
26+
let artifactUseCount = 0
27+
let indexedResults: Map<string, ArtifactToolResult> | null = null
2128

2229
for (const message of messages) {
2330
if (message.type !== 'assistant') continue
@@ -35,7 +42,19 @@ export function extractArtifacts(messages: Message[]): ArtifactInfo[] {
3542
const input = b.input as { file_path?: string } | undefined
3643
const filePath = input?.file_path ?? '<unknown>'
3744

38-
const resultBlock = findToolResult(messages, toolUseId)
45+
artifactUseCount++
46+
if (artifactUseCount === 2) {
47+
// One direct lookup is already linear and avoids allocating an index
48+
// for the common single-artifact case. Starting with the second use,
49+
// index tool results once instead of rescanning the transcript for
50+
// every artifact (which becomes quadratic in artifact-heavy sessions).
51+
indexedResults = indexToolResults(messages)
52+
}
53+
54+
const resultBlock = indexedResults
55+
? (indexedResults.get(toolUseId) ?? null)
56+
: findToolResult(messages, toolUseId)
57+
3958
if (!resultBlock) continue
4059

4160
const rawContent =
@@ -78,7 +97,7 @@ export function extractArtifacts(messages: Message[]): ArtifactInfo[] {
7897
function findToolResult(
7998
messages: Message[],
8099
toolUseId: string,
81-
): { content: unknown; is_error?: boolean } | null {
100+
): ArtifactToolResult | null {
82101
for (const message of messages) {
83102
if (message.type !== 'user') continue
84103
const content = message.message?.content
@@ -95,3 +114,32 @@ function findToolResult(
95114
}
96115
return null
97116
}
117+
118+
function indexToolResults(
119+
messages: Message[],
120+
): Map<string, ArtifactToolResult> {
121+
const results = new Map<string, ArtifactToolResult>()
122+
123+
for (const message of messages) {
124+
if (message.type !== 'user') continue
125+
const content = message.message?.content
126+
if (!Array.isArray(content)) continue
127+
128+
for (const block of content) {
129+
if (typeof block !== 'object' || block === null) continue
130+
if (!('type' in block)) continue
131+
const b = block as unknown as Record<string, unknown>
132+
if (b.type !== 'tool_result') continue
133+
const toolUseId = b.tool_use_id as string
134+
if (results.has(toolUseId)) continue
135+
136+
// Match findToolResult's first-result-wins behavior for duplicate IDs.
137+
results.set(toolUseId, {
138+
content: b.content,
139+
is_error: b.is_error as boolean | undefined,
140+
})
141+
}
142+
}
143+
144+
return results
145+
}

0 commit comments

Comments
 (0)