Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
23 changes: 21 additions & 2 deletions src/adapters/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
CursorEnvelopeEchoSniffer,
CursorMidstreamEchoObserver,
CursorRoutingCommentaryError,
CursorRoutingCommentarySniffer,
CursorToolResultEchoError,
Expand Down Expand Up @@ -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)
&& (
Expand All @@ -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 = [];
};
Expand Down Expand Up @@ -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 =
Expand All @@ -342,7 +361,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
: undefined;
emit(providerState ? { ...event, providerState } : event);
} else {
emit(event);
emitTextObserved(event);
}
}
},
Expand Down
128 changes: 128 additions & 0 deletions src/adapters/cursor/envelope-echo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: "" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the active finding when another marker appears

When two marker lines occur within the 512-character corruption window—most notably the normal adjacent [Tool Result]\n[tool_result] envelope—this assignment replaces the active watch before settleCorruption() records it. Consequently, distinct mid-stream echoes fewer than 512 characters apart are collapsed and the first marker/offset is silently lost, defeating the advertised multi-finding diagnostics. Record the first finding immediately or settle/preserve the active watch before starting another.

Useful? React with 👍 / 👎.

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.
Expand Down
Loading
Loading