@bitkyc08/opencodex 2.66.0-preview.20260925 → 2.67.0-preview.20260926

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 (60) hide show
  1. package/bin/ocx.mjs +24 -11
  2. package/gui/dist/assets/{App-BVjA0T6O.js → App-B3VRRYSj.js} +15 -15
  3. package/gui/dist/assets/{App-gkoJIEOV.css → App-BJsT8Icc.css} +1 -1
  4. package/gui/dist/assets/{Tray-BCFtiWwD.js → Tray-TYAA0EQY.js} +1 -1
  5. package/gui/dist/assets/index-9Ju7nHsu.js +86 -0
  6. package/gui/dist/assets/index-DtNmX7hW.css +1 -0
  7. package/gui/dist/assets/{tray-data-C4FiNaxk.js → tray-data-xKfS2gSG.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/chat/inbound.ts +82 -3
  11. package/src/chat/outbound.ts +5 -3
  12. package/src/claude/desktop-first-party.ts +12 -3
  13. package/src/claude/first-party-settings.ts +97 -0
  14. package/src/claude/intercept/client-class.ts +18 -0
  15. package/src/claude/intercept/listener.ts +5 -0
  16. package/src/claude/intercept/runtime.ts +6 -0
  17. package/src/cli/capabilities.ts +12 -0
  18. package/src/cli/claude-desktop.ts +11 -6
  19. package/src/cli/claude.ts +25 -1
  20. package/src/cli/ensure-desired-integrations.ts +14 -1
  21. package/src/cli/integrations.ts +9 -1
  22. package/src/cli/uninstall-client-state.ts +6 -0
  23. package/src/cli/uninstall-integrations.ts +115 -0
  24. package/src/codex/history-job.ts +2 -1
  25. package/src/codex/main-account-hard-lock.ts +36 -19
  26. package/src/codex/quota.ts +11 -1
  27. package/src/combos/resolve.ts +11 -3
  28. package/src/config/load-degrade.ts +4 -0
  29. package/src/generated/compatibility-version.json +67 -43
  30. package/src/generated/worker-bundles.gen.ts +4 -0
  31. package/src/images/loop.ts +10 -3
  32. package/src/integrations/aside-profile-context.ts +17 -0
  33. package/src/lab/fabric/executor.ts +25 -3
  34. package/src/lab/fabric/producer-isolate.ts +199 -32
  35. package/src/lab/fabric/scratch.ts +9 -2
  36. package/src/lib/shadow-call.ts +11 -4
  37. package/src/lib/worker-embed.ts +33 -0
  38. package/src/oauth/generic-account-failover.ts +5 -3
  39. package/src/oauth/index.ts +19 -29
  40. package/src/oauth/store.ts +106 -34
  41. package/src/remote-control/workspace-rpc.ts +114 -19
  42. package/src/router.ts +7 -1
  43. package/src/server/chat-completions.ts +2 -1
  44. package/src/server/index/claude-intercept-lifecycle.ts +13 -0
  45. package/src/server/management/agent-settings-routes.ts +123 -5
  46. package/src/server/management/context.ts +4 -0
  47. package/src/server/management/native-integration-routes.ts +19 -8
  48. package/src/server/responses/core-normalize.ts +13 -1
  49. package/src/server/responses/request-prepare.ts +5 -3
  50. package/src/server/responses/sidecar-execution.ts +9 -2
  51. package/src/server/search.ts +28 -7
  52. package/src/storage/policy-job.ts +2 -1
  53. package/src/storage/restore-job.ts +2 -1
  54. package/src/types/config.ts +11 -5
  55. package/src/update/transactional-install.mjs +5 -1
  56. package/src/web-search/alpha-search.ts +71 -0
  57. package/src/web-search/devin-executor.ts +192 -0
  58. package/src/web-search/loop.ts +10 -3
  59. package/gui/dist/assets/index-ComVsdSk.css +0 -1
  60. package/gui/dist/assets/index-vO4dgfiU.js +0 -86
@@ -15,6 +15,25 @@ import { FabricTaskError } from "./types";
15
15
 
16
16
  const CHILD_ENTRY = join(dirname(fileURLToPath(import.meta.url)), "producer-child.ts");
17
17
 
18
+ /**
19
+ * Bounded drain window between a child's `exit` and our decision. `close` also
20
+ * waits for the child's stdio to end, and a descendant holding an inherited pipe
21
+ * can delay it forever — so a missing `close` must not keep the run pending.
22
+ * The bound only has to cover an ordinary stdio flush after process death: the
23
+ * kernel releases a dead child's pipe ends immediately, so a `close` slower
24
+ * than this almost always means a descendant still holds a pipe. A slow-but-
25
+ * normal pipe that outlives the bound costs an inconclusive verdict plus
26
+ * deferred scratch cleanup — a bounded price that does not grow with the wait.
27
+ */
28
+ const EXIT_DRAIN_MS = 250;
29
+
30
+ /**
31
+ * Bounded wait for exit/close after a parent-owned SIGKILL. Neither event is
32
+ * guaranteed — a child in uninterruptible sleep, or a kill() that failed, produces
33
+ * neither — and a latched killReason must not leave the run pending forever.
34
+ */
35
+ const KILL_CONFIRM_MS = 2_000;
36
+
18
37
  type FabricProducerIsolationLimits = {
19
38
  totalTimeoutMs: number;
20
39
  inactivityTimeoutMs: number;
@@ -80,6 +99,24 @@ function killChild(child: ChildProcess): void {
80
99
  }
81
100
  }
82
101
 
102
+ /** Whether the producer rejected while its child might still be running. */
103
+ export function isUnconfirmedProducerTermination(error: unknown): boolean {
104
+ return error instanceof FabricTaskError
105
+ && (error as FabricTaskError & { unconfirmedTermination?: boolean }).unconfirmedTermination === true;
106
+ }
107
+
108
+ /**
109
+ * The release signal attached to an unconfirmed-termination rejection: resolves
110
+ * once every monitored inherited stdio pipe reports its natural close. This is
111
+ * observation only: closing a pipe does not prove a descendant exited and must
112
+ * not authorize deletion. Undefined when nothing monitorable remained.
113
+ */
114
+ export function producerTerminationSignal(error: unknown): Promise<void> | undefined {
115
+ if (!error || typeof error !== "object") return undefined;
116
+ const signal = (error as { stdioRelease?: unknown }).stdioRelease;
117
+ return signal instanceof Promise ? signal : undefined;
118
+ }
119
+
83
120
  /** Run a fabric patch producer in an isolated child process with parent-owned timeouts. */
84
121
  export async function runIsolatedFabricProducer(request: IsolateRequest): Promise<IsolatedProducerResult> {
85
122
  const now = request.now ?? (() => Date.now());
@@ -110,8 +147,37 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
110
147
  let stderrBytes = 0;
111
148
  let settled = false;
112
149
  let childClosed = false;
150
+ let childExitedAt: number | undefined;
113
151
  let receivedResult: SyntheticPatchV1 | undefined;
114
152
  let killReason: FabricTaskError | undefined;
153
+ let reapTimer: ReturnType<typeof setTimeout> | undefined;
154
+ let killWatchdog: ReturnType<typeof setTimeout> | undefined;
155
+ // Resolves once every still-open inherited stdio pipe reports its natural
156
+ // close. This is not a writer-termination proof or scratch deletion lease.
157
+ // Undefined when no open pipe can be monitored.
158
+ let stdioReleaseSignal: Promise<void> | undefined;
159
+
160
+ // Keep monitorable pipes open but unref'd so they never extend process
161
+ // lifetime; a stream without unref() is destroyed instead, and its
162
+ // self-inflicted close must not count toward the release signal.
163
+ const armStdioRelease = (): void => {
164
+ const waiters: Promise<void>[] = [];
165
+ let unmonitorable = false;
166
+ for (const stream of [child.stdout, child.stderr]) {
167
+ if (!stream || stream.destroyed) continue;
168
+ const unref = (stream as unknown as { unref?: unknown }).unref;
169
+ if (typeof unref === "function") {
170
+ waiters.push(new Promise<void>((resolve) => stream.once("close", resolve)));
171
+ unref.call(stream);
172
+ } else {
173
+ unmonitorable = true;
174
+ try { stream.destroy(); } catch { /* already closed */ }
175
+ }
176
+ }
177
+ if (waiters.length > 0 && !unmonitorable) {
178
+ stdioReleaseSignal = Promise.all(waiters).then(() => undefined);
179
+ }
180
+ };
115
181
 
116
182
  const finish = (fn: () => void) => {
117
183
  // A latched failure owns settlement, but scratch cleanup must wait for close.
@@ -119,6 +185,8 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
119
185
  settled = true;
120
186
  clearTimeout(totalTimer);
121
187
  clearTimeout(inactivityTimer);
188
+ if (reapTimer) clearTimeout(reapTimer);
189
+ if (killWatchdog) clearTimeout(killWatchdog);
122
190
  if (killReason) reject(killReason);
123
191
  else fn();
124
192
  };
@@ -127,7 +195,31 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
127
195
  if (settled || killReason) return;
128
196
  killReason = error;
129
197
  if (childClosed) finish(() => reject(error));
130
- else killChild(child);
198
+ else {
199
+ killChild(child);
200
+ // SIGKILL does not guarantee exit/close: an uninterruptible child, or a
201
+ // kill() that failed, emits neither, and the latched killReason would
202
+ // otherwise keep this run pending forever. Bound the wait; on expiry
203
+ // reject with the original reason flagged unconfirmed so the caller
204
+ // defers scratch cleanup instead of racing a child that may live.
205
+ killWatchdog = setTimeout(() => {
206
+ if (childClosed || settled) return;
207
+ settled = true;
208
+ clearTimeout(totalTimer);
209
+ clearTimeout(inactivityTimer);
210
+ if (reapTimer) clearTimeout(reapTimer);
211
+ if (killWatchdog) {
212
+ clearTimeout(killWatchdog);
213
+ killWatchdog = undefined;
214
+ }
215
+ armStdioRelease();
216
+ try { child.unref(); } catch { /* fake children may lack unref */ }
217
+ const reason = killReason! as FabricTaskError & { unconfirmedTermination?: boolean; stdioRelease?: Promise<void> };
218
+ reason.unconfirmedTermination = true;
219
+ reason.stdioRelease = stdioReleaseSignal;
220
+ reject(killReason);
221
+ }, KILL_CONFIRM_MS);
222
+ }
131
223
  };
132
224
 
133
225
  const expiredDeadline = (at: number): FabricTaskError | undefined => {
@@ -163,7 +255,10 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
163
255
  try {
164
256
  const message = parseProducerProtocolLine(line);
165
257
  if (message.type === "activity" || message.type === "result") {
166
- const at = budgetNow();
258
+ // Bytes drained after `exit` are judged at the exit timestamp: the
259
+ // process met its budgets when it died, so the drain must not
260
+ // condemn data it wrote while still inside them.
261
+ const at = childExitedAt ?? budgetNow();
167
262
  const expired = expiredDeadline(at);
168
263
  if (expired) {
169
264
  settleTimeout(expired);
@@ -172,13 +267,12 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
172
267
  if (message.type === "activity") {
173
268
  lastActivityAt = request.now ? at : now();
174
269
  inactivityDeadline = at + request.inactivityTimeoutMs;
175
- armInactivity();
270
+ if (childExitedAt === undefined) armInactivity();
176
271
  return;
177
272
  }
178
273
  }
179
274
  if (message.type === "result") {
180
275
  receivedResult = message.patch;
181
- finish(() => resolve({ patch: message.patch, lastActivityAt }));
182
276
  return;
183
277
  }
184
278
  if (message.type === "error") {
@@ -193,8 +287,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
193
287
  : "harness_failure";
194
288
  const attribution = message.attribution === "environment" ? "environment" : "harness";
195
289
  const fabricError = new FabricTaskError(message.message, fabricCode, attribution);
196
- finish(() => reject(fabricError));
197
- killChild(child);
290
+ settleTimeout(fabricError);
198
291
  return;
199
292
  }
200
293
  } catch (error) {
@@ -228,8 +321,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
228
321
  });
229
322
 
230
323
  child.stdout?.on("error", (error) => {
231
- if (settled) return;
232
- finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
324
+ settleTimeout(new FabricTaskError(error.message, "harness_failure", "harness"));
233
325
  });
234
326
 
235
327
  child.stderr?.on("data", (chunk: Buffer | string) => {
@@ -244,24 +336,32 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
244
336
  });
245
337
 
246
338
  child.on("error", (error) => {
247
- finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
339
+ if (child.pid === undefined) {
340
+ // A spawn failure has no process to supervise and may never emit close.
341
+ childClosed = true;
342
+ finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
343
+ return;
344
+ }
345
+ settleTimeout(new FabricTaskError(error.message, "harness_failure", "harness"));
248
346
  });
249
347
 
250
348
  child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
251
- if (settled || killReason || error.code === "EPIPE") return;
252
- killChild(child);
253
- finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
349
+ if (error.code === "EPIPE") return;
350
+ settleTimeout(new FabricTaskError(error.message, "harness_failure", "harness"));
254
351
  });
