@bitkyc08/opencodex 2.90.0 → 2.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  3. package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
  4. package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
  5. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  6. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  9. package/package.json +4 -3
  10. package/src/adapters/anthropic.ts +2 -5
  11. package/src/adapters/claude-cli/usage-admission.ts +14 -0
  12. package/src/adapters/google-vertex-endpoint.ts +9 -1
  13. package/src/adapters/google-vertex-key-project.ts +223 -0
  14. package/src/adapters/google.ts +13 -3
  15. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  16. package/src/adapters/openai-chat.ts +2 -2
  17. package/src/adapters/openai-responses/passthrough.ts +2 -1
  18. package/src/adapters/openai-responses/request-strips.ts +92 -0
  19. package/src/adapters/openai-responses/transport.ts +6 -0
  20. package/src/adapters/registry.ts +5 -0
  21. package/src/adapters/workbuddy-schema.ts +61 -0
  22. package/src/adapters/workbuddy.ts +68 -0
  23. package/src/bridge/response-json.ts +1 -1
  24. package/src/bridge/sse.ts +1 -1
  25. package/src/chat/inbound.ts +20 -2
  26. package/src/chat/outbound.ts +47 -8
  27. package/src/chat/reasoning-details.ts +137 -0
  28. package/src/cli/capabilities-agents-routing.ts +53 -3
  29. package/src/cli/capabilities-base.ts +15 -2
  30. package/src/cli/capabilities-observe-system.ts +4 -4
  31. package/src/cli/codex-cli-update.ts +8 -0
  32. package/src/cli/codex-shim-autorestore.ts +5 -0
  33. package/src/cli/combo-result.ts +6 -0
  34. package/src/cli/combo-stats.ts +3 -0
  35. package/src/cli/combo.ts +66 -42
  36. package/src/cli/dispatch.ts +30 -0
  37. package/src/cli/fallback.ts +127 -0
  38. package/src/cli/help.ts +1 -0
  39. package/src/cli/index.ts +1 -2
  40. package/src/cli/launcher-context.ts +49 -2
  41. package/src/cli/link-apply-restart.ts +72 -0
  42. package/src/cli/link-child.ts +6 -2
  43. package/src/cli/link-cleanup.ts +95 -0
  44. package/src/cli/link-enrollment.ts +4 -0
  45. package/src/cli/link.ts +123 -35
  46. package/src/cli/provider.ts +46 -3
  47. package/src/cli/registry.ts +13 -1
  48. package/src/cli/restart-failure.ts +4 -0
  49. package/src/cli/runtime-api.ts +1 -1
  50. package/src/cli/status-codex-settings.ts +56 -0
  51. package/src/cli/status-fallback.ts +29 -0
  52. package/src/cli/status-install.ts +52 -0
  53. package/src/cli/status.ts +20 -1
  54. package/src/cli/system-restart-client.ts +32 -1
  55. package/src/cli/update-restart-child.ts +4 -3
  56. package/src/cli/update-restart-home.ts +20 -8
  57. package/src/cli/usage-compare.ts +68 -12
  58. package/src/client/connect.ts +82 -6
  59. package/src/client/home-unreachable.ts +16 -0
  60. package/src/client/hub-relay.ts +8 -2
  61. package/src/client/link-home.ts +119 -6
  62. package/src/client/link-ingress.ts +1 -1
  63. package/src/client/link-parity-reporter.ts +107 -0
  64. package/src/client/link-readiness.ts +97 -0
  65. package/src/client/link-relay-transport.ts +84 -9
  66. package/src/client/link-relay.ts +98 -15
  67. package/src/client/link-tunnel.ts +45 -18
  68. package/src/client/machine-listener.ts +103 -3
  69. package/src/client/runtime-restart.ts +127 -0
  70. package/src/client/runtime.ts +155 -25
  71. package/src/codex/account-store.ts +17 -3
  72. package/src/codex/app-server-restart-service.ts +6 -5
  73. package/src/codex/auth-context.ts +10 -6
  74. package/src/codex/catalog/effort.ts +9 -2
  75. package/src/codex/catalog/gather-capture.ts +4 -2
  76. package/src/codex/catalog/parsing.ts +4 -0
  77. package/src/codex/catalog/provider-models.ts +14 -8
  78. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  79. package/src/codex/cli-install-provenance.ts +418 -0
  80. package/src/codex/cli-installation-targets.ts +5 -1
  81. package/src/codex/cli-update-plan.ts +43 -16
  82. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  83. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  84. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  85. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  86. package/src/codex/pool-refresh-backoff.ts +3 -1
  87. package/src/codex/quota-history.ts +7 -0
  88. package/src/codex/quota.ts +10 -0
  89. package/src/codex/subagent-model-fallback.ts +24 -1
  90. package/src/codex/user-identity.ts +35 -6
  91. package/src/codex/windows-current-sid.ts +162 -0
  92. package/src/combos/index.ts +1 -0
  93. package/src/combos/jev-quota-activation.ts +5 -1
  94. package/src/combos/jev-quota-route.ts +1 -1
  95. package/src/combos/jev-quota.ts +9 -1
  96. package/src/combos/jev-session-pin-config.ts +51 -0
  97. package/src/combos/jev.ts +6 -0
  98. package/src/combos/resolve.ts +12 -0
  99. package/src/combos/types.ts +19 -0
  100. package/src/config/diagnostics.ts +5 -0
  101. package/src/config/load-degrade.ts +2 -0
  102. package/src/config/provider-validation.ts +7 -0
  103. package/src/config/schema/config-schema.ts +3 -0
  104. package/src/config/schema/fallback-policy.ts +40 -0
  105. package/src/config/schema/leaf-validators.ts +1 -0
  106. package/src/generated/compatibility-version.json +375 -195
  107. package/src/lib/authorization-scheme.ts +95 -0
  108. package/src/lib/decision-quota-sink.ts +10 -0
  109. package/src/lib/provider-client-headers.ts +1 -0
  110. package/src/lib/redact.ts +2 -2
  111. package/src/lib/system-restart-contract.ts +24 -0
  112. package/src/lib/test-home-guard.ts +78 -11
  113. package/src/lib/translator-budget.ts +4 -4
  114. package/src/lib/upstream-retry.ts +4 -1
  115. package/src/link/child-parity.ts +124 -0
  116. package/src/link/codex-settings.ts +37 -0
  117. package/src/link/compatibility.ts +119 -0
  118. package/src/link/diagnostics.ts +193 -0
  119. package/src/link/management-routes.ts +2 -0
  120. package/src/link/peer-report.ts +100 -0
  121. package/src/link/relay-auth.ts +6 -0
  122. package/src/link/remote-platform.ts +78 -0
  123. package/src/link/ssh-argv.ts +19 -0
  124. package/src/link/status-projection.ts +63 -2
  125. package/src/link/supervisor.ts +45 -12
  126. package/src/link/tunnel-history.ts +109 -0
  127. package/src/link/tunnel-state.ts +80 -17
  128. package/src/messaging/remote-files.ts +24 -1
  129. package/src/oauth/generic-account-failover.ts +91 -7
  130. package/src/oauth/index.ts +16 -1
  131. package/src/oauth/kimi.ts +2 -0
  132. package/src/oauth/model-discovery-auth.ts +5 -0
  133. package/src/oauth/nous.ts +4 -1
  134. package/src/oauth/store.ts +6 -2
  135. package/src/oauth/token-guardian.ts +7 -0
  136. package/src/oauth/types.ts +9 -0
  137. package/src/oauth/workbuddy.ts +139 -0
  138. package/src/protocols/codecs/chat.ts +3 -3
  139. package/src/protocols/encoders/adapter-events.ts +26 -2
  140. package/src/protocols/encoders/chat.ts +55 -3
  141. package/src/providers/antigravity-quota-identity.ts +37 -0
  142. package/src/providers/base-url-choices.ts +8 -5
  143. package/src/providers/derive.ts +23 -3
  144. package/src/providers/mistral-transport.ts +37 -4
  145. package/src/providers/model-discovery.ts +2 -0
  146. package/src/providers/model-rename-fields.ts +1 -0
  147. package/src/providers/quota/account-cache.ts +3 -2
  148. package/src/providers/quota/antigravity.ts +27 -22
  149. package/src/providers/quota/report-cache.ts +13 -4
  150. package/src/providers/quota/vendor-probes-key.ts +60 -0
  151. package/src/providers/quota/workbuddy.ts +83 -0
  152. package/src/providers/quota-decision-antigravity.ts +64 -0
  153. package/src/providers/quota-decision-publication.ts +11 -0
  154. package/src/providers/quota-decision-snapshot.ts +21 -4
  155. package/src/providers/quota-types.ts +2 -0
  156. package/src/providers/quota.ts +3 -1
  157. package/src/providers/registry/entries-core.ts +9 -0
  158. package/src/providers/registry/entries-extended.ts +20 -7
  159. package/src/providers/registry/model-ids.ts +3 -0
  160. package/src/providers/registry/model-seeds.ts +27 -28
  161. package/src/providers/registry/types.ts +19 -1
  162. package/src/providers/resolved-model-policy.ts +4 -4
  163. package/src/providers/routed-config.ts +15 -0
  164. package/src/providers/stale-model-roster-migration.ts +83 -15
  165. package/src/providers/workbuddy-credit-balance.ts +81 -0
  166. package/src/providers/workbuddy-models.ts +37 -0
  167. package/src/providers/workbuddy.ts +60 -0
  168. package/src/quota-pool/assemble.ts +326 -0
  169. package/src/quota-pool/contract.ts +58 -0
  170. package/src/quota-pool/identity.ts +203 -0
  171. package/src/remote/features.ts +74 -0
  172. package/src/remote/protocol.ts +2 -0
  173. package/src/remote/served-by.ts +57 -0
  174. package/src/responses/parser.ts +7 -0
  175. package/src/responses/reasoning-envelope.ts +12 -0
  176. package/src/responses/reasoning-replay-cache.ts +18 -14
  177. package/src/router.ts +2 -0
  178. package/src/routing/analytics.ts +2 -3
  179. package/src/routing/fallback-policy.ts +290 -0
  180. package/src/routing/fallback-state.ts +179 -0
  181. package/src/server/audio-client.ts +2 -1
  182. package/src/server/audio-dictation.ts +2 -1
  183. package/src/server/audio-upstream.ts +2 -1
  184. package/src/server/auth-cors.ts +7 -5
  185. package/src/server/background-lifecycle.ts +4 -0
  186. package/src/server/chat-completions.ts +22 -2
  187. package/src/server/chat-native-reasoning.ts +10 -3
  188. package/src/server/chat-native.ts +13 -2
  189. package/src/server/claude-messages.ts +3 -0
  190. package/src/server/gui-session.ts +2 -1
  191. package/src/server/index/link-listener.ts +11 -1
  192. package/src/server/index/link-management.ts +47 -0
  193. package/src/server/index/link-relay-sessions.ts +20 -7
  194. package/src/server/index/optional-listeners.ts +11 -2
  195. package/src/server/index/serve-options.ts +19 -2
  196. package/src/server/live.ts +5 -0
  197. package/src/server/local-management-read-client.ts +3 -1
  198. package/src/server/management/combo-routes.ts +10 -4
  199. package/src/server/management/context.ts +1 -1
  200. package/src/server/management/fallback-routes.ts +75 -0
  201. package/src/server/management/link-enrollment.ts +21 -3
  202. package/src/server/management/link-route-state.ts +2 -0
  203. package/src/server/management/link-routes.ts +16 -2
  204. package/src/server/management/provider-routes.ts +1 -1
  205. package/src/server/management/quota-pool-routes.ts +24 -0
  206. package/src/server/management/route-registry.ts +3 -0
  207. package/src/server/management-api.ts +16 -0
  208. package/src/server/management-auth.ts +2 -1
  209. package/src/server/port-reclaim.ts +51 -9
  210. package/src/server/request-log-conversation.ts +49 -0
  211. package/src/server/request-log.ts +2 -0
  212. package/src/server/responses/agent-task-recovery.ts +1 -1
  213. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  214. package/src/server/responses/compact.ts +12 -4
  215. package/src/server/responses/core-auth.ts +11 -1
  216. package/src/server/responses/core-combo.ts +125 -32
  217. package/src/server/responses/core-lifetime.ts +2 -2
  218. package/src/server/responses/core-normalize.ts +6 -3
  219. package/src/server/responses/core-opaque-recovery.ts +1 -0
  220. package/src/server/responses/core-options.ts +5 -0
  221. package/src/server/responses/core-replay.ts +2 -1
  222. package/src/server/responses/encrypted-payload.ts +3 -3
  223. package/src/server/responses/jev-combo-choice.ts +39 -0
  224. package/src/server/responses/jev-session-pin.ts +203 -0
  225. package/src/server/responses/main-hard-lock-routing.ts +54 -2
  226. package/src/server/responses/passthrough-delivery.ts +4 -0
  227. package/src/server/responses/policy-fallback.ts +44 -1
  228. package/src/server/responses/request-prepare.ts +19 -6
  229. package/src/server/responses/request-transport.ts +25 -6
  230. package/src/server/responses/run-turn-execution.ts +2 -4
  231. package/src/server/responses/sidecar-execution.ts +2 -4
  232. package/src/server/responses-phase-inference.ts +267 -0
  233. package/src/server/restart-replacement.ts +50 -2
  234. package/src/server/sse-payload-rewrite.ts +14 -1
  235. package/src/server/ws-bridge.ts +5 -0
  236. package/src/service/cli.ts +25 -2
  237. package/src/service/definition.ts +20 -8
  238. package/src/service/diagnostics.ts +20 -6
  239. package/src/service/status-json.ts +116 -0
  240. package/src/types/config.ts +13 -0
  241. package/src/types/fallback.ts +38 -0
  242. package/src/types/provider.ts +9 -1
  243. package/src/types/request.ts +1 -0
  244. package/src/types/wire.ts +1 -0
  245. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  246. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  247. package/src/usage/jev-stats.ts +54 -14
  248. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  249. package/gui/dist/assets/index-TmaHa54u.js +0 -94
  250. package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The connected client runtime's answer to the bound `POST /api/system/restart` (OCX-6).
