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':