@nexrall/code-core 1.4.40 → 1.4.41
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 +1 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +120 -18
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +95 -3
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -70,7 +70,7 @@ export declare function executeAgentMemoryWrite(input: Record<string, unknown>,
|
|
|
70
70
|
* final answer, or the user pressed Ctrl+C and already knows why it stopped.
|
|
71
71
|
* EVERYTHING else owes the user an explanation, which is what stopReasonNotice covers.
|
|
72
72
|
*/
|
|
73
|
-
export type StopReason = 'clean' | 'aborted' | 'reported-elsewhere' | 'empty-response' | 'no-balance' | 'stalled' | 'stalled-repeat' | 'budget' | 'unknown';
|
|
73
|
+
export type StopReason = 'clean' | 'aborted' | 'reported-elsewhere' | 'empty-response' | 'output-limit' | 'no-balance' | 'stalled' | 'stalled-repeat' | 'budget' | 'unknown';
|
|
74
74
|
/**
|
|
75
75
|
* The message shown when a run ends for any reason other than a clean finish.
|
|
76
76
|
*
|
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,EAAkE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrH,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,CAAC;AA0K7B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9C,MAAM,CAKR;AAED,+CAA+C;AAC/C,eAAO,MAAM,YAAY;;;CAAsC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAgB3B,CAAC;AAEX,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,SAAS,GACT,oBAAoB,GACpB,gBAAgB,GAChB,YAAY,GACZ,SAAS,GACT,gBAAgB,GAChB,QAAQ,GACR,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACzD,MAAM,GAAG,IAAI,
|
|
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
|
@@ -331,6 +331,17 @@ function stopReasonNotice(reason, ctx = {}) {
|
|
|
331
331
|
case 'empty-response':
|
|
332
332
|
return `\n\u26a0\ufe0f The model returned an empty response, so nothing was done. This is usually a transient ` +
|
|
333
333
|
`upstream hiccup \u2014 send "continue" to retry.\n`;
|
|
334
|
+
// Deliberately NOT folded into 'empty-response'. Both arrive as an assistant
|
|
335
|
+
// message with no content once thinking blocks are stripped, so they used to
|
|
336
|
+
// be indistinguishable — and the user was told the truncation case was "a
|
|
337
|
+
// transient upstream hiccup" they should retry with "continue". Both halves
|
|
338
|
+
// were wrong: nothing was transient, and continuing re-runs the same prompt
|
|
339
|
+
// with the same budget and the same reasoning behaviour, reproducing it
|
|
340
|
+
// exactly. Naming the cause is the fix; the advice has to change with it.
|
|
341
|
+
case 'output-limit':
|
|
342
|
+
return `\n\u26a0\ufe0f The model used its entire output budget on internal reasoning and was cut off ` +
|
|
343
|
+
`before it could reply, so nothing was done. This will repeat identically if you just retry \u2014 ` +
|
|
344
|
+
`lower the reasoning effort (/effort) or split the task into smaller steps.\n`;
|
|
334
345
|
case 'no-balance':
|
|
335
346
|
return `\n\ud83d\udcb3 Stopped: your balance is empty, so the request was rejected before it started. ` +
|
|
336
347
|
`Top up and send "continue" \u2014 no tokens were used for this turn.\n`;
|
|
@@ -2076,6 +2087,26 @@ function originalTaskText(messages) {
|
|
|
2076
2087
|
.join('\n')
|
|
2077
2088
|
.trim();
|
|
2078
2089
|
}
|
|
2090
|
+
/**
|
|
2091
|
+
* The summariser's own model call failed — as opposed to running fine but not
|
|
2092
|
+
* shrinking anything.
|
|
2093
|
+
*
|
|
2094
|
+
* These two outcomes used to be one `return false`, and collapsing them was a
|
|
2095
|
+
* real bug: the in-loop circuit breaker disables auto-compaction permanently
|
|
2096
|
+
* after COMPACT_MAX_FAILURES, on the sound theory that a compaction which cannot
|
|
2097
|
+
* reclaim bytes will never start. But a summariser that THREW says nothing about
|
|
2098
|
+
* whether compaction would help — only that the network/provider was unavailable
|
|
2099
|
+
* for a moment. Feeding those into the same counter meant three transient blips
|
|
2100
|
+
* (a 529 burst, a brief outage, a rate-limit spike) permanently switched off the
|
|
2101
|
+
* one mechanism keeping the context under control, and the run then died at the
|
|
2102
|
+
* context wall minutes later with its own recovery already disabled.
|
|
2103
|
+
*/
|
|
2104
|
+
class CompactionUnavailableError extends Error {
|
|
2105
|
+
constructor() {
|
|
2106
|
+
super('Compaction summariser was unavailable');
|
|
2107
|
+
this.name = 'CompactionUnavailableError';
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2079
2110
|
async function autoCompactMessages(messages, options, ledger) {
|
|
2080
2111
|
const cut = findSafeCutIndex(messages, messages.length - COMPACT_KEEP_MIN);
|
|
2081
2112
|
if (cut < 2)
|
|
@@ -2124,7 +2155,13 @@ async function autoCompactMessages(messages, options, ledger) {
|
|
|
2124
2155
|
.trim();
|
|
2125
2156
|
}
|
|
2126
2157
|
catch {
|
|
2127
|
-
|
|
2158
|
+
// Summarisation FAILED — the model call itself threw (network blip, 529,
|
|
2159
|
+
// provider quota). Distinguished from "ran fine but didn't help" by the
|
|
2160
|
+
// caller, because the two must not feed the same circuit breaker: three
|
|
2161
|
+
// transient network errors would otherwise permanently disable compaction
|
|
2162
|
+
// for the rest of the run, leaving the context to grow until the turn dies
|
|
2163
|
+
// with no recovery left. Leave history as is; the turn may still fit.
|
|
2164
|
+
throw new CompactionUnavailableError();
|
|
2128
2165
|
}
|
|
2129
2166
|
if (!summary)
|
|
2130
2167
|
return false;
|
|
@@ -2264,17 +2301,30 @@ async function compactMessagesForResume(messages, opts) {
|
|
|
2264
2301
|
// No ledger at resume time — the ledger is per-run, in-memory, and would
|
|
2265
2302
|
// have been created fresh anyway since this is a new process/run. The
|
|
2266
2303
|
// ORIGINAL TASK verbatim pin (inside autoCompactMessages) still applies.
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2304
|
+
// A summariser failure is not fatal HERE. This runs before the session is
|
|
2305
|
+
// handed back to the user, so the worst case is resuming with a longer (more
|
|
2306
|
+
// expensive) prefix — strictly better than refusing to resume at all. The
|
|
2307
|
+
// in-loop caller treats the same signal differently, because there it must
|
|
2308
|
+
// decide whether to arm a circuit breaker.
|
|
2309
|
+
let did;
|
|
2310
|
+
try {
|
|
2311
|
+
did = await autoCompactMessages(messages, {
|
|
2312
|
+
workDir: opts.workDir,
|
|
2313
|
+
model: opts.model,
|
|
2314
|
+
clientType: opts.clientType,
|
|
2315
|
+
env: opts.env,
|
|
2316
|
+
onText: () => { },
|
|
2317
|
+
onToolUse: () => { },
|
|
2318
|
+
onToolResult: () => { },
|
|
2319
|
+
onUsage: () => { },
|
|
2320
|
+
requestPermission: async () => false,
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
catch (err) {
|
|
2324
|
+
if (err instanceof CompactionUnavailableError)
|
|
2325
|
+
break;
|
|
2326
|
+
throw err;
|
|
2327
|
+
}
|
|
2278
2328
|
if (!did)
|
|
2279
2329
|
break;
|
|
2280
2330
|
compacted = true;
|
|
@@ -2430,6 +2480,19 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2430
2480
|
// the in-loop path in line with it.
|
|
2431
2481
|
let compactFailures = 0;
|
|
2432
2482
|
let compactDisabled = false;
|
|
2483
|
+
// Body size at the moment the breaker tripped, so it can be RE-ARMED if the
|
|
2484
|
+
// conversation later grows substantially past that point.
|
|
2485
|
+
//
|
|
2486
|
+
// Without this the breaker is a one-way latch for the whole run: "compaction
|
|
2487
|
+
// couldn't shrink THIS history" silently becomes "compaction will never help
|
|
2488
|
+
// again", even after several more turns of tool output have been appended —
|
|
2489
|
+
// a materially different history, and one where summarising may well work.
|
|
2490
|
+
// The run would instead sail into the context wall with its own safety net
|
|
2491
|
+
// switched off. Requiring real growth (not just "still over threshold")
|
|
2492
|
+
// is what keeps this from re-entering the useless-retry loop the breaker
|
|
2493
|
+
// exists to stop.
|
|
2494
|
+
let compactDisabledAtBytes = 0;
|
|
2495
|
+
const COMPACT_REARM_GROWTH = 1.5; // 50% larger than when we gave up
|
|
2433
2496
|
try {
|
|
2434
2497
|
for (; iteration < budget; iteration++) {
|
|
2435
2498
|
if (options.abortSignal?.aborted) {
|
|
@@ -2480,6 +2543,12 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2480
2543
|
(options.onNotice ?? options.onText)(`\u267b\ufe0f Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of already-processed tool output to keep this chat cheap to continue.`);
|
|
2481
2544
|
}
|
|
2482
2545
|
}
|
|
2546
|
+
// Re-arm the breaker once the conversation has grown materially past the
|
|
2547
|
+
// size at which compaction was given up on. See compactDisabledAtBytes.
|
|
2548
|
+
if (compactDisabled && bodyBytes > compactDisabledAtBytes * COMPACT_REARM_GROWTH) {
|
|
2549
|
+
compactDisabled = false;
|
|
2550
|
+
compactFailures = 0;
|
|
2551
|
+
}
|
|
2483
2552
|
if (autoCompact && !compactDisabled && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
|
|
2484
2553
|
compacting = true;
|
|
2485
2554
|
try {
|
|
@@ -2488,7 +2557,19 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2488
2557
|
// reclaims nothing is a failure for our purposes — it leaves the trigger
|
|
2489
2558
|
// armed for the next iteration, which is precisely the runaway.
|
|
2490
2559
|
const bytesBefore = estimateBodyBytes(messages);
|
|
2491
|
-
|
|
2560
|
+
// Separated from `did` because they answer different questions: `did`
|
|
2561
|
+
// is "was history rewritten", `unavailable` is "did we even get to
|
|
2562
|
+
// find out". Only the former may feed the circuit breaker.
|
|
2563
|
+
let did = false;
|
|
2564
|
+
let unavailable = false;
|
|
2565
|
+
try {
|
|
2566
|
+
did = await autoCompactMessages(messages, options, ledger);
|
|
2567
|
+
}
|
|
2568
|
+
catch (err) {
|
|
2569
|
+
if (!(err instanceof CompactionUnavailableError))
|
|
2570
|
+
throw err;
|
|
2571
|
+
unavailable = true;
|
|
2572
|
+
}
|
|
2492
2573
|
const bytesAfter = did ? estimateBodyBytes(messages) : bytesBefore;
|
|
2493
2574
|
const reclaimed = bytesBefore - bytesAfter;
|
|
2494
2575
|
if (did) {
|
|
@@ -2506,17 +2587,28 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2506
2587
|
// Productive == it shrank the body meaningfully. A successful-but-useless
|
|
2507
2588
|
// compaction counts as a failure, otherwise the "cannot get under the byte
|
|
2508
2589
|
// cap" case would never trip the breaker.
|
|
2509
|
-
|
|
2590
|
+
//
|
|
2591
|
+
// `unavailable` is excluded entirely — not counted as a failure AND not
|
|
2592
|
+
// counted as a success. The breaker exists to stop re-summarising a
|
|
2593
|
+
// conversation that provably cannot shrink; a summariser that never ran
|
|
2594
|
+
// is no evidence either way, and treating it as failure is what used to
|
|
2595
|
+
// let three network blips permanently disable compaction mid-run.
|
|
2596
|
+
if (unavailable) {
|
|
2597
|
+
// Nothing to record. Pressure is unchanged, so the next iteration
|
|
2598
|
+
// retries — which is the correct response to a transient outage.
|
|
2599
|
+
}
|
|
2600
|
+
else if (did && reclaimed >= COMPACT_MIN_RECLAIM_BYTES) {
|
|
2510
2601
|
compactFailures = 0;
|
|
2511
2602
|
}
|
|
2512
2603
|
else if (++compactFailures >= COMPACT_MAX_FAILURES) {
|
|
2513
2604
|
compactDisabled = true;
|
|
2605
|
+
compactDisabledAtBytes = bytesAfter;
|
|
2514
2606
|
// Surfaced ONCE. The user needs to know the automatic safety net is off
|
|
2515
2607
|
// (so a context-window error later isn't a total surprise) and what to do
|
|
2516
2608
|
// about it, but repeating this every iteration would be its own spam.
|
|
2517
|
-
(options.onNotice ?? options.onText)(`\u26a0\ufe0f Auto-compaction isn't reducing this conversation any further, so it's
|
|
2518
|
-
`
|
|
2519
|
-
`chat or run /compact manually.`);
|
|
2609
|
+
(options.onNotice ?? options.onText)(`\u26a0\ufe0f Auto-compaction isn't reducing this conversation any further, so it's paused to avoid ` +
|
|
2610
|
+
`repeated summarising. It will be retried automatically if the conversation grows substantially. ` +
|
|
2611
|
+
`If the context fills up before then, start a fresh chat or run /compact manually.`);
|
|
2520
2612
|
}
|
|
2521
2613
|
}
|
|
2522
2614
|
finally {
|
|
@@ -2706,8 +2798,18 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2706
2798
|
// caller the task succeeded when the model had in fact produced nothing at all
|
|
2707
2799
|
// — indistinguishable, to the user, from a finished job with no summary. An
|
|
2708
2800
|
// empty turn is a (usually transient) upstream failure, so name it and say so.
|
|
2801
|
+
//
|
|
2802
|
+
// …but only ONE of the two ways to get here is transient. `stopReason` must be
|
|
2803
|
+
// consulted RIGHT HERE, before breaking: it is read again further down for the
|
|
2804
|
+
// truncated-tool-call guard, but that code is unreachable from this branch, so
|
|
2805
|
+
// this used to report every truncation as a transient "empty response" and tell
|
|
2806
|
+
// the user to send "continue" — advice that reproduces the failure verbatim.
|
|
2807
|
+
//
|
|
2808
|
+
// 'max_tokens' means the model was CUT OFF at its output ceiling. Landing here
|
|
2809
|
+
// with empty content means the entire budget went to thinking blocks that were
|
|
2810
|
+
// just stripped, leaving nothing to show. Deterministic, not a hiccup.
|
|
2709
2811
|
runSimpleHooks(hooks.PostMessageComplete, options.workDir);
|
|
2710
|
-
stopReason = 'empty-response';
|
|
2812
|
+
stopReason = assistantMessage.stopReason === 'max_tokens' ? 'output-limit' : 'empty-response';
|
|
2711
2813
|
break;
|
|
2712
2814
|
}
|
|
2713
2815
|
// stopReason is transient, single-turn metadata for the truncated-tool-call guard
|
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;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;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;
|
|
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;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;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AA8ID,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,CAimClB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAYxD;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CA0BlD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA+B1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BhF"}
|
package/dist/api/client.js
CHANGED
|
@@ -226,6 +226,89 @@ const MAX_TOTAL_RETRY_MS = (() => {
|
|
|
226
226
|
const raw = Number(process.env.NEXRALL_MAX_RETRY_MS);
|
|
227
227
|
return Number.isFinite(raw) && raw > 0 ? raw : 5 * 60000;
|
|
228
228
|
})();
|
|
229
|
+
/**
|
|
230
|
+
* Ceiling on how long `fetch()` may sit waiting for RESPONSE HEADERS.
|
|
231
|
+
*
|
|
232
|
+
* This closes a genuine hang, observed in production: a Terminal-Bench trial went
|
|
233
|
+
* silent 38 s in and stayed silent for 15 minutes until the harness killed it,
|
|
234
|
+
* with ZERO retry events emitted — proof no watchdog ever fired.
|
|
235
|
+
*
|
|
236
|
+
* The reason none could: every watchdog in this file lives inside the stream-
|
|
237
|
+
* reading promise, and that promise is only constructed once `response.body`
|
|
238
|
+
* exists. `await fetch(...)` resolves when headers arrive, so the window between
|
|
239
|
+
* "socket opened" and "headers received" is guarded by NOTHING. A server that
|
|
240
|
+
* accepts the TCP connection and then never replies (a wedged proxy, a pod
|
|
241
|
+
* mid-eviction, a silently dropped connection where no RST is delivered) parks
|
|
242
|
+
* that await forever. `signal` alone does not help: it only carries a manual
|
|
243
|
+
* abort, and there is no human present in a headless run.
|
|
244
|
+
*
|
|
245
|
+
* 120 s is deliberately generous relative to time-to-first-token — it does NOT
|
|
246
|
+
* bound generation. The backend calls res.flushHeaders() before it ever touches
|
|
247
|
+
* the model (routes/code.js), so headers are due within a network round trip
|
|
248
|
+
* plus request validation, no matter how large the prompt or how long the model
|
|
249
|
+
* subsequently thinks. Anything past two minutes here is a dead connection, not
|
|
250
|
+
* a slow one.
|
|
251
|
+
*
|
|
252
|
+
* Read at CALL time, not module-load time. Every other tunable in this file that
|
|
253
|
+
* tests need to reach (NEXRALL_PROGRESS_TIMEOUT_MS, NEXRALL_FIRST_EVENT_TIMEOUT_MS)
|
|
254
|
+
* is read inside the function that uses it for the same reason: this module is
|
|
255
|
+
* imported once per process, so a load-time constant would freeze whatever the
|
|
256
|
+
* environment happened to be at import and silently ignore any later override —
|
|
257
|
+
* making the timeout path untestable and env configuration a no-op for anything
|
|
258
|
+
* that sets it after startup.
|
|
259
|
+
*/
|
|
260
|
+
const headerTimeoutMs = () => Number(process.env.NEXRALL_HEADER_TIMEOUT_MS) || 120000;
|
|
261
|
+
/**
|
|
262
|
+
* `fetch` with a bound on the header-wait phase ONLY.
|
|
263
|
+
*
|
|
264
|
+
* The subtlety that makes this worth a helper: the timer MUST be cleared as soon
|
|
265
|
+
* as headers land. The returned body is consumed for minutes afterwards (that is
|
|
266
|
+
* the entire point of a streaming turn), and this signal stays attached to the
|
|
267
|
+
* live response — a timer left running would abort a perfectly healthy stream
|
|
268
|
+
* mid-generation at the two-minute mark. Streaming is instead policed by the
|
|
269
|
+
* heartbeat/progress watchdogs downstream, which understand SSE framing.
|
|
270
|
+
*
|
|
271
|
+
* The caller's own abort signal is chained in, so a user pressing Stop during
|
|
272
|
+
* the header wait still cancels immediately.
|
|
273
|
+
*/
|
|
274
|
+
async function fetchWithHeaderTimeout(url, init, outerSignal, timeoutMs = headerTimeoutMs()) {
|
|
275
|
+
const headerController = new AbortController();
|
|
276
|
+
const onOuterAbort = () => headerController.abort();
|
|
277
|
+
if (outerSignal.aborted)
|
|
278
|
+
headerController.abort();
|
|
279
|
+
else
|
|
280
|
+
outerSignal.addEventListener('abort', onOuterAbort, { once: true });
|
|
281
|
+
let timedOut = false;
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
timedOut = true;
|
|
284
|
+
headerController.abort();
|
|
285
|
+
}, timeoutMs);
|
|
286
|
+
try {
|
|
287
|
+
return await (0, node_fetch_1.default)(url, { ...init, signal: headerController.signal });
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
// Distinguish OUR timeout from the user's Stop. Without this the caller sees
|
|
291
|
+
// a bare AbortError and treats a dead server as a deliberate cancellation,
|
|
292
|
+
// which exits the run silently instead of retrying — the same class of
|
|
293
|
+
// "looks like the user did it" mistake the abort-vs-error split elsewhere
|
|
294
|
+
// in this file exists to prevent.
|
|
295
|
+
if (timedOut && !outerSignal.aborted) {
|
|
296
|
+
throw Object.assign(new Error(`The server accepted the connection but sent no response within ` +
|
|
297
|
+
`${Math.round(timeoutMs / 1000)} s. Reconnecting…`),
|
|
298
|
+
// Retryable by definition: nothing was received, so a fresh attempt
|
|
299
|
+
// cannot duplicate any rendered output.
|
|
300
|
+
{ retryable: true });
|
|
301
|
+
}
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
// Both of these are load-bearing, not hygiene. Leaving the timer armed would
|
|
306
|
+
// abort the live stream two minutes into a healthy turn; leaving the listener
|
|
307
|
+
// attached leaks one entry per attempt onto a signal that outlives them all.
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
outerSignal.removeEventListener('abort', onOuterAbort);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
229
312
|
/** Exponential backoff with full jitter, capped at RETRY_MAX_MS. The jitter spreads
|
|
230
313
|
* retries out so a fleet of clients hitting the SAME overloaded API (529) don't all
|
|
231
314
|
* reconnect in lockstep and re-trigger the overload (thundering herd). Returns a delay
|
|
@@ -315,14 +398,19 @@ async function streamChat(messages, options, onEvent) {
|
|
|
315
398
|
// of the one already generating server-side (GET /chat/resume + Last-Event-ID). The
|
|
316
399
|
// resume carries no body at all — the conversation is already being processed; re-sending
|
|
317
400
|
// it is exactly the waste this avoids.
|
|
401
|
+
// The `signal` set here is a FALLBACK: fetchWithHeaderTimeout overrides it with a
|
|
402
|
+
// chained one so the header wait can be timed out independently. It stays because
|
|
403
|
+
// this shape is also what an unwrapped fetch would need, and a request with no
|
|
404
|
+
// signal at all would silently become uncancellable if that wrapper were removed.
|
|
318
405
|
const buildFetchArgs = () => {
|
|
406
|
+
const signal = controller.signal;
|
|
319
407
|
if (resuming) {
|
|
320
408
|
return [
|
|
321
409
|
`${exports.API_BASE}/api/code/chat/resume?turnId=${encodeURIComponent(turnId)}`,
|
|
322
410
|
{
|
|
323
411
|
method: 'GET',
|
|
324
412
|
headers: { ...authHeaders(), 'Last-Event-ID': String(lastEventId) },
|
|
325
|
-
signal
|
|
413
|
+
signal,
|
|
326
414
|
},
|
|
327
415
|
];
|
|
328
416
|
}
|
|
@@ -332,7 +420,7 @@ async function streamChat(messages, options, onEvent) {
|
|
|
332
420
|
method: 'POST',
|
|
333
421
|
headers: authHeaders(),
|
|
334
422
|
body: JSON.stringify({ messages, model, env, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills, turnId, canWriteSharedMemory, hasOwnAgentStore, canSpawnSubAgents }),
|
|
335
|
-
signal
|
|
423
|
+
signal,
|
|
336
424
|
},
|
|
337
425
|
];
|
|
338
426
|
};
|
|
@@ -482,7 +570,11 @@ async function streamChat(messages, options, onEvent) {
|
|
|
482
570
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
483
571
|
try {
|
|
484
572
|
errorBodyOverride = null; // a fresh response — never reuse the previous body
|
|
485
|
-
|
|
573
|
+
// Header-phase timeout only — see fetchWithHeaderTimeout. It replaces the
|
|
574
|
+
// signal built here with its own (chained to controller.signal), because
|
|
575
|
+
// the timer must be able to abort the header wait without outliving that
|
|
576
|
+
// wait and killing the stream it was guarding.
|
|
577
|
+
response = await fetchWithHeaderTimeout(...buildFetchArgs(), controller.signal);
|
|
486
578
|
if (response.status === 429 && attempt < MAX_RETRIES && canRetry()) {
|
|
487
579
|
reportRetry('Rate limited by the API — retrying');
|
|
488
580
|
const retryAfter = parseInt(response.headers.get('retry-after') ?? '0', 10);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.41",
|
|
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)",
|