claudeReplayMapper.ts ×22

Frontier kind: Code frontier

unlabeled · c_655af8430601

238 tests · 19663 LOC · 73 files · introduces 0 tests · 195 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
22 ranges195 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1575 ranges19663 lines · 73 files · Browse complete extent
All tests (intent)
238 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 195 introduced LOC across 22 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts 195 introduced LOC · 22 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- claudeReplayMapper.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { SessionMessage } from '@anthropic-ai/claude-agent-sdk';
7 > import type { URI } from '../../../../base/common/uri.js';
8 > import type { ILogService } from '../../../log/common/log.js';
9 > import {
10 > ResponsePartKind,
11 > ToolCallCancellationReason,
12 > ToolCallConfirmationReason,
13 > ToolCallStatus,
14 > ToolResultContentType,
15 > TurnState,
16 > MessageKind,
17 > type ResponsePart,
18 > type ToolCallCancelledState,
19 > type ToolCallCompletedState,
20 > type ToolCallResponsePart,
21 > type ToolResultContent,
22 > type Turn,
23 > } from '../../common/state/protocol/state.js';
24 > import { buildSubagentSessionUri } from '../../common/state/sessionState.js';
25 > import { readToolCallMeta } from '../../common/meta/agentToolCallMeta.js';
26 > import { buildClaudeToolMeta, getClaudeInvocationMessage, getClaudePastTenseMessage, getClaudeToolDisplayName, getClaudeToolInputString } from './claudeToolDisplay.js';
27 > import { stripClientToolNamePrefix } from './clientTools/claudeClientToolMcpServer.js';
28 >
29 > /**
30 > * Phase 13 — replay mapper. Reduces a flat `SessionMessage[]` (the SDK's
31 > * on-disk JSONL transcript) into the protocol's `Turn[]` shape per
32 > * [CONTEXT.md M7](./CONTEXT.md). Pure function; no I/O, no DI.
33 > *
34 > * Distinct from the live mapper (`mapSDKMessageToAgentSignals`) because:
35 > * - input shape differs (`SessionMessage` envelope vs `SDKMessage` union),
36 > * - output shape differs (`Turn[]` vs `AgentSignal[]`),
37 > * - replay has no `'result'` envelope (SDK doesn't persist it) and no
38 > * `'stream_event'` lifecycle (terminal states only).
39 > *
40 > * Shared invariant with the live mapper: the `Map<tool_use_id, turnId>`
41 > * attribution rule from M7 — `tool_result` legitimately lands in a later
42 > * `'user'` envelope and must resolve back to the announcing `tool_use`'s
43 > * turn. This mapper builds an equivalent local map during its single pass.
44 > */
45 > export function mapSessionMessagesToTurns(
46 messages: readonly SessionMessage[],
47 session: URI,
58 return builder.finish();
59 }
61 > /**
62 > * Phase 6.5 — translate a protocol `turnId` (the last KEPT turn N) into the
63 > * SDK envelope `uuid` that `forkSession({ upToMessageId })` accepts
64 > * (INCLUSIVE). Returns the `uuid` of turn N's last `'assistant'` envelope,
65 > * or `turnId` itself when turn N has no assistant reply (still a valid
66 > * inclusive anchor), or `undefined` when `turnId` is not in the transcript.
67 > * Reuses {@link parseSessionMessage} so the turn-boundary rule matches
68 > * {@link ReplayBuilder}; always returns an envelope `uuid`, never a `msg_…` id.
69 > */
70 > export function resolveForkAnchorUuid(messages: readonly SessionMessage[], turnId: string): string | undefined {
71 let seenTarget = false;
72 let lastAssistantUuid: string | undefined;
94 return lastAssistantUuid ?? turnId;
95 }
97 > // #region Parsed message union — narrow-at-the-seam adapter
98 >
99 > interface UserTextBlock { readonly type: 'text'; readonly text: string }
100 > interface UserToolResultBlock { readonly type: 'tool_result'; readonly tool_use_id: string; readonly content: unknown; readonly is_error: boolean }
101 > interface AssistantBlock { readonly type: string; readonly text?: string; readonly thinking?: string; readonly id?: string; readonly name?: string; readonly input?: unknown }
102 >
103 > /**
104 > * Discriminated union of replay-relevant message shapes. Everything that
105 > * the mapper actually cares about is one of these; everything else (hooks,
106 > * CLI-echo entries, unallowed system subtypes, malformed envelopes) returns
107 > * `undefined` from {@link parseSessionMessage}.
108 > *
109 > * The split keeps SDK shape detection (this seam) separate from the
110 > * stateful reduction (the {@link ReplayBuilder}) — see CONTEXT M7.
111 > */
112 > type ParsedSessionMessage =
113 > | { readonly kind: 'user-text'; readonly uuid: string; readonly text: string; readonly timestamp?: string }
114 > | { readonly kind: 'user-tool-results'; readonly uuid: string; readonly results: readonly UserToolResultBlock[]; readonly timestamp?: string }
115 > | { readonly kind: 'assistant'; readonly uuid: string; readonly blocks: readonly AssistantBlock[]; readonly isInner: boolean; readonly timestamp?: string }
116 > | { readonly kind: 'system-notification'; readonly uuid: string; readonly subtype: string; readonly text: string; readonly timestamp?: string };
117 >
118 function parseSessionMessage(msg: SessionMessage): ParsedSessionMessage | undefined {
119 const timestamp = readTimestamp(msg);
125 }
126 }
128 function readTimestamp(msg: SessionMessage & { readonly timestamp?: unknown }): string | undefined {
129 if (typeof msg.timestamp !== 'string') {
133 return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
134 }
136 function parseUserMessage(msg: SessionMessage, timestamp: string | undefined): ParsedSessionMessage | undefined {
137 const content = readUserContent(msg.message);
154 return { kind: 'user-text', uuid: msg.uuid, text: textBlocks.map(b => b.text).join('\n'), timestamp };
155 }
157 function parseAssistantMessage(msg: SessionMessage, timestamp: string | undefined): ParsedSessionMessage | undefined {
158 const blocks = readAssistantBlocks(msg.message);
166 return { kind: 'assistant', uuid: msg.uuid, blocks, isInner: msg.parent_tool_use_id !== null, timestamp };
167 }
169 function parseSystemMessage(msg: SessionMessage, timestamp: string | undefined): ParsedSessionMessage | undefined {
170 const subtype = readSystemSubtype(msg.message);
175 return { kind: 'system-notification', uuid: msg.uuid, subtype, text, timestamp };
176 }
178 > // #endregion
179 >
180 > // #region Builder
181 >
182 > /**
183 > * Allowlist of `system` subtypes that survive replay as
184 > * {@link ResponsePartKind.SystemNotification} parts on the active turn.
185 > * Mirrors CONTEXT M7's table — anything not in this set is dropped.
186 > */
187 > const ALLOWED_SYSTEM_SUBTYPES: ReadonlySet<string> = new Set([
188 > 'compact_boundary',
189 > 'notification',
190 > ]);
191 >
192 > /**
193 > * CLI-echo markers the Claude Code CLI writes into the transcript for
194 > * replay fidelity. They are `type: 'user'` envelopes whose `message.content`
195 > * is a raw string starting with one of these tags — `<command-name>` /
196 > * `<command-args>` (slash-command echoes like `/model claude-opus-4.7`),
197 > * `<local-command-stdout>` / `<local-command-stderr>` (echo of the local
198 > * handler's output, e.g. "Set model to claude-opus-4.7"), and
199 > * `<local-command-caveat>` (the "messages below were generated while…"
200 > * preamble). The entries don't carry `isSynthetic` / `isMeta` reliably
201 > * (the `/model` echo lacks both, verified empirically), so the only reliable
202 > * discriminator is the content shape itself. Drop on replay so the workbench
203 > * doesn't render them as user turns.
204 > */
205 > const CLI_ECHO_MARKER_PATTERN = /^<(command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat)>/;
206 >
207 > interface InProgressTurn {
208 > readonly id: string;
209 > readonly userText: string;
210 > readonly startedAt?: string;
211 > lastResponseAt?: string;
212 > readonly responseParts: ResponsePart[];
213 > /**
214 > * `tool_use_id`s announced by THIS turn. Drained when the matching
215 > * `tool_result` lands (which may arrive in this turn's user-side
216 > * `tool_result` block or a later turn's). At turn close, non-empty →
217 > * tail Turn marked `Cancelled`.
218 > */
219 > readonly pendingToolUseIds: Set<string>;
220 > /**
221 > * Stash of completed `ToolCallResponsePart`s waiting on their result
222 > * content. `tool_use` opens with a placeholder; the matching
223 > * `tool_result` fills it in. Keyed by `tool_use_id`.
224 > */
225 > readonly toolCallParts: Map<string, ToolCallResponsePart>;
226 > }
227 >
228 > class ReplayBuilder {
229 > private readonly _turns: Turn[] = [];
230 > private _active: InProgressTurn | undefined;
231 > /**
232 > * Cross-turn tool-use tracking. Keyed by `tool_use_id`:
233 > * - `turnId` — the announcing turn (so a late `tool_result` in a
234 > * later `user` envelope can attach back to the right turn per M7).
235 > * - `parsedInput` — the original `tool_use.input`, looked up at
236 > * `_attachToolResult` so the past-tense message can include the
237 > * original parameters. Mirrors the live mapper's `_toolCallInfo`
238 > * pattern but simpler (replay has the full input synchronously on
239 > * the `tool_use` block).
240 > */
241 > private readonly _toolUses = new Map<string, { readonly turnId: string; readonly parsedInput: Record<string, unknown> | undefined }>();
242 >
243 > constructor(private readonly _session: URI, private readonly _logService: ILogService) { }
244 >
245 > consume(msg: ParsedSessionMessage): void {
246 switch (msg.kind) {
247 case 'user-text':
284 }
285 }
287 > finish(): readonly Turn[] {
288 this._closeActive();
289 return this._turns;
290 }
292 > private _consumeAssistant(msg: ParsedSessionMessage & { kind: 'assistant' }): void {
293 if (this._active === undefined) {
294 if (!msg.isInner) {
341 }
342 }
344 > private _openToolUse(toolUseId: string, toolName: string, input: unknown): void {
345 if (this._active === undefined) {
346 return;
369 this._toolUses.set(toolUseId, { turnId: this._active.id, parsedInput });
370 }
372 > private _attachToolResult(block: UserToolResultBlock): string | undefined {
373 const entry = this._toolUses.get(block.tool_use_id);
374 if (entry === undefined) {
421 return announcingTurnId;
422 }
424 > private _findToolCallPart(turnId: string, toolUseId: string): ToolCallResponsePart | undefined {
425 if (this._active && this._active.id === turnId) {
426 return this._active.toolCallParts.get(toolUseId);
440 return undefined;
441 }
443 > private _closeActive(): void {
444 if (this._active === undefined) {
445 return;
464 this._active = undefined;
465 }
467 >
468 > // #endregion
469 >
470 > // #region Helpers — narrow-at-the-seam shape readers
471 >
472 > /**
473 > * Returns string content (legacy form) or an array of recognised user
474 > * blocks (text + tool_result). Anything else returns `undefined` and the
475 > * caller drops the message — matches the production extension's parser
476 > * semantics per CONTEXT M7 glossary.
477 > */
478 function readUserContent(raw: unknown): string | ReadonlyArray<UserTextBlock | UserToolResultBlock> | undefined {
479 if (raw === null || typeof raw !== 'object') {
501 return out.length > 0 ? out : undefined;
502 }
504 function readAssistantBlocks(raw: unknown): readonly AssistantBlock[] | undefined {
505 if (raw === null || typeof raw !== 'object') {
530 return out;
531 }
533 function readSystemSubtype(raw: unknown): string | undefined {
534 if (raw === null || typeof raw !== 'object') {
538 return typeof subtype === 'string' ? subtype : undefined;
539 }
541 function readSystemText(raw: unknown): string | undefined {
542 if (raw === null || typeof raw !== 'object') {
552 return undefined;
553 }
555 > /**
556 > * Mirror of the live mapper's helper — kept inline so the two mappers
557 > * don't yet need a shared module. If a third consumer appears, factor
558 > * to `claudeToolResultContent.ts`.
559 > */
560 function extractToolResultContent(content: unknown): { type: ToolResultContentType.Text; text: string }[] | undefined {
561 if (typeof content === 'string') {
577 return out.length > 0 ? out : undefined;
578 }
580 function safeStringify(v: unknown): string | undefined {
581 try {
585 }
586 }
588 > /**
589 > * True when the message content is a CLI slash-command echo (e.g.
590 > * `<command-name>/model</command-name>...`) that the subprocess writes
591 > * to the transcript for restore fidelity but is not a user-authored prompt.
592 > * Checks the first text fragment only; mixed messages where the first
593 > * content block is a real prompt are NOT filtered.
594 > */
595 function isCliEchoContent(content: string | ReadonlyArray<UserTextBlock | UserToolResultBlock>): boolean {
596 if (typeof content === 'string') {
600 return firstText !== undefined && CLI_ECHO_MARKER_PATTERN.test(firstText.text);
601 }
603 > // #endregion