@bitkyc08/opencodex 2.56.0 → 2.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-D4zuyIxQ.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-BBOZWGB6.css">
19
+ <script type="module" crossorigin src="/assets/index-BbrHOIY0.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-C5-RdDmD.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.56.0",
3
+ "version": "2.58.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -76,17 +76,17 @@
76
76
  "@bufbuild/protobuf": "^2.14.0",
77
77
  "@modelcontextprotocol/sdk": "^1.30.0",
78
78
  "@napi-rs/keyring": "1.3.0",
79
- "bun": "1.4.2",
79
+ "bun": "1.4.0",
80
80
  "zod": "4.4.3"
81
81
  },
82
82
  "devDependencies": {
83
- "@types/bun": "1.4.2",
83
+ "@types/bun": "1.4.0",
84
84
  "typescript": "7.0.2"
85
85
  },
86
86
  "overrides": {
87
87
  "@hono/node-server": "2.1.0",
88
88
  "fast-uri": "^3.1.7",
89
- "hono": "4.13.1",
89
+ "hono": "4.13.8",
90
90
  "ip-address": "^10.4.0",
91
91
  "qs": "^6.16.0"
92
92
  },
@@ -4,6 +4,7 @@ import { mapReasoningEffort } from "../../reasoning-effort";
4
4
  import { buildSystemPrompt } from "../coding-agent/protocol";
5
5
  import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type SpawnFn } from "../coding-agent/turn";
6
6
  import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "./profiles";
7
+ import { guardCodeBuddyScaffolding } from "./scaffold-guard";
7
8
 
8
9
  export type { SpawnFn } from "../coding-agent/turn";
9
10
  export type CodeBuddyAdapterDeps = CodingAgentDeps;
@@ -75,7 +76,7 @@ export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBu
75
76
  provider,
76
77
  parsed,
77
78
  incoming,
78
- emit,
79
+ emit: guardCodeBuddyScaffolding(emit),
79
80
  buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov),
80
81
  buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey),
81
82
  deps,
