aether-code 0.41.0 → 0.42.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/api.js +23 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aether-code",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Uncensored AI coding agent for your terminal — Claude Code alternative with MCP support. Reads code, writes files, runs commands. Drives IDA Pro, Roblox Studio, Wireshark, Blender, and any MCP server. No refusal layer.",
5
5
  "homepage": "https://trynoguard.com",
6
6
  "repository": {
package/src/api.js CHANGED
@@ -7,18 +7,37 @@ const USER_AGENT = `aether-code/${getVersion()}`;
7
7
 
8
8
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
9
 
10
+ // Per-request connect timeout. The server retries upstream internally (up to
11
+ // 5 × 75s = 375s worst case), and during that time the CLI's fetch() just
12
+ // hangs with zero feedback. This cap ensures we abort, show a retry message,
13
+ // and try again rather than sitting dead for minutes.
14
+ const CONNECT_TIMEOUT_MS = 90_000;
15
+
10
16
  /**
11
17
  * fetch() with bounded exponential-backoff retries. Retries on thrown network
12
18
  * errors (DNS/reset/offline blip) and transient 5xx; never retries 4xx
13
19
  * (auth/credit/validation — a retry won't help). Body is always a small JSON
14
20
  * string, so resending is safe. For the streaming endpoint only the initial
15
21
  * connection is retried.
22
+ *
23
+ * Each request has a hard 90s connect timeout via AbortController — if the
24
+ * server hasn't responded with headers by then, we abort and retry. This
25
+ * prevents the CLI from hanging indefinitely while the server retries
26
+ * upstream internally.
16
27
  */
17
- export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 1000, onRetry } = {}) {
28
+ export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 1000, timeoutMs = CONNECT_TIMEOUT_MS, onRetry } = {}) {
18
29
  let attempt = 0;
19
30
  while (true) {
31
+ const ac = new AbortController();
32
+ const timer = setTimeout(() => ac.abort(new Error(`Server did not respond within ${Math.round(timeoutMs / 1000)}s`)), timeoutMs);
33
+ // If the caller already set a signal, chain it so both can abort.
34
+ const callerSignal = options?.signal;
35
+ if (callerSignal) {
36
+ callerSignal.addEventListener("abort", () => ac.abort(callerSignal.reason), { once: true });
37
+ }
20
38
  try {
21
- const res = await fetch(url, options);
39
+ const res = await fetch(url, { ...options, signal: ac.signal });
40
+ clearTimeout(timer);
22
41
  if (res.status >= 500 && attempt < retries) {
23
42
  // Release the connection back to the pool before retrying — an
24
43
  // unconsumed body keeps the socket open (undici won't reuse it).
@@ -30,6 +49,7 @@ export async function fetchWithRetry(url, options, { retries = 4, baseDelay = 10
30
49
  }
31
50
  return res;
32
51
  } catch (e) {
52
+ clearTimeout(timer);
33
53
  if (attempt < retries) {
34
54
  attempt++;
35
55
  if (onRetry) onRetry(attempt, e.message);
@@ -207,7 +227,7 @@ export async function agentTurnStream({
207
227
  let timer;
208
228
  const timeout = new Promise((_, reject) => {
209
229
  timer = setTimeout(
210
- () => reject(new AetherError("Stream stalled — no data for 120s", "STREAM_TIMEOUT", 0)),
230
+ () => reject(new AetherError(`Stream stalled — no data for ${Math.round(READ_TIMEOUT_MS / 1000)}s`, "STREAM_TIMEOUT", 0)),
211
231
  READ_TIMEOUT_MS,
212
232
  );
213
233
  });