From a1afa2acbb01c7d1e641c3ac18ec99194a49d3d7 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 11 Sep 2026 23:56:15 -0700 Subject: [PATCH] perf: scan chat activity lines from the end --- .../native-chat-activity-tail-line.test.ts | 87 +++++++++++++++++++ src/shared/native-chat-turn-activity.ts | 19 ++-- 2 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 src/shared/native-chat-activity-tail-line.test.ts diff --git a/src/shared/native-chat-activity-tail-line.test.ts b/src/shared/native-chat-activity-tail-line.test.ts new file mode 100644 index 00000000000..e441127a03a --- /dev/null +++ b/src/shared/native-chat-activity-tail-line.test.ts @@ -0,0 +1,87 @@ +import { expect, it, vi } from 'vitest' +import { normalizePromptField } from './agent-status-field-normalization' +import type { AgentJournalRenderItem } from './agent-session-journal-types' +import { selectStructuredAgentTurnActivity } from './native-chat-turn-activity' + +function status(text: string): AgentJournalRenderItem { + return { + itemId: 'status', + revision: 1, + sequence: 1, + observedAt: 1, + body: { kind: 'status', text } + } +} + +it('does not trim every preceding status line to select the final activity', () => { + const text = `${'Previous activity\n'.repeat(500)}Preparing the answer` + const trim = vi.spyOn(String.prototype, 'trim') + try { + expect(selectStructuredAgentTurnActivity([status(text)], 'turn')).toEqual({ + kind: 'description', + text: 'Preparing the answer' + }) + expect(trim.mock.calls.length).toBeLessThan(10) + } finally { + trim.mockRestore() + } +}) + +it('preserves the last nonempty LF-delimited line before prompt normalization', () => { + const texts = [ + '', + '\n', + '\n\n', + ' \r\n\t', + 'first\rsecond', + '\nfirst\r\nsecond\r\n', + 'first\n\u00a0\u2003\n', + 'first\n\u2028second\u2029', + 'a\nšŸ˜€', + 'a\n\ud800', + 'a\n\udc00', + `a\n${'x'.repeat(199)}šŸ˜€`, + `first\n${' '.repeat(3000)}last`, + 'first\n\0\n', + 'first\n\ufeff\n' + ] + const alphabet = [ + 'a', + ' ', + '\n', + '\r', + '\t', + '\u00a0', + '\u2003', + '\u2028', + '\ufeff', + 'šŸ˜€', + '\ud800', + '\udc00' + ] + let seed = 57 + const next = () => { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0 + return seed + } + for (let index = 0; index < 1000; index++) { + let text = '' + const length = next() % 300 + for (let offset = 0; offset < length; offset++) { + text += alphabet[next() % alphabet.length] + } + texts.push(text) + } + for (const text of texts) { + const line = text + .split('\n') + .map((part) => part.trim()) + .findLast((part) => part.length > 0) + const normalized = line ? normalizePromptField(line) : '' + const expected = normalized ? { kind: 'description', text: normalized } : null + expect(selectStructuredAgentTurnActivity([status(text)], 'turn')).toEqual(expected) + expect(selectStructuredAgentTurnActivity([], 'turn', { turnId: 'turn', text })).toEqual( + expected + ) + } +}) diff --git a/src/shared/native-chat-turn-activity.ts b/src/shared/native-chat-turn-activity.ts index fbc1756529b..cac19796334 100644 --- a/src/shared/native-chat-turn-activity.ts +++ b/src/shared/native-chat-turn-activity.ts @@ -7,12 +7,19 @@ import { describeActiveToolCall, formatActiveToolLabel } from './native-chat-too export type NativeChatTurnActivity = { kind: 'description'; text: string } function activityLine(text: string): string | null { - const lines = text - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - const latest = lines.at(-1) - return latest ? normalizePromptField(latest) || null : null + let end = text.length + while (end > 0) { + const start = text.lastIndexOf('\n', end - 1) + 1 + const latest = text.slice(start, end).trim() + if (latest) { + return normalizePromptField(latest) || null + } + if (start === 0) { + break + } + end = start - 1 + } + return null } function recentToolActivityLabels(items: readonly AgentJournalRenderItem[]): Set {