@@ -0,0 +1,249 @@
1
+ import type { AdapterEvent } from "../../types";
2
+
3
+ /** Error code for a CodeBuddy turn whose output contains vendor agent scaffolding. */
4
+ export const CODEBUDDY_SCAFFOLD_ERROR_CODE = "vendor_scaffold_detected";
5
+
6
+ // The observed control protocol uses FULLWIDTH VERTICAL LINE (U+FF5C). Detection stays
7
+ // deliberately narrower than the marker spelling: a calls control line must be followed by an
8
+ // invoke line with a non-empty tool name. That distinguishes an agent scaffold from prose quoting or
9
+ // discussing one tag.
10
+ const DSML_CALLS_LINE = "<||dsml|| calls>";
11
+ const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"";
12
+
13
+ export interface CodeBuddyScaffoldFilterResult {
14
+ /** Bytes released from a suffix withheld by an earlier event on this channel. */
15
+ releasedPending: string;
16
+ /** Safe bytes belonging to the event currently being processed. */
17
+ text: string;
18
+ /** The earlier pending event still owns the extended candidate. */
19
+ pendingContinues: boolean;
20
+ fail: boolean;
21
+ }
22
+
23
+ interface ScanResult {
24
+ safe: string;
25
+ held: string;
26
+ fail: boolean;
27
+ fence: "`" | "~" | null;
28
+ lineStart: boolean;
29
+ }
30
+
31
+ function prefixAtEnd(text: string, at: number, expected: string): boolean {
32
+ const rest = text.slice(at).toLowerCase();
33
+ return rest.length < expected.length && expected.startsWith(rest);
34
+ }
35
+
36
+ /**
37
+ * Scan complete bytes and retain only a bounded suffix that can still become a control sequence.
38
+ *
39
+ * Control tags are recognized only at column zero and outside fenced Markdown. Inline code,
40
+ * quoted strings, blockquotes, indented source, and prose all add syntax before the tag and are
41
+ * therefore forwarded unchanged. A calls line alone is harmless; refusal requires the observed
42
+ * two-line calls-plus-named-invoke grammar.
43
+ */
44
+ function scan(
45
+ text: string,
46
+ initialFence: "`" | "~" | null,
47
+ initialLineStart: boolean,
48
+ ): ScanResult {
49
+ let fence = initialFence;
50
+ let lineStart = initialLineStart;
51
+ let index = 0;
52
+
53
+ while (index < text.length) {
54
+ if (lineStart) {
55
+ const fenceMarkers = fence ? [fence.repeat(3)] : ["```", "~~~"];
56
+ const completeFence = fenceMarkers.find(marker => text.startsWith(marker, index));
57
+ if (completeFence) {
58
+ fence = fence ? null : (completeFence[0] as "`" | "~");
59
+ index += completeFence.length;
60
+ lineStart = false;
61
+ continue;
62
+ }
63
+ if (fenceMarkers.some(marker => prefixAtEnd(text, index, marker))) {
64
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
65
+ }
66
+
67
+ if (!fence) {
68
+ const lowered = text.slice(index).toLowerCase();
69
+ if (lowered.startsWith(DSML_CALLS_LINE)) {
70
+ const afterCalls = index + DSML_CALLS_LINE.length;
71
+ let invokeAt = -1;
72
+ if (text[afterCalls] === "\n") invokeAt = afterCalls + 1;
73
+ else if (text[afterCalls] === "\r" && text[afterCalls + 1] === "\n") invokeAt = afterCalls + 2;
74
+ else if (afterCalls === text.length || (text[afterCalls] === "\r" && afterCalls + 1 === text.length)) {
75
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
76
+ }
77
+
78
+ if (invokeAt >= 0) {
79
+ const invokeRest = text.slice(invokeAt).toLowerCase();
80
+ const invokeNameStart = invokeRest[DSML_INVOKE_PREFIX.length];
81
+ if (invokeRest.startsWith(DSML_INVOKE_PREFIX) && invokeNameStart && !/[\s"]/.test(invokeNameStart)) {
82
+ return { safe: text.slice(0, index), held: "", fail: true, fence, lineStart };
83
+ }
84
+ if (invokeRest.length === 0 || DSML_INVOKE_PREFIX.startsWith(invokeRest)) {
85
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
86
+ }
87
+ }
88
+ } else if (prefixAtEnd(text, index, DSML_CALLS_LINE)) {
89
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
90
+ }
91
+ }
92
+ }
93
+
94
+ const char = text[index]!;
95
+ index += 1;
96
+ lineStart = char === "\n";
97
+ }
98
+
99
+ return { safe: text, held: "", fail: false, fence, lineStart };
100
+ }
101
+
102
+ /** Streaming DSML control-sequence filter for one text or reasoning channel. */
103
+ export class CodeBuddyScaffoldFilter {
104
+ private pending = "";
105
+ private failed = false;
106
+ private fence: "`" | "~" | null = null;
107
+ private lineStart = true;
108
+
109
+ /** True while an earlier event owns an unresolved marker or fence prefix. */
110
+ hasPending(): boolean {
111
+ return this.pending.length > 0;
112
+ }
113
+
114
+ push(chunk: string): CodeBuddyScaffoldFilterResult {
115
+ if (this.failed) {
116
+ return { releasedPending: "", text: "", pendingContinues: false, fail: false };
117
+ }
118
+ if (!chunk) {
119
+ return {
120
+ releasedPending: "",
121
+ text: "",
122
+ pendingContinues: this.hasPending(),
123
+ fail: false,
124
+ };
125
+ }
126
+
127
+ const priorPending = this.pending;
128
+ const result = scan(priorPending + chunk, this.fence, this.lineStart);
129
+ this.pending = result.held;
130
+ this.fence = result.fence;
131
+ this.lineStart = result.lineStart;
132
+ this.failed = result.fail;
133
+
134
+ const releasedLength = Math.min(priorPending.length, result.safe.length);
135
+ return {
136
+ releasedPending: result.safe.slice(0, releasedLength),
137
+ text: result.safe.slice(releasedLength),
138
+ pendingContinues: priorPending.length > 0 && result.safe.length === 0 && result.held.length > 0,
139
+ fail: result.fail,
140
+ };
141
+ }
142
+
143
+ /** Release a suffix that never completed the two-line control grammar. */
144
+ flush(): CodeBuddyScaffoldFilterResult {
145
+ if (this.failed) {
146
+ return { releasedPending: "", text: "", pendingContinues: false, fail: false };
147
+ }
148
+ const text = this.pending;
149
+ this.pending = "";
150
+ return { releasedPending: text, text: "", pendingContinues: false, fail: false };
151
+ }
152
+ }
153
+
154
+ function codeBuddyScaffoldErrorMessage(): string {
155
+ return "CodeBuddy CLI emitted vendor tool-call markup in an assistant output channel. This route"
156
+ + " runs the CLI with its own tools and MCP servers disabled and Codex owns tool control, so"
157
+ + " the turn was refused rather than forwarding or executing vendor agent scaffolding.";
158
+ }
159
+
160
+ /** Guard both streamed channels while preserving event order around withheld marker prefixes. */
161
+ export function guardCodeBuddyScaffolding(emit: (event: AdapterEvent) => void): (event: AdapterEvent) => void {
162
+ const textFilter = new CodeBuddyScaffoldFilter();
163
+ const thinkingFilter = new CodeBuddyScaffoldFilter();
164
+ type PendingChannel = "text" | "thinking";
165
+ type EventSlot = { resolved: boolean; event?: AdapterEvent };
166
+ const eventQueue: EventSlot[] = [];
167
+ const pendingSlots = new Map<PendingChannel, EventSlot>();
168
+ let closed = false;
169
+
170
+ const channelEvent = (channel: PendingChannel, text: string): AdapterEvent => channel === "text"
171
+ ? { type: "text_delta", text }
172
+ : { type: "thinking_delta", thinking: text };
173
+
174
+ const drainResolved = (): void => {
175
+ while (eventQueue[0]?.resolved) {
176
+ const slot = eventQueue.shift()!;
177
+ if (slot.event) emit(slot.event);
178
+ }
179
+ };
180
+
181
+ const enqueueResolved = (event: AdapterEvent): void => {
182
+ eventQueue.push({ resolved: true, event });
183
+ drainResolved();
184
+ };
185
+
186
+ const resolvePendingSlot = (channel: PendingChannel, text: string): void => {
187
+ const slot = pendingSlots.get(channel);
188
+ if (!slot) return;
189
+ slot.resolved = true;
190
+ if (text) slot.event = channelEvent(channel, text);
191
+ pendingSlots.delete(channel);
192
+ drainResolved();
193
+ };
194
+
195
+ const enqueuePendingSlot = (channel: PendingChannel): void => {
196
+ const slot: EventSlot = { resolved: false };
197
+ eventQueue.push(slot);
198
+ pendingSlots.set(channel, slot);
199
+ };
200
+
201
+ const flushAllPending = (): void => {
202
+ for (const channel of ["text", "thinking"] as const) {
203
+ if (!pendingSlots.has(channel)) continue;
204
+ const filter = channel === "text" ? textFilter : thinkingFilter;
205
+ resolvePendingSlot(channel, filter.flush().releasedPending);
206
+ }
207
+ drainResolved();
208
+ };
209
+
210
+ const refuse = (): void => {
211
+ if (closed) return;
212
+ flushAllPending();
213
+ closed = true;
214
+ emit({
215
+ type: "error",
216
+ message: codeBuddyScaffoldErrorMessage(),
217
+ status: 502,
218
+ errorType: "upstream_error",
219
+ code: CODEBUDDY_SCAFFOLD_ERROR_CODE,
220
+ retryable: false,
221
+ });
222
+ };
223
+
224
+ return (event: AdapterEvent): void => {
225
+ if (closed) return;
226
+ if (event.type === "text_delta" || event.type === "thinking_delta") {
227
+ const channel: PendingChannel = event.type === "text_delta" ? "text" : "thinking";
228
+ const filter = channel === "text" ? textFilter : thinkingFilter;
229
+ const hadPending = filter.hasPending();
230
+ const cleaned = filter.push(event.type === "text_delta" ? event.text : event.thinking);
231
+ if (hadPending && !cleaned.pendingContinues) resolvePendingSlot(channel, cleaned.releasedPending);
232
+ if (cleaned.text) {
233
+ enqueueResolved(event.type === "text_delta"
234
+ ? { ...event, text: cleaned.text }
235
+ : { ...event, thinking: cleaned.text });
236
+ }
237
+ if (filter.hasPending() && !cleaned.pendingContinues) enqueuePendingSlot(channel);
238
+ if (cleaned.fail) refuse();
239
+ return;
240
+ }
241
+ if (event.type === "done" || event.type === "error" || event.type === "incomplete") {
242
+ flushAllPending();
243
+ closed = true;
244
+ emit(event);
245
+ return;
246
+ }
247
+ enqueueResolved(event);
248
+ };
249
+ }
@@ -13,6 +13,8 @@ import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from
13
13
  import { identifyRoutedModel } from "./identity";
14
14
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
15
15
  import { parseDataUrl } from "./image";
16
+ import { createAdapterPhysicalSend } from "./physical-send";
17
+ import { SendBudgetExhaustedError } from "../lib/upstream-retry";
16
18
 
17
19
  // Retain the short ids emitted by the first local integration. New requests use the live catalog's
18
20
  // provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
@@ -556,7 +558,8 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
556
558
  };
