@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.
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +14 -5
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +43 -16
- package/package.json +1 -1
package/dist/api/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/api/client.js
CHANGED
|
@@ -25,8 +25,17 @@ function authHeaders() {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
// ─── Retry helpers ────────────────────────────────────────────────────────────
|
|
28
|
-
const MAX_RETRIES =
|
|
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 :
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
|
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"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -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
|
-
|
|
1426
|
+
finish({ error: `Invalid redirect location: ${res.headers.location}` });
|
|
1413
1427
|
return;
|
|
1414
1428
|
}
|
|
1415
|
-
fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1).then(
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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)",
|