@nexrall/code-core 1.4.26 → 1.4.27
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 +42 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +193 -16
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +409 -381
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import type { Message, AgentLoopOptions, EnvContext } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
4
|
+
*
|
|
5
|
+
* Exported (with the limits) purely as a test seam: the guard's whole value is in the
|
|
6
|
+
* edge cases — that a DIFFERENT error each round must NOT trip it, that call order
|
|
7
|
+
* within a round is irrelevant, that a long error body doesn't make every occurrence
|
|
8
|
+
* look unique — and none of that is reachable without driving a live model loop.
|
|
9
|
+
*
|
|
10
|
+
* Sorted so parallel tool calls completing in a different order still compare equal;
|
|
11
|
+
* truncated because errors often embed a varying path or timestamp late in the string.
|
|
12
|
+
*/
|
|
13
|
+
export declare function errorRoundSignature(errored: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
error: string;
|
|
16
|
+
}>): string;
|
|
17
|
+
/** Runaway-guard limits, exposed for tests. */
|
|
18
|
+
export declare const _stallLimits: {
|
|
19
|
+
STALL_LIMIT: number;
|
|
20
|
+
REPEAT_STALL_LIMIT: number;
|
|
21
|
+
};
|
|
2
22
|
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
3
23
|
/**
|
|
4
24
|
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
@@ -111,6 +131,15 @@ export interface ProgressLedger {
|
|
|
111
131
|
tool: string;
|
|
112
132
|
edits: number;
|
|
113
133
|
}>;
|
|
134
|
+
/**
|
|
135
|
+
* DISTINCT paths ever touched, including any since evicted from `filesTouched`.
|
|
136
|
+
*
|
|
137
|
+
* The Map is bounded (see ledgerRecord), so `filesTouched.size` is a window, not a
|
|
138
|
+
* total. The preamble states "FILES CHANGED THIS SESSION (N)" as a fact the model
|
|
139
|
+
* reasons about, so N must not silently shrink when eviction kicks in on a very long
|
|
140
|
+
* run — that would tell the model less work happened than actually did.
|
|
141
|
+
*/
|
|
142
|
+
filesTouchedTotal: number;
|
|
114
143
|
verifications: Array<{
|
|
115
144
|
cmd: string;
|
|
116
145
|
ok: boolean;
|
|
@@ -121,6 +150,19 @@ export interface ProgressLedger {
|
|
|
121
150
|
path: string;
|
|
122
151
|
reason: string;
|
|
123
152
|
}>;
|
|
153
|
+
/**
|
|
154
|
+
* TOTAL test-integrity findings ever recorded, including ones since trimmed.
|
|
155
|
+
*
|
|
156
|
+
* `testIntegrity` is a bounded window (older entries are spliced off once it grows
|
|
157
|
+
* past 2×LEDGER_MAX_NOTES), so its `.length` STOPS being a running total after the
|
|
158
|
+
* first trim. The one-shot nudge compares "how many findings exist" against "how
|
|
159
|
+
* many I've already surfaced", and comparing against a window that shrinks meant the
|
|
160
|
+
* count could never move ahead again — silently disabling the reward-hacking warning
|
|
161
|
+
* for the remainder of a long session, i.e. exactly when it matters most.
|
|
162
|
+
*
|
|
163
|
+
* This counter only ever increases, so it is a safe basis for that comparison.
|
|
164
|
+
*/
|
|
165
|
+
testIntegrityTotal: number;
|
|
124
166
|
/**
|
|
125
167
|
* Monotonic mutation epoch: incremented on every successful source write. Two
|
|
126
168
|
* verification runs sharing an epoch had NO edit between them, so a PASS↔FAIL
|
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,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,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,8 @@ 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 = void 0;
|
|
36
|
+
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports._stallLimits = void 0;
|
|
37
|
+
exports.errorRoundSignature = errorRoundSignature;
|
|
37
38
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
38
39
|
exports.createLimiter = createLimiter;
|
|
39
40
|
exports.extractSubTaskText = extractSubTaskText;
|
|
@@ -189,6 +190,30 @@ const DEFAULT_MAX_ITERATIONS = 500;
|
|
|
189
190
|
const MAX_ITERATIONS_CEILING = 2000; // default auto-continue backstop (no explicit opt-in)
|
|
190
191
|
const HARD_ITERATIONS_CAP = 100000; // absolute safety cap — even explicit opt-in can't exceed this
|
|
191
192
|
const STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runaway guard)
|
|
193
|
+
// Consecutive rounds producing the IDENTICAL error(s) → give up, even if other calls in
|
|
194
|
+
// those rounds succeeded. Higher than STALL_LIMIT because a repeat is weaker evidence of
|
|
195
|
+
// being stuck than a total failure: legitimately retrying one failing command a few times
|
|
196
|
+
// while making progress elsewhere is normal, twelve times is not.
|
|
197
|
+
const REPEAT_STALL_LIMIT = 12;
|
|
198
|
+
/**
|
|
199
|
+
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
200
|
+
*
|
|
201
|
+
* Exported (with the limits) purely as a test seam: the guard's whole value is in the
|
|
202
|
+
* edge cases — that a DIFFERENT error each round must NOT trip it, that call order
|
|
203
|
+
* within a round is irrelevant, that a long error body doesn't make every occurrence
|
|
204
|
+
* look unique — and none of that is reachable without driving a live model loop.
|
|
205
|
+
*
|
|
206
|
+
* Sorted so parallel tool calls completing in a different order still compare equal;
|
|
207
|
+
* truncated because errors often embed a varying path or timestamp late in the string.
|
|
208
|
+
*/
|
|
209
|
+
function errorRoundSignature(errored) {
|
|
210
|
+
return errored
|
|
211
|
+
.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`)
|
|
212
|
+
.sort()
|
|
213
|
+
.join('|');
|
|
214
|
+
}
|
|
215
|
+
/** Runaway-guard limits, exposed for tests. */
|
|
216
|
+
exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
|
|
192
217
|
// Resolve the soft iteration budget. Precedence:
|
|
193
218
|
// options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
|
|
194
219
|
//
|
|
@@ -830,6 +855,26 @@ function compactionThresholds() {
|
|
|
830
855
|
// so we require at least this many bytes reclaimed before accepting a prune.
|
|
831
856
|
const PRUNE_MIN_RECLAIM_BYTES = 256 * 1024; // 256 KB
|
|
832
857
|
const COMPACT_KEEP_MIN = 6; // always keep at least the last N messages verbatim
|
|
858
|
+
/**
|
|
859
|
+
* Bytes a compaction must reclaim to count as productive.
|
|
860
|
+
*
|
|
861
|
+
* Deliberately much smaller than PRUNE_MIN_RECLAIM_BYTES: a prune declines when the
|
|
862
|
+
* gain isn't worth busting the prompt cache, whereas by the time we are summarising
|
|
863
|
+
* we are already committed to rewriting the prefix — the only question is whether the
|
|
864
|
+
* summariser is making ANY headway. 32 KB is small enough that a genuinely useful
|
|
865
|
+
* compaction always clears it, large enough that shuffling a few bytes doesn't.
|
|
866
|
+
*/
|
|
867
|
+
const COMPACT_MIN_RECLAIM_BYTES = 32 * 1024; // 32 KB
|
|
868
|
+
/**
|
|
869
|
+
* Consecutive non-productive compaction attempts before auto-compaction is switched
|
|
870
|
+
* off for the rest of the run.
|
|
871
|
+
*
|
|
872
|
+
* 3 rather than 1 because the failure is often transient — a summariser stream that
|
|
873
|
+
* blipped will usually succeed on the next turn, and giving up instantly would lose
|
|
874
|
+
* the safety net for a whole long session over one network hiccup. 3 also bounds the
|
|
875
|
+
* wasted spend: at most three summariser calls, not hundreds.
|
|
876
|
+
*/
|
|
877
|
+
const COMPACT_MAX_FAILURES = 3;
|
|
833
878
|
// Byte-level safety net, independent of the token estimate.
|
|
834
879
|
//
|
|
835
880
|
// Tool-heavy sessions on large codebases accumulate many tool_result blocks
|
|
@@ -1013,7 +1058,7 @@ function transcriptOf(messages) {
|
|
|
1013
1058
|
const LEDGER_MAX_FILES = 60; // cap the file list so the preamble can't balloon
|
|
1014
1059
|
const LEDGER_MAX_NOTES = 20; // cap verification/among notes
|
|
1015
1060
|
function createLedger() {
|
|
1016
|
-
return { filesTouched: new Map(), verifications: [], testIntegrity: [], epoch: 0 };
|
|
1061
|
+
return { filesTouched: new Map(), filesTouchedTotal: 0, verifications: [], testIntegrity: [], testIntegrityTotal: 0, epoch: 0 };
|
|
1017
1062
|
}
|
|
1018
1063
|
/** Record one tool call's effect on the ledger (deterministic, no model call). */
|
|
1019
1064
|
function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
|
|
@@ -1026,7 +1071,32 @@ function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
|
|
|
1026
1071
|
const p = typeof input?.path === 'string' ? input.path : undefined;
|
|
1027
1072
|
if (p) {
|
|
1028
1073
|
const prev = ledger.filesTouched.get(p);
|
|
1074
|
+
if (!prev)
|
|
1075
|
+
ledger.filesTouchedTotal++;
|
|
1076
|
+
// DELETE before SET, so a re-touched path moves to the BACK of the insertion
|
|
1077
|
+
// order. `Map.set` on an existing key keeps its ORIGINAL slot, which quietly
|
|
1078
|
+
// broke the eviction policy below: a file edited hundreds of times over a long
|
|
1079
|
+
// session kept the position of its FIRST edit, so it aged out like a file nobody
|
|
1080
|
+
// had looked at since — and on the next edit it was re-inserted as "new", double-
|
|
1081
|
+
// counting filesTouchedTotal (which is documented as DISTINCT paths). Making the
|
|
1082
|
+
// Map a true LRU-by-touch is what lets the `key !== p` guard below mean anything.
|
|
1083
|
+
ledger.filesTouched.delete(p);
|
|
1029
1084
|
ledger.filesTouched.set(p, { tool: toolName, edits: (prev?.edits ?? 0) + 1 });
|
|
1085
|
+
// Bound the Map itself, not just its rendering. LEDGER_MAX_FILES caps how many
|
|
1086
|
+
// paths the preamble PRINTS (see ledgerSummary's slice), but the Map was only ever
|
|
1087
|
+
// written to — so a multi-hour run touching thousands of files grew it without
|
|
1088
|
+
// limit, and it is deliberately retained across every compaction. Evict the
|
|
1089
|
+
// least-recently-touched entries once we hold well beyond what can ever be
|
|
1090
|
+
// displayed. Hysteresis (evict down to 2× only once we exceed 4×) keeps this an
|
|
1091
|
+
// occasional bulk sweep instead of a delete on every single write.
|
|
1092
|
+
if (ledger.filesTouched.size > LEDGER_MAX_FILES * 4) {
|
|
1093
|
+
for (const key of ledger.filesTouched.keys()) {
|
|
1094
|
+
if (ledger.filesTouched.size <= LEDGER_MAX_FILES * 2)
|
|
1095
|
+
break;
|
|
1096
|
+
if (key !== p)
|
|
1097
|
+
ledger.filesTouched.delete(key);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1030
1100
|
}
|
|
1031
1101
|
// Reward-hacking guard: if this write WEAKENED a test file, record it so the
|
|
1032
1102
|
// signal survives compaction and can be surfaced before the agent finishes.
|
|
@@ -1046,6 +1116,7 @@ function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
|
|
|
1046
1116
|
if (reasons.length && p) {
|
|
1047
1117
|
for (const reason of reasons) {
|
|
1048
1118
|
ledger.testIntegrity.push({ path: p, reason });
|
|
1119
|
+
ledger.testIntegrityTotal++;
|
|
1049
1120
|
}
|
|
1050
1121
|
if (ledger.testIntegrity.length > LEDGER_MAX_NOTES * 2) {
|
|
1051
1122
|
ledger.testIntegrity.splice(0, ledger.testIntegrity.length - LEDGER_MAX_NOTES);
|
|
@@ -1078,8 +1149,11 @@ function ledgerSummary(ledger) {
|
|
|
1078
1149
|
const lines = [];
|
|
1079
1150
|
if (ledger.filesTouched.size) {
|
|
1080
1151
|
const files = [...ledger.filesTouched.entries()];
|
|
1081
|
-
|
|
1082
|
-
|
|
1152
|
+
// The TAIL, not the head: the Map is ordered least-recently-touched first, so
|
|
1153
|
+
// slicing from the front showed the OLDEST files and reliably omitted the ones the
|
|
1154
|
+
// agent was working on right now — the opposite of what this preamble is for.
|
|
1155
|
+
const shown = files.slice(-LEDGER_MAX_FILES);
|
|
1156
|
+
lines.push(`FILES CHANGED THIS SESSION (${ledger.filesTouchedTotal || ledger.filesTouched.size}):`);
|
|
1083
1157
|
for (const [p, meta] of shown) {
|
|
1084
1158
|
lines.push(` • ${p} (${meta.tool}${meta.edits > 1 ? ` ×${meta.edits}` : ''})`);
|
|
1085
1159
|
}
|
|
@@ -1472,6 +1546,12 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1472
1546
|
let completedRounds = 0;
|
|
1473
1547
|
let stalledOut = false; // tripped the runaway guard (all-failed rounds)
|
|
1474
1548
|
let consecutiveErrorRounds = 0; // rounds where every tool call errored
|
|
1549
|
+
// Repeated-identical-failure guard: see the runaway guards below. Tracked
|
|
1550
|
+
// separately from consecutiveErrorRounds because a round can contain a succeeding
|
|
1551
|
+
// call and still be part of a livelock.
|
|
1552
|
+
let repeatedErrorRounds = 0;
|
|
1553
|
+
let lastErrorSignature = '';
|
|
1554
|
+
let stalledRepeatError = null;
|
|
1475
1555
|
let budget = maxIterations; // extended by auto-continue, capped at hardCap
|
|
1476
1556
|
let iteration = 0;
|
|
1477
1557
|
// ─── Verification nudge (GAP D) ───────────────────────────────────────────────
|
|
@@ -1498,6 +1578,31 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1498
1578
|
let claimEvidenceNudged = false;
|
|
1499
1579
|
// GAP E — deterministic progress ledger, preserved verbatim across compactions.
|
|
1500
1580
|
const ledger = createLedger();
|
|
1581
|
+
// ─── Auto-compact circuit breaker ─────────────────────────────────────────────
|
|
1582
|
+
//
|
|
1583
|
+
// The in-loop compaction trigger below re-derives its pressure from the CURRENT
|
|
1584
|
+
// body on every iteration. That is correct, but it means a compaction which does
|
|
1585
|
+
// not shrink anything leaves the trigger condition still true — so the next
|
|
1586
|
+
// iteration pays for another summariser call over the same (up to ~600KB)
|
|
1587
|
+
// transcript, and so on for the rest of the run. Two ways that happens:
|
|
1588
|
+
//
|
|
1589
|
+
// • autoCompactMessages returns false (summariser stream threw, or came back
|
|
1590
|
+
// empty). Nothing was replaced, so the pressure is unchanged.
|
|
1591
|
+
// • It returns true but cannot get under MAX_BODY_BYTES, because the messages
|
|
1592
|
+
// it must retain (COMPACT_KEEP_MIN) are themselves huge. `lastPromptTokens = 0`
|
|
1593
|
+
// suppresses only the TOKEN trigger; the BYTE trigger fires again immediately.
|
|
1594
|
+
//
|
|
1595
|
+
// Both are invisible to the user (the success notice only prints when `did`), so
|
|
1596
|
+
// the symptom is a long run that silently gets slower and more expensive. Count
|
|
1597
|
+
// consecutive non-productive attempts and stop trying after a few — losing
|
|
1598
|
+
// compaction degrades gracefully (the turn may still fit, and the prune pass
|
|
1599
|
+
// still runs), whereas an unbounded retry loop does not.
|
|
1600
|
+
//
|
|
1601
|
+
// The resume-time compactor already had exactly these guards
|
|
1602
|
+
// (compactMessagesForResume: a bounded loop plus `if (!did) break`); this brings
|
|
1603
|
+
// the in-loop path in line with it.
|
|
1604
|
+
let compactFailures = 0;
|
|
1605
|
+
let compactDisabled = false;
|
|
1501
1606
|
try {
|
|
1502
1607
|
for (; iteration < budget; iteration++) {
|
|
1503
1608
|
if (options.abortSignal?.aborted)
|
|
@@ -1546,10 +1651,17 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1546
1651
|
(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.`);
|
|
1547
1652
|
}
|
|
1548
1653
|
}
|
|
1549
|
-
if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
|
|
1654
|
+
if (autoCompact && !compactDisabled && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
|
|
1550
1655
|
compacting = true;
|
|
1551
1656
|
try {
|
|
1657
|
+
// Measured BEFORE, so "did it actually help?" is a fact about bytes rather
|
|
1658
|
+
// than a claim from the compactor. A compaction that returns true but
|
|
1659
|
+
// reclaims nothing is a failure for our purposes — it leaves the trigger
|
|
1660
|
+
// armed for the next iteration, which is precisely the runaway.
|
|
1661
|
+
const bytesBefore = estimateBodyBytes(messages);
|
|
1552
1662
|
const did = await autoCompactMessages(messages, options, ledger);
|
|
1663
|
+
const bytesAfter = did ? estimateBodyBytes(messages) : bytesBefore;
|
|
1664
|
+
const reclaimed = bytesBefore - bytesAfter;
|
|
1553
1665
|
if (did) {
|
|
1554
1666
|
lastPromptTokens = 0; // stale — next usage event refreshes it
|
|
1555
1667
|
const reason = bytePressure
|
|
@@ -1558,6 +1670,24 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1558
1670
|
// Same reasoning as above: this is a system notice about housekeeping,
|
|
1559
1671
|
// not part of the model's answer — keep it out of the text bubble.
|
|
1560
1672
|
(options.onNotice ?? options.onText)(`\u267b\ufe0f Auto-compacted earlier conversation to stay within the ${reason}.`);
|
|
1673
|
+
// Refresh local pressure so the rest of THIS iteration sees the new size.
|
|
1674
|
+
bodyBytes = bytesAfter;
|
|
1675
|
+
bytePressure = bodyBytes > MAX_BODY_BYTES;
|
|
1676
|
+
}
|
|
1677
|
+
// Productive == it shrank the body meaningfully. A successful-but-useless
|
|
1678
|
+
// compaction counts as a failure, otherwise the "cannot get under the byte
|
|
1679
|
+
// cap" case would never trip the breaker.
|
|
1680
|
+
if (did && reclaimed >= COMPACT_MIN_RECLAIM_BYTES) {
|
|
1681
|
+
compactFailures = 0;
|
|
1682
|
+
}
|
|
1683
|
+
else if (++compactFailures >= COMPACT_MAX_FAILURES) {
|
|
1684
|
+
compactDisabled = true;
|
|
1685
|
+
// Surfaced ONCE. The user needs to know the automatic safety net is off
|
|
1686
|
+
// (so a context-window error later isn't a total surprise) and what to do
|
|
1687
|
+
// about it, but repeating this every iteration would be its own spam.
|
|
1688
|
+
(options.onNotice ?? options.onText)(`\u26a0\ufe0f Auto-compaction isn't reducing this conversation any further, so it's been switched off ` +
|
|
1689
|
+
`for the rest of this run to avoid repeated summarising. If the context fills up, start a fresh ` +
|
|
1690
|
+
`chat or run /compact manually.`);
|
|
1561
1691
|
}
|
|
1562
1692
|
}
|
|
1563
1693
|
finally {
|
|
@@ -1772,9 +1902,19 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1772
1902
|
// and require the agent to either justify each change (legit refactor) or
|
|
1773
1903
|
// revert it and fix the real code. Deterministic — the signal comes from
|
|
1774
1904
|
// diff structure, not model self-report, so it can't be gamed away.
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1905
|
+
//
|
|
1906
|
+
// Compared against testIntegrityTotal, NOT testIntegrity.length: the array is a
|
|
1907
|
+
// bounded window that gets trimmed, so once a long session passed ~40 findings
|
|
1908
|
+
// its length stopped growing and could even fall BELOW the already-nudged count,
|
|
1909
|
+
// permanently wedging this condition false and disabling the guard for the rest
|
|
1910
|
+
// of the run. The total only ever increases.
|
|
1911
|
+
if (ledger.testIntegrityTotal > testIntegrityNudgedCount) {
|
|
1912
|
+
// How many are genuinely new, clamped to what the window still holds — the
|
|
1913
|
+
// trimmed-away ones are unrecoverable, and reporting the tail we DO have is
|
|
1914
|
+
// strictly better than reporting nothing.
|
|
1915
|
+
const newCount = Math.min(ledger.testIntegrityTotal - testIntegrityNudgedCount, ledger.testIntegrity.length);
|
|
1916
|
+
const fresh = ledger.testIntegrity.slice(ledger.testIntegrity.length - newCount);
|
|
1917
|
+
testIntegrityNudgedCount = ledger.testIntegrityTotal;
|
|
1778
1918
|
const bullet = fresh.map((t) => ` • ${t.path}: ${t.reason}`).join('\n');
|
|
1779
1919
|
messages.push({
|
|
1780
1920
|
role: 'user',
|
|
@@ -2073,16 +2213,46 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2073
2213
|
// tool_result user turn). Let the caller checkpoint progress so a crash
|
|
2074
2214
|
// mid-run loses only the in-flight step, not the whole session.
|
|
2075
2215
|
options.onProgress?.(messages);
|
|
2076
|
-
// Runaway
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
|
|
2216
|
+
// ── Runaway guards ────────────────────────────────────────────────────────
|
|
2217
|
+
//
|
|
2218
|
+
// TWO independent counters, because "stuck" has two shapes and the original
|
|
2219
|
+
// all-failed test only caught the first.
|
|
2220
|
+
//
|
|
2221
|
+
// 1. TOTAL failure: every call in the round errored (a command that always
|
|
2222
|
+
// errors, the user denying every permission). Unambiguous.
|
|
2223
|
+
//
|
|
2224
|
+
// 2. REPEATED failure: the SAME error keeps coming back, round after round,
|
|
2225
|
+
// even though other calls in those rounds succeed. This is the livelock the
|
|
2226
|
+
// `.every()` test missed entirely — one trivially-succeeding sibling (say a
|
|
2227
|
+
// `read_file` alongside an `edit_file` that fails identically every time)
|
|
2228
|
+
// reset the counter to 0 forever, so a genuine loop burned the full
|
|
2229
|
+
// 2000-iteration ceiling instead of stopping at 8. That is the expensive,
|
|
2230
|
+
// user-visible "it just spun for an hour" failure.
|
|
2231
|
+
//
|
|
2232
|
+
// Keyed on tool + error text so a DIFFERENT error each round (real progress
|
|
2233
|
+
// through a chain of distinct problems) does not trip it.
|
|
2234
|
+
const errored = toolResults.filter(({ result }) => result.error !== undefined);
|
|
2235
|
+
const allErrored = toolResults.length > 0 && errored.length === toolResults.length;
|
|
2081
2236
|
consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
|
|
2082
2237
|
if (consecutiveErrorRounds >= STALL_LIMIT) {
|
|
2083
2238
|
stalledOut = true;
|
|
2084
2239
|
break;
|
|
2085
2240
|
}
|
|
2241
|
+
// Signature of this round's failures, order-independent and truncated so a long
|
|
2242
|
+
// error body (or a path echoed inside it) doesn't make every occurrence unique.
|
|
2243
|
+
const errSignature = errorRoundSignature(errored.map(({ block, result }) => ({ name: block.name, error: String(result.error) })));
|
|
2244
|
+
if (errSignature && errSignature === lastErrorSignature) {
|
|
2245
|
+
repeatedErrorRounds++;
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
repeatedErrorRounds = 0;
|
|
2249
|
+
lastErrorSignature = errSignature;
|
|
2250
|
+
}
|
|
2251
|
+
if (repeatedErrorRounds >= REPEAT_STALL_LIMIT) {
|
|
2252
|
+
stalledOut = true;
|
|
2253
|
+
stalledRepeatError = errored[0] ? String(errored[0].result.error).slice(0, 300) : null;
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2086
2256
|
// Auto-continue: about to exhaust the current budget but the model is still
|
|
2087
2257
|
// calling tools (task unfinished) and we're under the ceiling → extend the
|
|
2088
2258
|
// budget by another segment and keep going, so the agent finishes on its own
|
|
@@ -2098,12 +2268,19 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2098
2268
|
// long run never just goes silent. History ends on a tool_result turn, so
|
|
2099
2269
|
// "continue" resumes exactly where it left off.
|
|
2100
2270
|
if (!options.abortSignal?.aborted && !completedCleanly) {
|
|
2271
|
+
// Routed through onNotice (falling back to onText) like every other housekeeping
|
|
2272
|
+
// message in this file — these are statements from the harness, not from the model,
|
|
2273
|
+
// and splicing them into the assistant's own bubble reads as if it said them.
|
|
2101
2274
|
if (stalledOut) {
|
|
2102
|
-
options.
|
|
2103
|
-
|
|
2275
|
+
(options.onNotice ?? options.onText)(stalledRepeatError
|
|
2276
|
+
? `\n🛑 Stopped: the same tool error repeated ${REPEAT_STALL_LIMIT} rounds in a row, so the agent ` +
|
|
2277
|
+
`was looping without making progress. The recurring error was:\n${stalledRepeatError}\n` +
|
|
2278
|
+
`Fix that underlying cause (or grant the needed permission) and send "continue".\n`
|
|
2279
|
+
: `\n🛑 Stopped: the last ${STALL_LIMIT} tool rounds all failed, so the agent looked stuck. ` +
|
|
2280
|
+
`Fix the underlying error (or grant the needed permission) and send "continue".\n`);
|
|
2104
2281
|
}
|
|
2105
2282
|
else if (iteration >= budget) {
|
|
2106
|
-
options.onText(`\n⏸️ Stopped at the ${budget}-step safety limit — the task may be incomplete. ` +
|
|
2283
|
+
(options.onNotice ?? options.onText)(`\n⏸️ Stopped at the ${budget}-step safety limit — the task may be incomplete. ` +
|
|
2107
2284
|
`Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json ` +
|
|
2108
2285
|
`(or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.\n`);
|
|
2109
2286
|
}
|
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;;;;;;;;;;;;;;;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,
|
|
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;;;;;;;;;;;;;;;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,CAwlClB;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"}
|