@yansigit/opencodex 2.31.2 → 2.32.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 (118) hide show
  1. package/bin/ocx.mjs +99 -70
  2. package/gui/dist/assets/index-BG43zwVe.js +102 -0
  3. package/gui/dist/assets/index-CiSI-jrP.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +6 -8
  7. package/src/adapters/base.ts +2 -0
  8. package/src/adapters/command-code-project-context.ts +377 -0
  9. package/src/adapters/command-code.ts +7 -4
  10. package/src/adapters/cursor/live-models.ts +8 -0
  11. package/src/adapters/cursor/live-transport.ts +21 -0
  12. package/src/adapters/cursor/native-exec-bridge.ts +141 -0
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/tool-definitions.ts +1 -1
  15. package/src/adapters/google-http.ts +12 -2
  16. package/src/adapters/google-wire-compiler.ts +83 -2
  17. package/src/adapters/google.ts +62 -21
  18. package/src/adapters/kiro.ts +0 -3
  19. package/src/adapters/openai-responses.ts +3 -0
  20. package/src/adapters/tool-catalog-nudge.ts +1 -1
  21. package/src/adapters/xai-web-search.ts +7 -2
  22. package/src/bridge.ts +21 -15
  23. package/src/cli/dispatch.ts +50 -2
  24. package/src/cli/doctor.ts +24 -11
  25. package/src/cli/help.ts +4 -3
  26. package/src/cli/index.ts +11 -4
  27. package/src/cli/models.ts +13 -3
  28. package/src/cli/observe.ts +20 -5
  29. package/src/cli/provider.ts +2 -1
  30. package/src/cli/registry.ts +7 -5
  31. package/src/cli/status.ts +2 -1
  32. package/src/cli/system-restart-client.ts +1 -1
  33. package/src/cli/usage-report.ts +134 -0
  34. package/src/codex/app-server-processes.ts +3 -1
  35. package/src/codex/catalog/aggregation.ts +13 -1
  36. package/src/codex/catalog/effort.ts +24 -9
  37. package/src/codex/catalog/model-metadata.ts +566 -0
  38. package/src/codex/catalog/parsing.ts +33 -0
  39. package/src/codex/catalog/provider-fetch.ts +123 -32
  40. package/src/codex/catalog/sync.ts +5 -4
  41. package/src/codex/desktop-app-restart.ts +342 -0
  42. package/src/codex/history-job.ts +32 -3
  43. package/src/codex/history-manifest.ts +112 -0
  44. package/src/codex/history-migration-guardian.ts +5 -5
  45. package/src/codex/history-provider.ts +825 -247
  46. package/src/codex/history-worker.ts +8 -5
  47. package/src/codex/inject.ts +49 -21
  48. package/src/codex/injected-marker.ts +1 -1
  49. package/src/codex/internal/history-writer.ts +4 -3
  50. package/src/codex/native-profile-startup.ts +157 -27
  51. package/src/codex/native-residue.ts +26 -33
  52. package/src/combos/failover.ts +27 -0
  53. package/src/compatibility/index.ts +26 -0
  54. package/src/compatibility/manifest.ts +253 -0
  55. package/src/compatibility/openai-responses.ts +81 -0
  56. package/src/config/atomic-write.ts +219 -0
  57. package/src/config/paths.ts +40 -0
  58. package/src/config/process-state.ts +308 -0
  59. package/src/config/provider-validation.ts +177 -0
  60. package/src/config.ts +77 -812
  61. package/src/generated/compatibility-version.json +162 -94
  62. package/src/images/plan.ts +5 -4
  63. package/src/integrations/ownership-policy.ts +141 -0
  64. package/src/integrations/ownership.ts +10 -0
  65. package/src/integrations/state.ts +44 -5
  66. package/src/integrations/writer.ts +6 -0
  67. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  68. package/src/lib/bounded-body.ts +14 -2
  69. package/src/lib/process-control.ts +2 -1
  70. package/src/lib/state-store-registrations.ts +2 -0
  71. package/src/lib/tool-argument-integers.ts +56 -5
  72. package/src/oauth/health.ts +1 -1
  73. package/src/oauth/index.ts +3 -0
  74. package/src/providers/registry.ts +1 -1
  75. package/src/reasoning-effort.ts +19 -2
  76. package/src/responses/apply-patch-envelope.ts +63 -0
  77. package/src/responses/custom-tool-compat.ts +132 -38
  78. package/src/responses/parser.ts +3 -2
  79. package/src/responses/reasoning-replay-cache.ts +81 -3
  80. package/src/routing/compatibility/behavior.ts +3 -0
  81. package/src/server/auth-cors.ts +9 -7
  82. package/src/server/index.ts +102 -21
  83. package/src/server/local-management-read-client.ts +1 -1
  84. package/src/server/local-provider-reload-client.ts +1 -1
  85. package/src/server/management/agent-settings-routes.ts +1 -1
  86. package/src/server/management/config-routes.ts +4 -1
  87. package/src/server/management/context.ts +1 -1
  88. package/src/server/management/logs-usage-routes.ts +27 -6
  89. package/src/server/management/model-routes.ts +8 -4
  90. package/src/server/management/native-integration-routes.ts +2 -1
  91. package/src/server/management/provider-capability-config.ts +1 -1
  92. package/src/server/management/system-restart.ts +1 -1
  93. package/src/server/port-reclaim.ts +1 -1
  94. package/src/server/proxy-liveness.ts +2 -1
  95. package/src/server/request-log-conversation.ts +30 -0
  96. package/src/server/responses/codex-auth-error.ts +55 -0
  97. package/src/server/responses/combo-stream-preflight.ts +171 -0
  98. package/src/server/responses/compact.ts +6 -21
  99. package/src/server/responses/core.ts +242 -101
  100. package/src/server/responses/fetch-helpers.ts +2 -97
  101. package/src/server/responses-custom-tool-repair.ts +41 -5
  102. package/src/server/responses-undeclared-tool-guard.ts +156 -15
  103. package/src/service.ts +8 -4
  104. package/src/types/provider.ts +7 -0
  105. package/src/types/request.ts +12 -1
  106. package/src/types/tools.ts +87 -11
  107. package/src/types.ts +1 -1
  108. package/src/update/index.ts +5 -4
  109. package/src/update/job.ts +3 -1
  110. package/src/update/transactional-install.mjs +8 -1
  111. package/src/usage/log.ts +16 -8
  112. package/src/usage/summary.ts +201 -8
  113. package/src/vision/describe.ts +18 -13
  114. package/src/web-search/executor.ts +10 -3
  115. package/src/web-search/gemini-executor.ts +35 -13
  116. package/src/web-search/index.ts +85 -1
  117. package/gui/dist/assets/index-BAMgarF9.js +0 -102
  118. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
