@bitkyc08/opencodex 2.24.1 → 2.25.0-preview.20260818

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 (58) hide show
  1. package/gui/dist/assets/{index-C3FiAveG.js → index-TFd4xi1L.js} +8 -8
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic.ts +42 -0
  5. package/src/adapters/client-fingerprint.ts +9 -5
  6. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  7. package/src/adapters/command-code.ts +17 -0
  8. package/src/adapters/cursor/cursor-errors.ts +49 -0
  9. package/src/adapters/cursor/live-models.ts +36 -2
  10. package/src/adapters/cursor/live-transport.ts +55 -4
  11. package/src/adapters/cursor/native-exec.ts +9 -0
  12. package/src/adapters/cursor/protobuf-request.ts +160 -9
  13. package/src/adapters/cursor/request-builder.ts +9 -1
  14. package/src/adapters/cursor/tool-definitions.ts +7 -2
  15. package/src/adapters/google-antigravity-wire.ts +1 -1
  16. package/src/adapters/google.ts +30 -12
  17. package/src/adapters/openai-responses-url.ts +5 -3
  18. package/src/adapters/registry.ts +3 -1
  19. package/src/adapters/tool-catalog-nudge.ts +76 -9
  20. package/src/bridge.ts +53 -9
  21. package/src/claude/context-windows.ts +2 -2
  22. package/src/claude/desktop-3p.ts +6 -6
  23. package/src/claude/model-info.ts +2 -2
  24. package/src/cli/claude-desktop.ts +2 -3
  25. package/src/codex/app-server-processes.ts +69 -35
  26. package/src/codex/catalog/metadata.ts +29 -10
  27. package/src/codex/catalog/provider-fetch.ts +21 -11
  28. package/src/codex/catalog.ts +1 -1
  29. package/src/codex/injected-marker.ts +9 -3
  30. package/src/codex/user-identity.ts +88 -6
  31. package/src/config.ts +1 -0
  32. package/src/generated/compatibility-version.json +61 -53
  33. package/src/grok/sync.ts +2 -4
  34. package/src/lab/projection/rebuild.ts +36 -18
  35. package/src/lib/windows-elevation.ts +18 -3
  36. package/src/lib/windows-secret-acl.ts +49 -19
  37. package/src/oauth/google-antigravity.ts +7 -2
  38. package/src/providers/antigravity-models.ts +126 -17
  39. package/src/providers/derive.ts +11 -1
  40. package/src/responses/parser.ts +4 -0
  41. package/src/responses/reasoning-replay-cache.ts +16 -1
  42. package/src/responses/thought-signature-replay.ts +17 -1
  43. package/src/responses/truncated-stop-reason.ts +60 -0
  44. package/src/router.ts +2 -10
  45. package/src/routing/capability.ts +5 -6
  46. package/src/server/index.ts +3 -4
  47. package/src/server/management/agent-settings-routes.ts +5 -5
  48. package/src/server/management/config-routes.ts +2 -2
  49. package/src/server/management/context.ts +2 -0
  50. package/src/server/management/native-integration-routes.ts +3 -3
  51. package/src/server/management/provider-routes.ts +22 -0
  52. package/src/server/management/shared.ts +4 -4
  53. package/src/server/management-api.ts +2 -2
  54. package/src/server/request-log.ts +11 -3
  55. package/src/server/responses/core.ts +4 -1
  56. package/src/server/responses/input-admission.ts +13 -10
  57. package/src/server/system-env.ts +3 -3
  58. package/src/types.ts +13 -1
@@ -122,6 +122,20 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
122
122
 
123
123
  const db = new Database(paths.sqlitePath);
124
124
  let transactionOpen = false;
