@bitkyc08/opencodex 2.68.0 → 2.69.0-preview.20260928

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 (175) hide show
  1. package/bin/ocx.mjs +25 -17
  2. package/gui/dist/assets/App-cDPyCb_V.js +51 -0
  3. package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-H1IAB8mN.js} +1 -1
  4. package/gui/dist/assets/index-BF9QnKO_.css +1 -0
  5. package/gui/dist/assets/index-CI6YTp8z.js +86 -0
  6. package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-CutOlPu2.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/factory-droid.svg +8 -0
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-output-schema.ts +109 -11
  11. package/src/adapters/coding-agent/protocol.ts +106 -11
  12. package/src/adapters/coding-agent/turn.ts +23 -18
  13. package/src/adapters/google-errors.ts +16 -1
  14. package/src/adapters/google-http.ts +14 -0
  15. package/src/adapters/openai-chat/passthrough.ts +2 -0
  16. package/src/adapters/openai-chat/summary-budget.ts +72 -0
  17. package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
  18. package/src/adapters/openai-chat.ts +6 -9
  19. package/src/bridge/response-json.ts +3 -1
  20. package/src/bridge/sse.ts +3 -1
  21. package/src/claude/desktop-picker.ts +6 -2
  22. package/src/claude/desktop-system-proxy.ts +155 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound.ts +1 -1
  25. package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
  26. package/src/claude/intercept/picker-ca.ts +283 -83
  27. package/src/claude/intercept/picker-models.ts +4 -1
  28. package/src/claude/intercept/picker-trust.ts +15 -2
  29. package/src/claude/intercept/runtime.ts +99 -32
  30. package/src/cli/account-api.ts +5 -2
  31. package/src/cli/account-extended.ts +47 -11
  32. package/src/cli/account-target.ts +12 -7
  33. package/src/cli/account.ts +47 -3
  34. package/src/cli/capabilities.ts +33 -7
  35. package/src/cli/claude-agent-startup-sync.ts +2 -0
  36. package/src/cli/claude-desktop.ts +37 -13
  37. package/src/cli/cross-home-owner.ts +138 -0
  38. package/src/cli/dispatch.ts +1 -1
  39. package/src/cli/doctor.ts +18 -8
  40. package/src/cli/ensure-desired-integrations.ts +5 -0
  41. package/src/cli/export-command.ts +6 -6
  42. package/src/cli/help.ts +1 -1
  43. package/src/cli/index.ts +15 -18
  44. package/src/cli/integrations.ts +8 -1
  45. package/src/cli/models-runtime-subcommands.ts +1 -0
  46. package/src/cli/models-runtime.ts +106 -4
  47. package/src/cli/models.ts +6 -1
  48. package/src/cli/opencode.ts +2 -83
  49. package/src/cli/registry.ts +3 -2
  50. package/src/cli/status.ts +86 -7
  51. package/src/client/connect.ts +24 -10
  52. package/src/client/link-ingress.ts +3 -1
  53. package/src/client/link-join.ts +76 -9
  54. package/src/client/link-relay-transport.ts +184 -0
  55. package/src/client/link-relay.ts +16 -4
  56. package/src/client/machine-listener.ts +2 -0
  57. package/src/clients/config-export/constants.ts +8 -0
  58. package/src/clients/config-export/contracts.ts +13 -1
  59. package/src/clients/config-export/droid.ts +71 -0
  60. package/src/clients/config-export/kilo.ts +116 -0
  61. package/src/clients/config-export.ts +33 -2
  62. package/src/codex/auth-api/routes.ts +1 -1
  63. package/src/codex/catalog/access-programs.ts +14 -2
  64. package/src/codex/catalog/provider-models.ts +1 -1
  65. package/src/codex/catalog-auto-refresh.ts +122 -0
  66. package/src/codex/config-drift-heal.ts +80 -0
  67. package/src/codex/desktop-switches.ts +7 -5
  68. package/src/codex/inject/bounded-config-reader.ts +69 -0
  69. package/src/codex/inject/config-toml.ts +31 -3
  70. package/src/codex/inject/routing-target.ts +6 -1
  71. package/src/codex/inject.ts +4 -0
  72. package/src/codex/model-entitlements.ts +32 -5
  73. package/src/codex/native-profile-startup.ts +17 -5
  74. package/src/codex/project-config-warnings.ts +72 -21
  75. package/src/codex/quota-auto-refresh-state.ts +8 -2
  76. package/src/codex/quota-auto-refresh.ts +80 -9
  77. package/src/codex/sync.ts +10 -0
  78. package/src/combos/failover.ts +4 -1
  79. package/src/combos/jev.ts +15 -4
  80. package/src/combos/types.ts +16 -0
  81. package/src/config/diagnostics.ts +19 -1
  82. package/src/config/load-degrade.ts +29 -0
  83. package/src/config/macos-system-proxy.ts +114 -0
  84. package/src/config/proxy-env.ts +67 -20
  85. package/src/config/schema/config-schema.ts +15 -0
  86. package/src/config/schema/leaf-validators.ts +23 -0
  87. package/src/config.ts +2 -0
  88. package/src/generated/compatibility-version.json +220 -144
  89. package/src/integrations/catalog-refresh.ts +1 -1
  90. package/src/integrations/config-io.ts +12 -3
  91. package/src/integrations/droid-settings.ts +65 -0
  92. package/src/integrations/kilo-candidates.ts +30 -0
  93. package/src/integrations/mutation-plan.ts +62 -7
  94. package/src/integrations/native/ownership-preflight.ts +46 -12
  95. package/src/integrations/registry.ts +136 -7
  96. package/src/integrations/state.ts +52 -4
  97. package/src/integrations/target.ts +3 -0
  98. package/src/integrations/writer.ts +55 -14
  99. package/src/lib/account-selection-events.ts +14 -0
  100. package/src/lib/jsonc.ts +110 -0
  101. package/src/lib/standalone.ts +9 -1
  102. package/src/lib/windows-system-proxy.ts +101 -1
  103. package/src/link/relay-auth.ts +29 -0
  104. package/src/link/ssh-argv.ts +6 -3
  105. package/src/link/ssh-runner.ts +5 -5
  106. package/src/oauth/generic-account-failover.ts +52 -8
  107. package/src/oauth/index.ts +37 -14
  108. package/src/oauth/store.ts +96 -11
  109. package/src/oauth/token-guardian.ts +7 -3
  110. package/src/oauth/types.ts +2 -0
  111. package/src/plugins/loader.ts +5 -3
  112. package/src/providers/fastwire.ts +3 -0
  113. package/src/providers/key-failover.ts +13 -5
  114. package/src/providers/kiro-model-catalog.ts +4 -3
  115. package/src/providers/model-rename-fields.ts +1 -0
  116. package/src/providers/openai-tiers-destination.ts +9 -0
  117. package/src/providers/quota/account-cache.ts +17 -3
  118. package/src/providers/quota/vendor-probes-oauth.ts +2 -0
  119. package/src/providers/quota.ts +2 -2
  120. package/src/responses/code-mode-helper-compat.ts +14 -1
  121. package/src/responses/custom-tool-compat.ts +1 -1
  122. package/src/server/auth-cors.ts +1 -0
  123. package/src/server/images.ts +70 -20
  124. package/src/server/index/link-listener.ts +20 -3
  125. package/src/server/index/link-relay-sessions.ts +138 -0
  126. package/src/server/index/optional-listeners.ts +11 -1
  127. package/src/server/index/serve-options.ts +12 -0
  128. package/src/server/inference/client-encoder-delivery.ts +1 -0
  129. package/src/server/local-management-read-client.ts +21 -2
  130. package/src/server/management/agent-settings-routes.ts +19 -5
  131. package/src/server/management/combo-routes.ts +5 -3
  132. package/src/server/management/config-routes.ts +32 -6
  133. package/src/server/management/model-routes.ts +232 -7
  134. package/src/server/management/model-rows.ts +199 -1
  135. package/src/server/management/oauth-account-routes.ts +63 -3
  136. package/src/server/management/route-registry.ts +2 -0
  137. package/src/server/port-reclaim.ts +170 -32
  138. package/src/server/responses/adapter-delivery.ts +6 -4
  139. package/src/server/responses/adapter-dispatch.ts +72 -2
  140. package/src/server/responses/core-combo.ts +6 -1
  141. package/src/server/responses/core-normalize.ts +19 -4
  142. package/src/server/responses/core-options.ts +2 -0
  143. package/src/server/responses/memory-models.ts +169 -0
  144. package/src/server/responses/passthrough-delivery.ts +4 -0
  145. package/src/server/responses/passthrough-dispatch.ts +10 -0
  146. package/src/server/responses/request-prepare.ts +86 -4
  147. package/src/server/responses/request-transport.ts +4 -0
  148. package/src/server/responses/run-turn-execution.ts +5 -2
  149. package/src/server/responses/shadow-target-availability.ts +17 -4
  150. package/src/server/responses/sidecar-execution.ts +18 -2
  151. package/src/server/responses/skills-snapshot.ts +239 -0
  152. package/src/server/responses-undeclared-tool-guard.ts +43 -6
  153. package/src/server/search.ts +1 -1
  154. package/src/service/guards.ts +4 -5
  155. package/src/service/state.ts +39 -2
  156. package/src/service.ts +2 -2
  157. package/src/types/config.ts +46 -4
  158. package/src/types/provider.ts +9 -0
  159. package/src/types/request.ts +6 -0
  160. package/src/types/tools.ts +38 -8
  161. package/src/types.ts +3 -0
  162. package/src/update/async-check.ts +5 -1
  163. package/src/update/index.ts +30 -12
  164. package/src/update/job.ts +3 -1
  165. package/src/update/pnpm-read-policy.d.mts +4 -0
  166. package/src/update/pnpm-read-policy.mjs +44 -0
  167. package/src/update/worker-launch.ts +152 -11
  168. package/src/usage/cost.ts +6 -3
  169. package/src/usage/log.ts +4 -0
  170. package/src/web-search/backends.ts +2 -2
  171. package/src/web-search/devin-executor.ts +1 -1
  172. package/src/web-search/sidecar-providers.ts +5 -5
  173. package/gui/dist/assets/App-mpsibsFj.js +0 -51
  174. package/gui/dist/assets/index-BDUBS8PW.css +0 -1
  175. package/gui/dist/assets/index-kUE8tuAK.js +0 -86
