@nexrall/code-core 1.4.5 → 1.4.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAgBD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA0alB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA0BD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA0alB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
@@ -25,8 +25,17 @@ function authHeaders() {
25
25
  };
26
26
  }
27
27
  // ─── Retry helpers ────────────────────────────────────────────────────────────
28
- const MAX_RETRIES = 3;
28
+ const MAX_RETRIES = 5;
29
29
  const RETRY_BASE_MS = 1000;
30
+ const RETRY_MAX_MS = 30000;
31
+ /** Exponential backoff with full jitter, capped at RETRY_MAX_MS. The jitter spreads
32
+ * retries out so a fleet of clients hitting the SAME overloaded API (529) don't all
33
+ * reconnect in lockstep and re-trigger the overload (thundering herd). Returns a delay
34
+ * in [exp/2, exp] where exp = min(RETRY_MAX_MS, RETRY_BASE_MS * 2^attempt). */
35
+ function backoffMs(attempt) {
36
+ const exp = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * Math.pow(2, attempt));
37
+ return Math.round(exp / 2 + Math.random() * (exp / 2));
38
+ }
30
39
  function sleep(ms) {
31
40
  return new Promise((r) => setTimeout(r, ms));
32
41
  }
@@ -92,12 +101,12 @@ async function streamChat(messages, options, onEvent) {
92
101
  if (response.status === 429 && attempt < MAX_RETRIES) {
93
102
  reportRetry('Rate limited by the API — retrying');
94
103
  const retryAfter = parseInt(response.headers.get('retry-after') ?? '0', 10);
95
- await sleep(retryAfter > 0 ? retryAfter * 1000 : RETRY_BASE_MS * Math.pow(2, attempt));
104
+ await sleep(retryAfter > 0 ? retryAfter * 1000 : backoffMs(attempt));
96
105
  continue;
97
106
  }
98
107
  if (response.status >= 500 && response.status < 600 && attempt < MAX_RETRIES) {
99
108
  reportRetry(`Server error (${response.status}) — retrying`);
100
- await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
109
+ await sleep(backoffMs(attempt));
101
110
  continue;
102
111
  }
103
112
  // 4xx (incl. 413 Payload Too Large, 400 Bad Request, 402 Insufficient balance)
@@ -115,7 +124,7 @@ async function streamChat(messages, options, onEvent) {
115
124
  // Connection couldn't even be established — network drop / DNS blip / the
116
125
  // machine just woke from sleep and Wi-Fi hasn't reconnected yet.
117
126
  reportRetry('Connection lost — attempting to reconnect');
118
- await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
127
+ await sleep(backoffMs(attempt));
119
128
  continue;
120
129
  }
121
130
  }
@@ -434,7 +443,7 @@ async function streamChat(messages, options, onEvent) {
434
443
  throw err;
435
444
  if (err.retryable && sAttempt < MAX_RETRIES) {
436
445
  reportRetry(err.message || 'Connection interrupted — reconnecting');
437
- await sleep(RETRY_BASE_MS * Math.pow(2, sAttempt));
446
+ await sleep(backoffMs(sAttempt));
438
447
  continue;
439
448
  }
440
449
  throw err;
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAw9DtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAk/DtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
@@ -1390,6 +1390,19 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
1390
1390
  return callback(null, address, family);
1391
1391
  });
1392
1392
  });
1393
+ // Guard against double-resolve and — critically — against our OWN intentional
1394
+ // req.destroy() (used to stop downloading once we hit MAX_FETCH_BYTES or to
1395
+ // hand off a redirect) surfacing as an ECONNRESET on res/req 'error'. Without
1396
+ // this flag, aborting the socket ourselves would reject with "socket hang up"
1397
+ // and the truncated-but-valid body we already collected would be thrown away.
1398
+ let settled = false;
1399
+ let selfAborted = false;
1400
+ const finish = (result) => {
1401
+ if (settled)
1402
+ return;
1403
+ settled = true;
1404
+ resolve(result);
1405
+ };
1393
1406
  const req = transport.get(url, {
1394
1407
  timeout: DEFAULT_TIMEOUT_MS,
1395
1408
  lookup: guardedLookup,
@@ -1402,6 +1415,7 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
1402
1415
  // Follow redirects (301/302/307/308) with depth counter to prevent infinite loops
1403
1416
  const status = res.statusCode ?? 0;
1404
1417
  if ((status === 301 || status === 302 || status === 307 || status === 308) && res.headers.location) {
1418
+ selfAborted = true; // our destroy() below is intentional — ignore the reset it triggers
1405
1419
  req.destroy();
1406
1420
  // Resolve relative Location headers against the current URL
1407
1421
  let nextUrl;
@@ -1409,10 +1423,10 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
1409
1423
  nextUrl = new URL(res.headers.location, url).href;
1410
1424
  }
1411
1425
  catch {
1412
- resolve({ error: `Invalid redirect location: ${res.headers.location}` });
1426
+ finish({ error: `Invalid redirect location: ${res.headers.location}` });
1413
1427
  return;
1414
1428
  }
1415
- fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1).then(resolve);
1429
+ fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1).then(finish);
1416
1430
  return;
