@bitkyc08/opencodex 2.90.0 → 2.92.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 (250) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  3. package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
  4. package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
  5. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  6. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  9. package/package.json +4 -3
  10. package/src/adapters/anthropic.ts +2 -5
  11. package/src/adapters/claude-cli/usage-admission.ts +14 -0
  12. package/src/adapters/google-vertex-endpoint.ts +9 -1
  13. package/src/adapters/google-vertex-key-project.ts +223 -0
  14. package/src/adapters/google.ts +13 -3
  15. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  16. package/src/adapters/openai-chat.ts +2 -2
  17. package/src/adapters/openai-responses/passthrough.ts +2 -1
  18. package/src/adapters/openai-responses/request-strips.ts +92 -0
  19. package/src/adapters/openai-responses/transport.ts +6 -0
  20. package/src/adapters/registry.ts +5 -0
  21. package/src/adapters/workbuddy-schema.ts +61 -0
  22. package/src/adapters/workbuddy.ts +68 -0
  23. package/src/bridge/response-json.ts +1 -1
  24. package/src/bridge/sse.ts +1 -1
  25. package/src/chat/inbound.ts +20 -2
  26. package/src/chat/outbound.ts +47 -8
  27. package/src/chat/reasoning-details.ts +137 -0
  28. package/src/cli/capabilities-agents-routing.ts +53 -3
  29. package/src/cli/capabilities-base.ts +15 -2
  30. package/src/cli/capabilities-observe-system.ts +4 -4
  31. package/src/cli/codex-cli-update.ts +8 -0
  32. package/src/cli/codex-shim-autorestore.ts +5 -0
  33. package/src/cli/combo-result.ts +6 -0
  34. package/src/cli/combo-stats.ts +3 -0
  35. package/src/cli/combo.ts +66 -42
  36. package/src/cli/dispatch.ts +30 -0
  37. package/src/cli/fallback.ts +127 -0
  38. package/src/cli/help.ts +1 -0
  39. package/src/cli/index.ts +1 -2
  40. package/src/cli/launcher-context.ts +49 -2
  41. package/src/cli/link-apply-restart.ts +72 -0
  42. package/src/cli/link-child.ts +6 -2
  43. package/src/cli/link-cleanup.ts +95 -0
  44. package/src/cli/link-enrollment.ts +4 -0
  45. package/src/cli/link.ts +123 -35
  46. package/src/cli/provider.ts +46 -3
  47. package/src/cli/registry.ts +13 -1
  48. package/src/cli/restart-failure.ts +4 -0
  49. package/src/cli/runtime-api.ts +1 -1
  50. package/src/cli/status-codex-settings.ts +56 -0
  51. package/src/cli/status-fallback.ts +29 -0
  52. package/src/cli/status-install.ts +52 -0
  53. package/src/cli/status.ts +20 -1
  54. package/src/cli/system-restart-client.ts +32 -1
  55. package/src/cli/update-restart-child.ts +4 -3
  56. package/src/cli/update-restart-home.ts +20 -8
  57. package/src/cli/usage-compare.ts +68 -12
  58. package/src/client/connect.ts +82 -6
  59. package/src/client/home-unreachable.ts +16 -0
  60. package/src/client/hub-relay.ts +8 -2
  61. package/src/client/link-home.ts +119 -6
  62. package/src/client/link-ingress.ts +1 -1
  63. package/src/client/link-parity-reporter.ts +107 -0
  64. package/src/client/link-readiness.ts +97 -0
  65. package/src/client/link-relay-transport.ts +84 -9
  66. package/src/client/link-relay.ts +98 -15
  67. package/src/client/link-tunnel.ts +45 -18
  68. package/src/client/machine-listener.ts +103 -3
  69. package/src/client/runtime-restart.ts +127 -0
  70. package/src/client/runtime.ts +155 -25
  71. package/src/codex/account-store.ts +17 -3
  72. package/src/codex/app-server-restart-service.ts +6 -5
  73. package/src/codex/auth-context.ts +10 -6
  74. package/src/codex/catalog/effort.ts +9 -2
  75. package/src/codex/catalog/gather-capture.ts +4 -2
  76. package/src/codex/catalog/parsing.ts +4 -0
  77. package/src/codex/catalog/provider-models.ts +14 -8
  78. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  79. package/src/codex/cli-install-provenance.ts +418 -0
  80. package/src/codex/cli-installation-targets.ts +5 -1
  81. package/src/codex/cli-update-plan.ts +43 -16
  82. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  83. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  84. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  85. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  86. package/src/codex/pool-refresh-backoff.ts +3 -1
  87. package/src/codex/quota-history.ts +7 -0
  88. package/src/codex/quota.ts +10 -0
  89. package/src/codex/subagent-model-fallback.ts +24 -1
  90. package/src/codex/user-identity.ts +35 -6
  91. package/src/codex/windows-current-sid.ts +162 -0
  92. package/src/combos/index.ts +1 -0
  93. package/src/combos/jev-quota-activation.ts +5 -1
  94. package/src/combos/jev-quota-route.ts +1 -1
  95. package/src/combos/jev-quota.ts +9 -1
  96. package/src/combos/jev-session-pin-config.ts +51 -0
  97. package/src/combos/jev.ts +6 -0
  98. package/src/combos/resolve.ts +12 -0
  99. package/src/combos/types.ts +19 -0
  100. package/src/config/diagnostics.ts +5 -0
  101. package/src/config/load-degrade.ts +2 -0
  102. package/src/config/provider-validation.ts +7 -0
  103. package/src/config/schema/config-schema.ts +3 -0
  104. package/src/config/schema/fallback-policy.ts +40 -0
  105. package/src/config/schema/leaf-validators.ts +1 -0
  106. package/src/generated/compatibility-version.json +375 -195
  107. package/src/lib/authorization-scheme.ts +95 -0
  108. package/src/lib/decision-quota-sink.ts +10 -0
  109. package/src/lib/provider-client-headers.ts +1 -0
  110. package/src/lib/redact.ts +2 -2
  111. package/src/lib/system-restart-contract.ts +24 -0
  112. package/src/lib/test-home-guard.ts +78 -11
  113. package/src/lib/translator-budget.ts +4 -4
  114. package/src/lib/upstream-retry.ts +4 -1
  115. package/src/link/child-parity.ts +124 -0
  116. package/src/link/codex-settings.ts +37 -0
  117. package/src/link/compatibility.ts +119 -0
  118. package/src/link/diagnostics.ts +193 -0
  119. package/src/link/management-routes.ts +2 -0
  120. package/src/link/peer-report.ts +100 -0
  121. package/src/link/relay-auth.ts +6 -0
  122. package/src/link/remote-platform.ts +78 -0
  123. package/src/link/ssh-argv.ts +19 -0
  124. package/src/link/status-projection.ts +63 -2
  125. package/src/link/supervisor.ts +45 -12
  126. package/src/link/tunnel-history.ts +109 -0
  127. package/src/link/tunnel-state.ts +80 -17
  128. package/src/messaging/remote-files.ts +24 -1
  129. package/src/oauth/generic-account-failover.ts +91 -7
  130. package/src/oauth/index.ts +16 -1
  131. package/src/oauth/kimi.ts +2 -0
  132. package/src/oauth/model-discovery-auth.ts +5 -0
  133. package/src/oauth/nous.ts +4 -1
  134. package/src/oauth/store.ts +6 -2
  135. package/src/oauth/token-guardian.ts +7 -0
  136. package/src/oauth/types.ts +9 -0
  137. package/src/oauth/workbuddy.ts +139 -0
  138. package/src/protocols/codecs/chat.ts +3 -3
  139. package/src/protocols/encoders/adapter-events.ts +26 -2
  140. package/src/protocols/encoders/chat.ts +55 -3
  141. package/src/providers/antigravity-quota-identity.ts +37 -0
  142. package/src/providers/base-url-choices.ts +8 -5
  143. package/src/providers/derive.ts +23 -3
  144. package/src/providers/mistral-transport.ts +37 -4
  145. package/src/providers/model-discovery.ts +2 -0
  146. package/src/providers/model-rename-fields.ts +1 -0
  147. package/src/providers/quota/account-cache.ts +3 -2
  148. package/src/providers/quota/antigravity.ts +27 -22
  149. package/src/providers/quota/report-cache.ts +13 -4
  150. package/src/providers/quota/vendor-probes-key.ts +60 -0
  151. package/src/providers/quota/workbuddy.ts +83 -0
  152. package/src/providers/quota-decision-antigravity.ts +64 -0
  153. package/src/providers/quota-decision-publication.ts +11 -0
  154. package/src/providers/quota-decision-snapshot.ts +21 -4
  155. package/src/providers/quota-types.ts +2 -0
  156. package/src/providers/quota.ts +3 -1
  157. package/src/providers/registry/entries-core.ts +9 -0
  158. package/src/providers/registry/entries-extended.ts +20 -7
  159. package/src/providers/registry/model-ids.ts +3 -0
  160. package/src/providers/registry/model-seeds.ts +27 -28
  161. package/src/providers/registry/types.ts +19 -1
  162. package/src/providers/resolved-model-policy.ts +4 -4
  163. package/src/providers/routed-config.ts +15 -0
  164. package/src/providers/stale-model-roster-migration.ts +83 -15
  165. package/src/providers/workbuddy-credit-balance.ts +81 -0
  166. package/src/providers/workbuddy-models.ts +37 -0
  167. package/src/providers/workbuddy.ts +60 -0
  168. package/src/quota-pool/assemble.ts +326 -0
  169. package/src/quota-pool/contract.ts +58 -0
  170. package/src/quota-pool/identity.ts +203 -0
  171. package/src/remote/features.ts +74 -0
  172. package/src/remote/protocol.ts +2 -0
  173. package/src/remote/served-by.ts +57 -0
  174. package/src/responses/parser.ts +7 -0
  175. package/src/responses/reasoning-envelope.ts +12 -0
  176. package/src/responses/reasoning-replay-cache.ts +18 -14
  177. package/src/router.ts +2 -0
  178. package/src/routing/analytics.ts +2 -3
  179. package/src/routing/fallback-policy.ts +290 -0
  180. package/src/routing/fallback-state.ts +179 -0
  181. package/src/server/audio-client.ts +2 -1
  182. package/src/server/audio-dictation.ts +2 -1
  183. package/src/server/audio-upstream.ts +2 -1
  184. package/src/server/auth-cors.ts +7 -5
  185. package/src/server/background-lifecycle.ts +4 -0
  186. package/src/server/chat-completions.ts +22 -2
  187. package/src/server/chat-native-reasoning.ts +10 -3
  188. package/src/server/chat-native.ts +13 -2
  189. package/src/server/claude-messages.ts +3 -0
  190. package/src/server/gui-session.ts +2 -1
  191. package/src/server/index/link-listener.ts +11 -1
  192. package/src/server/index/link-management.ts +47 -0
  193. package/src/server/index/link-relay-sessions.ts +20 -7
  194. package/src/server/index/optional-listeners.ts +11 -2
  195. package/src/server/index/serve-options.ts +19 -2
  196. package/src/server/live.ts +5 -0
  197. package/src/server/local-management-read-client.ts +3 -1
  198. package/src/server/management/combo-routes.ts +10 -4
  199. package/src/server/management/context.ts +1 -1
  200. package/src/server/management/fallback-routes.ts +75 -0
  201. package/src/server/management/link-enrollment.ts +21 -3
  202. package/src/server/management/link-route-state.ts +2 -0
  203. package/src/server/management/link-routes.ts +16 -2
  204. package/src/server/management/provider-routes.ts +1 -1
  205. package/src/server/management/quota-pool-routes.ts +24 -0
  206. package/src/server/management/route-registry.ts +3 -0
  207. package/src/server/management-api.ts +16 -0
  208. package/src/server/management-auth.ts +2 -1
  209. package/src/server/port-reclaim.ts +51 -9
  210. package/src/server/request-log-conversation.ts +49 -0
  211. package/src/server/request-log.ts +2 -0
  212. package/src/server/responses/agent-task-recovery.ts +1 -1
  213. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  214. package/src/server/responses/compact.ts +12 -4
  215. package/src/server/responses/core-auth.ts +11 -1
  216. package/src/server/responses/core-combo.ts +125 -32
  217. package/src/server/responses/core-lifetime.ts +2 -2
  218. package/src/server/responses/core-normalize.ts +6 -3
  219. package/src/server/responses/core-opaque-recovery.ts +1 -0
  220. package/src/server/responses/core-options.ts +5 -0
  221. package/src/server/responses/core-replay.ts +2 -1
  222. package/src/server/responses/encrypted-payload.ts +3 -3
  223. package/src/server/responses/jev-combo-choice.ts +39 -0
  224. package/src/server/responses/jev-session-pin.ts +203 -0
  225. package/src/server/responses/main-hard-lock-routing.ts +54 -2
  226. package/src/server/responses/passthrough-delivery.ts +4 -0
  227. package/src/server/responses/policy-fallback.ts +44 -1
  228. package/src/server/responses/request-prepare.ts +19 -6
  229. package/src/server/responses/request-transport.ts +25 -6
  230. package/src/server/responses/run-turn-execution.ts +2 -4
  231. package/src/server/responses/sidecar-execution.ts +2 -4
  232. package/src/server/responses-phase-inference.ts +267 -0
  233. package/src/server/restart-replacement.ts +50 -2
  234. package/src/server/sse-payload-rewrite.ts +14 -1
  235. package/src/server/ws-bridge.ts +5 -0
  236. package/src/service/cli.ts +25 -2
  237. package/src/service/definition.ts +20 -8
  238. package/src/service/diagnostics.ts +20 -6
  239. package/src/service/status-json.ts +116 -0
  240. package/src/types/config.ts +13 -0
  241. package/src/types/fallback.ts +38 -0
  242. package/src/types/provider.ts +9 -1
  243. package/src/types/request.ts +1 -0
  244. package/src/types/wire.ts +1 -0
  245. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  246. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  247. package/src/usage/jev-stats.ts +54 -14
  248. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  249. package/gui/dist/assets/index-TmaHa54u.js +0 -94
  250. package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
