@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
@@ -48,11 +48,12 @@ export async function planImageBridge(
48
48
  if (config.images?.bridgeEnabled !== true) return undefined;
49
49
  if (!parsed._imageGeneration) return undefined;
50
50
  const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
51
- const toolNames = new Set(
52
- [...parsed._imageGeneration.toolNames, IMAGE_GEN_TOOL_NAME]
53
- .filter(name => toolAllowed({ name })),
54
- );
51
+ const toolNames = new Set([...parsed._imageGeneration.toolNames].filter(name => toolAllowed({ name })));
52
+ if (toolAllowed({ name: IMAGE_GEN_TOOL_NAME })) toolNames.add(IMAGE_GEN_TOOL_NAME);
55
53
  if (toolNames.size === 0) return undefined;
54
+ // Responses advertises and rewrites authorized aliases to this synthetic name, so the loop
55
+ // must always intercept it once any image-generation name has armed the bridge.
56
+ toolNames.add(IMAGE_GEN_TOOL_NAME);
56
57
  // Don't intercept for OpenAI native passthrough
57
58
  const host = (() => { try { return new URL(routedProvider.baseUrl).hostname; } catch { return ""; } })();
58
59
  if (host === "api.openai.com") return undefined;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Client-scoped exceptions to strict managed-fragment ownership.
3
+ *
4
+ * Most clients must preserve every byte represented by their managed
5
+ * contribution. A client that persists runtime-derived fields back into an
6
+ * OpenCodex-owned fragment needs a narrower contract: name the exact paths it
7
+ * may rewrite, then fingerprint everything else. Keeping that policy here
8
+ * prevents a client quirk from weakening the shared classifier.
9
+ */
10
+ import {
11
+ OPENCODE_PROVIDER_ID,
12
+ type ManagedContribution,
13
+ type ManagedFragment,
14
+ } from "../clients/config-export";
15
+ import { canonicalContribution, fingerprint } from "./ownership";
16
+
17
+ type JsonObject = Record<string, unknown>;
18
+
19
+ function isObject(value: unknown): value is JsonObject {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+
23
+ function pathStartsWith(path: readonly string[], prefix: readonly string[]): boolean {
24
+ return prefix.length <= path.length && prefix.every((part, index) => path[index] === part);
25
+ }
26
+
27
+ /** Delete one nested object member and prune only the empty ancestors created by that deletion. */
28
+ function deletePath(root: unknown, path: readonly string[]): void {
29
+ if (!isObject(root) || path.length === 0) return;
30
+ const parents: Array<{ parent: JsonObject; key: string }> = [];
31
+ let cursor: JsonObject = root;
32
+ for (let index = 0; index < path.length - 1; index += 1) {
33
+ const key = path[index]!;
34
+ const next = cursor[key];
35
+ if (!isObject(next)) return;
36
+ parents.push({ parent: cursor, key });
37
+ cursor = next;
38
+ }
39
+ delete cursor[path[path.length - 1]!];
40
+ for (let index = parents.length - 1; index >= 0; index -= 1) {
41
+ if (Object.keys(cursor).length > 0) break;
42
+ const { parent, key } = parents[index]!;
43
+ delete parent[key];
44
+ cursor = parent;
45
+ }
46
+ }
47
+
48
+ function cloneFragment(fragment: ManagedFragment): ManagedFragment {
49
+ return {
50
+ path: [...fragment.path],
51
+ value: structuredClone(fragment.value),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Paths a client is documented to derive after OpenCodex writes its block.
57
+ *
58
+ * ZCode 3.8.1 persists reasoning and output defaults for every generated
59
+ * model. It may also fill a context default when OpenCodex intentionally
60
+ * omitted one; an authoritative context emitted by OpenCodex remains
61
+ * protected and is never listed here.
62
+ */
63
+ export function refreshablePathsOf(
64
+ contribution: ManagedContribution,
65
+ ): readonly (readonly string[])[] {
66
+ if (contribution.clientId !== "zcode") return [];
67
+ const fragment = contribution.fragments.find(candidate => (
68
+ candidate.path.length === 2
69
+ && candidate.path[0] === "provider"
70
+ && candidate.path[1] === OPENCODE_PROVIDER_ID
71
+ ));
72
+ if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return [];
73
+
74
+ const paths: string[][] = [];
75
+ for (const modelId of Object.keys(fragment.value.models).sort()) {
76
+ const entry = fragment.value.models[modelId];
77
+ if (!isObject(entry)) continue;
78
+ const base = [...fragment.path, "models", modelId];
79
+ paths.push([...base, "reasoning"]);
80
+ paths.push([...base, "limit", "output"]);
81
+ const limit = entry.limit;
82
+ if (!isObject(limit) || typeof limit.context !== "number") {
83
+ paths.push([...base, "limit", "context"]);
84
+ }
85
+ }
86
+ return paths;
87
+ }
88
+
89
+ export function validRefreshablePaths(
90
+ contribution: ManagedContribution,
91
+ value: unknown,
92
+ ): value is readonly (readonly string[])[] {
93
+ if (contribution.clientId !== "zcode" || !Array.isArray(value) || value.length === 0) {
94
+ return false;
95
+ }
96
+ const fragment = contribution.fragments.find(candidate => (
97
+ candidate.path.length === 2
98
+ && candidate.path[0] === "provider"
99
+ && candidate.path[1] === OPENCODE_PROVIDER_ID
100
+ ));
101
+ if (!fragment || !isObject(fragment.value) || !isObject(fragment.value.models)) return false;
102
+
103
+ const modelIds = new Set(Object.keys(fragment.value.models));
104
+ const seen = new Set<string>();
105
+ return value.every(path => {
106
+ if (!Array.isArray(path) || !path.every(part => typeof part === "string")) return false;
107
+ const modelId = path[3];
108
+ const isReasoning = path.length === 5 && path[4] === "reasoning";
109
+ const isLimitDefault = path.length === 6
110
+ && path[4] === "limit"
111
+ && (path[5] === "output" || path[5] === "context");
112
+ const key = path.join("\u0000");
113
+ if (
114
+ path[0] !== "provider"
115
+ || path[1] !== OPENCODE_PROVIDER_ID
116
+ || path[2] !== "models"
117
+ || typeof modelId !== "string"
118
+ || !modelIds.has(modelId)
119
+ || (!isReasoning && !isLimitDefault)
120
+ || seen.has(key)
121
+ ) return false;
122
+ seen.add(key);
123
+ return true;
124
+ });
125
+ }
126
+
127
+ /** Fingerprint a contribution after removing only its explicitly refreshable paths. */
128
+ export function protectedContributionFingerprint(
129
+ contribution: ManagedContribution,
130
+ refreshablePaths: readonly (readonly string[])[],
131
+ ): string {
132
+ const fragments = contribution.fragments.map(cloneFragment);
133
+ for (const refreshablePath of refreshablePaths) {
134
+ for (const fragment of fragments) {
135
+ if (!pathStartsWith(refreshablePath, fragment.path)) continue;
136
+ deletePath(fragment.value, refreshablePath.slice(fragment.path.length));
137
+ break;
138
+ }
139
+ }
140
+ return fingerprint(canonicalContribution({ ...contribution, fragments }));
141
+ }
@@ -41,6 +41,16 @@ export interface OwnershipRecord {
41
41
  fileFingerprint: string;
42
42
  /** Hash of our contribution — detects catalog/port drift. */
43
43
  blockFingerprint: string;
44
+ /**
45
+ * Hash of the fields the client must not rewrite. Present only when a
46
+ * client has explicitly declared runtime-derived paths below.
47
+ */
48
+ protectedBlockFingerprint?: string;
49
+ /**
50
+ * Exact document paths a client may derive after apply. These are recorded
51
+ * per operation so later catalog changes cannot widen an older grant.
52
+ */
53
+ refreshablePaths?: readonly (readonly string[])[];
44
54
  /** The exact paths we own. Removal touches these and nothing else. */
45
55
  fragmentPaths: readonly (readonly string[])[];
46
56
  /**
@@ -13,6 +13,11 @@ import type { OcxConfig } from "../types";
13
13
  import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
14
14
  import { SNAPSHOT_RETENTION } from "./journal";
15
15
  import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
16
+ import {
17
+ protectedContributionFingerprint,
18
+ refreshablePathsOf,
19
+ validRefreshablePaths,
20
+ } from "./ownership-policy";
16
21
  import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
17
22
  import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
18
23
 
@@ -106,10 +111,10 @@ export function blockedContainerPath(
106
111
  * values lets another integration or a user add a sibling without blocking a
107
112
  * later refresh, while a change inside our block still fails closed.
108
113
  */
109
- function recordedFragmentFingerprint(
114
+ function recordedContribution(
110
115
  doc: unknown,
111
116
  record: OwnershipRecord,
112
- ): string | null {
117
+ ): ManagedContribution | null {
113
118
  if (
114
119
  !Array.isArray(record.fragmentPaths)
115
120
  || record.fragmentPaths.length === 0
@@ -125,10 +130,44 @@ function recordedFragmentFingerprint(
125
130
  if (value === undefined) return null;
126
131
  fragments.push({ path, value });
127
132
  }
128
- return fingerprint(canonicalContribution({
133
+ return {
129
134
  clientId: record.clientId,
130
135
  fragments,
131
- }));
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Prove that every protected field still matches what OpenCodex wrote.
141
+ *
142
+ * New records carry an operation-scoped protected fingerprint and the exact
143
+ * paths excluded from it. Legacy records can recover only when the desired
144
+ * contribution has not moved since apply; otherwise catalog drift and a
145
+ * foreign edit are indistinguishable, so the classifier keeps failing closed.
146
+ */
147
+ function recordedBlockIsOwned(
148
+ doc: unknown,
149
+ record: OwnershipRecord,
150
+ desired: ManagedContribution,
151
+ ): boolean {
152
+ const observed = recordedContribution(doc, record);
153
+ if (!observed) return false;
154
+ if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
155
+
156
+ if (
157
+ typeof record.protectedBlockFingerprint === "string"
158
+ && validRefreshablePaths(observed, record.refreshablePaths)
159
+ && record.refreshablePaths.length > 0
160
+ ) {
161
+ return protectedContributionFingerprint(observed, record.refreshablePaths)
162
+ === record.protectedBlockFingerprint;
163
+ }
164
+
165
+ const desiredFingerprint = fingerprint(canonicalContribution(desired));
166
+ if (desiredFingerprint !== record.blockFingerprint) return false;
167
+ const legacyPaths = refreshablePathsOf(desired);
168
+ return legacyPaths.length > 0
169
+ && protectedContributionFingerprint(observed, legacyPaths)
170
+ === protectedContributionFingerprint(desired, legacyPaths);
132
171
  }
133
172
 
134
173
  /**
@@ -191,7 +230,7 @@ export function classifyIntegration(input: {
191
230
  * conflict no matter what the rest of the file looks like, so the sibling-
192
231
  * edit exemption below can never mask it.
193
232
  */
194
- if (recordedFragmentFingerprint(input.parsed, input.record) !== input.record.blockFingerprint) {
233
+ if (!recordedBlockIsOwned(input.parsed, input.record, input.contribution)) {
195
234
  return { state: "conflict", reason: "foreign-edit" };
196
235
  }
197
236
  if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
@@ -16,6 +16,7 @@ import { isLoopbackHostname } from "../codex/inject";
16
16
  import type { OcxConfig } from "../types";
17
17
  import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
18
18
  import { fingerprint, canonicalContribution, fragmentPathsOf, type OwnershipRecord } from "./ownership";
19
+ import { protectedContributionFingerprint, refreshablePathsOf } from "./ownership-policy";
19
20
  import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
20
21
  import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
21
22
  import { classifyIntegration, exportContextOf } from "./state";
@@ -353,12 +354,17 @@ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: bo
353
354
  opId, clientId, kind: classified.state === "stale" ? "refresh" : "apply", at, configPath,
354
355
  snapshot, resultFingerprint: fingerprint(text), resultAbsent: false, priorRecord: record,
355
356
  };
357
+ const refreshablePaths = refreshablePathsOf(contribution);
356
358
  return commit({
357
359
  io, store, clientId, configPath, before, nextText: text, state: "current",
358
360
  priorRecord: record,
359
361
  record: {
360
362
  clientId, configPath, fileFingerprint: fingerprint(text),
361
363
  blockFingerprint: fingerprint(canonicalContribution(contribution)),
364
+ ...(refreshablePaths.length > 0 ? {
365
+ protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
366
+ refreshablePaths,
367
+ } : {}),
362
368
  fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
363
369
  appliedAt: at, opId,
364
370
  },
@@ -3,7 +3,7 @@ import { jcsStringify } from "../digest";
3
3
  import type { LabBehaviorSource, LabBehaviorValues } from "../live/types";
4
4
 
5
5
  const CLOSED_KEYS = new Set([
6
- "wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
6
+ "wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.commandCodeProjectContext", "wire.modelSuffixMode",
7
7
  "auth.mode", "auth.transport",
8
8
  "responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.fastWireKind", "responses.fastWireValue", "responses.snapshotRepair", "responses.itemIdRepair",
9
9
  "limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
@@ -103,6 +103,16 @@ function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, r
103
103
  }
104
104
  }
105
105
 
106
+ function cancelBodyWithoutWaiting(body: ReadableStream<Uint8Array>, reason?: unknown): void {
107
+ // A signal can already be aborted before a reader is attached. Still settle the
108
+ // original body so fetch-backed streams cannot retain a rejected read in that gap.
109
+ try {
110
+ void body.cancel(reason).catch(() => undefined);
111
+ } catch {
112
+ // A locked or non-conforming stream may throw synchronously from cancel().
113
+ }
114
+ }
115
+
106
116
  /**
107
117
  * Consume the original response body as raw bytes under a strict memory ceiling.
108
118
  *
@@ -208,9 +218,11 @@ export async function readBoundedResponseBody(
208
218
  options: BoundedBodyOptions = {},
209
219
  ): Promise<BoundedBodyResult> {
210
220
  const signal = options.signal;
211
- if (signal?.aborted) throw signal.reason;
212
-
213
221
  const body = response.body;
222
+ if (signal?.aborted) {
223
+ if (body) cancelBodyWithoutWaiting(body, signal.reason);
224
+ throw signal.reason;
225
+ }
214
226
  if (!body) {
215
227
  return {
216
228
  text: "",
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { loadConfig, readRuntimePort } from "../config";
2
+ import { loadConfig } from "../config";
3
+ import { readRuntimePort } from "../config/process-state";
3
4
  import { configuredAdminToken } from "./admin-secrets";
4
5
 
5
6
  export function isProcessAlive(pid: number): boolean {
@@ -6,6 +6,7 @@ import {
6
6
  import { reconcileCodexReauthState } from "../codex/account-runtime-state";
7
7
  import { reconcileCatalogWarningMemos } from "../codex/catalog/aggregation";
8
8
  import { reconcileProviderFetchWarnings } from "../codex/catalog/provider-fetch";
9
+ import { reconcileModelMetadataSnapshot } from "../codex/catalog/model-metadata";
9
10
  import { reconcileModelCacheGeneration } from "../codex/model-cache";
10
11
  import { reconcilePoolRotationState } from "../codex/pool-rotation";
11
12
  import { reconcileCodexQuotaAccounts } from "../codex/quota";
@@ -103,6 +104,7 @@ export const STATE_STORE_REGISTRATIONS = [
103
104
  { name: "provider-quota-history", reconcileGeneration: reconcileProviderAccountQuotaRows },
104
105
  { name: "codex-routing-health", reconcileGeneration: reconcileCodexRoutingHealth },
105
106
  { name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration },
107
+ { name: "model-metadata-snapshot", reconcileGeneration: reconcileModelMetadataSnapshot },
106
108
  { name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState },
107
109
  { name: "combo-rotation", reconcileGeneration: reconcileComboRotationState },
108
110
  { name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff },
@@ -59,6 +59,35 @@ function declaresString(schema: SchemaNode): boolean {
59
59
  return Array.isArray(type) && type.includes("string");
60
60
  }
61
61
 
62
+ // Issue #2316: Codex advertises `multi_agent_v1__wait_agent`'s `timeout_ms` as a JSON
63
+ // Schema `number`, but its Rust runtime deserializes the field as `u64` and rejects
64
+ // `120000.0` with "invalid type: floating point `120000.0`, expected u64" before the
65
+ // tool ever runs. The schema lookup is not the problem — the error text comes from
66
+ // Codex's own deserializer, so the call reached it — the problem is that `number`
67
+ // alone never authorizes the integral-float repair below.
68
+ //
69
+ // This allowlist is deliberately one field wide. It names only what has a live
70
+ // reproduction against a real `u64`, because the repair is only unambiguous for a
71
+ // field that cannot legitimately hold a fraction. Generic names (`start`, `end`,
72
+ // `priority`, `port`) would silently rewrite a third-party tool's fractional value,
73
+ // so a new entry needs its own evidence, not a plausible-sounding name.
74
+ //
75
+ // Cursor's sibling `yield_time_ms` (src/adapters/cursor/tool-definitions.ts) is also
76
+ // declared `number`; it is NOT included here because no rejection has been captured
77
+ // against it. It gets its own change when it gets its own reproduction.
78
+ const U64_NUMBER_FIELDS = new Set(["timeout_ms"]);
79
+
80
+ // Issue #2443 / #2451: Codex Desktop's bare `wait` tool has the same schema/runtime
81
+ // split for `yield_time_ms` and `max_tokens`. Scope these names to that bare tool so a
82
+ // third-party or namespaced tool can still use fractional values legitimately.
83
+ // #2448 allowlisted the hyphenated `yield-time_ms`; live Grok 4.6 calls and the
84
+ // advertised wait schema both use the underscore form, so that name is the one
85
+ // with a captured u64 rejection. Cursor still uses the same underscore name on a
86
+ // namespaced tool; the wait-only map keeps that path byte-identical.
87
+ const U64_NUMBER_FIELDS_BY_TOOL = new Map<string, ReadonlySet<string>>([
88
+ ["wait", new Set(["yield_time_ms", "max_tokens"])],
89
+ ]);
90
+
62
91
  /** True when the node accepts a JSON number (`integer` or `number`), so a numeric
63
92
  * value is already schema-valid and must not be rewritten into a string. */
64
93
  function declaresNumeric(schema: SchemaNode): boolean {
@@ -118,7 +147,14 @@ interface CoerceResult {
118
147
  changed: boolean;
119
148
  }
120
149
 
121
- function coerceValue(value: unknown, schema: SchemaNode | undefined, root: SchemaNode, depth: number): CoerceResult {
150
+ function coerceValue(
151
+ value: unknown,
152
+ schema: SchemaNode | undefined,
153
+ root: SchemaNode,
154
+ depth: number,
155
+ toolName?: string,
156
+ propertyName?: string,
157
+ ): CoerceResult {
122
158
  // A hostile or deeply nested schema must not blow the stack.
123
159
  if (depth > 64) return { value, changed: false };
124
160
  const resolved = schema ? resolveRef(schema, root, new Set()) : undefined;
@@ -126,7 +162,19 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
126
162
  if (typeof value === "number") {
127
163
  if (!resolved) return { value, changed: false };
128
164
  const branches = compositionBranches(resolved);
129
- const integerDeclared = declaresInteger(resolved) || branches.some(declaresInteger);
165
+ // #2316: a known Codex-native u64 field counts as integer-declared even when the
166
+ // advertised schema says `number`, but only when the field really is numeric —
167
+ // an allowlisted name over a string-typed field is a disagreement, not a repair.
168
+ const nativeU64Field = propertyName !== undefined
169
+ && (
170
+ U64_NUMBER_FIELDS.has(propertyName)
171
+ || U64_NUMBER_FIELDS_BY_TOOL.get(toolName ?? "")?.has(propertyName) === true
172
+ );
173
+ const nativeU64Declared = nativeU64Field
174
+ && (declaresNumeric(resolved) || branches.some(declaresNumeric));
175
+ const integerDeclared = declaresInteger(resolved)
176
+ || branches.some(declaresInteger)
177
+ || nativeU64Declared;
130
178
  if (!integerDeclared && safelyIntegral(value)) {
131
179
  // Issue #1938: a bare integer in a string-only field has exactly one faithful
132
180
  // string reading. A field that also accepts a numeric type keeps the number.
@@ -148,7 +196,9 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
148
196
  const itemSchema = resolved ? asSchema(resolved.items) : undefined;
149
197
  let changed = false;
150
198
  const next = value.map(entry => {
151
- const result = coerceValue(entry, itemSchema, root, depth + 1);
199
+ // Array items have no property name of their own; passing the array's key would
200
+ // let `timeout_ms: [1.5]` inherit the allowlist. Items are judged by schema only.
201
+ const result = coerceValue(entry, itemSchema, root, depth + 1, toolName);
152
202
  if (result.changed) changed = true;
153
203
  return result.value;
154
204
  });
@@ -164,7 +214,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
164
214
  const next: Record<string, unknown> = {};
165
215
  for (const [key, entry] of Object.entries(object)) {
166
216
  const childSchema = asSchema(properties?.[key]) ?? additional;
167
- const result = coerceValue(entry, childSchema, root, depth + 1);
217
+ const result = coerceValue(entry, childSchema, root, depth + 1, toolName, key);
168
218
  if (result.changed) changed = true;
169
219
  next[key] = result.value;
170
220
  }
@@ -182,6 +232,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
182
232
  export function coerceIntegerToolArguments(
183
233
  args: string,
184
234
  parameters: Record<string, unknown> | undefined,
235
+ toolName?: string,
185
236
  ): string {
186
237
  if (!parameters || !args) return args;
187
238
  // Cheap reject: a payload with no digit cannot need either repair (integral-float
@@ -196,7 +247,7 @@ export function coerceIntegerToolArguments(
196
247
  return args;
197
248
  }
198
249
  const root = parameters as SchemaNode;
199
- const result = coerceValue(parsed, root, root, 0);
250
+ const result = coerceValue(parsed, root, root, 0, toolName);
200
251
  if (!result.changed) return args;
201
252
  return JSON.stringify(result.value);
202
253
  }
@@ -3,7 +3,7 @@ import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
3
3
  import { isAccountNeedsReauth } from "../codex/account-runtime-state";
4
4
  import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
5
5
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
6
- import { readRuntimePort } from "../config";
6
+ import { readRuntimePort } from "../config/process-state";
7
7
  import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
8
8
  import { maskAccountId } from "../lib/privacy";
9
9
  import { findLiveProxy } from "../server/proxy-liveness";
@@ -1073,6 +1073,9 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
1073
1073
  if (existing?.commandCodeVersion !== undefined) {
1074
1074
  next.commandCodeVersion = existing.commandCodeVersion;
1075
1075
  }
1076
+ if (existing?.projectContext !== undefined) {
1077
+ next.projectContext = existing.projectContext;
1078
+ }
1076
1079
  // User-configured price overlays are operator data, not preset state; a
1077
1080
  // re-login, add-account, or reauth must not silently drop them from the
1078
1081
  // Logs/Usage estimates.
@@ -442,7 +442,7 @@ const THINKING_TOGGLE_MAP: Record<string, string> = {
442
442
  max: "enabled",
443
443
  };
444
444
  const OPENCODE_GO_THINKING_TOGGLE_MODELS = [
445
- "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-omni", "mimo-v2-pro", "glm-5", "glm-5.1",
445
+ "mimo-v2.5", "mimo-v2.5-pro", "glm-5", "glm-5.1",
446
446
  ];
447
447
  /**
448
448
  * Zhipu's domestic BigModel platform. Text families first, then the vision member: modalities are
@@ -14,6 +14,16 @@ export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] =
14
14
  const CODEX_REASONING_ORDER = CODEX_REASONING_LEVELS.map(l => l.effort);
15
15
  const CODEX_REASONING_SET = new Set(CODEX_REASONING_ORDER);
16
16
 
17
+ /**
18
+ * Sentinel wire value in reasoningEffortMap / modelReasoningEffortMap to explicitly
19
+ * omit the reasoning_effort field from the upstream wire request (issue #2356).
20
+ */
21
+ export const REASONING_EFFORT_OMIT_SENTINEL = "__omit__";
22
+
23
+ export function isReasoningEffortOmitted(wireEffort: string | undefined): boolean {
24
+ return wireEffort === REASONING_EFFORT_OMIT_SENTINEL;
25
+ }
26
+
17
27
  /** True when `effort` is a member of the Codex reasoning ladder (low..ultra). */
18
28
  export function isCodexReasoningEffort(effort: string): boolean {
19
29
  return CODEX_REASONING_SET.has(effort);
@@ -170,7 +180,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
170
180
  const boundary = requested === "ultra" ? "max" : requested;
171
181
 
172
182
  const wireMap = reasoningEffortMapFor(provider, modelId);
173
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) return wireMap[boundary];
183
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, boundary)) {
184
+ const mapped = wireMap[boundary];
185
+ return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
186
+ }
174
187
 
175
188
  const supported = configuredReasoningEfforts(provider, modelId);
176
189
  const codexEffort = supported !== undefined ? clampToSupportedCodexEffort(boundary, supported) : requestToCodexEffort(boundary);
@@ -178,6 +191,10 @@ export function mapReasoningEffort(provider: OcxProviderConfig, modelId: string,
178
191
 
179
192
  // Belt for the odd config where the supported ladder is ultra-only and the clamp lands on it.
180
193
  const wire = codexEffort === "ultra" ? "max" : codexEffort;
181
- if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) return wireMap[wire];
194
+ if (wireMap && Object.prototype.hasOwnProperty.call(wireMap, wire)) {
195
+ const mapped = wireMap[wire];
196
+ return mapped === REASONING_EFFORT_OMIT_SENTINEL ? undefined : mapped;
197
+ }
198
+ if (wire === REASONING_EFFORT_OMIT_SENTINEL) return undefined;
182
199
  return wire;
183
200
  }
@@ -0,0 +1,63 @@
1
+ // Representation repair for top-level Codex apply_patch custom-tool payloads.
2
+ //
3
+ // Some routed models decorate the first and last lines as
4
+ // `*** Begin Patch ***` / `*** End Patch ***`. Codex rejects those otherwise
5
+ // valid custom-tool payloads. Repair is deliberately limited to a complete,
6
+ // structurally recognizable top-level patch: arbitrary `exec` JavaScript is
7
+ // caller-authored executable input and must remain byte-identical.
8
+ //
9
+ // This is the same intent boundary as `src/lib/tool-argument-integers.ts`:
10
+ // repair the one faithful reading, leave genuine patch content alone.
11
+
12
+ const PATCH_BEGIN = "*** Begin Patch";
13
+ const PATCH_END = "*** End Patch";
14
+ const TOP_LEVEL_PATCH_ENVELOPE = /^(\*\*\* Begin Patch(?: \*\*\*)?)(\r?\n)([\s\S]*)(\r?\n)(\*\*\* End Patch(?: \*\*\*)?)(\r?\n)?$/;
15
+ const PATCH_OPERATION_LINE = /^\*\*\* (?:Add|Update|Delete) File: .+$/m;
16
+
17
+ /** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
18
+ export function unwrapFreeformToolInput(argumentsText: unknown): string {
19
+ if (typeof argumentsText !== "string") return "";
20
+ try {
21
+ const parsed: unknown = JSON.parse(argumentsText);
22
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
23
+ const input = (parsed as { input?: unknown }).input;
24
+ if (typeof input === "string") return input;
25
+ }
26
+ } catch {
27
+ // The string is the freeform body, not nested JSON.
28
+ }
29
+ return argumentsText;
30
+ }
31
+
32
+ /**
33
+ * Strip trailing `***` only from the outer lines of one complete patch.
34
+ * Internal patch content, incomplete envelopes, and non-patch text are exact
35
+ * pass-throughs.
36
+ */
37
+ export function normalizeApplyPatchDelimiters(text: string): string {
38
+ const match = TOP_LEVEL_PATCH_ENVELOPE.exec(text);
39
+ if (!match) return text;
40
+ const [, begin, beginBreak, body, endBreak, end, trailingBreak = ""] = match;
41
+ if (!PATCH_OPERATION_LINE.test(body)) return text;
42
+ if (begin === PATCH_BEGIN && end === PATCH_END) return text;
43
+ return `${PATCH_BEGIN}${beginBreak}${body}${endBreak}${PATCH_END}${trailingBreak}`;
44
+ }
45
+
46
+ /**
47
+ * Repair freeform input before Codex sees it.
48
+ *
49
+ * Only a bare or reserved-`functions` `apply_patch` payload may receive delimiter
50
+ * repair. Remote namespaces own their grammar; those bodies and every other
51
+ * freeform input are unwrapped and left byte-exact.
52
+ */
53
+ export function repairFreeformToolInput(
54
+ argumentsText: unknown,
55
+ toolName = "",
56
+ namespace?: string,
57
+ ): string {
58
+ const unwrapped = unwrapFreeformToolInput(argumentsText);
59
+ const ownsApplyPatchGrammar = namespace === undefined || namespace === "functions";
60
+ return ownsApplyPatchGrammar && toolName === "apply_patch"
61
+ ? normalizeApplyPatchDelimiters(unwrapped)
62
+ : unwrapped;
63
+ }