Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/adapters/cursor/envelope-echo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export type RoutingCommentaryDecision =
| { kind: "flush" }
| { kind: "hallucination" };

const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b/giu;
const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|(?<![\p{L}\p{M}\p{N}_])네이티브\s*(?:셸|쉘)/giu;
const ROUTING_TOOL_HINT =
/(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
const ROUTING_FAILURE_CLAIM =
Expand Down Expand Up @@ -276,7 +276,7 @@ export class CursorRoutingCommentarySniffer {
private matchesHallucination(): boolean {
if (!ROUTING_FAILURE_CLAIM.test(this.buffered)) return false;
const nativeTools = new Set(
[...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
[...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase() ?? "shell"),
);
if (nativeTools.size === 0) return false;
return ROUTING_REDIRECT_CLAIM.test(this.buffered) || nativeTools.size >= 2;
Expand Down
2 changes: 1 addition & 1 deletion structure/adapters/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Request-local adapter bindings are separate from registry authority in the Respo
[core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior.

The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec).

Shared parsing and streaming follow the [request-copy](../transports/byte-accounting.md#request-copy-accounting) and [stream-buffer accounting](../transports/byte-accounting.md#stream-buffer-accounting) contracts. Response-attached WebSocket telemetry follows the [stage record identity contract](../transports/responses.md#passthrough-sse-stream-shapes-314).

Expand Down
2 changes: 1 addition & 1 deletion structure/data-planes/inbound-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Compatibility callers retain the public Responses ingress described by the
[core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior.

The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec).

## Standalone file transcription

Expand Down
2 changes: 1 addition & 1 deletion structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Chat Provider Compatibility

The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](cursor.md#cursor-native-exec).

Native Codex Spark-specific request exceptions are absent. General Lite and namespace repair
remain shared [Responses compatibility](../transports/responses.md#responses-httpsse), including
Expand Down
2 changes: 2 additions & 0 deletions structure/providers/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ that survives the transport budget: unified Desktop `exec` as well as the legacy
unified `exec` keeps its own schema and is surfaced back to Codex as a client tool. It must never
fall through to the separate native-local-exec dispatcher.

In external Cursor turns using code mode or shell aliases, the bounded leading-commentary guard in `src/adapters/cursor/envelope-echo.ts` counts `Shell`, `네이티브 셸`, and `네이티브 쉘` as one `shell` identity, including spacing variants and names split across text deltas. Korean aliases require a Unicode-aware left token boundary so wording embedded in a larger word or identifier is not counted; punctuation and following Korean grammatical suffixes remain supported. Rejection still requires a failure claim plus either an explicit redirect or at least two distinct native-tool identities; repeated aliases alone do not count as multiple tools.

> Decision record: [ADR-0048](../decisions/ADR-0048-cursor-native-exec.md)

## Cursor parameterized models
Expand Down
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Responses admission and finalization are composed through the
[core module ownership](transports/responses.md#core-module-ownership). This surface retains its existing behavior.

The configuration-only [plaintext V2 contract](subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](providers/cursor.md#cursor-native-exec).

Chat request serialization owns the destination-scoped
[OpenCode Go instruction ordering](providers/chat-compat.md#opencode-go-chronological-instructions);
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/byte-accounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Responses body-reader limits and lifetime handling follow the

How opencodex measures request and stream bytes without allocating copies solely to count
them. These contracts are shared by request parsing, SSE rewriting, the provider adapters and
the translator budget, which is why so many documents link here rather than restating them. Response-attached WebSocket telemetry follows the [stage record identity contract](responses.md#passthrough-sse-stream-shapes-314).
the translator budget, which is why so many documents link here rather than restating them. Response-attached WebSocket telemetry follows the [stage record identity contract](responses.md#passthrough-sse-stream-shapes-314). Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec).

## Request-copy accounting

Expand Down
2 changes: 1 addition & 1 deletion structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The existing Responses transport is divided by responsibility in the
[core module ownership](responses.md#core-module-ownership). This surface retains its existing behavior.

The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec).

The Chat adapter's [OpenCode Go instruction ordering](../providers/chat-compat.md#opencode-go-chronological-instructions)
changes translated message placement only; endpoint selection and transport stay with their existing owners.
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Responses Transport

The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages)
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged.
is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec).

Plaintext collaboration restoration treats a null namespace as absent, rejects non-string namespace types, and restores the native namespace/name pair before HTTP/WS delivery and continuation publication.

Expand Down
55 changes: 48 additions & 7 deletions tests/providers/cursor/cursor-envelope-echo-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import { createCursorAdapter as createCursorAdapterProduction } from "../../../src/adapters/cursor";
import {
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
CursorEnvelopeEchoSniffer,
CursorMidstreamEchoObserver,
CursorRoutingCommentarySniffer,
Expand Down Expand Up @@ -210,6 +211,42 @@ describe("cursor external output quarantine + corrective retry (devlog 260826 ga
expect(contextFree.finish().kind).toBe("flush");
});

test.each(["네이티브 셸", "네이티브 쉘", "네이티브셸", "네이티브\t쉘", "“네이티브 셸”", "(네이티브쉘)"])(
"localized shell requires a redirect or a second distinct tool (%s)", nativeShell => {
const redirect = new CursorRoutingCommentarySniffer();
expect(redirect.feed(`${nativeShell}이 차단되어 exec_command로 전환합니다.`).kind).toBe("hallucination");
const distinct = new CursorRoutingCommentarySniffer();
expect(distinct.feed(`${nativeShell}과 Read가 모두 unavailable 상태입니다.`).kind).toBe("hallucination");
},
);

test.each(["비네이티브 셸", "비네이티브쉘", "x네이티브 셸", "_네이티브쉘", "1네이티브 셸", "a\u0301네이티브 셸"])(
"embedded Korean shell wording does not fabricate a second tool (%s)", nativeShell => {
const sniffer = new CursorRoutingCommentarySniffer();
expect(sniffer.feed(`${nativeShell} 관련 Read가 unavailable 상태입니다.`).kind).toBe("hold");
expect(sniffer.finish().kind).toBe("flush");
},
);

test("localized shell detection spans native-name and redirect delta boundaries", () => {
const sniffer = new CursorRoutingCommentarySniffer();
for (const fragment of ["네이", "티브 ", "쉘이 차단되어 ", "exec_"]) {
expect(sniffer.feed(fragment).kind).toBe("hold");
}
expect(sniffer.feed("command로 전환합니다.").kind).toBe("hallucination");
});

test.each([
"네이티브 셸이 unavailable 상태입니다.",
"네이티브 셸과 네이티브 쉘이 모두 blocked 상태입니다.",
"Shell과 네이티브 셸이 모두 blocked 상태입니다.",
"SHELL과 네이티브쉘, 네이티브 셸이 모두 unavailable 상태입니다.",
])("shell aliases alone do not fabricate two distinct tools (%s)", text => {
const sniffer = new CursorRoutingCommentarySniffer();
expect(sniffer.feed(text).kind).toBe("hold");
expect(sniffer.finish().kind).toBe("flush");
});

test("external tool-result echo retries once with the corrective action text and no leaked envelope", async () => {
const { factory, runRequests, attempts } = echoingThenHealthyTransportFactory();
const adapter = createCursorAdapter({ ...provider, apiKey: "cursor-token" }, { createTransport: factory as never });
Expand Down Expand Up @@ -318,18 +355,21 @@ describe("cursor external output quarantine + corrective retry (devlog 260826 ga
expect(runRequests[1]?.echoRetryContinuationText).toBeDefined();
});

test("code-mode routing commentary that invents a blocked native Shell is quarantined and retried", async () => {
test.each([
{ fragments: ["`Shell` 경로는 차단됐으니 exec_command 경로로 읽겠습니다."] },
{ fragments: ["네이", "티브 셸은 차단됐으니 ", "exec_command 경로로 읽겠습니다."] },
{ fragments: ["네이티브", "쉘은 차단됐으니 ", "exec_command 경로로 읽겠습니다."] },
])("code-mode routing commentary is quarantined and retried once (%j)", async ({ fragments }) => {
let attempt = 0;
const runRequests: CursorRunRequest[] = [];
const factory = () => ({
async *run(request: CursorRunRequest) {
runRequests.push(request);
attempt += 1;
if (attempt === 1) {
yield {
type: "text",
text: "`Shell` 경로는 또 같은 문구로 차단됐으니, 통과가 확인된 `exec_command` 경로로 읽겠습니다.",
} satisfies CursorServerMessage;
for (const text of fragments) {
yield { type: "text", text } satisfies CursorServerMessage;
}
} else {
yield { type: "text", text: "READ_OK" } satisfies CursorServerMessage;
}
Expand Down Expand Up @@ -359,7 +399,8 @@ describe("cursor external output quarantine + corrective retry (devlog 260826 ga
const text = events.filter(e => e.type === "text_delta").map(e => (e as { text: string }).text).join("");
expect(attempt).toBe(2);
expect(text).toBe("READ_OK");
expect(text).not.toContain("Shell");
expect(runRequests[1]?.echoRetryContinuationText).toBeDefined();
expect(text).not.toContain(fragments.join(""));
expect(runRequests).toHaveLength(2);
expect(runRequests[1]?.echoRetryContinuationText).toBe(CURSOR_ROUTING_COMMENTARY_RETRY_TEXT);
});
});
Loading