@bitkyc08/opencodex 2.28.0 → 2.29.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 (90) hide show
  1. package/README.md +9 -1
  2. package/gui/dist/assets/index-BNESwCzn.js +102 -0
  3. package/gui/dist/assets/index-CH7ncHCC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/base.ts +3 -1
  7. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  8. package/src/adapters/cursor/discovery.ts +25 -0
  9. package/src/adapters/cursor/live-transport.ts +19 -2
  10. package/src/adapters/cursor/native-exec.ts +47 -2
  11. package/src/adapters/cursor/protobuf-request.ts +80 -23
  12. package/src/adapters/cursor/request-builder.ts +119 -5
  13. package/src/adapters/cursor/transport.ts +5 -0
  14. package/src/adapters/cursor/types.ts +13 -0
  15. package/src/adapters/cursor.ts +109 -3
  16. package/src/adapters/google-antigravity-replay.ts +31 -4
  17. package/src/adapters/google.ts +323 -38
  18. package/src/adapters/openai-chat.ts +19 -0
  19. package/src/adapters/openai-responses.ts +218 -37
  20. package/src/bridge.ts +38 -20
  21. package/src/claude/desktop-3p.ts +15 -6
  22. package/src/cli/agent.ts +44 -1
  23. package/src/cli/claude-agent-startup-sync.ts +73 -0
  24. package/src/cli/dispatch.ts +24 -1
  25. package/src/cli/ensure-desired-integrations.ts +152 -0
  26. package/src/cli/help.ts +1 -1
  27. package/src/cli/index.ts +40 -38
  28. package/src/cli/integrations.ts +1 -1
  29. package/src/cli/registry.ts +2 -2
  30. package/src/clients/config-export.ts +119 -20
  31. package/src/codex/affinity-debug.ts +162 -0
  32. package/src/codex/inject.ts +46 -14
  33. package/src/codex/journal.ts +22 -8
  34. package/src/config.ts +1 -0
  35. package/src/generated/compatibility-version.json +134 -66
  36. package/src/integrations/mutation-flight.ts +71 -0
  37. package/src/integrations/owned-refresh.ts +74 -0
  38. package/src/integrations/registry.ts +25 -2
  39. package/src/integrations/writer.ts +32 -1
  40. package/src/lab/public/signature.ts +25 -1
  41. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  42. package/src/lib/redact.ts +2 -2
  43. package/src/oauth/log.ts +3 -1
  44. package/src/providers/derive.ts +9 -0
  45. package/src/providers/fastwire.ts +24 -18
  46. package/src/providers/openai-tiers.ts +60 -1
  47. package/src/providers/registry.ts +14 -7
  48. package/src/providers/xai-responses-opt-in.ts +15 -0
  49. package/src/responses/compaction.ts +18 -0
  50. package/src/responses/custom-tool-compat.ts +70 -5
  51. package/src/responses/namespace-tool-compat.ts +356 -0
  52. package/src/responses/parser.ts +2 -2
  53. package/src/responses/provider-continuation.ts +98 -0
  54. package/src/responses/reasoning-replay-cache.ts +125 -7
  55. package/src/responses/spill-store.ts +6 -1
  56. package/src/responses/state.ts +11 -0
  57. package/src/router.ts +14 -0
  58. package/src/routing/compatibility/behavior.ts +1 -0
  59. package/src/server/auth-cors.ts +4 -0
  60. package/src/server/management/agent-settings-routes.ts +57 -9
  61. package/src/server/management/config-routes.ts +134 -15
  62. package/src/server/management/integration-routes.ts +8 -55
  63. package/src/server/management/model-routes.ts +23 -1
  64. package/src/server/management/provider-routes.ts +22 -0
  65. package/src/server/management/vision-sidecar-options.ts +18 -11
  66. package/src/server/management/web-search-sidecar-options.ts +120 -0
  67. package/src/server/responses/core.ts +588 -78
  68. package/src/server/responses/responses-field-backfill.ts +7 -11
  69. package/src/server/responses/terminal-guard.ts +22 -11
  70. package/src/server/responses-custom-tool-repair.ts +3 -1
  71. package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
  72. package/src/server/responses-tool-search-repair.ts +64 -14
  73. package/src/sidecar/auth.ts +92 -0
  74. package/src/sidecar/candidates.ts +83 -0
  75. package/src/types/config.ts +26 -5
  76. package/src/types/provider.ts +18 -0
  77. package/src/types/request.ts +26 -0
  78. package/src/types.ts +1 -0
  79. package/src/usage/log.ts +2 -0
  80. package/src/vision/index.ts +8 -9
  81. package/src/web-search/backends.ts +108 -0
  82. package/src/web-search/exa-executor.ts +88 -0
  83. package/src/web-search/gemini-executor.ts +141 -0
  84. package/src/web-search/index.ts +139 -12
  85. package/src/web-search/loop.ts +45 -5
  86. package/src/web-search/parse.ts +34 -23
  87. package/src/web-search/sources.ts +60 -0
  88. package/src/web-search/xai-executor.ts +219 -0
  89. package/gui/dist/assets/index-D2sP-biU.js +0 -102
  90. package/gui/dist/assets/index-DQsMZzI5.css +0 -1