3
+ *
4
+ * It is the same restart `ocx restart` drives on a standalone proxy, with one difference the
5
+ * Remote Link parity work requires: a failed restart keeps the old runtime. That is only possible
6
+ * when this process starts its own replacement, so:
7
+ *
8
+ * - Under the desktop app or a viable service supervisor the restart is refused (409
9
+ * `restart_supervised`) and the runtime keeps serving. The supervisor owns this process and its
10
+ * port; handing off to it means exiting before anything proves the replacement can start.
11
+ * - Otherwise the listener drains, a replacement `ocx start` is spawned and must answer within the
12
+ * budget. Success exits this process. A replacement that exited, or never answered and was then
13
+ * stopped, leaves this process to bind the port again, start a fresh tunnel supervisor and
14
+ * republish its records: the old runtime keeps serving and the CLI, still seeing its pid,
15
+ * reports the restart as failed.
16
+ *
17
+ * Everything runs inside {@link CLIENT_RESTART_BUDGET_MS} from acceptance.
18
+ */
19
+ import {
20
+ CLIENT_RESTART_DRAIN_MS,
21
+ CLIENT_RESTART_READY_BY_MS,
22
+ isDesktopSupervised,
23
+ } from "../lib/system-restart-contract";
24
+ import type { OcxClientConnectionConfig } from "../types";
25
+ import { sameClientConnectionOwner, type readClientConnectionState } from "./state";
26
+
27
+ type ClientConnectionState = ReturnType<typeof readClientConnectionState>;
28
+
29
+ export type ClientRestartSupervisor = "service" | "desktop";
30
+
31
+ export type ClientRestartAdmission =
32
+ | { kind: "accepted"; alreadyRestarting: boolean }
33
+ | { kind: "supervised"; supervisor: ClientRestartSupervisor };
34
+
35
+ export interface ClientRestartIo {
36
+ /** Which supervisor owns this process, or null when it starts its own replacement. */
37
+ supervisor(): Promise<ClientRestartSupervisor | null>;
38
+ /** Stop the tunnel supervisor and the listener, giving in-flight requests up to `drainMs`. */
39
+ stop(drainMs: number): Promise<void>;
40
+ /** Start the replacement; resolves once it answers, rejects when none survives. */
41
+ spawnReplacement(deadlineAt: number): Promise<void>;
42
+ /** Serve again on this process: listener, fresh tunnel supervisor, records. */
43
+ resume(): Promise<void>;
44
+ exitProcess(code: number): void;
45
+ now?: () => number;
46
+ sleep?: (ms: number) => Promise<void>;
47
+ warn?: (message: string) => void;
48
+ }
49
+
50
+ export interface ClientRestartController {
51
+ request(): Promise<ClientRestartAdmission>;
52
+ /** Settles when the restart in flight (if any) has finished or rolled back. Tests only. */
53
+ settled(): Promise<void>;
54
+ }
55
+
56
+ /** Stable, path-free label for a failure (an error message can carry a path). */
57
+ function failureCode(error: unknown): string {
58
+ const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
59
+ return typeof code === "string" && /^[A-Za-z0-9_]{1,64}$/.test(code) ? code : "failed";
60
+ }
61
+
62
+ export function createClientRestartController(io: ClientRestartIo): ClientRestartController {
63
+ const now = io.now ?? Date.now;
64
+ const sleep = io.sleep ?? Bun.sleep;
65
+ const warn = io.warn ?? ((message: string) => console.warn(message));
66
+ let flight: Promise<void> | null = null;
67
+
68
+ const run = async (acceptedAt: number): Promise<void> => {
69
+ // Let the 202 reach the CLI before the listener starts closing.
70
+ await sleep(200);
71
+ try {
72
+ await io.stop(CLIENT_RESTART_DRAIN_MS);
73
+ } catch (error) {
74
+ warn(`[client] restart: stopping the runtime failed (${failureCode(error)}); continuing`);
75
+ }
76
+ try {
77
+ await io.spawnReplacement(acceptedAt + CLIENT_RESTART_READY_BY_MS);
78
+ } catch (error) {
79
+ warn(`[client] restart: the replacement did not start (${failureCode(error)}); this runtime keeps serving`);
80
+ try {
81
+ await io.resume();
82
+ } catch (resumeError) {
83
+ warn(`[client] restart: this runtime could not serve again (${failureCode(resumeError)}); run 'ocx start'`);
84
+ io.exitProcess(1);
85
+ }
86
+ return;
87
+ }
88
+ io.exitProcess(0);
89
+ };
90
+
91
+ return {
92
+ async request() {
93
+ const supervisor = await io.supervisor();
94
+ if (supervisor) return { kind: "supervised", supervisor };
95
+ if (flight) return { kind: "accepted", alreadyRestarting: true };
96
+ flight = run(now()).finally(() => { flight = null; });
97
+ return { kind: "accepted", alreadyRestarting: false };
98
+ },
99
+ async settled() {
100
+ await flight;
101
+ },
102
+ };
103
+ }
104
+
105
+ /** Production supervision check: the desktop app, then a service that will really relaunch us. */
106
+ export async function clientRestartSupervisor(): Promise<ClientRestartSupervisor | null> {
107
+ if (isDesktopSupervised()) return "desktop";
108
+ if (process.env.OCX_SERVICE !== "1") return null;
109
+ // Same viability test as the standalone restart: stale service assets would not relaunch.
110
+ const { isServiceViable } = await import("../service/orchestration");
111
+ return isServiceViable() ? "service" : null;
112
+ }
113
+
114
+ /**
115
+ * After a failed replacement: serve the same connection again only while it is still the durable
116
+ * one (same owner and link). Anything else, a disconnect or a different connection, goes to the
117
+ * recycle, which starts what the current state calls for instead of a stale client runtime.
118
+ */
119
+ export function clientRestartRollbackAction(
120
+ served: OcxClientConnectionConfig,
121
+ current: ClientConnectionState,
122
+ ): "resume" | "recycle" {
123
+ return current.kind === "connected" && sameClientConnectionOwner(current.value, served)
124
+ && current.value.link?.linkId === served.link?.linkId
125
+ ? "resume"
126
+ : "recycle";
127
+ }
@@ -1,3 +1,4 @@
1
+ import { flushLinkDiagnostics, recordLinkDiagnostic } from "../link/diagnostics";
1
2
  import { existsSync } from "node:fs";
