diff --git a/apps/examples/desktop-app/sidecar/cloud-session-snapshots.test.ts b/apps/examples/desktop-app/sidecar/cloud-session-snapshots.test.ts new file mode 100644 index 0000000000..cc55198850 --- /dev/null +++ b/apps/examples/desktop-app/sidecar/cloud-session-snapshots.test.ts @@ -0,0 +1,505 @@ +import type { HubEventEnvelope } from "@cline/shared"; +import { describe, expect, it } from "vitest"; +import { reconcileBufferedCloudEvents } from "./cloud-session-snapshots"; + +describe("reconcileBufferedCloudEvents", () => { + const event = ( + name: HubEventEnvelope["event"], + id: string, + payload: Record = {}, + ): HubEventEnvelope => ({ + version: "v1", + event: name, + eventId: id, + timestamp: Date.now(), + sessionId: "inner-1", + payload, + }); + + it.each([ + "Continue", + "", + ])("preserves submitted lifecycle while marking only newly reflected %j prompts", (prompt) => { + const submitted = (id: string) => + event("session.pending_prompt_submitted", id, { + prompt: { + id, + prompt, + delivery: "queue", + ...(prompt + ? {} + : { + userImages: ["data:image/png;base64,AA=="], + attachmentCount: 1, + }), + }, + }); + const first = submitted("q-1"); + const later = submitted("q-2"); + const baseline = [ + { + role: "user", + content: prompt || [ + { + type: "image", + source: { type: "base64", media_type: "image/png", data: "AA==" }, + }, + ], + }, + ]; + // An older identical user message must not consume a new submission. + expect( + reconcileBufferedCloudEvents([first], baseline, { + baselineMessages: baseline, + }), + ).toEqual([first]); + const toolResult = { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "call-1", content: "done" }, + ], + }; + expect( + reconcileBufferedCloudEvents([first], [...baseline, toolResult], { + baselineMessages: baseline, + }), + ).toEqual([first]); + const snapshot = [ + ...baseline, + toolResult, + { + ...baseline[0], + ...(prompt + ? { + content: [ + ...toolResult.content, + { type: "text", text: `${prompt}` }, + ], + } + : {}), + }, + ]; + const completed = event("run.completed", "done"); + const running = event("run.started", "next"); + // Keep the next turn's start between lifecycle events; only its bubble is reflected. + expect( + reconcileBufferedCloudEvents( + [completed, first, running, later], + snapshot, + { + baselineMessages: baseline, + }, + ), + ).toEqual([ + completed, + { ...first, payload: { ...first.payload, transcriptReflected: true } }, + running, + later, + ]); + // A submission received after the transcript reply cannot be in it. + expect( + reconcileBufferedCloudEvents([later], snapshot, { + baselineMessages: baseline, + messagesSnapshotEventCutoff: 0, + }), + ).toEqual([later]); + }); + + it("replays content the snapshot does NOT contain", () => { + const buffered = [ + event("assistant.delta", "a-1", { text: "unpersisted reply" }), + event("run.completed", "done-1"), + ]; + expect( + reconcileBufferedCloudEvents(buffered, [ + { role: "assistant", content: "a completely different answer" }, + ]).map((item) => item.event), + ).toEqual(["assistant.delta", "run.completed"]); + }); + + it("does not let an interior substring claim another run's snapshot", () => { + const buffered = [ + event("assistant.finished", "a-1", { text: "foo" }), + event("run.completed", "done-1"), + event("assistant.finished", "a-2", { text: "The answer is foobar" }), + event("run.completed", "done-2"), + ]; + expect( + reconcileBufferedCloudEvents(buffered, [ + { role: "assistant", content: "The answer is foobar" }, + ]).map((item) => item.eventId), + ).toEqual(["a-1", "done-1", "done-2"]); + }); + + it.each([ + ["bar", "foobar", false], + ["bar", "foobar", true], + ["Done", "Done", false], + ] as const)("preserves aborted %j beside saved %j (reverse: %s)", (partial, saved, reverse) => { + const aborted = [ + event("assistant.delta", "partial", { text: partial }), + event("run.aborted", "aborted"), + ]; + const completed = [ + event("assistant.delta", "saved-delta", { text: saved }), + event("assistant.finished", "saved-finished", { text: saved }), + event("run.completed", "completed"), + ]; + expect( + reconcileBufferedCloudEvents( + reverse ? [...completed, ...aborted] : [...aborted, ...completed], + [{ role: "assistant", content: saved }], + ).map((item) => item.eventId), + ).toEqual( + reverse + ? ["completed", "partial", "aborted"] + : ["partial", "aborted", "completed"], + ); + }); + + it.each([ + "assistant", + "reasoning", + ] as const)("does not let finished %s hide the other aborted content", (finished) => { + const partial = finished === "assistant" ? "reasoning" : "assistant"; + const buffered = [ + event(`${finished}.finished`, "saved", { + text: "foobar", + reasoning: "foobar", + }), + event(`${partial}.delta`, "partial", { text: "bar" }), + event("run.aborted", "aborted"), + ]; + expect( + reconcileBufferedCloudEvents(buffered, [ + { + role: "assistant", + content: [ + { type: "text", text: "foobar" }, + { type: "thinking", thinking: "foobar" }, + ], + }, + ]).map((item) => item.eventId), + ).toEqual(["partial", "aborted"]); + }); + + it.each([ + ["assistant", false], + ["reasoning", false], + ["reasoning", true], + ] as const)("preserves unfinished %s after saved output (redacted: %s)", (kind, redacted) => { + const saved = [ + event(`${kind}.delta`, "saved-delta", { + text: redacted ? "" : "saved", + redacted, + }), + event(`${kind}.finished`, "saved-finished", { + [kind === "assistant" ? "text" : "reasoning"]: redacted + ? undefined + : "saved", + }), + ]; + const partial = event(`${kind}.delta`, "partial", { text: "unfinished" }); + const snapshot = [ + { + role: "assistant", + content: [ + redacted + ? { type: "redacted_thinking", data: "opaque" } + : kind === "assistant" + ? { type: "text", text: "saved" } + : { type: "thinking", thinking: "saved" }, + ], + }, + ]; + for (const terminal of ["run.aborted", "run.failed"] as const) { + const end = event(terminal, "end"); + const buffered = [...saved, partial, end]; + expect(reconcileBufferedCloudEvents(buffered, snapshot)).toEqual([ + partial, + end, + ]); + const earlier = [ + { + role: "assistant", + content: [ + kind === "assistant" + ? { type: "text", text: "earlier" } + : { type: "thinking", thinking: "earlier" }, + ], + }, + ]; + expect( + reconcileBufferedCloudEvents( + [ + event(`${kind}.finished`, "earlier", { + [kind === "assistant" ? "text" : "reasoning"]: "earlier", + }), + ...buffered, + ], + [...earlier, ...snapshot], + { baselineMessages: earlier }, + ), + ).toEqual([partial, end]); + expect(reconcileBufferedCloudEvents(buffered, [])).toEqual(buffered); + expect( + reconcileBufferedCloudEvents(buffered, snapshot, { + baselineMessages: snapshot, + }), + ).toEqual(buffered); + expect( + reconcileBufferedCloudEvents(buffered, snapshot, { + messagesSnapshotEventCutoff: 1, + }), + ).toEqual( + terminal === "run.aborted" ? buffered : [saved[1], partial, end], + ); + } + }); + + it.each([ + "assistant", + "reasoning", + ] as const)("preserves %s output completed after the snapshot", (kind) => { + const key = kind === "assistant" ? "text" : "reasoning"; + const saved = event(`${kind}.finished`, "saved", { [key]: "saved" }); + const partial = event(`${kind}.delta`, "partial", { text: "new reply" }); + const finished = event(`${kind}.finished`, "finished", { + [key]: "new reply", + }); + const end = event("run.completed", "end"); + expect( + reconcileBufferedCloudEvents( + [saved, partial, finished, end], + [ + { + role: "assistant", + content: [ + { + type: kind === "assistant" ? "text" : "thinking", + [kind === "assistant" ? "text" : "thinking"]: "saved", + }, + ], + }, + ], + { messagesSnapshotEventCutoff: 2 }, + ), + ).toEqual([partial, finished, end]); + }); + + it.each([ + "run.completed", + "run.aborted", + "run.failed", + ] as const)("ignores empty finishes after saved content in %s", (terminal) => { + const end = event(terminal, "end"); + expect( + reconcileBufferedCloudEvents( + [ + event("assistant.finished", "saved", { text: "saved" }), + event("assistant.finished", "empty", { text: "" }), + event("assistant.finished", "missing"), + end, + ], + [{ role: "assistant", content: "saved" }], + ), + ).toEqual([end]); + }); + + it("supersedes despite trailing whitespace in the streamed text", () => { + const buffered = [ + event("assistant.finished", "f-1", { text: "the answer \n" }), + event("run.completed", "done-1"), + ]; + expect( + reconcileBufferedCloudEvents(buffered, [ + { role: "assistant", content: "prefix the answer" }, + ]).map((item) => item.event), + ).toEqual(["run.completed"]); + }); + + it("drops buffered queue snapshots when a fresh queue snapshot was applied", () => { + const buffered = [ + event("session.pending_prompts", "q-1", { prompts: [] }), + event("assistant.delta", "a-1", { text: "live tail" }), + ]; + expect( + reconcileBufferedCloudEvents(buffered, []).map((item) => item.event), + ).toEqual(["assistant.delta"]); + }); + + it("replays the newest buffered queue snapshot when the queue fetch failed", () => { + const buffered = [ + event("session.pending_prompts", "q-1", { prompts: [] }), + event("session.pending_prompts", "q-2", { + prompts: [{ id: "p-1", prompt: "queued work" }], + }), + event("assistant.delta", "a-1", { text: "live tail" }), + ]; + expect( + reconcileBufferedCloudEvents(buffered, [], { + queueSnapshotApplied: false, + }).map((item) => item.eventId), + ).toEqual(["q-2", "a-1"]); + }); + + it("replays a queue snapshot received after the queue fetch", () => { + const buffered = [ + event("session.pending_prompts", "q-old", { prompts: [] }), + event("assistant.delta", "a-1", { text: "live tail" }), + event("session.pending_prompts", "q-new", { + prompts: [{ id: "p-1", prompt: "queued work" }], + }), + ]; + expect( + reconcileBufferedCloudEvents(buffered, [], { + queueSnapshotEventCutoff: 1, + }).map((item) => item.eventId), + ).toEqual(["a-1", "q-new"]); + }); + + it("supersedes content independently across two terminal run segments", () => { + const buffered = [ + event("assistant.delta", "a-1", { text: "first tail" }), + event("run.completed", "done-1"), + event("assistant.delta", "a-2", { text: "second tail" }), + event("run.completed", "done-2"), + ]; + + expect( + reconcileBufferedCloudEvents(buffered, [ + { role: "assistant", content: "prefix first tail" }, + { role: "assistant", content: "prefix second tail" }, + ]).map((item) => item.event), + ).toEqual(["run.completed", "run.completed"]); + }); + + it("does not let an older identical reply supersede a new buffered turn", () => { + const buffered = [ + event("assistant.delta", "a-2", { text: "Done" }), + event("run.completed", "done-2"), + ]; + const baseline = [{ role: "assistant", content: "Done" }]; + + expect( + reconcileBufferedCloudEvents(buffered, baseline, { + baselineMessages: baseline, + }).map((item) => item.event), + ).toEqual(["assistant.delta", "run.completed"]); + expect( + reconcileBufferedCloudEvents( + buffered, + [...baseline, { role: "assistant", content: "Done" }], + { baselineMessages: baseline }, + ).map((item) => item.event), + ).toEqual(["run.completed"]); + }); + + it("keeps run.failed while suppressing reflected content and dedupes tools by id", () => { + const buffered = [ + event("assistant.delta", "a-1", { text: "partial failure" }), + event("tool.started", "tool-1", { toolCallId: "call-1" }), + event("run.failed", "failed-1", { error: "boom" }), + ]; + + expect( + reconcileBufferedCloudEvents(buffered, [ + { role: "assistant", content: "saved partial failure" }, + { + role: "assistant", + content: [{ type: "tool_use", id: "call-1", name: "read_file" }], + }, + ]).map((item) => item.event), + ).toEqual(["run.failed"]); + }); + + it("only suppresses tool phases present in the transcript before its cutoff", () => { + const started = event("tool.started", "start", { toolCallId: "call-1" }); + const finished = event("tool.finished", "finish", { + toolCallId: "call-1", + output: "done", + }); + const snapshot = [ + { + role: "assistant", + content: [{ type: "tool_use", id: "call-1", name: "read_file" }], + }, + ]; + expect(reconcileBufferedCloudEvents([started, finished], snapshot)).toEqual( + [finished], + ); + const completedSnapshot = [ + ...snapshot, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "call-1", content: "done" }, + ], + }, + ]; + expect( + reconcileBufferedCloudEvents([started, finished], completedSnapshot), + ).toEqual([]); + expect( + reconcileBufferedCloudEvents([started, finished], completedSnapshot, { + messagesSnapshotEventCutoff: 1, + }), + ).toEqual([finished]); + }); + + it.each([ + ["run.failed", false], + ["run.aborted", false], + ["run.aborted", true], + ] as const)("does not replay persisted thinking for %s (redacted: %s)", (terminal, redacted) => { + const buffered = [ + event("reasoning.delta", "thinking", { + text: redacted ? "" : "Checking", + redacted, + }), + event("reasoning.finished", "thought", { + reasoning: redacted ? undefined : "Checking", + }), + event(terminal, "end"), + ]; + const snapshot = [ + { + role: "assistant", + content: [ + redacted + ? { type: "redacted_thinking", data: "opaque" } + : { type: "thinking", thinking: "Checking" }, + ], + }, + ]; + expect(reconcileBufferedCloudEvents(buffered, snapshot)).toEqual([ + buffered[2], + ]); + expect(reconcileBufferedCloudEvents(buffered, [])).toEqual(buffered); + expect( + reconcileBufferedCloudEvents(buffered, snapshot, { + baselineMessages: snapshot, + }), + ).toEqual(buffered); + expect( + reconcileBufferedCloudEvents(buffered, snapshot, { + messagesSnapshotEventCutoff: 0, + }), + ).toEqual(buffered); + }); + + it("does not treat persisted assistant text as proof that thinking was saved", () => { + const thinking = event("reasoning.delta", "thinking", { text: "Checking" }); + const done = event("run.completed", "done"); + expect( + reconcileBufferedCloudEvents( + [ + thinking, + event("assistant.finished", "answer", { text: "Done" }), + done, + ], + [{ role: "assistant", content: "Done" }], + ), + ).toEqual([thinking, done]); + }); +}); diff --git a/apps/examples/desktop-app/sidecar/cloud-session-snapshots.ts b/apps/examples/desktop-app/sidecar/cloud-session-snapshots.ts new file mode 100644 index 0000000000..5c4386fb4a --- /dev/null +++ b/apps/examples/desktop-app/sidecar/cloud-session-snapshots.ts @@ -0,0 +1,362 @@ +import { isUserRunMessage } from "@cline/core"; +import type { HubEventEnvelope } from "@cline/shared"; +import type { JsonRecord, PromptInQueue } from "./types"; + +export function readSessionRows( + payload: Record | undefined, +): JsonRecord[] { + return Array.isArray(payload?.sessions) + ? payload.sessions.filter( + (item): item is JsonRecord => + Boolean(item) && typeof item === "object" && !Array.isArray(item), + ) + : []; +} + +export function updatedAt(record: JsonRecord): number { + const value = record.updatedAt; + return typeof value === "number" + ? value + : Date.parse(String(value ?? "")) || 0; +} + +export function sessionRowModelId(record: JsonRecord | undefined): string { + const metadata = + record?.metadata && typeof record.metadata === "object" + ? (record.metadata as JsonRecord) + : undefined; + return String(metadata?.model ?? record?.model ?? "").trim(); +} + +export function isRootSessionRow(record: JsonRecord): boolean { + const metadata = + record.metadata && typeof record.metadata === "object" + ? (record.metadata as JsonRecord) + : undefined; + return !String( + metadata?.parentSessionId ?? record.parentSessionId ?? "", + ).trim(); +} + +function messageText( + message: unknown, + kind: "text" | "thinking" = "text", +): string { + if (!message || typeof message !== "object" || Array.isArray(message)) { + return ""; + } + const content = (message as JsonRecord).content; + if (typeof content === "string") { + return kind === "text" ? content.trim() : ""; + } + if (!Array.isArray(content)) { + return ""; + } + return content + .map((part) => + part && typeof part === "object" && !Array.isArray(part) + ? kind === "thinking" && + (part as JsonRecord).type === "redacted_thinking" + ? "[redacted]" + : String((part as JsonRecord)[kind] ?? "") + : "", + ) + .join("") + .trim(); +} + +function normalizeUserPrompt(text: string): string { + const trimmed = text.trim(); + const match = trimmed.match(/^]*>([\s\S]*)<\/user_input>$/); + return (match ? match[1] : trimmed).trim(); +} + +export function countPromptOccurrences( + messages: unknown[], + prompts: PromptInQueue[], + prompt: string, +): number { + const expected = normalizeUserPrompt(prompt); + return ( + messages.filter( + (message) => + Boolean(message) && + typeof message === "object" && + !Array.isArray(message) && + String((message as JsonRecord).role ?? "").toLowerCase() === "user" && + (expected !== "" || isUserRunMessage(message as JsonRecord)) && + normalizeUserPrompt(messageText(message)) === expected, + ).length + + prompts.filter((item) => normalizeUserPrompt(item.prompt) === expected) + .length + ); +} + +export function submittedPromptsFromEvents( + events: HubEventEnvelope[], +): PromptInQueue[] { + return events.flatMap((event) => { + if (event.event !== "session.pending_prompt_submitted") return []; + const prompt = + event.payload?.prompt && + typeof event.payload.prompt === "object" && + !Array.isArray(event.payload.prompt) + ? (event.payload.prompt as JsonRecord) + : undefined; + const id = String(prompt?.id ?? "").trim(); + if (!id) return []; + return [ + { + id, + prompt: String(prompt?.prompt ?? ""), + steer: prompt?.delivery === "steer", + attachmentCount: + typeof prompt?.attachmentCount === "number" + ? prompt.attachmentCount + : 0, + userImages: Array.isArray(prompt?.userImages) + ? prompt.userImages.filter( + (image): image is string => typeof image === "string", + ) + : undefined, + }, + ]; + }); +} + +const TERMINAL_RUN_EVENTS = new Set([ + "run.completed", + "run.aborted", + "run.failed", +]); +const SUPERSEDABLE_CONTENT_EVENTS = new Set([ + "assistant.delta", + "assistant.finished", + "reasoning.delta", + "reasoning.finished", +]); + +function assistantTexts( + messages: unknown[], + kind: "text" | "thinking", +): string[] { + return messages + .filter( + (message): message is JsonRecord => + Boolean(message) && + typeof message === "object" && + !Array.isArray(message) && + String((message as JsonRecord).role ?? "").toLowerCase() === + "assistant", + ) + .map((message) => messageText(message, kind)) + .filter(Boolean); +} + +function newlyPersistedAssistantTexts( + snapshotMessages: unknown[], + baselineMessages: unknown[], + kind: "text" | "thinking" = "text", +): string[] { + const baselineCounts = new Map(); + for (const text of assistantTexts(baselineMessages, kind)) { + baselineCounts.set(text, (baselineCounts.get(text) ?? 0) + 1); + } + return assistantTexts(snapshotMessages, kind).filter((text) => { + const count = baselineCounts.get(text) ?? 0; + if (count === 0) return true; + if (count === 1) baselineCounts.delete(text); + else baselineCounts.set(text, count - 1); + return false; + }); +} + +function collectToolCallIds( + value: unknown, + phase: "tool_use" | "tool_result", + result = new Set(), +): Set { + if (!value || typeof value !== "object") return result; + if (Array.isArray(value)) { + for (const item of value) collectToolCallIds(item, phase, result); + return result; + } + const record = value as JsonRecord; + const id = phase === "tool_use" ? record.id : record.tool_use_id; + if (record.type === phase && typeof id === "string") { + result.add(id); + } + for (const child of Object.values(record)) + collectToolCallIds(child, phase, result); + return result; +} + +function streamedAssistantText( + events: HubEventEnvelope[], + kind: "assistant" | "reasoning" = "assistant", +): string { + let finishedText = ""; + let deltas = ""; + for (const event of events) { + if (event.event === `${kind}.delta`) { + deltas += + kind === "reasoning" && event.payload?.redacted && !event.payload?.text + ? "[redacted]" + : typeof event.payload?.text === "string" + ? event.payload.text + : ""; + } else if (event.event === `${kind}.finished`) { + const text = event.payload?.[kind === "assistant" ? "text" : "reasoning"]; + const completed = typeof text === "string" && text ? text : deltas; + if (completed) finishedText = completed; + deltas = ""; + } + } + return (finishedText || deltas).trim(); +} + +/** Reconciles each completed run separately; tools dedupe by stable call id. */ +export function reconcileBufferedCloudEvents( + events: HubEventEnvelope[], + snapshotMessages: unknown[], + options: { + /** + * Whether a fresh queue snapshot was fetched and applied during + * rehydration. When it was, queue events received before its reply are + * stale; later events still win. When the fetch failed, the newest + * buffered queue event is the best state available. + */ + queueSnapshotApplied?: boolean; + queueSnapshotEventCutoff?: number; + /** Events received after the transcript reply cannot be reflected in it. */ + messagesSnapshotEventCutoff?: number; + baselineMessages?: unknown[]; + } = {}, +): HubEventEnvelope[] { + const queueSnapshotApplied = options.queueSnapshotApplied !== false; + const unclaimedAssistantTexts = newlyPersistedAssistantTexts( + snapshotMessages, + options.baselineMessages ?? [], + ); + const unclaimedThinking = newlyPersistedAssistantTexts( + snapshotMessages, + options.baselineMessages ?? [], + "thinking", + ); + const snapshotToolCallIds = collectToolCallIds(snapshotMessages, "tool_use"); + const snapshotToolResultIds = collectToolCallIds( + snapshotMessages, + "tool_result", + ); + const beforeTranscript = new Set( + events.slice(0, options.messagesSnapshotEventCutoff ?? events.length), + ); + const reflectedSubmissions = new Set(); + const unclaimedUserCounts = new Map(); + for (const event of events.slice( + 0, + options.messagesSnapshotEventCutoff ?? events.length, + )) { + const submitted = submittedPromptsFromEvents([event])[0]; + if (!submitted) continue; + const prompt = normalizeUserPrompt(submitted.prompt); + const count = + unclaimedUserCounts.get(prompt) ?? + Math.max( + 0, + countPromptOccurrences(snapshotMessages, [], prompt) - + countPromptOccurrences(options.baselineMessages ?? [], [], prompt), + ); + unclaimedUserCounts.set(prompt, Math.max(0, count - 1)); + if (count > 0) reflectedSubmissions.add(event); + } + // Queue events are full snapshots, so only the newest one matters. + const queueEvents = queueSnapshotApplied + ? events.slice(options.queueSnapshotEventCutoff ?? events.length) + : events; + const lastQueueEvent = queueEvents.findLast( + (event) => event.event === "session.pending_prompts", + ); + const reconciled: HubEventEnvelope[] = []; + let segment: HubEventEnvelope[] = []; + + const flush = (terminal: boolean) => { + if (segment.length === 0) return; + const snapshotSegment = segment.filter((event) => + beforeTranscript.has(event), + ); + // A buffered run can contain saved replies followed by unsaved output. + const contentEnd = (kind: "assistant" | "reasoning") => { + if (!terminal) return -1; + const finished = snapshotSegment.findLastIndex( + (event) => event.event === `${kind}.finished`, + ); + return finished >= 0 || segment.at(-1)?.event === "run.aborted" + ? finished + : snapshotSegment.length - 1; + }; + const assistantEnd = contentEnd("assistant"); + const reasoningEnd = contentEnd("reasoning"); + const streamed = streamedAssistantText( + snapshotSegment.slice(0, assistantEnd + 1), + ); + const persistedIndex = streamed + ? unclaimedAssistantTexts.findIndex((text) => text.endsWith(streamed)) + : -1; + const contentPersisted = persistedIndex >= 0; + if (contentPersisted) unclaimedAssistantTexts.splice(persistedIndex, 1); + const thinking = streamedAssistantText( + snapshotSegment.slice(0, reasoningEnd + 1), + "reasoning", + ); + const thinkingIndex = thinking + ? unclaimedThinking.findIndex((text) => text.endsWith(thinking)) + : -1; + if (thinkingIndex >= 0) unclaimedThinking.splice(thinkingIndex, 1); + for (const [index, event] of segment.entries()) { + // Preserve the turn-start lifecycle; the UI must only skip its user bubble. + if (reflectedSubmissions.has(event)) { + reconciled.push({ + ...event, + payload: { ...event.payload, transcriptReflected: true }, + }); + continue; + } + if ( + beforeTranscript.has(event) && + SUPERSEDABLE_CONTENT_EVENTS.has(event.event) && + (event.event.startsWith("reasoning.") + ? thinkingIndex >= 0 && index <= reasoningEnd + : contentPersisted && index <= assistantEnd) + ) { + continue; + } + if ( + event.event === "session.pending_prompts" && + event !== lastQueueEvent + ) { + continue; + } + // Keep terminal events: run.failed may carry the only error detail. + if (beforeTranscript.has(event) && event.event.startsWith("tool.")) { + const toolCallId = String(event.payload?.toolCallId ?? "").trim(); + if ( + snapshotToolResultIds.has(toolCallId) || + (event.event === "tool.started" && + snapshotToolCallIds.has(toolCallId)) + ) + continue; + } + reconciled.push(event); + } + segment = []; + }; + + for (const event of events) { + segment.push(event); + if (TERMINAL_RUN_EVENTS.has(event.event)) flush(true); + } + // Never supersede an unterminated tail. + flush(false); + return reconciled; +}