diff --git a/devlog/_plan/260828_cursor_ndjson_backlog_train/031_midstream_echo_fix.md b/devlog/_plan/260828_cursor_ndjson_backlog_train/031_midstream_echo_fix.md index 51d366ba1f..155a5ce06c 100644 --- a/devlog/_plan/260828_cursor_ndjson_backlog_train/031_midstream_echo_fix.md +++ b/devlog/_plan/260828_cursor_ndjson_backlog_train/031_midstream_echo_fix.md @@ -21,55 +21,62 @@ history is linear on this chain. ### 1. MODIFY src/adapters/cursor/envelope-echo.ts — mid-stream detector -ADD class CursorMidstreamEchoSniffer: -- feed(textDelta) maintains a rolling tail buffer (last 256 chars) of the - full turn text and scans for NEWLINE-ANCHORED markers: - /(^|\n)\s*\[Tool Result\]/ and likewise for "[tool_result]" and - "[Tool Error]" appearing at a line start BEYOND the first-line window - the prefix sniffer already owns. -- Detection returns { kind: "echo", marker } once; the caller treats it - exactly like the prefix sniffer's echo verdict (retryable semantic - failure). No holding/quarantine: mid-stream detection cannot un-emit - already-released deltas, so the value is the RETRY (fresh conversation, - corrective continuation text) rather than suppression — the same - contract as gap-10's CursorToolResultEchoError but from a later offset. - Note emittedOutput will be true by then; the retry gate in cursor.ts - currently requires !emittedOutput. See change 2. -- Bound: scanning stops after MAX_MIDSTREAM_SCAN_BYTES = 512 * 1024 per - turn (defensive; a turn that long without an echo is not echo-primed). +ADD class CursorMidstreamEchoObserver (A-gate blockers 1/3/4 folded): +- DIAGNOSTIC-ONLY: feed(textDelta) NEVER throws and never withholds + output. It returns void; findings are exposed via a findings() getter + read by the caller at turn end (and opportunistically after each feed). +- Detection: maintain lastLineStartBuffer — the text since the most + recent newline, capped at 128 chars (indentation beyond that disarms + matching for that line; bounds the \s* concern). A marker fires when + the post-newline line, after <=128 chars of leading whitespace, starts + with "[Tool Result]", "[tool_result]", or "[Tool Error]", at an offset + BEYOND the prefix-sniffer window. Marker split across deltas is handled + naturally because the line buffer accumulates across feeds. +- Corruption observation: after a marker fires, the observer enters a + post-marker window (next 512 chars) watching for the call-id lines. It + records callIdCorrupt=true when the window contains /fc_[0-9a-f]+\s+mar-/ + (the observed "space + mar-" splice) or a call_id line whose token is + split by whitespace (/call_id: \S+\s+\S+_0/). Only booleans and + numeric offsets are retained; window text is discarded after the check. +- findings(): { echoes: Array<{ marker, offset, callIdCorrupt }> } — + capped at 8 entries per turn. +- Bound: scanning disarms after MAX_MIDSTREAM_SCAN_LENGTH = 512 * 1024 + UTF-16 code units of cumulative fed text. A delta crossing the cap is + scanned up to its end (the cap is checked between feeds, not mid-delta), + so text before the boundary is never skipped. -### 2. MODIFY src/adapters/cursor.ts — arm + retry policy +### 2. MODIFY src/adapters/cursor.ts — arm + exactly-once feeding -- Arm CursorMidstreamEchoSniffer alongside the prefix sniffer (same - armEchoSniffer condition), feeding every text_delta AFTER guard release. -- On mid-stream echo: throw CursorToolResultEchoError only when the turn - can still be retried safely: replayUnsafe false and NO client tool call - emitted yet (emittedClientTool false). Since text deltas HAVE escaped, - the retry emits an assistant_boundary continuation instead of silent - replacement... NO — simpler audited contract: mid-stream echo does NOT - retry; it emits a diagnostic (debugProviderDiagnostic - "midstream-envelope-echo" with conversationHash, offset, marker, - callIdCorrupt flag) and pushes a text_delta warning? ALSO NO — do not - fabricate visible text. FINAL contract (see accept criteria): detection - is diagnostic-only in this PR (counter + structured log), giving F2 the - wire-side observability 030 asked for; the retry semantics for - already-streamed echoes need their own design round with user-visible - behavior decisions (NEEDS_HUMAN if pursued). -- callIdCorrupt detection: within a detected echo block, match - /call_id: (\S+)/ and /fc_[0-9a-f]/ tokens; flag when a token matches - /\smar-/ (the observed corruption) or call-id fragments split by - whitespace. Logged as booleans/offsets only — no content bytes - (privacy:scan constraint). +- Arm CursorMidstreamEchoObserver under the same armEchoSniffer condition. +- Exactly-once feed (A-gate blocker 2): introduce one helper + emitTextObserved(event) that (a) feeds observer.feed(event.text) then + (b) emits. BOTH release paths route through it: releaseGuardHeld()'s + per-held-event emit for text deltas, and the ordinary post-guard emit at + cursor.ts:~324. Held deltas are NOT fed while held — only on release — + so no double-feed is possible. +- At turn end (done event handling, before final emit): read + observer.findings(); for each finding emit debugProviderDiagnostic + ("cursor", "midstream-envelope-echo", { wireModel, conversationHash: + request.conversationId.slice(0,16), offset, marker, callIdCorrupt }). + marker stays a fixed enum string; no content bytes logged (audit + finding 6 conventions). -### 3. MODIFY tests/cursor-envelope-echo-retry.test.ts +### 3. MODIFY tests/cursor-envelope-echo-retry.test.ts (named activation +### tests, A-gate blocker 5 — one per conditional branch) -- NEW: mid-stream echo after legitimate leading text triggers the - detector exactly once, diagnostic carries marker + offset, - callIdCorrupt=true for a "fc_x mar-y" specimen, false for clean ids. -- NEW: newline-anchored only — "[Tool Result]" inside a quoted sentence - mid-line does NOT trigger (e.g. model legitimately discussing the - string in prose after a code fence on the same line). -- NEW: scan disarms past MAX_MIDSTREAM_SCAN_BYTES. +- "midstream echo after leading text is recorded with marker and offset" + (run-03 specimen block as fixture). +- "midstream corruption window flags a space-spliced mar call-id" + (callIdCorrupt=true) and "clean call-id lines do not flag corruption" + (callIdCorrupt=false). +- "a marker fragmented across delta boundaries still fires" (feed + "[Tool Res" then "ult]\n..."). +- "a mid-line marker mention does not fire" (negative). +- "indentation beyond the 128-char line cap disarms that line" (negative). +- "scanning disarms past the cumulative cap but keeps prior findings". +- "held-then-released deltas are fed exactly once" (adapter-level test via + the existing transport harness: prefix-guard hold + release, observer + offset arithmetic proves single feed). - KEEP: all existing prefix-sniffer tests unchanged. ## Accept criteria + activation diff --git a/devlog/_plan/260828_cursor_ndjson_backlog_train/040_closure_round.md b/devlog/_plan/260828_cursor_ndjson_backlog_train/040_closure_round.md index 3789d68e82..ba382b28ea 100644 --- a/devlog/_plan/260828_cursor_ndjson_backlog_train/040_closure_round.md +++ b/devlog/_plan/260828_cursor_ndjson_backlog_train/040_closure_round.md @@ -10,3 +10,37 @@ description names probe artifacts; devlog unit updated; goalplan criteria c1-c5 capturedEvidence filled. 5. D closes goal only when cxc loop validate passes (E8). + +## Closure results (2026-08-28 09:58-10:23 KST, stack 286a1e5a5 + a652f0dfe) + +Probe proxy 2.35.0 on 10199 (isolated homes, OCX_DEBUG=1), evidence in +macmini-cf ~/ocx-probe-260828/evidence/N5/. + +| Run | Result | Evidence | +|---|---|---| +| c1 5-step | PASS | avg=84, 24 cmdexec, 0 reconnects | +| c2 5-step | PASS | avg=84, 32 cmdexec, 0 reconnects | +| c3 5-step | TASK PASS / TURN STALL | all 5 steps done (avg=84 read at item_28/32) across repeated upstream H2 resets (NGHTTP2_INTERNAL_ERROR, honest no-retry after committed output, reconnect recovery worked); after final step the turn sat in a getBlobArgs/setBlobArgs frame loop and never emitted turn.completed; killed after ~20min. Capture: run-c3.stall-capture.txt. Matches inventory #8/080 stall class — upstream/blob-sync, bounded, now with frame-level capture | +| c4/c5 | NOT RUN | batch serialized behind c3 stall; killed with it. Coverage for their shapes exists in wp3 round 1 (N1 x6, N3) | +| midstream diagnostics | 0 fired | no echo occurred in this round (expected: F1 was 1-in-6 in round 1); detector verified by 17 unit/adapter tests instead | + +## Teardown + restoration proof + +- Probe proxy killed; 10199 closed; 10100 healthy (2.34.0 pid 43321). +- Worktree removed (git worktree list = 1); primary repo dev @ 802f04adc, + porcelain clean — identical to pre-state. +- Cursor credential NOT rotated (expiry 1792555734000 unchanged pre/post). + Primary auth.json/config.toml hashes moved only via the primary launchd + proxy's own token refresh + codex config injection during the window; + probe-side copies were isolated and are retained in evidence. + +## Per-defect disposition (final) + +| Defect | Disposition | Evidence chain | +|---|---|---| +| Backlog false-abort | FIXED (PR #2774) | RCA 001 -> repro tests -> 4cd1b99f0 -> N4 live: 509KB/4560 deltas behind 60s stall, 0 aborts | +| Mid-stream envelope echo (F1) | OBSERVED->INSTRUMENTED (PR #2795) | run-03 wire capture -> CursorMidstreamEchoObserver + 8 tests; retry semantics deliberately deferred | +| mar call-id corruption (F2) | INSTRUMENTED (PR #2795) | first wire capture in run-03; callIdCorrupt flag now fires on live echoes | +| Empty tool-result (inv #1) | NOT REPRODUCED (10 runs) | bridge marker intact in every N1/N3 run; remains WATCH bounded to deep checkpoint sessions | +| Turn stall (inv #8) | CAPTURED, upstream-class | c3 frame loop capture; adapter surfaced honest errors; fix surface is upstream blob sync — no speculative adapter patch | +| Double-batch echo / image loop / premature final (inv #5/6/9) | MODEL/APP-class | unchanged from 100/021 dispositions | diff --git a/src/adapters/cursor.ts b/src/adapters/cursor.ts index 6761a3d194..e3b2a0aed6 100644 --- a/src/adapters/cursor.ts +++ b/src/adapters/cursor.ts @@ -34,6 +34,7 @@ import { CURSOR_ECHO_RETRY_CONTINUATION_TEXT, CURSOR_ROUTING_COMMENTARY_RETRY_TEXT, CursorEnvelopeEchoSniffer, + CursorMidstreamEchoObserver, CursorRoutingCommentaryError, CursorRoutingCommentarySniffer, CursorToolResultEchoError, @@ -232,6 +233,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda isCursorExternalWireModel(activeRequest.modelId) && (_parsed.context.messages ?? []).some(message => message.role === "toolResult"); const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined; + // Mid-stream observer (devlog 260828 F1/F2): diagnostic-only; armed with the + // prefix sniffer because both fire on flattened tool-result replay priming. + const midstreamObserver = armEchoSniffer ? new CursorMidstreamEchoObserver() : undefined; const armRoutingCommentarySniffer = isCursorExternalWireModel(activeRequest.modelId) && ( @@ -242,10 +246,16 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda ? new CursorRoutingCommentarySniffer() : undefined; 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. + const emitTextObserved = (event: AdapterEvent): void => { + if (event.type === "text_delta") midstreamObserver?.feed(event.text); + emit(event); + }; const releaseGuardHeld = () => { for (const held of guardHeld) { if (held.type !== "heartbeat") emittedOutput = true; - emit(held); + emitTextObserved(held); } guardHeld = []; }; @@ -323,6 +333,15 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda } if (event.type !== "heartbeat") emittedOutput = true; if (event.type === "done") { + for (const finding of midstreamObserver?.findings() ?? []) { + debugProviderDiagnostic("cursor", "midstream-envelope-echo", { + wireModel: activeRequest.modelId, + conversationHash: activeRequest.conversationId.slice(0, 16), + marker: finding.marker, + offset: finding.offset, + callIdCorrupt: finding.callIdCorrupt, + }); + } commitCapturedCheckpoint(activeRequest); const inheritedCursor = _parsed._providerContinuation?.cursor; const isolatedOrCompaction = @@ -342,7 +361,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda : undefined; emit(providerState ? { ...event, providerState } : event); } else { - emit(event); + emitTextObserved(event); } } }, diff --git a/src/adapters/cursor/envelope-echo.ts b/src/adapters/cursor/envelope-echo.ts index ac7ec429c3..ffaf3df193 100644 --- a/src/adapters/cursor/envelope-echo.ts +++ b/src/adapters/cursor/envelope-echo.ts @@ -14,6 +14,14 @@ const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const; const MAX_SNIFF_BYTES = 40; +/** Mid-stream observer: max leading whitespace on a line before matching disarms. */ +const MAX_MIDSTREAM_LINE_INDENT = 128; +/** Mid-stream observer: post-marker window watched for call-id corruption. */ +const MIDSTREAM_CORRUPTION_WINDOW = 512; +/** Mid-stream observer: cumulative scan cap (UTF-16 code units, checked between feeds). */ +export const MAX_MIDSTREAM_SCAN_LENGTH = 512 * 1024; +/** Mid-stream observer: findings retained per turn. */ +const MAX_MIDSTREAM_FINDINGS = 8; const MAX_ROUTING_COMMENTARY_BYTES = 512; /** Aggregate quarantine cap: past this, flush and disarm. */ const MAX_HOLD_BYTES = 8 * 1024; @@ -43,6 +51,126 @@ export type EchoSnifferDecision = | { kind: "flush" } | { kind: "echo"; marker: string }; +export interface MidstreamEchoFinding { + marker: string; + /** UTF-16 offset of the marker's line start within the turn's full text. */ + offset: number; + callIdCorrupt: boolean; +} + +/** + * Diagnostic-only mid-stream envelope-echo observer (devlog 260828 F1/F2). + * + * The prefix sniffer only watches the first ~40 bytes of a turn, but live + * probing caught grok-4.6 echoing "[Tool Result]" envelope blocks in the + * MIDDLE of an agent message — after legitimate leading text — one of them + * carrying a whitespace-spliced call-id ("fc_x mar-y" instead of "fc_x-y"). + * Deltas at that point have already reached the client, so this observer + * never throws and never withholds output: it records findings so the + * adapter can emit a structured diagnostic at turn end. Only fixed marker + * enums, numeric offsets, and corruption booleans are retained — never + * content bytes. + */ +export class CursorMidstreamEchoObserver { + private lineBuffer = ""; + private lineStartOffset = 0; + private totalLength = 0; + private disarmed = false; + private lineDisarmed = false; + private corruptionWatch: { finding: MidstreamEchoFinding; remaining: number; window: string } | undefined; + private readonly recorded: MidstreamEchoFinding[] = []; + + feed(textDelta: string): void { + if (this.disarmed && !this.corruptionWatch) return; + let index = 0; + while (index < textDelta.length) { + const newline = textDelta.indexOf("\n", index); + const segment = newline === -1 ? textDelta.slice(index) : textDelta.slice(index, newline); + if (this.corruptionWatch) this.watchCorruption(segment + (newline === -1 ? "" : "\n")); + if (!this.disarmed && !this.lineDisarmed && segment.length > 0) { + this.lineBuffer += segment; + if (this.lineBuffer.length > MAX_MIDSTREAM_LINE_INDENT + 32) { + // Bound per-line work: nothing beyond the indent cap + longest marker can match. + this.lineDisarmed = !this.lineMatchesPrefixSoFar(); + this.lineBuffer = this.lineBuffer.slice(0, MAX_MIDSTREAM_LINE_INDENT + 32); + } + this.checkLine(); + } + if (newline === -1) break; + this.lineBuffer = ""; + this.lineDisarmed = false; + this.lineStartOffset = this.totalLength + newline + 1; + index = newline + 1; + } + this.totalLength += textDelta.length; + if (this.totalLength > MAX_MIDSTREAM_SCAN_LENGTH) this.disarmed = true; + } + + findings(): readonly MidstreamEchoFinding[] { + if (this.corruptionWatch) { + this.settleCorruption(); + } + return this.recorded; + } + + private lineMatchesPrefixSoFar(): boolean { + const probe = this.lineBuffer.replace(/^[ \t]*/, ""); + return ECHO_MARKERS.some(marker => probe.startsWith(marker) || marker.startsWith(probe)); + } + + private checkLine(): void { + const indentMatch = /^[ \t]*/.exec(this.lineBuffer); + const indent = indentMatch ? indentMatch[0].length : 0; + if (indent > MAX_MIDSTREAM_LINE_INDENT) { + this.lineDisarmed = true; + return; + } + const probe = this.lineBuffer.slice(indent); + for (const marker of ECHO_MARKERS) { + if (probe.startsWith(marker)) { + // The prefix sniffer owns the very start of the turn; only offsets past + // its window count as mid-stream. + if (this.lineStartOffset === 0) { + this.lineDisarmed = true; + return; + } + const finding: MidstreamEchoFinding = { + marker, + offset: this.lineStartOffset, + callIdCorrupt: false, + }; + this.corruptionWatch = { finding, remaining: MIDSTREAM_CORRUPTION_WINDOW, window: "" }; + this.lineDisarmed = true; + return; + } + } + if (!ECHO_MARKERS.some(marker => marker.startsWith(probe)) && probe.length > 0) { + this.lineDisarmed = true; + } + } + + private watchCorruption(text: string): void { + const watch = this.corruptionWatch; + if (!watch) return; + const take = Math.min(watch.remaining, text.length); + watch.window += text.slice(0, take); + watch.remaining -= take; + if (watch.remaining <= 0) this.settleCorruption(); + } + + private settleCorruption(): void { + const watch = this.corruptionWatch; + if (!watch) return; + const window = watch.window; + watch.finding.callIdCorrupt = + /fc_[0-9a-f]+[ \t]+mar-/.test(window) + || /call_id: \S+[ \t]+\S+_0\b/.test(window); + if (this.recorded.length < MAX_MIDSTREAM_FINDINGS) this.recorded.push(watch.finding); + // Window text is discarded here; only booleans/offsets survive. + this.corruptionWatch = undefined; + } +} + /** * Incremental envelope-prefix sniffer. Leading whitespace is tolerated so a * marker copied after a newline is still caught. diff --git a/tests/cursor-envelope-echo-retry.test.ts b/tests/cursor-envelope-echo-retry.test.ts index 0cba08740b..e4f4e65ff1 100644 --- a/tests/cursor-envelope-echo-retry.test.ts +++ b/tests/cursor-envelope-echo-retry.test.ts @@ -3,7 +3,9 @@ import { createCursorAdapter as createCursorAdapterProduction } from "../src/ada import { CURSOR_ECHO_RETRY_CONTINUATION_TEXT, CursorEnvelopeEchoSniffer, + CursorMidstreamEchoObserver, CursorRoutingCommentarySniffer, + MAX_MIDSTREAM_SCAN_LENGTH, } from "../src/adapters/cursor/envelope-echo"; import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../src/types"; import type { CursorRunRequest, CursorServerMessage } from "../src/adapters/cursor/types"; @@ -61,6 +63,115 @@ function echoingThenHealthyTransportFactory() { } describe("cursor external output quarantine + corrective retry (devlog 260826 gaps 10-11)", () => { + describe("mid-stream echo observer (devlog 260828 F1/F2)", () => { + const RUN03_SPECIMEN = + "Step 2 produced no stdout, as expected. Next I'll cat the file.\n" + + "[Tool Result]\n[tool_result]\ncall_id: call-c5b79188-edec-4a92\n" + + "fc_63367283 mar-2aec-9a25-b7df-9b125bd8d1b5_0\nname: exec\nis_error: false\noutput:\n70\n84\n98\n"; + + test("midstream echo after leading text is recorded with marker and offset", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("Legit leading sentence.\n"); + observer.feed("[Tool Result]\ncall_id: fc_1234-abcd_0\nrest of block\n"); + const findings = observer.findings(); + expect(findings).toHaveLength(1); + expect(findings[0]!.marker).toBe("[Tool Result]"); + expect(findings[0]!.offset).toBe("Legit leading sentence.\n".length); + }); + + test("midstream corruption window flags a space-spliced mar call-id", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("Leading text about progress.\n"); + observer.feed(RUN03_SPECIMEN); + const findings = observer.findings(); + expect(findings).toHaveLength(1); + expect(findings[0]!.callIdCorrupt).toBe(true); + }); + + test("clean call-id lines do not flag corruption", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("Leading text.\n"); + observer.feed("[Tool Result]\n[tool_result]\ncall_id: call-1\nfc_63367283-2aec-9a25_1\noutput:\nok\n"); + const findings = observer.findings(); + expect(findings).toHaveLength(1); + expect(findings[0]!.callIdCorrupt).toBe(false); + }); + + test("a marker fragmented across delta boundaries still fires", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("prose first\n[Tool Res"); + observer.feed("ult]\ncall_id: fc_9 mar-broken_0\n"); + const findings = observer.findings(); + expect(findings).toHaveLength(1); + expect(findings[0]!.marker).toBe("[Tool Result]"); + expect(findings[0]!.callIdCorrupt).toBe(true); + }); + + test("a mid-line marker mention does not fire", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("first line\nThe string [Tool Result] appeared in the transcript I reviewed.\n"); + expect(observer.findings()).toHaveLength(0); + }); + + test("a turn-start marker belongs to the prefix sniffer, not the observer", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("[Tool Result]\ncall_id: fc_1 mar-2_0\n"); + expect(observer.findings()).toHaveLength(0); + }); + + test("indentation beyond the 128-char line cap disarms that line", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("first\n" + " ".repeat(200) + "[Tool Result]\n"); + expect(observer.findings()).toHaveLength(0); + }); + + test("scanning disarms past the cumulative cap but keeps prior findings", () => { + const observer = new CursorMidstreamEchoObserver(); + observer.feed("lead\n[Tool Result]\ncall_id: clean_0\n"); + const filler = "x".repeat(64 * 1024); + for (let fed = 0; fed <= MAX_MIDSTREAM_SCAN_LENGTH; fed += filler.length) observer.feed(filler + "\n"); + observer.feed("late\n[Tool Error]\n"); + const findings = observer.findings(); + expect(findings).toHaveLength(1); + expect(findings[0]!.marker).toBe("[Tool Result]"); + }); + }); + + test("held-then-released deltas are fed exactly once (adapter diagnostic offset proves single feed)", async () => { + // The prefix guard holds early deltas then releases them through the same + // observed emit path. If a delta were fed twice, the mid-stream echo + // offset would shift by the duplicated length; asserting the exact + // offset in the diagnostic proves exactly-once feeding. + const previousDebug = process.env.OCX_DEBUG; + process.env.OCX_DEBUG = "1"; + const errLines: string[] = []; + const originalError = console.error; + console.error = (line: unknown) => { errLines.push(String(line)); }; + try { + const LEAD = "Progressing through the steps now.\n"; + const factory = () => ({ + async *run() { + yield { type: "text", text: LEAD } satisfies CursorServerMessage; + yield { type: "text", text: "[Tool Result]\ncall_id: fc_9 mar-broken_0\n" } satisfies CursorServerMessage; + yield { type: "done", usage: { inputTokens: 1, outputTokens: 1 } } satisfies CursorServerMessage; + }, + writeClient() {}, + }); + const adapter = createCursorAdapter({ ...provider, apiKey: "cursor-token" }, { createTransport: factory as never }); + const events: AdapterEvent[] = []; + await adapter.runTurn?.(toolResultBody("cursor/kimi-k3"), { headers: new Headers() }, event => events.push(event)); + const diag = errLines.find(line => line.includes("midstream-envelope-echo")); + expect(diag).toBeDefined(); + const payload = JSON.parse(diag!.slice(diag!.indexOf("{"))) as { offset: number; callIdCorrupt: boolean }; + expect(payload.offset).toBe(LEAD.length); + expect(payload.callIdCorrupt).toBe(true); + } finally { + console.error = originalError; + if (previousDebug === undefined) delete process.env.OCX_DEBUG; + else process.env.OCX_DEBUG = previousDebug; + } + }); + test("sniffer: marker split across deltas is detected; divergent text flushes", () => { const echo = new CursorEnvelopeEchoSniffer(); expect(echo.feed("[Tool ").kind).toBe("hold");