255
352
 
256
- child.on("close", (code, signal) => {
257
- childClosed = true;
353
+ const decide = (code: number | null, signal: NodeJS.Signals | null, reaped = false) => {
258
354
  if (settled) return;
259
355
  if (killReason) {
260
- finish(() => reject(killReason!));
261
- return;
262
- }
263
- if (receivedResult) {
264
- finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
356
+ const reason = killReason as FabricTaskError & { unconfirmedTermination?: boolean; stdioRelease?: Promise<void> };
357
+ if (reaped) {
358
+ // The kill was answered by exit but the pipes stayed open: a
359
+ // descendant can still hold them, so scratch cleanup must defer to
360
+ // the same release contract as an unconfirmed kill.
361
+ reason.unconfirmedTermination = true;
362
+ reason.stdioRelease = stdioReleaseSignal;
363
+ }
364
+ finish(() => reject(reason));
265
365
  return;
266
366
  }
267
367
  if (stdoutBuffer.trim()) {
@@ -272,15 +372,85 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
272
372
  /* fall through */
273
373
  }
274
374
  }
275
- if (signal === "SIGKILL") {
276
- finish(() => reject(new FabricTaskError("total timeout exceeded", "timeout", "environment")));
375
+ if (reaped) {
376
+ // `close` never followed `exit`: the pipes outlived the producer, which
377
+ // may mean a descendant escaped supervision — but the drain also cannot
378
+ // rule out a stalled event loop or a slow pipe, so this is reported as
379
+ // an inconclusive harness failure rather than a sandbox escape. Either
380
+ // way the result is rejected: it must never resolve while a descendant
381
+ // might still be alive to mutate scratch after cleanup. The recorded
382
+ // exit status only narrows the message; the deferral contract is the
383
+ // same for a clean exit and a nonzero or signaled one.
384
+ const failure = new FabricTaskError(
385
+ code !== 0 || signal
386
+ ? `isolated producer exited (${code ?? signal ?? "unknown"}); its stdio never closed`
387
+ : "isolated producer exited but its stdio never closed",
388
+ "harness_failure",
389
+ "harness",
390
+ ) as FabricTaskError & { unconfirmedTermination?: boolean; stdioRelease?: Promise<void> };
391
+ // A descendant holding an inherited pipe can still use scratch after
392
+ // the direct child exited, so the caller must retain scratch for manual
393
+ // review — pipe closure alone cannot prove a descendant terminated.
394
+ failure.unconfirmedTermination = true;
395
+ failure.stdioRelease = stdioReleaseSignal;
396
+ finish(() => reject(failure));
277
397
  return;
278
398
  }
279
- finish(() => reject(new FabricTaskError(
280
- code === 0 ? "isolated producer returned no result" : `isolated producer exited (${code ?? signal ?? "unknown"})`,
281
- "harness_failure",
282
- "harness",
283
- )));
399
+ // A stored result is accepted only when the child exited normally. A
400
+ // signaled or nonzero exit without a latched reason is a harness failure —
401
+ // parent-owned kills always carry a killReason, so this is never a timeout.
402
+ if (code !== 0 || signal) {
403
+ finish(() => reject(new FabricTaskError(
404
+ `isolated producer exited (${code ?? signal ?? "unknown"})`,
405
+ "harness_failure",
406
+ "harness",
407
+ )));
408
+ return;
409
+ }
410
+ if (receivedResult) {
411
+ finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
412
+ return;
413
+ }
414
+ finish(() => reject(new FabricTaskError("isolated producer returned no result", "harness_failure", "harness")));
415
+ };
416
+
417
+ child.on("exit", (code, signal) => {
418
+ if (childClosed || settled) return;
419
+ // `exit` ends the budget window even while `close` is still pending on
420
+ // stdio: an already-met deadline still applies, and no producer code can
421
+ // breach one after this point, so both budget timers are disarmed now.
422
+ childExitedAt = budgetNow();
423
+ if (!killReason) {
424
+ const expired = expiredDeadline(childExitedAt);
425
+ if (expired) killReason = expired;
426
+ }
427
+ clearTimeout(totalTimer);
428
+ clearTimeout(inactivityTimer);
429
+ // The direct child is confirmed dead, so the kill-confirmation watchdog
430
+ // is moot; only the stdio drain still needs its bound.
431
+ if (killWatchdog) {
432
+ clearTimeout(killWatchdog);
433
+ killWatchdog = undefined;
434
+ }
435
+ // The direct child is dead, but `close` also waits for its stdio to end.
436
+ // Bound the drain so a descendant holding an inherited pipe cannot keep
437
+ // the run pending, then settle from the recorded exit status.
438
+ reapTimer = setTimeout(() => {
439
+ if (reapTimer) {
440
+ clearTimeout(reapTimer);
441
+ reapTimer = undefined;
442
+ }
443
+ if (childClosed || settled) return;
444
+ childClosed = true;
445
+ armStdioRelease();
446
+ decide(code, signal, true);
447
+ }, EXIT_DRAIN_MS);
448
+ });
449
+
450
+ child.on("close", (code, signal) => {
451
+ if (childClosed) return;
452
+ childClosed = true;
453
+ decide(code, signal);
284
454
  });
285
455
 
286
456
  const payload = JSON.stringify({
@@ -299,8 +469,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
299
469
  : undefined,
300
470
  });
301
471
  if (Buffer.byteLength(payload, "utf8") > FABRIC_PRODUCER_REQUEST_MAX_BYTES) {
302
- killChild(child);
303
- finish(() => reject(new FabricTaskError("producer request exceeds protocol limit", "budget_exhausted", "environment")));
472
+ settleTimeout(new FabricTaskError("producer request exceeds protocol limit", "budget_exhausted", "environment"));
304
473
  return;
305
474
  }
306
475
 
@@ -308,13 +477,11 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
308
477
  child.stdin?.write(payload);
309
478
  child.stdin?.end();
310
479
  } catch (error) {
311
- if (killReason) return;
312
- killChild(child);
313
- finish(() => reject(new FabricTaskError(
480
+ settleTimeout(new FabricTaskError(
314
481
  error instanceof Error ? error.message : String(error),
315
482
  "harness_failure",
316
483
  "harness",
317
- )));
484
+ ));
318
485
  return;
319
486
  }
320
487
  });
