diff --git a/src/adapters/cursor/envelope-echo.ts b/src/adapters/cursor/envelope-echo.ts index ffaf3df193..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/giu; +const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|(? 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 83e8f4f466..7a4d23583e 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -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). diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 33ca8c76b0..503a8bea80 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -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 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..ee5356c228 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. 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 diff --git a/structure/runtime.md b/structure/runtime.md index bd9ebbd561..5d1960c9b0 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -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); diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 7f01dee197..8f7967db01 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -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 diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index ca80373a8e..476f6b5a6f 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -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. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 4a6a664cad..c5cb9b36e5 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..abb6e9fde4 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,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 }); @@ -318,7 +355,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 +367,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 +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); }); });