@bitkyc08/opencodex 2.91.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 (148) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
  3. package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
  4. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  5. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  8. package/package.json +4 -3
  9. package/src/adapters/google-vertex-endpoint.ts +9 -1
  10. package/src/adapters/google-vertex-key-project.ts +223 -0
  11. package/src/adapters/google.ts +13 -3
  12. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  13. package/src/adapters/openai-chat.ts +2 -2
  14. package/src/adapters/openai-responses/request-strips.ts +60 -0
  15. package/src/adapters/registry.ts +5 -0
  16. package/src/adapters/workbuddy-schema.ts +61 -0
  17. package/src/adapters/workbuddy.ts +68 -0
  18. package/src/bridge/response-json.ts +1 -1
  19. package/src/bridge/sse.ts +1 -1
  20. package/src/chat/outbound.ts +11 -4
  21. package/src/cli/capabilities-agents-routing.ts +3 -3
  22. package/src/cli/codex-cli-update.ts +8 -0
  23. package/src/cli/combo-result.ts +6 -0
  24. package/src/cli/combo-stats.ts +3 -0
  25. package/src/cli/combo.ts +66 -42
  26. package/src/cli/launcher-context.ts +49 -2
  27. package/src/cli/link.ts +45 -32
  28. package/src/cli/provider.ts +2 -1
  29. package/src/cli/usage-compare.ts +68 -12
  30. package/src/client/link-ingress.ts +1 -1
  31. package/src/client/link-readiness.ts +97 -0
  32. package/src/client/link-relay-transport.ts +79 -11
  33. package/src/client/link-relay.ts +74 -15
  34. package/src/client/link-tunnel.ts +45 -18
  35. package/src/client/machine-listener.ts +20 -2
  36. package/src/client/runtime.ts +20 -2
  37. package/src/codex/account-store.ts +17 -3
  38. package/src/codex/app-server-restart-service.ts +6 -5
  39. package/src/codex/catalog/effort.ts +9 -2
  40. package/src/codex/catalog/gather-capture.ts +4 -2
  41. package/src/codex/catalog/provider-models.ts +14 -8
  42. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  43. package/src/codex/cli-install-provenance.ts +418 -0
  44. package/src/codex/cli-installation-targets.ts +5 -1
  45. package/src/codex/cli-update-plan.ts +43 -16
  46. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  47. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  48. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  49. package/src/codex/pool-refresh-backoff.ts +3 -1
  50. package/src/codex/user-identity.ts +35 -6
  51. package/src/codex/windows-current-sid.ts +162 -0
  52. package/src/combos/jev-quota-activation.ts +5 -1
  53. package/src/combos/jev-quota-route.ts +1 -1
  54. package/src/combos/jev-quota.ts +9 -1
  55. package/src/combos/jev-session-pin-config.ts +51 -0
  56. package/src/combos/jev.ts +6 -0
  57. package/src/combos/types.ts +19 -0
  58. package/src/config/provider-validation.ts +7 -0
  59. package/src/config/schema/leaf-validators.ts +1 -0
  60. package/src/generated/compatibility-version.json +194 -122
  61. package/src/lib/decision-quota-sink.ts +10 -0
  62. package/src/lib/translator-budget.ts +4 -4
  63. package/src/lib/upstream-retry.ts +4 -1
  64. package/src/link/diagnostics.ts +193 -0
  65. package/src/link/relay-auth.ts +6 -0
  66. package/src/link/status-projection.ts +39 -0
  67. package/src/link/supervisor.ts +45 -12
  68. package/src/link/tunnel-history.ts +109 -0
  69. package/src/link/tunnel-state.ts +80 -17
  70. package/src/messaging/remote-files.ts +24 -1
  71. package/src/oauth/generic-account-failover.ts +91 -7
  72. package/src/oauth/index.ts +15 -0
  73. package/src/oauth/model-discovery-auth.ts +5 -0
  74. package/src/oauth/store.ts +4 -1
  75. package/src/oauth/token-guardian.ts +7 -0
  76. package/src/oauth/types.ts +9 -0
  77. package/src/oauth/workbuddy.ts +139 -0
  78. package/src/providers/antigravity-quota-identity.ts +37 -0
  79. package/src/providers/base-url-choices.ts +8 -5
  80. package/src/providers/derive.ts +23 -3
  81. package/src/providers/model-discovery.ts +2 -0
  82. package/src/providers/model-rename-fields.ts +1 -0
  83. package/src/providers/quota/account-cache.ts +3 -2
  84. package/src/providers/quota/antigravity.ts +25 -14
  85. package/src/providers/quota/report-cache.ts +13 -4
  86. package/src/providers/quota/vendor-probes-key.ts +60 -0
  87. package/src/providers/quota/workbuddy.ts +83 -0
  88. package/src/providers/quota-decision-antigravity.ts +64 -0
  89. package/src/providers/quota-decision-publication.ts +11 -0
  90. package/src/providers/quota-decision-snapshot.ts +21 -4
  91. package/src/providers/quota-types.ts +2 -0
  92. package/src/providers/quota.ts +3 -1
  93. package/src/providers/registry/entries-core.ts +9 -0
  94. package/src/providers/registry/entries-extended.ts +20 -7
  95. package/src/providers/registry/model-ids.ts +3 -0
  96. package/src/providers/registry/model-seeds.ts +27 -28
  97. package/src/providers/registry/types.ts +19 -1
  98. package/src/providers/resolved-model-policy.ts +4 -4
  99. package/src/providers/routed-config.ts +15 -0
  100. package/src/providers/stale-model-roster-migration.ts +83 -15
  101. package/src/providers/workbuddy-credit-balance.ts +81 -0
  102. package/src/providers/workbuddy-models.ts +37 -0
  103. package/src/providers/workbuddy.ts +60 -0
  104. package/src/responses/reasoning-replay-cache.ts +18 -14
  105. package/src/router.ts +2 -0
  106. package/src/routing/analytics.ts +2 -3
  107. package/src/routing/fallback-policy.ts +3 -3
  108. package/src/server/auth-cors.ts +1 -0
  109. package/src/server/chat-completions.ts +14 -1
  110. package/src/server/chat-native-reasoning.ts +10 -3
  111. package/src/server/chat-native.ts +13 -2
  112. package/src/server/claude-messages.ts +3 -0
  113. package/src/server/index/link-relay-sessions.ts +9 -5
  114. package/src/server/index/optional-listeners.ts +4 -0
  115. package/src/server/local-management-read-client.ts +3 -1
  116. package/src/server/management/combo-routes.ts +10 -4
  117. package/src/server/management/link-routes.ts +5 -2
  118. package/src/server/management/provider-routes.ts +1 -1
  119. package/src/server/port-reclaim.ts +51 -9
  120. package/src/server/request-log-conversation.ts +49 -0
  121. package/src/server/request-log.ts +2 -0
  122. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  123. package/src/server/responses/compact.ts +10 -2
  124. package/src/server/responses/core-combo.ts +53 -31
  125. package/src/server/responses/core-lifetime.ts +2 -2
  126. package/src/server/responses/core-normalize.ts +6 -3
  127. package/src/server/responses/core-opaque-recovery.ts +1 -0
  128. package/src/server/responses/core-options.ts +5 -0
  129. package/src/server/responses/core-replay.ts +2 -1
  130. package/src/server/responses/jev-combo-choice.ts +39 -0
  131. package/src/server/responses/jev-session-pin.ts +203 -0
  132. package/src/server/responses/passthrough-delivery.ts +4 -0
  133. package/src/server/responses/request-prepare.ts +10 -5
  134. package/src/server/responses/request-transport.ts +25 -6
  135. package/src/server/responses/run-turn-execution.ts +2 -4
  136. package/src/server/responses/sidecar-execution.ts +2 -4
  137. package/src/server/responses-phase-inference.ts +267 -0
  138. package/src/server/sse-payload-rewrite.ts +14 -1
  139. package/src/service/definition.ts +20 -8
  140. package/src/types/config.ts +6 -0
  141. package/src/types/provider.ts +9 -1
  142. package/src/types/request.ts +1 -0
  143. package/src/types/wire.ts +1 -0
  144. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  145. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  146. package/src/usage/jev-stats.ts +54 -14
  147. package/gui/dist/assets/index-DGXS51sp.js +0 -94
  148. package/gui/dist/assets/quota-summary-DO1Sx5TM.js +0 -1