@@ -15,9 +15,10 @@
15
15
  */
16
16
  import {
17
17
  clearDecisionAccountQuotas, invalidateDecisionKeyQuotas, observeDecisionKeyCredential, publishDecisionAccountQuota,
18
- publishDecisionKeyQuota, publishDecisionQuotaRoster, readLoadedDecisionKeyQuota, readLoadedDecisionQuotaPool,
18
+ publishDecisionKeyQuota, publishDecisionQuotaRoster, readLoadedDecisionKeyQuota, readLoadedDecisionQuotaPool, replaceDecisionKeyQuotas,
19
19
  setDecisionAccountUsable, withdrawDecisionQuotaRoster,
20
20
  } from "../providers/quota-decision-snapshot";
21
+ import { captureAntigravityReceipt, observeAntigravityQuotaStore, replaceAntigravityQuotas } from "../providers/quota-decision-antigravity";
21
22
  import { registerDecisionQuotaSink, type DecisionQuotaSink, type MainDecisionRow } from "../lib/decision-quota-sink";
22
23
  import { registerLiveConfigObserver } from "../lib/state-store-registrations";
23
24
  import { getConfigDir } from "../config/paths";
@@ -32,6 +33,7 @@ const POOL_PROVIDERS = ["anthropic", "codex", "codex-main"] as const;
32
33
 