@@ -9,6 +9,7 @@ import {
9
9
  readdirSync,
10
10
  readSync,
11
11
  rmSync,
12
+ statSync,
12
13
  writeSync,
13
14
  type Stats,
14
15
  } from "node:fs";
@@ -30,6 +31,9 @@ interface TrustedScratchDir {
30
31
  identity: string;
31
32
  }
32
33
 
34
+ // Unconfirmed producer trees are retained for manual review. Neither a marker
35
+ // inside producer-writable scratch nor its age grants later deletion authority.
36
+
33
37
  /** Require stats to describe a regular file, not a symlink or special node. */
34
38
  function assertRegularFile(stats: Stats, label: string): void {
35
39
  if (!stats.isFile() || stats.isSymbolicLink() || stats.isDirectory() || stats.isFIFO() || stats.isSocket() || stats.isCharacterDevice() || stats.isBlockDevice()) {
@@ -299,11 +303,14 @@ export function createSyntheticScratch(configDir?: string): ScratchTree {
299
303
  } finally {
300
304
  closeSync(fd);
301
305
  }
302
- const trustedForCleanup = trusted;
306
+ // The pinned root is only needed to write the fixture; every later access
307
+ // opens its own trusted handle. Close it now so deferred cleanup — which
308
+ // may wait on a descendant-held pipe or never observe a release signal —
309
+ // cannot leak the descriptor (or hold the directory open on Windows).
310
+ closeTrustedScratchRoot(trusted);
303
311
  return {
304
312
  root,
305
313
  cleanup: () => {
306
- closeTrustedScratchRoot(trustedForCleanup);
307
314
  try {
308
315
  rmSync(root, { recursive: true, force: true, maxRetries: 3 });
309
316
  } catch {
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * Shadow-call intercept source models.
3
3
  *
4
- * Codex 0.145.0+ uses `gpt-5.6-luna` for helper calls. Older clients through
5
- * 0.144.x used `gpt-5.4-mini`; operators supporting them can restore that
6
- * prefix with the `sourceModels` override. Every surface that names the
4
+ * Codex 0.154.0+ sends `gpt-6-luna` for helper calls. Clients from 0.145.0
5
+ * through 0.153.x sent `gpt-5.6-luna`, which stays a default prefix so those
6
+ * clients keep their interception. Clients through 0.144.x used `gpt-5.4-mini`;
7
+ * operators supporting them can restore that prefix with the `sourceModels`
8
+ * override. The GPT-6 slug comes first because surfaces show the list in order.
9
+ * Every surface that names the
7
10
  * intercepted model (management API, GUI badges/tooltips, CLI) reads it from
8
11
  * here instead of hard-coding a slug that goes stale on the next client bump.
9
12
  */
10
- export const DEFAULT_SHADOW_SOURCE_MODELS = ["gpt-5.6-luna"] as const;
13
+ export const DEFAULT_SHADOW_SOURCE_MODELS = ["gpt-6-luna", "gpt-5.6-luna"] as const;
11
14
 
12
15
  /**
13
16
  * Optional blocked model redirects at the shared routing layer.
@@ -84,6 +87,10 @@ export function shadowCallTargetsIntersect(
84
87
  * intercept every configured shadow source model regardless of request kind.
85
88
  * A replacement intersecting the same provider+model source set remains a
86
89
  * no-op because rewriting it would only create self-interception (#2706).
90
+ *
91
+ * Callers skip this check entirely for spawned sub-agent turns
92
+ * (`isThreadSpawnRequest`): `gpt-6-luna` is both the helper slug and a default
93
+ * sub-agent model, and an explicitly spawned child must keep the model it chose.
87
94
  */
88
95
  export function shouldInterceptShadowCall(
89
96
  modelId: string,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Worker spawning that survives `bun build --compile`.
3
+ *
4
+ * Bun standalone executables fail to resolve nested worker entrypoints
5
+ * (oven-sh/bun#29124): the main bundle is flattened into /$bunfs/root while
6
+ * additional entrypoints keep their nested paths, so `new Worker(new URL(...))`
7
+ * dies with ModuleNotFound in compiled binaries. Standalone builds therefore
8
+ * embed each worker as a pre-bundled source string (generated by
9
+ * scripts/build-standalone.ts into src/generated/worker-bundles.gen.ts) and
10
+ * spawn it from a Blob URL, which works in both compiled binaries and source
11
+ * checkouts. Source checkouts without generated bundles fall back to the
12
+ * dev-mode URL form.
13
+ */
14
+ import { WORKER_BUNDLES } from "../generated/worker-bundles.gen";
15
+
16
+ export function spawnWorker(devUrl: string, key: string): Worker {
17
+ const src = WORKER_BUNDLES[key];
18
+ if (src) {
19
+ const blobUrl = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
20
+ let worker: Worker;
21
+ try {
22
+ worker = new Worker(blobUrl);
23
+ } catch (error) {
24
+ URL.revokeObjectURL(blobUrl);
25
+ throw error;
26
+ }
27
+ // Long-lived servers schedule workers repeatedly; release the blob URL
28
+ // once the thread exits instead of leaking one per run.
29
+ worker.addEventListener("close", () => URL.revokeObjectURL(blobUrl));
30
+ return worker;
31
+ }
32
+ return new Worker(devUrl);
33
+ }
@@ -41,7 +41,6 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
41
41
  export const GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST = 3;
42
42
 
43
43
  const DEFAULT_COOLDOWN_MS = 60_000;
44
- const MAX_COOLDOWN_MS = 15 * 60_000;
45
44
 
46
45
  /**
47
46
  * How long a presence answer may be reused before the store is consulted again.
@@ -349,12 +348,15 @@ export function rotateGenericOAuthAccountOn429(
349
348
  // A single stored account has nowhere to go; rotating to itself would just replay the 429.
350
349
  if (!set || set.accounts.length < 2) return null;
351
350
 
352
- const parsed = parseRetryAfterMs(retryAfterHeader, now, { preserveImmediate: true });
351
+ // `preserveServerDelay` keeps the delay the server actually stated, bounded by the parser's
352
+ // one-day ceiling, exactly as the combo path does. Truncating it locally only guarantees a
353
+ // second 429 on an account we were told to leave alone.
354
+ const parsed = parseRetryAfterMs(retryAfterHeader, now, { preserveImmediate: true, preserveServerDelay: true });
353
355
  // An account whose allowance is provably spent gets a reset-aligned cooldown instead of
354
356
  // the default minute: retrying it every 60s until the window rolls over is pure waste.
355
357
  // A Retry-After from upstream still wins — it is the server's own instruction.
356
358
  const exhausted = parsed === undefined ? exhaustedCooldownMs(providerName, failedAccountId, now) : null;
357
- const cooldownMs = exhausted ?? Math.min(parsed ?? DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS);
359
+ const cooldownMs = exhausted ?? parsed ?? DEFAULT_COOLDOWN_MS;
358
360
  const family = classifyModelFamilyForQuota(providerName, requestedModelId);
359
361
  health.set(healthKey(providerName, failedAccountId, family), {
360
362
  cooldownUntil: now + cooldownMs,
@@ -22,11 +22,12 @@ import {
22
22
  mergeAccountCredential,
23
23
  normalizeAuthStoreBuffer,
24
24
  readOAuthRefreshIntent,
25
- removeAccount,
25
+ rollbackCredentialWriteIfMatch,
26
26
  saveAccountCredential,
27
27
  saveCredential,
28
- setActiveAccount,
28
+ saveCredentialWithReceipt,
29
29
  writeOAuthRefreshIntent,
30
+ type OAuthCredentialWriteReceipt,
30
31
  type OAuthRefreshIntent,
31
32
  type OAuthRefreshIntentCleanupPending,
32
33
  } from "./store";
@@ -1590,33 +1591,15 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
1590
1591
 
1591
1592
  interface RunLoginDeps {
1592
1593
  saveCredential?: typeof saveCredential;
1594
+ saveCredentialWithReceipt?: typeof saveCredentialWithReceipt;
1593
1595
  saveAccountCredential?: typeof saveAccountCredential;
1594
1596
  loadConfig?: typeof loadConfig;
1595
1597
  saveConfig?: typeof saveConfig;
1596
1598
  settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
1597
- removeAccount?: typeof removeAccount;
1598
- setActiveAccount?: typeof setActiveAccount;
1599
+ rollbackCredentialWrite?: typeof rollbackCredentialWriteIfMatch;
1599
1600
  assertCurrentOwner?: () => void;
1600
1601
  }
1601
1602
 
1602
- /** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
1603
- async function rollbackForcedKiroAccountWrite(
1604
- provider: string,
1605
- previousActiveId: string | undefined,
1606
- previousAccountIds: ReadonlySet<string>,
1607
- deps: Pick<RunLoginDeps, "removeAccount" | "setActiveAccount">,
1608
- ): Promise<void> {
1609
- const set = getAccountSet(provider);
1610
- if (!set) return;
1611
- for (const account of [...set.accounts]) {
1612
- if (previousAccountIds.has(account.id)) continue;
1613
- await (deps.removeAccount ?? removeAccount)(provider, account.id);
1614
- }
1615
- if (previousActiveId && getAccountCredential(provider, previousActiveId)) {
1616
- await (deps.setActiveAccount ?? setActiveAccount)(provider, previousActiveId);
1617
- }
1618
- }
1619
-
1620
1603
  /** Run the login flow, persist the credential + upsert the provider entry to disk, return cred. */
1621
1604
  export async function runLogin(
1622
1605
  provider: string,
@@ -1639,9 +1622,7 @@ export async function runLogin(
1639
1622
  // loginKiro keys its pending CLI-session transaction by object identity. Keep this exact object
1640
1623
  // for settlement even when source normalization below creates a derived credential object.
1641
1624
  const shouldRollbackKiroAccounts = provider === "kiro" && opts?.forceLogin === true;
1642
- const previousKiroAccounts = shouldRollbackKiroAccounts ? getAccountSet(provider) : undefined;
1643
- const previousKiroActiveId = previousKiroAccounts?.activeAccountId;
1644
- const previousKiroAccountIds = new Set(previousKiroAccounts?.accounts.map(account => account.id) ?? []);
1625
+ let kiroCredentialWrite: OAuthCredentialWriteReceipt | null = null;
1645
1626
  const loginProviderConfig = preflightConfig
1646
1627
  ? (def.resolveProviderConfig?.(preflightConfig) ?? preflightConfig.providers[provider] ?? def.providerConfig)
1647
1628
  : def.providerConfig;
@@ -1674,10 +1655,19 @@ export async function runLogin(
1674
1655
  assertBeforePersist: deps.assertCurrentOwner,
1675
1656
  });
1676
1657
  } else {
1677
- await (deps.saveCredential ?? saveCredential)(provider, cred, {
1658
+ const saveOptions = {
1678
1659
  preserveIdentityless: opts?.forceLogin === true,
1679
1660
  assertBeforePersist: deps.assertCurrentOwner,
1680
- });
1661
+ };
1662
+ if (shouldRollbackKiroAccounts && !deps.saveCredential) {
1663
+ kiroCredentialWrite = await (deps.saveCredentialWithReceipt ?? saveCredentialWithReceipt)(
1664
+ provider,
1665
+ cred,
1666
+ saveOptions,
1667
+ );
1668
+ } else {
1669
+ await (deps.saveCredential ?? saveCredential)(provider, cred, saveOptions);
1670
+ }
1681
1671
  }
1682
1672
  if (provider !== "chatgpt") {
1683
1673
  // Re-run against post-credential state so same-provider API-key additions, removals,
@@ -1695,9 +1685,9 @@ export async function runLogin(
1695
1685
  }
1696
1686
  } catch (error) {
1697
1687
  const errors: unknown[] = [error];
1698
- if (shouldRollbackKiroAccounts) {
1688
+ if (kiroCredentialWrite) {
1699
1689
  try {
1700
- await rollbackForcedKiroAccountWrite(provider, previousKiroActiveId, previousKiroAccountIds, deps);
1690
+ await (deps.rollbackCredentialWrite ?? rollbackCredentialWriteIfMatch)(kiroCredentialWrite);
1701
1691
  } catch (rollbackError) {
1702
1692
  errors.push(rollbackError);
1703
1693
  }