@nexrall/code-core 1.4.14 → 1.4.15

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":"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,CA80BlB;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"}
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,CA05BlB;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"}
@@ -328,6 +328,36 @@ async function streamChat(messages, options, onEvent) {
328
328
  await sleepWithinBudget(backoffMs(attempt));
329
329
  continue;
330
330
  }
331
+ // 403 `{"error":{"type":"forbidden","message":"Request not allowed"}}` is
332
+ // Anthropic's own upstream response shape (reproduced verbatim through the AI
333
+ // Gateway passthrough — see services/aiGatewayClient.js), not something Nexrall's
334
+ // auth layer produces (an expired/invalid Nexrall JWT is a 401/403 with a
335
+ // completely different body — see middleware/auth.js). Reports of this exact
336
+ // error on Anthropic's own CLI describe it as intermittent and gone on retry, not
337
+ // a real permissions failure — so unlike every OTHER 4xx below (which are
338
+ // deterministic given the same body and must not be retried), this one gets a
339
+ // short, capped number of attempts before being treated as terminal. Capped well
340
+ // below MAX_RETRIES so a genuinely revoked/banned key still fails fast instead of
341
+ // burning the whole reconnect budget on something that will never succeed.
342
+ const FORBIDDEN_RETRY_LIMIT = 2;
343
+ if (response.status === 403 && attempt < FORBIDDEN_RETRY_LIMIT && canRetry()) {
344
+ const bodyText = await response.text().catch(() => '');
345
+ let isUpstreamForbidden = false;
346
+ try {
347
+ const parsed = JSON.parse(bodyText);
348
+ isUpstreamForbidden = parsed?.error?.type === 'forbidden';
349
+ }
350
+ catch { /* not JSON — not this shape, fall through as terminal below */ }
351
+ if (isUpstreamForbidden) {
352
+ reportRetry('Upstream rejected the request — retrying');
353
+ await sleepWithinBudget(backoffMs(attempt));
354
+ continue;
355
+ }
356
+ // Not the known transient shape — treat as terminal, but reuse the body we
357
+ // already consumed (node-fetch bodies are single-use) instead of losing it.
358
+ errorBodyOverride = bodyText;
359
+ break;
360
+ }
331
361
  // 410 Gone — a resume was refused because the buffered window no longer contains
332
362
  // our position (we were away too long, or the turn produced a huge amount in the
333
363
  // meantime). Continuity can't be proven, and silently skipping frames would corrupt
@@ -841,6 +871,16 @@ async function streamChat(messages, options, onEvent) {
841
871
  case 'error': {
842
872
  const message = typeof evt.message === 'string' ? evt.message :
843
873
  typeof evt.error === 'string' ? evt.error : 'Unknown SSE error';
874
+ // The GET /chat/resume tail loop sends this when the turn's Redis frame
875
+ // buffer developed a gap (trimmed window, a swallowed flush error, or the
876
+ // owning pod died) or the turn is simply gone — see routes/code.js. Unlike
877
+ // `isRetryableStreamMsg` below, this is NOT a fuzzy text match: the backend
878
+ // sets the flag explicitly, so it can never silently stop matching if either
879
+ // side rewrites the English message. There is no cursor left to resume
880
+ // from — a plain "retryable" resume-with-Last-Event-ID would just 410 again
881
+ // — so this always needs a fresh turnId, which the outer loop mints via
882
+ // `forceRestart`.
883
+ const notResumable = evt.notResumable === true;
844
884
  // Transient upstream failure before any output → let the outer loop retry it
845
885
  // transparently instead of killing the turn (this is the "Overloaded" case).
846
886
  // But never retry once the turn's message has already been delivered in
@@ -852,6 +892,11 @@ async function streamChat(messages, options, onEvent) {
852
892
  onEvent({ type: 'error', message });
853
893
  resolve();
854
894
  }
895
+ else if (notResumable && (!emittedToCaller || allowRestartAfterRender)) {
896
+ clearInterval(heartbeatWatchdog);
897
+ stream.destroy?.();
898
+ reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
899
+ }
855
900
  else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
856
901
  clearInterval(heartbeatWatchdog);
857
902
  stream.destroy?.();
@@ -939,6 +984,38 @@ async function streamChat(messages, options, onEvent) {
939
984
  throw err;
940
985
  const e = err;
941
986
  if (e.retryable && sAttempt < MAX_RETRIES && canRetry()) {
987
+ // The backend explicitly said this turnId's buffer is unrecoverable (gap in the
988
+ // Redis frame stream, or the owning pod is gone — see the `notResumable` case in
989
+ // client.ts's SSE parser). Resuming with the same `lastEventId` would just hit
990
+ // the same wall again, so — exactly like the inline 410-while-resuming handling
991
+ // above — release the old claim, mint a fresh turnId, and restart clean instead
992
+ // of falling into the "prefer resuming" branch below.
993
+ if (e.forceRestart) {
994
+ await cancelTurn(turnId);
995
+ turnId = (0, crypto_1.randomUUID)();
996
+ resuming = false;
997
+ serverResumable = false;
998
+ lastEventId = 0;
999
+ carryText = [];
1000
+ carryToolUse = [];
1001
+ const hadRendered = emittedAnythingAcrossAttempts;
1002
+ const renderedChars = emittedCharsAcrossAttempts;
1003
+ emittedAnythingAcrossAttempts = false;
1004
+ emittedCharsAcrossAttempts = 0;
1005
+ if (hadRendered && !allowRestartAfterRender) {
1006
+ throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
1007
+ }
1008
+ if (hadRendered) {
1009
+ onEvent({
1010
+ type: 'stream_restart',
1011
+ reason: err.message || 'Could not resume — restarting this turn',
1012
+ discardedChars: renderedChars,
1013
+ });
1014
+ }
1015
+ reportRetry('Could not resume — restarting this turn');
1016
+ await sleepWithinBudget(backoffMs(sAttempt));
1017
+ continue;
1018
+ }
942
1019
  // PREFER RESUMING over restarting.
943
1020
  //
944
1021
  // 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.14",
3
+ "version": "1.4.15",
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)",