33
34
  /** Project an auth store the owner already read into the secret-free Anthropic roster (primary pool only); a failure withdraws so the pool reads as unknown. The generation is a one-way digest of the credential, never the credential itself. */
34
35
  function publishAuthDecisionQuotaRoster(store: AuthStore): void {
36
+ observeAntigravityQuotaStore(store);
35
37
  try {
36
38
  publishDecisionQuotaRoster("anthropic", (store.anthropic?.accounts ?? []).map(row => ({
37
39
  id: row.id,
@@ -82,6 +84,8 @@ const snapshotSink: DecisionQuotaSink = {
82
84
  mainRosterChanged: row => publishMainRow(row),
83
85
  readKeyQuota: readLoadedDecisionKeyQuota,
84
86
  readPool: readLoadedDecisionQuotaPool,
87
+ captureAntigravityReceipt,
88
+ replaceCollectedQuotas: (keys, oauth) => { replaceDecisionKeyQuotas(keys); replaceAntigravityQuotas(oauth); },
85
89
  };
86
90
 
87
91
  let active: { detach: () => void; root: string } | undefined;
@@ -7,7 +7,7 @@ export function jevQuotaCandidates(options: ResolveJevDecisionOptions): readonly
7
7
  const tiers = normalizeJevQuotaTiers(options.decisionQuotaTiers) ?? JEV_QUOTA_DEFAULT_TIERS;
8
8
  const now = (options.now ?? Date.now)();
9
9
  return options.candidates.map(candidate => ({ ...candidate,
10
- quota: jevQuotaSignalForTarget(options.config, candidate.provider, candidate.model, now, tiers) }));
10
+ quota: candidate.quotaIdentity === null ? { tier: "unknown" as const } : jevQuotaSignalForTarget(options.config, candidate.quotaIdentity?.provider ?? candidate.provider, candidate.quotaIdentity?.model ?? candidate.model, now, tiers) }));
11
11
  }
12
12
  /** Attach the per-tier quota summary to a decision when any candidate carried quota evidence. */
13
13
  export function withJevQuotaSummary(decision: JevDecision, candidates: readonly JevCandidate[]): JevDecision {
@@ -6,6 +6,7 @@ import type { DecisionQuotaWindow } from "../providers/quota-decision-snapshot";
6
6
  import { readLoadedDecisionKeyQuota, readLoadedDecisionQuotaPool } from "../lib/decision-quota-sink";
7
7
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
8
8
  import { providerCodexAccountMode } from "../providers/registry";
9
+ import { isAntigravityArgonIdentity } from "../providers/antigravity-quota-identity";
9
10
  import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
10
11
  import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
11
12
  import { JEV_QUOTA_DEFAULT_TIERS, jevQuotaRank, jevQuotaTier, type JevQuotaTier, type ResolvedJevQuotaTiers, type JevQuotaDecisionSummary } from "./jev-quota-config";
@@ -18,11 +19,18 @@ export interface JevQuotaSignal {
18
19
  }
19
20
  /** Build the signal used when evidence is missing, stale or invalid; it ties healthy. */
20
21
  const unknown = (): JevQuotaSignal => ({ tier: "unknown" });
22
+ /** Antigravity quota buckets are metered per model family; Gemini and Claude/3P draw from separate counters. */
23
+ function antigravityFamily(model: string): DecisionQuotaWindow["family"] {
24
+ const id = model.toLowerCase();
25
+ if (isAntigravityArgonIdentity(id)) return undefined;
26
+ return id.startsWith("gemini-") ? "gem" : id.startsWith("claude-") ? "cla" : undefined;
27
+ }
21
28
  /** Reduce decision quota windows to the worst applicable window for a model, ignoring cold, stale, future, invalid or reset-expired rows. */
22
29
  export function jevQuotaSignalFromWindows(windows: readonly DecisionQuotaWindow[] | undefined, model: string, now: number, tiers = JEV_QUOTA_DEFAULT_TIERS): JevQuotaSignal {
23
30
  let worst: DecisionQuotaWindow | undefined;
24
31
  for (const row of windows ?? []) {
25
- if (!["5h", "weekly", "monthly"].includes(row.window) && anthropicModelFamily(model)?.toLowerCase() !== row.window) continue;
32
+ if (row.family !== undefined) { if (row.family !== antigravityFamily(model)) continue; }
33
+ else if (!["5h", "weekly", "monthly"].includes(row.window) && anthropicModelFamily(model)?.toLowerCase() !== row.window) continue;
26
34
  if (!Number.isFinite(now) || now < 0 || !Number.isFinite(row.observedAt) || row.observedAt < 0 || row.observedAt > now
27
35
  || now - row.observedAt > JEV_QUOTA_SIGNAL_MAX_AGE_MS || !Number.isFinite(row.percent) || row.percent < 0 || row.percent > 100
28
36
  || row.resetAt !== undefined && (!Number.isFinite(row.resetAt) || row.resetAt < 0 || row.resetAt <= now)) continue;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Opt-in JEV session pin settings (#7525). Dependency-free so combo validation, the management
3
+ * API and the CLI share one grammar and one range.
4
+ */
5
+
6
+ /** Shortest idle window a pin may be configured with. */
7
+ export const JEV_SESSION_PIN_TTL_MIN_MS = 60_000;
8
+ /** Longest idle window a pin may be configured with (one day). */
9
+ export const JEV_SESSION_PIN_TTL_MAX_MS = 86_400_000;
10
+
11
+ export interface JevSessionPinConfig {
12
+ /** Idle window after the last successful pinned turn; a quieter session is decided afresh. */
13
+ ttlMs: number;
14
+ }
15
+
16
+ /**
17
+ * Closed persisted vocabulary: `session` marks a turn that reused its pin; every other value names
18
+ * why an existing pin was released before a fresh decision.
19
+ */
20
+ export const JEV_SESSION_PIN_REASONS = [
21
+ "session",
22
+ "expired",
23
+ "config_changed",
24
+ "target_unavailable",
25
+ "effort_unavailable",
26
+ "quota_nearly_exhausted",
27
+ ] as const;
28
+ export type JevSessionPinReason = (typeof JEV_SESSION_PIN_REASONS)[number];
29
+ export type JevSessionPinRelease = Exclude<JevSessionPinReason, "session">;
30
+
31
+ /** The canonical pin setting, or undefined for anything that is not exactly `{ ttlMs }` in range. */
32
+ export function normalizeJevSessionPin(value: unknown): JevSessionPinConfig | undefined {
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
34
+ const keys = Object.keys(value);
35
+ if (keys.length !== 1 || keys[0] !== "ttlMs") return undefined;
36
+ const ttlMs = (value as { ttlMs?: unknown }).ttlMs;
37
+ return typeof ttlMs === "number" && Number.isSafeInteger(ttlMs)
38
+ && ttlMs >= JEV_SESSION_PIN_TTL_MIN_MS && ttlMs <= JEV_SESSION_PIN_TTL_MAX_MS
39
+ ? { ttlMs }
40
+ : undefined;
41
+ }
42
+
43
+ const DURATION_UNITS_MS: Readonly<Record<string, number>> = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 };
44
+
45
+ /** Parse `90s`, `20m`, `1h` or bare milliseconds into an in-range TTL, else undefined. */
46
+ export function parseJevSessionPinDuration(raw: string): number | undefined {
47
+ const match = /^(\d{1,9})(ms|s|m|h)?$/.exec(raw.trim());
48
+ if (!match) return undefined;
49
+ const ttlMs = Number(match[1]) * DURATION_UNITS_MS[match[2] ?? "ms"]!;
50
+ return ttlMs >= JEV_SESSION_PIN_TTL_MIN_MS && ttlMs <= JEV_SESSION_PIN_TTL_MAX_MS ? ttlMs : undefined;
51
+ }
package/src/combos/jev.ts CHANGED
@@ -80,6 +80,8 @@ export interface JevCandidate {
80
80
  /** Optional operator note sent as decision evidence for this target only. */
81
81
  modelProfile?: string;
82
82
  quota?: JevQuotaSignal;
83
+ /** Internal native identity from ordinary routing; never replaces the configured allowlist. */
84
+ quotaIdentity?: { provider: string; model: string } | null;
83
85
  }
84
86
 
85
87
  /**
@@ -107,6 +109,10 @@ export interface JevDecision {
107
109
  confidence?: number;
108
110
  chosenProbability?: number;
109
111
  usage?: Record<string, number>;
112
+ /** Set when an opt-in session pin (#7525) supplied this decision without asking the backend. */
113
+ pinned?: true;
114
+ /** `session` with `pinned`; otherwise why an existing session pin was released first. */
115
+ pinReason?: import("./jev-session-pin-config").JevSessionPinReason;
110
116
  }
111
117
 
112
118
  export interface ResolveJevDecisionOptions {
@@ -74,6 +74,18 @@ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now:
74
74
  return providerExclusionReason(config, target, now) === undefined;
75
75
  }
76
76
 
77
+ /**
78
+ * OCX-11: the coarse fallback trigger for a target the picker passed over for a reason other
79
+ * than a cooldown (provider missing or disabled, cached quota spent, ineligible for the request).
80
+ */
81
+ export function comboTargetSkipTrigger(
82
+ config: OcxConfig,
83
+ target: OcxComboTarget,
84
+ now = Date.now(),
85
+ ): "quota_exhausted" | "unavailable" {
86
+ return providerExclusionReason(config, target, now) === "quota exhausted" ? "quota_exhausted" : "unavailable";
87
+ }
88
+
77
89
  /**
78
90
  * Why each target of a combo cannot be picked right now, for a zero-attempt refusal. Only the
79
91
  * configured provider/model and a coarse label are named, never account or request data.
@@ -1,4 +1,10 @@
1
1
  import { normalizeJevQuotaTiers, type ResolvedJevQuotaTiers } from "./jev-quota-config";
2
+ import {
3
+ JEV_SESSION_PIN_TTL_MAX_MS,
4
+ JEV_SESSION_PIN_TTL_MIN_MS,
5
+ normalizeJevSessionPin,
6
+ type JevSessionPinConfig,
7
+ } from "./jev-session-pin-config";
2
8
  import { isCodexReasoningEffort, isDeclaredReasoningEffort } from "../reasoning-effort";
3
9
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
4
10
  import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
@@ -79,6 +85,8 @@ export interface NormalizedComboConfig {
79
85
  decisionTimeoutMs?: number;
80
86
  decisionQuotaSignals?: boolean;
81
87
  decisionQuotaTiers?: ResolvedJevQuotaTiers;
88
+ /** JEV only, opt-in: keep a session on the target JEV chose for it (#7525). */
89
+ sessionPin?: JevSessionPinConfig;
82
90
  targets: NormalizedComboTarget[];
83
91
  }
84
92
 
@@ -310,6 +318,16 @@ export function comboConfigIssues(
310
318
  if (field === "decisionQuotaSignals" ? typeof body[field] !== "boolean" : !normalizeJevQuotaTiers(body[field]))
311
319
  issues.push({ path: [field], message: field === "decisionQuotaSignals" ? "decisionQuotaSignals must be a boolean" : "decisionQuotaTiers must contain finite 0..100 strictly ascending thresholds" });
312
320
  }
321
+ if (body.sessionPin !== undefined && body.sessionPin !== null) {
322
+ if (body.strategy !== "jev") {
323
+ issues.push({ path: ["sessionPin"], message: 'sessionPin is only valid with strategy "jev"' });
324
+ } else if (!normalizeJevSessionPin(body.sessionPin)) {
325
+ issues.push({
326
+ path: ["sessionPin"],
327
+ message: `sessionPin must be { "ttlMs": <integer from ${JEV_SESSION_PIN_TTL_MIN_MS} to ${JEV_SESSION_PIN_TTL_MAX_MS}> }`,
328
+ });
329
+ }
330
+ }
313
331
  if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
314
332
  const decisionProvider = typeof body.decisionProvider === "string" ? body.decisionProvider.trim() : "";
315
333
  if (!decisionProvider) {
@@ -533,6 +551,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
533
551
  ...(typeof raw.decisionTimeoutMs === "number" ? { decisionTimeoutMs: raw.decisionTimeoutMs } : {}),
534
552
  ...(raw.strategy === "jev" && raw.decisionQuotaSignals === true ? { decisionQuotaSignals: true } : {}),
535
553
  ...(raw.strategy === "jev" && normalizeJevQuotaTiers(raw.decisionQuotaTiers) ? { decisionQuotaTiers: normalizeJevQuotaTiers(raw.decisionQuotaTiers)! } : {}),
554
+ ...(raw.strategy === "jev" && normalizeJevSessionPin(raw.sessionPin) ? { sessionPin: normalizeJevSessionPin(raw.sessionPin)! } : {}),
536
555
  targets: raw.targets.map(target => ({
537
556
  provider: target.provider.trim(),
538
557
  model: target.model.trim(),
@@ -11,6 +11,7 @@ import { nativeReasoningRetentionConfigError } from "./schema/native-reasoning-r
11
11
  import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
12
12
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
13
13
  import { mainAccountHardLockFallbackError } from "./schema/main-account-hard-lock-fallback";
14
+ import { fallbackPolicyError } from "./schema/fallback-policy";
14
15
  import type { OcxConfig } from "../types";
15
16
  import { anthropicAccountPoolConfigError, anthropicOAuthInstanceConfigError, anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
16
17
  import { configReasoningPinsConfigError } from "./provider-validation";
@@ -145,6 +146,7 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
145
146
  if (blockedModelRedirectsError(rawParsed)) warnings.push("blockedModelRedirects ignored: expected a map of nonempty model keys to nonempty string targets");
146
147
  if (nativeReasoningRetentionConfigError(rawParsed)) warnings.push("nativeReasoningRetention ignored: expected only optional boolean modelSwitch and accountSwitch fields; both remain off");
147
148
  if (mainAccountHardLockFallbackError(rawParsed)) warnings.push("codexMainAccountHardLockFallback ignored: expected a nonblank model selector and an optional declared reasoningEffort");
149
+ if (fallbackPolicyError(rawParsed)) warnings.push("fallback partly ignored: expected boolean model/provider switches, providers/routes/devices maps of them, and devices.<id>.localFallback; ignored leaves keep today's fallback behavior");
148
150
  const quotaAutoRefreshWarning = degradedCodexQuotaAutoRefreshWarning(rawParsed, normalized);
149
151
  if (quotaAutoRefreshWarning) warnings.push(quotaAutoRefreshWarning);
150
152
  if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
@@ -729,6 +731,9 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
729
731
  const anthropicError = anthropicOAuthInstanceConfigError(value) ?? anthropicAccountPoolConfigError(value) ?? anthropicSidecarConfigError(value);
730
732
  if (anthropicError) return { ok: false, error: anthropicError };
731
733
  const boundaryError = blockedModelRedirectsError(value)
734
+ // OCX-11: the read schema degrades a malformed fallback leaf to absent, which would silently
735
+ // drop a refusal on the next whole-config save; the write boundary rejects it instead.
736
+ ?? fallbackPolicyError(value)
732
737
  ?? nativeReasoningRetentionConfigError(value)
733
738
  ?? mainAccountHardLockFallbackError(value)
734
739
  ?? compactionRecoveryConfigError(value) ?? desktopCompatibilityConfigError(value) ?? configReasoningPinsConfigError(value)
@@ -7,6 +7,7 @@ import { nativeReasoningRetentionConfigError } from "./schema/native-reasoning-r
7
7
  import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
8
8
  import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
9
9
  import { mainAccountHardLockFallbackError } from "./schema/main-account-hard-lock-fallback";
10
+ import { fallbackPolicyError } from "./schema/fallback-policy";
10
11
  import { anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
11
12
  import {
12
13
  modelPinnedEffortsConfigError,
@@ -142,6 +143,7 @@ export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxCon
142
143
  if (desktopCompatibilityConfigError(rawParsed)) console.warn("Invalid desktopCompatibility startup preference ignored; no desktop compatibility service will start automatically.");
143
144
  if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
144
145
  if (mainAccountHardLockFallbackError(rawParsed)) console.warn("⚠️ invalid codexMainAccountHardLockFallback ignored; the main-account hard lock still refuses while blocked");
146
+ if (fallbackPolicyError(rawParsed)) console.warn("⚠️ invalid fallback leaves ignored; those scopes keep today's fallback behavior");
145
147
  const chatgptDesktop = chatgptDesktopConfigIssue(rawParsed);
146
148
  if (chatgptDesktop) console.warn(`⚠️ config.json ${chatgptDesktop} — the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
147
149
  warnDegradedStreamMode(rawParsed, validated);
@@ -1,4 +1,5 @@
1
1
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
2
+ import { assertWorkBuddyDestination } from "../providers/workbuddy";
2
3
  import { resolveDshAccountMessagesUrl } from "../providers/dsh-account-destination";
3
4
  import { redactSecretString } from "../lib/redact";
4
5
  import {
@@ -421,6 +422,12 @@ export function credentialPinnedProviderConfigError(
421
422
  providerName: string,
422
423
  provider: { adapter?: unknown; baseUrl?: unknown },
423
424
  ): string | null {
425
+ if (providerName === "workbuddy-ai" || provider.adapter === "workbuddy-ai") {
426
+ if (provider.adapter !== undefined && provider.adapter !== "workbuddy-ai") return 'adapter must stay "workbuddy-ai"';
427
+ try { if (typeof provider.baseUrl === "string") assertWorkBuddyDestination(provider.baseUrl); }
428
+ catch { return "WorkBuddy AI credentials require https://www.workbuddy.ai/v2"; }
429
+ return null;
430
+ }
424
431
  if (providerName !== "dsh-account") return null;
425
432
  if (provider.adapter !== undefined && provider.adapter !== "dsh-account") {
426
433
  return 'adapter must stay "dsh-account": its account token is pinned to that adapter';
@@ -8,6 +8,7 @@ import { nativeReasoningRetentionSchema } from "./native-reasoning-retention";
8
8
  import { desktopCompatibilitySchema } from "./desktop-compatibility";
9
9
  import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
10
10
  import { mainAccountHardLockFallbackSchema } from "./main-account-hard-lock-fallback";
11
+ import { fallbackPolicySchema } from "./fallback-policy";
11
12
  import {
12
13
  agentTaskRecoverySchema,
13
14
  catalogAutoRefreshSchema,
@@ -241,6 +242,8 @@ export const configSchema = z.object({
241
242
  // A malformed fallback section disables only the reroute, never the lock's refusal
242
243
  // behavior; the management write boundary still rejects the bad value.
243
244
  codexMainAccountHardLockFallback: mainAccountHardLockFallbackSchema.optional().catch(undefined),
245
+ // OCX-11: a malformed section reads as absent, which keeps today's fallback behavior.
246
+ fallback: fallbackPolicySchema.optional().catch(undefined),
244
247
  // #2999: a malformed hand edit falls back to auto detection rather than rejecting the config.
245
248
  nativeMainRefresh: z.enum(["codex", "opencodex"]).optional().catch(undefined),
246
249
  // Future versions remain opaque through passthrough-compatible whole-config saves.
@@ -0,0 +1,40 @@
1
+ import * as z from "zod/v4";
2
+
3
+ /**
4
+ * Read schema of `fallback` (OCX-11). A malformed leaf degrades to absent, which is today's
5
+ * behavior, and config diagnostics name it; the management write boundary rejects it instead.
6
+ * Map keys are checked by the resolver's normalization, so one bad key never drops its siblings.
7
+ */
8
+ const switchValue = z.boolean().optional().catch(undefined);
9
+ const switchesSchema = z.object({ model: switchValue, provider: switchValue });
10
+ const deviceSchema = switchesSchema.extend({ localFallback: switchValue });
11
+
12
+ export const fallbackPolicySchema = z.object({
13
+ model: switchValue,
14
+ provider: switchValue,
15
+ providers: z.record(z.string(), switchesSchema.catch({})).optional().catch(undefined),
16
+ routes: z.record(z.string(), switchesSchema.catch({})).optional().catch(undefined),
17
+ devices: z.record(z.string(), deviceSchema.catch({})).optional().catch(undefined),
18
+ });
19
+
20
+ export const FALLBACK_DEVICE_ID = /^(?:lnk_[0-9a-f]{16}|[A-Za-z0-9][A-Za-z0-9_.:-]{0,255})$/;
21
+ export const FALLBACK_SELECTOR = /^[^\s]{1,256}$/;
22
+
23
+ const strictSwitches = z.object({ model: z.boolean().optional(), provider: z.boolean().optional() }).strict();
24
+ const strictPolicy = z.object({
25
+ model: z.boolean().optional(),
26
+ provider: z.boolean().optional(),
27
+ providers: z.record(z.string().regex(FALLBACK_SELECTOR), strictSwitches).optional(),
28
+ routes: z.record(z.string().regex(FALLBACK_SELECTOR), strictSwitches).optional(),
29
+ devices: z.record(z.string().regex(FALLBACK_DEVICE_ID), strictSwitches.extend({ localFallback: z.boolean().optional() }).strict()).optional(),
30
+ }).strict();
31
+
32
+ /** Diagnostics text for a section the read schema would partly ignore, or undefined. */
33
+ export function fallbackPolicyError(rawConfig: unknown): string | undefined {
34
+ if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) return undefined;
35
+ const raw = (rawConfig as Record<string, unknown>).fallback;
36
+ if (raw === undefined) return undefined;
37
+ return strictPolicy.safeParse(raw).success
38
+ ? undefined
39
+ : "schema_invalid: fallback accepts boolean model/provider switches, providers/routes/devices maps of them, and devices.<id>.localFallback";
40
+ }
@@ -427,6 +427,7 @@ export const providerConfigSchema = z.object({
427
427
  repairInvalidIds: z.boolean().optional(),
428
428
  }).strict().optional(),
429
429
  responsesSnapshotRepair: z.boolean().optional(),
430
+ messagePhaseInference: z.boolean().optional(),
430
431
  hideRawReasoning: z.boolean().optional(),
431
432
  // Invalid blocks degrade to "absent" rather than failing the whole config load: an unusable
432
433
  // bridge block must never send an operator through invalid-config recovery for an opt-in