@@ -5,107 +5,12 @@ import {
5
5
  shouldUseCodexWsUpstream,
6
6
  type BunRuntimeGateInput,
7
7
  } from "./ws-upstream";
8
- import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
9
- import {
10
- getConfigPath,
11
- multiAgentGuidanceEnabled,
12
- resolveEnvValue,
13
- } from "../../config";
14
- import { parseRequest } from "../../responses/parser";
15
- import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
16
- import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
17
- import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
18
- import { routeModel } from "../../router";
19
- import {
20
- advanceComboAfterFailure,
21
- comboDefaultEffort,
22
- comboFailureDecision,
23
- comboIdFromRawBody,
24
- concreteComboRequestBody,
25
- getCombo,
26
- isComboTargetInCooldown,
27
- NoAvailableComboTargetsError,
28
- noteComboSuccess,
29
- parseRetryAfterMs,
30
- pickComboTarget,
31
- targetKey,
32
- } from "../../combos";
33
- import { isInjectionDebugEnabled } from "../../lib/debug-settings";
34
- import { injectionDebugLog } from "../../lib/injection-debug-log";
35
- import { modelInList, namespacedToolName } from "../../types";
36
- import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
37
- import {
38
- forceRefreshOAuthAccessSnapshot,
39
- getOAuthCredentialApiBaseUrl,
40
- getOAuthCredentialProjectId,
41
- getValidAccessTokenSnapshot,
42
- type OAuthAccessSnapshot,
43
- UnsupportedOAuthProviderError,
44
- } from "../../oauth";
45
- import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
46
- import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
47
- import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
48
- import {
49
- applyCodexAuthContextToProvider,
50
- CodexAccountCooldownError,
51
- CodexAuthContextError,
52
- CodexDirectAuthenticationError,
53
- CodexPoolAuthenticationError,
54
- CodexThreadAffinityExpiredError,
55
- headersForCodexAuthContext,
56
- isCodexAuthContextUsable,
57
- resolveCodexAuthContext,
58
- type CodexAuthContext,
59
- } from "../../codex/auth-context";
60
- import {
61
- formatCodexProviderForLog,
62
- recordCodexUpstreamOutcome,
63
- type CodexUpstreamOutcome,
64
- } from "../../codex/routing";
65
- import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../../lib/upstream-retry";
66
- import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
67
- import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
68
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
69
- import { slugsEquivalent } from "../../providers/slug-codec";
70
- import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
71
- import { isUsageDebugEnabled } from "../../usage/debug";
72
- import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
73
- import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
74
- import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
75
- import { shouldAttemptImageTierRetry } from "../image-retry";
76
- import { resolveProviderTransport } from "../../providers/xai-transport";
8
+ import type { OcxProviderConfig } from "../../types";
77
9
  import type { WsData } from "../ws-bridge";
