@nexrall/code-core 1.4.14 → 1.4.16
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 +98 -1
- 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":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAeD,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;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,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,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAeD,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;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,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,CAi7BlB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;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
|
@@ -314,6 +314,10 @@ async function streamChat(messages, options, onEvent) {
|
|
|
314
314
|
// A 409's body, already consumed to classify it. node-fetch bodies are single-use, so
|
|
315
315
|
// the !response.ok handler below reads this instead of calling response.text() again.
|
|
316
316
|
let errorBodyOverride = null;
|
|
317
|
+
// Set when a 403 upstream-forbidden (Anthropic passthrough, see below) used up its
|
|
318
|
+
// short in-loop retry budget — the final throw tags `retryable: true` so it still
|
|
319
|
+
// reaches the outer 5-minute stream-retry loop instead of dying as a terminal error.
|
|
320
|
+
let forbiddenExhausted = false;
|
|
317
321
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
318
322
|
try {
|
|
319
323
|
response = await (0, node_fetch_1.default)(...buildFetchArgs());
|
|
@@ -328,6 +332,48 @@ async function streamChat(messages, options, onEvent) {
|
|
|
328
332
|
await sleepWithinBudget(backoffMs(attempt));
|
|
329
333
|
continue;
|
|
330
334
|
}
|
|
335
|
+
// 403 `{"error":{"type":"forbidden","message":"Request not allowed"}}` is
|
|
336
|
+
// Anthropic's own upstream response shape (reproduced verbatim through the AI
|
|
337
|
+
// Gateway passthrough — see services/aiGatewayClient.js), not something Nexrall's
|
|
338
|
+
// auth layer produces (an expired/invalid Nexrall JWT is a 401/403 with a
|
|
339
|
+
// completely different body — see middleware/auth.js). Reports of this exact
|
|
340
|
+
// error on Anthropic's own CLI describe it as intermittent and gone on retry, not
|
|
341
|
+
// a real permissions failure — so unlike every OTHER 4xx below (which are
|
|
342
|
+
// deterministic given the same body and must not be retried), this one gets a
|
|
343
|
+
// short, capped number of attempts before being treated as terminal. Capped well
|
|
344
|
+
// below MAX_RETRIES so a genuinely revoked/banned key still fails fast instead of
|
|
345
|
+
// burning the whole reconnect budget on something that will never succeed.
|
|
346
|
+
const FORBIDDEN_RETRY_LIMIT = 2;
|
|
347
|
+
if (response.status === 403) {
|
|
348
|
+
const bodyText = await response.text().catch(() => '');
|
|
349
|
+
let isUpstreamForbidden = false;
|
|
350
|
+
try {
|
|
351
|
+
const parsed = JSON.parse(bodyText);
|
|
352
|
+
isUpstreamForbidden = parsed?.error?.type === 'forbidden';
|
|
353
|
+
}
|
|
354
|
+
catch { /* not JSON — not this shape, fall through as terminal below */ }
|
|
355
|
+
if (isUpstreamForbidden) {
|
|
356
|
+
if (attempt < FORBIDDEN_RETRY_LIMIT && canRetry()) {
|
|
357
|
+
reportRetry('Upstream rejected the request — retrying');
|
|
358
|
+
await sleepWithinBudget(backoffMs(attempt));
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
// Exhausted the short in-connect-loop attempt budget, but this is still the
|
|
362
|
+
// SAME transient upstream shape (not a real permissions failure) — it must not
|
|
363
|
+
// die here after only ~2-4s of backoff. `forbiddenExhausted` makes the final
|
|
364
|
+
// throw below tag `retryable: true` so the OUTER stream-retry loop (which owns
|
|
365
|
+
// the shared 5-minute reconnect budget — see MAX_TOTAL_RETRY_MS) picks it back
|
|
366
|
+
// up and keeps trying, instead of surfacing a bare terminal "API error 403"
|
|
367
|
+
// while minutes of retry budget were still available.
|
|
368
|
+
forbiddenExhausted = true;
|
|
369
|
+
errorBodyOverride = bodyText;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
// Not the known transient shape — treat as terminal, but reuse the body we
|
|
373
|
+
// already consumed (node-fetch bodies are single-use) instead of losing it.
|
|
374
|
+
errorBodyOverride = bodyText;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
331
377
|
// 410 Gone — a resume was refused because the buffered window no longer contains
|
|
332
378
|
// our position (we were away too long, or the turn produced a huge amount in the
|
|
333
379
|
// meantime). Continuity can't be proven, and silently skipping frames would corrupt
|
|
@@ -468,7 +514,11 @@ async function streamChat(messages, options, onEvent) {
|
|
|
468
514
|
// Tag a 402 (pre-flight "wallet already empty" gate — see routes/code.js) so the
|
|
469
515
|
// agent loop can distinguish it from a generic error and surface the low/zero-
|
|
470
516
|
// balance topup nudge immediately, instead of a plain "API error 402" message.
|
|
471
|
-
|
|
517
|
+
//
|
|
518
|
+
// `retryable: true` on `forbiddenExhausted` hands the transient 403-upstream-forbidden
|
|
519
|
+
// case (see above) to the outer stream-retry loop instead of letting it die as a
|
|
520
|
+
// terminal error the moment the short in-connect-loop budget runs out.
|
|
521
|
+
throw Object.assign(new Error(errMsg), { status: response.status, balance, ...(forbiddenExhausted ? { retryable: true } : {}) });
|
|
472
522
|
}
|
|
473
523
|
if (!response.body) {
|
|
474
524
|
throw new Error('Response body is null');
|
|
@@ -841,6 +891,16 @@ async function streamChat(messages, options, onEvent) {
|
|
|
841
891
|
case 'error': {
|
|
842
892
|
const message = typeof evt.message === 'string' ? evt.message :
|
|
843
893
|
typeof evt.error === 'string' ? evt.error : 'Unknown SSE error';
|
|
894
|
+
// The GET /chat/resume tail loop sends this when the turn's Redis frame
|
|
895
|
+
// buffer developed a gap (trimmed window, a swallowed flush error, or the
|
|
896
|
+
// owning pod died) or the turn is simply gone — see routes/code.js. Unlike
|
|
897
|
+
// `isRetryableStreamMsg` below, this is NOT a fuzzy text match: the backend
|
|
898
|
+
// sets the flag explicitly, so it can never silently stop matching if either
|
|
899
|
+
// side rewrites the English message. There is no cursor left to resume
|
|
900
|
+
// from — a plain "retryable" resume-with-Last-Event-ID would just 410 again
|
|
901
|
+
// — so this always needs a fresh turnId, which the outer loop mints via
|
|
902
|
+
// `forceRestart`.
|
|
903
|
+
const notResumable = evt.notResumable === true;
|
|
844
904
|
// Transient upstream failure before any output → let the outer loop retry it
|
|
845
905
|
// transparently instead of killing the turn (this is the "Overloaded" case).
|
|
846
906
|
// But never retry once the turn's message has already been delivered in
|
|
@@ -852,6 +912,11 @@ async function streamChat(messages, options, onEvent) {
|
|
|
852
912
|
onEvent({ type: 'error', message });
|
|
853
913
|
resolve();
|
|
854
914
|
}
|
|
915
|
+
else if (notResumable && (!emittedToCaller || allowRestartAfterRender)) {
|
|
916
|
+
clearInterval(heartbeatWatchdog);
|
|
917
|
+
stream.destroy?.();
|
|
918
|
+
reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
|
|
919
|
+
}
|
|
855
920
|
else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
|
|
856
921
|
clearInterval(heartbeatWatchdog);
|
|
857
922
|
stream.destroy?.();
|
|
@@ -939,6 +1004,38 @@ async function streamChat(messages, options, onEvent) {
|
|
|
939
1004
|
throw err;
|
|
940
1005
|
const e = err;
|
|
941
1006
|
if (e.retryable && sAttempt < MAX_RETRIES && canRetry()) {
|
|
1007
|
+
// The backend explicitly said this turnId's buffer is unrecoverable (gap in the
|
|
1008
|
+
// Redis frame stream, or the owning pod is gone — see the `notResumable` case in
|
|
1009
|
+
// client.ts's SSE parser). Resuming with the same `lastEventId` would just hit
|
|
1010
|
+
// the same wall again, so — exactly like the inline 410-while-resuming handling
|
|
1011
|
+
// above — release the old claim, mint a fresh turnId, and restart clean instead
|
|
1012
|
+
// of falling into the "prefer resuming" branch below.
|
|
1013
|
+
if (e.forceRestart) {
|
|
1014
|
+
await cancelTurn(turnId);
|
|
1015
|
+
turnId = (0, crypto_1.randomUUID)();
|
|
1016
|
+
resuming = false;
|
|
1017
|
+
serverResumable = false;
|
|
1018
|
+
lastEventId = 0;
|
|
1019
|
+
carryText = [];
|
|
1020
|
+
carryToolUse = [];
|
|
1021
|
+
const hadRendered = emittedAnythingAcrossAttempts;
|
|
1022
|
+
const renderedChars = emittedCharsAcrossAttempts;
|
|
1023
|
+
emittedAnythingAcrossAttempts = false;
|
|
1024
|
+
emittedCharsAcrossAttempts = 0;
|
|
1025
|
+
if (hadRendered && !allowRestartAfterRender) {
|
|
1026
|
+
throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
|
|
1027
|
+
}
|
|
1028
|
+
if (hadRendered) {
|
|
1029
|
+
onEvent({
|
|
1030
|
+
type: 'stream_restart',
|
|
1031
|
+
reason: err.message || 'Could not resume — restarting this turn',
|
|
1032
|
+
discardedChars: renderedChars,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
reportRetry('Could not resume — restarting this turn');
|
|
1036
|
+
await sleepWithinBudget(backoffMs(sAttempt));
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
942
1039
|
// PREFER RESUMING over restarting.
|
|
943
1040
|
//
|
|
944
1041
|
// The turn is still generating on the server (a dropped socket no longer aborts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.16",
|
|
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)",
|