@nexrall/code-core 1.4.55 → 1.4.56

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":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
@@ -213,6 +213,42 @@ function isExpiredTokenResponse(status, body) {
213
213
  const MAX_RETRIES = 5;
214
214
  const RETRY_BASE_MS = 1000;
215
215
  const RETRY_MAX_MS = 30000;
216
+ /**
217
+ * SSE frames that are backend HOUSEKEEPING, not model output.
218
+ *
219
+ * These decide which STALL LIMIT a turn is held to: the generous time-to-first-token
220
+ * allowance (300 s) while the upstream is still processing the prompt, or the tighter
221
+ * mid-stream one (150 s) once tokens are flowing. See the `sawModelEvent` assignment
222
+ * in the parser for the full story.
223
+ *
224
+ * They are emitted by routes/code.js BEFORE (or independently of) the upstream model
225
+ * call, so their arrival proves only that the BACKEND is alive — never that the MODEL
226
+ * has started responding:
227
+ *
228
+ * resumable — turn registered + frame numbering announced. Emitted at
229
+ * routes/code.js:1151; openStream() is not called until :1318.
230
+ * This one alone held every production turn to the 150 s
231
+ * mid-stream limit during prompt processing.
232
+ * context_warning — server-side history trim/compact, already handled server-side.
233
+ * tool_progress — a liveness ping during long tool-argument generation.
234
+ *
235
+ * DELIBERATELY NOT LISTED (they mean the model really has started responding, and MUST
236
+ * count): text, thinking, thinking_delta, thinking_progress, tool_use,
237
+ * tool_result_info, message_complete, usage, balance_status, done, error.
238
+ *
239
+ * `thinking_progress` counts on purpose: it is derived from tokens the model has
240
+ * actually emitted, so time-to-first-token is genuinely over by then.
241
+ *
242
+ * Kept as an explicit ALLOWLIST of things to ignore rather than a denylist of things
243
+ * that count: a new backend frame type then defaults to "the model is responding",
244
+ * whose worst case is holding a turn to the tighter stall limit — visible and
245
+ * self-correcting, unlike the silent 300 s→150 s downgrade this list exists to stop.
246
+ */
247
+ const HOUSEKEEPING_FRAMES = new Set([
248
+ 'resumable',
249
+ 'context_warning',
250
+ 'tool_progress',
251
+ ]);
216
252
  /**
217
253
  * Wall-clock ceiling on ALL reconnect effort for a single streamChat() call.
218
254
  *
@@ -437,7 +473,21 @@ async function streamChat(messages, options, onEvent) {
437
473
  // see it. We detect it in the parser and retry the WHOLE attempt with exponential backoff
438
474
  // — but only while nothing has been emitted to the caller yet, so a retry can never
439
475
  // duplicate already-rendered text or tool calls.
440
- const isRetryableStreamMsg = (m) => /overloaded|rate.?limit|temporarily|unavailable|try again|internal server error/i.test(String(m ?? ''));
476
+ const isRetryableStreamMsg = (m) => {
477
+ const s = String(m ?? '');
478
+ // Anthropic's intermittent upstream refusal, reproduced verbatim through the AI
479
+ // Gateway passthrough:
480
+ // 403 {"error":{"type":"forbidden","message":"Request not allowed"}}
481
+ // Sporadic and gone on retry rather than a real permissions failure. A modern
482
+ // backend now flags this explicitly (`retryable` on the error frame), which is the
483
+ // reliable signal; this pattern is the FALLBACK for older backends that don't,
484
+ // and is deliberately narrow — it requires the upstream's own `"type":"forbidden"`
485
+ // envelope, so Nexrall's own auth 403s (a completely different body) stay terminal
486
+ // and a genuinely revoked key is still bounded by the capped retry budget.
487
+ if (/403[\s\S]*"type"\s*:\s*"forbidden"/i.test(s))
488
+ return true;
489
+ return /overloaded|rate.?limit|temporarily|unavailable|try again|internal server error/i.test(s);
490
+ };
441
491
  // Tracks whether a "reconnecting…" signal has been shown to the caller so we know to
442
492
  // clear it (retry_resolved) once real data starts flowing again. Shared across both
443
493
  // the connect-time retry loop and the outer stream-retry loop below, and across a
@@ -1056,10 +1106,35 @@ async function streamChat(messages, options, onEvent) {
1056
1106
  return;
1057
1107
  }
1058
1108
  const evt = parsed;
1059
- // Reaching here means a well-formed model event was parsed (": ping" keepalive
1109
+ // Reaching here means a well-formed frame was parsed (": ping" keepalive
1060
1110
  // comments never produce a ParsedEvent with data) — count it as real progress.
1111
+ //
1112
+ // `lastProgressAt` is bumped for EVERY frame, including the housekeeping ones
1113
+ // below: they prove the backend is alive and talking to us, which is exactly
1114
+ // what the stall watchdog measures.
1061
1115
  lastProgressAt = Date.now();
1062
- sawModelEvent = true;
1116
+ // …but `sawModelEvent` means something STRICTLY NARROWER: "the upstream model
1117
+ // has started responding". It selects which stall limit applies — the generous
1118
+ // time-to-first-token allowance (FIRST_EVENT_TIMEOUT_MS, 300 s) or the tighter
1119
+ // mid-stream one (PROGRESS_TIMEOUT_MS, 150 s) — so it must not be flipped by a
1120
+ // frame the BACKEND sent on its own initiative.
1121
+ //
1122
+ // routes/code.js emits `resumable` as soon as the turn is registered
1123
+ // (line ~1151) — BEFORE it calls openStream() (line ~1318), i.e. before the
1124
+ // model has been asked anything at all. Counting that as "the model is
1125
+ // responding" downgraded every turn to the 150 s mid-stream limit while the
1126
+ // upstream was still doing prompt processing, which is exactly the phase that
1127
+ // legitimately needs 300 s on a large cold-cache context. The result was
1128
+ // spurious stalls on big conversations that then "worked on retry" — because
1129
+ // the first attempt had warmed the prompt cache.
1130
+ //
1131
+ // NOTE: this flag deliberately no longer gates the empty-turn recovery in
1132
+ // `stream.on('end')`. That check now asks what the turn actually PRODUCED
1133
+ // (see `producedNothing` there), because "a frame arrived" and "content
1134
+ // exists" are different questions — conflating them is what made that guard
1135
+ // dead code in production.
1136
+ if (!HOUSEKEEPING_FRAMES.has(evt.type))
1137
+ sawModelEvent = true;
1063
1138
  // Real data is flowing again — clear any "reconnecting…" indicator the caller
1064
1139
  // may be showing from an earlier retry on THIS same attempt.
1065
1140
  clearRetryIfNeeded();
@@ -1250,6 +1325,26 @@ async function streamChat(messages, options, onEvent) {
1250
1325
  // — so this always needs a fresh turnId, which the outer loop mints via
1251
1326
  // `forceRestart`.
1252
1327
  const notResumable = evt.notResumable === true;
1328
+ // Does the BACKEND say this is worth retrying? Set by routes/code.js from
1329
+ // the upstream HTTP STATUS (403/408/409/429/5xx and bare transport
1330
+ // failures — see isTransientUpstreamError there), which is information
1331
+ // only the server has and which no amount of reading the message text can
1332
+ // reliably recover.
1333
+ //
1334
+ // This closes a real hole. `res.flushHeaders()` runs before the model is
1335
+ // called, so the HTTP status is pinned at 200 and an upstream failure can
1336
+ // only arrive as this frame — making the connect-time 403 retry logic
1337
+ // below unreachable in production. The only remaining hook was
1338
+ // `isRetryableStreamMsg`, whose regex does NOT match the string Anthropic
1339
+ // actually emits (`403 {"error":{"type":"forbidden",…}}`), so an
1340
+ // intermittent upstream refusal killed the run with a raw `⚠️ 403 …`
1341
+ // instead of being retried. Same lesson as `notResumable` directly above:
1342
+ // read a flag the server sets deliberately, never English prose.
1343
+ //
1344
+ // OR'd with the legacy text match rather than replacing it, so a client
1345
+ // running against an older backend that doesn't send the flag keeps
1346
+ // exactly its current behaviour.
1347
+ const serverSaysRetryable = evt.retryable === true;
1253
1348
  // Transient upstream failure before any output → let the outer loop retry it
1254
1349
  // transparently instead of killing the turn (this is the "Overloaded" case).
1255
1350
  // But never retry once the turn's message has already been delivered in
@@ -1266,7 +1361,7 @@ async function streamChat(messages, options, onEvent) {
1266
1361
  stream.destroy?.();
1267
1362
  reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
1268
1363
  }
1269
- else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
1364
+ else if ((serverSaysRetryable || isRetryableStreamMsg(message)) && (!emittedToCaller || allowRestartAfterRender)) {
1270
1365
  clearInterval(heartbeatWatchdog);
1271
1366
  stream.destroy?.();
1272
1367
  reject(tagTransient(new Error(message)));
@@ -1299,12 +1394,34 @@ async function streamChat(messages, options, onEvent) {
1299
1394
  });
1300
1395
  stream.on('end', () => {
1301
1396
  clearInterval(heartbeatWatchdog);
1302
- // A clean 'end' with ZERO model events means the connection was dropped before
1303
- // the backend produced anything (deploy restart, proxy reset). Without this,
1304
- // the fallback below returns an EMPTY assistant message and the agent loop
1305
- // treats it as a silent no-op the turn just vanishes. Nothing was emitted,
1306
- // so a retry cannot duplicate output tag retryable.
1307
- if (!sawModelEvent && !completedMessage) {
1397
+ // A clean 'end' that produced NOTHING USABLE means the turn died before the
1398
+ // model committed any content (deploy restart, proxy reset, upstream refusal).
1399
+ // Without this, the fallback below returns an EMPTY assistant message and the
1400
+ // agent loop reports "⚠️ The model returned an empty response" telling the
1401
+ // user to send "continue" by hand to do the retry that belongs here.
1402
+ //
1403
+ // The condition is deliberately "produced no usable content", NOT "saw no
1404
+ // frames". Those are different, and the difference is the whole bug:
1405
+ //
1406
+ // • `resumable` / `context_warning` / `tool_progress` are emitted by
1407
+ // routes/code.js BEFORE (or independently of) the model call, so keying on
1408
+ // "any frame arrived" made this guard dead code on every production turn —
1409
+ // `resumable` alone is sent at routes/code.js:1151, while openStream() is
1410
+ // not reached until :1318.
1411
+ // • `thinking_progress` is the subtler half: it IS real evidence the model is
1412
+ // working (it counts tokens the model emitted), so classifying frames by
1413
+ // name would keep it "counting" — yet thinking tokens are stripped from
1414
+ // history, so a turn that dies there still has zero content and still
1415
+ // surfaces as an empty response. Frame-name classification cannot express
1416
+ // that; asking what the turn actually PRODUCED can.
1417
+ //
1418
+ // Retrying is safe precisely because there is no content: with nothing
1419
+ // accumulated, a fresh attempt cannot duplicate anything on screen. Anything
1420
+ // already rendered (text/tool_use) leaves `textParts`/`toolUseBlocks` non-empty
1421
+ // and takes the normal resolve path, so a real partial answer is never
1422
+ // discarded or re-billed.
1423
+ const producedNothing = !completedMessage && textParts.length === 0 && toolUseBlocks.length === 0;
1424
+ if (producedNothing) {
1308
1425
  reject(Object.assign(new Error('Connection closed before the model responded. Retrying…'), { retryable: true }));
1309
1426
  return;
1310
1427
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.55",
3
+ "version": "1.4.56",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code \u2014 embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",