2
3
  import type { Server } from "bun";
3
4
  import { siblingRuntimeField, withSiblingMarker } from "../codex/sibling-start";
@@ -9,6 +10,7 @@ import { createLocalAttestationSecret } from "../lib/local-management-attestatio
9
10
  import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
10
11
  import { handledSignalExitCode } from "../lib/handled-signal-exit";
11
12
  import {
13
+ CLIENT_RESTART_REBIND_WAIT_MS,
12
14
  DESKTOP_RESTART_EXIT_CODE,
13
15
  DESKTOP_SUPERVISED_ENV,
14
16
  DESKTOP_SUPERVISED_PORT_WAIT_MS,
@@ -19,16 +21,27 @@ import type { ReplacementStartRequest } from "../server/restart-replacement";
19
21
  import type { OcxClientConnectionConfig } from "../types";
20
22
  import { createLinkKeySource } from "./link-ingress";
21
23
  import { HOME_INITIATED_LINK_TUNNEL } from "./link-relay";
24
+ import { clientInitiatedLinkReadiness, createHomeInitiatedLinkReadiness } from "./link-readiness";
22
25
  import { createClientLinkSupervisor, type ClientLinkSupervisor } from "./link-tunnel";
23
26
  import { clientLinkStatePath, isChildInitiatedLink, recordChildInitiatedLink } from "./link-state";
24
27
  import { startMachineListener, type MachineListenerDeps } from "./machine-listener";
25
28
  import { isLinkConnection, readClientConnectionState } from "./state";
29
+ import { withClientLifecycleSync } from "./lifecycle-lock";
30
+ import { clientRestartRollbackAction, clientRestartSupervisor, createClientRestartController, type ClientRestartController } from "./runtime-restart";
31
+ import { createLinkParityReporter, type LinkParityReporter } from "./link-parity-reporter";
26
32
 
27
33
  let activeServer: Server<unknown> | null = null;
28
34
  let activePort: number | null = null;
29
35
  let activeSupervisor: ClientLinkSupervisor | null = null;
36
+ let activeReporter: LinkParityReporter | null = null;
30
37
  let recycleScheduled = false;
31
38
 
39
+ /**
40
+ * When this runtime's process started (OCX-6, P5): what `/healthz` reports as `startedAt` and the
41
+ * Child reports to its Home. A Home settings apply recorded after it means this runtime predates it.
42
+ */
43
+ export const CLIENT_RUNTIME_STARTED_AT = new Date(Date.now() - process.uptime() * 1000).toISOString();
44
+
32
45
  /**
33
46
  * How long link mode waits for its configured port: the budget a hard-pinned `ocx start --port`
34
47
  * gives `reclaimListenPort` (`src/cli/index.ts`).
@@ -53,6 +66,8 @@ export interface ClientRuntimeIo {
53
66
  /** Link-mode port budget; defaults to {@link LINK_PORT_WAIT_MS}. */
54
67
  portWaitMs?: number;
55
68
  startListener?: typeof startMachineListener;
69
+ /** Which supervisor owns this process for a bound restart; tests replace the production check. */
70
+ restartSupervisor?: () => Promise<"service" | "desktop" | null>;
56
71
  }
