@nexrall/code-core 1.4.26 → 1.4.28
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 +65 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +287 -38
- 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,47 @@
|
|
|
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
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Why an agent run stopped.
|
|
24
|
+
*
|
|
25
|
+
* `'clean'` and `'aborted'` are the two silent-by-design outcomes: the model gave a
|
|
26
|
+
* final answer, or the user pressed Ctrl+C and already knows why it stopped.
|
|
27
|
+
* EVERYTHING else owes the user an explanation, which is what stopReasonNotice covers.
|
|
28
|
+
*/
|
|
29
|
+
export type StopReason = 'clean' | 'aborted' | 'reported-elsewhere' | 'empty-response' | 'no-balance' | 'stalled' | 'stalled-repeat' | 'budget' | 'unknown';
|
|
30
|
+
/**
|
|
31
|
+
* The message shown when a run ends for any reason other than a clean finish.
|
|
32
|
+
*
|
|
33
|
+
* Pure and exported so every branch is testable: reaching some of these for real needs
|
|
34
|
+
* an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
|
|
35
|
+
* the two outcomes that are deliberately silent.
|
|
36
|
+
*
|
|
37
|
+
* `'unknown'` deliberately produces a message rather than nothing. If a future `break`
|
|
38
|
+
* forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
|
|
39
|
+
* annoying and reportable — not the silent stop that made this refactor necessary.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stopReasonNotice(reason: StopReason, ctx?: {
|
|
42
|
+
budget?: number;
|
|
43
|
+
repeatError?: string | null;
|
|
44
|
+
}): string | null;
|
|
2
45
|
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
3
46
|
/**
|
|
4
47
|
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
@@ -111,6 +154,15 @@ export interface ProgressLedger {
|
|
|
111
154
|
tool: string;
|
|
112
155
|
edits: number;
|
|
113
156
|
}>;
|
|
157
|
+
/**
|
|
158
|
+
* DISTINCT paths ever touched, including any since evicted from `filesTouched`.
|
|
159
|
+
*
|
|
160
|
+
* The Map is bounded (see ledgerRecord), so `filesTouched.size` is a window, not a
|
|
161
|
+
* total. The preamble states "FILES CHANGED THIS SESSION (N)" as a fact the model
|
|
162
|
+
* reasons about, so N must not silently shrink when eviction kicks in on a very long
|
|
163
|
+
* run — that would tell the model less work happened than actually did.
|
|
164
|
+
*/
|
|
165
|
+
filesTouchedTotal: number;
|
|
114
166
|
verifications: Array<{
|
|
115
167
|
cmd: string;
|
|
116
168
|
ok: boolean;
|
|
@@ -121,6 +173,19 @@ export interface ProgressLedger {
|
|
|
121
173
|
path: string;
|
|
122
174
|
reason: string;
|
|
123
175
|
}>;
|
|
176
|
+
/**
|
|
177
|
+
* TOTAL test-integrity findings ever recorded, including ones since trimmed.
|
|
178
|
+
*
|
|
179
|
+
* `testIntegrity` is a bounded window (older entries are spliced off once it grows
|
|
180
|
+
* past 2×LEDGER_MAX_NOTES), so its `.length` STOPS being a running total after the
|
|
181
|
+
* first trim. The one-shot nudge compares "how many findings exist" against "how
|
|
182
|
+
* many I've already surfaced", and comparing against a window that shrinks meant the
|
|
183
|
+
* count could never move ahead again — silently disabling the reward-hacking warning
|
|
184
|
+
* for the remainder of a long session, i.e. exactly when it matters most.
|
|
185
|
+
*
|
|
186
|
+
* This counter only ever increases, so it is a safe basis for that comparison.
|
|
187
|
+
*/
|
|
188
|
+
testIntegrityTotal: number;
|
|
124
189
|
/**
|
|
125
190
|
* Monotonic mutation epoch: incremented on every successful source write. Two
|
|
126
191
|
* 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,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 = void 0;
|
|
36
|
+
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports._stallLimits = void 0;
|
|
37
|
+
exports.errorRoundSignature = errorRoundSignature;
|
|
38
|
+
exports.stopReasonNotice = stopReasonNotice;
|
|
37
39
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
38
40
|
exports.createLimiter = createLimiter;
|
|
39
41
|
exports.extractSubTaskText = extractSubTaskText;
|
|
@@ -189,6 +191,74 @@ const DEFAULT_MAX_ITERATIONS = 500;
|
|
|
189
191
|
const MAX_ITERATIONS_CEILING = 2000; // default auto-continue backstop (no explicit opt-in)
|
|
190
192
|
const HARD_ITERATIONS_CAP = 100000; // absolute safety cap — even explicit opt-in can't exceed this
|
|
191
193
|
const STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runaway guard)
|
|
194
|
+
// Consecutive rounds producing the IDENTICAL error(s) → give up, even if other calls in
|
|
195
|
+
// those rounds succeeded. Higher than STALL_LIMIT because a repeat is weaker evidence of
|
|
196
|
+
// being stuck than a total failure: legitimately retrying one failing command a few times
|
|
197
|
+
// while making progress elsewhere is normal, twelve times is not.
|
|
198
|
+
const REPEAT_STALL_LIMIT = 12;
|
|
199
|
+
/**
|
|
200
|
+
* Fingerprint one round's tool failures, for the repeated-failure runaway guard.
|
|
201
|
+
*
|
|
202
|
+
* Exported (with the limits) purely as a test seam: the guard's whole value is in the
|
|
203
|
+
* edge cases — that a DIFFERENT error each round must NOT trip it, that call order
|
|
204
|
+
* within a round is irrelevant, that a long error body doesn't make every occurrence
|
|
205
|
+
* look unique — and none of that is reachable without driving a live model loop.
|
|
206
|
+
*
|
|
207
|
+
* Sorted so parallel tool calls completing in a different order still compare equal;
|
|
208
|
+
* truncated because errors often embed a varying path or timestamp late in the string.
|
|
209
|
+
*/
|
|
210
|
+
function errorRoundSignature(errored) {
|
|
211
|
+
return errored
|
|
212
|
+
.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`)
|
|
213
|
+
.sort()
|
|
214
|
+
.join('|');
|
|
215
|
+
}
|
|
216
|
+
/** Runaway-guard limits, exposed for tests. */
|
|
217
|
+
exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
|
|
218
|
+
/**
|
|
219
|
+
* The message shown when a run ends for any reason other than a clean finish.
|
|
220
|
+
*
|
|
221
|
+
* Pure and exported so every branch is testable: reaching some of these for real needs
|
|
222
|
+
* an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
|
|
223
|
+
* the two outcomes that are deliberately silent.
|
|
224
|
+
*
|
|
225
|
+
* `'unknown'` deliberately produces a message rather than nothing. If a future `break`
|
|
226
|
+
* forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
|
|
227
|
+
* annoying and reportable — not the silent stop that made this refactor necessary.
|
|
228
|
+
*/
|
|
229
|
+
function stopReasonNotice(reason, ctx = {}) {
|
|
230
|
+
switch (reason) {
|
|
231
|
+
case 'clean':
|
|
232
|
+
case 'aborted':
|
|
233
|
+
// A dedicated channel (e.g. the zero-balance bubble via onBalanceStatus) has already
|
|
234
|
+
// told the user why this stopped. Naming this case explicitly — rather than reusing
|
|
235
|
+
// 'aborted' — keeps "the user cancelled" from silently coming to mean two things.
|
|
236
|
+
case 'reported-elsewhere':
|
|
237
|
+
return null;
|
|
238
|
+
case 'empty-response':
|
|
239
|
+
return `\n\u26a0\ufe0f The model returned an empty response, so nothing was done. This is usually a transient ` +
|
|
240
|
+
`upstream hiccup \u2014 send "continue" to retry.\n`;
|
|
241
|
+
case 'no-balance':
|
|
242
|
+
return `\n\ud83d\udcb3 Stopped: your balance is empty, so the request was rejected before it started. ` +
|
|
243
|
+
`Top up and send "continue" \u2014 no tokens were used for this turn.\n`;
|
|
244
|
+
case 'stalled':
|
|
245
|
+
return `\n\ud83d\uded1 Stopped: the last ${STALL_LIMIT} tool rounds all failed, so the agent looked stuck. ` +
|
|
246
|
+
`Fix the underlying error (or grant the needed permission) and send "continue".\n`;
|
|
247
|
+
case 'stalled-repeat':
|
|
248
|
+
return `\n\ud83d\uded1 Stopped: the same tool error repeated ${REPEAT_STALL_LIMIT} rounds in a row, so the agent ` +
|
|
249
|
+
`was looping without making progress.` +
|
|
250
|
+
(ctx.repeatError ? ` The recurring error was:\n${ctx.repeatError}\n` : '\n') +
|
|
251
|
+
`Fix that underlying cause (or grant the needed permission) and send "continue".\n`;
|
|
252
|
+
case 'budget':
|
|
253
|
+
return `\n\u23f8\ufe0f Stopped at the ${ctx.budget}-step safety limit \u2014 the task may be incomplete. ` +
|
|
254
|
+
`Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json ` +
|
|
255
|
+
`(or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.\n`;
|
|
256
|
+
case 'unknown':
|
|
257
|
+
default:
|
|
258
|
+
return `\n\u26a0\ufe0f The run ended unexpectedly without completing. Your work so far is preserved \u2014 ` +
|
|
259
|
+
`send "continue" to resume.\n`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
192
262
|
// Resolve the soft iteration budget. Precedence:
|
|
193
263
|
// options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
|
|
194
264
|
//
|
|
@@ -830,6 +900,26 @@ function compactionThresholds() {
|
|
|
830
900
|
// so we require at least this many bytes reclaimed before accepting a prune.
|
|
831
901
|
const PRUNE_MIN_RECLAIM_BYTES = 256 * 1024; // 256 KB
|
|
832
902
|
const COMPACT_KEEP_MIN = 6; // always keep at least the last N messages verbatim
|
|
903
|
+
/**
|
|
904
|
+
* Bytes a compaction must reclaim to count as productive.
|
|
905
|
+
*
|
|
906
|
+
* Deliberately much smaller than PRUNE_MIN_RECLAIM_BYTES: a prune declines when the
|
|
907
|
+
* gain isn't worth busting the prompt cache, whereas by the time we are summarising
|
|
908
|
+
* we are already committed to rewriting the prefix — the only question is whether the
|
|
909
|
+
* summariser is making ANY headway. 32 KB is small enough that a genuinely useful
|
|
910
|
+
* compaction always clears it, large enough that shuffling a few bytes doesn't.
|
|
911
|
+
*/
|
|
912
|
+
const COMPACT_MIN_RECLAIM_BYTES = 32 * 1024; // 32 KB
|
|
913
|
+
/**
|
|
914
|
+
* Consecutive non-productive compaction attempts before auto-compaction is switched
|
|
915
|
+
* off for the rest of the run.
|
|
916
|
+
*
|
|
917
|
+
* 3 rather than 1 because the failure is often transient — a summariser stream that
|
|
918
|
+
* blipped will usually succeed on the next turn, and giving up instantly would lose
|
|
919
|
+
* the safety net for a whole long session over one network hiccup. 3 also bounds the
|
|
920
|
+
* wasted spend: at most three summariser calls, not hundreds.
|
|
921
|
+
*/
|
|
922
|
+
const COMPACT_MAX_FAILURES = 3;
|
|
833
923
|
// Byte-level safety net, independent of the token estimate.
|
|
834
924
|
//
|
|
835
925
|
// Tool-heavy sessions on large codebases accumulate many tool_result blocks
|
|
@@ -1013,7 +1103,7 @@ function transcriptOf(messages) {
|
|
|
1013
1103
|
const LEDGER_MAX_FILES = 60; // cap the file list so the preamble can't balloon
|
|
1014
1104
|
const LEDGER_MAX_NOTES = 20; // cap verification/among notes
|
|
1015
1105
|
function createLedger() {
|
|
1016
|
-
return { filesTouched: new Map(), verifications: [], testIntegrity: [], epoch: 0 };
|
|
1106
|
+
return { filesTouched: new Map(), filesTouchedTotal: 0, verifications: [], testIntegrity: [], testIntegrityTotal: 0, epoch: 0 };
|
|
1017
1107
|
}
|
|
1018
1108
|
/** Record one tool call's effect on the ledger (deterministic, no model call). */
|
|
1019
1109
|
function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
|
|
@@ -1026,7 +1116,32 @@ function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
|
|
|
1026
1116
|
const p = typeof input?.path === 'string' ? input.path : undefined;
|
|
1027
1117
|
if (p) {
|
|
1028
1118
|
const prev = ledger.filesTouched.get(p);
|
|
1119
|
+
if (!prev)
|
|
1120
|
+
ledger.filesTouchedTotal++;
|
|
1121
|
+
// DELETE before SET, so a re-touched path moves to the BACK of the insertion
|
|
1122
|
+
// order. `Map.set` on an existing key keeps its ORIGINAL slot, which quietly
|
|
1123
|
+
// broke the eviction policy below: a file edited hundreds of times over a long
|
|
1124
|
+
// session kept the position of its FIRST edit, so it aged out like a file nobody
|
|
1125
|
+
// had looked at since — and on the next edit it was re-inserted as "new", double-
|
|
1126
|
+
// counting filesTouchedTotal (which is documented as DISTINCT paths). Making the
|
|
1127
|
+
// Map a true LRU-by-touch is what lets the `key !== p` guard below mean anything.
|
|
1128
|
+
ledger.filesTouched.delete(p);
|
|
1029
1129
|
ledger.filesTouched.set(p, { tool: toolName, edits: (prev?.edits ?? 0) + 1 });
|
|
1130
|
+
// Bound the Map itself, not just its rendering. LEDGER_MAX_FILES caps how many
|
|
1131
|
+
// paths the preamble PRINTS (see ledgerSummary's slice), but the Map was only ever
|
|
1132
|
+
// written to — so a multi-hour run touching thousands of files grew it without
|
|
1133
|
+
// limit, and it is deliberately retained across every compaction. Evict the
|
|
1134
|
+
// least-recently-touched entries once we hold well beyond what can ever be
|
|
1135
|
+
// displayed. Hysteresis (evict down to 2× only once we exceed 4×) keeps this an
|
|
1136
|
+
// occasional bulk sweep instead of a delete on every single write.
|
|
1137
|
+
if (ledger.filesTouched.size > LEDGER_MAX_FILES * 4) {
|
|
1138
|
+
for (const key of ledger.filesTouched.keys()) {
|
|
1139
|
+
if (ledger.filesTouched.size <= LEDGER_MAX_FILES * 2)
|
|
1140
|
+
break;
|
|
1141
|
+
if (key !== p)
|
|
1142
|
+
ledger.filesTouched.delete(key);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1030
1145
|
}
|
|
1031
1146
|
// Reward-hacking guard: if this write WEAKENED a test file, record it so the
|
|
1032
1147
|
// signal survives compaction and can be surfaced before the agent finishes.
|
|
@@ -1046,6 +1161,7 @@ function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
|
|
|
1046
1161
|
if (reasons.length && p) {
|
|
1047
1162
|
for (const reason of reasons) {
|
|
1048
1163
|
ledger.testIntegrity.push({ path: p, reason });
|
|
1164
|
+
ledger.testIntegrityTotal++;
|
|
1049
1165
|
}
|
|
1050
1166
|
if (ledger.testIntegrity.length > LEDGER_MAX_NOTES * 2) {
|
|
1051
1167
|
ledger.testIntegrity.splice(0, ledger.testIntegrity.length - LEDGER_MAX_NOTES);
|
|
@@ -1078,8 +1194,11 @@ function ledgerSummary(ledger) {
|
|
|
1078
1194
|
const lines = [];
|
|
1079
1195
|
if (ledger.filesTouched.size) {
|
|
1080
1196
|
const files = [...ledger.filesTouched.entries()];
|
|
1081
|
-
|
|
1082
|
-
|
|
1197
|
+
// The TAIL, not the head: the Map is ordered least-recently-touched first, so
|
|
1198
|
+
// slicing from the front showed the OLDEST files and reliably omitted the ones the
|
|
1199
|
+
// agent was working on right now — the opposite of what this preamble is for.
|
|
1200
|
+
const shown = files.slice(-LEDGER_MAX_FILES);
|
|
1201
|
+
lines.push(`FILES CHANGED THIS SESSION (${ledger.filesTouchedTotal || ledger.filesTouched.size}):`);
|
|
1083
1202
|
for (const [p, meta] of shown) {
|
|
1084
1203
|
lines.push(` • ${p} (${meta.tool}${meta.edits > 1 ? ` ×${meta.edits}` : ''})`);
|
|
1085
1204
|
}
|
|
@@ -1461,17 +1580,35 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1461
1580
|
// condition where one sub-agent's chdir overwrites another's, making every tool
|
|
1462
1581
|
// that falls back to process.cwd() resolve paths against the wrong directory.
|
|
1463
1582
|
// workDir is now threaded explicitly through executeTool → resolvePath instead.
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1583
|
+
// ─── Why did this run stop? ───────────────────────────────────────────────────
|
|
1584
|
+
//
|
|
1585
|
+
// The epilogue used to INFER the reason by re-testing state (`stalledOut`, then
|
|
1586
|
+
// `iteration >= budget`). Inference only covers the cases someone thought to test,
|
|
1587
|
+
// so any `break` that matched none of them fell through and the run ended in total
|
|
1588
|
+
// silence — the worst possible outcome after an hour of work, because the user
|
|
1589
|
+
// cannot tell "finished" from "died". Two such holes existed:
|
|
1590
|
+
//
|
|
1591
|
+
// • the pre-flight 402 (wallet empty) broke out with no flag set at all, leaving
|
|
1592
|
+
// the optional onBalanceStatus callback as the ONLY signal;
|
|
1593
|
+
// • a thinking-only/empty model turn set completedCleanly = true, actively
|
|
1594
|
+
// claiming success for a turn that produced nothing.
|
|
1595
|
+
//
|
|
1596
|
+
// So the reason is now RECORDED at each exit instead of reconstructed after it.
|
|
1597
|
+
// Anything that isn't an explicit clean finish or a user abort must name itself,
|
|
1598
|
+
// and `default` in the switch below means a future `break` cannot go silent.
|
|
1599
|
+
let stopReason = 'unknown';
|
|
1468
1600
|
// Tool rounds actually completed in this call. Counted explicitly rather than derived
|
|
1469
1601
|
// from `messages.length` because auto-compaction splices the history SHORTER mid-run:
|
|
1470
1602
|
// a long session that compacted and then lost its connection would show a negative
|
|
1471
1603
|
// length delta and be judged "no progress", discarding the work it most needs to keep.
|
|
1472
1604
|
let completedRounds = 0;
|
|
1473
|
-
let stalledOut = false; // tripped the runaway guard (all-failed rounds)
|
|
1474
1605
|
let consecutiveErrorRounds = 0; // rounds where every tool call errored
|
|
1606
|
+
// Repeated-identical-failure guard: see the runaway guards below. Tracked
|
|
1607
|
+
// separately from consecutiveErrorRounds because a round can contain a succeeding
|
|
1608
|
+
// call and still be part of a livelock.
|
|
1609
|
+
let repeatedErrorRounds = 0;
|
|
1610
|
+
let lastErrorSignature = '';
|
|
1611
|
+
let stalledRepeatError = null;
|
|
1475
1612
|
let budget = maxIterations; // extended by auto-continue, capped at hardCap
|
|
1476
1613
|
let iteration = 0;
|
|
1477
1614
|
// ─── Verification nudge (GAP D) ───────────────────────────────────────────────
|
|
@@ -1498,10 +1635,37 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1498
1635
|
let claimEvidenceNudged = false;
|
|
1499
1636
|
// GAP E — deterministic progress ledger, preserved verbatim across compactions.
|
|
1500
1637
|
const ledger = createLedger();
|
|
1638
|
+
// ─── Auto-compact circuit breaker ─────────────────────────────────────────────
|
|
1639
|
+
//
|
|
1640
|
+
// The in-loop compaction trigger below re-derives its pressure from the CURRENT
|
|
1641
|
+
// body on every iteration. That is correct, but it means a compaction which does
|
|
1642
|
+
// not shrink anything leaves the trigger condition still true — so the next
|
|
1643
|
+
// iteration pays for another summariser call over the same (up to ~600KB)
|
|
1644
|
+
// transcript, and so on for the rest of the run. Two ways that happens:
|
|
1645
|
+
//
|
|
1646
|
+
// • autoCompactMessages returns false (summariser stream threw, or came back
|
|
1647
|
+
// empty). Nothing was replaced, so the pressure is unchanged.
|
|
1648
|
+
// • It returns true but cannot get under MAX_BODY_BYTES, because the messages
|
|
1649
|
+
// it must retain (COMPACT_KEEP_MIN) are themselves huge. `lastPromptTokens = 0`
|
|
1650
|
+
// suppresses only the TOKEN trigger; the BYTE trigger fires again immediately.
|
|
1651
|
+
//
|
|
1652
|
+
// Both are invisible to the user (the success notice only prints when `did`), so
|
|
1653
|
+
// the symptom is a long run that silently gets slower and more expensive. Count
|
|
1654
|
+
// consecutive non-productive attempts and stop trying after a few — losing
|
|
1655
|
+
// compaction degrades gracefully (the turn may still fit, and the prune pass
|
|
1656
|
+
// still runs), whereas an unbounded retry loop does not.
|
|
1657
|
+
//
|
|
1658
|
+
// The resume-time compactor already had exactly these guards
|
|
1659
|
+
// (compactMessagesForResume: a bounded loop plus `if (!did) break`); this brings
|
|
1660
|
+
// the in-loop path in line with it.
|
|
1661
|
+
let compactFailures = 0;
|
|
1662
|
+
let compactDisabled = false;
|
|
1501
1663
|
try {
|
|
1502
1664
|
for (; iteration < budget; iteration++) {
|
|
1503
|
-
if (options.abortSignal?.aborted)
|
|
1665
|
+
if (options.abortSignal?.aborted) {
|
|
1666
|
+
stopReason = 'aborted';
|
|
1504
1667
|
break;
|
|
1668
|
+
}
|
|
1505
1669
|
// Auto-compact: keep the resent prompt small BEFORE the next stream so we
|
|
1506
1670
|
// never hit the context-window wall — or the backend body-size limit — and,
|
|
1507
1671
|
// just as importantly, so we stop paying cache-read on an ever-growing prefix
|
|
@@ -1546,10 +1710,17 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1546
1710
|
(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
1711
|
}
|
|
1548
1712
|
}
|
|
1549
|
-
if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
|
|
1713
|
+
if (autoCompact && !compactDisabled && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
|
|
1550
1714
|
compacting = true;
|
|
1551
1715
|
try {
|
|
1716
|
+
// Measured BEFORE, so "did it actually help?" is a fact about bytes rather
|
|
1717
|
+
// than a claim from the compactor. A compaction that returns true but
|
|
1718
|
+
// reclaims nothing is a failure for our purposes — it leaves the trigger
|
|
1719
|
+
// armed for the next iteration, which is precisely the runaway.
|
|
1720
|
+
const bytesBefore = estimateBodyBytes(messages);
|
|
1552
1721
|
const did = await autoCompactMessages(messages, options, ledger);
|
|
1722
|
+
const bytesAfter = did ? estimateBodyBytes(messages) : bytesBefore;
|
|
1723
|
+
const reclaimed = bytesBefore - bytesAfter;
|
|
1553
1724
|
if (did) {
|
|
1554
1725
|
lastPromptTokens = 0; // stale — next usage event refreshes it
|
|
1555
1726
|
const reason = bytePressure
|
|
@@ -1558,6 +1729,24 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1558
1729
|
// Same reasoning as above: this is a system notice about housekeeping,
|
|
1559
1730
|
// not part of the model's answer — keep it out of the text bubble.
|
|
1560
1731
|
(options.onNotice ?? options.onText)(`\u267b\ufe0f Auto-compacted earlier conversation to stay within the ${reason}.`);
|
|
1732
|
+
// Refresh local pressure so the rest of THIS iteration sees the new size.
|
|
1733
|
+
bodyBytes = bytesAfter;
|
|
1734
|
+
bytePressure = bodyBytes > MAX_BODY_BYTES;
|
|
1735
|
+
}
|
|
1736
|
+
// Productive == it shrank the body meaningfully. A successful-but-useless
|
|
1737
|
+
// compaction counts as a failure, otherwise the "cannot get under the byte
|
|
1738
|
+
// cap" case would never trip the breaker.
|
|
1739
|
+
if (did && reclaimed >= COMPACT_MIN_RECLAIM_BYTES) {
|
|
1740
|
+
compactFailures = 0;
|
|
1741
|
+
}
|
|
1742
|
+
else if (++compactFailures >= COMPACT_MAX_FAILURES) {
|
|
1743
|
+
compactDisabled = true;
|
|
1744
|
+
// Surfaced ONCE. The user needs to know the automatic safety net is off
|
|
1745
|
+
// (so a context-window error later isn't a total surprise) and what to do
|
|
1746
|
+
// about it, but repeating this every iteration would be its own spam.
|
|
1747
|
+
(options.onNotice ?? options.onText)(`\u26a0\ufe0f Auto-compaction isn't reducing this conversation any further, so it's been switched off ` +
|
|
1748
|
+
`for the rest of this run to avoid repeated summarising. If the context fills up, start a fresh ` +
|
|
1749
|
+
`chat or run /compact manually.`);
|
|
1561
1750
|
}
|
|
1562
1751
|
}
|
|
1563
1752
|
finally {
|
|
@@ -1672,8 +1861,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1672
1861
|
}, onEvent);
|
|
1673
1862
|
}
|
|
1674
1863
|
catch (err) {
|
|
1675
|
-
if (options.abortSignal?.aborted || err.name === 'AbortError')
|
|
1864
|
+
if (options.abortSignal?.aborted || err.name === 'AbortError') {
|
|
1865
|
+
stopReason = 'aborted';
|
|
1676
1866
|
break;
|
|
1867
|
+
}
|
|
1677
1868
|
// Pre-flight 402 (routes/code.js — wallet already empty, the turn couldn't even
|
|
1678
1869
|
// start) is tagged with `status`/`balance` by client.ts. Surface it as the same
|
|
1679
1870
|
// low/zero-balance nudge a mid-turn 'balance_status' event would, instead of a
|
|
@@ -1681,7 +1872,16 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1681
1872
|
const status = err.status;
|
|
1682
1873
|
if (status === 402) {
|
|
1683
1874
|
const balance = err.balance ?? 0;
|
|
1875
|
+
// Prefer the rich, actionable UI (a persistent bubble with a link to Billing)
|
|
1876
|
+
// where the client implements it…
|
|
1684
1877
|
options.onBalanceStatus?.(balance, true);
|
|
1878
|
+
// …but do not DEPEND on it. The callback is optional, so a client without it
|
|
1879
|
+
// used to show the user absolutely nothing — the run simply stopped mid-task.
|
|
1880
|
+
//
|
|
1881
|
+
// Set the reason ONLY when there is no richer channel, so clients that do
|
|
1882
|
+
// render the bubble don't also get a redundant plain-text line saying the same
|
|
1883
|
+
// thing. Either way the user is told something, which is the actual guarantee.
|
|
1884
|
+
stopReason = options.onBalanceStatus ? 'reported-elsewhere' : 'no-balance';
|
|
1685
1885
|
break;
|
|
1686
1886
|
}
|
|
1687
1887
|
runSimpleHooks(hooks.OnError, options.workDir);
|
|
@@ -1694,8 +1894,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1694
1894
|
// assistant→tool_result pair here, so it is valid to resend as-is.
|
|
1695
1895
|
throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
|
|
1696
1896
|
}
|
|
1697
|
-
if (options.abortSignal?.aborted)
|
|
1897
|
+
if (options.abortSignal?.aborted) {
|
|
1898
|
+
stopReason = 'aborted';
|
|
1698
1899
|
break;
|
|
1900
|
+
}
|
|
1699
1901
|
// 2. Strip thinking blocks — API rejects them in message history
|
|
1700
1902
|
assistantMessage.content = assistantMessage.content.filter((b) => b.type !== 'thinking' && b.type !== 'redacted_thinking');
|
|
1701
1903
|
// If the model returned nothing usable (e.g. thinking-only, then stopped), the
|
|
@@ -1711,8 +1913,12 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1711
1913
|
messages.push({ role: 'user', content: [{ type: 'text', text }] });
|
|
1712
1914
|
continue;
|
|
1713
1915
|
}
|
|
1916
|
+
// NOT a clean finish. This used to set completedCleanly = true, which told the
|
|
1917
|
+
// caller the task succeeded when the model had in fact produced nothing at all
|
|
1918
|
+
// — indistinguishable, to the user, from a finished job with no summary. An
|
|
1919
|
+
// empty turn is a (usually transient) upstream failure, so name it and say so.
|
|
1714
1920
|
runSimpleHooks(hooks.PostMessageComplete, options.workDir);
|
|
1715
|
-
|
|
1921
|
+
stopReason = 'empty-response';
|
|
1716
1922
|
break;
|
|
1717
1923
|
}
|
|
1718
1924
|
// stopReason is transient, single-turn metadata for the truncated-tool-call guard
|
|
@@ -1772,9 +1978,19 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1772
1978
|
// and require the agent to either justify each change (legit refactor) or
|
|
1773
1979
|
// revert it and fix the real code. Deterministic — the signal comes from
|
|
1774
1980
|
// diff structure, not model self-report, so it can't be gamed away.
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1981
|
+
//
|
|
1982
|
+
// Compared against testIntegrityTotal, NOT testIntegrity.length: the array is a
|
|
1983
|
+
// bounded window that gets trimmed, so once a long session passed ~40 findings
|
|
1984
|
+
// its length stopped growing and could even fall BELOW the already-nudged count,
|
|
1985
|
+
// permanently wedging this condition false and disabling the guard for the rest
|
|
1986
|
+
// of the run. The total only ever increases.
|
|
1987
|
+
if (ledger.testIntegrityTotal > testIntegrityNudgedCount) {
|
|
1988
|
+
// How many are genuinely new, clamped to what the window still holds — the
|
|
1989
|
+
// trimmed-away ones are unrecoverable, and reporting the tail we DO have is
|
|
1990
|
+
// strictly better than reporting nothing.
|
|
1991
|
+
const newCount = Math.min(ledger.testIntegrityTotal - testIntegrityNudgedCount, ledger.testIntegrity.length);
|
|
1992
|
+
const fresh = ledger.testIntegrity.slice(ledger.testIntegrity.length - newCount);
|
|
1993
|
+
testIntegrityNudgedCount = ledger.testIntegrityTotal;
|
|
1778
1994
|
const bullet = fresh.map((t) => ` • ${t.path}: ${t.reason}`).join('\n');
|
|
1779
1995
|
messages.push({
|
|
1780
1996
|
role: 'user',
|
|
@@ -1850,7 +2066,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1850
2066
|
}
|
|
1851
2067
|
}
|
|
1852
2068
|
runSimpleHooks(hooks.PostMessageComplete, options.workDir);
|
|
1853
|
-
|
|
2069
|
+
stopReason = 'clean';
|
|
1854
2070
|
break;
|
|
1855
2071
|
}
|
|
1856
2072
|
// 5. Execute all tool uses in parallel
|
|
@@ -2073,14 +2289,44 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2073
2289
|
// tool_result user turn). Let the caller checkpoint progress so a crash
|
|
2074
2290
|
// mid-run loses only the in-flight step, not the whole session.
|
|
2075
2291
|
options.onProgress?.(messages);
|
|
2076
|
-
// Runaway
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
|
|
2292
|
+
// ── Runaway guards ────────────────────────────────────────────────────────
|
|
2293
|
+
//
|
|
2294
|
+
// TWO independent counters, because "stuck" has two shapes and the original
|
|
2295
|
+
// all-failed test only caught the first.
|
|
2296
|
+
//
|
|
2297
|
+
// 1. TOTAL failure: every call in the round errored (a command that always
|
|
2298
|
+
// errors, the user denying every permission). Unambiguous.
|
|
2299
|
+
//
|
|
2300
|
+
// 2. REPEATED failure: the SAME error keeps coming back, round after round,
|
|
2301
|
+
// even though other calls in those rounds succeed. This is the livelock the
|
|
2302
|
+
// `.every()` test missed entirely — one trivially-succeeding sibling (say a
|
|
2303
|
+
// `read_file` alongside an `edit_file` that fails identically every time)
|
|
2304
|
+
// reset the counter to 0 forever, so a genuine loop burned the full
|
|
2305
|
+
// 2000-iteration ceiling instead of stopping at 8. That is the expensive,
|
|
2306
|
+
// user-visible "it just spun for an hour" failure.
|
|
2307
|
+
//
|
|
2308
|
+
// Keyed on tool + error text so a DIFFERENT error each round (real progress
|
|
2309
|
+
// through a chain of distinct problems) does not trip it.
|
|
2310
|
+
const errored = toolResults.filter(({ result }) => result.error !== undefined);
|
|
2311
|
+
const allErrored = toolResults.length > 0 && errored.length === toolResults.length;
|
|
2081
2312
|
consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
|
|
2082
2313
|
if (consecutiveErrorRounds >= STALL_LIMIT) {
|
|
2083
|
-
|
|
2314
|
+
stopReason = 'stalled';
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
// Signature of this round's failures, order-independent and truncated so a long
|
|
2318
|
+
// error body (or a path echoed inside it) doesn't make every occurrence unique.
|
|
2319
|
+
const errSignature = errorRoundSignature(errored.map(({ block, result }) => ({ name: block.name, error: String(result.error) })));
|
|
2320
|
+
if (errSignature && errSignature === lastErrorSignature) {
|
|
2321
|
+
repeatedErrorRounds++;
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
repeatedErrorRounds = 0;
|
|
2325
|
+
lastErrorSignature = errSignature;
|
|
2326
|
+
}
|
|
2327
|
+
if (repeatedErrorRounds >= REPEAT_STALL_LIMIT) {
|
|
2328
|
+
stopReason = 'stalled-repeat';
|
|
2329
|
+
stalledRepeatError = errored[0] ? String(errored[0].result.error).slice(0, 300) : null;
|
|
2084
2330
|
break;
|
|
2085
2331
|
}
|
|
2086
2332
|
// Auto-continue: about to exhaust the current budget but the model is still
|
|
@@ -2094,20 +2340,23 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
2094
2340
|
}
|
|
2095
2341
|
// Loop back to step 1
|
|
2096
2342
|
}
|
|
2097
|
-
// Explain why we stopped
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2343
|
+
// Explain why we stopped, so a long run never just goes silent. History ends on a
|
|
2344
|
+
// tool_result turn, so "continue" resumes exactly where it left off.
|
|
2345
|
+
//
|
|
2346
|
+
// The budget case is resolved HERE rather than at a break: exhausting the `for`
|
|
2347
|
+
// condition is a normal loop exit, not a branch we can annotate. Everything else
|
|
2348
|
+
// named itself on the way out, and 'unknown' is the loud fallback for a break that
|
|
2349
|
+
// forgot to.
|
|
2350
|
+
if (stopReason === 'unknown' && iteration >= budget)
|
|
2351
|
+
stopReason = 'budget';
|
|
2352
|
+
if (options.abortSignal?.aborted)
|
|
2353
|
+
stopReason = 'aborted';
|
|
2354
|
+
// Routed through onNotice (falling back to onText) like every other housekeeping
|
|
2355
|
+
// message in this file — these are statements from the harness, not from the model,
|
|
2356
|
+
// and splicing them into the assistant's own bubble reads as if it said them.
|
|
2357
|
+
const notice = stopReasonNotice(stopReason, { budget, repeatError: stalledRepeatError });
|
|
2358
|
+
if (notice)
|
|
2359
|
+
(options.onNotice ?? options.onText)(notice);
|
|
2111
2360
|
}
|
|
2112
2361
|
catch (err) {
|
|
2113
2362
|
// Every other failure path (a tool executor blowing up, a hook throwing, an
|
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"}
|