@@ -1,4 +1,8 @@
1
+ import { namespacedToolName } from "../types";
2
+ import { collectResponsesToolGroups } from "./tool-groups";
3
+
1
4
  const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
5
+ const BUILTIN_FUNCTIONS_NAMESPACE = "functions";
2
6
 
3
7
  function isPlainObject(value: unknown): value is Record<string, unknown> {
4
8
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -13,6 +17,65 @@ function customToolInput(argumentsText: unknown): string {
13
17
  return argumentsText;
14
18
  }
15
19
 
20
+ function customToolWireName(namespace: string | undefined, name: string): string {
21
+ return namespace === BUILTIN_FUNCTIONS_NAMESPACE ? name : namespacedToolName(namespace, name);
22
+ }
23
+
24
+ /** Final upstream identity of a call, including a namespace restored by an earlier rewrite. */
25
+ export function routedCustomToolWireName(value: unknown): string | undefined {
26
+ if (!isPlainObject(value) || typeof value.name !== "string") return undefined;
27
+ return customToolWireName(
28
+ typeof value.namespace === "string" ? value.namespace : undefined,
29
+ value.name,
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Names of converted custom declarations after namespace lowering. Restoration uses these exact
35
+ * wire identities so same-named function and custom children in different namespaces stay distinct.
36
+ */
37
+ function collectRoutedCustomToolWireNames(body: unknown): Set<string> {
38
+ const names = new Set<string>();
39
+ const groups = collectResponsesToolGroups(body);
40
+ const bareWireNames = new Set<string>();
41
+ for (const group of groups) {
42
+ for (const tool of group) {
43
+ if (
44
+ isPlainObject(tool)
45
+ && tool.type !== "namespace"
46
+ && typeof tool.name === "string"
47
+ ) bareWireNames.add(tool.name);
48
+ }
49
+ }
50
+
51
+ for (const group of groups) {
52
+ for (const tool of group) {
53
+ if (!isPlainObject(tool)) continue;
54
+ if (
55
+ tool.type === "custom"
56
+ && typeof tool.name === "string"
57
+ && !ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(tool.name)
58
+ ) {
59
+ names.add(tool.name);
60
+ continue;
61
+ }
62
+ if (tool.type !== "namespace" || typeof tool.name !== "string" || !Array.isArray(tool.tools)) {
63
+ continue;
64
+ }
65
+ for (const child of tool.tools) {
66
+ if (
67
+ isPlainObject(child)
68
+ && child.type === "custom"
69
+ && typeof child.name === "string"
70
+ && !ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(child.name)
71
+ && !(tool.name === BUILTIN_FUNCTIONS_NAMESPACE && bareWireNames.has(child.name))
72
+ ) names.add(customToolWireName(tool.name, child.name));
73
+ }
74
+ }
75
+ }
76
+ return names;
77
+ }
78
+
16
79
  export function customToolItemId(id: unknown): unknown {
17
80
  if (typeof id !== "string") return id;
18
81
  return id.startsWith("fc_") ? `ctc_${id.slice(3)}` : id;
@@ -125,11 +188,12 @@ export function rewriteRoutedCustomToolsForUpstream(body: unknown): {
125
188
  body: unknown;
126
189
  names: Set<string>;
127
190
  } {
128
- const names = collectRoutedCustomToolNames(body);
129
- if (names.size === 0) return { body, names };
191
+ const conversionNames = collectRoutedCustomToolNames(body);
192
+ const names = collectRoutedCustomToolWireNames(body);
193
+ if (conversionNames.size === 0) return { body, names };
130
194
  const callIds = new Set<string>();
131
- collectConvertedCallIds(body, names, callIds);
132
- return { body: rewriteForUpstream(body, names, callIds), names };
195
+ collectConvertedCallIds(body, conversionNames, callIds);
196
+ return { body: rewriteForUpstream(body, conversionNames, callIds), names };
133
197
  }
134
198
 
135
199
  export function restoreRoutedCustomCalls(
@@ -155,7 +219,8 @@ export function restoreRoutedCustomCalls(
155
219
  changed ||= result.changed;
156
220
  }
157
221
 
158
- if (value.type === "function_call" && typeof value.name === "string" && names.has(value.name)) {
222
+ const wireName = routedCustomToolWireName(value);
223
+ if (value.type === "function_call" && wireName !== undefined && names.has(wireName)) {
159
224
  restored.type = "custom_tool_call";
160
225
  restored.id = customToolItemId(value.id);
161
226
  restored.input = customToolInput(value.arguments);
@@ -0,0 +1,356 @@
1
+ import { namespacedToolName } from "../types";
2
+ import { collectResponsesToolGroups } from "./tool-groups";
3
+
4
+ export interface RoutedNamespaceToolIdentity {
5
+ namespace: string;
6
+ name: string;
7
+ }
8
+
9
+ export type RoutedNamespaceToolAliases = ReadonlyMap<string, RoutedNamespaceToolIdentity>;
10
+
11
+ const BUILTIN_FUNCTIONS_NAMESPACE = "functions";
12
+
13
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
14
+ return !!value && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+
17
+ function namespaceIdentity(namespace: string, name: string): string {
18
+ return `${namespace}\u0000${name}`;
19
+ }
20
+
21
+ /**
22
+ * A name that can become a wire tool name. Control characters are rejected because the identity
23
+ * key below joins namespace and name with NUL: a name carrying one could otherwise forge another
24
+ * tool's identity and silently take over its wire name.
25
+ */
26
+ function isRepresentableName(name: unknown): name is string {
27
+ if (typeof name !== "string" || name.length === 0) return false;
28
+ for (let index = 0; index < name.length; index += 1) {
29
+ const code = name.charCodeAt(index);
30
+ // C0 controls and DEL, written as code points so this source never carries one itself.
31
+ if (code <= 0x1f || code === 0x7f) return false;
32
+ }
33
+ return true;
34
+ }
35
+
36
+ type NamespaceGroup = {
37
+ namespace: string;
38
+ /** Children that can be lowered to a flat declaration; unrepresentable ones are omitted. */
39
+ children: Record<string, unknown>[];
40
+ };
41
+
42
+ /**
43
+ * Read a private namespace group, or return undefined when the value is not one.
44
+ *
45
+ * Children that cannot be expressed as a flat declaration — a nested group, a missing name, a
46
+ * control character in the name — are dropped, and a group left with no children is dropped whole
47
+ * by the rewrite. Preserving the private `namespace` shape instead would lose every tool in the
48
+ * request rather than one: the strict gateways this layer exists for reject that tool type before
49
+ * inference, which is the failure the layer was written to prevent.
50
+ */
51
+ function parseNamespaceGroup(tool: unknown): NamespaceGroup | undefined {
52
+ if (
53
+ !isPlainObject(tool)
54
+ || tool.type !== "namespace"
55
+ || !isRepresentableName(tool.name)
56
+ || !Array.isArray(tool.tools)
57
+ ) return undefined;
58
+ const children: Record<string, unknown>[] = [];
59
+ for (const child of tool.tools) {
60
+ if (!isPlainObject(child) || child.type === "namespace" || !isRepresentableName(child.name)) continue;
61
+ children.push(child);
62
+ }
63
+ return { namespace: tool.name, children };
64
+ }
65
+
66
+ /**
67
+ * Wire identity of a lowered tool. A `functions` child and an identical top-level declaration share
68
+ * one identity because they denote the same logical tool: `buildTools` flattens the reserved group
69
+ * without a namespace, so the parser already treats them as one and tolerates the duplicate.
70
+ */
71
+ function loweredIdentity(namespace: string, name: string): string {
72
+ return namespace === BUILTIN_FUNCTIONS_NAMESPACE
73
+ ? namespaceIdentity(BUILTIN_FUNCTIONS_NAMESPACE, name)
74
+ : namespaceIdentity(namespace, name);
75
+ }
76
+
77
+ function loweredWireName(namespace: string, name: string): string {
78
+ return namespace === BUILTIN_FUNCTIONS_NAMESPACE ? name : namespacedToolName(namespace, name);
79
+ }
80
+
81
+ function addSelector(
82
+ selectors: Map<string, string | null>,
83
+ selector: string,
84
+ wireName: string,
85
+ ): void {
86
+ const current = selectors.get(selector);
87
+ if (current === undefined) selectors.set(selector, wireName);
88
+ else if (current !== wireName) selectors.set(selector, null);
89
+ }
90
+
91
+ type NamespaceRewritePlan = {
92
+ aliases: Map<string, RoutedNamespaceToolIdentity>;
93
+ bareWireNames: Set<string>;
94
+ identities: Map<string, string>;
95
+ selectors: Map<string, string | null>;
96
+ };
97
+
98
+ /** Two distinct logical tools would occupy one wire name; the caller maps this to a 400. */
99
+ export class NamespaceToolCollisionError extends Error {}
100
+
101
+ function buildRewritePlan(groups: readonly unknown[][]): NamespaceRewritePlan {
102
+ const aliases = new Map<string, RoutedNamespaceToolIdentity>();
103
+ const bareWireNames = new Set<string>();
104
+ const identities = new Map<string, string>();
105
+ const selectors = new Map<string, string | null>();
106
+ const wireOwners = new Map<string, string>();
107
+
108
+ for (const group of groups) {
109
+ for (const tool of group) {
110
+ if (isPlainObject(tool) && tool.type !== "namespace" && isRepresentableName(tool.name)) {
111
+ // A bare declaration is the reserved group's flattened form, so it claims that identity:
112
+ // declaring the same tool both ways is the duplicate the parser already tolerates, not a
113
+ // collision, and `promoteClientLoadedTools` produces exactly that shape.
114
+ wireOwners.set(tool.name, loweredIdentity(BUILTIN_FUNCTIONS_NAMESPACE, tool.name));
115
+ bareWireNames.add(tool.name);
116
+ addSelector(selectors, tool.name, tool.name);
117
+ }
118
+ }
119
+ }
120
+
121
+ for (const group of groups) {
122
+ for (const tool of group) {
123
+ const parsed = parseNamespaceGroup(tool);
124
+ if (!parsed) continue;
125
+ for (const child of parsed.children) {
126
+ const childName = child.name as string;
127
+ const identity = loweredIdentity(parsed.namespace, childName);
128
+ const wireName = loweredWireName(parsed.namespace, childName);
129
+ const owner = wireOwners.get(wireName);
130
+ if (owner !== undefined && owner !== identity) {
131
+ throw new NamespaceToolCollisionError(
132
+ `namespace tool wire-name collision for "${wireName}"; rename one of the colliding tools`,
133
+ );
134
+ }
135
+ wireOwners.set(wireName, identity);
136
+ identities.set(identity, wireName);
137
+ addSelector(selectors, wireName, wireName);
138
+ addSelector(selectors, `${parsed.namespace}.${childName}`, wireName);
139
+ addSelector(selectors, childName, wireName);
140
+ if (parsed.namespace !== BUILTIN_FUNCTIONS_NAMESPACE) {
141
+ aliases.set(wireName, { namespace: parsed.namespace, name: childName });
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return { aliases, bareWireNames, identities, selectors };
148
+ }
149
+
150
+ /**
151
+ * Lower every namespace group in one tool container. `emitted` is shared across the whole body so
152
+ * a tool declared both bare and under `functions` is written once rather than twice.
153
+ *
154
+ * No `type: "namespace"` value survives this pass, including a group this layer cannot read:
155
+ * relaying the private shape is what the strict gateway rejects.
156
+ */
157
+ function rewriteToolList(
158
+ tools: unknown[],
159
+ plan: NamespaceRewritePlan,
160
+ emitted: Set<string>,
161
+ ): unknown[] {
162
+ let changed = false;
163
+ const rewritten: unknown[] = [];
164
+ for (const tool of tools) {
165
+ if (isPlainObject(tool) && tool.type === "namespace") {
166
+ changed = true;
167
+ const parsed = parseNamespaceGroup(tool);
168
+ if (!parsed) continue;
169
+ for (const child of parsed.children) {
170
+ const wireName = plan.identities.get(loweredIdentity(parsed.namespace, child.name as string));
171
+ // A bare declaration is the canonical representation of a `functions` child. Decide that
172
+ // from the complete catalog rather than whichever container happens to be rewritten first.
173
+ if (
174
+ wireName === undefined
175
+ || (parsed.namespace === BUILTIN_FUNCTIONS_NAMESPACE && plan.bareWireNames.has(wireName))
176
+ || emitted.has(wireName)
177
+ ) continue;
178
+ emitted.add(wireName);
179
+ rewritten.push(wireName === child.name ? child : { ...child, name: wireName });
180
+ }
181
+ continue;
182
+ }
183
+ if (isPlainObject(tool) && isRepresentableName(tool.name)) {
184
+ if (emitted.has(tool.name)) {
185
+ changed = true;
186
+ continue;
187
+ }
188
+ emitted.add(tool.name);
189
+ }
190
+ rewritten.push(tool);
191
+ }
192
+ return changed ? rewritten : tools;
193
+ }
194
+
195
+ /**
196
+ * Resolve one `{namespace?, name}` reference to its wire name and drop the private `namespace` key.
197
+ *
198
+ * `bareFallback` is for tool_choice, where a bare name is a selector the caller expects resolved
199
+ * against the catalog. Replayed call items pass `false`: a history item records which tool actually
200
+ * ran, so resolving a bare name through a same-named namespace child would rewrite history on a
201
+ * coincidence rather than translate it.
202
+ *
203
+ * An explicit namespace is always lowered, even when this turn's catalog no longer declares that
204
+ * group — a compaction turn drops the whole catalog, and a catalog can change mid-session. Leaving
205
+ * the key in place ships a Codex-private field to a gateway that rejects unknown fields, which is
206
+ * the failure this layer exists to prevent, and this layer's own response restoration is what put
207
+ * the key on the item.
208
+ */
209
+ function rewriteNamedSelector(
210
+ value: unknown,
211
+ plan: NamespaceRewritePlan,
212
+ bareFallback: boolean,
213
+ ): unknown {
214
+ if (!isPlainObject(value) || typeof value.name !== "string") return value;
215
+ if (typeof value.namespace !== "string") {
216
+ if (!bareFallback) return value;
217
+ const wireName = plan.selectors.get(value.name) ?? undefined;
218
+ return wireName === undefined || wireName === value.name ? value : { ...value, name: wireName };
219
+ }
220
+ const { namespace, ...rest } = value;
221
+ const wireName = plan.identities.get(loweredIdentity(namespace, value.name))
222
+ ?? loweredWireName(namespace, value.name);
223
+ return { ...rest, name: wireName };
224
+ }
225
+
226
+ function rewriteToolChoice(value: unknown, plan: NamespaceRewritePlan): unknown {
227
+ if (!isPlainObject(value)) return value;
228
+ if ((value.type === "function" || value.type === "custom") && typeof value.name === "string") {
229
+ return rewriteNamedSelector(value, plan, true);
230
+ }
231
+ if (value.type !== "allowed_tools" || !Array.isArray(value.tools)) return value;
232
+ let changed = false;
233
+ const tools = value.tools.map(tool => {
234
+ if (!isPlainObject(tool) || typeof tool.name !== "string") return tool;
235
+ const rewritten = rewriteNamedSelector(tool, plan, true);
236
+ changed ||= rewritten !== tool;
237
+ return rewritten;
238
+ });
239
+ return changed ? { ...value, tools } : value;
240
+ }
241
+
242
+ function rewriteInputItem(item: unknown, plan: NamespaceRewritePlan, emitted: Set<string>): unknown {
243
+ if (!isPlainObject(item)) return item;
244
+ if (item.type === "additional_tools" && Array.isArray(item.tools)) {
245
+ const tools = rewriteToolList(item.tools, plan, emitted);
246
+ return tools === item.tools ? item : { ...item, tools };
247
+ }
248
+ if (
249
+ (item.type === "function_call" || item.type === "custom_tool_call")
250
+ && typeof item.name === "string"
251
+ ) return rewriteNamedSelector(item, plan, false);
252
+ return item;
253
+ }
254
+
255
+ /**
256
+ * Lower Codex's private Responses namespace declarations for public/third-party gateways.
257
+ *
258
+ * Codex 0.147 groups ordinary tools under the reserved `functions` namespace; those children
259
+ * become bare top-level declarations. Other namespaces use the same collision-checked
260
+ * `<namespace>__<name>` wire identity as the chat adapters. The returned request-local aliases
261
+ * are the only names response restoration is allowed to expand.
262
+ */
263
+ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
264
+ body: unknown;
265
+ aliases: Map<string, RoutedNamespaceToolIdentity>;
266
+ } {
267
+ if (!isPlainObject(body)) return { body, aliases: new Map() };
268
+ const groups = collectResponsesToolGroups(body);
269
+ const plan = buildRewritePlan(groups);
270
+
271
+ // Deliberately not gated on the plan being non-empty: a turn whose catalog is gone still replays
272
+ // call items carrying a private `namespace`, and the routed compaction turn strips the whole tool
273
+ // surface before this runs.
274
+ const emitted = new Set<string>();
275
+ const tools = Array.isArray(body.tools) ? rewriteToolList(body.tools, plan, emitted) : body.tools;
276
+
277
+ let input = body.input;
278
+ if (Array.isArray(body.input)) {
279
+ let inputChanged = false;
280
+ const rewrittenInput = body.input.map(item => {
281
+ const next = rewriteInputItem(item, plan, emitted);
282
+ if (next !== item) inputChanged = true;
283
+ return next;
284
+ });
285
+ if (inputChanged) input = rewrittenInput;
286
+ }
287
+
288
+ const toolChoice = rewriteToolChoice(body.tool_choice, plan);
289
+ return {
290
+ body: {
291
+ ...body,
292
+ ...(tools !== body.tools ? { tools } : {}),
293
+ ...(input !== body.input ? { input } : {}),
294
+ ...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
295
+ },
296
+ aliases: plan.aliases,
297
+ };
298
+ }
299
+
300
+ export function restoreRoutedNamespaceCalls(
301
+ value: unknown,
302
+ aliases: RoutedNamespaceToolAliases,
303
+ ): { value: unknown; changed: boolean } {
304
+ if (Array.isArray(value)) {
305
+ let changed = false;
306
+ const restored = value.map(entry => {
307
+ const result = restoreRoutedNamespaceCalls(entry, aliases);
308
+ changed ||= result.changed;
309
+ return result.value;
310
+ });
311
+ return changed ? { value: restored, changed: true } : { value, changed: false };
312
+ }
313
+ if (!isPlainObject(value)) return { value, changed: false };
314
+
315
+ let changed = false;
316
+ const restored: Record<string, unknown> = {};
317
+ for (const [key, entry] of Object.entries(value)) {
318
+ const result = restoreRoutedNamespaceCalls(entry, aliases);
319
+ restored[key] = result.value;
320
+ changed ||= result.changed;
321
+ }
322
+
323
+ if (
324
+ (value.type === "function_call" || value.type === "custom_tool_call")
325
+ && typeof value.name === "string"
326
+ ) {
327
+ const identity = aliases.get(value.name);
328
+ if (identity) {
329
+ restored.name = identity.name;
330
+ restored.namespace = identity.namespace;
331
+ changed = true;
332
+ }
333
+ }
334
+ return changed ? { value: restored, changed: true } : { value, changed: false };
335
+ }
336
+
337
+ export function restoreRoutedNamespaceCallsInJson(
338
+ text: string,
339
+ aliases: RoutedNamespaceToolAliases,
340
+ ): string {
341
+ if (aliases.size === 0) return text;
342
+ let payload: unknown;
343
+ try {
344
+ payload = JSON.parse(text);
345
+ } catch {
346
+ return text;
347
+ }
348
+ const restored = restoreRoutedNamespaceCalls(payload, aliases);
349
+ return restored.changed ? JSON.stringify(restored.value) : text;
350
+ }
351
+
352
+ export function createRoutedNamespaceCallRestoreRewrite(
353
+ aliases: RoutedNamespaceToolAliases,
354
+ ): (payload: string) => string {
355
+ return payload => restoreRoutedNamespaceCallsInJson(payload, aliases);
356
+ }
@@ -15,7 +15,7 @@ import { namespacedToolName, toolChoiceCandidates } from "../types";
15
15
  import { responsesRequestSchema } from "./schema";
16
16
  import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
17
17
  import { lookupReplayThoughtSignature } from "./thought-signature-replay";
18
- import { compactionItemToText } from "./compaction";
18
+ import { compactionItemToText, isCompactionItemType } from "./compaction";
19
19
  import { previousResponseReplayPrefixLength } from "./state";
20
20
  import { decodeReasoningEnvelope } from "./reasoning-envelope";
21
21
  import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
@@ -434,7 +434,7 @@ export function parseRequest(
434
434
  continue;
435
435
  }
436
436
 
437
- if (effectiveType === "compaction" || effectiveType === "compaction_summary" || effectiveType === "context_compaction") {
437
+ if (isCompactionItemType(effectiveType)) {
438
438
  // A stored summary from a previous compaction. Decode our ocx1 envelope into plain text so
439
439
  // the routed model keeps the compacted context; real OpenAI-encrypted blobs degrade to a note.
440
440
  // `context_compaction` (encrypted_content optional) is codex-rs's local-compaction marker;
@@ -0,0 +1,98 @@
1
+ import type {
2
+ OcxProviderContinuationOwner,
3
+ OcxReasoningReplayIdentity,
4
+ } from "../types";
5
+
6
+ const bounded = (value: unknown, max: number): value is string =>
7
+ typeof value === "string" && value.length > 0 && value.length <= max;
8
+
9
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
10
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
11
+ const prototype = Object.getPrototypeOf(value);
12
+ return prototype === Object.prototype || prototype === null;
13
+ };
14
+
15
+ const defineOwn = (target: Record<string, unknown>, key: string, value: unknown): void => {
16
+ Object.defineProperty(target, key, {
17
+ value,
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ });
22
+ };
23
+
24
+ /** Recursively merge provider-owned plain objects; arrays and scalar values replace prior state. */
25
+ export function mergeProviderContinuationPayload(
26
+ inherited: Readonly<Record<string, unknown>>,
27
+ emitted: Readonly<Record<string, unknown>>,
28
+ ): Record<string, unknown> {
29
+ const merged: Record<string, unknown> = {};
30
+ for (const [key, value] of Object.entries(inherited)) defineOwn(merged, key, value);
31
+ for (const [key, value] of Object.entries(emitted)) {
32
+ const prior = Object.hasOwn(merged, key) ? merged[key] : undefined;
33
+ defineOwn(
34
+ merged,
35
+ key,
36
+ isPlainRecord(prior) && isPlainRecord(value)
37
+ ? mergeProviderContinuationPayload(prior, value)
38
+ : value,
39
+ );
40
+ }
41
+ return merged;
42
+ }
43
+
44
+ /** Validate proxy-authored continuation ownership before trusting persisted state. */
45
+ export function isValidProviderContinuationOwner(
46
+ value: unknown,
47
+ ): value is OcxProviderContinuationOwner {
48
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
49
+ const owner = value as Record<string, unknown>;
50
+ return owner.version === 1
51
+ && bounded(owner.providerName, 256)
52
+ && typeof owner.providerDestinationIdentity === "string"
53
+ && /^destination:[0-9a-f]{64}$/.test(owner.providerDestinationIdentity)
54
+ && bounded(owner.adapterName, 128)
55
+ && bounded(owner.modelId, 512)
56
+ && typeof owner.credentialIdentity === "string"
57
+ && /^(key|oauth|codex):[0-9a-f]{64}$/.test(owner.credentialIdentity);
58
+ }
59
+
60
+ /** Reuse the exact process-local route identity already established for reasoning replay. */
61
+ export function providerContinuationOwnerFromReplayIdentity(
62
+ identity: Readonly<OcxReasoningReplayIdentity> | undefined,
63
+ ): OcxProviderContinuationOwner | undefined {
64
+ if (!identity) return undefined;
65
+ const owner: OcxProviderContinuationOwner = {
66
+ version: 1,
67
+ providerName: identity.providerName,
68
+ providerDestinationIdentity: identity.providerDestinationIdentity,
69
+ adapterName: identity.adapterName,
70
+ modelId: identity.modelId,
71
+ credentialIdentity: identity.credentialIdentity,
72
+ };
73
+ return isValidProviderContinuationOwner(owner) ? owner : undefined;
74
+ }
75
+
76
+ export function sameProviderContinuationOwner(
77
+ left: OcxProviderContinuationOwner,
78
+ right: OcxProviderContinuationOwner,
79
+ ): boolean {
80
+ return left.version === right.version
81
+ && left.providerName === right.providerName
82
+ && left.providerDestinationIdentity === right.providerDestinationIdentity
83
+ && left.adapterName === right.adapterName
84
+ && left.modelId === right.modelId
85
+ && left.credentialIdentity === right.credentialIdentity;
86
+ }
87
+
88
+ /** Cursor hashes this non-secret namespace with the client thread id. */
89
+ export function providerContinuationRouteScope(owner: OcxProviderContinuationOwner): string {
90
+ return JSON.stringify([
91
+ "provider-continuation-v1",
92
+ owner.providerName,
93
+ owner.providerDestinationIdentity,
94
+ owner.adapterName,
95
+ owner.modelId,
96
+ owner.credentialIdentity,
97
+ ]);
98
+ }