57
72
 
58
73
  export interface StandaloneRecycleIo {
@@ -151,6 +166,7 @@ export async function recycleStandalone(
151
166
  console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
152
167
  }
153
168
  cleanup();
169
+ await flushLinkDiagnostics();
154
170
  // Recycling back to standalone after `ocx disconnect` must actually bring a standalone
155
171
  // proxy back, under every launch shape.
156
172
  //
@@ -214,7 +230,7 @@ export async function bindClientListener(
214
230
  preferred: number;
215
231
  explicitPort: boolean;
216
232
  configuredPort: number;
217
- } & Pick<MachineListenerDeps, "linkStatus" | "linkKeySource" | "linkTunnel">,
233
+ } & Pick<MachineListenerDeps, "linkStatus" | "linkKeySource" | "linkTunnel" | "linkReadiness" | "restart">,
218
234
  io: ClientRuntimeIo = {},
219
235
  ): Promise<{ server: Server<unknown>; port: number }> {
220
236
  const { linkMode } = request;
@@ -254,6 +270,8 @@ export async function bindClientListener(
254
270
  ...(request.linkStatus ? { linkStatus: request.linkStatus } : {}),
255
271
  ...(request.linkKeySource ? { linkKeySource: request.linkKeySource } : {}),
256
272
  ...(request.linkTunnel ? { linkTunnel: request.linkTunnel } : {}),
273
+ ...(request.linkReadiness ? { linkReadiness: request.linkReadiness } : {}),
274
+ ...(request.restart ? { restart: request.restart } : {}),
257
275
  });
