@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830

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 (155) hide show
  1. package/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
  2. package/gui/dist/assets/index-DPl4nBMA.js +112 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +2 -1
  5. package/src/AGENTS.md +2 -1
  6. package/src/adapters/agentrouter.ts +50 -0
  7. package/src/adapters/anthropic.ts +1 -51
  8. package/src/adapters/cursor/call-id.ts +76 -8
  9. package/src/adapters/cursor/checkpoint-store.ts +6 -1
  10. package/src/adapters/cursor/cursor-errors.ts +44 -0
  11. package/src/adapters/cursor/native-exec.ts +13 -0
  12. package/src/adapters/cursor/protobuf-request.ts +651 -29
  13. package/src/adapters/cursor/tool-result-normalize.ts +3 -3
  14. package/src/adapters/cursor/transport-retry.ts +5 -1
  15. package/src/adapters/cursor.ts +15 -1
  16. package/src/adapters/empty-tool-output-annotation.ts +43 -0
  17. package/src/adapters/exec-tool-result-normalize.ts +70 -5
  18. package/src/adapters/google.ts +22 -2
  19. package/src/adapters/kiro.ts +26 -2
  20. package/src/adapters/ollama-native-url.ts +111 -0
  21. package/src/adapters/ollama-native.ts +1131 -0
  22. package/src/adapters/openai-chat.ts +30 -7
  23. package/src/adapters/openai-responses.ts +72 -4
  24. package/src/adapters/registry.ts +7 -0
  25. package/src/adapters/xai-web-search.ts +58 -0
  26. package/src/claude/desktop-3p.ts +21 -1
  27. package/src/claude/desktop-policy.ts +149 -0
  28. package/src/cli/account.ts +16 -2
  29. package/src/cli/claude-desktop.ts +13 -3
  30. package/src/cli/combo.ts +8 -5
  31. package/src/cli/doctor.ts +77 -11
  32. package/src/cli/help.ts +1 -1
  33. package/src/cli/index.ts +16 -0
  34. package/src/cli/models.ts +20 -3
  35. package/src/cli/registry.ts +2 -1
  36. package/src/cli/status.ts +140 -2
  37. package/src/cli/storage.ts +10 -1
  38. package/src/codex/account-runtime-state.ts +39 -5
  39. package/src/codex/account-store.ts +393 -13
  40. package/src/codex/account-usability.ts +11 -4
  41. package/src/codex/app-server-processes.ts +46 -5
  42. package/src/codex/auth-context.ts +160 -32
  43. package/src/codex/catalog/bundled.ts +7 -5
  44. package/src/codex/catalog/metadata.ts +1 -1
  45. package/src/codex/catalog/parsing.ts +57 -1
  46. package/src/codex/catalog/provider-fetch.ts +61 -4
  47. package/src/codex/catalog/sync.ts +4 -3
  48. package/src/codex/convergence.ts +3 -2
  49. package/src/codex/data/upstream-models.json +40 -8
  50. package/src/codex/inject-coordination.ts +111 -14
  51. package/src/codex/integration-record.ts +12 -2
  52. package/src/codex/main-account.ts +225 -1
  53. package/src/codex/model-entitlements.ts +339 -27
  54. package/src/codex/prompt-layers.ts +346 -7
  55. package/src/codex/prompt-text-probe.ts +272 -21
  56. package/src/codex/routing.ts +693 -132
  57. package/src/codex/runtime.ts +12 -0
  58. package/src/codex/subagent-model-fallback.ts +62 -24
  59. package/src/codex/user-identity.ts +33 -25
  60. package/src/combos/index.ts +1 -0
  61. package/src/combos/reset-window.ts +46 -0
  62. package/src/combos/resolve.ts +84 -2
  63. package/src/combos/types.ts +5 -2
  64. package/src/config/atomic-write.ts +104 -22
  65. package/src/config/provider-validation.ts +11 -0
  66. package/src/config.ts +75 -3
  67. package/src/generated/compatibility-version.json +207 -131
  68. package/src/generated/model-metadata.ts +1 -1
  69. package/src/grok/catalog.ts +71 -0
  70. package/src/grok/effort.ts +83 -0
  71. package/src/grok/inject.ts +952 -127
  72. package/src/grok/models.ts +56 -0
  73. package/src/grok/status.ts +21 -8
  74. package/src/grok/sync.ts +10 -18
  75. package/src/images/loop.ts +6 -3
  76. package/src/integrations/native/ownership-preflight.ts +4 -1
  77. package/src/lab/fabric/producer-isolate.ts +36 -3
  78. package/src/lib/destination-policy.ts +93 -7
  79. package/src/lib/redact.ts +6 -1
  80. package/src/lib/shadow-call.ts +38 -3
  81. package/src/lib/test-home-guard.ts +18 -3
  82. package/src/lib/upstream-retry.ts +43 -6
  83. package/src/lib/windows-secret-acl.ts +66 -0
  84. package/src/lib/windows-text.ts +28 -2
  85. package/src/lib/windows-user-principal.ts +35 -23
  86. package/src/oauth/account-quota-rank.ts +107 -0
  87. package/src/oauth/anthropic-routing.ts +125 -30
  88. package/src/oauth/chatgpt.ts +5 -1
  89. package/src/oauth/generic-account-failover.ts +114 -7
  90. package/src/oauth/index.ts +15 -8
  91. package/src/oauth/store.ts +16 -0
  92. package/src/providers/account-quota-disk.ts +79 -0
  93. package/src/providers/command-code-efforts.ts +24 -0
  94. package/src/providers/derive.ts +6 -0
  95. package/src/providers/key-failover.ts +33 -1
  96. package/src/providers/kiro-usage.ts +272 -0
  97. package/src/providers/ollama-show.ts +311 -0
  98. package/src/providers/openai-sidecar.ts +5 -0
  99. package/src/providers/quota-routing-cache.ts +32 -0
  100. package/src/providers/quota-types.ts +36 -0
  101. package/src/providers/quota-wire.ts +102 -0
  102. package/src/providers/quota.ts +208 -147
  103. package/src/providers/registry.ts +68 -8
  104. package/src/providers/slug-codec.ts +12 -4
  105. package/src/providers/vercel-gateway-routing.ts +108 -0
  106. package/src/router.ts +22 -12
  107. package/src/server/auth-cors.ts +26 -0
  108. package/src/server/catalog-download.ts +73 -0
  109. package/src/server/chat-native.ts +12 -2
  110. package/src/server/gui-static.ts +4 -1
  111. package/src/server/index.ts +132 -9
  112. package/src/server/management/agent-settings-routes.ts +38 -5
  113. package/src/server/management/codex-prompt-routes.ts +7 -1
  114. package/src/server/management/combo-routes.ts +10 -1
  115. package/src/server/management/config-routes.ts +9 -1
  116. package/src/server/management/context.ts +5 -0
  117. package/src/server/management/model-routes.ts +16 -6
  118. package/src/server/management/native-integration-routes.ts +12 -17
  119. package/src/server/management/oauth-account-routes.ts +13 -0
  120. package/src/server/management/provider-routes.ts +32 -5
  121. package/src/server/management/routing-profile-routes.ts +15 -0
  122. package/src/server/management/shadow-call-validation.ts +29 -0
  123. package/src/server/management-api.ts +7 -3
  124. package/src/server/request-log.ts +3 -5
  125. package/src/server/responses/agent-task-recovery-cache.ts +8 -0
  126. package/src/server/responses/agent-task-recovery.ts +52 -20
  127. package/src/server/responses/codex-auth-error.ts +26 -0
  128. package/src/server/responses/compact.ts +345 -10
  129. package/src/server/responses/core.ts +736 -108
  130. package/src/server/responses/empty-completion-guard.ts +16 -0
  131. package/src/server/responses/fetch-helpers.ts +42 -0
  132. package/src/server/responses/policy-fallback.ts +11 -6
  133. package/src/server/responses-undeclared-tool-guard.ts +16 -3
  134. package/src/server/startup-health-cache.ts +59 -13
  135. package/src/service-manager-probe.ts +115 -9
  136. package/src/service.ts +139 -40
  137. package/src/storage/cleanup.ts +10 -0
  138. package/src/storage/storage-mutation-coordinator.ts +14 -3
  139. package/src/tray/windows-tray.ps1 +10 -4
  140. package/src/tray/windows.ts +30 -2
  141. package/src/types/config.ts +27 -14
  142. package/src/types/provider.ts +54 -0
  143. package/src/types/tools.ts +13 -3
  144. package/src/types.ts +4 -0
  145. package/src/usage/summary.ts +421 -177
  146. package/src/vision/anthropic-describe.ts +3 -3
  147. package/src/vision/describe.ts +5 -3
  148. package/src/web-search/anthropic-executor.ts +9 -2
  149. package/src/web-search/exa-executor.ts +3 -3
  150. package/src/web-search/executor.ts +8 -3
  151. package/src/web-search/gemini-executor.ts +3 -3
  152. package/src/web-search/loop.ts +11 -3
  153. package/src/web-search/xai-executor.ts +3 -3
  154. package/gui/dist/assets/index-DNdRKXK9.js +0 -112
  155. package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
