@bman654/clodex 2.18.10 → 2.19.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.
package/README.md CHANGED
@@ -457,8 +457,76 @@ clodex --version # version
457
457
  `CLODEX_UPSTREAM_MAX_RETRIES` value takes precedence on the HTTP MITM path;
458
458
  otherwise, Claude Code's own `CLAUDE_CODE_MAX_RETRIES=0` disables that replay
459
459
  so telling the client never to resend a request is not quietly undone one
460
- layer down.
460
+ layer down. On that path the retry setting controls only this pooled-connection
461
+ replay. A separate outage hold, with its own setting, also runs there; see
462
+ the next entry.
461
463
  Recovered requests appear in the inference log as `response_retried`.
464
+ - **Outage hold (proxy mode):** when a Claude Code request cannot reach
465
+ `api.anthropic.com` at all, clodex keeps retrying it for a while instead of
466
+ answering 502 at once, so a laptop waking from sleep, a Wi-Fi switch or a VPN
467
+ reconnect does not end the turn. A failure qualifies when the name does not
468
+ resolve, the connection is refused or has no route, or the connection is
469
+ reset or times out before its TLS handshake completes. clodex sends the
470
+ request only after the handshake, so none of these attempts sent anything
471
+ and retrying cannot submit the request twice. A failure after the handshake,
472
+ a certificate error, and a reset on a reused connection are not held. Retries
473
+ wait 1, 2, 4 and 8 seconds, then 15 seconds between attempts. The hold is a
474
+ deadline counted from when the request arrived: at the deadline clodex drops
475
+ any attempt that has not finished its handshake and answers 502, with a
476
+ message saying the outage hold expired and, if there was one, naming the last
477
+ upstream error.
478
+ If a reused connection is reset after the deadline and clodex replays the
479
+ request, the replay gets a 502 at once unless it can go out on another
480
+ reused connection.
481
+ `CLODEX_PASSTHROUGH_OUTAGE_HOLD_MS` sets the deadline in milliseconds. The
482
+ default is `120000` (2 minutes) and `0` turns the hold off. Values above
483
+ `570000` clamp to it with a one-time warning; malformed values are reported
484
+ once and ignored. Only messages generation (`/v1/messages`, including
485
+ `/v1/messages?beta=true`) is held. `count_tokens`, OAuth, startup and
486
+ telemetry calls still get their 502 at once. Each held retry appears in the
487
+ inference log as `response_retried` with `outageHold: true`.
488
+ Claude Code resends a 502 itself, so at its default of 10 retries a turn can
489
+ last through roughly 25 minutes of outage at the default hold. That figure is a
490
+ projection from Claude Code's retry schedule, not a measurement. Claude Code
491
+ can also give up sooner than the hold. It waits 180 seconds plus 1 second per
492
+ 32 KiB of request body for a query's first response headers. After one
493
+ request gets no response, later requests in the query get `API_TIMEOUT_MS`
494
+ minus 1 second (599 seconds at its default), and a second request with no
495
+ response ends the turn even with retries left. At Claude Code's defaults the
496
+ default hold answers inside the first window, so a request still waiting for
497
+ its connection gets its 502 before either limit. A hold above about
498
+ 180 seconds means the query's first held request is cut off by Claude Code
499
+ instead, and the hold must then stay below the longer window; the `570000`
500
+ ceiling does that at Claude Code's default `API_TIMEOUT_MS`. If you lower
501
+ `API_TIMEOUT_MS` in Claude Code's environment so that `API_TIMEOUT_MS` minus
502
+ 1 second is shorter than the hold, every turn whose outage outlasts both
503
+ windows ends on that second no-response after about min(180 s plus the body
504
+ allowance, `API_TIMEOUT_MS` − 1 s) + (`API_TIMEOUT_MS` − 1 s); keep the hold
505
+ below `API_TIMEOUT_MS` − 1 s, or set it to `0`. For
506
+ `0 < API_TIMEOUT_MS < 11000`, Claude Code skips both of those waits for
507
+ response headers, but its own request timeout still applies: if that is
508
+ shorter than the hold, it aborts each held request before clodex can return
509
+ its 502.
510
+ The cost is a longer wait when the outage does not end: with the hold off,
511
+ Claude Code gives up on a persistent fast-failing outage after about 3
512
+ minutes, and at the default hold after about 25 (both projections).
513
+ The hold has its own switch. `CLODEX_UPSTREAM_MAX_RETRIES=0` and
514
+ `CLAUDE_CODE_MAX_RETRIES=0` do not turn it off, because a held request was
515
+ never sent. If you want outages to fail fast, for example in a headless
516
+ `claude -p` or CI run that would otherwise wait through each hold unless its
517
+ caller sets a limit, also set `CLODEX_PASSTHROUGH_OUTAGE_HOLD_MS=0`. The
518
+ server process reads the variable: with a standalone `clodex server`, set it
519
+ when starting the server, because setting it on `clodex-claude` changes
520
+ nothing. When clodex itself uses `HTTPS_PROXY`, the hold covers an
521
+ unreachable proxy and a tunnel that fails before its TLS handshake completes.
522
+ When the proxy is up but Anthropic is not, the proxy usually answers clodex's
523
+ `CONNECT` with an error status, and that error response comes back at once,
524
+ as it does with the hold off. If the proxy closes the connection before
525
+ replying, clodex answers 502 at once; if it resets the connection instead,
526
+ the request is held as for an unreachable proxy. A replay after the deadline
527
+ that needs a new tunnel waits for the proxy's `CONNECT` reply and is not cut
528
+ while it waits, so a proxy that never replies leaves that request with no
529
+ response, and an error reply to it gets clodex's 502 instead of the proxy's.
462
530
  - **Connection pacing (ChatGPT/Codex plans):** when many agents run at once,
