Repository navigation
fix(cursor,xai): stop grok-4.6 mid-turn tool-result echoes from sticking #5098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ import { | |
| recordCursorIncompleteToolRemint, | ||
| recordCursorEnvelopeEchoRemint, | ||
| recordCursorOverflowRemint, | ||
| rememberCursorConversationRewrite, | ||
| rememberCursorThreadConversation, | ||
| shouldSkipCursorOverflowRemint, | ||
| shouldSurfaceCursorOverflowFirst, | ||
|
|
@@ -55,6 +56,7 @@ import { | |
| CursorRoutingCommentaryError, | ||
| CursorRoutingCommentarySniffer, | ||
| CursorToolResultEchoError, | ||
| stripAssistantEchoedToolEnvelope, | ||
| } from "./cursor/envelope-echo"; | ||
| import { | ||
| createDisabledCursorTransport, | ||
|
|
@@ -306,8 +308,22 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| let guardHeld: AdapterEvent[] = []; | ||
| // Exactly-once observation: every client-bound text delta passes through here | ||
| // exactly once — held deltas only on release, ordinary deltas at emit time. | ||
| let suppressMidstreamEchoTail = false; | ||
| const emitTextObserved = (event: AdapterEvent): void => { | ||
| if (event.type === "text_delta") midstreamObserver?.feed(event.text); | ||
| if (event.type === "text_delta") { | ||
| if (suppressMidstreamEchoTail) return; | ||
| if (midstreamObserver) { | ||
| midstreamObserver.feed(event.text); | ||
| const kept = stripAssistantEchoedToolEnvelope(event.text); | ||
| const found = midstreamObserver.findings().length > 0 || kept !== event.text; | ||
| if (found) { | ||
| suppressMidstreamEchoTail = true; | ||
| sawMidstreamEnvelopeEcho = true; | ||
| if (kept && kept !== event.text) emit({ ...event, text: kept }); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| emit(event); | ||
| }; | ||
| const releaseGuardHeld = () => { | ||
|
|
@@ -448,6 +464,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| // the next turn does not recompute the stale deterministic thread hash. Isolated helper / | ||
| // compaction turns must not park their throwaway id under the parent or Desktop owner. | ||
| const threadOwner = cursorClientThreadOwner(_parsed); | ||
| rememberCursorConversationRewrite(failedConversationId, next.conversationId); | ||
| if (threadOwner && _parsed._cursorIsolateConversation !== true) { | ||
| rememberCursorThreadConversation( | ||
| threadOwner, | ||
|
|
@@ -570,11 +587,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| } else if (!sawIncompleteToolCall && completedNormally && incompleteToolRemintScopeKey) { | ||
| clearCursorIncompleteToolRemint(incompleteToolRemintScopeKey); | ||
| } | ||
| // A mid-stream envelope echo has ALREADY reached the client — the prefix sniffer only | ||
| // watches the first bytes of a turn, and grok-4.6 writes a real sentence before pasting | ||
| // the envelope. It cannot be quarantined, so the recovery is the same as the | ||
| // incomplete-tool case: leave this turn alone and rotate the next turn's id, otherwise | ||
| // the stored echo is replayed and primes the model to echo again. | ||
| // A mid-stream envelope echo is stripped from client-visible deltas, but any leading | ||
| // prose already escaped, so this send is not retried. grok-4.6 writes a real sentence | ||
| // before pasting the envelope; rotating the next turn's id still matters because Codex | ||
| // Desktop may restore the pre-remint conversation from providerState. | ||
| // | ||
| // Its own budget, not the incomplete-tool one: echoing is cheap and repeatable while an | ||
| // incomplete client-tool stream is rare and structural, so a shared counter would let a | ||
|
|
@@ -584,12 +600,19 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda | |
| _parsed._cursorIsolateConversation !== true | ||
| && request.contextUsageStoreCheckpoints !== false | ||
| ? cursorEnvelopeEchoRemintScopeKey( | ||
| cursorClientThreadOwner(_parsed), | ||
| cursorClientThreadOwner(_parsed) | ||
| ?? _parsed._cursorConversationId | ||
| ?? request.conversationId, | ||
|
Comment on lines
+603
to
+605
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '150,220p' src/adapters/cursor.ts
sed -n '440,490p' src/adapters/cursor.ts
sed -n '570,640p' src/adapters/cursor.ts
rg -n 'recordCursorEnvelopeEchoRemint|resolveCursorConversationRewrite|_cursorConversationId' src/adapters/cursor src/adapters/cursor.tsRepository: lidge-jun/opencodex Length of output: 11807 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- thread-continuity definitions ---'
sed -n '1,150p' src/adapters/cursor/thread-continuity.ts
sed -n '300,380p' src/adapters/cursor/thread-continuity.ts
printf '%s\n' '--- request-builder conversation resolution ---'
sed -n '320,390p' src/adapters/cursor/request-builder.ts
printf '%s\n' '--- scope-key and rewrite usages ---'
rg -n -C 4 'cursorEnvelopeEchoRemintScopeKey|rememberCursorConversationRewrite|resolveCursorConversationRewrite|recordCursorEnvelopeEchoRemint|clearCursorEnvelopeEchoRemint' src tests
printf '%s\n' '--- candidate focused tests ---'
rg -n -C 3 'EnvelopeEchoRemint|envelope.?echo|conversationRewrite|conversation rewrite|remint' tests src/adapters/cursor | head -n 260Repository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- thread-continuity definitions ---'
sed -n '1,150p' src/adapters/cursor/thread-continuity.ts
sed -n '300,380p' src/adapters/cursor/thread-continuity.ts
printf '%s\n' '--- request-builder conversation resolution ---'
sed -n '320,390p' src/adapters/cursor/request-builder.ts
printf '%s\n' '--- scope-key and rewrite usages ---'
rg -n -C 4 'cursorEnvelopeEchoRemintScopeKey|rememberCursorConversationRewrite|resolveCursorConversationRewrite|recordCursorEnvelopeEchoRemint|clearCursorEnvelopeEchoRemint' src tests
printf '%s\n' '--- focused tests ---'
rg -n -C 3 'EnvelopeEchoRemint|envelope.?echo|conversationRewrite|conversation rewrite|remint' tests src/adapters/cursor | head -n 260Repository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
sed -n '168,205p' src/adapters/cursor/thread-continuity.ts
sed -n '440,545p' tests/providers/cursor/cursor-envelope-echo-retry.test.tsRepository: lidge-jun/opencodex Length of output: 6538 Retain a stable budget scope when no thread owner exists. When Retain a stable, identity-scoped key for the remint chain and use it in the no-thread-owner path. Do not let a missing scope select the unconditional 🤖 Prompt for AI Agents |
||
| _parsed._cursorIdentityScope, | ||
| ) | ||
| : null; | ||
| if (sawMidstreamEnvelopeEcho && !sawIncompleteToolCall && envelopeEchoRemintScopeKey) { | ||
| if (recordCursorEnvelopeEchoRemint(envelopeEchoRemintScopeKey)) { | ||
| if (sawMidstreamEnvelopeEcho && !sawIncompleteToolCall | ||
| && _parsed._cursorIsolateConversation !== true | ||
| && request.contextUsageStoreCheckpoints !== false) { | ||
| const allowed = envelopeEchoRemintScopeKey | ||
| ? recordCursorEnvelopeEchoRemint(envelopeEchoRemintScopeKey) | ||
| : true; | ||
| if (allowed) { | ||
| if (inheritedCheckpointRef) invalidateCursorCheckpoint(inheritedCheckpointRef); | ||
| debugProviderDiagnostic("cursor", "midstream-envelope-echo-remint", { | ||
| wireModel: request.modelId, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,8 +62,70 @@ export function lookupCursorThreadConversation( | |
| return entry.conversationId; | ||
| } | ||
|
|
||
| const conversationRewrites = new Map<string, { to: string; updatedAt: number }>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
printf '%s\n' '--- thread continuity ---'
cat -n src/adapters/cursor/thread-continuity.ts | sed -n '55,135p'
printf '%s\n' '--- request builder ---'
cat -n src/adapters/cursor/request-builder.ts | sed -n '340,385p'
printf '%s\n' '--- identity scope references ---'
rg -n -C 3 '_cursorIdentityScope|cursorIdentityScope' src/adapters/cursor.ts src/adapters/cursor src | head -n 240Repository: lidge-jun/opencodex Length of output: 23726 Reachability: External Reachability pathScope conversation rewrites by Cursor identity.
🤖 Prompt for AI AgentsSources: Coding guidelines, Path instructions |
||
|
|
||
| function pruneConversationRewrites(at: number): void { | ||
| for (const [key, entry] of conversationRewrites) { | ||
| if (at - entry.updatedAt > OVERRIDE_TTL_MS) conversationRewrites.delete(key); | ||
| else break; // Map iterates insertion order; refreshed entries are moved to the end | ||
| } | ||
| while (conversationRewrites.size > OVERRIDE_MAX_ENTRIES) { | ||
| const oldest = conversationRewrites.keys().next().value; | ||
| if (oldest === undefined) break; | ||
| conversationRewrites.delete(oldest); | ||
| } | ||
| } | ||
|
|
||
| function rememberRewriteEntry(from: string, to: string, at: number): void { | ||
| conversationRewrites.delete(from); | ||
| conversationRewrites.set(from, { to, updatedAt: at }); | ||
| } | ||
|
|
||
| /** | ||
| * Remember that a poisoned Cursor conversation id was rotated away. | ||
| * | ||
| * Codex Desktop often restores the pre-remint id from providerState on the next | ||
| * Responses request and may omit x-codex-parent-thread-id. Thread-owner | ||
| * lookup then misses, and the next turn resumes the same Cursor conversation. | ||
| * Rewriting the old wire id itself survives that restore path. | ||
| */ | ||
| export function rememberCursorConversationRewrite( | ||
| fromConversationId: string, | ||
| toConversationId: string, | ||
| ): void { | ||
| if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return; | ||
| const at = now(); | ||
| rememberRewriteEntry(fromConversationId, toConversationId, at); | ||
| const redirected: string[] = []; | ||
| for (const [from, entry] of conversationRewrites) { | ||
| if (from !== fromConversationId && entry.to === fromConversationId) redirected.push(from); | ||
| } | ||
| for (const from of redirected) rememberRewriteEntry(from, toConversationId, at); | ||
| pruneConversationRewrites(at); | ||
| } | ||
|
|
||
| /** Follow a remint chain; missing or expired entries return the input id. */ | ||
| export function resolveCursorConversationRewrite(conversationId: string): string { | ||
| const at = now(); | ||
| const seen = new Set<string>(); | ||
| let current = conversationId; | ||
| while (!seen.has(current)) { | ||
| const entry = conversationRewrites.get(current); | ||
| if (!entry) break; | ||
| if (at - entry.updatedAt > OVERRIDE_TTL_MS) { | ||
| conversationRewrites.delete(current); | ||
| break; | ||
| } | ||
| rememberRewriteEntry(current, entry.to, at); | ||
| seen.add(current); | ||
| current = entry.to; | ||
| } | ||
| return current; | ||
| } | ||
|
|
||
| export function clearCursorThreadContinuityForTests(): void { | ||
| overrides.clear(); | ||
| conversationRewrites.clear(); | ||
| } | ||
|
|
||
| /** Max conversation-id remints after the first surfaced overflow per retained scope. */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { CursorMidstreamEchoObserver, stripAssistantEchoedToolEnvelope } from "../adapters/cursor/envelope-echo"; | ||
| import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite"; | ||
|
|
||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return !!value && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
|
|
||
| function stripOutputTextFields(value: unknown): boolean { | ||
| let stripped = false; | ||
| if (Array.isArray(value)) { | ||
| for (const item of value) stripped = stripOutputTextFields(item) || stripped; | ||
| return stripped; | ||
| } | ||
| if (!isRecord(value)) return false; | ||
| if (typeof value.text === "string") { | ||
| const next = stripAssistantEchoedToolEnvelope(value.text); | ||
| if (next !== value.text) { | ||
| value.text = next; | ||
| stripped = true; | ||
| } | ||
| } | ||
| if (typeof value.delta === "string") { | ||
| const next = stripAssistantEchoedToolEnvelope(value.delta); | ||
| if (next !== value.delta) { | ||
| value.delta = next; | ||
| stripped = true; | ||
| } | ||
| } | ||
| for (const nested of Object.values(value)) { | ||
| stripped = stripOutputTextFields(nested) || stripped; | ||
| } | ||
| return stripped; | ||
| } | ||
|
|
||
| /** Strip a whole-line [Tool Result] echo from a completed Responses JSON body. */ | ||
| export function stripGrokUpstreamEnvelopeEchoFromResponsesJson(json: string): string { | ||
| try { | ||
| const parsed = JSON.parse(json) as unknown; | ||
| if (!stripOutputTextFields(parsed)) return json; | ||
| return JSON.stringify(parsed); | ||
| } catch { | ||
| return json; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Drop grok-4.6 mid-turn [Tool Result] envelopes on the xAI Responses SSE path. | ||
| * Cursor already quarantines this on protobuf; xAI relays SSE verbatim, so the | ||
| * same paste otherwise lands in Codex as a successful final answer. | ||
| */ | ||
| export function createGrokUpstreamEnvelopeEchoBlockRewrite(): SseBlockRewrite { | ||
| const observer = new CursorMidstreamEchoObserver(); | ||
| let suppress = false; | ||
| const rewrite: SseBlockRewrite = (block) => { | ||
| const payload = sseDataPayload(block); | ||
| if (payload === null || payload === "[DONE]") return [block]; | ||
| let event: unknown; | ||
| try { | ||
| event = JSON.parse(payload); | ||
| } catch { | ||
| return [block]; | ||
| } | ||
| if (!isRecord(event) || typeof event.type !== "string") return [block]; | ||
| if (event.type === "response.output_text.delta" && typeof event.delta === "string") { | ||
| if (suppress) return []; | ||
| observer.feed(event.delta); | ||
| const kept = stripAssistantEchoedToolEnvelope(event.delta); | ||
| const found = observer.findings().length > 0 || kept !== event.delta; | ||
|
Comment on lines
+66
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Buffer a possible marker prefix before you forward it. A marker can span logical Keep an uncommitted suffix that can still be an 🤖 Prompt for AI Agents |
||
| if (!found) return [block]; | ||
| suppress = true; | ||
| if (!kept || kept === event.delta) return []; | ||
| event.delta = kept; | ||
| return [replaceSseDataPayload(block, JSON.stringify(event))]; | ||
| } | ||
| if (suppress && event.type === "response.output_text.delta") return []; | ||
| if (event.type === "response.output_text.done" && typeof event.text === "string") { | ||
| const kept = stripAssistantEchoedToolEnvelope(event.text); | ||
| if (kept === event.text) return [block]; | ||
| event.text = kept; | ||
| return [replaceSseDataPayload(block, JSON.stringify(event))]; | ||
| } | ||
| if (event.type === "response.completed" || event.type === "response.output_item.done") { | ||
| if (stripOutputTextFields(event)) { | ||
| return [replaceSseDataPayload(block, JSON.stringify(event))]; | ||
| } | ||
| } | ||
| return [block]; | ||
| }; | ||
| return rewrite; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,11 @@ import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-sea | |
| import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair"; | ||
| import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame"; | ||
| import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair"; | ||
| import { isXaiResponsesDestination } from "../../providers/xai-transport"; | ||
| import { | ||
| createGrokUpstreamEnvelopeEchoBlockRewrite, | ||
| stripGrokUpstreamEnvelopeEchoFromResponsesJson, | ||
| } from "../grok-upstream-envelope-echo"; | ||
| import { | ||
| createPlaintextV2AgentMessageCallRestoreRewrite, | ||
| restorePlaintextV2AgentMessageCallsInJsonResult, | ||
|
|
@@ -335,6 +340,7 @@ export async function deliverPassthroughResponse( | |
| // (src/server/relay-eager.ts; policy: | ||
| // devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md). | ||
| // The bundled known-bad runtime remains on tee by default on both platforms. | ||
| const grokUpstreamEchoEnabled = isXaiResponsesDestination(route.provider); | ||
| if (isEventStream && upstreamResponse.body) { | ||
| // For streamed passthrough, a successful terminal response means non-error upstream status | ||
| // before relay starts. Waiting for SSE completion would retain request state across the whole | ||
|
|
@@ -501,6 +507,9 @@ export async function deliverPassthroughResponse( | |
| grokClientCompatibilityEnabled | ||
| ? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget) | ||
| : undefined, | ||
| grokUpstreamEchoEnabled | ||
| ? createGrokUpstreamEnvelopeEchoBlockRewrite() | ||
| : undefined, | ||
|
Comment on lines
+510
to
+512
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '470,550p' src/server/responses/passthrough-delivery.ts
sed -n '740,820p' src/server/responses/passthrough-delivery.ts
rg -n 'rememberPassthroughResponseChecked|continuation|inspection' src/server/responses/passthrough-delivery.ts src/server/responsesRepository: lidge-jun/opencodex Length of output: 41223 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- passthrough-delivery imports/types and setup ---'
sed -n '1,220p' src/server/responses/passthrough-delivery.ts
printf '%s\n' '--- SSE delivery branches ---'
sed -n '300,725p' src/server/responses/passthrough-delivery.ts
printf '%s\n' '--- bounded JSON branch ---'
sed -n '725,845p' src/server/responses/passthrough-delivery.ts
printf '%s\n' '--- recorder implementation ---'
sed -n '450,620p' src/server/responses/passthrough-dispatch.ts
printf '%s\n' '--- xAI rewrite definitions and usages ---'
rg -n -C 8 'createGrokUpstreamEnvelopeEchoBlockRewrite|stripGrokUpstreamEnvelopeEchoFromResponsesJson|grokUpstreamEchoEnabled|rememberPassthroughResponseChecked' src/server src | head -n 500Repository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- xAI helper ---'
cat -n src/server/grok-upstream-envelope-echo.ts
printf '%s\n' '--- eager relay implementation ---'
rg -n -C 12 'function relaySseEagerBounded|const relaySseEagerBounded|export .*relaySseEagerBounded' src/server/relay-eager.ts
sed -n '1,280p' src/server/relay-eager.ts
printf '%s\n' '--- block rewrite and inspection ordering ---'
rg -n -C 10 'function relaySseWithBlockRewrite|const relaySseWithBlockRewrite|rewriteBlocks|inspectChunk' src/server/sse-payload-rewrite.ts src/server/relay-eager.ts src/server/relay.ts
printf '%s\n' '--- continuation recorder consumers ---'
rg -n -C 12 'rememberPassthroughResponse\\??\\(|rememberPassthroughResponse\\b|previous_response_id|response_id' src/server/responses src | head -n 500Repository: lidge-jun/opencodex Length of output: 25559 🏁 Script executed: #!/bin/bash
set -e
cat -n src/server/grok-upstream-envelope-echo.ts
printf '%s\n' '--- eager relay ---'
rg -n -C 15 'relaySseEagerBounded|inspectChunk|rewriteBlocks' src/server/relay-eager.ts
printf '%s\n' '--- block rewrite ---'
rg -n -C 15 'relaySseWithBlockRewrite|rewriteBlocks' src/server/sse-payload-rewrite.ts
printf '%s\n' '--- replay consumers ---'
rg -n -C 10 'rememberPassthroughResponse\\b|previous_response_id' src/server/responses src | head -n 400Repository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- xAI helper ---'
cat -n src/server/grok-upstream-envelope-echo.ts
printf '%s\n' '--- eager ordering ---'
rg -n -C 25 'relaySseEagerBounded|inspectChunk|rewriteBlocks' src/server/relay-eager.ts
printf '%s\n' '--- replay storage and lookup ---'
rg -n -C 15 'rememberPassthroughResponse\\b|previous_response_id' src/server/responses/passthrough-dispatch.ts src/server/responses/core-replay.ts src/server/responses/request-prepare.tsRepository: lidge-jun/opencodex Length of output: 33711 Persist the xAI-sanitized response snapshot. On the standard SSE path, the inspector receives raw upstream chunks before Apply the xAI sanitization at the shared 🤖 Prompt for AI Agents |
||
| snapshotRepairEnabled | ||
| ? createResponsesSnapshotBlockRewrite(nativeExchange.outboundRequestBody, translatorBudget) | ||
| : undefined, | ||
|
|
@@ -769,6 +778,9 @@ export async function deliverPassthroughResponse( | |
| // the reframed-SSE branch below are built from this body, so one check covers them. This | ||
| // runs BEFORE the continuation cache write below: a refused turn must not become state a | ||
| // later `previous_response_id` replay can expand from. | ||
| if (grokUpstreamEchoEnabled) { | ||
| clientJson = stripGrokUpstreamEnvelopeEchoFromResponsesJson(clientJson); | ||
| } | ||
| if (nativeExchange.undeclaredToolGuardActive) { | ||
| const undeclared = (() => { | ||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Buffer fragmented midstream markers before emitting text.
This code strips only the current
text_delta. A stream can split[Tool Result]into"[Tool "and"Result]\n...".Line 317 cannot strip either fragment. If
midstreamObserverdetects the combined marker, the first fragment has already reached the client. If it does not detect the combined marker, the full envelope reaches the client across multiple deltas.Hold the unresolved marker prefix until it diverges from every marker. Emit only the confirmed safe prefix. Add a regression test that splits the marker across deltas after leading prose.
Based on learnings, “never assume a 1:1 mapping between emitted stream chunks and logical frames/events.”
🤖 Prompt for AI Agents
Source: Learnings