@@ -77,6 +77,7 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS
77
77
  import type { PersistedUsageAttempt } from "../../usage/log";
78
78
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
79
79
  import { providerServiceTierConfigError } from "./provider-capability-config";
80
+ import { providerEmptyToolOutputConfigError } from "../../config/provider-validation";
80
81
  import { applySystemEnvToggle } from "../system-env";
81
82
  import {
82
83
  LOCAL_PROVIDER_RELOAD_NAME_HEADER,
@@ -187,6 +188,17 @@ function applyProviderPatchFields(
187
188
  next.liveModels = rawBody.liveModels;
188
189
  touched = true;
189
190
  }
191
+ if (Object.hasOwn(rawBody, "annotateEmptyToolOutputs")) {
192
+ const value = rawBody.annotateEmptyToolOutputs;
193
+ if (value === null) {
194
+ delete next.annotateEmptyToolOutputs;
195
+ } else if (typeof value === "boolean") {
196
+ next.annotateEmptyToolOutputs = value;
197
+ } else {
198
+ return { error: "annotateEmptyToolOutputs must be a boolean or null" };
199
+ }
200
+ touched = true;
201
+ }
190
202
  if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
191
203
  if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
192
204
  if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
@@ -412,7 +424,8 @@ function canonicalOpenAiBudgetPatchError(
412
424
  }
413
425
  const applied = applyProviderPatchFields("openai", seed, rawBody, keys, config);
414
426
  if ("error" in applied) return applied.error;
415
- return providerManagementConfigError("openai", applied.next);
427
+ return providerManagementConfigError("openai", applied.next)
428
+ ?? providerEmptyToolOutputConfigError("openai", applied.next);
416
429
  }
