@bitkyc08/opencodex 2.19.0 → 2.20.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 (119) hide show
  1. package/gui/dist/assets/index-DF_UFrGS.css +1 -0
  2. package/gui/dist/assets/index-DSK3S5HY.js +76 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/mimo-free.ts +17 -0
  6. package/src/adapters/openai-chat.ts +367 -32
  7. package/src/adapters/registry.ts +144 -0
  8. package/src/chat/inbound.ts +13 -7
  9. package/src/cli/claude.ts +2 -1
  10. package/src/cli/combo.ts +3 -0
  11. package/src/cli/dispatch.ts +8 -0
  12. package/src/cli/export-command.ts +2 -2
  13. package/src/cli/help.ts +2 -0
  14. package/src/cli/index.ts +3 -2
  15. package/src/cli/lab.ts +135 -1
  16. package/src/cli/minimax.ts +491 -0
  17. package/src/cli/models-runtime.ts +22 -1
  18. package/src/cli/models.ts +67 -2
  19. package/src/cli/opencode.ts +2 -1
  20. package/src/cli/registry.ts +22 -2
  21. package/src/clients/config-export.ts +125 -7
  22. package/src/codex/app-server-processes.ts +57 -2
  23. package/src/codex/app-server-restart-service.ts +232 -0
  24. package/src/codex/catalog/aggregation.ts +10 -1
  25. package/src/codex/catalog/effort.ts +15 -3
  26. package/src/codex/catalog/parsing.ts +3 -1
  27. package/src/codex/catalog/provider-fetch.ts +45 -5
  28. package/src/codex/catalog/sync.ts +74 -4
  29. package/src/codex/convergence.ts +2 -0
  30. package/src/combos/index.ts +1 -0
  31. package/src/combos/request.ts +30 -0
  32. package/src/combos/types.ts +6 -0
  33. package/src/config.ts +52 -0
  34. package/src/generated/compatibility-version.json +224 -76
  35. package/src/images/loop.ts +11 -1
  36. package/src/integrations/registry.ts +7 -0
  37. package/src/lab/conformance/jcs.ts +42 -2
  38. package/src/lab/conformance/negative-controls.ts +6 -2
  39. package/src/lab/conformance/runner.ts +16 -5
  40. package/src/lab/fabric/observe.ts +49 -14
  41. package/src/lab/index.ts +16 -0
  42. package/src/lab/ledger/purge.ts +152 -83
  43. package/src/lab/ledger/store.ts +168 -54
  44. package/src/lab/observe/from-conformance.ts +8 -6
  45. package/src/lab/observe/from-live.ts +8 -2
  46. package/src/lab/paths.ts +23 -0
  47. package/src/lab/public/bundle.ts +217 -0
  48. package/src/lab/public/community-authority.ts +175 -0
  49. package/src/lab/public/community-files.ts +29 -0
  50. package/src/lab/public/community.ts +479 -0
  51. package/src/lab/public/file-safety.ts +155 -0
  52. package/src/lab/public/ids.ts +26 -0
  53. package/src/lab/public/index.ts +16 -0
  54. package/src/lab/public/mutation-lock.ts +424 -0
  55. package/src/lab/public/operator.ts +353 -0
  56. package/src/lab/public/origin-purge.ts +79 -0
  57. package/src/lab/public/origin.ts +203 -0
  58. package/src/lab/public/privacy.ts +143 -0
  59. package/src/lab/public/private-file.ts +261 -0
  60. package/src/lab/public/project.ts +124 -0
  61. package/src/lab/public/purge-test-fault.ts +21 -0
  62. package/src/lab/public/purge.ts +223 -0
  63. package/src/lab/public/registry.ts +44 -0
  64. package/src/lab/public/revocation.ts +252 -0
  65. package/src/lab/public/signature.ts +219 -0
  66. package/src/lab/public/storage.ts +105 -0
  67. package/src/lab/public/strict-json.ts +206 -0
  68. package/src/lab/public/time.ts +26 -0
  69. package/src/lab/public/types.ts +172 -0
  70. package/src/lab/public/validate.ts +391 -0
  71. package/src/lib/codex-restart-contract.ts +120 -0
  72. package/src/lib/lab-activation.ts +109 -47
  73. package/src/lib/lab-live-pinned-sender.ts +16 -5
  74. package/src/lib/pinned-http.ts +70 -16
  75. package/src/lib/self-launch-argv.ts +15 -0
  76. package/src/lib/state-store-registrations.ts +2 -0
  77. package/src/lib/upstream-reachability.ts +4 -0
  78. package/src/lib/windows-elevation.ts +10 -1
  79. package/src/providers/derive.ts +24 -4
  80. package/src/providers/registry.ts +7 -3
  81. package/src/providers/request-pacing.ts +310 -0
  82. package/src/providers/service-tier.ts +143 -0
  83. package/src/providers/static-model-discovery.ts +86 -0
  84. package/src/reasoning-effort.ts +27 -1
  85. package/src/router.ts +23 -6
  86. package/src/routing/capability.ts +4 -2
  87. package/src/routing/compatibility/behavior.ts +5 -1
  88. package/src/server/adapter-resolve.ts +2 -32
  89. package/src/server/auth-cors.ts +8 -0
  90. package/src/server/chat-completions.ts +74 -36
  91. package/src/server/chat-native-sse.ts +331 -0
  92. package/src/server/chat-native.ts +371 -0
  93. package/src/server/management/combo-routes.ts +16 -2
  94. package/src/server/management/config-routes.ts +6 -4
  95. package/src/server/management/context.ts +17 -0
  96. package/src/server/management/lab-routes.ts +181 -19
  97. package/src/server/management/model-routes.ts +76 -2
  98. package/src/server/management/model-rows.ts +8 -0
  99. package/src/server/management/provider-capability-config.ts +48 -0
  100. package/src/server/management/provider-routes.ts +76 -4
  101. package/src/server/management/system-restart.ts +4 -2
  102. package/src/server/management/system-routes.ts +38 -0
  103. package/src/server/relay.ts +17 -3
  104. package/src/server/responses/compact.ts +4 -1
  105. package/src/server/responses/core.ts +257 -46
  106. package/src/server/responses/empty-completion-guard.ts +276 -0
  107. package/src/server/responses/fetch-helpers.ts +35 -4
  108. package/src/server/responses/pacing-overload.ts +13 -0
  109. package/src/server/responses/policy-fallback.ts +16 -2
  110. package/src/server/responses/terminal-guard.ts +1 -1
  111. package/src/server/responses/upstream-error.ts +5 -0
  112. package/src/server/responses.ts +17 -2
  113. package/src/types.ts +66 -4
  114. package/src/update/index.ts +6 -5
  115. package/src/update/job.ts +5 -6
  116. package/src/update/notify.ts +5 -3
  117. package/src/usage/log.ts +11 -1
  118. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  119. package/gui/dist/assets/index-D_JUZLEC.js +0 -76