@@ -1,7 +1,7 @@
1
1
  import type { Server } from "bun";
2
2
  import {
3
3
  LINK_RELAY_AUTH_PATH, LINK_RELAY_AUTH_TIMEOUT_MS, LINK_RELAY_AUTH_VERSION,
4
- LINK_RELAY_SESSION_HEADER, linkRelayProof, linkRelayProofMatches,
4
+ LINK_RELAY_PROBE_HEADER, LINK_RELAY_SESSION_HEADER, linkRelayProof, linkRelayProofMatches,
5
5
  } from "../../link/relay-auth";
6
6
  import { parseLinkPeerReport, type LinkPeerObservations } from "../../link/peer-report";
7
7
 
@@ -81,13 +81,17 @@ export function createLinkRelaySessions(options: LinkRelaySessionOptions) {
81
81
  linkRelayProofMatches(q.get("proof"), linkRelayProof(candidate, "caller", keyId, linkId, nonce)));
82
82
  if (!fingerprint) return refuse();
83
83
  options.peers?.observe(linkId, LINK_RELAY_AUTH_VERSION, parseLinkPeerReport(req.headers));
84
- const release = acquire();
85
- const timer = setTimeout(() => { consume(nonce)?.release(); }, reservationMs);
86
- sessions.set(nonce, { peer, keyId, linkId, fingerprint, release, timer, expiresAt: Date.now() + reservationMs });
87
- return new Response(null, { status: 204, headers: {
84
+ const answer = () => new Response(null, { status: 204, headers: {
88
85
  "x-opencodex-link-proof": linkRelayProof(fingerprint, "listener", keyId, linkId, nonce)!,
89
86
  "cache-control": "no-store",
90
87
  } });
88
+ // A readiness probe closes after this answer and sends no data request, so it reserves
89
+ // nothing: no session to consume, no capacity and no drain lease held until expiry.
90
+ if (req.headers.get(LINK_RELAY_PROBE_HEADER) === "1") return answer();
91
+ const release = acquire();
92
+ const timer = setTimeout(() => { consume(nonce)?.release(); }, reservationMs);
93
+ sessions.set(nonce, { peer, keyId, linkId, fingerprint, release, timer, expiresAt: Date.now() + reservationMs });
94
+ return answer();
91
95
  }
92
96
  const nonce = req.headers.get(LINK_RELAY_SESSION_HEADER);
93
97
  let release: () => void;
@@ -14,6 +14,7 @@ import {
14
14
  type LinkListenerStatus,
15
15
  } from "./link-listener";
16
16
  import { createLinkSupervisor, type LinkSupervisor } from "../../link/supervisor";
17
+ import { flushLinkDiagnostics, recordLinkDiagnostic } from "../../link/diagnostics";
17
18
  import { linkManagementRouteAllowed } from "../../link/management-routes";
18
19
  import { linkChildParityRouteAllowed } from "../../link/child-parity";
19
20
  import { saveConfigPreservingClaudeCode } from "../../config/live-reconcile";
@@ -56,6 +57,9 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
56
57
  let activeConfig: OcxConfig | undefined;
57
58
  let stopJevQuotaActivation: (() => void) | undefined;
58
59
  const supervisor = createLinkSupervisor({
60
+ // Tunnel state changes and their causes reach <configDir>/link/diagnostics.jsonl (bounded).
61
+ diagnostic: recordLinkDiagnostic,
62
+ flushDiagnostics: flushLinkDiagnostics,
59
63
  apiKeys: () => activeConfig?.apiKeys ?? [],
60
64
  revokeApiKey: id => {
61
65
  if (!activeConfig) return false;
@@ -51,7 +51,7 @@ export interface LocalManagementReadRequestDeps extends LocalManagementReadDeps
51
51
  */
52
52
  export async function fetchBoundLocalManagementRead(
53
53
  target: LiveProxy,
54
- path: LocalManagementReadPath,
54
+ path: LocalManagementReadPath | `${LocalManagementReadPath}?${string}`,
55
55
  deps: LocalManagementReadRequestDeps = {},
56
56
  ): Promise<LocalManagementReadResult> {
57
57
  if (
@@ -89,6 +89,8 @@ export async function fetchBoundLocalManagementRead(
89
89
  const timeoutMs = deps.timeoutMs ?? 4_000;
90
90
  const url = `http://${probeHostname(target.hostname)}:${target.port}${path}`;
91
91
  const init: RequestInit = {
92
+ redirect: "error",
93
+ credentials: "omit",
92
94
  headers: {
93
95
  [LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
94
96
  [LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
@@ -208,7 +208,6 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
208
208
  const {
209
209
  clearComboSelectionState,
210
210
  clearComboTargetCooldowns,
211
- comboConfigError,
212
211
  comboDisabledModelId,
213
212
  comboDisabledModelSelectors,
214
213
  comboModelId,
@@ -225,6 +224,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
225
224
  ...requestedCombo,
226
225
  ...(!Object.hasOwn(requestedCombo, "decisionQuotaSignals") && requestedCombo.strategy === "jev" && previous?.decisionQuotaSignals !== undefined ? { decisionQuotaSignals: previous.decisionQuotaSignals } : {}),
227
226
  ...(!Object.hasOwn(requestedCombo, "decisionQuotaTiers") && requestedCombo.strategy === "jev" && previous?.decisionQuotaTiers !== undefined ? { decisionQuotaTiers: previous.decisionQuotaTiers } : {}),
227
+ // The dashboard does not edit the session pin (#7525): an omitted field keeps it on a JEV save.
228
+ ...(!Object.hasOwn(requestedCombo, "sessionPin") && requestedCombo.strategy === "jev" && previous?.sessionPin != null ? { sessionPin: previous.sessionPin } : {}),
228
229
  ...(!Object.hasOwn(requestedCombo, "allowMeteredTargets") && previous?.allowMeteredTargets === true
229
230
  ? { allowMeteredTargets: true }
230
231
  : {}),
@@ -257,7 +258,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
257
258
  ...(Array.isArray(requestedCombo.targets) && Array.isArray(previous?.targets)
258
259
  ? {
259
260
  targets: requestedCombo.targets.map(target => {
260
- // A non-record entry stays as the client sent it so comboConfigError reports it.
261
+ // A non-record entry stays as the client sent it so comboConfigIssues reports it.
261
262
  // Reading `lastResort` off it here would throw in place of that structured 400.
262
263
  if (!isPlainRecord(target)) return target;
263
264
  if (["lastResort", "metered"].every(key => Object.hasOwn(target, key))) return target;
@@ -331,8 +332,13 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
331
332
  excludeComboId: sourceId,
332
333
  normalizeDecisionModel: (model: string) => normalizeDecisionModelSelector(prospectiveConfig, model),
333
334
  };
334
- const error = comboConfigError(id, effectiveCombo, config.providers, validationOptions);
335
- if (error) return jsonResponse({ error }, 400);
335
+ // `field` names the top-level combo field of the first issue, so a client can tell a target or
336
+ // JEV-setting refusal from an alias or rename one without parsing the message (#7579).
337
+ const invalid = comboConfigIssues(id, effectiveCombo, config.providers, validationOptions)[0];
338
+ if (invalid) {
339
+ const field = invalid.path[0];
340
+ return jsonResponse({ error: invalid.message, ...(typeof field === "string" ? { field } : {}) }, 400);
341
+ }
336
342
  for (const [otherId, combo] of Object.entries(nextCombos)) {
337
343
  if (typeof combo.decisionModel !== "string") continue;
338
344
  const issue = otherId === id ? undefined : comboConfigIssues(otherId, combo, config.providers, {
@@ -6,7 +6,7 @@ import { clearCompensationFailed, compensationPath, readCompensation } from "../
6
6
  import { loadHostCandidates } from "../../link/ssh-config";
7
7
  import { newLinkId, type LinkStore } from "../../link/store";
8
8
  import { sshFailureHint, sshRunnerErrorHint } from "../../link/ssh-runner";
9
- import { attachLinkPeers, projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
9
+ import { attachLinkPeers, attachLinkTunnelDiagnostics, projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
10
10
  import { homeLinkChildParity } from "../../link/child-parity";
11
11
  import { localLinkPeer } from "../../link/compatibility";
12
12
  import { packageVersion } from "../../lib/package-version";
@@ -268,8 +268,9 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
268
268
  if (denied) return denied;
269
269
  const store = readStoreFor(ctx);
270
270
  const listenerStatus = state.listener.status();
271
+ const supervisorStates = state.supervisor.status();
271
272
  const dto: LinkStatusDto = projectLinkStatus(
272
- store, state.supervisor.status(), listenerStatus, ctx.config, readCompensation(),
273
+ store, supervisorStates, listenerStatus, ctx.config, readCompensation(),
273
274
  ctx.config.runtimeRole === "client" ? clientLinkTunnelStatus() : null,
274
275
  ctx.config.runtimeRole === "client" && ctx.config.client?.link ? clientLinkStatusIdentity(ctx.config.client.link.linkId) : null,
275
276
  linkId => homeLinkChildParity.get(linkId),
@@ -288,6 +289,8 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
288
289
  attachLinkPeers(dto, localLinkPeer(packageVersion("0.0.0"), buildPeerFeatures()),
289
290
  linkId => state.listener.peerObservation?.(linkId) ?? { observed: null, refused: null });
290
291
  }
292
+ // The same opt-in for each Home-initiated tunnel's retry progress and last failure cause.
293
+ if (url.searchParams.get("diagnostics") === "1") attachLinkTunnelDiagnostics(dto, supervisorStates);
291
294
  // A dashboard session also learns why it cannot join as a Child. The admin-token answer stays
292
295
  // the exact K16 document that `ocx link status` validates key by key.
293
296
  const denial = ctx.principal === "gui-session" ? joinDenied(ctx) : null;
@@ -1726,7 +1726,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1726
1726
  return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
1727
1727
  }
1728
1728
  const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name, {
1729
- oauthApiBaseUrl: snapshot?.apiBaseUrl,
1729
+ oauthApiBaseUrl: snapshot?.apiBaseUrl, workbuddy: snapshot?.workbuddy,
1730
1730
  });
1731
1731
  const discovery = resolveProviderModelDiscovery(name, prov);
1732
1732
  const started = Date.now();
@@ -405,22 +405,37 @@ async function runOwnerLookup(file: string, args: string[], timeoutMs: number):
405
405
  return stdout;
406
406
  }
407
407
 
408
+ export type LoopbackOwnerLookupResult = {
409
+ kind: "match" | "mismatch" | "lookup_error" | "deadline";
410
+ elapsedMs: number;
411
+ };
412
+
408
413
  export interface LoopbackOwnerLookupIo {
409
414
  platform?: NodeJS.Platform;
415
+ nowMs?: () => number;
416
+ scheduleDeadline?: (callback: () => void, timeoutMs: number) => () => void;
410
417
  readProc?: (path: string) => Promise<string>;
411
418
  listFds?: (path: string) => Promise<string[]>;
412
419
  readFdLink?: (path: string) => Promise<string>;
413
420
  run?: (file: string, args: string[], timeoutMs: number) => Promise<string>;
414
421
  }
415
422
 
416
- /** Async, bounded proof that this PID owns the IPv4 loopback LISTEN socket used by the relay. */
417
- export async function ownsIpv4LoopbackListener(
423
+ /**
424
+ * Classify fresh IPv4 loopback LISTEN ownership within the bounded lookup deadline.
425
+ * Only match proves the expected PID; mismatch, inspection failure and timeout remain distinct.
426
+ * elapsedMs uses a monotonic clock by default, and no raw OS output is returned.
427
+ */
428
+ export async function inspectIpv4LoopbackListener(
418
429
  port: number,
419
430
  expectedPid: number,
420
431
  io: LoopbackOwnerLookupIo = {},
421
- ): Promise<boolean> {
432
+ ): Promise<LoopbackOwnerLookupResult> {
433
+ const now = io.nowMs ?? (() => performance.now());
434
+ const startedAt = now();
435
+ const result = (kind: LoopbackOwnerLookupResult["kind"]): LoopbackOwnerLookupResult =>
436
+ ({ kind, elapsedMs: Math.max(0, now() - startedAt) });
422
437
  if (!Number.isInteger(port) || port < 1 || port > 65535
423
- || !Number.isSafeInteger(expectedPid) || expectedPid < 1) return false;
438
+ || !Number.isSafeInteger(expectedPid) || expectedPid < 1) return result("mismatch");
424
439
  const platform = io.platform ?? process.platform;
425
440
  const lookup = async (): Promise<boolean> => {
426
441
  if (platform === "linux") {
@@ -458,13 +473,40 @@ export async function ownsIpv4LoopbackListener(
458
473
  }
459
474
  return false;
460
475
  };
461
- let timer: ReturnType<typeof setTimeout> | undefined;
462
- const deadline = new Promise<false>(resolve => {
463
- timer = setTimeout(() => resolve(false), OWNER_LOOKUP_TIMEOUT_MS);
476
+ const scheduleDeadline = io.scheduleDeadline ?? ((callback, timeoutMs) => {
477
+ const timer = setTimeout(callback, timeoutMs);
464
478
  timer.unref?.();
479
+ return () => clearTimeout(timer);
465
480
  });
466
- try { return await Promise.race([lookup().catch(() => false), deadline]); }
467
- finally { if (timer) clearTimeout(timer); }
481
+ let cancelDeadline: (() => void) | undefined;
482
+ const deadline = new Promise<LoopbackOwnerLookupResult["kind"]>(resolve => {
483
+ cancelDeadline = scheduleDeadline(() => resolve("deadline"), OWNER_LOOKUP_TIMEOUT_MS);
484
+ });
485
+ const inspected = lookup().then(
486
+ matches => matches ? "match" as const : "mismatch" as const,
487
+ error => {
488
+ const failure = error as {
489
+ killed?: boolean; signal?: string | null; code?: unknown; stdout?: unknown; stderr?: unknown;
490
+ } | null;
491
+ // lsof exits 1 with no output when no socket matches its selectors.
492
+ if (platform === "darwin" && failure?.code === 1 && !failure.killed
493
+ && failure.signal == null && failure.stdout === "" && failure.stderr === "") return "mismatch" as const;
494
+ // execFile's timeout kills with SIGTERM; other spawn/exit/buffer errors are lookup failures.
495
+ return failure?.killed === true && failure.signal === "SIGTERM" && failure.code === null
496
+ ? "deadline" as const : "lookup_error" as const;
497
+ },
498
+ );
499
+ try { return result(await Promise.race([inspected, deadline])); }
500
+ finally { cancelDeadline?.(); }
501
+ }
502
+
503
+ /** Boolean compatibility API: only a proven match admits the listener. */
504
+ export async function ownsIpv4LoopbackListener(
505
+ port: number,
506
+ expectedPid: number,
507
+ io: LoopbackOwnerLookupIo = {},
508
+ ): Promise<boolean> {
509
+ return (await inspectIpv4LoopbackListener(port, expectedPid, io)).kind === "match";
468
510
  }
469
511
 
470
512
  /**
@@ -172,6 +172,55 @@ export function conversationIdFromResponsesRequest(input: {
172
172
  );
173
173
  }
174
174
 
175
+ /**
176
+ * Hermes Agent session affinity header for log correlation (#330).
177
+ * Kept separate from runtime/session identities so it cannot affect lanes or routing.
178
+ */
179
+ export function conversationIdFromHermesHeader(headers: Headers): string | undefined {
180
+ return normalizeLogConversationId(headers.get("x-hermes-conversation-id"));
181
+ }
182
+
183
+ /**
184
+ * Validates and normalizes native header candidates in priority order for logging:
185
+ * parent-thread -> session_id -> session-id -> thread-id.
186
+ * Each candidate spelling is evaluated and normalized independently so an empty or
187
+ * whitespace-only candidate (e.g. empty session_id) cannot suppress a subsequent valid candidate.
188
+ */
189
+ export function nativeLogConversationIdFromHeaders(headers: Headers): string | undefined {
190
+ return (
191
+ normalizeLogConversationId(headers.get("x-codex-parent-thread-id")) ??
192
+ normalizeLogConversationId(headers.get("session_id")) ??
193
+ normalizeLogConversationId(headers.get("session-id")) ??
194
+ normalizeLogConversationId(headers.get("thread-id"))
195
+ );
196
+ }
197
+
198
+ /** The request-log context's conversation slot; structural so callers need not import request-log. */
199
+ export interface LogConversationSlot {
200
+ conversationId?: string;
201
+ conversationIdIsHermesFallback?: boolean;
202
+ }
203
+
204
+ /**
205
+ * Record a native log identity. Fills an empty slot or replaces the log-only Hermes fallback,
206
+ * so a native id found late (Responses preparation, a Cursor conversation created mid-turn)
207
+ * still wins. A caller-owned id already in the slot (routed Claude metadata) is kept.
208
+ */
209
+ export function assignNativeLogConversationId(slot: LogConversationSlot, nativeId: string | undefined): void {
210
+ if (!nativeId || (slot.conversationId && !slot.conversationIdIsHermesFallback)) return;
211
+ slot.conversationId = nativeId;
212
+ delete slot.conversationIdIsHermesFallback;
213
+ }
214
+
215
+ /** Fill an empty slot with the log-only Hermes digest, marked so any native id replaces it. */
216
+ export function fillHermesLogConversationFallback(slot: LogConversationSlot, headers: Headers): void {
217
+ if (slot.conversationId) return;
218
+ const hermes = conversationIdFromHermesHeader(headers);
219
+ if (!hermes) return;
220
+ slot.conversationId = hermes;
221
+ slot.conversationIdIsHermesFallback = true;
222
+ }
223
+
175
224
  /**
176
225
  * Claude Code metadata.user_id only — never the system-hash Desktop fallback.
177
226
  * Hashes the raw user_id once (same opacity goal as inbound prompt_cache_key).
@@ -150,6 +150,8 @@ export interface RequestLogContext extends UpstreamErrorDiagnostics {
150
150
  lastOutputAt?: number;
151
151
  /** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
152
152
  conversationId?: string;
153
+ /** In-memory only: conversationId holds the log-only Hermes digest, which a native id replaces. */
154
+ conversationIdIsHermesFallback?: boolean;
153
155
  surface?: "claude" | "claude-desktop" | "grok";
154
156
  /** The matched configured key's id. Set ONLY for admissionKind "configured" —
155
157
  * never a sentinel, so a hand-edited entry whose id happens to be "loopback"
@@ -130,6 +130,7 @@ export function createAnthropicThinkingRecovery(
130
130
  || response.status !== "completed" || !Array.isArray(response.output) || response.output.length === 0
131
131
  || !rejectedScope
132
132
  || rejectedScope.clientThreadId !== parsed._reasoningReplayScope?.clientThreadId
133
+ || rejectedScope.clientPrincipalId !== parsed._reasoningReplayScope?.clientPrincipalId
133
134
  || JSON.stringify(rejectedScope.current) !== JSON.stringify(parsed._reasoningReplayScope?.current)) return;
134
135
  remembered = true;
135
136
  rememberReasoningReplayOpaqueBlobRejection(rejectedScope, rejectedPayloads);
@@ -129,6 +129,7 @@ import {
129
129
  ForwardAdmissionCredentialError,
130
130
  contextPrincipalIdOf,
131
131
  hasForwardableCodexBearer,
132
+ resolveContextPrincipal,
132
133
  validateForwardAdmissionCredential,
133
134
  } from "../auth-cors";
134
135
  import type { DataPlaneAdmission } from "../auth-cors";
@@ -1002,7 +1003,12 @@ export async function handleResponsesCompact(
1002
1003
  cursorConversationId: gatewayReplayParsed._cursorConversationId,
1003
1004
  sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
1004
1005
  });
1005
- if (clientThreadId) gatewayReplayParsed._reasoningReplayScope = { clientThreadId };
1006
+ if (clientThreadId) {
1007
+ gatewayReplayParsed._reasoningReplayScope = {
1008
+ clientThreadId,
1009
+ clientPrincipalId: resolveContextPrincipal(req, config, admission),
1010
+ };
1011
+ }
1006
1012
  }
1007
1013
  let compactUrl = gatewayTransport ? `${gatewayTransport.url.replace(/\/+$/, "")}/compact` : `${base}/responses/compact`;
1008
1014
  const compactTargetKey = `${route.providerName}|${route.modelId}|compact`;
@@ -1723,7 +1729,9 @@ export async function handleResponsesCompact(
1723
1729
  // The routed compaction turn is a handoff inside the same logical request, so it draws the
1724
1730
  // REMAINDER. Minting here is what let a native attempt spend three sends and the routed
1725
1731
  // fallback spend four more.
1726
- const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRecoveryKind: "compaction-v1", compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
1732
+ // internalReq keeps only FORWARD_HEADERS, so the caller principal is resolved from req.
1733
+ const replayPrincipal = { clientPrincipalId: resolveContextPrincipal(req, config, admission) };
1734
+ const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRecoveryKind: "compaction-v1", compactionRoutingOverride: options.compactionRoutingOverride, replayPrincipal, ...(admission ? { admission } : {}) });
1727
1735
  if (req.signal.aborted) {
1728
1736
  void response.body?.cancel(req.signal.reason).catch(() => undefined);
1729
1737
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
@@ -68,7 +68,7 @@ import {
68
68
  import { codexAccountLogLabel, oauthAccountLogLabel } from "../../codex/account-label";
69
69
  import { isGenericFailoverProvider, rotateGenericOAuthAccountOn429 } from "../../oauth/generic-account-failover";
70
70
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
71
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
71
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
72
72
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
73
73
  import {
74
74
  agentTaskRecoveryConfig,
@@ -76,6 +76,7 @@ import {
76
76
  recoverEncryptedAgentTaskWithResult,
77
77
  } from "./agent-task-recovery";
78
78
  import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
79
+ import { jevChoiceEfforts } from "./jev-combo-choice";
79
80
  import { applyDroidResponsesReasoningDefault } from "../droid-reasoning-default";
80
81
  import { isPlainObject } from "../../lib/plain-data";
81
82
  import {
@@ -102,6 +103,7 @@ import type { ResponsesTerminalStatus } from "../../bridge";
102
103
  import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
103
104
  import { rememberComboForLane } from "./combo-session-recall";
104
105
  import { COMBO_TOOL_LOOP_PIN_REASON, toolLoopPinnedPick } from "./combo-tool-loop-pin";
106
+ import { JEV_SESSION_PIN_ROUTE_REASON, jevSessionPinScope } from "./jev-session-pin";
105
107
  import { runTurnAdapterSseResponses } from "./core-lifetime";
106
108
  import { normalizePersistedJevDecision } from "../../usage/jev-stats";
107
109
  import {
@@ -118,6 +120,7 @@ import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo
118
120
  import { createJevModelInvoker } from "./jev-model-invoke";
119
121
  import { comboRequestedEffortLabel } from "./combo-requested-effort";
120
122
  import { clientWireOf } from "../inference/client-wire";
123
+ import { resolveContextPrincipal } from "../auth-cors";
121
124
 
122
125
  /**
123
126
  * Sends one combo target may run on its own before the ladder moves on. A target is a whole
@@ -316,30 +319,6 @@ interface JevComboChoice {
316
319
  candidate: JevCandidate;
317
320
  }
318
321
 
319
- /** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
320
- function jevChoiceEfforts(
321
- config: OcxConfig,
322
- target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
323
- ): OcxComboDefaultEffort[] | undefined {
324
- let ladder: string[] | undefined;
325
- try {
326
- const route = routeConcreteModel(config, targetKey(target));
327
- ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
328
- } catch {
329
- // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
330
- // no-effort choice rather than broadening JEV's effort allowlist.
331
- ladder = undefined;
332
- }
333
- const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
334
- const configuredEfforts = target.reasoningEfforts;
335
- const reasoningEfforts = configuredEfforts === undefined
336
- ? supportedEfforts
337
- : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
338
- // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
339
- // effort remains supported, omit the target instead of silently broadening JEV's choices.
340
- return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
341
- }
342
-
343
322
  /** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
344
323
  function eligibleJevComboChoices(
345
324
  config: OcxConfig,
@@ -358,15 +337,15 @@ function eligibleJevComboChoices(
358
337
  excluded.add(key);
359
338
  // The TypeSafe row owns a decision credential, not an inference transport.
360
339
  if (pick.target.provider === JEV_PROVIDER_ID) continue;
361
- const reasoningEfforts = jevChoiceEfforts(config, pick.target);
362
- if (!reasoningEfforts) continue;
340
+ const identity = jevChoiceEfforts(config, pick.target, combo.decisionQuotaSignals === true);
341
+ if (!identity) continue;
363
342
  choices.push({
364
343
  pick: { ...pick, attempted: [key] },
365
344
  candidate: {
366
345
  key,
367
346
  provider: pick.target.provider,
368
347
  model: pick.target.model,
369
- reasoningEfforts,
348
+ ...identity,
370
349
  modelProfile: pick.target.modelProfile,
371
350
  },
372
351
  });
@@ -405,6 +384,10 @@ export async function executeComboResponses(
405
384
  if (!combo) {
406
385
  return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
407
386
  }
387
+ // Resolve against the original request once; every bridge candidate receives the same trusted
388
+ // result after its provisional Authorization is removed, including an explicitly keyless result.
389
+ const replayPrincipal = options.replayPrincipal
390
+ ?? { clientPrincipalId: resolveContextPrincipal(req, config, options.admission) };
408
391
  // PF-07: present only for a Chat combo with `nativeChatCombos` on; otherwise every child
409
392
  // takes the bridge below exactly as before.
410
393
  const protocolLanes = createComboProtocolLanes({
@@ -481,6 +464,7 @@ export async function executeComboResponses(
481
464
  const unavailable = mandatoryResponsesReasoningReplayUnavailable({
482
465
  body,
483
466
  clientThreadId: reasoningReplayConversationId,
467
+ clientPrincipalId: replayPrincipal.clientPrincipalId,
484
468
  providerName: route.providerName,
485
469
  provider: route.provider,
486
470
  adapterName: route.provider.adapter,
@@ -623,6 +607,10 @@ export async function executeComboResponses(
623
607
  eligible: targetEligible,
624
608
  now: initialNow,
625
609
  });
610
+ // Opt-in JEV session pin (#7525); override routes neither read nor write it, like the lane recall.
611
+ const jevPin = options.compactionRoutingOverride || options.mainHardLockFallbackOverride
612
+ ? undefined
613
+ : jevSessionPinScope({ combo, comboId, admission: options.admission, lane: sessionLaneIdFromRequest(req.headers) });
626
614
 
627
615
  if (unreadableEncryptedAgentTask && !pick) {
628
616
  pick = await pickWithWait({ now: initialNow });
@@ -684,7 +672,7 @@ export async function executeComboResponses(
684
672
  const first = choices[0];
685
673
  if (!first) {
686
674
  return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
687
- targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
675
+ targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target, false) !== undefined));
688
676
  }
689
677
  const resolvedFailOpenEffort = resolveEffortAtOrBelow(
690
678
  "medium",
@@ -697,8 +685,13 @@ export async function executeComboResponses(
697
685
  : null,
698
686
  };
699
687
  const decisionStartedAt = Date.now();
688
+ const pinReuse = jevPin?.reuse(
689
+ choices.map(choice => choice.candidate),
690
+ combo.decisionQuotaSignals === true ? { config, tiers: combo.decisionQuotaTiers } : undefined,
691
+ );
700
692
  let decision: JevDecision;
701
- try {
693
+ if (pinReuse?.decision) decision = pinReuse.decision;
694
+ else try {
702
695
  decision = await resolveJevComboDecision({
703
696
  body,
704
697
  candidates: choices.map(choice => choice.candidate),
@@ -732,6 +725,7 @@ export async function executeComboResponses(
732
725
  latencyMs: Math.max(0, Date.now() - decisionStartedAt),
733
726
  };
734
727
  }
728
+ if (pinReuse?.released) decision = { ...decision, pinReason: pinReuse.released };
735
729
  jevDecision = decision;
736
730
  const selected = choices.find(choice => choice.candidate.key === decision.targetKey) ?? first;
737
731
  pick = { ...selected.pick, attempted: [targetKey(selected.pick.target)] };
@@ -752,6 +746,8 @@ export async function executeComboResponses(
752
746
  : {}),
753
747
  ...(decision.usage ? { usage: decision.usage } : {}),
754
748
  ...(decision.quota ? { quota: decision.quota } : {}),
749
+ ...(decision.pinned ? { pinned: true } : {}),
750
+ ...(decision.pinReason ? { pinReason: decision.pinReason } : {}),
755
751
  });
756
752
  console.debug("[combo] JEV decision", {
757
753
  backend: decision.backend,
@@ -759,6 +755,7 @@ export async function executeComboResponses(
759
755
  effort: decision.effort,
760
756
  gate: decision.gate,
761
757
  latencyMs: decision.latencyMs,
758
+ ...(decision.pinReason ? { pinReason: decision.pinReason } : {}),
762
759
  ...(decision.confidence !== undefined ? { confidence: decision.confidence } : {}),
763
760
  ...(decision.chosenProbability !== undefined
764
761
  ? { chosenProbability: decision.chosenProbability }
@@ -771,7 +768,7 @@ export async function executeComboResponses(
771
768
  const decision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
772
769
  const selectedReason = options.memoryModelPhase
773
770
  ? memoryModelRouteReason(options.memoryModelPhase)
774
- : toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : undefined;
771
+ : toolLoopPin ? COMBO_TOOL_LOOP_PIN_REASON : jevDecision?.pinned ? JEV_SESSION_PIN_ROUTE_REASON : undefined;
775
772
  logCtx.routeDecision = selectedReason
776
773
  ? { ...decision, selected: { ...decision.selected, reason: selectedReason } }
777
774
  : decision;
@@ -858,6 +855,22 @@ export async function executeComboResponses(
858
855
  // The physical destination after provider aliases and blockedModelRedirects: hops are
859
856
  // classified by where the request really goes, not by the selector the combo names.
860
857
  const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
858
+ // The hard-lock rewrite may have previewed a different random pick. Enforce this actual
859
+ // first destination against the locked source, after aliases and redirects resolve.
860
+ if (firstComboTarget && options.mainHardLockFallbackOverride) {
861
+ const hop = guardFallbackHop(config, {
862
+ site: "combo",
863
+ route: `combo/${comboId}`,
864
+ aliases: [requestedModel],
865
+ from: { provider: OPENAI_CODEX_PROVIDER_ID, model: options.mainHardLockFallbackOverride.sourceModel },
866
+ to: { provider: targetRoute.providerName, model: targetRoute.modelId },
867
+ trigger: "hard_lock",
868
+ admission: options.admission,
869
+ });
870
+ if (!hop.ok) return hop.response;
871
+ const declaredCommit = pendingHopCommit;
872
+ pendingHopCommit = declaredCommit ? () => { declaredCommit(); hop.commit(); } : hop.commit;
873
+ }
861
874
  // OCX-11: every later dispatch after a failure is a hop when the target changed. One check
862
875
  // here covers the ordinary failover pick and the recovery picks alike.
863
876
  if (!firstComboTarget && lastFailure && lastFailedTarget) {
@@ -1014,6 +1027,7 @@ export async function executeComboResponses(
1014
1027
  && provider && provider.disabled !== true
1015
1028
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
1016
1029
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
1030
+ jevPin?.complete(targetKey(completedTarget), initialJevDecision, writerGeneration);
1017
1031
  }
1018
1032
  options.onResponseComplete?.(model);
1019
1033
  },
@@ -1026,6 +1040,8 @@ export async function executeComboResponses(
1026
1040
  if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
1027
1041
  if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
1028
1042
  if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
1043
+ // A committed stream that ends failed is a target failure too; incomplete is an answer.
1044
+ if (status === "failed") jevPin?.forget();
1029
1045
  options.onNativePassthroughTerminal?.(status);
1030
1046
  },
1031
1047
  });
@@ -1065,6 +1081,7 @@ export async function executeComboResponses(
1065
1081
  },
1066
1082
  }) : await requestDispatchers.handleResponses(childRequest, config, childLog, {
1067
1083
  ...options,
1084
+ replayPrincipal,
1068
1085
  // A bridge child is a concrete route; the native source belongs to this loop only.
1069
1086
  ...(options.protocolSource ? { protocolSource: undefined } : {}),
1070
1087
  // After the spread: the child must run on THIS target's ladder, not on the holder the
@@ -1099,6 +1116,7 @@ export async function executeComboResponses(
1099
1116
  retainCancelledAttempt();
1100
1117
  return clientCancelledResponse();
1101
1118
  }
1119
+ jevPin?.forget();
1102
1120
  finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
1103
1121
  throw error;
1104
1122
  }
@@ -1123,6 +1141,7 @@ export async function executeComboResponses(
1123
1141
  retainCancelledAttempt();
1124
1142
  return clientCancelledResponse();
1125
1143
  }
1144
+ jevPin?.forget();
1126
1145
  finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
1127
1146
  throw error;
1128
1147
  }
@@ -1179,6 +1198,7 @@ export async function executeComboResponses(
1179
1198
  retainCancelledAttempt();
1180
1199
  return clientCancelledResponse();
1181
1200
  }
1201
+ jevPin?.forget();
1182
1202
  finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
1183
1203
  throw error;
1184
1204
  }
@@ -1210,6 +1230,8 @@ export async function executeComboResponses(
1210
1230
  }
1211
1231
  attemptRetained = true;
1212
1232
  lastFailure = failure.response;
1233
+ // A real target failure ends the session pin; the next turn is decided afresh (#7525).
1234
+ jevPin?.forget();
1213
1235
  if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
1214
1236
  quotaFailure = lastFailure;
1215
1237
  quotaFailedChildLog = childLog;
@@ -1,7 +1,7 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
2
  import { carryResponseToolOwnership } from "./response-tool-ownership";
3
3
  import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
4
- import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
4
+ import { carryReplayRefusal, isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
5
5
  import {
6
6
  isNativePassthroughSseResponse,
7
7
  markNativePassthroughSseResponse,
@@ -76,7 +76,7 @@ export function finalizeAccountLease(response: Response, release: () => void, on
76
76
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
77
77
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
78
78
  if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
79
- return carryResponseToolOwnership(response, wrapped);
79
+ return carryResponseToolOwnership(response, carryReplayRefusal(response, wrapped));
80
80
  }
81
81
 
82
82
 
@@ -358,14 +358,17 @@ export async function applyFinalRouteRequestNormalization(args: {
358
358
 
359
359
  {
360
360
  const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
361
+ const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
362
+ // The parser folds `ultra` to `max` only in options.reasoning; the passthrough adapter
363
+ // serializes the raw body, so a raw `ultra` must reach the clamp itself (#7590).
364
+ const rawUltra = raw?.reasoning && typeof raw.reasoning === "object" && raw.reasoning.effort === "ultra";
361
365
  const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
362
- ? nativeEffortClamp(route.modelId, parsed.options.reasoning)
366
+ ? nativeEffortClamp(route.modelId, rawUltra ? "ultra" : parsed.options.reasoning)
363
367
  : null;
364
368
  if (clamped) {
365
369
  parsed.options.reasoning = clamped;
366
- const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
367
370
  if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
368
- logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
371
+ logCtx.requestedEffort = `${logCtx.requestedEffort ?? (rawUltra ? "ultra" : "max")}->${clamped}`;
369
372
  }
370
373
  }
371
374
  // Chat ingress cannot strip effort against a provisional policy pick. Apply the
@@ -547,6 +547,7 @@ export async function attemptOpaqueBlobRecovery<Failure = Response>(
547
547
  const rejectedScope = args.parsed._reasoningReplayScope
548
548
  ? {
549
549
  clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
550
+ clientPrincipalId: args.parsed._reasoningReplayScope.clientPrincipalId,
550
551
  ...(args.parsed._reasoningReplayScope.current
551
552
  ? { current: { ...args.parsed._reasoningReplayScope.current } }
552
553
  : {}),