125
+ // Every statement prepared below, so they can be finalized before the close.
126
+ //
127
+ // Bun keeps a prepared statement alive until it is finalized or garbage collected,
128
+ // and on Windows an unfinalized statement holds the database file open: `close()`
129
+ // leaves the handle, and `close(true)` throws "database is locked". A second
130
+ // rebuild then failed to unlink the previous projection with EBUSY, and the retry
131
+ // loop in `wipeSqlite` could only turn that into a slower failure. POSIX allows
132
+ // unlinking an open file, which is why this never surfaced there.
133
+ const prepared: Array<{ finalize(): void }> = [];
134
+ const prepare = (sql: string) => {
135
+ const statement = db.prepare(sql);
136
+ prepared.push(statement);
137
+ return statement;
138
+ };
125
139
  try {
126
140
  db.exec("PRAGMA journal_mode=DELETE;");
127
141
  db.exec("PRAGMA foreign_keys=OFF;");
@@ -129,50 +143,45 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
129
143
  transactionOpen = true;
130
144
  resetProjectionSchema(db);
131
145
 
132
- db.prepare(
133
- "INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
134
- ).run("schema_version", String(LAB_SQLITE_SCHEMA_VERSION));
135
- db.prepare(
136
- "INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
137
- ).run("projection_spec_version", LAB_PROJECTION_SPEC_VERSION);
138
- db.prepare(
139
- "INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
140
- ).run("built_at_ms", String(Date.now()));
146
+ const insertMeta = prepare("INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)");
147
+ insertMeta.run("schema_version", String(LAB_SQLITE_SCHEMA_VERSION));
148
+ insertMeta.run("projection_spec_version", LAB_PROJECTION_SPEC_VERSION);
149
+ insertMeta.run("built_at_ms", String(Date.now()));
141
150
 
142
- const insertCorruption = db.prepare(
151
+ const insertCorruption = prepare(
143
152
  "INSERT INTO corruption(kind, line_number, event_id, detail) VALUES (?, ?, ?, ?)",
144
153
  );
145
154
  for (const c of corruptions) {
146
155
  insertCorruption.run(c.kind, c.lineNumber ?? null, c.eventId ?? null, c.detail);
147
156
  }
148
157
 
149
- const insertEvent = db.prepare(
158
+ const insertEvent = prepare(
150
159
  `INSERT INTO events(event_id, event_kind, recorded_at, producer, producer_version, payload_json, excluded, exclusion_reason)
151
160
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
152
161
  );
153
- const insertSubject = db.prepare(
162
+ const insertSubject = prepare(
154
163
  `INSERT OR IGNORE INTO subjects(subject_id, subject_kind, subject_json) VALUES (?, ?, ?)`,
155
164
  );
156
- const insertObs = db.prepare(
165
+ const insertObs = prepare(
157
166
  `INSERT INTO observations(
158
167
  event_id, subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest,
159
168
  scenario_id, scenario_version, scenario_manifest_digest, outcome, completed_at, execution_mode
160
169
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
161
170
  );
162
- const insertClaim = db.prepare(
171
+ const insertClaim = prepare(
163
172
  `INSERT INTO claims(
164
173
  event_id, subject_id, capability, polarity, source_manifest_digest,
165
174
  effective_at, recorded_at, supersedes_json, current, usable
166
175
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
167
176
  );
168
- const insertInv = db.prepare(
177
+ const insertInv = prepare(
169
178
  `INSERT INTO invalidations(event_id, reason, targets_json, recorded_at, applied) VALUES (?, ?, ?, ?, ?)`,
170
179
  );
171
- const insertPurge = db.prepare(
180
+ const insertPurge = prepare(
172
181
  `INSERT INTO purges(event_id, target_event_ids_json, target_artifact_digests_json, purge_actions_json, recorded_at)
173
182
  VALUES (?, ?, ?, ?, ?)`,
174
183
  );
175
- const insertArtifact = db.prepare(
184
+ const insertArtifact = prepare(
176
185
  `INSERT INTO artifacts(digest, artifact_class, media_type, byte_count, status, last_error)
177
186
  VALUES (?, ?, ?, ?, ?, ?)
178
187
  ON CONFLICT(digest) DO UPDATE SET
@@ -319,7 +328,7 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
319
328
  }
320
329
  }
321
330
 
322
- const insertVerdict = db.prepare(
331
+ const insertVerdict = prepare(
323
332
  `INSERT INTO verdicts(
324
333
  projection_key, subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest,
325
334
  projection_spec_version, verdict, as_of, scenario_manifest_digests_json, claim_source_digest,
@@ -369,6 +378,15 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
369
378
  } catch {
370
379
  // Closing the disposable DB is still safe if pragma restoration fails.
371
380
  }
381
+ // Finalize before closing: an outstanding statement keeps the file open on
382
+ // Windows, and the next rebuild cannot unlink the projection it is replacing.
383
+ for (const statement of prepared) {
384
+ try {
385
+ statement.finalize();
386
+ } catch {
387
+ // A statement already finalized by an error path is not a rebuild failure.
388
+ }
389
+ }
372
390
  db.close();
373
391
  artifactStore.close();
374
392
  }
@@ -175,7 +175,7 @@ export function assertTrustedSystemExecutableForTests(candidate: string, label:
175
175
  return assertTrustedSystemExecutable(candidate, label);
176
176
  }
177
177
 
178
- type ElevationExeOverrides = { powershell?: string; schtasks?: string; taskkill?: string };
178
+ type ElevationExeOverrides = { powershell?: string; schtasks?: string; taskkill?: string; icacls?: string };
179
179
  let elevationExeOverridesForTests: ElevationExeOverrides | null = null;
180
180
 
181
181
  /**
@@ -220,6 +220,15 @@ export function resolveTrustedWindowsTaskkillExe(): string {
220
220
  return assertTrustedSystemExecutable(candidate, "taskkill.exe");
221
221
  }
222
222
 
223
+ /** Absolute path to System32\\icacls.exe from a trusted system directory. */
224
+ export function resolveTrustedWindowsIcaclsExe(): string {
225
+ if (elevationExeOverridesForTests?.icacls) {
226
+ return elevationExeOverridesForTests.icacls;
227
+ }
228
+ const candidate = join(resolveTrustedWindowsSystemDirectory(), "icacls.exe");
229
+ return assertTrustedSystemExecutable(candidate, "icacls.exe");
230
+ }
231
+
223
232
  /** Stable machine-readable marker for a denied `schtasks /create`. Crosses the CLI→proxy boundary. */
224
233
  export const WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER =
225
234
  "OCX_ERROR_CODE=WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED";
@@ -631,15 +640,21 @@ export function runWindowsElevatedScheduledTaskRegistration(
631
640
  xml: string,
632
641
  ): Promise<number> {
633
642
  const xmlBase64 = Buffer.from(xml, "utf16le").toString("base64");
643
+ const powerShellPath = windowsPowerShell();
644
+ const powerShellDirectory = powerShellPath.replace(/[\\/][^\\/]+$/, "");
645
+ const scheduledTasksModule = `${powerShellDirectory}\\Modules\\ScheduledTasks\\ScheduledTasks.psd1`;
634
646
  const inner = [
635
647
  `$taskName = ${psSingleQuote(taskName)}`,
636
648
  `$xmlBase64 = ${psSingleQuote(xmlBase64)}`,
637
649
  "$xml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($xmlBase64))",
638
- "Register-ScheduledTask -TaskName $taskName -Xml $xml -Force -ErrorAction Stop | Out-Null",
650
+ `$module = Microsoft.PowerShell.Core\\Import-Module -Name ${psSingleQuote(scheduledTasksModule)} -PassThru -Force -ErrorAction Stop`,
651
+ "$registerTask = $module.ExportedCommands['Register-ScheduledTask']",
652
+ "if ($null -eq $registerTask) { throw 'Trusted ScheduledTasks module does not export Register-ScheduledTask.' }",
653
+ "& $registerTask -TaskName $taskName -Xml $xml -Force -ErrorAction Stop | Out-Null",
639
654
  ].join("; ");
640
655
  const encodedCommand = Buffer.from(inner, "utf16le").toString("base64");
641
656
  const script = [
642
- `$p = Start-Process -FilePath ${psSingleQuote(windowsPowerShell())}`,
657
+ `$p = Start-Process -FilePath ${psSingleQuote(powerShellPath)}`,
643
658
  ` -ArgumentList ${psSingleQuote(buildWindowsElevatedArgumentList([
644
659
  "-NoProfile",
645
660
  "-NonInteractive",
@@ -31,6 +31,7 @@
31
31
 
32
32
  import { existsSync, statSync } from "node:fs";
33
33
  import { env, platform } from "node:process";
34
+ import { resolveTrustedWindowsIcaclsExe } from "./windows-elevation";
34
35
  import {
35
36
  resolveCurrentWindowsPrincipal,
36
37
  resolveCurrentWindowsPrincipalAsync,
@@ -273,22 +274,55 @@ export interface IcaclsResult {
273
274
  type IcaclsRunner = (args: string[], timeoutMs: number) => IcaclsResult;
274
275
  type AsyncIcaclsRunner = (args: string[], timeoutMs: number) => Promise<IcaclsResult>;
275
276
 
277
+ function resolveIcaclsExecutable(): string {
278
+ // Same authority as schtasks/powershell: never take icacls from PATH.
279
+ // A bun-shim or stripped PATH makes `icacls.exe` throw ENOENT, which used to
280
+ // surface as "filesystem may not support per-user NTFS ACLs".
281
+ return resolveTrustedWindowsIcaclsExe();
282
+ }
283
+
284
+ function spawnFailedResult(): IcaclsResult {
285
+ return { success: false, exitCode: null, timedOut: false, stdout: "" };
286
+ }
287
+
288
+ /**
289
+ * Spawn icacls asynchronously, or return null when the executable cannot be
290
+ * launched. The pipe/ignore stdio literals stay inferred here so `stdout` keeps
291
+ * its `ReadableStream` type instead of widening to the generic default.
292
+ */
293
+ function trySpawnIcacls(args: string[]) {
294
+ try {
295
+ return Bun.spawn([resolveIcaclsExecutable(), ...args], {
296
+ stdin: "ignore",
297
+ stdout: "pipe",
298
+ stderr: "ignore",
299
+ windowsHide: true,
300
+ });
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+
276
306
  function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
277
307
  // Bun.spawnSync with windowsHide: Node execFileSync has hung under the GUI/proxy even
278
308
  // with windowsHide, and console-subsystem tools flash a visible window otherwise.
279
- const result = Bun.spawnSync(["icacls.exe", ...args], {
280
- stdin: "ignore",
281
- stdout: "pipe",
282
- stderr: "ignore",
283
- timeout: timeoutMs,
284
- windowsHide: true,
285
- });
286
- return {
287
- success: result.success,
288
- exitCode: result.exitCode,
289
- timedOut: result.exitedDueToTimeout ?? false,
290
- stdout: result.stdout ? result.stdout.toString() : "",
291
- };
309
+ try {
310
+ const result = Bun.spawnSync([resolveIcaclsExecutable(), ...args], {
311
+ stdin: "ignore",
312
+ stdout: "pipe",
313
+ stderr: "ignore",
314
+ timeout: timeoutMs,
315
+ windowsHide: true,
316
+ });
317
+ return {
318
+ success: result.success,
319
+ exitCode: result.exitCode,
320
+ timedOut: result.exitedDueToTimeout ?? false,
321
+ stdout: result.stdout ? result.stdout.toString() : "",
322
+ };
323
+ } catch {
324
+ return spawnFailedResult();
325
+ }
292
326
  }
293
327
 
294
328
  /**
@@ -297,12 +331,8 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
297
331
  * we still await process exit before classifying so settlement is confirmed.
298
332
  */
299
333
  async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
300
- const proc = Bun.spawn(["icacls.exe", ...args], {
301
- stdin: "ignore",
302
- stdout: "pipe",
303
- stderr: "ignore",
304
- windowsHide: true,
305
- });
334
+ const proc = trySpawnIcacls(args);
335
+ if (!proc) return spawnFailedResult();
306
336
  let timedOutByUs = false;
307
337
  const timer = setTimeout(() => {
308
338
  timedOutByUs = true;
@@ -12,7 +12,7 @@
12
12
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
13
13
  import { generatePKCE } from "./pkce";
14
14
  import type { OAuthController, OAuthCredentials } from "./types";
15
- import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA } from "../adapters/client-fingerprint";
15
+ import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA, ANTIGRAVITY_IDE_VERSION } from "../adapters/client-fingerprint";
16
16
 
17
17
  const CLIENT_ID = process.env.GOOGLE_ANTIGRAVITY_CLIENT_ID
18
18
  || "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
@@ -111,7 +111,12 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
111
111
  const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, {
112
112
  method: "POST",
113
113
  headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent(), "x-goog-api-client": ANTIGRAVITY_GOOG_API_CLIENT_UA },
114
- body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: antigravityUserAgent() } }),
114
+ // `ide_version` is a version, not a User-Agent. `antigravityUserAgent()` returns the whole
115
+ // header — `antigravity/ide/2.5.5 (aidev_client; os_type=...; arch=...)` — so onboarding was
116
+ // sending a parenthesized UA string in a field the real client fills with `2.5.5`. It is a
117
+ // fingerprint mismatch rather than a crash, which is why nothing failed: the request still
118
+ // succeeds, it just does not look like Antigravity.
119
+ body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: ANTIGRAVITY_IDE_VERSION } }),
115
120
  signal: requestSignal(signal),
116
121
  });
117
122
  if (!response.ok) {
@@ -1,4 +1,5 @@
1
1
  import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./model-discovery-limits";
2
+ import { isModelCacheGenerationCurrent } from "../codex/model-cache";
2
3
 
3
4
  // Google Antigravity (Cloud Code Assist) bundled model list.
4
5
  //
@@ -74,8 +75,12 @@ const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
74
75
 
75
76
  function pickerModelIdForDiscoveredWireId(
76
77
  wireId: string,
78
+ info: Record<string, unknown>,
77
79
  available: ReadonlyMap<string, Record<string, unknown>>,
78
80
  ): string {
81
+ const displayModelId = antigravityDisplayModelId(info.displayName, wireId);
82
+ if (displayModelId) return displayModelId;
83
+
79
84
  const explicitPickerId = Object.hasOwn(ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID, wireId)
80
85
  ? ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId]
81
86
  : undefined;
@@ -240,6 +245,8 @@ export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
240
245
 
241
246
  export interface AntigravityAvailableModel {
242
247
  id: string;
248
+ /** CCA model id used by the agent envelope when `id` comes from display metadata. */
249
+ wireModelId: string;
243
250
  contextWindow?: number;
244
251
  inputModalities?: string[];
245
252
  }
@@ -254,6 +261,100 @@ function antigravityPositiveInteger(value: unknown): number | undefined {
254
261
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
255
262
  }
256
263
 
264
+ interface DiscoveredWireModelMapping {
265
+ readonly models: ReadonlyMap<string, string>;
266
+ readonly generation?: { provider: string; cacheGeneration: string };
267
+ }
268
+
269
+ const discoveredWireModelsByBaseUrl = new Map<string, DiscoveredWireModelMapping>();
270
+
271
+ /**
272
+ * Strip trailing slashes without a backtracking regex.
273
+ *
274
+ * `/\/+$/` is polynomial-ReDoS on attacker-influenceable input (CodeQL js/polynomial-redos):
275
+ * a long run of slashes makes the engine retry every suffix. The base URL comes from provider
276
+ * config, which is not hostile in the ordinary case — but "not hostile today" is a property of
277
+ * the caller, not of this function, and a linear scan costs nothing.
278
+ */
279
+ function stripTrailingSlashes(value: string): string {
280
+ let end = value.length;
281
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
282
+ return end === value.length ? value : value.slice(0, end);
283
+ }
284
+
285
+ function antigravityBaseUrlKey(baseUrl: string | undefined): string | undefined {
286
+ if (typeof baseUrl !== "string" || !baseUrl.trim()) return undefined;
287
+ const trimmed = stripTrailingSlashes(baseUrl.trim());
288
+ try {
289
+ const url = new URL(trimmed);
290
+ url.hash = "";
291
+ url.search = "";
292
+ return stripTrailingSlashes(url.toString()).toLowerCase();
293
+ } catch {
294
+ return trimmed.toLowerCase();
295
+ }
296
+ }
297
+
298
+ /** Remember the wire ids returned by one live CCA discovery for request routing. */
299
+ export function registerAntigravityDiscoveredWireModels(
300
+ baseUrl: string | undefined,
301
+ models: readonly AntigravityAvailableModel[],
302
+ generation?: { provider: string; cacheGeneration: string },
303
+ ): void {
304
+ const key = antigravityBaseUrlKey(baseUrl);
305
+ if (!key) return;
306
+ const wireModels = new Map<string, string>();
307
+ for (const model of models) wireModels.set(model.id, model.wireModelId);
308
+ discoveredWireModelsByBaseUrl.set(key, {
309
+ models: wireModels,
310
+ ...(generation ? { generation } : {}),
311
+ });
312
+ }
313
+
314
+ function discoveredAntigravityWireModelId(
315
+ modelId: string,
316
+ baseUrl: string | undefined,
317
+ ): string | undefined {
318
+ const key = antigravityBaseUrlKey(baseUrl);
319
+ if (!key) return undefined;
320
+ const mapping = discoveredWireModelsByBaseUrl.get(key);
321
+ if (!mapping) return undefined;
322
+ if (mapping.generation
323
+ && !isModelCacheGenerationCurrent(mapping.generation.provider, mapping.generation.cacheGeneration)) {
324
+ discoveredWireModelsByBaseUrl.delete(key);
325
+ return undefined;
326
+ }
327
+ return mapping.models.get(modelId);
328
+ }
329
+
330
+ /**
331
+ * Convert the CCA display label used by `agy` into its public model selector.
332
+ *
333
+ * The wire id is authoritative for requests, while the label is authoritative for the
334
+ * user-facing selector when Google has renamed or re-tiered a model. Keep both instead
335
+ * of maintaining a provider-specific list of known model names.
336
+ */
337
+ function antigravityDisplayModelId(displayName: unknown, wireId: string): string | undefined {
338
+ if (typeof displayName !== "string") return undefined;
339
+ const label = displayName.trim();
340
+ if (!label || label.length > 512) return undefined;
341
+ const slug = (replaceDots: boolean): string => label
342
+ .normalize("NFKC")
343
+ .toLowerCase()
344
+ .replace(replaceDots ? /\./g : /\s+/g, replaceDots ? "-" : " ")
345
+ .replace(/[^a-z0-9.-]+/g, "-")
346
+ .replace(/-+/g, "-")
347
+ .replace(/^-|-$/g, "");
348
+ const preserved = slug(false);
349
+ const compact = slug(true);
350
+ if (!isValidModelDiscoveryModelId(preserved) && !isValidModelDiscoveryModelId(compact)) return undefined;
351
+ if (preserved === wireId || compact === wireId
352
+ || preserved === `${wireId}-thinking` || compact === `${wireId}-thinking`) {
353
+ return wireId;
354
+ }
355
+ return isValidModelDiscoveryModelId(preserved) ? preserved : compact;
356
+ }
357
+
257
358
  /**
258
359
  * Extract the CCA models that are valid for agent requests. The endpoint also returns tab,
259
360
  * command, commit-message, transcription, and standalone image-generation models; those are not
@@ -288,13 +389,6 @@ export function parseAntigravityAvailableModels(
288
389
  }
289
390
  }
290
391
  }
291
- // This model is exposed by Antigravity's agent chat surface even though it is grouped under
292
- // image generation in the discovery response.
293
- if (Array.isArray(body.imageGenerationModelIds)
294
- && body.imageGenerationModelIds.includes("gemini-3.1-flash-image")) {
295
- if (ids.length >= limit) return null;
296
- ids.push("gemini-3.1-flash-image");
297
- }
298
392
  // Newer CCA responses identify tiered Flash models through this index instead of
299
393
  // adding their synthetic wire ids to agentModelSorts.
300
394
  const tieredModelIds = antigravityRecord(body.tieredModelIds);
@@ -305,6 +399,12 @@ export function parseAntigravityAvailableModels(
305
399
  || !Object.hasOwn(models, id)
306
400
  || !antigravityRecord(models[id])
307
401
  || ids.length >= limit) return null;
402
+ const baseId = id.endsWith("-tiered") ? id.slice(0, -"-tiered".length) : id;
403
+ if (ids.some(agentId =>
404
+ agentId === id
405
+ || agentId === baseId
406
+ || ANTIGRAVITY_DISCOVERY_EFFORTS.some(effort => agentId === `${baseId}-${effort}`)
407
+ )) continue;
308
408
  ids.push(id);
309
409
  }
310
410
  }
@@ -313,23 +413,18 @@ export function parseAntigravityAvailableModels(
313
413
  for (const wireId of ids) {
314
414
  const info = antigravityRecord(models[wireId]);
315
415
  if (!info || available.has(wireId)) continue;
316
- // Legacy compatibility aliases are deliberately routed to newer wire ids for saved
317
- // selections. They are not safe as independently discovered picker rows.
318
- const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, wireId)
319
- ? ANTIGRAVITY_MODEL_ALIASES[wireId]
320
- : undefined;
321
- if (alias && alias !== wireId) continue;
322
416
  available.set(wireId, info);
323
417
  }
324
418
 
325
419
  const out: AntigravityAvailableModel[] = [];
326
420
  const seen = new Set<string>();
327
421
  for (const [wireId, info] of available) {
328
- const id = pickerModelIdForDiscoveredWireId(wireId, available);
422
+ const id = pickerModelIdForDiscoveredWireId(wireId, info, available);
329
423
  if (seen.has(id)) continue;
330
424
  seen.add(id);
331
425
  out.push({
332
426
  id,
427
+ wireModelId: wireId,
333
428
  ...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
334
429
  // Tri-state, deliberately not a ternary: `true` asserts image support,
335
430
  // `false` asserts against it, and ABSENT is unknown. Collapsing absent into
@@ -347,7 +442,9 @@ export function parseAntigravityAvailableModels(
347
442
  return out;
348
443
  }
349
444
 
350
- export function resolveAntigravityWireModelId(modelId: string): string {
445
+ export function resolveAntigravityWireModelId(modelId: string, baseUrl?: string): string {
446
+ const discovered = discoveredAntigravityWireModelId(modelId, baseUrl);
447
+ if (discovered) return discovered;
351
448
  return Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, modelId)
352
449
  ? ANTIGRAVITY_MODEL_ALIASES[modelId]
353
450
  : modelId;
@@ -380,7 +477,19 @@ export function retiredAntigravityFlashTier(modelId: string): string | undefined
380
477
  export function resolveAntigravityEffortWireModel(
381
478
  modelId: string,
382
479
  effort?: string,
480
+ baseUrl?: string,
383
481
  ): { wireModelId: string; thinkingLevel?: string } {
482
+ const discoveredWireModelId = discoveredAntigravityWireModelId(modelId, baseUrl);
483
+ if (discoveredWireModelId && (discoveredWireModelId !== modelId || isAntigravitySuffixModelId(modelId))) {
484
+ const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
485
+ return {
486
+ wireModelId: discoveredWireModelId,
487
+ ...(defaultLevel
488
+ ? { thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel }
489
+ : {}),
490
+ };
491
+ }
492
+
384
493
  // Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
385
494
  // current generation and carry the tier the retired id encoded. This runs BEFORE the
386
495
  // suffix check because those ids are aliases, and rule 1 would drop the tier.
@@ -394,7 +503,7 @@ export function resolveAntigravityEffortWireModel(
394
503
 
395
504
  // Rule 1: suffix/compat alias — suffix IS the effort.
396
505
  if (isAntigravitySuffixModelId(modelId)) {
397
- return { wireModelId: resolveAntigravityWireModelId(modelId) };
506
+ return { wireModelId: resolveAntigravityWireModelId(modelId, baseUrl) };
398
507
  }
399
508
 
400
509
  // Rule 1b: single-wire-id Gemini model whose tiers ride on thinkingLevel. Without
@@ -424,7 +533,7 @@ export function resolveAntigravityEffortWireModel(
424
533
  }
425
534
 
426
535
  // Rule 5: everything else.
427
- return { wireModelId: resolveAntigravityWireModelId(modelId) };
536
+ return { wireModelId: resolveAntigravityWireModelId(modelId, baseUrl) };
428
537
  }
429
538
 
430
539
 
@@ -393,6 +393,14 @@ function enrichReasoningSummariesByDestination(prov: OcxProviderConfig): void {
393
393
  applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
394
394
  }
395
395
 
396
+ /** Repair the exact low-only ClinePass ladder generated by older key-login presets. */
397
+ export function hasLegacyClinePassReasoningEfforts(name: string, prov: OcxProviderConfig): boolean {
398
+ return name === "cline-pass"
399
+ && prov.reasoningWireFormat === "gateway-object"
400
+ && prov.reasoningEfforts?.length === 1
401
+ && prov.reasoningEfforts[0] === "low";
402
+ }
403
+
396
404
  export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
397
405
  const entry = PROVIDER_REGISTRY.find(row => row.id === name);
398
406
  if (!entry || !providerMatchesRegistryTransportWithStaticGuards(name, prov)) {
@@ -426,7 +434,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
426
434
  if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
427
435
  if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
428
436
  if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
429
- if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
437
+ if ((!prov.reasoningEfforts || hasLegacyClinePassReasoningEfforts(name, prov)) && seed.reasoningEfforts) {
438
+ prov.reasoningEfforts = [...seed.reasoningEfforts];
439
+ }
430
440
  if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.modelReasoningEfforts);
431
441
  if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
432
442
  if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
@@ -171,6 +171,10 @@ function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
171
171
  };
172
172
  for (const t of tools) {
173
173
  if (!isObj(t)) continue;
174
+ if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) {
175
+ pushFn(t.function as Record<string, unknown>);
176
+ continue;
177
+ }
174
178
  if (t.type === "function" && typeof t.name === "string") {
175
179
  pushFn(t);
176
180
  } else if (t.type === "namespace" && Array.isArray(t.tools)) {
@@ -19,7 +19,7 @@
19
19
  * long-lived proxy cannot grow without limit.
20
20
  */
21
21
 
22
- import { createHmac, randomBytes } from "node:crypto";
22
+ import { createHash, createHmac, randomBytes } from "node:crypto";
23
23
  import type {
24
24
  OcxProviderConfig,
25
25
  OcxReasoningReplayIdentity,
@@ -108,6 +108,21 @@ export function reasoningReplayDestinationIdentity(baseUrl: string | undefined):
108
108
  return `destination:${processLocalIdentity("destination", canonical)}`;
109
109
  }
110
110
 
111
+ /**
112
+ * The same destination identity, but stable across restarts.
113
+ *
114
+ * The process-local form above is keyed by `randomBytes(32)` minted at module load, which
115
+ * is correct for an in-memory cache and fatal for a durable one: every key would change on
116
+ * restart and the store would silently stop matching anything. A plain digest of the same
117
+ * canonical URL is equally non-reversible for this purpose — the input is a configured
118
+ * endpoint, not a secret — and needs no persisted salt or new on-disk state.
119
+ */
120
+ export function durableReplayDestinationIdentity(baseUrl: string | undefined): string | undefined {
121
+ if (!nonEmpty(baseUrl)) return undefined;
122
+ const canonical = baseUrl.trim().replace(/\/+$/, "");
123
+ return `destination:${createHash("sha256").update("destination\0").update(canonical).digest("hex")}`;
124
+ }
125
+
111
126
  /** Produce a non-reversible process-local identity for credential material. */
112
127
  export function reasoningReplayCredentialIdentity(
113
128
  kind: "key" | "oauth" | "codex",
@@ -27,6 +27,11 @@ import type { OcxProviderOpaqueToolCallMetadata, OcxReasoningReplayScopeRef } fr
27
27
  import { isCarryableSignature, responsesExtraContentFromProviderMetadata } from "./provider-opaque-metadata";
28
28
 
29
29
  const STORE_FILE_NAME = "thought-signature-replay.json";
30
+ /**
31
+ * Bumped whenever `keyFor` changes shape. v3 added the durable destination identity, so a
32
+ * v2 file's keys can never match and are dropped on load instead of aging out invisibly.
33
+ */
34
+ const STORE_VERSION = 3;
30
35
 
31
36
  /** Bound on remembered entries; real signatures are a few hundred bytes, so this stays small. */
32
37
  const MAX_ENTRIES = 16_384;
@@ -83,6 +88,11 @@ function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined):
83
88
  return JSON.stringify([
84
89
  scope.clientThreadId,
85
90
  identity.providerName,
91
+ // Destination, unlike the credential identity, has a restart-stable form: it is a
92
+ // configured endpoint rather than a secret, so a plain digest works where the
93
+ // reasoning cache's randomBytes-keyed HMAC cannot. Without it, one provider NAME
94
+ // serving two endpoints shares signatures across both.
95
+ identity.providerDestinationDurableIdentity ?? "destination:unknown",
86
96
  identity.adapterName,
87
97
  identity.modelId,
88
98
  callId,
@@ -103,6 +113,12 @@ function load(): void {
103
113
  if (typeof parsed !== "object" || parsed === null || !Array.isArray((parsed as { entries?: unknown }).entries)) {
104
114
  return;
105
115
  }
116
+ // The version was written but never read, so a key-shape change could not be
117
+ // announced — old entries simply went dead and aged out on TTL, which is silent and
118
+ // indistinguishable from a store that is not working. Reading it makes a shape change
119
+ // an explicit drop: entries keyed by an older scheme are discarded on load rather than
120
+ // lingering as permanent misses.
121
+ if ((parsed as { version?: unknown }).version !== STORE_VERSION) return;
106
122
  const nowMs = Date.now();
107
123
  for (const entry of (parsed as { entries: unknown[] }).entries) {
108
124
  if (typeof entry !== "object" || entry === null) continue;
@@ -141,7 +157,7 @@ function persist(): Promise<void> {
141
157
  persistChain = persistChain
142
158
  .then(async () => {
143
159
  const snapshot = JSON.stringify({
144
- version: 2,
160
+ version: STORE_VERSION,
145
161
  entries: [...entries].map(([key, entry]) => ({ key, sig: entry.sig, savedAt: entry.savedAt })),
146
162
  });
147
163
  await atomicWriteFileAsync(storePath(), snapshot);