@@ -29,6 +29,7 @@ import { labAutomationPolicyPath } from "../lab/paths";
29
29
  import type { OcxConfig } from "../types";
30
30
  import { LabAutomationError } from "../lab/automation/types";
31
31
  import { registerLabPassiveRouteLinker } from "./lab-passive-linker-registration";
32
+ import { registerCurrentServerResourceCleanup } from "./server-resource-ownership";
32
33
  import { setCompatibilityEvidenceProvider } from "../routing/compatibility/provider-slot";
33
34
  import { labCompatibilityEvidenceProvider } from "../routing/compatibility/lab-evidence-provider";
34
35
  import {
@@ -37,8 +38,18 @@ import {
37
38
  } from "../lab/automation/orchestrator";
38
39
  import { createProductionLabRouteExecutor } from "./lab-live-route-production";
39
40
 
41
+ interface LabRuntimeBinding {
42
+ release(): void;
43
+ }
44
+
45
+ interface LabActivationRecord {
46
+ staticDetach: Array<() => void>;
47
+ runtime: LabRuntimeBinding | null;
48
+ seenRuntimeConfigs: WeakSet<OcxConfig>;
49
+ }
50
+
40
51
  /** Activation records keyed by configDir, so one process can own several configs. */
41
- const activated = new Map<string, Array<() => void>>();
52
+ const activated = new Map<string, LabActivationRecord>();
42
53
 
43
54
  const activationKey = (configDir?: string): string => configDir ?? "";
44
55
 
@@ -83,60 +94,110 @@ export function labActivationRequired(config: OcxConfig, configDir?: string): bo
83
94
  return labAutomationEnabledOnDisk(configDir);
84
95
  }
85
96
 