417
430
 
418
431
  export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
@@ -484,7 +497,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
484
497
  return jsonResponse({ error: "provider reload target unavailable" }, 404);
485
498
  }
486
499
  const provider = diskConfig.providers[name]!;
487
- const providerError = providerManagementConfigError(name, provider);
500
+ const providerError = providerManagementConfigError(name, provider)
501
+ ?? providerEmptyToolOutputConfigError(name, provider);
488
502
  if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
489
503
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(
490
504
  diskConfig.codexAccountNamespaces,
@@ -543,7 +557,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
543
557
  let body: { name?: unknown; provider?: unknown; setDefault?: boolean };
544
558
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
545
559
  const name = typeof body.name === "string" ? body.name.trim() : "";
546
- const providerError = providerManagementConfigError(name, body.provider);
560
+ const providerError = providerManagementConfigError(name, body.provider)
561
+ ?? providerEmptyToolOutputConfigError(name, body.provider);
547
562
  if (providerError) return jsonResponse({ error: providerError }, 400);
548
563
  const serviceTierError = providerServiceTierConfigError(name, body.provider);
549
564
  if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
@@ -585,6 +600,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
585
600
  const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
586
601
  const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
587
602
  const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
603
+ // Same trap, one more field: DeepSeek carries a registry default of `true` for
604
+ // annotateEmptyToolOutputs, so enrichment cannot distinguish "the client omitted it"
605
+ // from "the registry supplied it" either. Without this sample, an unrelated edit that
606
+ // omits the key resurrects the registry default over an operator's explicit `false`.
607
+ const submittedAnnotateEmptyToolOutputs = Object.hasOwn(prov, "annotateEmptyToolOutputs");
588
608
  enrichProviderFromCatalog(name, prov);
589
609
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
590
610
  // Overwriting an existing provider must not drop its multi-key pool: carry it over, then
@@ -613,6 +633,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
613
633
  if (!submittedContextWindow && existing?.contextWindow !== undefined) {
614
634
  prov.contextWindow = existing.contextWindow;
615
635
  }
636
+ // `!== undefined` rather than a truthiness test: the whole point of this field is that
637
+ // an explicit `false` must survive, and `false` is falsy.
638
+ if (!submittedAnnotateEmptyToolOutputs && existing?.annotateEmptyToolOutputs !== undefined) {
639
+ prov.annotateEmptyToolOutputs = existing.annotateEmptyToolOutputs;
640
+ }
616
641
  if (existing?.modelContextWindows) {
617
642
  // When the client did send a map, its keys win and the user's other keys survive. When
618
643
  // it did not, the stored value is the user's map alone: merging the registry seed in
@@ -717,7 +742,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
717
742
  if (applied.editorTouched && !pacingOnly) {
718
743
  const providerError = canonicalBudgetOnly
719
744
  ? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
720
- : providerManagementConfigError(name, next);
745
+ : providerManagementConfigError(name, next)
746
+ ?? providerEmptyToolOutputConfigError(name, next);
721
747
  if (providerError) return jsonResponse({ error: providerError }, 400);
722
748
  if (!canonicalBudgetOnly) {
723
749
  const serviceTierError = providerServiceTierConfigError(name, next);
@@ -750,7 +776,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
750
776
  if (replay.editorTouched && !pacingOnly) {
751
777
  const syncError = canonicalBudgetOnly
752
778
  ? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
753
- : providerManagementConfigError(name, replay.next);
779
+ : providerManagementConfigError(name, replay.next)
780
+ ?? providerEmptyToolOutputConfigError(name, replay.next);
754
781
  if (syncError) {
755
782
  replayError = syncError;
756
783
  return;
@@ -27,6 +27,7 @@ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
27
27
  import { jsonResponse } from "../auth-cors";
28
28
  import type { ManagementContext } from "./context";
29
29
  import type { OcxConfig, OcxRoutingProfileConfig } from "../../types";
30
+ import { shadowCallTargetError } from "./shadow-call-validation";
30
31
 
31
32
  function profileDto(config: Parameters<typeof getRoutingProfile>[0], id: string): Record<string, unknown> | null {
32
33
  const profile = getRoutingProfile(config, id);
@@ -276,10 +277,12 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
276
277
  }
277
278
 
278
279
  const previousProfile = mode === "update" ? getRoutingProfile(config, id) : undefined;
280
+ let aliasMigration: { oldPublicModel: string; newPublicModel: string } | undefined;
279
281
  if (mode === "update" && previousProfile) {
280
282
  const oldPublicModel = policyPublicModelId(id, previousProfile);
281
283
  const newProfile = normalizeRoutingProfile(id, body.profile as OcxRoutingProfileConfig);
282
284
  const newPublicModel = policyPublicModelId(id, newProfile);
285
+ aliasMigration = { oldPublicModel, newPublicModel };
283
286
  const collision = modelMapMigrationCollision(config, oldPublicModel, newPublicModel);
284
287
  if (collision) {
285
288
  return jsonResponse({
@@ -289,6 +292,18 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
289
292
  }
290
293
  const nextProfiles = { ...(config.routingProfiles ?? {}) };
291
294
  nextProfiles[id] = storedProfile(id, body.profile as OcxRoutingProfileConfig);
295
+ const currentShadowTarget = config.shadowCallIntercept?.model;
296
+ if (aliasMigration && currentShadowTarget === aliasMigration.oldPublicModel) {
297
+ const targetError = shadowCallTargetError(
298
+ { ...config, routingProfiles: nextProfiles },
299
+ aliasMigration.newPublicModel,
300
+ );
301
+ if (targetError) {
302
+ return jsonResponse({
303
+ error: { code: "invalid_shadow_call_target", message: targetError },
304
+ }, 400, req, config);
305
+ }
306
+ }
292
307
  config.routingProfiles = nextProfiles;
293
308
  // Creating the first profile on a process started profile-less must install the
294
309
  // compatibility provider now; activation is synchronous and idempotent per configDir.
@@ -0,0 +1,29 @@
1
+ import { shadowCallTargetsIntersect, shadowSourceModels } from "../../lib/shadow-call";
2
+ import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
3
+ import { routeConcreteModel, routeModel } from "../../router";
4
+ import type { OcxConfig } from "../../types";
5
+
6
+ /** Validate a prospective persisted shadow-call target against its resolved source identities. */
7
+ export function shadowCallTargetError(config: OcxConfig, targetModel: string | undefined): string | null {
8
+ if (!targetModel) return null;
9
+
10
+ let target;
11
+ try {
12
+ target = routeModel(config, targetModel);
13
+ } catch {
14
+ return "model must resolve to a configured provider";
15
+ }
16
+
17
+ const intersectsSource = shadowSourceModels(config.shadowCallIntercept?.sourceModels).some(sourceModel => {
18
+ let source = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourceModel };
19
+ try {
20
+ const resolved = routeConcreteModel(config, sourceModel);
21
+ source = { providerName: resolved.providerName, modelId: sourceModel };
22
+ } catch { /* Unconfigured native Codex source models remain OpenAI-owned. */ }
23
+ return shadowCallTargetsIntersect(source, target);
24
+ });
25
+
26
+ return intersectsSource
27
+ ? "shadow-call target must not intersect a source model"
28
+ : null;
29
+ }
@@ -197,7 +197,7 @@ export async function handleManagementAPI(
197
197
  try {
198
198
  const { injectClaudeAgentDefs } = await import("../claude/agents-inject");
199
199
  if (config.claudeCode?.enabled === false || config.claudeCode?.injectAgents === false) {
200
- injectClaudeAgentDefs(config, {});
200
+ injectClaudeAgentDefs(config, {}, deps.claudeAgentConfigDir);
201
201
  return;
202
202
  }
203
203
  try {
@@ -206,11 +206,15 @@ export async function handleManagementAPI(
206
206
  import("../claude/context-windows"),
207
207
  import("../codex/catalog"),
208
208
  ]);
209
- injectClaudeAgentDefs(config, buildClaudeContextWindows([...visibleNativeSlugs(config)], models, nativeContextLimits(config)));
209
+ injectClaudeAgentDefs(
210
+ config,
211
+ buildClaudeContextWindows([...visibleNativeSlugs(config)], models, nativeContextLimits(config)),
212
+ deps.claudeAgentConfigDir,
213
+ );
210
214
  } catch {
211
215
  // Keep routes available through a provider-discovery blip. A later
212
216
  // launch-time sync restores any context markers missing from this pass.
213
- injectClaudeAgentDefs(config, {});
217
+ injectClaudeAgentDefs(config, {}, deps.claudeAgentConfigDir);
214
218
  }
215
219
  } catch { /* best-effort */ }
216
220
  }
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
2
3
  import type { ResponsesTerminalStatus } from "../bridge";
3
4
  import {
4
5
  classifyError,
@@ -201,7 +202,6 @@ const requestLog: RequestLogEntry[] = [];
201
202
  const MAX_LOG_SIZE = 2000;
202
203
  const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
203
204
  let requestLogBytes = 0;
204
- let requestLogSeq = 0;
205
205
  /** True after hydrateRequestLogsFromDisk ran once in this process. */
206
206
  let requestLogsHydratedFromDisk = false;
207
207
 
@@ -430,9 +430,8 @@ export function addRequestLog(entry: RequestLogEntry) {
430
430
  }
431
431
  }
432
432
 
433
- export function nextRequestLogId(timestamp = Date.now()): string {
434
- requestLogSeq = (requestLogSeq % 1_000_000) + 1;
435
- return `ocx-${timestamp.toString(36)}-${requestLogSeq.toString(36)}`;
433
+ export function nextRequestLogId(_timestamp = Date.now()): string {
434
+ return `ocx-${randomBytes(16).toString("hex")}`;
436
435
  }
437
436
 
438
437
  /**
@@ -1308,6 +1307,5 @@ export function getRequestLogEntries(): RequestLogEntry[] { return requestLog; }
1308
1307
  export function clearRequestLogsForTests(): void {
1309
1308
  requestLog.length = 0;
1310
1309
  requestLogBytes = 0;
1311
- requestLogSeq = 0;
1312
1310
  requestLogsHydratedFromDisk = false;
1313
1311
  }
@@ -128,6 +128,10 @@ export async function resolveCachedAgentTaskRecovery(
128
128
  return flight ? waitForRecoveryFlight(flight, abortSignal) : null;
129
129
  }
130
130
 
131
+ export function discardCachedAgentTaskRecovery(key: string): void {
132
+ deleteRecoveryCacheEntry(key);
133
+ }
134
+
131
135
  export function resetAgentTaskRecoveryCache(): void {
132
136
  for (const flight of RECOVERY_FLIGHTS.values()) {
133
137
  flight.controller.abort(new DOMException("Recovery state reset", "AbortError"));
@@ -141,3 +145,7 @@ export function agentTaskRecoveryWaiterCountForTests(): number {
141
145
  for (const flight of RECOVERY_FLIGHTS.values()) count += flight.waiters;
142
146
  return count;
143
147
  }
148
+
149
+ export function agentTaskRecoveryCacheSnapshotForTests(): { entries: number; bytes: number } {
150
+ return { entries: RECOVERY_CACHE.size, bytes: recoveryCacheBytes };
151
+ }
@@ -5,6 +5,7 @@ import { readBoundedResponseBody } from "../../lib/bounded-body";
5
5
  import { isApiAuthRequired, isProxyAdmissionSecret } from "../auth-cors";
6
6
  import { structurallyValidFernetTokens } from "./encrypted-payload";
7
7
  import {
8
+ discardCachedAgentTaskRecovery,
8
9
  resetAgentTaskRecoveryCache,
9
10
  resolveCachedAgentTaskRecovery,
10
11
  } from "./agent-task-recovery-cache";
@@ -266,6 +267,38 @@ function recoveryAdmission(req: Request, config: OcxConfig): RecoveryAdmission |
266
267
  return { headers, cacheScope };
267
268
  }
268
269
 
270
+ interface AdmittedRecovery {
271
+ envelope: AgentEnvelope;
272
+ admission: RecoveryAdmission;
273
+ cacheKey: string;
274
+ }
275
+
276
+ function admittedRecovery(
277
+ req: Request,
278
+ input: unknown,
279
+ config: OcxConfig,
280
+ parentThreadId?: string | null,
281
+ ): AdmittedRecovery | null {
282
+ const envelope = findEnvelope(input);
283
+ if (!envelope) return null;
284
+ const admission = recoveryAdmission(req, config);
285
+ if (!admission) return null;
286
+ const cacheKey = createHash("sha256")
287
+ .update(admission.cacheScope)
288
+ .update("\0")
289
+ .update(parentThreadId ?? "")
290
+ .update("\0")
291
+ .update(envelope.messageType)
292
+ .update("\0")
293
+ .update(envelope.taskName)
294
+ .update("\0")
295
+ .update(envelope.sender)
296
+ .update("\0")
297
+ .update(envelope.ciphertext)
298
+ .digest("hex");
299
+ return { envelope, admission, cacheKey };
300
+ }
301
+
269
302
  function recoveryPayload(envelope: AgentEnvelope, model: string): string {
270
303
  return JSON.stringify({
271
304
  model,
@@ -430,34 +463,33 @@ export async function recoverEncryptedAgentTask(
430
463
  config: OcxConfig,
431
464
  context: { parentThreadId?: string | null; abortSignal?: AbortSignal } = {},
432
465
  ): Promise<boolean> {
433
- const envelope = findEnvelope(input);
434
- if (!envelope) return false;
435
466
  // Admission is deliberately checked before cache access. A cache hit must not
436
467
  // turn this process into a plaintext oracle for an unauthenticated caller.
437
- const admission = recoveryAdmission(req, config);
438
- if (!admission) return false;
439
-
440
- const cacheKey = createHash("sha256")
441
- .update(admission.cacheScope)
442
- .update("\0")
443
- .update(context.parentThreadId ?? "")
444
- .update("\0")
445
- .update(envelope.messageType)
446
- .update("\0")
447
- .update(envelope.taskName)
448
- .update("\0")
449
- .update(envelope.sender)
450
- .update("\0")
451
- .update(envelope.ciphertext)
452
- .digest("hex");
468
+ const admitted = admittedRecovery(req, input, config, context.parentThreadId);
469
+ if (!admitted) return false;
470
+ const { admission, cacheKey, envelope } = admitted;
453
471
  const assignment = await resolveCachedAgentTaskRecovery(
454
472
  cacheKey,
455
473
  options.cacheEntries ?? 200,
456
474
  signal => requestRecovery(admission, envelope, options, signal),
457
475
  context.abortSignal,
458
476
  );
459
- if (!assignment || context.abortSignal?.aborted) return false;
460
- return injectAssignment(input, envelope, assignment);
477
+ if (!assignment) return false;
478
+ if (context.abortSignal?.aborted || !injectAssignment(input, envelope, assignment)) {
479
+ discardCachedAgentTaskRecovery(cacheKey);
480
+ return false;
481
+ }
482
+ return true;
483
+ }
484
+
485
+ export function discardEncryptedAgentTaskRecovery(
486
+ req: Request,
487
+ input: unknown,
488
+ config: OcxConfig,
489
+ context: { parentThreadId?: string | null } = {},
490
+ ): void {
491
+ const admitted = admittedRecovery(req, input, config, context.parentThreadId);
492
+ if (admitted) discardCachedAgentTaskRecovery(admitted.cacheKey);
461
493
  }
462
494
 
463
495
  export function resetAgentTaskRecoveryState(): void {
@@ -10,12 +10,35 @@ import {
10
10
  CodexPoolAuthenticationError,
11
11
  CodexThreadAffinityExpiredError,
12
12
  } from "../../codex/auth-context";
13
+ import {
14
+ MAIN_CODEX_ACCOUNT_ID,
15
+ MainAccountTokenRefreshError,
16
+ MainAuthJsonChangedDuringRefreshError,
17
+ } from "../../codex/main-account";
13
18
 
14
19
  export interface CodexAuthContextErrorResponseOptions {
15
20
  accountSelector?: string;
16
21
  now: number;
17
22
  }
18
23
 
24
+ export function nativeMainRefreshFailureResponse(error: unknown): Response {
25
+ if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth") {
26
+ return formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication");
27
+ }
28
+ if (error instanceof MainAccountTokenRefreshError
29
+ || error instanceof MainAuthJsonChangedDuringRefreshError) {
30
+ const response = formatErrorResponse(
31
+ 503,
32
+ "server_busy",
33
+ "Codex main credential refresh did not complete; retry this request",
34
+ );
35
+ const headers = new Headers(response.headers);
36
+ headers.set("Retry-After", "1");
37
+ return new Response(response.body, { status: response.status, headers });
38
+ }
39
+ return formatErrorResponse(401, "authentication_error", "No usable Codex main credential to serve this request");
40
+ }
41
+
19
42
  /** Shared HTTP contract for Codex auth-context failures on Responses surfaces. */
20
43
  export function mapCodexAuthContextErrorToResponse(
21
44
  error: unknown,
@@ -35,6 +58,9 @@ export function mapCodexAuthContextErrorToResponse(
35
58
  );
36
59
  }
37
60
  if (error instanceof CodexAuthContextError) {
61
+ if (error.accountId === MAIN_CODEX_ACCOUNT_ID) {
62
+ return nativeMainRefreshFailureResponse(error.cause);
63
+ }
38
64
  return formatErrorResponse(
39
65
  401,
40
66
  "authentication_error",