@bman654/clodex 2.18.9 → 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 +69 -1
- package/dist/{chunk-VFYRDLDD.js → chunk-Q2YFYQQT.js} +76 -5
- package/dist/chunk-Q2YFYQQT.js.map +1 -0
- package/dist/claude-wrapper.js +17 -7
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +298 -63
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +4 -3
- package/package.json +1 -1
- package/dist/chunk-VFYRDLDD.js.map +0 -1
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-
|
|
2280
|
+
//# sourceMappingURL=chunk-Q2YFYQQT.js.map
|