97
+ function startAutomationIfEnabled(configDir?: string): void {
98
+ if (!labAutomationEnabledOnDisk(configDir)) return;
99
+ try {
100
+ startLabAutomationScheduler(configDir);
101
+ } catch (err) {
102
+ // Neither a malformed automation file nor a busy state lock may take the proxy down
103
+ // at startup. Lab automation stays off for this run; routing, evidence, and every
104
+ // other subsystem keep working.
105
+ //
106
+ // The two causes get different messages because they need different actions, and a
107
+ // lock-contention failure reported as "invalid config" sends the operator to fix a
108
+ // file that is fine. Contention can also stall startup by up to the 5s lock wait.
109
+ const code = err instanceof LabAutomationError ? err.code : null;
110
+ if (code === "state_lock_busy" || code === "state_lock_failed") {
111
+ console.warn(
112
+ "[lab] Lab automation did not start: another process holds the automation state lock."
113
+ + " Automation stays off for this run and will be retried on the next start.",
114
+ );
115
+ } else {
116
+ console.warn(
117
+ "[lab] Lab automation is disabled for this run because its configuration could not be"
118
+ + " loaded:",
119
+ err instanceof Error ? err.message : err,
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ function installLabAutomationRuntime(
126
+ record: LabActivationRecord,
127
+ config: OcxConfig,
128
+ configDir?: string,
129
+ ): void {
130
+ const previous = record.runtime;
131
+ const routeExecutor = createProductionLabRouteExecutor({ configDir, loadConfig: () => config });
132
+ const releaseDispatchDeps = setLabAutomationDispatchDeps({
133
+ configDir,
134
+ loadConfig: () => config,
135
+ routeExecutor,
136
+ });
137
+
138
+ let released = false;
139
+ let detachOwnerCleanup = () => {};
140
+ const binding: LabRuntimeBinding = {
141
+ release() {
142
+ if (released) return;
143
+ released = true;
144
+ detachOwnerCleanup();
145
+ releaseDispatchDeps();
146
+ if (record.runtime === binding) record.runtime = null;
147
+ },
148
+ };
149
+ // `setLabAutomationDispatchDeps` already registers its own owner cleanup. This second
150
+ // receipt only keeps the activation record in sync with that owner-scoped lifetime, so a
151
+ // later same-process server can see that the static Lab slots survived but CL-08 authority
152
+ // did not. The release is idempotent, so cleanup order does not matter.
153
+ detachOwnerCleanup = registerCurrentServerResourceCleanup(binding.release);
154
+
155
+ // Install the successor before releasing the predecessor. The dispatcher token check then
156
+ // makes the predecessor release a no-op for the successor scheduler/authority.
157
+ record.runtime = binding;
158
+ record.seenRuntimeConfigs.add(config);
159
+ previous?.release();
160
+ }
161
+
86
162
  /**
87
- * Register Lab into the core slots. Idempotent per configDir and safe to call again after
88
- * a routing profile is created at runtime.
163
+ * Register Lab into the core slots. Static activation is idempotent per configDir. The
164
+ * server-owned CL-08 runtime binding is refreshed when its prior owner ended or when a new
165
+ * server instance arrives with a config object that has not owned this activation before.
89
166
  */
90
167
  export function activateLab(config: OcxConfig, configDir?: string): void {
91
168
  const key = activationKey(configDir);
92
- // INVARIANT: activation is all-or-nothing and reason-independent. Every slot is
93
- // registered here regardless of WHY activation was required, which is what makes this
94
- // key safe as configDir alone -- an automation-only activation still installs the
95
- // compatibility provider a later profile needs. If any registration ever becomes
96
- // conditional on the activation reason, this key must include that reason, or the early
97
- // return will silently skip it forever.
98
- if (activated.has(key)) return;
99
-
100
- const detach: Array<() => void> = [];
101
- detach.push(registerLabPassiveRouteLinker(configDir));
102
- detach.push(setCompatibilityEvidenceProvider(labCompatibilityEvidenceProvider));
169
+ const existing = activated.get(key);
170
+ if (existing) {
171
+ // A released predecessor leaves the static Lab slots resident but removes dispatch
172
+ // authority and its scheduler. A live successor uses a fresh config object. Rebind in
173
+ // either case, but never let an older already-seen server steal authority back from a
174
+ // newer successor merely because it receives another management request.
175
+ if (existing.runtime === null || !existing.seenRuntimeConfigs.has(config)) {
176
+ installLabAutomationRuntime(existing, config, configDir);
177
+ startAutomationIfEnabled(configDir);
178
+ }
179
+ return;
180
+ }
103
181
 
104
- const routeExecutor = createProductionLabRouteExecutor({ configDir, loadConfig: () => config });
105
- detach.push(setLabAutomationDispatchDeps({ configDir, loadConfig: () => config, routeExecutor }));
182
+ // INVARIANT: static activation is all-or-nothing and reason-independent. Every static
183
+ // slot is registered here regardless of WHY activation was required, so automation-only
184
+ // activation still installs the compatibility provider a later profile needs.
185
+ const record: LabActivationRecord = {
186
+ staticDetach: [],
187
+ runtime: null,
188
+ seenRuntimeConfigs: new WeakSet<OcxConfig>(),
189
+ };
190
+ record.staticDetach.push(registerLabPassiveRouteLinker(configDir));
191
+ record.staticDetach.push(setCompatibilityEvidenceProvider(labCompatibilityEvidenceProvider));
192
+ installLabAutomationRuntime(record, config, configDir);
106
193
 
107
194
  // Record the activation BEFORE the scheduler start. startLabAutomationScheduler runs the
108
195
  // full automation normalizer, which throws on any field violation, and this call sits on
109
196
  // the startup path of every install that has a routing profile. Storing the record first
110
197
  // means a throw cannot orphan the detach receipts and leave slots registered with no
111
- // activation record -- which would let a later activateLab register them a second time.
112
- activated.set(key, detach);
113
-
114
- if (labAutomationEnabledOnDisk(configDir)) {
115
- try {
116
- startLabAutomationScheduler(configDir);
117
- } catch (err) {
118
- // Neither a malformed automation file nor a busy state lock may take the proxy down
119
- // at startup. Lab automation stays off for this run; routing, evidence, and every
120
- // other subsystem keep working.
121
- //
122
- // The two causes get different messages because they need different actions, and a
123
- // lock-contention failure reported as "invalid config" sends the operator to fix a
124
- // file that is fine. Contention can also stall startup by up to the 5s lock wait.
125
- const code = err instanceof LabAutomationError ? err.code : null;
126
- if (code === "state_lock_busy" || code === "state_lock_failed") {
127
- console.warn(
128
- "[lab] Lab automation did not start: another process holds the automation state lock."
129
- + " Automation stays off for this run and will be retried on the next start.",
130
- );
131
- } else {
132
- console.warn(
133
- "[lab] Lab automation is disabled for this run because its configuration could not be"
134
- + " loaded:",
135
- err instanceof Error ? err.message : err,
136
- );
137
- }
138
- }
139
- }
198
+ // activation record, which would let a later activateLab register them a second time.
199
+ activated.set(key, record);
200
+ startAutomationIfEnabled(configDir);
140
201
  }
141
202
 
142
203
  /** True when this configDir has been activated. */
@@ -152,9 +213,10 @@ export function isLabActivated(configDir?: string): boolean {
152
213
  * users who never opted in.
153
214
  */
154
215
  export function resetLabActivationForTests(): void {
155
- for (const [key, detach] of [...activated]) {
216
+ for (const [key, record] of [...activated]) {
156
217
  activated.delete(key);
157
- for (const release of [...detach].reverse()) {
218
+ try { record.runtime?.release(); } catch { /* teardown is best-effort */ }
219
+ for (const release of [...record.staticDetach].reverse()) {
158
220
  try { release(); } catch { /* teardown is best-effort */ }
159
221
  }
160
222
  }
@@ -19,22 +19,33 @@ export function createLabAuthorizedPinnedSender(
19
19
  headers,
20
20
  maxBytes: limits.maxOutputBytes,
21
21
  connectTimeoutMs: limits.connectTimeoutMs,
22
- idleTimeoutMs: Math.min(limits.firstByteTimeoutMs, limits.inactivityTimeoutMs),
22
+ firstByteTimeoutMs: limits.firstByteTimeoutMs,
23
+ inactivityTimeoutMs: limits.inactivityTimeoutMs,
23
24
  rejectUnauthorized: true,
24
25
  context: "Lab provider response",
25
26
  };
26
27
  let response: Response;
28
+ let body: string;
27
29
  try {
28
30
  response = request.method === "POST"
29
31
  ? await pinnedHttpPost(url, pinned, request.body ?? "", signal, options)
30
32
  : await pinnedHttpGet(url, pinned, signal, options);
33
+ body = await response.text();
31
34
  } catch (error) {
32
- if (error instanceof PinnedHttpError && error.code === "connect_timeout") {
33
- throw new TransportError("connect_timeout", "pinned provider connection timed out");
35
+ if (error instanceof PinnedHttpError) {
36
+ switch (error.code) {
37
+ case "connect_timeout":
38
+ throw new TransportError("connect_timeout", "pinned provider connection timed out");
39
+ case "first_byte_timeout":
40
+ throw new TransportError("first_byte_timeout", "pinned provider first byte timed out");
41
+ case "inactivity_timeout":
42
+ throw new TransportError("inactivity_timeout", "pinned provider response stalled");
43
+ case "output_byte_limit":
44
+ throw new TransportError("output_byte_limit", "pinned provider response exceeded byte budget");
45
+ }
34
46
  }
35
47
  throw error;
36
48
  }
37
- const body = await response.text();
38
49
  const responseHeaders: Record<string, string> = {};
39
50
  for (const headerName of LAB_RESPONSE_HEADER_ALLOWLIST) {
40
51
  const value = response.headers.get(headerName);
@@ -42,4 +53,4 @@ export function createLabAuthorizedPinnedSender(
42
53
  }
43
54
  return { status: response.status, headers: responseHeaders, body };
44
55
  };
45
- }
56
+ }
@@ -3,7 +3,11 @@ import https from "node:https";
3
3
 
4
4
  export type PinnedAddress = { address: string; family: number };
5
5
 
6
- export type PinnedHttpErrorCode = "connect_timeout";
6
+ export type PinnedHttpErrorCode =
7
+ | "connect_timeout"
8
+ | "first_byte_timeout"
9
+ | "inactivity_timeout"
10
+ | "output_byte_limit";
7
11
 
8
12
  export class PinnedHttpError extends Error {
9
13
  override readonly name = "PinnedHttpError";
@@ -15,6 +19,11 @@ export interface PinnedHttpRequestOptions {
15
19
  maxBytes?: number;
16
20
  /** Optional deadline for establishing the TCP connection and, for HTTPS, completing TLS. */
17
21
  connectTimeoutMs?: number;
22
+ /** Optional deadline from connection establishment until response headers arrive. */
23
+ firstByteTimeoutMs?: number;
24
+ /** Optional maximum idle interval between response-body chunks. */
25
+ inactivityTimeoutMs?: number;
26
+ /** @deprecated Use firstByteTimeoutMs and inactivityTimeoutMs. */
18
27
  idleTimeoutMs?: number;
19
28
  rejectUnauthorized?: boolean;
20
29
  context?: string;
@@ -37,7 +46,11 @@ function pinnedHttpRequest(
37
46
  }
38
47
  const context = options?.context ?? "request";
39
48
  const connectTimeoutMs = options?.connectTimeoutMs;
40
- const idleTimeoutMs = options?.idleTimeoutMs ?? 60_000;
49
+ const legacyIdleTimeoutMs = options?.idleTimeoutMs ?? 60_000;
50
+ const usesLegacyIdleTimeout = options?.firstByteTimeoutMs === undefined
51
+ && options?.inactivityTimeoutMs === undefined;
52
+ const firstByteTimeoutMs = options?.firstByteTimeoutMs ?? legacyIdleTimeoutMs;
53
+ const inactivityTimeoutMs = options?.inactivityTimeoutMs ?? legacyIdleTimeoutMs;
41
54
  const maxBytes = options?.maxBytes;
42
55
  const headers = new Headers(options?.headers);
43
56
  headers.set("host", parsed.host);
@@ -56,17 +69,30 @@ function pinnedHttpRequest(
56
69
  let settled = false;
57
70
  let req: ClientRequest | undefined;
58
71
  let connectTimer: ReturnType<typeof setTimeout> | undefined;
72
+ let firstByteTimer: ReturnType<typeof setTimeout> | undefined;
59
73
  const clearConnectTimer = () => {
60
74
  if (connectTimer !== undefined) clearTimeout(connectTimer);
61
75
  connectTimer = undefined;
62
76
  };
77
+ const clearFirstByteTimer = () => {
78
+ if (firstByteTimer !== undefined) clearTimeout(firstByteTimer);
79
+ firstByteTimer = undefined;
80
+ };
63
81
  const fail = (error: unknown) => {
64
82
  clearConnectTimer();
83
+ clearFirstByteTimer();
65
84
  try { req?.destroy(); } catch { /* ignore */ }
66
85
  if (settled) return;
67
86
  settled = true;
68
87
  reject(error instanceof Error ? error : new Error(String(error)));
69
88
  };
89
+ const startFirstByteTimer = () => {
90
+ clearFirstByteTimer();
91
+ firstByteTimer = setTimeout(
92
+ () => fail(new PinnedHttpError("first_byte_timeout", `${context} first byte timed out`)),
93
+ firstByteTimeoutMs,
94
+ );
95
+ };
70
96
  const requestOptions: RequestOptions & { servername?: string } = {
71
97
  protocol: parsed.protocol,
72
98
  hostname: parsed.hostname,
@@ -101,6 +127,7 @@ function pinnedHttpRequest(
101
127
 
102
128
  const onResponse = (response: IncomingMessage) => {
103
129
  clearConnectTimer();
130
+ clearFirstByteTimer();
104
131
  const status = response.statusCode ?? 0;
105
132
  const responseHeaders = new Headers();
106
133
  for (const [key, value] of Object.entries(response.headers)) {
@@ -124,28 +151,35 @@ function pinnedHttpRequest(
124
151
  let received = 0;
125
152
  const stream = new ReadableStream<Uint8Array>({
126
153
  start(controller) {
127
- response.setTimeout(idleTimeoutMs, () => {
128
- const error = new Error(`${context} stalled`);
129
- fail(error);
154
+ let bodySettled = false;
155
+ const failBody = (error: Error) => {
156
+ if (bodySettled) return;
157
+ bodySettled = true;
130
158
  try { controller.error(error); } catch { /* closed */ }
159
+ try { response.destroy(); } catch { /* ignore */ }
160
+ try { req?.destroy(); } catch { /* ignore */ }
161
+ };
162
+
163
+ response.setTimeout(inactivityTimeoutMs, () => {
164
+ failBody(new PinnedHttpError("inactivity_timeout", `${context} stalled`));
131
165
  });
132
166
  response.on("data", (chunk: Buffer | string) => {
167
+ if (bodySettled) return;
133
168
  const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
134
169
  received += buffer.byteLength;
135
170
  if (maxBytes !== undefined && received > maxBytes) {
136
- const error = new Error(`${context} exceeds ${maxBytes} byte cap`);
137
- fail(error);
138
- try { controller.error(error); } catch { /* closed */ }
171
+ failBody(new PinnedHttpError("output_byte_limit", `${context} exceeds ${maxBytes} byte cap`));
139
172
  return;
140
173
  }
141
174
  try { controller.enqueue(buffer); } catch { /* closed */ }
142
175
  });
143
176
  response.on("end", () => {
177
+ if (bodySettled) return;
178
+ bodySettled = true;
144
179
  try { controller.close(); } catch { /* closed */ }
145
180
  });
146
181
  response.on("error", (error: Error) => {
147
- fail(error);
148
- try { controller.error(error); } catch { /* closed */ }
182
+ failBody(error);
149
183
  });
150
184
  },
151
185
  cancel() {
@@ -163,20 +197,40 @@ function pinnedHttpRequest(
163
197
  const onAbort = () => fail(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
164
198
  signal?.addEventListener("abort", onAbort, { once: true });
165
199
  req.on("socket", (socket) => {
166
- if (!socket.connecting || connectTimeoutMs === undefined) return;
167
200
  const connectedEvent = parsed.protocol === "https:" ? "secureConnect" : "connect";
168
- connectTimer = setTimeout(() => fail(new PinnedHttpError("connect_timeout", `${context} connect timed out`)), connectTimeoutMs);
169
- socket.once(connectedEvent, clearConnectTimer);
170
- socket.once("error", clearConnectTimer);
171
- socket.once("close", clearConnectTimer);
201
+ if (!socket.connecting) {
202
+ if (!usesLegacyIdleTimeout) startFirstByteTimer();
203
+ return;
204
+ }
205
+ if (connectTimeoutMs !== undefined) {
206
+ connectTimer = setTimeout(
207
+ () => fail(new PinnedHttpError("connect_timeout", `${context} connect timed out`)),
208
+ connectTimeoutMs,
209
+ );
210
+ }
211
+ socket.once(connectedEvent, () => {
212
+ clearConnectTimer();
213
+ if (!usesLegacyIdleTimeout) startFirstByteTimer();
214
+ });
215
+ socket.once("error", () => {
216
+ clearConnectTimer();
217
+ clearFirstByteTimer();
218
+ });
219
+ socket.once("close", () => {
220
+ clearConnectTimer();
221
+ clearFirstByteTimer();
222
+ });
172
223
  });
173
- req.setTimeout(idleTimeoutMs, () => fail(new Error(`${context} timed out`)));
224
+ if (usesLegacyIdleTimeout) {
225
+ req.setTimeout(legacyIdleTimeoutMs, () => fail(new Error(`${context} timed out`)));
226
+ }
174
227
  req.on("error", error => {
175
228
  signal?.removeEventListener("abort", onAbort);
176
229
  fail(error);
177
230
  });
178
231
  req.on("close", () => {
179
232
  clearConnectTimer();
233
+ clearFirstByteTimer();
180
234
  signal?.removeEventListener("abort", onAbort);
181
235
  });
182
236
  req.end(body);
@@ -0,0 +1,15 @@
1
+ interface SelfLaunchArgvOptions {
2
+ isStandaloneExecutable?: boolean;
3
+ sourceEntrypoint?: string;
4
+ }
5
+
6
+ /** Build argv for re-entering the current CLI in compiled or source mode. */
7
+ export function selfLaunchArgv(
8
+ args: readonly string[],
9
+ options: SelfLaunchArgvOptions = {},
10
+ ): string[] {
11
+ const bunStandalone = (Bun as unknown as { isStandaloneExecutable?: boolean }).isStandaloneExecutable;
12
+ const isStandaloneExecutable = options.isStandaloneExecutable ?? Boolean(bunStandalone);
13
+ if (isStandaloneExecutable) return [...args];
14
+ return [options.sourceEntrypoint ?? process.argv[1], ...args];
15
+ }
@@ -34,6 +34,7 @@ import { sweepExpiredAnthropicRoutingHealth } from "../oauth/anthropic-routing";
34
34
  import { listLiveOAuthAccountKeys, reconcileOAuthReauthState } from "../oauth/store";
35
35
  import { reconcileGuardianBackoff } from "../oauth/token-guardian";
36
36
  import { sweepExpiredApiKeyCooldowns } from "../providers/key-failover";
37
+ import { reconcileProviderRequestPacing } from "../providers/request-pacing";
37
38
  import { sweepExpiredResponseStates } from "../responses/state";
38
39
  import { sweepExpiredAntigravityReplay } from "../adapters/google-antigravity-replay";
39
40
  import { reconcileProviderAccountQuotaRows } from "../providers/quota";
@@ -75,6 +76,7 @@ export function buildGenerationContext(): GenerationContext {
75
76
  export const STATE_STORE_REGISTRATIONS = [
76
77
  { name: "subagent-model-health", sweepExpired: sweepExpiredSubagentModelHealth },
77
78
  { name: "api-key-cooldowns", sweepExpired: sweepExpiredApiKeyCooldowns },
79
+ { name: "provider-request-pacing", reconcileGeneration: reconcileProviderRequestPacing },
78
80
  {
79
81
  name: "combo-target-cooldowns",
80
82
  sweepExpired: sweepExpiredComboTargetCooldowns,
@@ -23,6 +23,7 @@
23
23
  * MUST stay a leaf module: imports nothing from server.ts or adapters.
24
24
  */
25
25
 
26
+ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
26
27
  import { UpstreamRetryEvidenceError } from "./upstream-retry";
27
28
 
28
29
  export const PRE_CONNECT_REACHABILITY_CODES = new Set([
@@ -68,6 +69,9 @@ export type TransportFailureKind = "timeout" | "connect_neutral" | "connect_erro
68
69
  * account-attributed behavior.
69
70
  */
70
71
  export function classifyTransportFailureKind(err: unknown): TransportFailureKind {
72
+ // A local pacing admission failure happened before transport classification and must
73
+ // remain a client-visible overload, not account or host health evidence.
74
+ if (err instanceof RequestPacingQueueOverloadError) throw err;
71
75
  const evidence = err instanceof UpstreamRetryEvidenceError ? err : undefined;
72
76
  const rejection = evidence ? evidence.cause : err;
73
77
  if (rejection instanceof Error && rejection.name === "TimeoutError") return "timeout";
@@ -175,7 +175,7 @@ export function assertTrustedSystemExecutableForTests(candidate: string, label:
175
175
  return assertTrustedSystemExecutable(candidate, label);
176
176
  }
177
177
 
178
- type ElevationExeOverrides = { powershell?: string; schtasks?: string };
178
+ type ElevationExeOverrides = { powershell?: string; schtasks?: string; taskkill?: string };
179
179
  let elevationExeOverridesForTests: ElevationExeOverrides | null = null;
180
180
 
181
181
  /**
@@ -211,6 +211,15 @@ export function resolveTrustedWindowsSchtasksExe(): string {
211
211
  return assertTrustedSystemExecutable(candidate, "schtasks.exe");
212
212
  }
213
213
 
214
+ /** Absolute path to System32\\taskkill.exe from a trusted system directory. */
215
+ export function resolveTrustedWindowsTaskkillExe(): string {
216
+ if (elevationExeOverridesForTests?.taskkill) {
217
+ return elevationExeOverridesForTests.taskkill;
218
+ }
219
+ const candidate = join(resolveTrustedWindowsSystemDirectory(), "taskkill.exe");
220
+ return assertTrustedSystemExecutable(candidate, "taskkill.exe");
221
+ }
222
+
214
223
  /** Stable machine-readable marker for a denied `schtasks /create`. Crosses the CLI→proxy boundary. */
215
224
  export const WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER =
216
225
  "OCX_ERROR_CODE=WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED";
@@ -1,10 +1,14 @@
1
1
  import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
2
  import {
3
3
  PROVIDER_REGISTRY,
4
- providerMatchesRegistryTransport,
5
4
  registryEntryForProviderDestination,
6
5
  type ProviderRegistryEntry,
7
6
  } from "./registry";
7
+ import {
8
+ providerMatchesRegistryTransportWithStaticGuards,
9
+ registryEntrySupportsLiveModelDiscovery,
10
+ repairStaticModelCatalogProvider,
11
+ } from "./static-model-discovery";
8
12
 
9
13
  export interface DerivedKeyLoginProvider {
10
14
  label: string;
@@ -204,6 +208,7 @@ export function applyDirectReasoningEffortContracts(
204
208
  * keep distinguishing local runtimes from API-key providers after the seed round-trip.
205
209
  */
206
210
  export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
211
+ const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
207
212
  return {
208
213
  adapter: entry.adapter,
209
214
  baseUrl: entry.baseUrl,
@@ -219,7 +224,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
219
224
  ...(entry.staticHeaders ? { headers: { ...entry.staticHeaders } } : {}),
220
225
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
221
226
  ...(entry.models ? { models: [...entry.models] } : {}),
222
- ...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
227
+ ...(liveModels !== undefined ? { liveModels } : {}),
223
228
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
224
229
  ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
225
230
  ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
@@ -263,6 +268,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
263
268
  for (const entry of PROVIDER_REGISTRY) {
264
269
  if (entry.authKind !== "key") continue;
265
270
  if (!entry.dashboardUrl) throw new Error(`Registry key provider missing dashboardUrl: ${entry.id}`);
271
+ const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
266
272
  out[entry.id] = {
267
273
  label: entry.label,
268
274
  baseUrl: entry.baseUrl,
@@ -272,7 +278,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
272
278
  ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
273
279
  dashboardUrl: entry.dashboardUrl,
274
280
  ...(entry.models ? { models: [...entry.models] } : {}),
275
- ...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
281
+ ...(liveModels !== undefined ? { liveModels } : {}),
276
282
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
277
283
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
278
284
  ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
@@ -359,6 +365,17 @@ function applyReasoningSummaryDefaults(
359
365
  };
360
366
  }
361
367
 
368
+ function applyServiceTierModelDefaults(
369
+ prov: OcxProviderConfig,
370
+ defaults: Readonly<Record<string, boolean>> | undefined,
371
+ ): void {
372
+ if (!defaults) return;
373
+ prov.modelSupportsServiceTier = {
374
+ ...defaults,
375
+ ...(prov.modelSupportsServiceTier ?? {}),
376
+ };
377
+ }
378
+
362
379
  /**
363
380
  * Last-resort enrichment for a provider whose NAME matches no registry id.
364
381
  *
@@ -378,7 +395,7 @@ function enrichReasoningSummariesByDestination(prov: OcxProviderConfig): void {
378
395
 
379
396
  export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
380
397
  const entry = PROVIDER_REGISTRY.find(row => row.id === name);
381
- if (!entry || !providerMatchesRegistryTransport(name, prov)) {
398
+ if (!entry || !providerMatchesRegistryTransportWithStaticGuards(name, prov)) {
382
399
  // Name lookup failed, but the row may still point at a vendor route we know. #1100 was
383
400
  // reported against a hand-added provider literally named "GLM": routing worked, yet every
384
401
  // piece of registry metadata was skipped because no registry id is called "GLM".
@@ -386,6 +403,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
386
403
  // which vendor endpoint is this row talking to — and is already restricted to fixed key
387
404
  // destinations, so a templated or overridable base URL cannot be claimed by it.
388
405
  enrichReasoningSummariesByDestination(prov);
406
+ applyServiceTierModelDefaults(prov, registryEntryForProviderDestination(prov)?.modelSupportsServiceTier);
389
407
  return;
390
408
  }
391
409
  const explicitDirectReasoning: DirectReasoningEffortOverrides = {
@@ -395,6 +413,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
395
413
  modelReasoningEffortMap: prov.modelReasoningEffortMap,
396
414
  };
397
415
  const seed = providerConfigSeed(entry);
416
+ repairStaticModelCatalogProvider(name, prov);
398
417
  if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
399
418
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
400
419
  if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
@@ -433,6 +452,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
433
452
  if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
434
453
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
435
454
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
455
+ applyServiceTierModelDefaults(prov, entry.modelSupportsServiceTier);
436
456
  // Registry-only repair policy (#938): fill only when the runtime provider has
437
457
  // no explicit policy, and deep-clone so saved/user values never alias the
438
458
  // registry constant.
@@ -200,13 +200,15 @@ export interface ProviderRegistryEntry {
200
200
  */
201
201
  requiresAdjacentResponsesToolResults?: boolean;
202
202
  /**
203
- * Registry default for the provider's Responses `service_tier` support; see
203
+ * Registry default for the provider's `service_tier` support; see
204
204
  * `OcxProviderConfig.supportsServiceTier`. Registry-only: backfilled (never
205
205
  * overriding) at enrich/route time and deliberately NOT seeded into saved
206
206
  * config, so an explicit user value stays distinguishable from the default
207
207
  * (and the canonical openai seed comparison keeps its exact key set).
208
208
  */
209
209
  supportsServiceTier?: boolean;
210
+ /** Registry default for exact model service-tier capability; explicit config keys win. */
211
+ modelSupportsServiceTier?: Record<string, boolean>;
210
212
  /** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
211
213
  preserveResponsesReasoningContent?: boolean;
212
214
  /** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
@@ -903,6 +905,7 @@ const CLINE_PASS_MODELS = [
903
905
  "cline-pass/mimo-v2.5",
904
906
  "cline-pass/mimo-v2.5-pro",
905
907
  "cline-pass/minimax-m3",
908
+ "cline-pass/qwen3.8-max",
906
909
  "cline-pass/qwen3.7-max",
907
910
  "cline-pass/qwen3.7-plus",
908
911
  ];
@@ -928,9 +931,10 @@ const CLINE_PASS_IMAGE_MODELS = new Set([
928
931
  "cline-pass/minimax-m3",
929
932
  "cline-pass/qwen3.7-plus",
930
933
  ]);
931
- const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODELS.filter(id => !CLINE_PASS_IMAGE_MODELS.has(id));
934
+ const CLINE_PASS_MODALITY_KNOWN_MODELS = CLINE_PASS_MODELS.filter(id => id !== "cline-pass/qwen3.8-max");
935
+ const CLINE_PASS_TEXT_ONLY_MODELS = CLINE_PASS_MODALITY_KNOWN_MODELS.filter(id => !CLINE_PASS_IMAGE_MODELS.has(id));
932
936
  const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
933
- CLINE_PASS_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
937
+ CLINE_PASS_MODALITY_KNOWN_MODELS.map(id => [id, CLINE_PASS_IMAGE_MODELS.has(id) ? ["text", "image"] : ["text"]]),
934
938
  );
935
939
 
936
940
  export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [