@bitkyc08/opencodex 2.55.0 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -86,34 +86,34 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
86
86
  // server/management-api
87
87
  { method: "POST", path: "/api/stop", module: "server/management-api", mutates: true },
88
88
  // codex/auth-api
89
- { method: "DELETE", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
90
- { method: "GET", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: false },
91
- { method: "GET", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: false },
92
- { method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api", mutates: false },
93
- { method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api", mutates: false },
94
- { method: "GET", path: "/api/codex-auth/quota/history", module: "codex/auth-api", mutates: false },
95
- { method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api", mutates: false },
96
- { method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true },
97
- { method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
98
- { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api", mutates: true },
99
- { method: "POST", path: "/api/codex-auth/accounts/refresh", module: "codex/auth-api", mutates: true },
89
+ { method: "DELETE", path: "/api/codex-auth/accounts", module: "codex/auth-api/routes", mutates: true },
90
+ { method: "GET", path: "/api/codex-auth/accounts", module: "codex/auth-api/routes", mutates: false },
91
+ { method: "GET", path: "/api/codex-auth/active", module: "codex/auth-api/routes", mutates: false },
92
+ { method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api/routes", mutates: false },
93
+ { method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api/routes", mutates: false },
94
+ { method: "GET", path: "/api/codex-auth/quota/history", module: "codex/auth-api/routes", mutates: false },
95
+ { method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api/routes", mutates: false },
96
+ { method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api/routes", mutates: true },
97
+ { method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api/routes", mutates: true },
98
+ { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api/routes", mutates: true },
99
+ { method: "POST", path: "/api/codex-auth/accounts/refresh", module: "codex/auth-api/routes", mutates: true },
100
100
  // codex/main-device-reauth-api (#3898): the native-main device reauth namespace;
101
101
  // /api/codex-auth/login stays pool-only and keeps rejecting __main__.
102
102
  { method: "POST", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: true },
103
103
  { method: "GET", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: false },
104
104
  { method: "DELETE", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: true },
105
- { method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api", mutates: true },
106
- { method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api", mutates: true },
107
- { method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api", mutates: true },
108
- { method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api", mutates: true },
109
- { method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api", mutates: true },
110
- { method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api", mutates: true },
111
- { method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api", mutates: true },
112
- { method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api", mutates: true },
113
- { method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: true },
114
- { method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api", mutates: true },
115
- { method: "PUT", path: "/api/codex-auth/failover", module: "codex/auth-api", mutates: true },
116
- { method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true, exempt: { reason: "compatibility-alias", why: "Superseded by PUT /api/pool/settings, which the CLI now drives. Kept working for existing clients and pinned by exact-body goldens in tests/server/account-pool-management-api.test.ts; no CLI verb targets it any more." } },
105
+ { method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api/routes", mutates: true },
106
+ { method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api/routes", mutates: true },
107
+ { method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api/routes", mutates: true },
108
+ { method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api/routes", mutates: true },
109
+ { method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api/routes", mutates: true },
110
+ { method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api/routes", mutates: true },
111
+ { method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api/routes", mutates: true },
112
+ { method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api/routes", mutates: true },
113
+ { method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api/routes", mutates: true },
114
+ { method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api/routes", mutates: true },
115
+ { method: "PUT", path: "/api/codex-auth/failover", module: "codex/auth-api/routes", mutates: true },
116
+ { method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api/routes", mutates: true, exempt: { reason: "compatibility-alias", why: "Superseded by PUT /api/pool/settings, which the CLI now drives. Kept working for existing clients and pinned by exact-body goldens in tests/server/account-pool-management-api.test.ts; no CLI verb targets it any more." } },
117
117
  // codex/native-profile-api
118
118
  { method: "GET", path: "/api/native-main-profiles", module: "codex/native-profile-api", mutates: false },
119
119
  { method: "GET", path: "/api/native-main-profiles/doctor", module: "codex/native-profile-api", mutates: false },
@@ -308,6 +308,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
308
308
  { method: "PUT", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: true },
309
309
  // server/management/quota-reset-routes
310
310
  { method: "GET", path: "/api/quota-resets", module: "server/management/quota-reset-routes", mutates: false, mechanism: "negated-guard" },
311
+ // server/management/workflow-budget-routes
312
+ { method: "GET", path: "/api/workflow-budget", module: "server/management/workflow-budget-routes", mutates: false, exempt: { reason: "deferred-verb", why: "Reading a root's live budget is owed a CLI verb -- an operator staring at a 429 is usually already in a terminal -- but the ledger is process memory with no local transport to read it through, so the verb has to be an HTTP call the CLI does not yet make.", owner: "260915_workflow_budget_window wfc", ownerDoc: "devlog/_plan/260915_workflow_budget_window/030_wfc_diff_plan.md" } },
313
+ { method: "POST", path: "/api/workflow-budget/clear", module: "server/management/workflow-budget-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Clearing one root is owed the same verb as the read above and for the same reason. It is deliberately not shipped as a verb in this work-phase: the read comes first, because an operator who cannot see which ceiling fired has no basis for deciding to forgive it.", owner: "260915_workflow_budget_window wfc", ownerDoc: "devlog/_plan/260915_workflow_budget_window/030_wfc_diff_plan.md" } },
311
314
  // server/management/request-history-routes
312
315
  { method: "GET", path: "/api/request-history", module: "server/management/request-history-routes", mutates: false },
313
316
  // server/management/routing-analytics-routes
@@ -37,7 +37,7 @@ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap
37
37
  import { resolveCodexHomeDir } from "../../codex/home";
38
38
  import { readUsageEntries } from "../../usage/log";
39
39
  import { getUsageDebugLogEntries } from "../../usage/debug";
40
- import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
40
+ import { cacheObservationFromUsage, parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
41
41
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
42
42
  import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
43
43
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
@@ -97,7 +97,7 @@ export type CostResult =
97
97
  | { kind: "value"; estimate: NonNullable<ReturnType<typeof estimateRequestCost>>; estimateReasons: CostEstimateReason[] }
98
98
  | { kind: "unavailable"; reason: MetricUnavailableReason };
99
99
 
100
- export type MetricSource = Pick<RequestLogEntry, "provider" | "model" | "durationMs" | "firstOutputMs" | "usageStatus" | "usage" | "requestedServiceTier" | "configuredServiceTier" | "responseServiceTier" | "tierOutcome" | "routeDecision"> & {
100
+ export type MetricSource = Pick<RequestLogEntry, "provider" | "model" | "durationMs" | "firstOutputMs" | "usageStatus" | "usage" | "requestedServiceTier" | "configuredServiceTier" | "responseServiceTier" | "tierOutcome" | "routeDecision" | "cacheProvenance"> & {
101
101
  attempts?: readonly PersistedUsageAttempt[];
102
102
  };
103
103
 
@@ -186,9 +186,12 @@ export function costResult(entry: MetricSource): CostResult {
186
186
  if (!estimate) return { kind: "unavailable", reason: unavailableCostReason(entry) };
187
187
  const estimateReasons = [
188
188
  entry.usageStatus === "estimated" || entry.usage?.estimated ? "usage_estimated" as const : undefined,
189
- entry.usage && entry.usage.cachedInputTokens === undefined
190
- && entry.usage.cacheReadInputTokens === undefined
191
- && entry.usage.cacheCreationInputTokens === undefined ? "cache_detail_missing" as const : undefined,
189
+ // A cost estimate is qualified by cache detail it can TRUST. A detail object that exists only
190
+ // because a strict client requires the field carries no cache reading, so it qualifies the
191
+ // estimate exactly as a missing one does — reading it as a measured zero prices the request
192
+ // as an uncached send that nothing observed.
193
+ entry.usage && cacheObservationFromUsage(entry.usage, entry.cacheProvenance).provenance !== "observed"
194
+ ? "cache_detail_missing" as const : undefined,
192
195
  estimate.price?.source === "expected" || estimate.attempts?.some(a => a.price.source === "expected")
193
196
  ? "expected_price_overlay" as const : undefined,
194
197
  estimate.price?.source === "user" || estimate.attempts?.some(a => a.price.source === "user")
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Operator view of the in-memory workflow-budget ledger, plus a targeted clear.
3
+ *
4
+ * Loaded on demand from src/server/management-api.ts, which is the FOURTH entry in the protected
5
+ * set of tests/core-lab-boundary.test.ts — added precisely because eagerly importing handlers
6
+ * there put ~70 modules on every dashboard request. A static import here would make this
7
+ * subsystem the next instance of that bug.
8
+ *
9
+ * Authentication is inherited: every /api route passes through requireManagementAuth before the
10
+ * chain runs, so these handlers add no auth code of their own. The GET spends no user identity.
11
+ * The POST clears one root's windowed count ceilings; it does not spend identity, and the
12
+ * underlying ledger leaves in-flight concurrency and the token spend record untouched.
13
+ */
14
+
15
+ import { jsonResponse } from "../auth-cors";
16
+ import type { OcxConfig } from "../../types";
17
+ import type { ManagementContext } from "./context";
18
+ import { readManagementJsonBodyOr } from "./body";
19
+ import {
20
+ clearWorkflowBudgetForRoot,
21
+ listTrackedWorkflowRoots,
22
+ listWorkflowBudgetEvents,
23
+ workflowBudgetSnapshot,
24
+ WORKFLOW_EVENT_CAPACITY,
25
+ } from "../../lib/workflow-budget";
26
+
27
+ const DEFAULT_LIMIT = 20;
28
+ const MAX_LIMIT = WORKFLOW_EVENT_CAPACITY;
29
+ const MAX_ROOT_ID_LENGTH = 200;
30
+
31
+ /**
32
+ * A root id is an opaque caller-thread token, not a path. Without a length cap a client
33
+ * could POST a multi-megabyte string that we would then store as a map key and echo back
34
+ * in events; 200 is well above any thread id we have seen and small enough to put in a URL.
35
+ */
36
+ function parseRootId(raw: unknown): string | null {
37
+ if (typeof raw !== "string") return null;
38
+ const trimmed = raw.trim();
39
+ if (!trimmed || trimmed.length > MAX_ROOT_ID_LENGTH) return null;
40
+ return trimmed;
41
+ }
42
+
43
+ function invalidRootResponse(req: Request, config: OcxConfig): Response {
44
+ return jsonResponse(
45
+ { error: { code: "invalid_root", message: "root must be a non-empty string of at most 200 characters" } },
46
+ 400,
47
+ req,
48
+ config,
49
+ );
50
+ }
51
+
52
+ function parseLimitParam(rawLimit: string | null): { ok: true; limit: number } | { ok: false } {
53
+ if (rawLimit !== null && !/^\d+$/.test(rawLimit)) return { ok: false };
54
+ return {
55
+ ok: true,
56
+ limit: rawLimit === null ? DEFAULT_LIMIT : Math.min(MAX_LIMIT, Number.parseInt(rawLimit, 10)),
57
+ };
58
+ }
59
+
60
+ export async function handleWorkflowBudgetRoutes(ctx: ManagementContext): Promise<Response | null> {
61
+ const { url, req, config } = ctx;
62
+
63
+ if (url.pathname === "/api/workflow-budget") {
64
+ if (req.method !== "GET") return null;
65
+
66
+ const parsedLimit = parseLimitParam(url.searchParams.get("limit"));
67
+ if (!parsedLimit.ok) {
68
+ return jsonResponse(
69
+ { error: { code: "invalid_limit", message: "limit must be a non-negative integer" } },
70
+ 400,
71
+ req,
72
+ config,
73
+ );
74
+ }
75
+ const { limit } = parsedLimit;
76
+
77
+ const rawRoot = url.searchParams.get("root");
78
+ if (rawRoot !== null) {
79
+ const rootId = parseRootId(rawRoot);
80
+ if (rootId === null) return invalidRootResponse(req, config);
81
+ const snapshot = workflowBudgetSnapshot(rootId);
82
+ // An unknown id is a 200 with `root: null`, not a 404: the operator asked what this
83
+ // process currently holds for that token, and "nothing" is a legitimate answer. POST
84
+ // /clear is the opposite — claiming to forgive a ceiling that was never tracked would
85
+ // report a success that did not happen.
86
+ return jsonResponse(
87
+ {
88
+ root: snapshot ? { rootId, ...snapshot } : null,
89
+ events: listWorkflowBudgetEvents(WORKFLOW_EVENT_CAPACITY)
90
+ .filter((event) => event.rootId === rootId)
91
+ .slice(0, limit),
92
+ },
93
+ 200,
94
+ req,
95
+ config,
96
+ );
97
+ }
98
+
99
+ return jsonResponse(
100
+ {
101
+ roots: listTrackedWorkflowRoots(limit),
102
+ events: listWorkflowBudgetEvents(limit),
103
+ },
104
+ 200,
105
+ req,
106
+ config,
107
+ );
108
+ }
109
+
110
+ if (url.pathname === "/api/workflow-budget/clear") {
111
+ if (req.method !== "POST") return null;
112
+
113
+ const body = await readManagementJsonBodyOr(req, {});
114
+ const rawRoot = body && typeof body === "object" && !Array.isArray(body)
115
+ ? (body as { root?: unknown }).root
116
+ : undefined;
117
+ const rootId = parseRootId(rawRoot);
118
+ if (rootId === null) return invalidRootResponse(req, config);
119
+
120
+ const before = clearWorkflowBudgetForRoot(rootId);
121
+ if (!before) {
122
+ return jsonResponse(
123
+ { error: { code: "unknown_root", message: "root is not currently tracked" } },
124
+ 404,
125
+ req,
126
+ config,
127
+ );
128
+ }
129
+ return jsonResponse({ cleared: true, root: rootId, before }, 200, req, config);
130
+ }
131
+
132
+ return null;
133
+ }
@@ -144,6 +144,17 @@ async function handleQuotaResetRoutesOnDemand(ctx: ManagementContext): Promise<R
144
144
  return handleQuotaResetRoutes(ctx);
145
145
  }
146
146
 
147
+ /**
148
+ * Lazy like the Lab and routing-profile handlers, and for the same recorded reason: this file is
149
+ * mounted for every dashboard request, so a static import would put the workflow-budget ledger
150
+ * on all of them.
151
+ */
152
+ async function handleWorkflowBudgetRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
153
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/workflow-budget", true)) return null;
154
+ const { handleWorkflowBudgetRoutes } = await import("./management/workflow-budget-routes");
155
+ return handleWorkflowBudgetRoutes(ctx);
156
+ }
157
+
147
158
  async function handleGrokCouponRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
148
159
  if (!pathInManagementNamespace(ctx.url.pathname, "/api/grok/reset-coupons", true)) return null;
149
160
  const { handleGrokCouponRoutes } = await import("./management/grok-coupon-routes");
@@ -263,6 +274,7 @@ export async function handleManagementAPI(
263
274
  ?? (await handleLogsUsageRoutes(ctx))
264
275
  ?? (await handleRequestHistoryRoutes(ctx))
265
276
  ?? (await handleQuotaResetRoutesOnDemand(ctx))
277
+ ?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
266
278
  ?? (await handleGrokCouponRoutesOnDemand(ctx))
267
279
  ?? (await handleRoutingAnalyticsRoutes(ctx))
268
280
  ?? (await handleRoutingProfileRoutesOnDemand(ctx))
@@ -64,13 +64,16 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
64
64
  /**
65
65
  * Fixed-size logical turn lane (#820).
66
66
  *
67
- * A lane must be as SPECIFIC as the identity available, which is the opposite of what
68
- * `codexPoolAffinityKey` wants. Affinity deliberately prefers the parent thread so a whole
69
- * subagent fan-out pins to one account; a lane keyed that way would put every parallel
70
- * subagent of one parent into a single lane and reject all but the first with 503 — the
71
- * fan-out is the normal case, not an abuse.
67
+ * A lane must be as SPECIFIC as the identity available. `codexPoolAffinityKey` used to be the
68
+ * opposite: it preferred the parent thread, so a whole subagent fan-out shared one entry, and a
69
+ * lane keyed that way would have put every parallel subagent of one parent into a single lane
70
+ * and rejected all but the first with 503 — the fan-out is the normal case, not an abuse.
71
+ * Since #4546 affinity keys every thread as ITSELF and reads the parent only as a first-placement
72
+ * hint, so the two now agree on the unit. They still derive it differently: a lane is a digest an
73
+ * operator can match against what the client sent, while an affinity key is an opaque HMAC
74
+ * precisely so no caller-supplied identifier ends up in Pool state.
72
75
  *
73
- * So the parent is a QUALIFIER, never the lane on its own when a child thread exists: the
76
+ * The parent stays a QUALIFIER here, never the lane on its own when a child thread exists: the
74
77
  * pair separates siblings while still keeping one conversation's overlapping turns together.
75
78
  */
76
79
  export function sessionLaneIdFromRequest(headers: Headers): string | undefined {
@@ -256,4 +259,3 @@ export function getOrAllocateRequestSessionLane(req: Request): string {
256
259
  export function linkRequestSessionLane(sourceReq: Request, targetReq: Request): void {
257
260
  requestAllocatedSessionLanes.set(targetReq, getOrAllocateRequestSessionLane(sourceReq));
258
261
  }
259
-