1417
1431
  }
1418
1432
  const contentType = res.headers['content-type'] ?? '';
@@ -1420,6 +1434,19 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
1420
1434
  const chunks = [];
1421
1435
  let totalBytes = 0;
1422
1436
  let truncated = false;
1437
+ // Emit whatever we've collected so far (used by both the clean 'end' and the
1438
+ // size-limit truncation path — the two must produce identical output shape).
1439
+ const emitBody = () => {
1440
+ let body = Buffer.concat(chunks).toString('utf-8');
1441
+ if (isHtml)
1442
+ body = stripHtml(body);
1443
+ const truncNote = truncated ? `\n\n[Truncated at ${MAX_FETCH_BYTES / 1024}KB]` : '';
1444
+ // Prompt-injection mitigation: fetched web content is fully attacker-controlled
1445
+ // (anyone can put "ignore previous instructions..." on a page). Wrap it in an
1446
+ // explicit untrusted-data delimiter so the model treats it as DATA to read, not
1447
+ // as instructions to follow — defense-in-depth alongside the system-prompt rule.
1448
+ finish({ output: `HTTP ${status}\n\n<untrusted_web_content url="${url}">\n${body}${truncNote}\n</untrusted_web_content>` });
1449
+ };
1423
1450
  res.on('data', (chunk) => {
1424
1451
  if (totalBytes + chunk.length > MAX_FETCH_BYTES) {
1425
1452
  const remaining = MAX_FETCH_BYTES - totalBytes;
@@ -1427,30 +1454,30 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
1427
1454
  chunks.push(chunk.subarray(0, remaining));
1428
1455
  totalBytes = MAX_FETCH_BYTES;
1429
1456
  truncated = true;
1457
+ // We deliberately stop here. Mark the abort as self-inflicted so the
1458
+ // error handlers don't turn the resulting ECONNRESET into a failure,
1459
+ // then emit the (valid, truncated) body we already have.
1460
+ selfAborted = true;
1430
1461
  req.destroy();
1462
+ emitBody();
1431
1463
  }
1432
1464
  else {
1433
1465
  chunks.push(chunk);
1434
1466
  totalBytes += chunk.length;
1435
1467
  }
1436
1468
  });
1437
- res.on('end', () => {
1438
- let body = Buffer.concat(chunks).toString('utf-8');
1439
- if (isHtml)
1440
- body = stripHtml(body);
1441
- const truncNote = truncated ? `\n\n[Truncated at ${MAX_FETCH_BYTES / 1024}KB]` : '';
1442
- // Prompt-injection mitigation: fetched web content is fully attacker-controlled
1443
- // (anyone can put "ignore previous instructions..." on a page). Wrap it in an
1444
- // explicit untrusted-data delimiter so the model treats it as DATA to read, not
1445
- // as instructions to follow — defense-in-depth alongside the system-prompt rule.
1446
- resolve({ output: `HTTP ${status}\n\n<untrusted_web_content url="${url}">\n${body}${truncNote}\n</untrusted_web_content>` });
1447
- });
1448
- res.on('error', (err) => resolve({ error: err.message }));
1469
+ res.on('end', emitBody);
1470
+ // Ignore errors caused by our own intentional destroy (truncation / redirect);
1471
+ // only real transport failures should surface.
1472
+ res.on('error', (err) => { if (!selfAborted)
1473
+ finish({ error: err.message }); });
1449
1474
  });
1450
- req.on('error', (err) => resolve({ error: err.message }));
1475
+ req.on('error', (err) => { if (!selfAborted)
1476
+ finish({ error: err.message }); });
1451
1477
  req.on('timeout', () => {
1478
+ selfAborted = true;
1452
1479
  req.destroy();
1453
- resolve({ error: `Request timed out after ${DEFAULT_TIMEOUT_MS}ms` });
1480
+ finish({ error: `Request timed out after ${DEFAULT_TIMEOUT_MS}ms` });
1454
1481
  });
1455
1482
  });
1456
1483
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",