78
- import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
79
- import { redactSecretString } from "../../lib/redact";
80
- import { readBoundedResponseBody } from "../../lib/bounded-body";
81
- import { supportedLadderFor } from "../effort-policy";
82
- import {
83
- beginRequestAttempt,
84
- catalogModelSupportsServiceTier,
85
- finishRequestAttempt,
86
- inspectResponseLogJson,
87
- noteAttemptSend,
88
- readConfiguredCodexServiceTier,
89
- requestLogSpeedLabel,
90
- sealRequestAttemptIdentity,
91
- usageFromResponsesPayload,
92
- type RequestLogContext,
93
- } from "../request-log";
94
- import type { AttemptRecoveryKind } from "../../usage/log";
95
- import {
96
- consumeForInspection,
97
- consumeForResponseLogMetadata,
98
- markNativePassthroughSseResponse,
99
- relaySseWithFailedTail,
100
- relayWithAbort,
101
- sanitizePassthroughHeaders,
102
- } from "../relay";
103
- import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
104
- import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
105
10
  import { waitForProviderRequestSlot } from "../../providers/request-pacing";
106
11
  import { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
107
12
 
108
- export { withUpstreamHttpVersion } from "../../lib/upstream-http-version";
13
+ export { withUpstreamHttpVersion };
109
14
 
110
15
  export function disableResponsesRequestTimeout(req: Request, server: Pick<Server<WsData>, "timeout"> | undefined): boolean {
111
16
  if (!server) return false;
@@ -1,4 +1,5 @@
1
1
  import type { TranslatorBudget } from "../lib/translator-budget";
2
+ import { normalizeApplyPatchDelimiters } from "../responses/apply-patch-envelope";
2
3
  import {
3
4
  customToolItemId,
4
5
  restoreRoutedCustomCalls,
@@ -85,8 +86,10 @@ type PendingArgumentBlock = {
85
86
  export function createRoutedCustomToolRestoreBlockRewrite(
86
87
  names: ReadonlySet<string>,
87
88
  budget?: TranslatorBudget,
89
+ repairNames: ReadonlySet<string> = new Set(),
88
90
  ): SseBlockRewrite {
89
- const itemNames = new Map<string, string>();
91
+ const itemNames = new Map<string, { name: string; namespace?: string }>();
92
+ const repairItemNames = new Map<string, string>();
90
93
  const ordinaryItemIds = new Set<string>();
91
94
  const openCalls = new Map<string, OpenCustomCall>();
92
95
  let pendingArguments: PendingArgumentBlock[] = [];
@@ -111,6 +114,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
111
114
  }
112
115
  pendingArguments = [];
113
116
  itemNames.clear();
117
+ repairItemNames.clear();
114
118
  ordinaryItemIds.clear();
115
119
  };
116
120
 
@@ -175,6 +179,23 @@ export function createRoutedCustomToolRestoreBlockRewrite(
175
179
  && parsed.output_index >= 0
176
180
  ? parsed.output_index
177
181
  : undefined;
182
+ if (
183
+ (type === "response.output_item.added" || type === "response.output_item.done")
184
+ && isPlainObject(parsed.item)
185
+ && parsed.item.type === "custom_tool_call"
186
+ && typeof parsed.item.name === "string"
187
+ ) {
188
+ const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
189
+ const wireName = routedCustomToolWireName(parsed.item);
190
+ const repairable = wireName !== undefined && repairNames.has(wireName);
191
+ if (upstreamItemId && repairable) repairItemNames.set(upstreamItemId, parsed.item.name);
192
+ const restored = repairable
193
+ ? restoreRoutedCustomCalls(parsed, names, repairNames)
194
+ : { value: parsed, changed: false };
195
+ return restored.changed
196
+ ? [replaceSseDataPayload(block, JSON.stringify(restored.value))]
197
+ : [block];
198
+ }
178
199
  if (
179
200
  (type === "response.output_item.added" || type === "response.output_item.done")
180
201
  && isPlainObject(parsed.item)
@@ -186,7 +207,10 @@ export function createRoutedCustomToolRestoreBlockRewrite(
186
207
  const routed = wireName !== undefined && names.has(wireName);
187
208
  if (upstreamItemId) {
188
209
  if (routed) {
189
- itemNames.set(upstreamItemId, parsed.item.name);
210
+ itemNames.set(upstreamItemId, {
211
+ name: parsed.item.name,
212
+ ...(typeof parsed.item.namespace === "string" ? { namespace: parsed.item.namespace } : {}),
213
+ });
190
214
  ordinaryItemIds.delete(upstreamItemId);
191
215
  } else {
192
216
  ordinaryItemIds.add(upstreamItemId);
@@ -203,7 +227,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
203
227
  if (upstreamItemId && pending.length > 0 && !openCalls.has(upstreamItemId)) {
204
228
  openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
205
229
  }
206
- const restored = restoreRoutedCustomCalls(parsed, names);
230
+ const restored = restoreRoutedCustomCalls(parsed, names, repairNames);
207
231
  const restoredBlock = restored.changed
208
232
  ? replaceSseDataPayload(block, JSON.stringify(restored.value))
209
233
  : block;
@@ -215,6 +239,17 @@ export function createRoutedCustomToolRestoreBlockRewrite(
215
239
  }
216
240
 
217
241
  const upstreamItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
242
+ if (
243
+ type === "response.custom_tool_call_input.done"
244
+ && upstreamItemId
245
+ && repairItemNames.has(upstreamItemId)
246
+ && typeof parsed.input === "string"
247
+ ) {
248
+ const input = normalizeApplyPatchDelimiters(parsed.input);
249
+ if (input !== parsed.input) {
250
+ return [replaceSseDataPayload(block, JSON.stringify({ ...parsed, input }))];
251
+ }
252
+ }
218
253
  const argumentEvent = type === "response.function_call_arguments.delta"
219
254
  || type === "response.function_call_arguments.done";
220
255
  if (argumentEvent && (!upstreamItemId || (!itemNames.has(upstreamItemId) && !ordinaryItemIds.has(upstreamItemId)))) {
@@ -261,16 +296,17 @@ export function createRoutedCustomToolRestoreBlockRewrite(
261
296
  ? parsed.arguments
262
297
  : openCalls.get(upstreamItemId)?.argumentsText ?? "";
263
298
  const { arguments: _arguments, ...rest } = parsed;
299
+ const itemName = itemNames.get(upstreamItemId);
264
300
  const next = {
265
301
  ...rest,
266
302
  type: nextType,
267
303
  item_id: customToolItemId(upstreamItemId),
268
- input: unwrapRoutedCustomToolArguments(source),
304
+ input: unwrapRoutedCustomToolArguments(source, itemName?.name ?? "", itemName?.namespace),
269
305
  };
270
306
  return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
271
307
  }
272
308
 
273
- const restored = restoreRoutedCustomCalls(parsed, names);
309
+ const restored = restoreRoutedCustomCalls(parsed, names, repairNames);
274
310
  const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
275
311
  if (terminal) releaseAll();
276
312
  return restored.changed
@@ -1,13 +1,41 @@
1
1
  import { namespacedToolName } from "../types";
2
2
  import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
3
3
 
4
- /**
5
- * Item types the CLIENT executes by name. Hosted calls (`web_search_call`,
6
- * `image_generation_call`, `local_shell_call`, `tool_search_call`, …) are run upstream or
7
- * carry no tool name, so they are never matched against the request catalog.
8
- */
4
+ /** Item types the client executes through a request-declared wire name. */
9
5
  const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
10
6
 
7
+ /** Nameless declaration kinds whose response items still require client execution. */
8
+ const NAMELESS_CLIENT_DECLARATION_CALL_TYPES = new Map([
9
+ ["local_shell", "local_shell_call"],
10
+ ["tool_search", "tool_search_call"],
11
+ ["computer_use_preview", "computer_call"],
12
+ ["computer_use", "computer_call"],
13
+ ]);
14
+
15
+ const NAMELESS_CLIENT_CALL_DISPLAY_NAMES = new Map([
16
+ ["local_shell_call", "local_shell"],
17
+ ["tool_search_call", "tool_search"],
18
+ ["computer_call", "computer_use"],
19
+ ]);
20
+
21
+ const EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES: ReadonlySet<string> = new Set();
22
+
23
+ /** Supported hosted/private declarations that carry no client-executable wire name. */
24
+ const NAMELESS_TOOL_SPEC_TYPES = new Set([
25
+ "web_search",
26
+ "web_search_preview",
27
+ "file_search",
28
+ "computer_use_preview",
29
+ "computer_use",
30
+ "code_interpreter",
31
+ "image_generation",
32
+ "image_gen",
33
+ "mcp",
34
+ "tool_search",
35
+ "local_shell",
36
+ "x_search",
37
+ ]);
38
+
11
39
  /** An upstream-supplied name reaches the error message; keep it bounded. */
12
40
  const MAX_REPORTED_NAME_CHARS = 100;
13
41
 
@@ -18,12 +46,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
18
46
  }
19
47
 
20
48
  function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
21
- if (!isPlainObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return;
22
- names.add(tool.name);
49
+ if (!isPlainObject(tool)) return;
50
+ const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
51
+ ? tool.function
52
+ : undefined;
53
+ const name = typeof tool.name === "string" && tool.name.length > 0
54
+ ? tool.name
55
+ : typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
56
+ ? nestedFunction.name
57
+ : undefined;
58
+ if (!name) return;
59
+ names.add(name);
23
60
  // Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
24
61
  // as a bare inner name or as the flattened form; accept both rather than guess which
25
62
  // coordinate system this provider echoes back.
26
- if (namespace) names.add(namespacedToolName(namespace, tool.name));
63
+ if (namespace) names.add(namespacedToolName(namespace, name));
64
+ }
65
+
66
+ /**
67
+ * Catalog view owned by the current Responses turn.
68
+ *
69
+ * `previous_response_id` expansion prepends stored input items, including historical
70
+ * `additional_tools` declarations. Those items remain conversation history but cannot grant
71
+ * execution authority to this turn. Top-level `tools` always belongs to the current request;
72
+ * only input catalogs at or after the replay boundary are current.
73
+ */
74
+ export function currentTurnWireToolCatalogBody(
75
+ body: unknown,
76
+ replayPrefixLength: number | undefined,
77
+ ): unknown {
78
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
79
+ if (typeof replayPrefixLength !== "number" || !Number.isFinite(replayPrefixLength)) return body;
80
+ const start = Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)));
81
+ if (start === 0) return body;
82
+ return { ...body, input: body.input.slice(start) };
27
83
  }
28
84
 
29
85
  function addWireToolSpecs(names: Set<string>, specs: unknown): void {
@@ -53,15 +109,97 @@ export function collectDeclaredWireToolNames(body: unknown): Set<string> {
53
109
  addWireToolSpecs(names, body.tools);
54
110
  if (Array.isArray(body.input)) {
55
111
  for (const item of body.input) {
56
- if (isPlainObject(item) && item.type === "additional_tools") addWireToolSpecs(names, item.tools);
112
+ if (
113
+ isPlainObject(item)
114
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
115
+ ) addWireToolSpecs(names, item.tools);
57
116
  }
58
117
  }
59
118
  return names;
60
119
  }
61
120
 
62
- function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): string | undefined {
121
+ function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
122
+ if (!Array.isArray(specs)) return;
123
+ for (const spec of specs) {
124
+ if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
125
+ const callType = NAMELESS_CLIENT_DECLARATION_CALL_TYPES.get(spec.type);
126
+ if (callType) callTypes.add(callType);
127
+ }
128
+ }
129
+
130
+ /** Nameless client-call item types authorized by supported request tool declarations. */
131
+ export function collectDeclaredNamelessClientCallTypes(body: unknown): Set<string> {
132
+ const callTypes = new Set<string>();
133
+ if (!isPlainObject(body)) return callTypes;
134
+ addNamelessClientCallTypes(callTypes, body.tools);
135
+ if (Array.isArray(body.input)) {
136
+ for (const item of body.input) {
137
+ if (
138
+ isPlainObject(item)
139
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
140
+ ) {
141
+ addNamelessClientCallTypes(callTypes, item.tools);
142
+ }
143
+ }
144
+ }
145
+ return callTypes;
146
+ }
147
+
148
+ function isReadableWireToolSpec(spec: unknown): boolean {
149
+ if (!isPlainObject(spec) || typeof spec.type !== "string" || spec.type.length === 0) return false;
150
+ if (spec.type === "function") {
151
+ return (typeof spec.name === "string" && spec.name.length > 0)
152
+ || (isPlainObject(spec.function)
153
+ && typeof spec.function.name === "string"
154
+ && spec.function.name.length > 0);
155
+ }
156
+ if (spec.type === "custom") return typeof spec.name === "string" && spec.name.length > 0;
157
+ if (spec.type === "namespace") {
158
+ return typeof spec.name === "string"
159
+ && spec.name.length > 0
160
+ && Array.isArray(spec.tools)
161
+ && (spec.tools.length === 0 || spec.tools.some(inner =>
162
+ isPlainObject(inner)
163
+ && (inner.type === "function" || inner.type === "custom")
164
+ && typeof inner.name === "string"
165
+ && inner.name.length > 0
166
+ ));
167
+ }
168
+ if (NAMELESS_TOOL_SPEC_TYPES.has(spec.type)) return true;
169
+ return typeof spec.name === "string" && spec.name.length > 0;
170
+ }
171
+
172
+ function isReadableWireToolCatalog(value: unknown): boolean {
173
+ return Array.isArray(value)
174
+ && (value.length === 0 || value.some(isReadableWireToolSpec));
175
+ }
176
+
177
+ /** Whether a request contains a supported catalog, including an explicit empty deny-all array. */
178
+ export function hasExplicitWireToolCatalog(body: unknown): boolean {
179
+ if (!isPlainObject(body)) return false;
180
+ if (isReadableWireToolCatalog(body.tools)) return true;
181
+ if (!Array.isArray(body.input)) return false;
182
+ return body.input.some(item =>
183
+ isPlainObject(item)
184
+ && item.type === "additional_tools"
185
+ && isReadableWireToolCatalog(item.tools)
186
+ );
187
+ }
188
+
189
+ function undeclaredNameInItem(
190
+ item: unknown,
191
+ declared: ReadonlySet<string>,
192
+ declaredNamelessClientCallTypes: ReadonlySet<string>,
193
+ ): string | undefined {
63
194
  if (!isPlainObject(item)) return undefined;
64
- if (typeof item.type !== "string" || !CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
195
+ if (typeof item.type !== "string") return undefined;
196
+ const namelessDisplayName = NAMELESS_CLIENT_CALL_DISPLAY_NAMES.get(item.type);
197
+ if (namelessDisplayName !== undefined) {
198
+ // Only Codex's explicit `execution: "client"` form delegates tool search to the client.
199
+ if (item.type === "tool_search_call" && item.execution !== "client") return undefined;
200
+ return declaredNamelessClientCallTypes.has(item.type) ? undefined : namelessDisplayName;
201
+ }
202
+ if (!CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
65
203
  const name = item.name;
66
204
  if (typeof name !== "string" || name.length === 0) return undefined;
67
205
  if (declared.has(name)) return undefined;
@@ -75,14 +213,15 @@ function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): str
75
213
  export function undeclaredToolCallName(
76
214
  payload: unknown,
77
215
  declared: ReadonlySet<string>,
216
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
78
217
  ): string | undefined {
79
218
  if (!isPlainObject(payload)) return undefined;
80
219
  if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
81
- return undeclaredNameInItem(payload.item, declared);
220
+ return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes);
82
221
  }
83
222
  // Sparse gateways skip incremental items and only ever ship the terminal snapshot.
84
223
  if (payload.type === "response.completed" || payload.type === "response.incomplete") {
85
- return undeclaredToolCallNameInResponse(payload.response, declared);
224
+ return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes);
86
225
  }
87
226
  return undefined;
88
227
  }
@@ -91,10 +230,11 @@ export function undeclaredToolCallName(
91
230
  export function undeclaredToolCallNameInResponse(
92
231
  response: unknown,
93
232
  declared: ReadonlySet<string>,
233
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
94
234
  ): string | undefined {
95
235
  if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
96
236
  for (const item of response.output) {
97
- const name = undeclaredNameInItem(item, declared);
237
+ const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes);
98
238
  if (name !== undefined) return name;
99
239
  }
100
240
  return undefined;
@@ -133,6 +273,7 @@ function failedBlocks(name: string, newline: string): readonly string[] {
133
273
  */
134
274
  export function createUndeclaredToolCallGuardBlockRewrite(
135
275
  declared: ReadonlySet<string>,
276
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
136
277
  ): SseBlockRewrite {
137
278
  let tripped = false;
138
279
  return (block: string) => {
@@ -145,7 +286,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
145
286
  } catch {
146
287
  return [block];
147
288
  }
148
- const name = undeclaredToolCallName(parsed, declared);
289
+ const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes);
149
290
  if (name === undefined) return [block];
150
291
  tripped = true;
151
292
  return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
package/src/service.ts CHANGED
@@ -10,8 +10,8 @@ import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/
10
10
  import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { homedir, tmpdir } from "node:os";
12
12
  import { dirname, join, posix, resolve, win32 } from "node:path";
13
- import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
14
- import { loadConfig } from "./config";
13
+ import { expandUserPath, getConfigDir, loadConfig } from "./config";
14
+ import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config/process-state";
15
15
  import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
16
16
  import { stripGrokConfig } from "./grok/inject";
17
17
  import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
@@ -97,13 +97,17 @@ function defaultOpenCodexHome(): string {
97
97
  return resolve(join(homedir(), ".opencodex"));
98
98
  }
99
99
 
100
- function serviceStatePaths(): string[] {
101
- const paths = [serviceStatePath()];
100
+ export function serviceStatePathsForOpenCodexHome(opencodexHome: string): string[] {
101
+ const paths = [join(opencodexHome, "service-state.json")];
102
102
  const defaultPath = join(defaultOpenCodexHome(), "service-state.json");
103
103
  if (normalizePathForCompare(defaultPath) !== normalizePathForCompare(paths[0])) paths.push(defaultPath);
104
104
  return paths;
105
105
  }
106
106
 
107
+ function serviceStatePaths(): string[] {
108
+ return serviceStatePathsForOpenCodexHome(currentOpenCodexHome());
109
+ }
110
+
107
111
  function currentCodexHome(deps: CodexHomeDeps = {}): string {
108
112
  // Service ownership must identify the same home as the runtime. In WSL an
109
113
  // unset CODEX_HOME can resolve to the single Windows Desktop home rather than
@@ -169,6 +169,13 @@ export interface OcxProviderConfig {
169
169
  * version here instead of waiting for a code change. Absent uses the adapter's current default.
170
170
  */
171
171
  commandCodeVersion?: string;
172
+ /**
173
+ * Command Code OAuth `/alpha/generate` project-context envelope. When `"on"`, the adapter
174
+ * fills `memory`, `taste`, and `skills` from bounded files under `process.cwd()`. Absent or
175
+ * `"off"` keeps the empty envelope (`memory: ""`, `taste: null`, `skills: null`). Does not
176
+ * enable taste learning (`x-taste-learning` stays `"false"`).
177
+ */
178
+ projectContext?: "off" | "on";
172
179
  /**
173
180
  * Responses upstream that stores nothing server-side (DeepSeek documents "the API
174
181
  * is stateless"). Stateful request parameters are dropped, `store` is pinned false,
@@ -30,6 +30,11 @@ export interface OcxReasoningReplayIdentity {
30
30
  * the holder, so late tool-call cache writes see the active physical identity.
31
31
  */
32
32
  export interface OcxReasoningReplayScopeRef {
33
+ /**
34
+ * Conversation namespace for replay state. Historically this was always the Codex parent-thread
35
+ * id; headerless Responses callers use a raw sanitized thread/Cursor/session fallback, never the
36
+ * hashed request-log conversation id.
37
+ */
33
38
  readonly clientThreadId: string;
34
39
  current?: Readonly<OcxReasoningReplayIdentity>;
35
40
  }
@@ -63,7 +68,7 @@ export interface OcxParsedRequest {
63
68
  _cursorConversationId?: string;
64
69
  /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
65
70
  _clientThreadId?: string;
66
- /** Provider/account/model-bound namespace for process-local raw-reasoning replay. */
71
+ /** Conversation/provider/account/model-bound namespace for reasoning replay state. */
67
72
  _reasoningReplayScope?: OcxReasoningReplayScopeRef;
68
73
  /**
69
74
  * Set by bindRouteReasoningReplayScope after a proven serving-identity change, or by
@@ -94,6 +99,12 @@ export interface OcxParsedRequest {
94
99
  * executes searches via the gpt-5.4-mini sidecar (see src/web-search). Absent when not requested.
95
100
  */
96
101
  _webSearch?: Record<string, unknown>;
102
+ /**
103
+ * Antigravity Gemini in-turn CCA grounding: google_search and optional url_context ride the main
104
+ * routed fetch instead of the web-search sidecar loop. Set by core.ts when resolveCcaInTurnGrounding
105
+ * matches; consumed by the Google adapter at buildRequest/parseStream time.
106
+ */
107
+ _ccaInTurnGrounding?: { search: boolean; urlContext: boolean };
97
108
  /** Hosted image_generation tool config stashed for the image bridge sidecar (see src/images). */
98
109
  _imageGeneration?: { toolNames: Set<string>; originalTool?: Record<string, unknown> };
99
110
  /**
@@ -43,6 +43,74 @@ function sameToolIdentity(
43
43
  return left.namespace === right.namespace && left.name === right.name;
44
44
  }
45
45
 
46
+ type ToolIdentity = Readonly<Pick<OcxTool, "namespace" | "name">>;
47
+
48
+ function snapshotToolIdentity(tool: Pick<OcxTool, "namespace" | "name">): ToolIdentity {
49
+ return Object.freeze({
50
+ name: tool.name,
51
+ ...(tool.namespace === undefined ? {} : { namespace: tool.namespace }),
52
+ });
53
+ }
54
+
55
+ function buildToolChoiceCatalog(
56
+ tools: readonly ToolIdentity[],
57
+ ): {
58
+ candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
59
+ sourceCandidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>;
60
+ identitiesByTool: WeakMap<object, ToolIdentity>;
61
+ } {
62
+ const index = new Map<string, ToolIdentity[]>();
63
+ const sourceIndex = new Map<string, ToolIdentity[]>();
64
+ const identities = new Map<string, Set<string>>();
65
+ const identitiesByTool = new WeakMap<object, ToolIdentity>();
66
+ for (const tool of tools) {
67
+ const snapshot = snapshotToolIdentity(tool);
68
+ identitiesByTool.set(tool, snapshot);
69
+ const identity = JSON.stringify([snapshot.namespace ?? null, snapshot.name]);
70
+ for (const selector of [...toolChoiceAliases(snapshot), snapshot.name]) {
71
+ const candidates = index.get(selector);
72
+ if (!candidates) {
73
+ index.set(selector, [snapshot]);
74
+ sourceIndex.set(selector, [tool]);
75
+ identities.set(selector, new Set([identity]));
76
+ } else if (!identities.get(selector)!.has(identity)) {
77
+ candidates.push(snapshot);
78
+ sourceIndex.get(selector)!.push(tool);
79
+ identities.get(selector)!.add(identity);
80
+ }
81
+ }
82
+ }
83
+ return { candidatesByName: index, sourceCandidatesByName: sourceIndex, identitiesByTool };
84
+ }
85
+
86
+ /** Compile one immutable view of a request's tool catalog for repeated policy checks. */
87
+ export function createToolChoiceResolver(tools: readonly ToolIdentity[] | undefined) {
88
+ const compiled = tools ? buildToolChoiceCatalog(tools) : undefined;
89
+ const candidatesByName = compiled?.candidatesByName;
90
+ const snapshotFor = (tool: ToolIdentity): ToolIdentity | undefined => {
91
+ const snapshot = compiled?.identitiesByTool.get(tool);
92
+ return snapshot && sameToolIdentity(snapshot, tool) ? snapshot : undefined;
93
+ };
94
+ return {
95
+ candidates(name: string): ToolIdentity[] {
96
+ return (candidatesByName?.get(name) ?? []).map(candidate => ({ ...candidate }));
97
+ },
98
+ candidateCount(name: string): number {
99
+ return candidatesByName?.get(name)?.length ?? 0;
100
+ },
101
+ allows(tool: ToolIdentity, allowedTools: ReadonlySet<string>): boolean {
102
+ if (!candidatesByName) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
103
+ const snapshot = snapshotFor(tool);
104
+ return snapshot ? toolAllowedByChoiceFromIndex(snapshot, allowedTools, candidatesByName) : false;
105
+ },
106
+ selects(tool: ToolIdentity, name: string): boolean {
107
+ const snapshot = snapshotFor(tool);
108
+ const candidates = candidatesByName?.get(name);
109
+ return !!snapshot && candidates?.length === 1 && sameToolIdentity(candidates[0], snapshot);
110
+ },
111
+ };
112
+ }
113
+
46
114
  /**
47
115
  * All tools that could be selected by one client-facing name. Bare logical names are included
48
116
  * here because they are a compatibility selector for namespaced tools, while wire and dotted
@@ -53,12 +121,7 @@ export function toolChoiceCandidates(
53
121
  name: string,
54
122
  ): Pick<OcxTool, "namespace" | "name">[] {
55
123
  if (!tools) return [];
56
- const candidates: Pick<OcxTool, "namespace" | "name">[] = [];
57
- for (const tool of tools) {
58
- if (tool.name !== name && !toolChoiceAliases(tool).includes(name)) continue;
59
- if (!candidates.some(candidate => sameToolIdentity(candidate, tool))) candidates.push(tool);
60
- }
61
- return candidates;
124
+ return [...(buildToolChoiceCatalog(tools).sourceCandidatesByName.get(name) ?? [])];
62
125
  }
63
126
 
64
127
  /**
@@ -72,10 +135,22 @@ export function toolAllowedByChoice(
72
135
  tools?: readonly Pick<OcxTool, "namespace" | "name">[],
73
136
  ): boolean {
74
137
  if (!tools) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
138
+ return toolAllowedByChoiceFromIndex(
139
+ snapshotToolIdentity(tool),
140
+ allowedTools,
141
+ buildToolChoiceCatalog(tools).candidatesByName,
142
+ );
143
+ }
144
+
145
+ function toolAllowedByChoiceFromIndex(
146
+ tool: ToolIdentity,
147
+ allowedTools: ReadonlySet<string>,
148
+ candidatesByName: ReadonlyMap<string, readonly ToolIdentity[]>,
149
+ ): boolean {
75
150
  for (const name of [...toolChoiceAliases(tool), tool.name]) {
76
151
  if (!allowedTools.has(name)) continue;
77
- const candidates = toolChoiceCandidates(tools, name);
78
- if (candidates.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
152
+ const candidates = candidatesByName.get(name);
153
+ if (candidates?.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
79
154
  }
80
155
  return false;
81
156
  }
@@ -123,9 +198,10 @@ export function toolChoiceToolPredicate(
123
198
  if (choice === "none") return () => false;
124
199
  if (isAllowedToolChoice(choice)) {
125
200
  const allowed = new Set(choice.allowedTools);
126
- return tool => toolAllowedByChoice(tool, allowed, tools);
201
+ const resolver = createToolChoiceResolver(tools);
202
+ return tool => resolver.allows(tool, allowed);
127
203
  }
128
204
  if (!tools) return tool => toolChoiceAliases(tool).includes(choice.name);
129
- const candidates = toolChoiceCandidates(tools, choice.name);
130
- return tool => candidates.length === 1 && sameToolIdentity(candidates[0], tool);
205
+ const resolver = createToolChoiceResolver(tools);
206
+ return tool => resolver.selects(tool, choice.name);
131
207
  }