463
531
  clodex spaces out the new connections it opens to OpenAI, which should make a
464
532
  burst of parallel work less likely to trip OpenAI's own rate limit. (In the
@@ -282,8 +282,19 @@ function tryAcquireRegistryLock(lockPath = getRegistryLockPath(), options = {})
282
282
  }
283
283
  return null;
284
284
  }
285
- function sleep(ms) {
286
- return new Promise((resolve) => setTimeout(resolve, ms));
285
+ function sleep(ms, signal) {
286
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
287
+ return new Promise((resolve, reject) => {
288
+ const onAbort = () => {
289
+ clearTimeout(timer);
290
+ reject(signal.reason);
291
+ };
292
+ const timer = setTimeout(() => {
293
+ signal.removeEventListener("abort", onAbort);
294
+ resolve();
295
+ }, ms);
296
+ signal.addEventListener("abort", onAbort, { once: true });
297
+ });
287
298
  }
288
299
  function sleepSync(ms) {
289
300
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -314,6 +325,7 @@ async function withRegistryWriteLock(operation, options = {}) {
314
325
  const deadline = now() + waitMs;
315
326
  let lease = null;
316
327
  while (!lease) {
328
+ options.signal?.throwIfAborted();
317
329
  lease = tryAcquireRegistryLock(lockPath, {
318
330
  now,
319
331
  isAlive: options.isAlive
@@ -321,7 +333,7 @@ async function withRegistryWriteLock(operation, options = {}) {
321
333
  if (lease) break;
322
334
  if (now() >= deadline)
323
335
  throw lockTimeoutError(lockPath, waitMs, options.isAlive ?? isPidAlive);
324
- await sleep(retryMs);
336
+ await sleep(retryMs, options.signal);
325
337
  }
326
338
  const leases = new Map(inheritedLeases);
327
339
  leases.set(lockPath, lease);
@@ -1749,6 +1761,63 @@ function computeWrapperEnv(baseEnv, state) {
1749
1761
  return env;
1750
1762
  }
1751
1763
 
1764
+ // src/process-exit.ts
1765
+ import { writeSync as writeSync3 } from "fs";
1766
+ var EXIT_DRAIN_GRACE_MS = 1e3;
1767
+ var exitCancellers = /* @__PURE__ */ new Set();
1768
+ var exitRequested = false;
1769
+ function cancelOnExit(cancel) {
1770
+ if (exitRequested) {
1771
+ runCanceller(cancel);
1772
+ return () => {
1773
+ };
1774
+ }
1775
+ exitCancellers.add(cancel);
1776
+ return () => {
1777
+ exitCancellers.delete(cancel);
1778
+ };
1779
+ }
1780
+ function runCanceller(cancel) {
1781
+ try {
1782
+ cancel();
1783
+ } catch {
1784
+ }
1785
+ }
1786
+ function describeActiveResources(resources = process.getActiveResourcesInfo()) {
1787
+ if (resources.length === 0) return "nothing reported";
1788
+ const counts = /* @__PURE__ */ new Map();
1789
+ for (const resource of resources) counts.set(resource, (counts.get(resource) ?? 0) + 1);
1790
+ return [...counts].map(([name, count]) => count > 1 ? `${name} x${count}` : name).join(", ");
1791
+ }
1792
+ function exitAfterDrain(code, options = {}) {
1793
+ if (exitRequested) return;
1794
+ exitRequested = true;
1795
+ process.exitCode = code;
1796
+ const ignoreStdioError = () => {
1797
+ };
1798
+ process.stdout.on("error", ignoreStdioError);
1799
+ process.stderr.on("error", ignoreStdioError);
1800
+ const cancellers = [...exitCancellers];
1801
+ exitCancellers.clear();
1802
+ for (const cancel of cancellers) runCanceller(cancel);
1803
+ const graceMs = options.graceMs ?? EXIT_DRAIN_GRACE_MS;
1804
+ const fallback = setTimeout(() => {
1805
+ if (options.trace || process.env.CLODEX_TRACE === "1") {
1806
+ const label = options.label ?? "clodex";
1807
+ try {
1808
+ writeSync3(
1809
+ 2,
1810
+ `${label}: event loop still busy ${graceMs}ms after exit was requested; exiting with process.exit(${code}). Still active: ${describeActiveResources()}
1811
+ `
1812
+ );
1813
+ } catch {
1814
+ }
1815
+ }
1816
+ process.exit(code);
1817
+ }, graceMs);
1818
+ fallback.unref();
1819
+ }
1820
+
1752
1821
  // src/patch-manifest.ts
1753
1822
  import { readFileSync as readFileSync5 } from "fs";
1754
1823
  import { join as join5 } from "path";
@@ -2204,6 +2273,8 @@ export {
2204
2273
  planPristineSource,
2205
2274
  planInspectedPristineSource,
2206
2275
  planRestoreOnly,
2207
- collectPristineFacts
2276
+ collectPristineFacts,
2277
+ cancelOnExit,
2278
+ exitAfterDrain
2208
2279
  };
2209
- //# sourceMappingURL=chunk-VFYRDLDD.js.map
2280
+ //# sourceMappingURL=chunk-Q2YFYQQT.js.map