@nexrall/code-core 1.4.55 → 1.4.57
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/agent/loop.d.ts +26 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +96 -3
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +127 -10
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -2,6 +2,32 @@ import type { Message, ToolResult, AgentLoopOptions, EnvContext } from '../types
|
|
|
2
2
|
import { type AgentType, type AgentMemoryScope } from './agentTypes';
|
|
3
3
|
import { bashNeedsRepoLock } from '../permissions/bashClassify';
|
|
4
4
|
export { bashNeedsRepoLock };
|
|
5
|
+
/** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
|
|
6
|
+
export declare function emptyTurnBackoffMs(attempt: number): number;
|
|
7
|
+
/**
|
|
8
|
+
* Should an empty assistant turn be retried automatically, rather than surfaced?
|
|
9
|
+
*
|
|
10
|
+
* Split out as a pure function because the two "empty" causes look IDENTICAL at the
|
|
11
|
+
* call site (both are `content.length === 0`) and telling them apart is the entire
|
|
12
|
+
* point — the previous bug in this area was treating a deterministic truncation as a
|
|
13
|
+
* transient hiccup and advising a retry that reproduced it verbatim.
|
|
14
|
+
*
|
|
15
|
+
* • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
|
|
16
|
+
* OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
|
|
17
|
+
* behaviour, so it fails the same way while billing again. Never retried; the user
|
|
18
|
+
* is told to lower effort or split the task (stopReasonNotice 'output-limit').
|
|
19
|
+
* • anything else → a genuinely transient empty turn. Retried, up to the limit.
|
|
20
|
+
*
|
|
21
|
+
* The attempt cap matters as much as the classification: a model that has decided to
|
|
22
|
+
* return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
|
|
23
|
+
* on a paid endpoint. After the cap we fall back to the old visible notice.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldRetryEmptyTurn(stopReason: string | null | undefined, attemptsSoFar: number): boolean;
|
|
26
|
+
/** Empty-turn retry limits, exposed for tests. */
|
|
27
|
+
export declare const _emptyTurnRetry: {
|
|
28
|
+
EMPTY_TURN_RETRY_LIMIT: number;
|
|
29
|
+
EMPTY_TURN_RETRY_BASE_MS: number;
|
|
30
|
+
};
|
|
5
31
|
/**
|
|
6
32
|
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
7
33
|
*
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAKP,UAAU,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAIlB,OAAO,EAA8D,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMjI,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports.bashNeedsRepoLock = void 0;
|
|
36
|
+
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports._emptyTurnRetry = exports.bashNeedsRepoLock = void 0;
|
|
37
|
+
exports.emptyTurnBackoffMs = emptyTurnBackoffMs;
|
|
38
|
+
exports.shouldRetryEmptyTurn = shouldRetryEmptyTurn;
|
|
37
39
|
exports.errorRoundSignature = errorRoundSignature;
|
|
38
40
|
exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
|
|
39
41
|
exports.stopReasonNotice = stopReasonNotice;
|
|
@@ -226,6 +228,60 @@ const STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runawa
|
|
|
226
228
|
// being stuck than a total failure: legitimately retrying one failing command a few times
|
|
227
229
|
// while making progress elsewhere is normal, twelve times is not.
|
|
228
230
|
const REPEAT_STALL_LIMIT = 12;
|
|
231
|
+
// ─── Empty-turn auto-retry ────────────────────────────────────────────────────
|
|
232
|
+
//
|
|
233
|
+
// A model turn can come back STRUCTURALLY FINE (the stream completed, `message_complete`
|
|
234
|
+
// arrived) and still contain nothing usable once thinking blocks are stripped for
|
|
235
|
+
// history. Anthropic models hit this materially more often than the OpenAI-compatible
|
|
236
|
+
// ones, because only they emit `thinking`/`redacted_thinking` blocks — a turn that
|
|
237
|
+
// reasons and then ends without committing text or a tool call strips down to `[]`.
|
|
238
|
+
//
|
|
239
|
+
// The transport layer cannot fix this. client.ts only retries when the stream produced
|
|
240
|
+
// NOTHING; here the stream produced a valid message that happens to be empty, so it
|
|
241
|
+
// resolves normally and the loop used to break immediately and tell the user to type
|
|
242
|
+
// "continue" — asking a human to press a button the loop can press itself.
|
|
243
|
+
//
|
|
244
|
+
// Retrying here is safe for a specific, checkable reason, not a hopeful one:
|
|
245
|
+
// • tools are dispatched from `assistantMessage.content` AFTER this check, and this
|
|
246
|
+
// content is empty, so no tool ran and no file changed;
|
|
247
|
+
// • nothing was pushed to `messages` (the push happens below this branch), so the
|
|
248
|
+
// history is byte-identical to the previous attempt — a retry is a clean re-ask,
|
|
249
|
+
// not a resend of a corrupted turn;
|
|
250
|
+
// • no round was counted, no hook fired, no checkpoint was taken.
|
|
251
|
+
// Contrast the output-limit case, which is NOT retried: see shouldRetryEmptyTurn.
|
|
252
|
+
const EMPTY_TURN_RETRY_LIMIT = 3;
|
|
253
|
+
// Short, escalating pause (1s, 2s, 4s). Long enough to ride out the upstream blip that
|
|
254
|
+
// causes this; short enough that three failures cost ~7s rather than a visible stall.
|
|
255
|
+
const EMPTY_TURN_RETRY_BASE_MS = 1000;
|
|
256
|
+
/** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
|
|
257
|
+
function emptyTurnBackoffMs(attempt) {
|
|
258
|
+
return EMPTY_TURN_RETRY_BASE_MS * Math.pow(2, Math.max(0, attempt));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Should an empty assistant turn be retried automatically, rather than surfaced?
|
|
262
|
+
*
|
|
263
|
+
* Split out as a pure function because the two "empty" causes look IDENTICAL at the
|
|
264
|
+
* call site (both are `content.length === 0`) and telling them apart is the entire
|
|
265
|
+
* point — the previous bug in this area was treating a deterministic truncation as a
|
|
266
|
+
* transient hiccup and advising a retry that reproduced it verbatim.
|
|
267
|
+
*
|
|
268
|
+
* • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
|
|
269
|
+
* OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
|
|
270
|
+
* behaviour, so it fails the same way while billing again. Never retried; the user
|
|
271
|
+
* is told to lower effort or split the task (stopReasonNotice 'output-limit').
|
|
272
|
+
* • anything else → a genuinely transient empty turn. Retried, up to the limit.
|
|
273
|
+
*
|
|
274
|
+
* The attempt cap matters as much as the classification: a model that has decided to
|
|
275
|
+
* return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
|
|
276
|
+
* on a paid endpoint. After the cap we fall back to the old visible notice.
|
|
277
|
+
*/
|
|
278
|
+
function shouldRetryEmptyTurn(stopReason, attemptsSoFar) {
|
|
279
|
+
if (stopReason === 'max_tokens')
|
|
280
|
+
return false;
|
|
281
|
+
return attemptsSoFar < EMPTY_TURN_RETRY_LIMIT;
|
|
282
|
+
}
|
|
283
|
+
/** Empty-turn retry limits, exposed for tests. */
|
|
284
|
+
exports._emptyTurnRetry = { EMPTY_TURN_RETRY_LIMIT, EMPTY_TURN_RETRY_BASE_MS };
|
|
229
285
|
/**
|
|
230
286
|
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
231
287
|
*
|
|
@@ -331,9 +387,14 @@ function stopReasonNotice(reason, ctx = {}) {
|
|
|
331
387
|
// 'aborted' — keeps "the user cancelled" from silently coming to mean two things.
|
|
332
388
|
case 'reported-elsewhere':
|
|
333
389
|
return null;
|
|
390
|
+
// Reaching this notice now means the loop ALREADY retried automatically and the
|
|
391
|
+
// model came back empty every time (see shouldRetryEmptyTurn). Saying "send
|
|
392
|
+
// continue to retry" without that context reads as if nothing was tried, and the
|
|
393
|
+
// user's manual retry is then the fourth identical attempt — so name the attempts.
|
|
334
394
|
case 'empty-response':
|
|
335
|
-
return `\n\u26a0\ufe0f The model returned an empty response, so nothing was done.
|
|
336
|
-
`upstream hiccup
|
|
395
|
+
return `\n\u26a0\ufe0f The model returned an empty response ${EMPTY_TURN_RETRY_LIMIT} times in a row, so nothing was done. ` +
|
|
396
|
+
`This is usually a transient upstream hiccup that the agent retries by itself; it did not clear this time. ` +
|
|
397
|
+
`Send "continue" to try again, or switch model with /model if it persists.\n`;
|
|
337
398
|
// Deliberately NOT folded into 'empty-response'. Both arrive as an assistant
|
|
338
399
|
// message with no content once thinking blocks are stripped, so they used to
|
|
339
400
|
// be indistinguishable — and the user was told the truncation case was "a
|
|
@@ -2565,6 +2626,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2565
2626
|
let stalledRepeatError = null;
|
|
2566
2627
|
let budget = maxIterations; // extended by auto-continue, capped at hardCap
|
|
2567
2628
|
let iteration = 0;
|
|
2629
|
+
// Consecutive empty (thinking-only) model turns auto-retried in this run. Reset on
|
|
2630
|
+
// any turn that produces content, so a session that hits one blip an hour keeps its
|
|
2631
|
+
// full retry budget each time instead of slowly using it up over a long run.
|
|
2632
|
+
let emptyTurnRetries = 0;
|
|
2568
2633
|
// ─── Verification nudge (GAP D) ───────────────────────────────────────────────
|
|
2569
2634
|
// Coding agents commonly claim "done" after editing files without ever running a
|
|
2570
2635
|
// build/test/lint command to confirm the change actually works (the industry's
|
|
@@ -2953,10 +3018,38 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2953
3018
|
// 'max_tokens' means the model was CUT OFF at its output ceiling. Landing here
|
|
2954
3019
|
// with empty content means the entire budget went to thinking blocks that were
|
|
2955
3020
|
// just stripped, leaving nothing to show. Deterministic, not a hiccup.
|
|
3021
|
+
//
|
|
3022
|
+
// …and before giving up at all, RETRY IT. Nothing has been pushed to `messages`,
|
|
3023
|
+
// no tool ran (tools are dispatched from this same empty content, below), so the
|
|
3024
|
+
// next attempt re-asks the identical prompt with no duplicated side effects. This
|
|
3025
|
+
// is the fix for users being told to type "continue" for a hiccup the loop can
|
|
3026
|
+
// ride out itself; shouldRetryEmptyTurn keeps the deterministic `max_tokens`
|
|
3027
|
+
// truncation OUT of it, because retrying that reproduces it exactly.
|
|
3028
|
+
if (shouldRetryEmptyTurn(assistantMessage.stopReason, emptyTurnRetries)) {
|
|
3029
|
+
const waitMs = emptyTurnBackoffMs(emptyTurnRetries);
|
|
3030
|
+
emptyTurnRetries++;
|
|
3031
|
+
// Reuse the existing "reconnecting…" channel rather than printing a line into
|
|
3032
|
+
// the transcript: this is the same class of event (a transparent retry the user
|
|
3033
|
+
// does not have to act on), and clients already render + auto-clear it.
|
|
3034
|
+
options.onRetry?.(emptyTurnRetries, EMPTY_TURN_RETRY_LIMIT, 'The model returned an empty response — retrying automatically');
|
|
3035
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
3036
|
+
if (options.abortSignal?.aborted) {
|
|
3037
|
+
stopReason = 'aborted';
|
|
3038
|
+
break;
|
|
3039
|
+
}
|
|
3040
|
+
options.onRetryResolved?.();
|
|
3041
|
+
// Does NOT count as an iteration: a turn that produced nothing must not eat
|
|
3042
|
+
// the step budget, or three blips would silently shorten the task by three
|
|
3043
|
+
// steps. `continue` re-enters the loop body without incrementing.
|
|
3044
|
+
iteration--;
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
2956
3047
|
runSimpleHooks(hooks.PostMessageComplete, options.workDir);
|
|
2957
3048
|
stopReason = assistantMessage.stopReason === 'max_tokens' ? 'output-limit' : 'empty-response';
|
|
2958
3049
|
break;
|
|
2959
3050
|
}
|
|
3051
|
+
// A turn with real content: the blip (if any) is over, restore the full budget.
|
|
3052
|
+
emptyTurnRetries = 0;
|
|
2960
3053
|
// stopReason is transient, single-turn metadata for the truncated-tool-call guard
|
|
2961
3054
|
// below — it must never be replayed to the API as part of message history (Anthropic
|
|
2962
3055
|
// rejects unknown fields on a request message with a 400).
|
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.57",
|
|
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)",
|