258
276
  return { server, port: server.port ?? port };
259
277
  } catch (error) {
@@ -286,33 +304,142 @@ export async function startClientRuntime(
286
304
  if (linkMode && state.value.link) {
287
305
  try { recordChildInitiatedLink(state.value.link.linkId); } catch { /* the sidecar check below still applies */ }
288
306
  }
289
- const supervisor = linkMode && existsSync(clientLinkStatePath())
290
- ? createClientLinkSupervisor({
291
- onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
292
- linkKey,
293
- })
294
- : null;
307
+ const supervised = linkMode && existsSync(clientLinkStatePath());
295
308
  // A Child with no sidecar and no record of joining itself was connected by its Home over
296
309
  // `ssh -R`; that link keeps 2.67.0's unproven forward. A Child-initiated link that lost its
297
310
  // sidecar gets no gate at all, so the relay refuses it.
298
- const homeInitiated = linkMode && !supervisor && !!state.value.link
311
+ const homeInitiated = linkMode && !supervised && !!state.value.link
299
312
  && !isChildInitiatedLink(state.value.link.linkId);
300
- const { server, port: boundPort } = await bindClientListener({
301
- state: state.value,
302
- linkMode,
303
- preferred,
304
- explicitPort: options.port !== undefined,
305
- configuredPort: config.port,
306
- ...(linkMode ? { linkStatus: () => supervisor?.status() ?? { kind: "stopped" as const }, linkKeySource: linkKey } : {}),
307
- ...(supervisor ? { linkTunnel: supervisor } : homeInitiated ? { linkTunnel: HOME_INITIATED_LINK_TUNNEL } : {}),
308
- }, io);
309
- activeServer = server;
310
- activePort = boundPort;
311
- activeSupervisor = supervisor;
312
- supervisor?.start();
313
+ // One secret for the life of this process: a rollback republishes the same identity.
314
+ const attestationSecret = createLocalAttestationSecret();
315
+
316
+ // Bind, start a fresh tunnel supervisor and publish. Runs once at start and again when a failed
317
+ // restart rolls back (OCX-6); the stopped supervisor cannot be started twice, so each call builds one.
318
+ // The port this process actually serves on; a rollback binds exactly this one again.
319
+ let servedPort = preferred;
320
+ // Returns false only for a rollback whose connection changed while it was binding: the new
321
+ // listener is then closed unpublished and the caller recycles.
322
+ const serve = async (rollback?: { port: number; portWaitMs: number }): Promise<boolean> => {
323
+ const supervisor = supervised
324
+ ? createClientLinkSupervisor({
325
+ diagnostic: recordLinkDiagnostic,
326
+ onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
327
+ linkKey,
328
+ })
329
+ : null;
330
+ // What `/readyz` reports about the link: the supervisor's tunnel, or for a Home-initiated
331
+ // link the shared relay preflight. A link with neither refuses every relayed request.
332
+ const link = state.value.link;
333
+ const linkReadiness: MachineListenerDeps["linkReadiness"] = !linkMode ? undefined
334
+ : supervisor ? () => clientInitiatedLinkReadiness(supervisor.status())
335
+ : homeInitiated && link && linkKey
336
+ ? createHomeInitiatedLinkReadiness({ target: { tunnelPort: link.tunnelPort, linkId: link.linkId, apiKeyId: state.value.apiKeyId }, linkKey })
337
+ : () => ({ ready: false, reason: "tunnel_unavailable" });
338
+ const { server, port: boundPort } = await bindClientListener({
339
+ state: state.value,
340
+ linkMode,
341
+ // A rollback takes back exactly the port it gave up: never the preferred one, never another.
342
+ preferred: rollback?.port ?? preferred,
343
+ explicitPort: rollback !== undefined || options.port !== undefined,
344
+ configuredPort: config.port,
345
+ restart: { attestationSecret, startedAt: CLIENT_RUNTIME_STARTED_AT, request: () => restartController.request() },
346
+ ...(linkMode ? { linkStatus: () => activeSupervisor?.status() ?? { kind: "stopped" as const }, linkKeySource: linkKey } : {}),
347
+ ...(supervisor ? { linkTunnel: supervisor } : homeInitiated ? { linkTunnel: HOME_INITIATED_LINK_TUNNEL } : {}),
348
+ ...(linkReadiness ? { linkReadiness } : {}),
349
+ }, rollback === undefined ? io : { ...io, portWaitMs: rollback.portWaitMs });
350
+ const publish = (): void => {
351
+ activeServer = server;
352
+ activePort = boundPort;
353
+ servedPort = boundPort;
354
+ activeSupervisor = supervisor;
355
+ supervisor?.start();
356
+ writePid(process.pid);
357
+ writeRuntimePort(clientRuntimeRecord(process.pid, boundPort, attestationSecret));
358
+ if (linkMode && state.value.link && linkKey) {
359
+ activeReporter = createLinkParityReporter({ connection: state.value, linkKey, runtimeStartedAt: CLIENT_RUNTIME_STARTED_AT });
360
+ activeReporter.start();
361
+ }
362
+ };
363
+ if (!rollback) {
364
+ publish();
365
+ return true;
366
+ }
367
+ // A rollback publishes under the client lifecycle lock, after rechecking the connection there,
368
+ // so a disconnect or connect that committed during the bind can never be served over. A lock
369
+ // held by such an operation counts as a change.
370
+ let published = false;
371
+ try {
372
+ published = withClientLifecycleSync(() => {
373
+ if (clientRestartRollbackAction(state.value, readClientConnectionState()) === "recycle") return false;
374
+ publish();
375
+ return true;
376
+ });
377
+ } catch {
378
+ published = false;
379
+ }
380
+ if (!published) {
381
+ try { await server.stop(true); } catch { /* never published */ }
382
+ }
383
+ return published;
384
+ };
385
+
386
+ // Stop everything this process serves, giving in-flight requests up to `drainMs`.
387
+ const stopServing = async (drainMs: number): Promise<void> => {
388
+ activeReporter?.stop();
389
+ activeReporter = null;
390
+ try {
391
+ await activeSupervisor?.stop();
392
+ } catch (error) {
393
+ console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`);
394
+ }
395
+ activeSupervisor = null;
396
+ const server = activeServer;
397
+ activeServer = null;
398
+ if (!server) return;
399
+ let timer: ReturnType<typeof setTimeout> | undefined;
400
+ try {
401
+ await Promise.race([
402
+ Promise.resolve(server.stop(false)),
403
+ new Promise<void>(resolve => { timer = setTimeout(resolve, drainMs); }),
404
+ ]);
405
+ } catch { /* closed below */ } finally {
406
+ if (timer) clearTimeout(timer);
407
+ }
408
+ try { await server.stop(true); } catch { /* already closed */ }
409
+ };
410
+
411
+ const restartController: ClientRestartController = createClientRestartController({
412
+ supervisor: io.restartSupervisor ?? clientRestartSupervisor,
413
+ stop: stopServing,
414
+ spawnReplacement: async deadlineAt => {
415
+ const { replacementStartEnvironment } = await import("../server/management/system-restart");
416
+ const { spawnReplacementStart } = await import("../server/restart-replacement");
417
+ await spawnReplacementStart(
418
+ { port: activePort ?? config.port, waitForHealth: true, env: replacementStartEnvironment(true), notReady: "terminate" },
419
+ { deadlineAt },
420
+ );
421
+ },
422
+ resume: async () => {
423
+ // The connection may have changed while the replacement was starting (`ocx disconnect`, a
424
+ // new connect). Serving the old one again would be a stale runtime nothing recycles; hand
425
+ // off to the recycle, which starts whatever the durable state now calls for.
426
+ if (clientRestartRollbackAction(state.value, readClientConnectionState()) === "recycle") {
427
+ await recycleStandalone(state.value.tokenFingerprint);
428
+ return;
429
+ }
430
+ if (!(await serve({ port: servedPort, portWaitMs: CLIENT_RESTART_REBIND_WAIT_MS }))) {
431
+ await recycleStandalone(state.value.tokenFingerprint);
432
+ }
433
+ },
434
+ exitProcess: async code => {
435
+ await flushLinkDiagnostics();
436
+ process.exit(code);
437
+ },
438
+ });
439
+
313
440
  installCrashGuards();
314
- writePid(process.pid);
315
- writeRuntimePort(clientRuntimeRecord(process.pid, boundPort));
441
+ // serve() publishes the pid and runtime record; the ownership lease is released only after it.
442
+ await serve();
316
443
  options.afterPublish?.();
317
444
 
318
445
  let shuttingDown = false;
@@ -320,18 +447,21 @@ export async function startClientRuntime(
320
447
  if (shuttingDown) return;
321
448
  shuttingDown = true;
322
449
  void (async () => {
450
+ activeReporter?.stop();
451
+ activeReporter = null;
323
452
  try {
324
- await supervisor?.stop();
453
+ await activeSupervisor?.stop();
325
454
  } catch (error) {
326
455
  console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`);
327
456
  }
328
457
  activeSupervisor = null;
329
458
  try {
330
- server.stop(true);
459
+ activeServer?.stop(true);
331
460
  } catch (error) {
332
461
  console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
333
462
  } finally {
334
463
  cleanup();
464
+ await flushLinkDiagnostics();
335
465
  // launchd relaunches only unsuccessful exits; an external signal must still count.
336
466
  process.exit(handledSignalExitCode(signal));
337
467
  }
@@ -12,13 +12,15 @@ import {
12
12
  backupInvalidConfig,
13
13
  hardenConfigDir,
14
14
  hardenExistingSecret,
15
+ loadConfig,
15
16
  withConfigMutationLockSync,
16
17
  } from "../config";
17
18
  import { notifyCodexStoreLoaded } from "../lib/decision-quota-sink";
18
19
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
19
- import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
20
+ import type { CodexAccount, CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
20
21
  import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
21
22
  import { isValidCodexAccountId } from "./account-id";
23
+ import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "./account-label";
22
24
  import type { PoolQuotaWriter } from "./quota-types";
23
25
  import { CODEX_REFRESH_FLIGHT_CEILING_MS } from "./quota-recovery-timing";
24
26
 
@@ -183,6 +185,18 @@ function persistCredentialMutation(store: CodexAccountStore): void {
183
185
  advanceCodexCredentialMutationEpoch();
184
186
  }
185
187
 
188
+ /**
189
+ * The durable `p<hex6>` label the dashboard and usage log show for this pool account, so a refresh
190
+ * failure line can be matched to the account row (#7592). The id hash is only the label of a
191
+ * legacy row that never received one. A config read failure must not replace the refresh error
192
+ * being reported, so it degrades to that same hash.
193
+ */
194
+ function poolAccountLogLabel(id: string): string {
195
+ let account: CodexAccount | undefined;
196
+ try { account = loadConfig().codexAccounts?.find(row => row.id === id); } catch { account = undefined; }
197
+ return account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(id);
198
+ }
199
+
186
200
  /**
187
201
  * Validation metadata that survives a credential write.
188
202
  *
@@ -1055,7 +1069,7 @@ export async function forceRefreshCodexPoolToken(
1055
1069
  if (isTerminalCodexPoolRefreshFailure(error) || isOperationalCodexPoolRefreshFailure(error)) {
1056
1070
  if (isTerminalCodexPoolRefreshFailure(error)) clearCodexPoolRefreshFailure(id);
1057
1071
  } else {
1058
- noteCodexPoolRefreshFailure(id, classifyCodexPoolRefreshFailureReason(error), undefined, refreshFence);
1072
+ noteCodexPoolRefreshFailure(id, classifyCodexPoolRefreshFailureReason(error), undefined, refreshFence, poolAccountLogLabel);
1059
1073
  }
1060
1074
  settle({ kind: "failed", error });
1061
1075
  },
@@ -1376,7 +1390,7 @@ async function resolveCodexToken(
1376
1390
  .catch(() => undefined);
1377
1391
  const failure = classifyChatgptRefreshFailure(res.status, body?.displaySafe ? body.text : "");
1378
1392
  const { reason } = failure;
1379
- noteChatgptRefreshFailure("pool", res.status, failure);
1393
+ noteChatgptRefreshFailure("pool", res.status, failure, poolAccountLogLabel(id));
1380
1394
  throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`, res.status, failure.code);
1381
1395
  }
1382
1396
  const data = (await res.json()) as { access_token: string; refresh_token?: string; expires_in: number };
@@ -182,11 +182,12 @@ async function runCodexRestart(io: CodexRestartServiceIo): Promise<CodexRestartR
182
182
  // Decided here, before the early returns, because the model picker lives in the DESKTOP
183
183
  // app: "no app-server is running" is not a reason to leave a stale roster on screen.
184
184
  //
185
- // The shell restarts only when its picker predates the catalog (#4761). Quitting it
186
- // discards composer drafts, unsent picker selections and pending approvals, and when
187
- // the picker is already current the app-server restart below is all the operator
188
- // needs: the desktop respawns its app-server child, which rereads config and catalog.
189
- // An unreadable picker state takes the full restart, the behaviour before #4761.
185
+ // The shell gets a restart attempt whenever its picker cannot be proven current: at
186
+ // or before the catalog mtime it is stale, and newer is unknown because an earlier
187
+ // sync may have rebound an already-existing catalog file (#4761). Quitting it
188
+ // discards composer drafts, unsent picker selections and pending approvals. The
189
+ // attempt is explicit, not a guarantee: the restart helper can refuse or partially
190
+ // fail, and desktopApp reports that outcome.
190
191
  //
191
192
  // allowHandoff is false and that is deliberate. The handoff waits for the CALLING
192
193
  // process to exit, and this runs inside a long-lived proxy that does not, so every
@@ -98,6 +98,7 @@ import { getMainReserveAuthorization, isMainReserveAuthorizationLive, nativeUser
98
98
  import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
99
99
  import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
100
100
  import { CODEX_MAIN_DUPLICATE_ACTION, configuredCodexMainDuplicate } from "./collision-identity";
101
+ import { AgentAssertionUnsupportedError, authorizationCredential, hasAgentAssertionAuthorization } from "../lib/authorization-scheme";
101
102
 
102
103
  /**
103
104
  * A request-owned bearer cannot inspect the physical main credential for its plan, but cached
@@ -701,7 +702,7 @@ async function authorizeReserveCredential(
701
702
 
702
703
  function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAccountId: string } {
703
704
  return {
704
- accessToken: headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim() ?? "",
705
+ accessToken: authorizationCredential(headers) ?? "",
705
706
  chatgptAccountId: headers.get("chatgpt-account-id") ?? "",
706
707
  };
707
708
  }
@@ -716,7 +717,7 @@ function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAcc
716
717
  export function callerCodexWorkspaceAccountId(headers: Headers): string | undefined {
717
718
  const explicit = headers.get("chatgpt-account-id");
718
719
  if (explicit) return explicit;
719
- const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
720
+ const bearer = authorizationCredential(headers);
720
721
  return bearer ? extractAccountId(undefined, bearer) : undefined;
721
722
  }
722
723
 
@@ -807,7 +808,7 @@ function assertMainAccountPolicy(
807
808
 
808
809
  /** No auth-file I/O: an unsigned claim alone never identifies a caller as stored main. */
809
810
  function callerMatchesObservedMain(headers: Headers): boolean {
810
- const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
811
+ const bearer = authorizationCredential(headers);
811
812
  if (!bearer) return false;
812
813
  const effectiveAccountId = headers.get("chatgpt-account-id")
813
814
  ?? extractAccountId(undefined, bearer);
@@ -831,7 +832,7 @@ function sameCredentialMaterial(a: string, b: string): boolean {
831
832
  * cooldown — the safe direction.
832
833
  */
833
834
  function callerIsCooledPoolAccount(headers: Headers, config: OcxConfig, accountId: string): boolean {
834
- const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
835
+ const bearer = authorizationCredential(headers);
835
836
  if (!bearer) return true;
836
837
  const callerAccountId = headers.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
837
838
  if (callerAccountId === undefined) return true;
@@ -1767,7 +1768,7 @@ export class CodexMainSubstitutionUnavailableError extends Error {
1767
1768
 
1768
1769
  /** Dispatch identity comes only from an owned observation of the selected credential. */
1769
1770
  function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
1770
- const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
1771
+ const bearer = authorizationCredential(selected);
1771
1772
  if (!bearer) return undefined;
1772
1773
  const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
1773
1774
  return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
@@ -1792,6 +1793,8 @@ export function materializeCodexUpstreamAuth(
1792
1793
  ctx: CodexAuthContext,
1793
1794
  options: CodexAuthMaterializationOptions = {},
1794
1795
  ): Headers {
1796
+ // assertion_only must never be answered with a pool, stored-main, or other substituted Bearer.
1797
+ if (hasAgentAssertionAuthorization(headers)) throw new AgentAssertionUnsupportedError();
1795
1798
  if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1796
1799
  const selected = new Headers();
1797
1800
  for (const name of FORWARD_HEADERS) {
@@ -1814,7 +1817,7 @@ export function materializeCodexUpstreamAuth(
1814
1817
  }
1815
1818
  if (ctx.kind === "main" && options.substituteMainCredential !== true
1816
1819
  && !selected.has("chatgpt-account-id")) {
1817
- const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
1820
+ const bearer = authorizationCredential(selected);
1818
1821
  const accountId = bearer ? extractAccountId(undefined, bearer) : undefined;
1819
1822
  if (accountId) selected.set("chatgpt-account-id", accountId);
1820
1823
  }
@@ -1892,6 +1895,7 @@ export async function materializeCodexUpstreamAuthAsync(
1892
1895
  ctx: CodexAuthContext,
1893
1896
  options: CodexAuthMaterializationOptions = {},
1894
1897
  ): Promise<Headers> {
1898
+ if (hasAgentAssertionAuthorization(headers)) throw new AgentAssertionUnsupportedError();
1895
1899
  if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1896
1900
  if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
1897
1901
  return materializeReserveUpstreamAuth(headers, ctx, options);
@@ -55,6 +55,12 @@ import {
55
55
  export function nativeEffortClamp(slug: string, effort: string | undefined): string | null {
56
56
  if (!effort || (effort !== "max" && effort !== "ultra")) return null;
57
57
  if (slug.includes("/")) return null; // routed models map efforts in their adapters
58
+ // `ultra` is a client-facing catalog choice (max reasoning plus proactive delegation) that
59
+ // codex-rs converts to `max` before any request (core/src/client.rs
60
+ // `reasoning_effort_for_request`). The ChatGPT backend rejects it with HTTP 400 even for a
61
+ // model whose pinned ladder lists it (#7590), so it is never a native wire value: fold it to
62
+ // `max`, then clamp `max` like a direct selection.
63
+ const wire = "max";
58
64
  const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
59
65
  const levels = Array.isArray(entry?.supported_reasoning_levels)
60
66
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
@@ -65,10 +71,11 @@ export function nativeEffortClamp(slug: string, effort: string | undefined): str
65
71
  // a retired slug a client still asks for, and future old-ladder slugs)
66
72
  // really stops at xhigh — the ChatGPT backend error names exactly none..xhigh — so clamp
67
73
  // the synthetic top tier.
68
- return isGpt56NativeSlug(slug) ? null : "xhigh";
74
+ if (!isGpt56NativeSlug(slug)) return "xhigh";
75
+ return effort === wire ? null : wire;
69
76
  }
70
77
  const supported = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
71
- if (supported.includes(effort)) return null;
78
+ if (supported.includes(wire)) return effort === wire ? null : wire;
72
79
  const rank = ["minimal", "low", "medium", "high", "xhigh", "max"];
73
80
  const highest = supported
74
81
  .filter(e => rank.includes(e))
@@ -119,6 +119,7 @@ export interface CatalogGatherProviderModelOutcome {
119
119
  readonly state: "authoritative" | "degraded";
120
120
  }
121
121
  export interface ModelsAuthResolution {
122
+ readonly workbuddy?: import("../../oauth/types").WorkBuddyOAuthMetadata;
122
123
  readonly apiKey: string | undefined;
123
124
  readonly observed: boolean;
124
125
  readonly oauthAccountId?: string;
@@ -334,8 +335,9 @@ export function captureModelsRequest(
334
335
  name: string,
335
336
  provider: OcxProviderConfig,
336
337
  oauthApiBaseUrl: string | undefined,
338
+ workbuddy?: import("../../oauth/types").WorkBuddyOAuthMetadata,
337
339
  ): CapturedModelsRequest {
338
- const observed = { oauthApiBaseUrl };
340
+ const observed = { oauthApiBaseUrl, workbuddy };
339
341
  const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
340
342
  const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
341
343
  const method = withoutCredential.method ?? "GET";
@@ -400,7 +402,7 @@ export function captureProviderGather(
400
402
  : authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
401
403
  ? getOAuthCredentialApiBaseUrl(name)
402
404
  : undefined;
403
- const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
405
+ const request = captureModelsRequest(name, provider, oauthApiBaseUrl, observedAuth?.workbuddy);
404
406
  const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
405
407
  ? getAccountSet(name)?.activeAccountId
406
408
  : undefined;
@@ -809,6 +809,10 @@ export function normalizeRoutedCatalogEntry(
809
809
  delete entry.multi_agent_version;
810
810
  delete entry.multi_agent_reasoning_effort;
811
811
  delete entry.use_responses_lite;
812
+ // Codex 0.164 sends positional `configuration_update` effort items only for rows declaring this
813
+ // and freezes the request-level effort for the context window. A routed provider never
814
+ // negotiated that contract, so a row cloned from a GPT-6 template must not inherit it.
815
+ delete entry.supports_reasoning_effort_updates;
812
816
  delete entry.supports_websockets;
813
817
  // Routed rows cloned from native templates must not inherit OpenAI-only experimental context
814
818
  // delivery. Codex reads the flag as "this model accepts experimental context history" and drives