557
559
  },
558
560
  async fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
559
- const response = await fetchCommandCode(request, ctx, executor);
561
+ const send = createAdapterPhysicalSend(ctx, executor);
562
+ const response = await send({ url: request.url, dispatch: physical => fetchCommandCode(request, ctx, physical) });
560
563
  if (response.ok) return response;
561
564
  const currentEffort = (() => {
562
565
  try { return (JSON.parse(request.body) as { params?: { reasoning_effort?: unknown } }).params?.reasoning_effort; } catch { return undefined; }
@@ -577,8 +580,14 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
577
580
  if (!refreshed || refreshed.includes(currentEffort)) return response;
578
581
  const retry = requestWithoutReasoningEffort(request);
579
582
  if (!retry) return response;
580
- try { void response.body?.cancel(); } catch { /* already closed */ }
581
- return fetchCommandCode(retry, ctx, executor);
583
+ try {
584
+ return await send({ url: retry.url, sendClass: "repair", recovery: "reasoning-effort-downgrade",
585
+ beforeDispatch: () => { try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ } },
586
+ dispatch: physical => fetchCommandCode(retry, ctx, physical) });
587
+ } catch (error) {
588
+ if (error instanceof SendBudgetExhaustedError) return response;
589
+ throw error;
590
+ }
582
591
  },
