@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -9,6 +9,7 @@ import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
9
9
  import { mapCursorServerMessage } from "./cursor/message-mapper";
10
10
  import {
11
11
  createCursorRequest,
12
+ cursorClientThreadOwner,
12
13
  cursorCoveredPrefixDigest,
13
14
  cursorInstructionDigest,
14
15
  } from "./cursor/request-builder";
@@ -28,6 +29,16 @@ import { debugProviderDiagnostic } from "../lib/debug";
28
29
  import { estimateTokens } from "../lib/token-estimate";
29
30
  import { rememberCursorThreadConversation } from "./cursor/thread-continuity";
30
31
  import { runCursorTurnWithRetry } from "./cursor/transport-retry";
32
+ import { cursorRequestHasShellAlias, cursorRequestUsesCodeMode } from "./cursor/tool-definitions";
33
+ import {
34
+ CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
35
+ CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
36
+ CursorEnvelopeEchoSniffer,
37
+ CursorMidstreamEchoObserver,
38
+ CursorRoutingCommentaryError,
39
+ CursorRoutingCommentarySniffer,
40
+ CursorToolResultEchoError,
41
+ } from "./cursor/envelope-echo";
31
42
  import {
32
43
  createDisabledCursorTransport,
33
44
  CursorTransportDisabledError,
@@ -145,15 +156,36 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
145
156
  let completedNormally = false;
146
157
  let lastTransport: { captured?: Uint8Array } | undefined;
147
158
  let emittedClientTool = false;
159
+ // Ordering proof for tool-suspended checkpoints: true only when the newest captured
160
+ // checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream
161
+ // serialized its suspended-on-tool-call state. Only that snapshot can safely resume
162
+ // with the covered-prefix + trailing-toolResult path (devlog 260826 050).
163
+ let capturedAfterClientTool = false;
148
164
 
149
165
  const commitCapturedCheckpoint = (activeRequest: ReturnType<typeof createCursorRequest>): void => {
166
+ const toolSuspendedCommit =
167
+ emittedClientTool
168
+ && capturedAfterClientTool
169
+ && isCursorExternalWireModel(activeRequest.modelId);
150
170
  if (
151
171
  replayUnsafe
152
- || emittedClientTool
172
+ || (emittedClientTool && !toolSuspendedCommit)
153
173
  || activeRequest.contextUsageStoreCheckpoints === false
154
174
  || !lastTransport?.captured
155
175
  || lastTransport.captured.byteLength === 0
156
- ) return;
176
+ ) {
177
+ // Refusal diagnostics (devlog 260826 050/080): name the exact guard so a live
178
+ // missing_ref chain can be attributed without instrumented rebuilds.
179
+ debugProviderDiagnostic("cursor", "checkpoint-commit-refused", {
180
+ replayUnsafe,
181
+ emittedClientTool,
182
+ capturedAfterClientTool,
183
+ externalModel: isCursorExternalWireModel(activeRequest.modelId),
184
+ storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false,
185
+ capturedBytes: lastTransport?.captured?.byteLength ?? 0,
186
+ });
187
+ return;
188
+ }
157
189
  const previousRef = _parsed._providerContinuation?.cursor?.checkpointRef;
158
190
  const coveredMessageCount = _parsed.context.messages.length;
159
191
  const checkpointRef = commitCursorCheckpoint({
@@ -172,7 +204,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
172
204
  cursor: {
173
205
  ...(_parsed._providerContinuation?.cursor ?? {}),
174
206
  conversationId: activeRequest.conversationId,
175
- checkpointUsable: true,
207
+ // A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
208
+ // continuation; the request-builder guard keys on checkpointUsable=false for that.
209
+ checkpointUsable: !toolSuspendedCommit,
176
210
  checkpointRef,
177
211
  },
178
212
  };
@@ -182,10 +216,52 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
182
216
  checkpointRefHash: cursorCheckpointRefHash(checkpointRef),
183
217
  checkpointBytes: lastTransport.captured.byteLength,
184
218
  wireModel: activeRequest.modelId,
219
+ ...(toolSuspendedCommit ? { toolSuspended: true } : {}),
185
220
  });
186
221
  };
187
222
 