@@ -5,13 +5,13 @@ import {
5
5
  fstatSync,
6
6
  lstatSync,
7
7
  openSync,
8
- readFileSync,
9
8
  readSync,
10
9
  realpathSync,
11
10
  } from "node:fs";
12
11
  import path, { dirname, join, resolve } from "node:path";
13
12
  import { expandUserPath } from "../config";
14
13
  import { defaultCodexHome } from "./home";
14
+ import { readBoundedCodexConfig } from "./inject/bounded-config-reader";
15
15
  import { readRootTomlString } from "./paths";
16
16
  import { truncateRetainedUtf8 } from "../lib/admission";
17
17
 
@@ -53,7 +53,8 @@ function resolveCodexConfigPath(): string {
53
53
  return join(home, "config.toml");
54
54
  }
55
55
 
56
- export type ProjectCodexConfigIssueCode = "model_providers_table" | "profile_selector" | "model_provider_root";
56
+ export type ProjectCodexConfigIssueCode = "model_providers_table" | "profile_selector" | "model_provider_root"
57
+ | "global_config_unreadable";
57
58
 
58
59
  export interface ProjectCodexConfigWarning {
59
60
  path: string;
@@ -265,17 +266,18 @@ export function resolveEffectiveProjectModelProvider(content: string): Effective
265
266
  /** True when global Codex config routes through the opencodex proxy. */
266
267
  export function isGlobalOpencodexRoutingActive(
267
268
  codexConfigPath: string = resolveCodexConfigPath(),
268
- content?: string,
269
+ content?: string | null,
269
270
  ): boolean {
270
271
  let text = content;
271
272
  if (text === undefined) {
272
- if (!existsSync(codexConfigPath)) return false;
273
273
  try {
274
- text = readFileSync(codexConfigPath, "utf-8");
274
+ text = readBoundedCodexConfig(codexConfigPath) ?? undefined;
275
275
  } catch {
276
276
  return false;
277
277
  }
278
+ if (text === undefined) return false;
278
279
  }
280
+ if (text === null) return false;
279
281
  if (hasInjectedOpenaiBaseUrl(text)) return true;
280
282
  if (readRootTomlString(text, "model_provider") === "opencodex") return true;
281
283
  return false;
@@ -379,6 +381,8 @@ export function discoverProjectCodexConfigPaths(options: {
379
381
  cwd?: string;
380
382
  codexConfigPath?: string;
381
383
  maxWalkParents?: number;
384
+ /** Explicit null keeps an absent/unreadable observation; undefined permits a fresh read. */
385
+ globalContent?: string | null;
382
386
  } = {}): string[] {
383
387
  const found = new Set<string>();
384
388
  const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath();
@@ -416,15 +420,16 @@ export function discoverProjectCodexConfigPaths(options: {
416
420
  cwd = parent;
417
421
  }
418
422
 
419
- if (existsSync(codexConfigPath)) {
420
- try {
421
- const global = readFileSync(codexConfigPath, "utf-8");
423
+ try {
424
+ const global = options.globalContent === undefined
425
+ ? readBoundedCodexConfig(codexConfigPath) : options.globalContent;
426
+ if (global !== null) {
422
427
  for (const projectPath of parseTrustedProjectPathsFromCodexConfig(global)) {
423
428
  addIfExists(projectPath);
424
429
  }
425
- } catch {
426
- /* ignore unreadable global config */
427
430
  }
431
+ } catch {
432
+ /* ignore unreadable global config */
428
433
  }
429
434
 
430
435
  return [...found];
@@ -437,10 +442,32 @@ export function collectProjectCodexConfigWarnings(options: {
437
442
  } = {}): ProjectCodexConfigWarning[] {
438
443
  const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath();
439
444
  const requireRouting = options.requireOpencodexRouting ?? true;
440
- if (requireRouting && !isGlobalOpencodexRoutingActive(codexConfigPath)) return [];
445
+
446
+ // The routing question has three answers: active, inactive, and unreadable. An oversized
447
+ // or swapped-underneath global config must not silently collapse to "inactive" — that
448
+ // would erase both project-bypass coverage and trusted-path discovery without a trace.
449
+ let globalContent: string | null = null;
450
+ let globalUnreadable = false;
451
+ try {
452
+ globalContent = readBoundedCodexConfig(codexConfigPath);
453
+ } catch {
454
+ globalUnreadable = true;
455
+ }
456
+ if (requireRouting && !globalUnreadable
457
+ && !isGlobalOpencodexRoutingActive(codexConfigPath, globalContent)) {
458
+ return [];
459
+ }
441
460
 
442
461
  const warnings: ProjectCodexConfigWarning[] = [];
443
- for (const path of discoverProjectCodexConfigPaths({ cwd: options.cwd, codexConfigPath })) {
462
+ if (globalUnreadable) {
463
+ warnings.push({
464
+ path: codexConfigPath,
465
+ code: "global_config_unreadable",
466
+ detail: "unreadable",
467
+ message: "The global Codex config could not be read within the 1 MiB bound — whether it routes through OpenCodex, and which projects it declares trusted, is undetermined.",
468
+ });
469
+ }
470
+ for (const path of discoverProjectCodexConfigPaths({ cwd: options.cwd, codexConfigPath, globalContent })) {
444
471
  const content = readBoundedProjectConfig(path);
445
472
  if (content !== null) warnings.push(...analyzeProjectCodexConfig(content, path));
446
473
  }
@@ -480,6 +507,8 @@ export function summarizeProjectCodexIssue(warning: ProjectCodexConfigWarning):
480
507
  return warning.profileName ? `profile="${warning.profileName}"` : `model_provider="${warning.detail}"`;
481
508
  case "model_provider_root":
482
509
  return `model_provider="${warning.detail}"`;
510
+ case "global_config_unreadable":
511
+ return "config.toml unreadable or oversized";
483
512
  }
484
513
  }
485
514
 
@@ -501,6 +530,8 @@ export interface ProjectCodexConfigWarningGroup {
501
530
  path: string;
502
531
  issues: string[];
503
532
  bypass: string;
533
+ /** True when the group is the global-config-unreadable caveat, not a project bypass. */
534
+ globalUnreadable?: boolean;
504
535
  }
505
536
 
506
537
  export function groupProjectCodexConfigWarningsByPath(
@@ -512,22 +543,34 @@ export function groupProjectCodexConfigWarningsByPath(
512
543
  list.push(warning);
513
544
  grouped.set(warning.path, list);
514
545
  }
515
- return [...grouped.entries()].map(([path, pathWarnings]) => ({
516
- path,
517
- issues: pathWarnings.map(summarizeProjectCodexIssue),
518
- bypass: explainProjectConfigBypass(pathWarnings),
519
- }));
546
+ return [...grouped.entries()].map(([path, pathWarnings]) => {
547
+ const globalUnreadable = pathWarnings.every(warning => warning.code === "global_config_unreadable");
548
+ return {
549
+ path,
550
+ issues: pathWarnings.map(summarizeProjectCodexIssue),
551
+ bypass: globalUnreadable ? pathWarnings[0]!.message : explainProjectConfigBypass(pathWarnings),
552
+ ...(globalUnreadable ? { globalUnreadable } : {}),
553
+ };
554
+ });
520
555
  }
521
556
 
522
557
  export function formatProjectCodexConfigWarningsForDoctor(warnings: ProjectCodexConfigWarning[]): string[] {
523
558
  const grouped = groupProjectCodexConfigWarningsByPath(warnings);
524
559
  if (grouped.length === 0) return [];
525
560
  const lines: string[] = [];
526
- for (const { path, issues, bypass } of grouped) {
561
+ let hasBypassEntries = false;
562
+ for (const { path, issues, bypass, globalUnreadable } of grouped) {
527
563
  lines.push(` -- ${relPath(path)} — ${issues.join(", ")}`);
528
564
  lines.push(` ${bypass}`);
565
+ if (globalUnreadable) {
566
+ lines.push(" fix: keep the global config.toml a readable regular file within the 1 MiB bound");
567
+ } else {
568
+ hasBypassEntries = true;
569
+ }
570
+ }
571
+ if (hasBypassEntries) {
572
+ lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
529
573
  }
530
- lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
531
574
  return lines;
532
575
  }
533
576
 
@@ -535,11 +578,19 @@ export function formatProjectCodexConfigWarningsForConsole(warnings: ProjectCode
535
578
  const grouped = groupProjectCodexConfigWarningsByPath(warnings);
536
579
  if (grouped.length === 0) return [];
537
580
  const lines = ["⚠️ Project Codex config bypasses OpenCodex:"];
538
- for (const { path, issues, bypass } of grouped) {
581
+ let hasBypassEntries = false;
582
+ for (const { path, issues, bypass, globalUnreadable } of grouped) {
539
583
  lines.push(` ${relPath(path)} — ${issues.join(", ")}`);
540
584
  lines.push(` ${bypass}`);
585
+ if (globalUnreadable) {
586
+ lines.push(" fix: keep the global config.toml a readable regular file within the 1 MiB bound");
587
+ } else {
588
+ hasBypassEntries = true;
589
+ }
590
+ }
591
+ if (hasBypassEntries) {
592
+ lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
541
593
  }
542
- lines.push(" fix: remove those entries so OpenCodex proxy routing applies in this project");
543
594
  return lines;
544
595
  }
545
596
 
@@ -2,10 +2,16 @@
2
2
  /** Completed/due markers use epoch milliseconds; persisted legacy markers may use seconds. */
3
3
  export type CodexQuotaAutoRefreshWindows = { fiveHour?: number; weekly?: number };
4
4
 
5
+ /**
6
+ * Backoff evidence for one account. `generation` names the credential the failure was observed
7
+ * under; a record from another generation says nothing about the credential in use now.
8
+ */
9
+ export type CodexQuotaRetry = { after: number; delay: number; generation: string };
10
+
5
11
  export const completedByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
6
- export const retryAfterByAccount = new Map<string, number>();
12
+ export const retryAfterByAccount = new Map<string, CodexQuotaRetry>();
7
13
  export const scheduledByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
8
- export const quotaRefreshAfterByAccount = new Map<string, number>();
14
+ export const quotaRefreshAfterByAccount = new Map<string, CodexQuotaRetry>();
9
15
 
10
16
  /** Drop every activation record when its account is removed. */
11
17
  export function forgetCodexQuotaAutoRefreshAccount(accountId: string): void {
@@ -21,13 +21,14 @@ import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./
21
21
  import {
22
22
  completedByAccount, retryAfterByAccount, scheduledByAccount, quotaRefreshAfterByAccount,
23
23
  resetCodexQuotaAutoRefreshStateForTests,
24
- type CodexQuotaAutoRefreshWindows,
24
+ type CodexQuotaAutoRefreshWindows, type CodexQuotaRetry,
25
25
  } from "./quota-auto-refresh-state";
26
26
  export type { CodexQuotaAutoRefreshWindows } from "./quota-auto-refresh-state";
27
27
  export { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
28
28
 
29
29
  export const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
30
30
  const RETRY_MS = 5 * 60_000;
31
+ const MAX_RETRY_MS = 60 * 60_000;
31
32
  const CONCURRENCY = 4;
32
33
 
33
34
  export interface CodexQuotaAutoRefreshStatus {
@@ -51,6 +52,57 @@ export interface CodexQuotaAutoRefreshRunDeps {
51
52
 
52
53
  let inFlight: Promise<void> | null = null;
53
54
 
55
+ const LOCAL_BUSY_RETRY_MS = 60_000;
56
+
57
+ /** The native main profile was claimed locally, so no upstream request was sent. */
58
+ export class NativeMainBusyError extends Error {
59
+ constructor() {
60
+ super("native main busy");
61
+ this.name = "NativeMainBusyError";
62
+ }
63
+ }
64
+
65
+ /** The credential a retry record describes: main's quota generation, or the pool record's. */
66
+ function credentialGeneration(accountId: string): string {
67
+ return accountId === MAIN_CODEX_ACCOUNT_ID
68
+ ? `main:${getMainQuotaCredentialGeneration()}`
69
+ : `pool:${readCodexAccountRecord(accountId)?.generation ?? "none"}`;
70
+ }
71
+
72
+ /** A retry recorded under a replaced credential is spent; drop it rather than hold the new one. */
73
+ function liveRetry(
74
+ retries: Map<string, CodexQuotaRetry>,
75
+ accountId: string,
76
+ generation: string,
77
+ ): CodexQuotaRetry | undefined {
78
+ const retry = retries.get(accountId);
79
+ if (retry && retry.generation !== generation) {
80
+ retries.delete(accountId);
81
+ return undefined;
82
+ }
83
+ return retry;
84
+ }
85
+
86
+ /** Back off unsuccessful discovery/activation without adding another timer. */
87
+ function deferRetry(
88
+ retries: Map<string, CodexQuotaRetry>,
89
+ accountId: string,
90
+ now: number,
91
+ generation: string,
92
+ ): number {
93
+ const delay = Math.min((liveRetry(retries, accountId, generation)?.delay ?? RETRY_MS / 2) * 2, MAX_RETRY_MS);
94
+ retries.set(accountId, { after: now + delay, delay, generation });
95
+ return delay;
96
+ }
97
+
98
+ /** Every enabled window needs a retained, uncompleted deadline, not fresh usage percentages. */
99
+ function hasScheduledWindows(config: OcxConfig, accountId: string): boolean {
100
+ const setting = config.codexQuotaAutoRefresh?.[accountId];
101
+ const scheduled = scheduledByAccount.get(accountId);
102
+ return (!setting?.fiveHour || scheduled?.fiveHour !== undefined)
103
+ && (!setting?.weekly || scheduled?.weekly !== undefined);
104
+ }
105
+
54
106
  /** Report upstream window availability separately from persisted spending intent. */
55
107
  export function codexQuotaAutoRefreshStatus(
56
108
  config: OcxConfig,
@@ -187,7 +239,7 @@ async function warmAccount(config: OcxConfig, accountId: string): Promise<void |
187
239
  return;
188
240
  }
189
241
  const lease = tryAcquireNativeMainProfileClaim();
190
- if (!lease) throw new Error("native main busy");
242
+ if (!lease) throw new NativeMainBusyError();
191
243
  try {
192
244
  reconcileMainCodexAccountRuntimeState();
193
245
  if (mainWarmupRestricted(config)) return false;
@@ -309,14 +361,21 @@ export async function runCodexQuotaAutoRefresh(
309
361
  // Capture before WHAM can move an idle window's reset into the future.
310
362
  rememberWindows(config, accountId, quotaFor(accountId));
311
363
  const quota = quotaFor(accountId);
312
- if ((!quota || now - quota.updatedAt >= RETRY_MS)
313
- && (quotaRefreshAfterByAccount.get(accountId) ?? 0) <= now) {
314
- quotaRefreshAfterByAccount.set(accountId, now + RETRY_MS);
315
- try { await refresh(config, accountId); } catch { /* Retry metadata at the bounded cadence. */ }
364
+ // A known deadline remains actionable even when its usage snapshot is old.
365
+ // Only discover missing windows; never poll merely to keep percentages fresh.
366
+ if (hasScheduledWindows(config, accountId)) {
367
+ quotaRefreshAfterByAccount.delete(accountId);
368
+ } else if ((!quota || now - quota.updatedAt >= RETRY_MS)
369
+ && (liveRetry(quotaRefreshAfterByAccount, accountId, credentialGeneration(accountId))?.after ?? 0) <= now) {
370
+ deferRetry(quotaRefreshAfterByAccount, accountId, now, credentialGeneration(accountId));
371
+ try { await refresh(config, accountId); } catch { /* Retry missing metadata with backoff. */ }
316
372
  }
317
373
  if (!eligible(accountId)) return;
318
374
  rememberWindows(config, accountId, quotaFor(accountId));
319
- if ((retryAfterByAccount.get(accountId) ?? 0) > now) return;
375
+ // Backoff from a replaced credential is dropped here, so reauthenticating or rotating an
376
+ // account never waits out the failures of the credential it replaced.
377
+ const generation = credentialGeneration(accountId);
378
+ if ((liveRetry(retryAfterByAccount, accountId, generation)?.after ?? 0) > now) return;
320
379
  const windows = dueCodexQuotaAutoRefreshWindows(config, accountId, quotaFor(accountId), now);
321
380
  if (!windows) return;
322
381
  try {
@@ -327,11 +386,23 @@ export async function runCodexQuotaAutoRefresh(
327
386
  persist(config, accountId, completed);
328
387
  rememberWindows(config, accountId, quotaFor(accountId));
329
388
  } catch (error) {
330
- retryAfterByAccount.set(accountId, now + RETRY_MS);
389
+ // A failure that raced a credential replacement describes the old credential; the next
390
+ // sweep evaluates the replacement on its own evidence.
391
+ if (credentialGeneration(accountId) !== generation) return;
392
+ if (error instanceof NativeMainBusyError) {
393
+ // Local admission refused before any upstream request: retry soon, and keep the
394
+ // upstream backoff where it was instead of doubling it.
395
+ const previous = liveRetry(retryAfterByAccount, accountId, generation);
396
+ retryAfterByAccount.set(accountId, {
397
+ after: now + LOCAL_BUSY_RETRY_MS, delay: previous?.delay ?? RETRY_MS / 2, generation,
398
+ });
399
+ return;
400
+ }
401
+ const delay = deferRetry(retryAfterByAccount, accountId, now, generation);
331
402
  const account = config.codexAccounts?.find(candidate => candidate.id === accountId);
332
403
  const label = account ? codexAccountLogLabel(account) : "main";
333
404
  console.warn(`[codex-quota-auto-refresh] ${label}: ${codexWarmupFailureReason(error)}; ${
334
- isAccountNeedsReauth(accountId) ? "reauthentication required" : "retry in five minutes"
405
+ isAccountNeedsReauth(accountId) ? "reauthentication required" : `retry in ${delay / 60_000} minutes`
335
406
  }`);
336
407
  }
337
408
  }));
package/src/codex/sync.ts CHANGED
@@ -132,6 +132,16 @@ export async function syncModelsToCodex(
132
132
  // the injector, before it gets a chance to create any artifact.
133
133
  const admission = (deps.admitCodexWrite ?? admitCodexWrite)();
134
134
  if (admission.kind === "refused" && admission.authority === "service-home") {
135
+ // Startup turns this result into a failed /readyz and records nothing else, and
136
+ // `ocx sync` only says the sync did not complete, so an unattended refusal must
137
+ // leave one line behind (#5782). The admission message can name private home and
138
+ // service-definition paths, so the log carries the authority only; POST /api/sync
139
+ // returns the specific reason to an authenticated operator.
140
+ log?.error(
141
+ "[opencodex] Codex write refused (service-home): this proxy could not prove it owns the "
142
+ + "installed service homes, so catalog sync and Codex config injection were skipped. "
143
+ + "POST /api/sync returns the specific reason.",
144
+ );
135
145
  return {
136
146
  status: "refused",
137
147
  authority: "service-home",
@@ -326,7 +326,10 @@ function normalizedFailureCode(code?: string | null): string {
326
326
  // are quota-limit codes whose window length this gateway has no evidence for, and guessing long on
327
327
  // them would hold a target that may clear sooner.
328
328
  const ACCOUNT_EXHAUSTION_CODES = new Set(["usage_limit_exceeded", "usage_limit_reached", "1308"]);
329
- const ACCOUNT_EXHAUSTION_TEXT = /usage limit (?:has been )?reached/;
329
+ // Token-plan windows (Alibaba's DeepSeek/Qwen plans) report "Your token-plan 1-week quota has been
330
+ // exhausted" (#5494). The match is anchored to that phrasing: a looser "quota ... exhausted" would
331
+ // also catch per-minute limits, and this arm outranks the transient rate-limit duration.
332
+ const ACCOUNT_EXHAUSTION_TEXT = /usage limit (?:has been )?reached|token-plan\s+\S+\s+quota has been exhausted/;
330
333
 
331
334
  function isAccountWindowExhausted(message: string, code?: string | null): boolean {
332
335
  return ACCOUNT_EXHAUSTION_CODES.has(normalizedFailureCode(code))
package/src/combos/jev.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  import { resolveProviderApiKey } from "../providers/api-key-resolve";
7
7
  import { providerMatchesRegistryTransport } from "../providers/registry";
8
8
  import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
9
+ import { JEV_MAX_CANDIDATE_FIELD_CHARS } from "./types";
9
10
 
10
11
  export const JEV_PROVIDER_ID = "jev";
11
12
  export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
@@ -13,7 +14,6 @@ export const JEV_MODEL = "jev-latest";
13
14
 
14
15
  const JEV_TIMEOUT_MS = 4_000;
15
16
  const JEV_MAX_CANDIDATES = 64;
16
- const JEV_MAX_CANDIDATE_FIELD_CHARS = 512;
17
17
  const JEV_MAX_REQUEST_BYTES = 65_536;
18
18
  const JEV_MAX_RESPONSE_BYTES = 65_536;
19
19
  const JEV_OUTBOUND_DEPENDENCIES = {
@@ -70,6 +70,8 @@ export interface JevCandidate {
70
70
  provider: string;
71
71
  model: string;
72
72
  reasoningEfforts: readonly OcxComboDefaultEffort[];
73
+ /** Optional operator note sent as decision evidence for this target only. */
74
+ modelProfile?: string;
73
75
  }
74
76
 
75
77
  export interface JevDecision {
@@ -337,7 +339,7 @@ function hasImageContent(item: Record<string, unknown>): boolean {
337
339
  return item.content.some(part => isRecord(part) && (part.type === "input_image" || part.type === "image_url"));
338
340
  }
339
341
 
340
- export function buildJevState(body: unknown): Record<string, unknown> {
342
+ export function buildJevState(body: unknown, candidates: readonly JevCandidate[] = []): Record<string, unknown> {
341
343
  const input = isRecord(body) ? body.input : undefined;
342
344
  let task = "";
343
345
  let previousAssistant = "";
@@ -383,11 +385,18 @@ export function buildJevState(body: unknown): Record<string, unknown> {
383
385
  }
384
386
  }
385
387
 
388
+ const operatorNotes: Record<string, string> = {};
389
+ for (const candidate of candidates) {
390
+ const note = candidate.modelProfile?.trim();
391
+ if (note) operatorNotes[candidate.key] = note;
392
+ }
393
+
386
394
  return {
387
395
  task,
388
396
  signals: { has_image: hasImage, tool_history: toolHistory },
389
397
  step,
390
398
  ...(previousAssistant ? { previous_assistant: previousAssistant.slice(-ASSISTANT_TAIL_CHARS) } : {}),
399
+ ...(Object.keys(operatorNotes).length ? { operator_notes: operatorNotes } : {}),
391
400
  };
392
401
  }
393
402
 
@@ -425,7 +434,9 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
425
434
  function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
426
435
  if (candidates.length > JEV_MAX_CANDIDATES) return false;
427
436
  return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
428
- .every(value => value.length > 0 && value.length <= JEV_MAX_CANDIDATE_FIELD_CHARS));
437
+ .every(value => value.length > 0 && value.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)
438
+ && (candidate.modelProfile === undefined
439
+ || (typeof candidate.modelProfile === "string" && candidate.modelProfile.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)));
429
440
  }
430
441
 
431
442
  function modelProfile(candidate: JevCandidate): string {
@@ -566,7 +577,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
566
577
 
567
578
  let requestBody: string;
568
579
  try {
569
- const state = buildJevState(options.body);
580
+ const state = buildJevState(options.body, options.candidates);
570
581
  if (!hasJevDecisionState(state)) return failed("no_state");
571
582
  requestBody = JSON.stringify({
572
583
  model: JEV_MODEL,
@@ -4,6 +4,7 @@ import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort,
4
4
  import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
5
5
 
6
6
  export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
7
+ export const JEV_MAX_CANDIDATE_FIELD_CHARS = 512;
7
8
  export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers";
8
9
 
9
10
  /**
@@ -27,6 +28,8 @@ export interface NormalizedComboTarget {
27
28
  /** Emergency-only target, deferred under `cooldownWaitPolicy` (#5691). */
28
29
  lastResort: boolean;
29
30
  reasoningEfforts?: OcxComboDefaultEffort[];
31
+ /** Optional JEV decision description. */
32
+ modelProfile?: string;
30
33
  }
31
34
 
32
35
  export interface NormalizedComboConfig {
@@ -333,6 +336,16 @@ export function comboConfigIssues(
333
336
  message: `targets[${i}].lastResort must be a boolean`,
334
337
  });
335
338
  }
339
+ if (target.modelProfile !== undefined
340
+ && (typeof target.modelProfile !== "string"
341
+ || target.modelProfile.trim().length === 0
342
+ || target.modelProfile.length > JEV_MAX_CANDIDATE_FIELD_CHARS
343
+ || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(target.modelProfile))) {
344
+ issues.push({
345
+ path: ["targets", i, "modelProfile"],
346
+ message: `targets[${i}].modelProfile must be a non-empty string of at most ${JEV_MAX_CANDIDATE_FIELD_CHARS} characters; only tab, line feed and carriage return are allowed among control characters`,
347
+ });
348
+ }
336
349
 
337
350
  if (provider && model) {
338
351
  const key = targetKey({ provider, model });
@@ -389,6 +402,9 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
389
402
  ...(target.reasoningEfforts !== undefined
390
403
  ? { reasoningEfforts: [...target.reasoningEfforts] }
391
404
  : {}),
405
+ ...(typeof target.modelProfile === "string" && target.modelProfile.trim()
406
+ ? { modelProfile: target.modelProfile.trim() }
407
+ : {}),
392
408
  lastResort: target.lastResort === true,
393
409
  })),
394
410
  };
@@ -63,6 +63,8 @@ import {
63
63
  runtimeRoleSchema,
64
64
  spendSchema,
65
65
  compactionRoutingSchema,
66
+ skillsConfigSchema,
67
+ memoryModelsSchema,
66
68
  } from "./schema/leaf-validators";
67
69
 
68
70
  export type ConfigDiagnostics = {
@@ -594,11 +596,26 @@ export function metricsExportConfigError(value: unknown): string | null {
594
596
  return null;
595
597
  }
596
598
 
599
+
600
+ function skillsConfigError(value: unknown): string | null {
601
+ const raw = rawConfigRecord(value);
602
+ if (!raw || !Object.hasOwn(raw, "skills") || raw.skills === undefined) return null;
603
+ const result = skillsConfigSchema.safeParse(raw.skills);
604
+ if (result.success) return null;
605
+ const issue = result.error.issues[0];
606
+ const field = issue?.path.join(".");
607
+ return "schema_invalid: skills" + (field ? "." + field : "") + ": " + (issue?.message ?? "invalid configuration");
608
+ }
609
+
597
610
  export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
598
611
  const compactionRouting = rawConfigRecord(value)?.compactionRouting;
599
612
  if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
600
613
  return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
601
614
  }
615
+ const memoryModels = rawConfigRecord(value)?.memoryModels;
616
+ if (memoryModels !== undefined && !memoryModelsSchema.safeParse(memoryModels).success) {
617
+ return { ok: false, error: "schema_invalid: memoryModels: requires a nonblank model and an optional declared reasoningEffort per configured phase, and no other fields" };
618
+ }
602
619
  const boundaryError = compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
603
620
  ?? blankHostnameError(value)
604
621
  ?? claudeSubagentEffortError(value)
@@ -625,7 +642,8 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
625
642
  ?? clientRolePairError(value)
626
643
  ?? loopbackListenerPortError(value)
627
644
  ?? managementIngressConfigError(value)
628
- ?? metricsExportConfigError(value);
645
+ ?? metricsExportConfigError(value)
646
+ ?? skillsConfigError(value);
629
647
  if (boundaryError) return { ok: false, error: boundaryError };
630
648
  const result = configSchema.safeParse(value);
631
649
  if (result.success) {
@@ -122,6 +122,35 @@ export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxCon
122
122
  if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
123
123
  warnDegradedStreamMode(rawParsed, validated);
124
124
  warnDegradedCompactionRouting(rawParsed, validated);
125
+ warnDegradedMemoryModels(rawParsed, validated);
126
+ }
127
+
128
+ /**
129
+ * A malformed `memoryModels` phase disables that phase rather than failing the whole schema, so
130
+ * say so once: silently keeping whatever route the phase already had — which may be the shadow
131
+ * intercept rather than Codex's own model — is the outcome a typo must not produce quietly.
132
+ */
133
+ export function warnDegradedMemoryModels(rawParsed: unknown, validated: OcxConfig): void {
134
+ if (!rawParsed || typeof rawParsed !== "object") return;
135
+ const raw = (rawParsed as Record<string, unknown>).memoryModels;
136
+ if (raw === undefined) return;
137
+ if (validated.memoryModels === undefined || raw === null || typeof raw !== "object" || Array.isArray(raw)) {
138
+ console.warn("\u26a0\ufe0f config.json memoryModels is invalid (expected { extract?: { model, reasoningEffort? }, consolidation?: { model, reasoningEffort? } } with a nonblank model and a declared effort per phase) \u2014 the memory pipeline keeps its existing route, which may include shadow-call interception");
139
+ return;
140
+ }
141
+ // A misspelled phase key is stripped by the permissive load schema, so without this warning it
142
+ // disappears silently and the next settings save persists the sanitized map without it.
143
+ for (const key of Object.keys(raw as Record<string, unknown>)) {
144
+ if (key === "extract" || key === "consolidation") continue;
145
+ // Redact and JSON-escape the key name: a malformed hand-edit can place a secret in a property
146
+ // name, and a control character in one must not be able to forge a log line.
147
+ console.warn("\u26a0\ufe0f config.json memoryModels." + JSON.stringify(redactSecretString(key)) + " is not a recognized phase \u2014 ignoring it");
148
+ }
149
+ for (const phase of ["extract", "consolidation"] as const) {
150
+ if ((raw as Record<string, unknown>)[phase] !== undefined && validated.memoryModels[phase] === undefined) {
151
+ console.warn("\u26a0\ufe0f config.json memoryModels." + phase + " is invalid (expected { model, reasoningEffort? } with a nonblank model) \u2014 that phase keeps its existing route, which may include shadow-call interception");
152
+ }
153
+ }
125
154
  }
126
155
 
127
156
  /**