@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.
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +127 -10
- 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;;;;;;;;;;;;;;;;;;;;;GAqBG;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;AAiGD,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;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"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"}
|
package/dist/api/client.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
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
|
|
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'
|
|
1303
|
-
//
|
|
1304
|
-
// the fallback below returns an EMPTY assistant message and the
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
|
|
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.
|
|
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)",
|