188
223
  const runOnce = async (activeRequest: ReturnType<typeof createCursorRequest>) => {
224
+ // Envelope echo quarantine (devlog 260826 gap-10): external full-replay continuations
225
+ // whose trailing input is a tool result sometimes ECHO the replayed "[Tool Result]"
226
+ // envelope as assistant text (kimi-k3 ~30-40% of multi-round probes). Hold the first
227
+ // text deltas until they provably diverge from the markers; a completed marker turns
228
+ // the turn into a retryable semantic failure BEFORE any client-visible delta escapes.
229
+ // Armed for ANY external turn whose replayed history contains a tool result — echo
230
+ // priming was observed live on user-action rounds too (the envelope lives in the
231
+ // flattened history regardless of which role ends the input).
232
+ const armEchoSniffer =
233
+ isCursorExternalWireModel(activeRequest.modelId)
234
+ && (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
235
+ const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
236
+ // Mid-stream observer (devlog 260828 F1/F2): diagnostic-only; armed with the
237
+ // prefix sniffer because both fire on flattened tool-result replay priming.
238
+ const midstreamObserver = armEchoSniffer ? new CursorMidstreamEchoObserver() : undefined;
239
+ const armRoutingCommentarySniffer =
240
+ isCursorExternalWireModel(activeRequest.modelId)
241
+ && (
242
+ cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
243
+ || cursorRequestHasShellAlias(activeRequest.tools)
244
+ );
245
+ const routingCommentarySniffer = armRoutingCommentarySniffer
246
+ ? new CursorRoutingCommentarySniffer()
247
+ : undefined;
248
+ let guardHeld: AdapterEvent[] = [];
249
+ // Exactly-once observation: every client-bound text delta passes through here
250
+ // exactly once — held deltas only on release, ordinary deltas at emit time.
251
+ const emitTextObserved = (event: AdapterEvent): void => {
252
+ if (event.type === "text_delta") midstreamObserver?.feed(event.text);
253
+ emit(event);
254
+ };
255
+ const releaseGuardHeld = () => {
256
+ for (const held of guardHeld) {
257
+ if (held.type !== "heartbeat") emittedOutput = true;
258
+ emitTextObserved(held);
259
+ }
260
+ guardHeld = [];
261
+ };
262
+ const guardsSettled = () =>
263
+ (!echoSniffer || echoSniffer.settled)
264
+ && (!routingCommentarySniffer || routingCommentarySniffer.settled);
189
265
  await runCursorTurnWithRetry(
190
266
  makeTransport,
191
267
  {
@@ -207,16 +283,65 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
207
283
  if (message.type === "done") completedNormally = true;
208
284
  if (message.type === "tool_call_end") emittedClientTool = true;
209
285
  const captured = capturedCursorCheckpointBytes(activeTransport);
210
- if (captured) lastTransport = { captured };
211
- const events = mapCursorServerMessage(message, {
212
- kv,
213
- writeClient: clientMessage => {
214
- void activeTransport.writeClient(clientMessage);
215
- },
216
- });
217
- for (const event of events) {
286
+ if (captured) {
287
+ if (captured !== lastTransport?.captured) capturedAfterClientTool = emittedClientTool;
288
+ lastTransport = { captured };
289
+ }
290
+ const events = mapCursorServerMessage(message, {
291
+ kv,
292
+ writeClient: clientMessage => {
293
+ void activeTransport.writeClient(clientMessage);
294
+ },
295
+ });
296
+ for (const event of events) {
297
+ if (!guardsSettled()) {
298
+ if (event.type === "text_delta") {
299
+ guardHeld.push(event);
300
+ if (echoSniffer && !echoSniffer.settled) {
301
+ const decision = echoSniffer.feed(event.text);
302
+ if (decision.kind === "echo") {
303
+ guardHeld = [];
304
+ throw new CursorToolResultEchoError(decision.marker);
305
+ }
306
+ }
307
+ if (routingCommentarySniffer && !routingCommentarySniffer.settled) {
308
+ const decision = routingCommentarySniffer.feed(event.text);
309
+ if (decision.kind === "hallucination") {
310
+ guardHeld = [];
311
+ throw new CursorRoutingCommentaryError();
312
+ }
313
+ }
314
+ if (guardsSettled()) releaseGuardHeld();
315
+ continue;
316
+ } else if (event.type === "thinking_delta" || event.type === "heartbeat") {
317
+ // Reasoning before first text stays ordered; liveness still passes through.
318
+ if (event.type === "thinking_delta") {
319
+ guardHeld.push(event);
320
+ continue;
321
+ }
322
+ } else {
323
+ // A tool call, done, or error closes the text quarantine. Re-check the full
324
+ // held first line before release so a fragmented routing claim cannot leak.
325
+ echoSniffer?.finish();
326
+ const routingDecision = routingCommentarySniffer?.finish();
327
+ if (routingDecision?.kind === "hallucination") {
328
+ guardHeld = [];
329
+ throw new CursorRoutingCommentaryError();
330
+ }
331
+ releaseGuardHeld();
332
+ }
333
+ }
218
334
  if (event.type !== "heartbeat") emittedOutput = true;
219
335
  if (event.type === "done") {
336
+ for (const finding of midstreamObserver?.findings() ?? []) {
337
+ debugProviderDiagnostic("cursor", "midstream-envelope-echo", {
338
+ wireModel: activeRequest.modelId,
339
+ conversationHash: activeRequest.conversationId.slice(0, 16),
340
+ marker: finding.marker,
341
+ offset: finding.offset,
342
+ callIdCorrupt: finding.callIdCorrupt,
343
+ });
344
+ }
220
345
  commitCapturedCheckpoint(activeRequest);
221
346
  const inheritedCursor = _parsed._providerContinuation?.cursor;
222
347
  const isolatedOrCompaction =
@@ -236,7 +361,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
236
361
  : undefined;
237
362
  emit(providerState ? { ...event, providerState } : event);
238
363
  } else {
239
- emit(event);
364
+ emitTextObserved(event);
240
365
  }
241
366
  }
242
367
  },
@@ -246,36 +371,82 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
246
371
  try {
247
372
  await runOnce(request);
248
373
  } catch (err) {
249
- // One-shot fallback for external-model Connect invalid_argument before any
250
- // non-heartbeat output. Retries apply only to safe plain-user turns; tool-result
251
- // resumes, local exec/MCP side effects, and already-emitted output fail closed.
374
+ const outputGuardRetryText =
375
+ err instanceof CursorToolResultEchoError
376
+ ? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
377
+ : err instanceof CursorRoutingCommentaryError
378
+ ? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
379
+ : undefined;
380
+ // One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
381
+ // The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
382
+ // retry is safe. A second rejection propagates as an error rather than looping.
252
383
  if (
253
- !isCursorInvalidArgumentError(err)
254
- || !isCursorExternalWireModel(request.modelId)
255
- || lastRawIsToolResult
256
- || emittedOutput
257
- || replayUnsafe
258
- || incoming.abortSignal?.aborted
384
+ outputGuardRetryText
385
+ && !emittedOutput
386
+ && !replayUnsafe
387
+ && !incoming.abortSignal?.aborted
259
388
  ) {
260
- throw err;
261
- }
262
- const failedConversationId = request.conversationId;
263
- lastTransport = undefined;
264
- _parsed._cursorConversationId = undefined;
265
- request = createCursorRequest(_parsed, { forceFreshConversation: true });
266
- rekeyContextUsage(failedConversationId, request.conversationId);
267
- _parsed._cursorConversationId = request.conversationId;
268
- // Persist recovery for store:false clients that only send a parent thread id, so the
269
- // next turn does not recompute the stale deterministic thread hash. Isolated helper /
270
- // compaction turns must not park their throwaway id under the parent thread key.
271
- if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
272
- rememberCursorThreadConversation(
273
- _parsed._clientThreadId,
274
- request.conversationId,
275
- _parsed._cursorIdentityScope,
389
+ debugProviderDiagnostic(
390
+ "cursor",
391
+ err instanceof CursorToolResultEchoError
392
+ ? "envelope-echo-retry"
393
+ : "routing-commentary-retry",
394
+ {
395
+ wireModel: request.modelId,
396
+ conversationHash: request.conversationId.slice(0, 16),
397
+ },
276
398
  );
399
+ const echoedConversationId = request.conversationId;
400
+ lastTransport = undefined;
401
+ _parsed._cursorConversationId = undefined;
402
+ request = {
403
+ ...createCursorRequest(_parsed, { forceFreshConversation: true }),
404
+ echoRetryContinuationText: outputGuardRetryText,
405
+ };
406
+ rekeyContextUsage(echoedConversationId, request.conversationId);
407
+ _parsed._cursorConversationId = request.conversationId;
408
+ const echoThreadOwner = cursorClientThreadOwner(_parsed);
409
+ if (echoThreadOwner && _parsed._cursorIsolateConversation !== true) {
410
+ rememberCursorThreadConversation(
411
+ echoThreadOwner,
412
+ request.conversationId,
413
+ _parsed._cursorIdentityScope,
414
+ );
415
+ }
416
+ await runOnce(request);
417
+ } else {
418
+ // One-shot fallback for external-model Connect invalid_argument before any
419
+ // non-heartbeat output. Retries apply only to safe plain-user turns; tool-result
420
+ // resumes, local exec/MCP side effects, and already-emitted output fail closed.
421
+ if (
422
+ !isCursorInvalidArgumentError(err)
423
+ || !isCursorExternalWireModel(request.modelId)
424
+ || lastRawIsToolResult
425
+ || emittedOutput
426
+ || replayUnsafe
427
+ || incoming.abortSignal?.aborted
428
+ ) {
429
+ throw err;
430
+ }
431
+ const failedConversationId = request.conversationId;
432
+ lastTransport = undefined;
433
+ _parsed._cursorConversationId = undefined;
434
+ request = createCursorRequest(_parsed, { forceFreshConversation: true });
435
+ rekeyContextUsage(failedConversationId, request.conversationId);
436
+ _parsed._cursorConversationId = request.conversationId;
437
+ // Persist recovery for store:false clients that send any stable Cursor thread owner, so
438
+ // the next turn does not recompute the stale deterministic thread hash. Isolated helper /
439
+ // compaction turns must not park their throwaway id under the parent or Desktop owner.
440
+ const threadOwner = cursorClientThreadOwner(_parsed);
441
+ if (threadOwner && _parsed._cursorIsolateConversation !== true) {
442
+ rememberCursorThreadConversation(
443
+ threadOwner,
444
+ request.conversationId,
445
+ _parsed._cursorIdentityScope,
446
+ );
447
+ }
448
+ await runOnce(request);
277
449
  }
278
- await runOnce(request);
279
450
  }
280
451
  if (
281
452
  request.checkpointInvalidationReason
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Provider-neutral empty-exec-output normalization.
3
+ *
4
+ * A code-mode `exec` cell that never calls `text()`/`notify()` returns nothing: the last
5
+ * expression value is NOT echoed automatically. The routed model reads a blank tool result,
6
+ * concludes its earlier output was lost, and burns turns re-running the same call or restarting
7
+ * the task from scratch. Naming that state explicitly is what breaks the loop.
8
+ *
9
+ * This module owns the shared detection so every adapter reports the same thing. Cursor keeps its
10
+ * own wrapper (`normalizeCursorToolResultText`) for Computer Use precedence and isError policy;
11
+ * Kiro consumes this helper directly.
12
+ */
13
+
14
+ /**
15
+ * Matches exec wrappers whose only payload is an empty-output marker.
16
+ *
17
+ * `Script failed` is deliberately NOT in this set. A failed cell with no captured output is still
18
+ * a FAILURE, and the success guidance below ("not a blocked tool", "do not re-run") would erase the
19
+ * only signal that anything went wrong — reachable through Responses history, where
20
+ * `function_call_output` is parsed with `isError: false`. Cursor keeps its own broader regex for
21
+ * Computer Use, where a failed wrapper is separately marked `isError`.
22
+ */
23
+ export const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
24
+
25
+ /** Wrapper for a cell that FAILED without emitting output: empty, but not a success. */
26
+ export const FAILED_EXEC_OUTPUT_REGEX = /^Script failed[^\n]*\n*(?:Wall time[^\n]*\n*)?(?:Output:\s*)?(?:<empty>)?\s*$/;
27
+
28
+ /** Guidance for a failed cell whose output was empty: the failure must survive normalization. */
29
+ export const FAILED_EXEC_OUTPUT_MESSAGE =
30
+ "[exec failed with no captured output: the cell raised before emitting anything. This is a real failure, not an empty success — inspect the call for a thrown error or syntax problem before retrying.]";
31
+
32
+ /**
33
+ * The guidance itself. Worded to close all three wrong conclusions a model draws from a blank
34
+ * result: that context was lost, that the tool is blocked, and that retrying will differ.
35
+ */
36
+ export const EMPTY_EXEC_OUTPUT_MESSAGE =
37
+ "[empty output: the exec cell completed but emitted nothing. This is NOT lost context and NOT a blocked tool — in code mode call text(...) or notify(...) on any value you need to see (a bare await tools.exec_command(...) is not echoed automatically); in shell mode the command simply printed nothing. Do not re-run the same call expecting different output.]";
38
+
39
+ /**
40
+ * The SAME rule stated BEFORE the first call, for the code-mode tool-catalog nudge.
41
+ *
42
+ * `EMPTY_EXEC_OUTPUT_MESSAGE` above is a repair: it fires only after a model has already spent a
43
+ * call and read a blank result. That recovers the turn but cannot prevent the wasted round trip,
44
+ * and the model still has to guess whether its command failed or its output was merely dropped.
45
+ * Stating the echo rule up front removes the failure instead of explaining it afterwards.
46
+ *
47
+ * Kept beside the recovery text on purpose: the two are one pair guarding one defect, and wording
48
+ * that drifts apart is how a model gets told two different things about the same isolate.
49
+ */
50
+ export const CODE_MODE_RESULT_ECHO_SENTENCE =
51
+ "Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: \"ls\"})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context.";
52
+
53
+ /**
54
+ * Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
55
+ * here is almost always a code-mode cell that never called text()/notify().
56
+ */
57
+ export function isCodexExecBridgeTool(toolName?: string, toolNamespace?: string): boolean {
58
+ if (toolNamespace && toolNamespace.includes("opencodex-responses")) return true;
59
+ if (!toolName) return false;
60
+ const lower = toolName.toLowerCase();
61
+ return (
62
+ lower === "exec"
63
+ || lower === "exec_command"
64
+ || lower === "shell_command"
65
+ // Codex CLI/desktop native tool names: the multi-round "이전 출력이 비어 있어 처음부터"
66
+ // restart loop reproduced via codex exec because `shell` was not in this set
67
+ // (devlog 260826 gap-8 QA round 2).
68
+ || lower === "shell"
69
+ || lower === "local_shell"
70
+ || lower === "container.exec"
71
+ || lower.startsWith("mcp_opencodex-responses_")
72
+ || lower.startsWith("mcp__opencodex-responses__")
73
+ );
74
+ }
75
+
76
+ /** True when this result is an exec-bridge call that produced no usable output. */
77
+ export function isEmptyExecToolResult(
78
+ text: string,
79
+ options: { toolName?: string; toolNamespace?: string } = {},
80
+ ): boolean {
81
+ return isCodexExecBridgeTool(options.toolName, options.toolNamespace)
82
+ && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim());
83
+ }
84
+
85
+ /**
86
+ * Returns the guidance text when this is an empty exec-bridge result, else `undefined` so the
87
+ * caller keeps its own fallback. Undefined rather than the original text: an adapter must be able
88
+ * to tell "not my case" from "normalized to the same string".
89
+ */
90
+ export function normalizeEmptyExecToolResultText(
91
+ text: string,
92
+ options: { toolName?: string; toolNamespace?: string } = {},
93
+ ): string | undefined {
94
+ if (!isCodexExecBridgeTool(options.toolName, options.toolNamespace)) return undefined;
95
+ const trimmed = text.trim();
96
+ // Failure first: a failed wrapper must never be described as an empty success.
97
+ if (FAILED_EXEC_OUTPUT_REGEX.test(trimmed)) return FAILED_EXEC_OUTPUT_MESSAGE;
98
+ return EMPTY_EXEC_OUTPUT_REGEX.test(trimmed) ? EMPTY_EXEC_OUTPUT_MESSAGE : undefined;
99
+ }