583
592
  async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
584
593
  let sawFinish = false;
@@ -46,6 +46,21 @@ export class CursorStreamTruncatedError extends Error {
46
46
  }
47
47
  }
48
48
 
49
+ export const CURSOR_INCOMPLETE_TOOL_CALL_MESSAGE_PREFIX =
50
+ "Cursor stream ended with incomplete tool call(s):";
51
+
52
+ /**
53
+ * True when Cursor ended the stream with a client tool still open. The adapter fail-closes
54
+ * the current turn (no partial `tool_call_start`) and remints the conversation afterwards
55
+ * so the next turn does not resume a session left waiting for `mcpResult`.
56
+ */
57
+ export function isCursorIncompleteToolCallMessage(value: unknown): boolean {
58
+ const message = typeof value === "string" ? value : errorMessage(value);
59
+ const lower = message.toLowerCase();
60
+ return lower.includes(CURSOR_INCOMPLETE_TOOL_CALL_MESSAGE_PREFIX.toLowerCase())
61
+ || lower.includes("tool call(s) left incomplete");
62
+ }
63
+
49
64
  /**
50
65
  * A cancel-shaped stream failure that WE did not request. `cancelCursorRun` is the only place
51
66
  * that cancels our own stream, and it sets `expectedClose` first, so a cancel arriving without it
@@ -24,8 +24,54 @@ const CONTEXT_272K = 272_000;
24
24
  const CONTEXT_262K = 262_144;
25
25
  const CONTEXT_256K = 256_000;
26
26
  const CONTEXT_200K = 200_000;
27
+ export const CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES = 2_048;
27
28
 
28
- export function inferCursorContextWindow(modelId: string): number {
29
+ /**
30
+ * Process-local ceilings from `ConversationTokenDetails.maxTokens` on live
31
+ * checkpoints. Each observation belongs to the Cursor identity scope that
32
+ * produced it; plan-gated accounts sharing one proxy must not overwrite each
33
+ * other's overflow prior (senpi `cursor-context-limit`).
34
+ */
35
+ const observedCursorContextWindows = new Map<string, number>();
36
+
37
+ interface CursorContextWindowOptions {
38
+ identityScope?: string;
39
+ observed?: number;
40
+ }
41
+
42
+ function normalizeObservedWindowKey(modelId: string, identityScope?: string): string {
43
+ return `${identityScope?.trim() || "local"}\0${modelId.trim().toLowerCase()}`;
44
+ }
45
+
46
+ export function recordObservedCursorContextWindow(
47
+ modelId: string,
48
+ maxTokens: number | undefined,
49
+ options: Pick<CursorContextWindowOptions, "identityScope"> = {},
50
+ ): void {
51
+ if (!modelId.trim()) return;
52
+ if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens) || maxTokens <= 0) return;
53
+ const key = normalizeObservedWindowKey(modelId, options.identityScope);
54
+ observedCursorContextWindows.delete(key);
55
+ observedCursorContextWindows.set(key, Math.floor(maxTokens));
56
+ while (observedCursorContextWindows.size > CURSOR_OBSERVED_CONTEXT_WINDOW_MAX_ENTRIES) {
57
+ const oldest = observedCursorContextWindows.keys().next().value;
58
+ if (oldest === undefined) break;
59
+ observedCursorContextWindows.delete(oldest);
60
+ }
61
+ }
62
+
63
+ export function observedCursorContextWindow(
64
+ modelId: string,
65
+ options: Pick<CursorContextWindowOptions, "identityScope"> = {},
66
+ ): number | undefined {
67
+ return observedCursorContextWindows.get(normalizeObservedWindowKey(modelId, options.identityScope));
68
+ }
69
+
70
+ export function resetObservedCursorContextWindowsForTests(): void {
71
+ observedCursorContextWindows.clear();
72
+ }
73
+
74
+ function inferCursorContextWindowHeuristic(modelId: string): number {
29
75
  const id = modelId.trim().toLowerCase();
30
76
  if (id.includes("1m")) return CONTEXT_1M;
31
77
  if (id.startsWith("gemini-")) return CONTEXT_1M;
@@ -40,6 +86,24 @@ export function inferCursorContextWindow(modelId: string): number {
40
86
  return CURSOR_DEFAULT_CONTEXT_WINDOW;
41
87
  }
42
88
 
89
+ /**
90
+ * Infer a conservative context window for a Cursor model id.
91
+ *
92
+ * A positive explicit observation wins, then an identity-scoped process-local
93
+ * checkpoint `maxTokens`, then the id heuristic. Cursor's `AvailableModelsResponse`
94
+ * does not currently include per-model context window metadata.
95
+ */
96
+ export function inferCursorContextWindow(
97
+ modelId: string,
98
+ options: CursorContextWindowOptions = {},
99
+ ): number {
100
+ const { observed } = options;
101
+ if (typeof observed === "number" && Number.isFinite(observed) && observed > 0) {
102
+ return Math.floor(observed);
103
+ }
104
+ return observedCursorContextWindow(modelId, options) ?? inferCursorContextWindowHeuristic(modelId);
105
+ }
106
+
43
107
  function normalizeInputModalities(input: string[] | undefined): string[] {
44
108
  const values = (input ?? [...CURSOR_DEFAULT_INPUT_MODALITIES])
45
109
  .map(item => item.trim())
@@ -217,7 +217,13 @@ export type RoutingCommentaryDecision =
217
217
  | { kind: "flush" }
218
218
  | { kind: "hallucination" };
219
219
 
220
- const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b/giu;
220
+ // The Korean alternative is deliberately asymmetric: it has a left boundary and no right one.
221
+ // Korean attaches particles directly to the noun, so the real sentences this detector exists to
222
+ // catch read "네이티브 셸이 차단되어..." and "네이티브 셸과 Read가...". A mirrored
223
+ // (?![\p{L}\p{M}\p{N}_]) lookahead would see the 이/과 particle as a letter and stop matching
224
+ // every one of them, which is why the negative cases below only probe the left side. Adding the
225
+ // right boundary looks like an obvious fix and disables the check; do not.
226
+ const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|(?<![\p{L}\p{M}\p{N}_])네이티브\s*(?:셸|쉘)/giu;
221
227
  const ROUTING_TOOL_HINT =
222
228
  /(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
223
229
  const ROUTING_FAILURE_CLAIM =
@@ -276,7 +282,7 @@ export class CursorRoutingCommentarySniffer {
276
282
  private matchesHallucination(): boolean {
277
283
  if (!ROUTING_FAILURE_CLAIM.test(this.buffered)) return false;
278
284
  const nativeTools = new Set(
279
- [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
285
+ [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase() ?? "shell"),
280
286
  );
281
287
  if (nativeTools.size === 0) return false;
282
288
  return ROUTING_REDIRECT_CLAIM.test(this.buffered) || nativeTools.size >= 2;
@@ -20,6 +20,7 @@ import {
20
20
  createCursorContextUsageTracker,
21
21
  createCursorProtobufEventState,
22
22
  finalizeTurnEvents,
23
+ hasBufferedTextToolCalls,
23
24
  mapCursorProtobufServerMessage,
24
25
  mapSyntheticMcpExecToToolEvents,
25
26
  reportableContextTokens,
@@ -732,6 +733,8 @@ class LiveCursorTransport implements CursorTransport {
732
733
  syntheticStructuredEditToolNames,
733
734
  translatorBudget: this.translatorBudget,
734
735
  contextUsage,
736
+ wireModelId: request.modelId,
737
+ identityScope: request._cursorIdentityScope,
735
738
  ...(prepared.estimatedInputTokens !== undefined
736
739
  ? { estimatedInputTokens: prepared.estimatedInputTokens }
737
740
  : {}),
@@ -1265,6 +1268,7 @@ class LiveCursorTransport implements CursorTransport {
1265
1268
  state.openToolCalls.size > 0
1266
1269
  || this.sawAssistantText
1267
1270
  || hasPendingClientToolFinalization
1271
+ || hasBufferedTextToolCalls(state)
1268
1272
  )
1269
1273
  ) {
1270
1274
  const terminal = hasPendingClientToolFinalization && state.openToolCalls.size === 0
@@ -1431,7 +1435,7 @@ class LiveCursorTransport implements CursorTransport {
1431
1435
  settler.settleFinish();
1432
1436
  return;
1433
1437
  }
1434
- if (this.framesReceived > 0 && this.sawAssistantText) {
1438
+ if (this.framesReceived > 0 && (this.sawAssistantText || hasBufferedTextToolCalls(state))) {
1435
1439
  for (const event of finalizeTurnEvents(state)) push(event);
1436
1440
  releaseBacklogLease();
1437
1441
  settler.settleFinish();