From fc260e9ce5d5dc1a1134b76e575c3aa4526d2a3e Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:53:57 +0900 Subject: [PATCH 1/2] fix(cursor): normalize localized native-shell routing claims --- src/adapters/cursor/envelope-echo.ts | 4 +- structure/adapters/registry.md | 2 +- structure/data-planes/inbound-compat.md | 2 +- structure/providers/chat-compat.md | 2 +- structure/providers/cursor.md | 2 + structure/runtime.md | 2 +- structure/transports/byte-accounting.md | 2 +- structure/transports/inventory.md | 2 +- structure/transports/responses.md | 2 +- .../cursor/cursor-envelope-echo-retry.test.ts | 47 ++++++++++++++++--- 10 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/adapters/cursor/envelope-echo.ts b/src/adapters/cursor/envelope-echo.ts index ffaf3df193..6848106a0b 100644 --- a/src/adapters/cursor/envelope-echo.ts +++ b/src/adapters/cursor/envelope-echo.ts @@ -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|네이티브\s*(?:셸|쉘)/giu; const ROUTING_TOOL_HINT = /(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu; const ROUTING_FAILURE_CLAIM = @@ -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; diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 908633f265..a607422084 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -1,7 +1,7 @@ # Adapter Registry Authority 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. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index aa9aa15f52..9fe6ae2919 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -1,7 +1,7 @@ # Inbound Compatibility Surfaces 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 diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 5ee17ed807..ea9630cb3b 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -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 diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 5ae38028d8..b95af45a94 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -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. 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 diff --git a/structure/runtime.md b/structure/runtime.md index e4a50ba3f8..c882249a5c 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -1,7 +1,7 @@ # Runtime 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); diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index e758afeaf2..67c63745be 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -2,7 +2,7 @@ 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. +the translator budget, which is why so many documents link here rather than restating them. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec). ## Request-copy accounting diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 8c04f7b633..7366843bf9 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -1,7 +1,7 @@ # Transport Inventory 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. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 1bc2b4de0d..d56b52d168 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -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. diff --git a/tests/providers/cursor/cursor-envelope-echo-retry.test.ts b/tests/providers/cursor/cursor-envelope-echo-retry.test.ts index 0470eee8c6..112cf84ad6 100644 --- a/tests/providers/cursor/cursor-envelope-echo-retry.test.ts +++ b/tests/providers/cursor/cursor-envelope-echo-retry.test.ts @@ -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, @@ -210,6 +211,34 @@ 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("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 }); @@ -318,7 +347,11 @@ 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 = () => ({ @@ -326,10 +359,9 @@ describe("cursor external output quarantine + corrective retry (devlog 260826 ga 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; } @@ -359,7 +391,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); }); }); From 588c33395d088ca258343ab21e365e41882fdd60 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:05:25 +0900 Subject: [PATCH 2/2] fix(cursor): bound Korean shell aliases at token boundaries --- src/adapters/cursor/envelope-echo.ts | 2 +- structure/providers/cursor.md | 2 +- .../cursor/cursor-envelope-echo-retry.test.ts | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/adapters/cursor/envelope-echo.ts b/src/adapters/cursor/envelope-echo.ts index 6848106a0b..962d93e170 100644 --- a/src/adapters/cursor/envelope-echo.ts +++ b/src/adapters/cursor/envelope-echo.ts @@ -217,7 +217,7 @@ export type RoutingCommentaryDecision = | { kind: "flush" } | { kind: "hallucination" }; -const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|네이티브\s*(?:셸|쉘)/giu; +const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|(? Decision record: [ADR-0048](../decisions/ADR-0048-cursor-native-exec.md) diff --git a/tests/providers/cursor/cursor-envelope-echo-retry.test.ts b/tests/providers/cursor/cursor-envelope-echo-retry.test.ts index 112cf84ad6..abb6e9fde4 100644 --- a/tests/providers/cursor/cursor-envelope-echo-retry.test.ts +++ b/tests/providers/cursor/cursor-envelope-echo-retry.test.ts @@ -211,7 +211,7 @@ describe("cursor external output quarantine + corrective retry (devlog 260826 ga expect(contextFree.finish().kind).toBe("flush"); }); - test.each(["네이티브 셸", "네이티브 쉘", "네이티브셸", "네이티브\t쉘"])( + 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"); @@ -220,6 +220,14 @@ describe("cursor external output quarantine + corrective retry (devlog 260826 ga }, ); + 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_"]) {