@bitkyc08/opencodex 2.59.0 → 2.60.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 (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -0,0 +1,279 @@
1
+ // Bounded classification of a PARTIAL freeform tool-call wrapper.
2
+ //
3
+ // `unwrapFreeformToolInput` decides the completed input with `JSON.parse`, which does not care
4
+ // what order an object's properties arrive in or how their names are spelled. The streaming side
5
+ // has to reach the same answer from a prefix, and it used to do that by comparing the buffer
6
+ // against the literal `{"input":"`. Spellings `JSON.parse` calls identical therefore matched no
7
+ // wrapper at all, streamed as raw JSON, and then completed as the unwrapped body:
8
+ // `{"metadata":1,"input":"cmd"}` and `{"\u0069nput":"cmd"}` both previewed the whole object and
9
+ // completed as `cmd` (#5151). #5047 and #5129 closed the compact and whitespace spellings of the
10
+ // same disagreement; reordering and escaping are the two the literal matcher could never see,
11
+ // because RFC 8259 objects are unordered and their names are strings with escapes.
12
+ //
13
+ // So the prefix is scanned as JSON instead of matched as text. The scan answers one question —
14
+ // which wrapper, if any, the completed text will unwrap to — and it answers it three ways:
15
+ //
16
+ // `input` the canonical key is present with a string value, whatever preceded it and however
17
+ // its name was spelled. Its value can be decoded progressively, because
18
+ // `unwrapFreeformToolInput` gives an own `input` precedence over every other property.
19
+ // `raw` no wrapper can apply, because the text is not an object or because it is one that
20
+ // `JSON.parse` will reject. Completion returns the buffer, so streaming it agrees.
21
+ // `hold` undecided. A key that has not arrived yet can still change the answer, so nothing
22
+ // is published until the object parses and completion's own rule decides.
23
+ //
24
+ // The bound matters as much as the classification. A scan that walks the whole buffer on every
25
+ // delta is quadratic in the argument size, so classification gives up after
26
+ // `MAX_FREEFORM_WRAPPER_SCAN_CHARS` and holds. Giving up costs preview, never agreement: a held
27
+ // buffer is still resolved by the authoritative parse at completion.
28
+
29
+ /** The insignificant whitespace `JSON.parse` accepts between tokens. */
30
+ export const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
31
+
32
+ /** The two-character escapes JSON defines, and nothing else. */
33
+ export const JSON_ESCAPES = new Map<string, string>([
34
+ ['"', '"'], ["\\", "\\"], ["/", "/"],
35
+ ["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
36
+ ]);
37
+
38
+ /**
39
+ * How far a classification scan walks before it gives up and holds.
40
+ *
41
+ * A wrapper's own structure — its property names and the small scalar siblings a model puts
42
+ * beside the body — lives at the front of the object. A buffer still undecided after this much
43
+ * scanning is one whose preceding values are large, and holding it is what the routed
44
+ * restoration path already does for every unrecognized object.
45
+ */
46
+ export const MAX_FREEFORM_WRAPPER_SCAN_CHARS = 4096;
47
+
48
+ const CANONICAL_KEY = "input";
49
+
50
+ /** The buffer ends inside a token: what follows can still change the answer. */
51
+ const HOLD = -1;
52
+ /** `JSON.parse` will reject this text no matter what is appended to it. */
53
+ const NEVER = -2;
54
+
55
+ export type FreeformWrapperScan =
56
+ | { kind: "hold"; parse: boolean }
57
+ | { kind: "raw" }
58
+ | { kind: "input"; valueStart: number };
59
+
60
+ /** Index of the first non-whitespace character at or after `from`, or HOLD past the end. */
61
+ function skipWhitespace(text: string, from: number): number {
62
+ let i = from;
63
+ while (i < text.length && JSON_WHITESPACE.has(text[i]!)) i++;
64
+ return i < text.length ? i : HOLD;
65
+ }
66
+
67
+ /**
68
+ * Index just past the complete JSON string opening at `from`, or HOLD / NEVER.
69
+ *
70
+ * This reports a BOUNDARY where `decodeJsonStringPrefix` reports a decoded PREFIX: one has to
71
+ * fail on a truncated string and the other has to return what it decoded so far. They answer
72
+ * different questions about the same bytes and share one escape table so they cannot disagree
73
+ * about which escapes exist.
74
+ */
75
+ function scanString(text: string, from: number): number {
76
+ let i = from + 1;
77
+ while (i < text.length) {
78
+ const c = text[i]!;
79
+ if (c === '"') return i + 1;
80
+ if (c === "\\") {
81
+ const n = text[i + 1];
82
+ if (n === undefined) return HOLD;
83
+ if (n === "u") {
84
+ const hex = text.slice(i + 2, i + 6);
85
+ if (hex.length < 4) return HOLD;
86
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return NEVER;
87
+ i += 6;
88
+ continue;
89
+ }
90
+ if (!JSON_ESCAPES.has(n)) return NEVER;
91
+ i += 2;
92
+ continue;
93
+ }
94
+ // A literal control character is not legal inside a JSON string.
95
+ if (c.charCodeAt(0) <= 0x1f) return NEVER;
96
+ i++;
97
+ }
98
+ return HOLD;
99
+ }
100
+
101
+ const JSON_NUMBER_CHARS = /[-+0-9.eE]/;
102
+ const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
103
+
104
+ /** Index just past one complete scalar at `from`, or HOLD / NEVER. */
105
+ function scanScalar(text: string, from: number): number {
106
+ const c = text[from]!;
107
+ if (c === '"') return scanString(text, from);
108
+ if (c === "t" || c === "f" || c === "n") {
109
+ const literal = c === "t" ? "true" : c === "f" ? "false" : "null";
110
+ if (text.startsWith(literal, from)) return from + literal.length;
111
+ return literal.startsWith(text.slice(from)) ? HOLD : NEVER;
112
+ }
113
+ let end = from;
114
+ while (end < text.length && JSON_NUMBER_CHARS.test(text[end]!)) end++;
115
+ if (end === from) return NEVER;
116
+ // A number running to the end of the buffer can still grow another digit or exponent.
117
+ if (end >= text.length) return HOLD;
118
+ return JSON_NUMBER.test(text.slice(from, end)) ? end : NEVER;
119
+ }
120
+
121
+ /** Index where the value of the member whose name opens at `from` begins, or HOLD / NEVER. */
122
+ function scanMemberKey(text: string, from: number): number {
123
+ if (text[from] !== '"') return NEVER;
124
+ const nameEnd = scanString(text, from);
125
+ if (nameEnd < 0) return nameEnd;
126
+ const colon = skipWhitespace(text, nameEnd);
127
+ if (colon === HOLD) return HOLD;
128
+ if (text[colon] !== ":") return NEVER;
129
+ return skipWhitespace(text, colon + 1);
130
+ }
131
+
132
+ /**
133
+ * Index just past one complete JSON value at `from`, or HOLD / NEVER.
134
+ *
135
+ * Containers are walked with an explicit stack rather than recursion: the value being skipped
136
+ * is provider-controlled, and a deeply nested one must not be able to exhaust the call stack.
137
+ */
138
+ function scanValue(text: string, from: number): number {
139
+ const closers: string[] = [];
140
+ let i = from;
141
+ for (;;) {
142
+ const c = text[i]!;
143
+ if (c === "{" || c === "[") {
144
+ const closer = c === "{" ? "}" : "]";
145
+ closers.push(closer);
146
+ const first = skipWhitespace(text, i + 1);
147
+ if (first === HOLD) return HOLD;
148
+ if (text[first] === closer) {
149
+ closers.pop();
150
+ i = first + 1;
151
+ } else if (closer === "}") {
152
+ const value = scanMemberKey(text, first);
153
+ if (value < 0) return value;
154
+ i = value;
155
+ continue;
156
+ } else {
157
+ i = first;
158
+ continue;
159
+ }
160
+ } else {
161
+ const end = scanScalar(text, i);
162
+ if (end < 0) return end;
163
+ i = end;
164
+ }
165
+
166
+ // One value is complete: close whatever it finished and find the next value, if any.
167
+ for (;;) {
168
+ if (closers.length === 0) return i;
169
+ const at = skipWhitespace(text, i);
170
+ if (at === HOLD) return HOLD;
171
+ const closer = closers[closers.length - 1]!;
172
+ if (text[at] === closer) {
173
+ closers.pop();
174
+ i = at + 1;
175
+ continue;
176
+ }
177
+ if (text[at] !== ",") return NEVER;
178
+ const next = skipWhitespace(text, at + 1);
179
+ if (next === HOLD) return HOLD;
180
+ if (closer === "}") {
181
+ const value = scanMemberKey(text, next);
182
+ if (value < 0) return value;
183
+ i = value;
184
+ } else {
185
+ i = next;
186
+ }
187
+ break;
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Which wrapper the completed text will unwrap to, as far as this prefix can say.
194
+ *
195
+ * Fallback keys are deliberately not recognized here. They only unwrap when exactly one of them
196
+ * carries a string, and a second one can still arrive, so no prefix decides them — which makes
197
+ * them indistinguishable from any other undecided object and lets one HOLD cover both.
198
+ */
199
+ export function scanFreeformWrapper(text: string): FreeformWrapperScan {
200
+ // One clamp rather than a budget threaded through every helper. Every helper already holds
201
+ // when it runs off the end of what it can see, so a buffer whose classification needs more
202
+ // than this holds for exactly the right reason, and no scan can cost more than this many
203
+ // characters however large the arguments grow. Indices into the clamp are indices into the
204
+ // full text, because the clamp is a prefix of it.
205
+ const bounded = text.length > MAX_FREEFORM_WRAPPER_SCAN_CHARS
206
+ ? text.slice(0, MAX_FREEFORM_WRAPPER_SCAN_CHARS)
207
+ : text;
208
+ // `parse` is true only where this scan actually SAW the object close. Every other hold ran
209
+ // out of buffer or out of budget, and in both cases asking `JSON.parse` is work with no
210
+ // possible payoff: the first is provably incomplete, and the second would re-read a growing
211
+ // buffer on every delta that happens to end in a brace — repeated braces inside a long
212
+ // unterminated string are enough to make that quadratic. Holding a budget-exhausted prefix
213
+ // costs nothing that matters, because the value it would release arrives in the same instant
214
+ // as the authoritative completion that follows it.
215
+ const hold = (): FreeformWrapperScan => ({ kind: "hold", parse: false });
216
+ const open = skipWhitespace(bounded, 0);
217
+ if (open === HOLD) return hold();
218
+ // Not an object, so no wrapper rule reaches it: arrays, scalars and ordinary bodies all
219
+ // complete as themselves.
220
+ if (bounded[open] !== "{") return { kind: "raw" };
221
+
222
+ let i = open + 1;
223
+ for (;;) {
224
+ const at = skipWhitespace(bounded, i);
225
+ if (at === HOLD) return hold();
226
+ if (bounded[at] === "}") return afterTopLevelClose(bounded, at + 1);
227
+ if (bounded[at] !== '"') return { kind: "raw" };
228
+
229
+ const nameEnd = scanString(bounded, at);
230
+ if (nameEnd === HOLD) return hold();
231
+ if (nameEnd === NEVER) return { kind: "raw" };
232
+ let name: string;
233
+ try {
234
+ // The authoritative decoder for the name, so `{"\u0069nput":...}` resolves to the same
235
+ // key completion sees. Decoding it by hand here is the second implementation that would
236
+ // drift from `JSON.parse`, which is how this defect existed in the first place.
237
+ name = JSON.parse(bounded.slice(at, nameEnd)) as string;
238
+ } catch {
239
+ return { kind: "raw" };
240
+ }
241
+
242
+ const colon = skipWhitespace(bounded, nameEnd);
243
+ if (colon === HOLD) return hold();
244
+ if (bounded[colon] !== ":") return { kind: "raw" };
245
+ const valueAt = skipWhitespace(bounded, colon + 1);
246
+ if (valueAt === HOLD) return hold();
247
+
248
+ if (name === CANONICAL_KEY) {
249
+ // An own `input` wins over everything else in the object, so this is decidable now. A
250
+ // non-string value is decidable too, in the other direction: completion hands back the
251
+ // argument text unchanged rather than unwrapping a value it cannot use.
252
+ return bounded[valueAt] === '"'
253
+ ? { kind: "input", valueStart: valueAt + 1 }
254
+ : { kind: "raw" };
255
+ }
256
+
257
+ const valueEnd = scanValue(bounded, valueAt);
258
+ if (valueEnd === HOLD) return hold();
259
+ if (valueEnd === NEVER) return { kind: "raw" };
260
+
261
+ const next = skipWhitespace(bounded, valueEnd);
262
+ if (next === HOLD) return hold();
263
+ if (bounded[next] === ",") {
264
+ i = next + 1;
265
+ continue;
266
+ }
267
+ if (bounded[next] === "}") return afterTopLevelClose(bounded, next + 1);
268
+ return { kind: "raw" };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * The object closed without a canonical key. Only whitespace may follow one that parses, so
274
+ * anything else makes the text raw; otherwise the completed parse decides between a fallback
275
+ * wrapper and no wrapper at all.
276
+ */
277
+ function afterTopLevelClose(text: string, from: number): FreeformWrapperScan {
278
+ return skipWhitespace(text, from) === HOLD ? { kind: "hold", parse: true } : { kind: "raw" };
279
+ }
@@ -0,0 +1,134 @@
1
+ import { collectResponsesToolGroups } from "./tool-groups";
2
+ import { isSchemaValidResponsesToolName, wireToolInnerName } from "./tool-name-aliases";
3
+ import { CODE_MODE_HELPER_WIRE_NAMES, dottedToolName, namespacedToolName } from "../types";
4
+
5
+ /**
6
+ * The one invented namespace this proxy is known to have emitted. Routed providers flatten a
7
+ * namespaced call as `ns.name`, and a provider that invents the grouping picks `default`; #3402,
8
+ * #4176 and #4412 all repaired that spelling on the way IN, each on its own path.
9
+ *
10
+ * This is deliberately one literal prefix rather than a rule. "Strip everything before the first
11
+ * dot" would silently rewrite a legitimate tool name that happens to contain a dot in some other
12
+ * provider's vocabulary, and a replayed history item is the worst possible place to guess: it names
13
+ * a call that already happened.
14
+ */
15
+ const LEGACY_INVENTED_NAMESPACE_PREFIX = "default.";
16
+
17
+ /** Item types whose `name` the upstream validates against the tool-name pattern. */
18
+ const NAMED_CALL_ITEM_TYPES = new Set(["function_call", "custom_tool_call"]);
19
+
20
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
21
+ return !!value && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+
24
+ type DeclarationView = {
25
+ /** Wire spellings the caller's own catalog declares, including flattened namespace forms. */
26
+ readonly spellings: ReadonlySet<string>;
27
+ /** How many declared identities answer to a given inner name. */
28
+ readonly ownersByInnerName: ReadonlyMap<string, number>;
29
+ };
30
+
31
+ /**
32
+ * Read the request's own catalog. Namespace children contribute their inner name and both
33
+ * flattened spellings, which is what makes a declared `default` namespace distinguishable from an
34
+ * invented one.
35
+ */
36
+ function readDeclarations(body: unknown): DeclarationView {
37
+ const spellings = new Set<string>();
38
+ const owners = new Map<string, number>();
39
+ const identities = new Map<string, Set<string>>();
40
+ const claim = (innerName: string, identity: string): void => {
41
+ const seen = identities.get(innerName);
42
+ if (seen === undefined) identities.set(innerName, new Set([identity]));
43
+ else seen.add(identity);
44
+ };
45
+ for (const group of collectResponsesToolGroups(body)) {
46
+ for (const spec of group) {
47
+ if (!isPlainObject(spec)) continue;
48
+ if (spec.type === "namespace" && Array.isArray(spec.tools)) {
49
+ const namespace = typeof spec.name === "string" ? spec.name : undefined;
50
+ if (namespace === undefined) continue;
51
+ for (const child of spec.tools) {
52
+ const inner = wireToolInnerName(child);
53
+ if (!inner) continue;
54
+ spellings.add(namespacedToolName(namespace, inner));
55
+ spellings.add(dottedToolName(namespace, inner));
56
+ claim(inner, JSON.stringify([namespace, inner]));
57
+ }
58
+ continue;
59
+ }
60
+ const name = wireToolInnerName(spec);
61
+ if (!name) continue;
62
+ spellings.add(name);
63
+ claim(name, JSON.stringify([null, name]));
64
+ }
65
+ }
66
+ for (const [inner, owning] of identities) owners.set(inner, owning.size);
67
+ return { spellings, ownersByInnerName: owners };
68
+ }
69
+
70
+ /**
71
+ * Canonical name a legacy dotted alias should be replayed under, or undefined to leave it alone.
72
+ *
73
+ * Fail-closed in both directions that matter. A dotted spelling the caller's catalog actually
74
+ * declares is a real tool identity, so it is never stripped — rewriting it would change which tool
75
+ * the history says was called. A suffix claimed by two declared identities is ambiguous and is
76
+ * left as it is. Only two things resolve: a suffix that names exactly one declared tool, and the
77
+ * bounded set of code-mode helper spellings, which a code-mode catalog never declares because they
78
+ * exist only as nested `tools.*` helpers inside `exec`.
79
+ */
80
+ function repairedCallName(name: string, declarations: DeclarationView): string | undefined {
81
+ if (isSchemaValidResponsesToolName(name)) return undefined;
82
+ if (!name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)) return undefined;
83
+ if (declarations.spellings.has(name)) return undefined;
84
+ const suffix = name.slice(LEGACY_INVENTED_NAMESPACE_PREFIX.length);
85
+ if (!isSchemaValidResponsesToolName(suffix)) return undefined;
86
+ const owners = declarations.ownersByInnerName.get(suffix);
87
+ if (owners === 1) return suffix;
88
+ if (owners !== undefined) return undefined;
89
+ return CODE_MODE_HELPER_WIRE_NAMES.has(suffix) ? suffix : undefined;
90
+ }
91
+
92
+ function repairedItem(item: unknown, declarations: DeclarationView): unknown {
93
+ if (!isPlainObject(item)) return item;
94
+ if (typeof item.type !== "string" || !NAMED_CALL_ITEM_TYPES.has(item.type)) return item;
95
+ // An item carrying an explicit namespace is a namespaced identity whose `name` is the inner
96
+ // name; it is not the flattened shape this repairs, and its own field is the authority.
97
+ if (item.namespace !== undefined) return item;
98
+ if (typeof item.name !== "string") return item;
99
+ const repaired = repairedCallName(item.name, declarations);
100
+ return repaired === undefined ? item : { ...item, name: repaired };
101
+ }
102
+
103
+ /**
104
+ * Make an already-damaged conversation replayable.
105
+ *
106
+ * A `function_call` whose name violates `^[a-zA-Z0-9_-]+$` is not a one-turn failure: Codex stores
107
+ * the item, and the upstream then refuses EVERY later request that replays it
108
+ * (`Invalid 'input[N].name': ...`) — including the automatic compaction a long task or a side chat
109
+ * triggers. The emit-side fix stops new items from being created; without this the conversations
110
+ * that already contain one stay permanently unusable, which is the part of #5095 that actually
111
+ * hurts.
112
+ *
113
+ * Only replayed input items are touched. The caller's tool catalog is never rewritten, and a body
114
+ * with nothing to repair is returned by reference.
115
+ */
116
+ export function repairLegacyDottedToolCallNames(body: unknown): unknown {
117
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
118
+ if (!body.input.some(item =>
119
+ isPlainObject(item)
120
+ && typeof item.type === "string"
121
+ && NAMED_CALL_ITEM_TYPES.has(item.type)
122
+ && typeof item.name === "string"
123
+ && item.name.startsWith(LEGACY_INVENTED_NAMESPACE_PREFIX)
124
+ )) return body;
125
+
126
+ const declarations = readDeclarations(body);
127
+ let changed = false;
128
+ const input = body.input.map(item => {
129
+ const repaired = repairedItem(item, declarations);
130
+ if (repaired !== item) changed = true;
131
+ return repaired;
132
+ });
133
+ return changed ? { ...body, input } : body;
134
+ }
@@ -0,0 +1,130 @@
1
+ import { unwrapFreeformToolInput } from "./apply-patch-envelope";
2
+ import { JSON_ESCAPES, scanFreeformWrapper } from "./freeform-wrapper-scan";
3
+
4
+ /**
5
+ * Whether a body could still grow into one complete outer Markdown fence.
6
+ *
7
+ * `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
8
+ * own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
9
+ * the stream holds. A buffer that does not open with a fence can never acquire one, so
10
+ * ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
11
+ * whole call, because a closing fence can still be followed by more text that withdraws it.
12
+ */
13
+ function mayBecomeFencedBody(text: string, toolName: string): boolean {
14
+ if (toolName !== "exec" && toolName !== "apply_patch") return false;
15
+ const head = text.trimStart();
16
+ if (head === "") return true;
17
+ return head.startsWith("```") || "```".startsWith(head);
18
+ }
19
+
20
+ const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
21
+
22
+ /**
23
+ * The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
24
+ *
25
+ * Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
26
+ * and anything invented here would be retracted. Three of them are not obvious:
27
+ *
28
+ * An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
29
+ * next, so completion falls back to the raw text. Returning `null` for that case stops the
30
+ * preview rather than continuing to decode a value the completed item will never carry.
31
+ * `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
32
+ *
33
+ * A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
34
+ * delta the client has to decode by itself, so the pair is emitted together or not at all.
35
+ */
36
+ function decodeJsonStringPrefix(body: string): string | null {
37
+ let out = "";
38
+ for (let i = 0; i < body.length; i++) {
39
+ const c = body[i];
40
+ if (c === '"') break; // unescaped closing quote: value complete
41
+ if (c !== "\\") {
42
+ // A literal control character is not legal inside a JSON string, so `JSON.parse` will
43
+ // reject this wrapper and completion will return the raw text. Emitting the decoded value
44
+ // first is the disagreement this decoder exists to prevent, so stop instead.
45
+ if (c!.charCodeAt(0) <= 0x1f) return null;
46
+ out += c;
47
+ continue;
48
+ }
49
+ const n = body[i + 1];
50
+ if (n === undefined) break; // escape split across chunks: wait for more
51
+ if (n === "u") {
52
+ const hex = body.slice(i + 2, i + 6);
53
+ if (hex.length < 4) break; // split across chunks: wait for more
54
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
55
+ const code = parseInt(hex, 16);
56
+ if (code >= 0xd800 && code <= 0xdbff) {
57
+ const low = body.slice(i + 6, i + 12);
58
+ if (!LOW_SURROGATE_ESCAPE.test(low)) break;
59
+ out += String.fromCharCode(code, parseInt(low.slice(2), 16));
60
+ i += 11;
61
+ continue;
62
+ }
63
+ out += String.fromCharCode(code);
64
+ i += 5;
65
+ continue;
66
+ }
67
+ const escaped = JSON_ESCAPES.get(n);
68
+ if (escaped === undefined) return null; // never parses
69
+ out += escaped;
70
+ i += 1;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * The value to stream so far, or `null` to HOLD because nothing can be decided yet.
77
+ *
78
+ * `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
79
+ * key is present, whatever else the object carries, so its value can be unescaped
80
+ * progressively and never retracted.
81
+ *
82
+ * One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
83
+ * `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
84
+ * b. Closing it means holding every canonical wrapper until its object parses, which is the
85
+ * progressive streaming this path exists to provide. The completed item stays authoritative,
86
+ * and no model emits a duplicate key in practice.
87
+ *
88
+ * A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
89
+ * answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
90
+ * arrives, and completion returns the raw text because nothing parses. The preview stops
91
+ * there: no rewind, and no decoded text the completed item does not contain. Bounding the
92
+ * damage is what is available without giving up progressive streaming, and the args are
93
+ * unusable in that case whichever representation wins.
94
+ *
95
+ * A fallback key is not decidable. It only unwraps when it is the SINGLE string field, and a
96
+ * second key can still arrive — so a value emitted early would have to be taken back. That is
97
+ * the rewind this holds instead: stream nothing until the object closes, then publish the one
98
+ * repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
99
+ * any object prefix for the same reason (#5047).
100
+ *
101
+ * An object that has not reached a canonical key YET is in exactly that position, and used to
102
+ * be treated as raw because it did not match the literal `{"input":"`. It holds now: `input`
103
+ * can still arrive after other properties, or wearing an escaped spelling, and completion would
104
+ * then unwrap a body whose wrapper syntax had already been published as deltas (#5151). The
105
+ * cost of holding is preview on an object body that turns out not to be a wrapper; the cost of
106
+ * not holding was publishing bytes the completed item removes.
107
+ */
108
+ export function progressiveFreeformInput(args: string, toolName: string): string | null {
109
+ const scan = scanFreeformWrapper(args);
110
+ if (scan.kind === "input") {
111
+ const decoded = decodeJsonStringPrefix(args.slice(scan.valueStart));
112
+ if (decoded === null) return null;
113
+ return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
114
+ }
115
+ if (scan.kind === "raw") return mayBecomeFencedBody(args, toolName) ? null : args;
116
+
117
+ // Undecided: some wrapper may still apply, and only the completed object says which one.
118
+ // The parse runs exactly where the scan SAW the object close, so it reads a buffer the scan
119
+ // already walked and its cost is bounded by the same clamp. A hold from either limit stays
120
+ // held: an incomplete object has nothing to parse, and re-reading a budget-exhausted buffer
121
+ // on every delta is quadratic work for a delta that would arrive in the same instant as the
122
+ // authoritative completion behind it.
123
+ if (!scan.parse) return null;
124
+ try {
125
+ JSON.parse(args);
126
+ } catch {
127
+ return null;
128
+ }
129
+ return unwrapFreeformToolInput(args, toolName);
130
+ }
@@ -63,6 +63,36 @@ export function encodeReasoningEnvelope(envelope: ReasoningEnvelope, budget?: Tr
63
63
  }
64
64
  }
65
65
 
66
+ /**
67
+ * Whether a thinking part's `signature` is an attestation some provider issued, rather than
68
+ * the serialized reasoning item the parser parks in the same field.
69
+ *
70
+ * `src/responses/parser.ts` stores `JSON.stringify(reasoningItem)` on an UNSIGNED thinking
71
+ * part so the opaque item survives a same-provider round trip. Every adapter that forwards a
72
+ * signature upstream is forwarding an opaque attestation, and a provider asked to verify our
73
+ * own parser state cannot recognize it. The predicate lives beside the envelope code that owns
74
+ * this field's representation rather than in one adapter, because a private second copy is how
75
+ * the side that writes the field and a side that reads it come to disagree about what it holds.
76
+ *
77
+ * This is a deny-list for exactly that one shape, not a guess at what an attestation looks
78
+ * like. `src/adapters/anthropic.ts` applies a stricter allow-list on top of it, because an
79
+ * Anthropic signature has a known base64 spelling; that shape is a fact about Anthropic's wire
80
+ * and is not assumed of any other provider's token here.
81
+ */
82
+ export function isProviderIssuedThinkingSignature(
83
+ signature: string | undefined,
84
+ ): signature is string {
85
+ if (typeof signature !== "string" || signature.length === 0) return false;
86
+ if (!signature.startsWith("{")) return true;
87
+ try {
88
+ const parsed: unknown = JSON.parse(signature);
89
+ return !parsed || typeof parsed !== "object" || Array.isArray(parsed)
90
+ || (parsed as { type?: unknown }).type !== "reasoning";
91
+ } catch {
92
+ return true;
93
+ }
94
+ }
95
+
66
96
  /** Decode an ocxr1 envelope; returns null for native (OpenAI-encrypted) blobs or garbage. */
67
97
  export function decodeReasoningEnvelope(encryptedContent: string, budget?: TranslatorBudget): ReasoningEnvelope | null {
68
98
  if (!encryptedContent.startsWith(OCX_REASONING_PREFIX)) return null;
@@ -135,11 +135,10 @@ export type ResidentInput = Omit<ResidentResponseState, "kind" | "sizeBytes">;
135
135
 
136
136
  export type PreviousResponseReplayFailure = {
137
137
  code: "previous_response_not_found";
138
- reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large";
138
+ reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large" | "scope_mismatch";
139
139
  };
140
140
 
141
141
  const states = new Map<string, StoredResponseState>();
142
- const replayScopeMismatches = new WeakSet<object>();
143
142
  let storedResponseBytes = 0;
144
143
  let residentResponseBytes = 0;
145
144
  let oldestResidentId: string | undefined;
@@ -1044,11 +1043,6 @@ function normalizedClientThreadId(value: unknown): string | undefined {
1044
1043
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
1045
1044
  }
1046
1045
 
1047
- function withoutPreviousResponseId(request: Record<string, unknown>): Record<string, unknown> {
1048
- const { previous_response_id: _previousResponseId, ...freshRequest } = request;
1049
- return freshRequest;
1050
- }
1051
-
1052
1046
  export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown {
1053
1047
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
1054
1048
  const request = body as Record<string, unknown>;
@@ -1068,10 +1062,9 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1068
1062
  // A Codex task must never inherit another task's continuation, nor a legacy unscoped entry.
1069
1063
  // Unscoped callers retain backward-compatible replay only with other unscoped entries.
1070
1064
  if (requestThreadId !== storedThreadId) {
1071
- const freshRequest = withoutPreviousResponseId(request);
1072
- replayScopeMismatches.add(freshRequest);
1065
+ replayFailures.set(request, { code: "previous_response_not_found", reason: "scope_mismatch" });
1073
1066
  replayScopeMismatchDrops += 1;
1074
- return freshRequest;
1067
+ return body;
1075
1068
  }
1076
1069
  // The client already replayed this history verbatim. Prepending the stored copy would
1077
1070
  // double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
@@ -1136,9 +1129,9 @@ export function copyPreviousResponseReplayProvenance(source: unknown, target: un
1136
1129
  replayedInputPrefixLengths.set(target, prefixLength);
1137
1130
  }
1138
1131
 
1139
- /** True when a stale or foreign previous_response_id was removed from this exact request body. */
1132
+ /** True when this exact request could not replay because its task scope did not match. */
1140
1133
  export function previousResponseScopeMismatch(body: unknown): boolean {
1141
- return !!body && typeof body === "object" && replayScopeMismatches.has(body as object);
1134
+ return previousResponseReplayFailure(body)?.reason === "scope_mismatch";
1142
1135
  }
1143
1136
 
1144
1137
  export function previousResponseConversationId(responseId: string | undefined): string | undefined {
@@ -19,6 +19,21 @@ export function dottedAliasIsUnambiguous(namespace: string, name: string): boole
19
19
  return !namespace.includes("__") && !name.includes("__");
20
20
  }
21
21
 
22
+ /**
23
+ * Charset the Responses API enforces on a tool-call `name`.
24
+ *
25
+ * A name outside it is not merely rejected for the turn that carries it: Codex stores the item in
26
+ * the conversation, and every later request that replays that history is refused before it runs
27
+ * (`Invalid 'input[N].name': string does not match pattern '^[a-zA-Z0-9_-]+$'`). One relayed item
28
+ * therefore ends the conversation permanently, which is why the emit boundary treats an invalid
29
+ * name as a defect to repair rather than as a name to forward. See #5095.
30
+ */
31
+ const RESPONSES_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
32
+
33
+ export function isSchemaValidResponsesToolName(name: string): boolean {
34
+ return RESPONSES_TOOL_NAME_PATTERN.test(name);
35
+ }
36
+
22
37
  export function wireToolInnerName(tool: unknown): string | undefined {
23
38
  if (!isPlainObject(tool)) return undefined;
24
39
  const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
@@ -76,4 +91,3 @@ export function collectAmbiguousDottedAliases(specGroups: readonly unknown[]): S
76
91
  for (const [alias, owner] of owners) if (owner === null) ambiguous.add(alias);
77
92
  return ambiguous;
78
93
  }
79
-