nexrall-code 0.5.51 → 0.5.52

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.
Files changed (2) hide show
  1. package/dist/index.js +288 -228
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -10234,248 +10234,254 @@ var require_client = __commonJS({
10234
10234
  };
10235
10235
  const haveCompleteMessage = () => completedMessage !== null;
10236
10236
  const partialInputs = {};
10237
- await new Promise((resolve3, reject) => {
10238
- const stream = response.body;
10239
- const HEARTBEAT_TIMEOUT_MS = 9e4;
10240
- const FIRST_EVENT_TIMEOUT_MS = Number(process.env.NEXRALL_FIRST_EVENT_TIMEOUT_MS) || 3e5;
10241
- const PROGRESS_TIMEOUT_MS = Number(process.env.NEXRALL_PROGRESS_TIMEOUT_MS) || 15e4;
10242
- let sawModelEvent = false;
10243
- let lastDataAt = Date.now();
10244
- let lastProgressAt = Date.now();
10245
- const heartbeatWatchdog = setInterval(() => {
10246
- const now = Date.now();
10247
- if (haveCompleteMessage() && now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
10248
- clearInterval(heartbeatWatchdog);
10249
- stream.destroy?.();
10250
- resolve3();
10251
- return;
10252
- }
10253
- if (now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
10254
- clearInterval(heartbeatWatchdog);
10255
- stream.destroy?.();
10256
- const recoverable = !emittedToCaller || !!allowRestartAfterRender;
10257
- const secs = Math.round(HEARTBEAT_TIMEOUT_MS / 1e3);
10258
- reject(tagTransient(new Error(recoverable ? `Connection lost \u2014 no data received for ${secs} s. Reconnecting\u2026` : `Connection lost \u2014 no data received for ${secs} s. Retry your message.`)));
10259
- return;
10260
- }
10261
- const stallLimitMs = sawModelEvent ? PROGRESS_TIMEOUT_MS : FIRST_EVENT_TIMEOUT_MS;
10262
- if (now - lastProgressAt > stallLimitMs) {
10263
- clearInterval(heartbeatWatchdog);
10264
- stream.destroy?.();
10265
- if (haveCompleteMessage()) {
10237
+ let heartbeatWatchdog;
10238
+ try {
10239
+ await new Promise((resolve3, reject) => {
10240
+ const stream = response.body;
10241
+ const HEARTBEAT_TIMEOUT_MS = 9e4;
10242
+ const FIRST_EVENT_TIMEOUT_MS = Number(process.env.NEXRALL_FIRST_EVENT_TIMEOUT_MS) || 3e5;
10243
+ const PROGRESS_TIMEOUT_MS = Number(process.env.NEXRALL_PROGRESS_TIMEOUT_MS) || 15e4;
10244
+ let sawModelEvent = false;
10245
+ let lastDataAt = Date.now();
10246
+ let lastProgressAt = Date.now();
10247
+ heartbeatWatchdog = setInterval(() => {
10248
+ const now = Date.now();
10249
+ if (haveCompleteMessage() && now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
10250
+ clearInterval(heartbeatWatchdog);
10251
+ stream.destroy?.();
10266
10252
  resolve3();
10267
10253
  return;
10268
10254
  }
10269
- reject(tagTransient(new Error(sawModelEvent ? `The model stopped responding mid-stream (no output for ${Math.round(stallLimitMs / 1e3)} s).` : `The model did not start responding within ${Math.round(stallLimitMs / 1e3)} s (large context can take a while to process).`)));
10270
- return;
10271
- }
10272
- }, 5e3);
10273
- const parser = (0, eventsource_parser_1.createParser)((event) => {
10274
- lastDataAt = Date.now();
10275
- if (event.type !== "event") {
10276
- return;
10277
- }
10278
- {
10279
- const frameId = event.id;
10280
- if (frameId) {
10281
- const n = Number(frameId);
10282
- if (Number.isFinite(n) && n > lastEventId)
10283
- lastEventId = n;
10284
- }
10285
- const raw = event.data;
10286
- if (!raw || raw === "[DONE]") {
10287
- resolve3();
10255
+ if (now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
10256
+ clearInterval(heartbeatWatchdog);
10257
+ stream.destroy?.();
10258
+ const recoverable = !emittedToCaller || !!allowRestartAfterRender;
10259
+ const secs = Math.round(HEARTBEAT_TIMEOUT_MS / 1e3);
10260
+ reject(tagTransient(new Error(recoverable ? `Connection lost \u2014 no data received for ${secs} s. Reconnecting\u2026` : `Connection lost \u2014 no data received for ${secs} s. Retry your message.`)));
10288
10261
  return;
10289
10262
  }
10290
- let parsed;
10291
- try {
10292
- parsed = JSON.parse(raw);
10293
- } catch {
10263
+ const stallLimitMs = sawModelEvent ? PROGRESS_TIMEOUT_MS : FIRST_EVENT_TIMEOUT_MS;
10264
+ if (now - lastProgressAt > stallLimitMs) {
10265
+ clearInterval(heartbeatWatchdog);
10266
+ stream.destroy?.();
10267
+ if (haveCompleteMessage()) {
10268
+ resolve3();
10269
+ return;
10270
+ }
10271
+ reject(tagTransient(new Error(sawModelEvent ? `The model stopped responding mid-stream (no output for ${Math.round(stallLimitMs / 1e3)} s).` : `The model did not start responding within ${Math.round(stallLimitMs / 1e3)} s (large context can take a while to process).`)));
10294
10272
  return;
10295
10273
  }
10296
- if (typeof parsed !== "object" || parsed === null || !("type" in parsed)) {
10274
+ }, 5e3);
10275
+ const parser = (0, eventsource_parser_1.createParser)((event) => {
10276
+ lastDataAt = Date.now();
10277
+ if (event.type !== "event") {
10297
10278
  return;
10298
10279
  }
10299
- const evt = parsed;
10300
- lastProgressAt = Date.now();
10301
- sawModelEvent = true;
10302
- clearRetryIfNeeded();
10303
- switch (evt.type) {
10304
- case "text": {
10305
- const text = typeof evt.text === "string" ? evt.text : "";
10306
- textParts.push(text);
10307
- emittedToCaller = true;
10308
- emittedChars += text.length;
10309
- emittedAnythingAcrossAttempts = true;
10310
- emittedCharsAcrossAttempts += text.length;
10311
- onEvent({ type: "text", text });
10312
- break;
10280
+ {
10281
+ const frameId = event.id;
10282
+ if (frameId) {
10283
+ const n = Number(frameId);
10284
+ if (Number.isFinite(n) && n > lastEventId)
10285
+ lastEventId = n;
10313
10286
  }
10314
- case "tool_use": {
10315
- const block = {
10316
- type: "tool_use",
10317
- id: typeof evt.id === "string" ? evt.id : "",
10318
- name: typeof evt.name === "string" ? evt.name : "",
10319
- input: evt.input ?? {}
10320
- };
10321
- if (typeof evt.input_json_delta === "string") {
10322
- partialInputs[block.id] = (partialInputs[block.id] ?? "") + evt.input_json_delta;
10323
- try {
10324
- block.input = JSON.parse(partialInputs[block.id]);
10325
- } catch {
10326
- return;
10327
- }
10328
- }
10329
- toolUseBlocks.push(block);
10330
- emittedToCaller = true;
10331
- emittedAnythingAcrossAttempts = true;
10332
- onEvent({ type: "tool_use", id: block.id, name: block.name, input: block.input });
10333
- break;
10334
- }
10335
- case "message_complete": {
10336
- const nestedMessage = evt.message;
10337
- const stopReason = typeof nestedMessage?.stop_reason === "string" ? nestedMessage.stop_reason : null;
10338
- const contentBlocks2 = [];
10339
- const fullText2 = textParts.join("");
10340
- if (fullText2) {
10341
- contentBlocks2.push({ type: "text", text: fullText2 });
10342
- }
10343
- contentBlocks2.push(...toolUseBlocks);
10344
- const rawContent = Array.isArray(nestedMessage?.content) ? nestedMessage.content : null;
10345
- const finalContent = chooseFinalContent(contentBlocks2, rawContent);
10346
- completedMessage = {
10347
- role: "assistant",
10348
- content: finalContent,
10349
- stopReason
10350
- };
10351
- onEvent({ type: "message_complete", message: completedMessage });
10352
- break;
10287
+ const raw = event.data;
10288
+ if (!raw || raw === "[DONE]") {
10289
+ resolve3();
10290
+ return;
10353
10291
  }
10354
- case "usage": {
10355
- if (typeof evt.input_tokens === "number" && typeof evt.output_tokens === "number") {
10356
- onEvent({
10357
- type: "usage",
10358
- // Forwarded, not dropped: the backend tags the usage of an attempt it
10359
- // billed but never completed (its stream `abort` path). Without this
10360
- // the flag dies here and every consumer that sums usage silently folds
10361
- // a discarded attempt's tokens into the successful turn's total.
10362
- ...evt.partial === true ? { partial: true } : {},
10363
- // `replayed` means these tokens are being reported a SECOND time: the
10364
- // turn completed and was billed on an earlier attempt whose `done` never
10365
- // reached us, and the backend served this one from its idempotency cache
10366
- // instead of re-running the model. Nothing new was charged, so a cost
10367
- // display must not add them again.
10368
- ...evt.replayed === true ? { replayed: true } : {},
10369
- usage: {
10370
- input_tokens: evt.input_tokens,
10371
- output_tokens: evt.output_tokens,
10372
- cache_creation_input_tokens: typeof evt.cache_creation_input_tokens === "number" ? evt.cache_creation_input_tokens : void 0,
10373
- cache_read_input_tokens: typeof evt.cache_read_input_tokens === "number" ? evt.cache_read_input_tokens : void 0
10374
- }
10375
- });
10376
- }
10377
- break;
10292
+ let parsed;
10293
+ try {
10294
+ parsed = JSON.parse(raw);
10295
+ } catch {
10296
+ return;
10378
10297
  }
10379
- case "context_warning": {
10380
- break;
10298
+ if (typeof parsed !== "object" || parsed === null || !("type" in parsed)) {
10299
+ return;
10381
10300
  }
10382
- case "thinking": {
10383
- const text = typeof evt.text === "string" ? evt.text : "";
10384
- if (text) {
10301
+ const evt = parsed;
10302
+ lastProgressAt = Date.now();
10303
+ sawModelEvent = true;
10304
+ clearRetryIfNeeded();
10305
+ switch (evt.type) {
10306
+ case "text": {
10307
+ const text = typeof evt.text === "string" ? evt.text : "";
10308
+ textParts.push(text);
10385
10309
  emittedToCaller = true;
10386
10310
  emittedChars += text.length;
10387
10311
  emittedAnythingAcrossAttempts = true;
10388
10312
  emittedCharsAcrossAttempts += text.length;
10389
- onEvent({ type: "thinking", text });
10313
+ onEvent({ type: "text", text });
10314
+ break;
10390
10315
  }
10391
- break;
10392
- }
10393
- case "thinking_progress": {
10394
- const tokens = typeof evt.tokens === "number" ? evt.tokens : 0;
10395
- onEvent({ type: "thinking_progress", tokens });
10396
- break;
10397
- }
10398
- case "thinking_delta": {
10399
- const text = typeof evt.text === "string" ? evt.text : "";
10400
- if (text) {
10316
+ case "tool_use": {
10317
+ const block = {
10318
+ type: "tool_use",
10319
+ id: typeof evt.id === "string" ? evt.id : "",
10320
+ name: typeof evt.name === "string" ? evt.name : "",
10321
+ input: evt.input ?? {}
10322
+ };
10323
+ if (typeof evt.input_json_delta === "string") {
10324
+ partialInputs[block.id] = (partialInputs[block.id] ?? "") + evt.input_json_delta;
10325
+ try {
10326
+ block.input = JSON.parse(partialInputs[block.id]);
10327
+ } catch {
10328
+ return;
10329
+ }
10330
+ }
10331
+ toolUseBlocks.push(block);
10401
10332
  emittedToCaller = true;
10402
- emittedChars += text.length;
10403
10333
  emittedAnythingAcrossAttempts = true;
10404
- emittedCharsAcrossAttempts += text.length;
10405
- onEvent({ type: "thinking_delta", text });
10334
+ onEvent({ type: "tool_use", id: block.id, name: block.name, input: block.input });
10335
+ break;
10406
10336
  }
10407
- break;
10408
- }
10409
- case "resumable": {
10410
- serverResumable = true;
10411
- break;
10412
- }
10413
- case "balance_status": {
10414
- const balance = typeof evt.balance === "number" ? evt.balance : 0;
10415
- const zero = !!evt.zero;
10416
- onEvent({ type: "balance_status", balance, zero });
10417
- break;
10418
- }
10419
- case "done": {
10420
- onEvent({ type: "done" });
10421
- resolve3();
10422
- break;
10423
- }
10424
- case "error": {
10425
- const message = typeof evt.message === "string" ? evt.message : typeof evt.error === "string" ? evt.error : "Unknown SSE error";
10426
- const notResumable = evt.notResumable === true;
10427
- if (haveCompleteMessage()) {
10428
- clearInterval(heartbeatWatchdog);
10429
- onEvent({ type: "error", message });
10337
+ case "message_complete": {
10338
+ const nestedMessage = evt.message;
10339
+ const stopReason = typeof nestedMessage?.stop_reason === "string" ? nestedMessage.stop_reason : null;
10340
+ const contentBlocks2 = [];
10341
+ const fullText2 = textParts.join("");
10342
+ if (fullText2) {
10343
+ contentBlocks2.push({ type: "text", text: fullText2 });
10344
+ }
10345
+ contentBlocks2.push(...toolUseBlocks);
10346
+ const rawContent = Array.isArray(nestedMessage?.content) ? nestedMessage.content : null;
10347
+ const finalContent = chooseFinalContent(contentBlocks2, rawContent);
10348
+ completedMessage = {
10349
+ role: "assistant",
10350
+ content: finalContent,
10351
+ stopReason
10352
+ };
10353
+ onEvent({ type: "message_complete", message: completedMessage });
10354
+ break;
10355
+ }
10356
+ case "usage": {
10357
+ if (typeof evt.input_tokens === "number" && typeof evt.output_tokens === "number") {
10358
+ onEvent({
10359
+ type: "usage",
10360
+ // Forwarded, not dropped: the backend tags the usage of an attempt it
10361
+ // billed but never completed (its stream `abort` path). Without this
10362
+ // the flag dies here and every consumer that sums usage silently folds
10363
+ // a discarded attempt's tokens into the successful turn's total.
10364
+ ...evt.partial === true ? { partial: true } : {},
10365
+ // `replayed` means these tokens are being reported a SECOND time: the
10366
+ // turn completed and was billed on an earlier attempt whose `done` never
10367
+ // reached us, and the backend served this one from its idempotency cache
10368
+ // instead of re-running the model. Nothing new was charged, so a cost
10369
+ // display must not add them again.
10370
+ ...evt.replayed === true ? { replayed: true } : {},
10371
+ usage: {
10372
+ input_tokens: evt.input_tokens,
10373
+ output_tokens: evt.output_tokens,
10374
+ cache_creation_input_tokens: typeof evt.cache_creation_input_tokens === "number" ? evt.cache_creation_input_tokens : void 0,
10375
+ cache_read_input_tokens: typeof evt.cache_read_input_tokens === "number" ? evt.cache_read_input_tokens : void 0
10376
+ }
10377
+ });
10378
+ }
10379
+ break;
10380
+ }
10381
+ case "context_warning": {
10382
+ break;
10383
+ }
10384
+ case "thinking": {
10385
+ const text = typeof evt.text === "string" ? evt.text : "";
10386
+ if (text) {
10387
+ emittedToCaller = true;
10388
+ emittedChars += text.length;
10389
+ emittedAnythingAcrossAttempts = true;
10390
+ emittedCharsAcrossAttempts += text.length;
10391
+ onEvent({ type: "thinking", text });
10392
+ }
10393
+ break;
10394
+ }
10395
+ case "thinking_progress": {
10396
+ const tokens = typeof evt.tokens === "number" ? evt.tokens : 0;
10397
+ onEvent({ type: "thinking_progress", tokens });
10398
+ break;
10399
+ }
10400
+ case "thinking_delta": {
10401
+ const text = typeof evt.text === "string" ? evt.text : "";
10402
+ if (text) {
10403
+ emittedToCaller = true;
10404
+ emittedChars += text.length;
10405
+ emittedAnythingAcrossAttempts = true;
10406
+ emittedCharsAcrossAttempts += text.length;
10407
+ onEvent({ type: "thinking_delta", text });
10408
+ }
10409
+ break;
10410
+ }
10411
+ case "resumable": {
10412
+ serverResumable = true;
10413
+ break;
10414
+ }
10415
+ case "balance_status": {
10416
+ const balance = typeof evt.balance === "number" ? evt.balance : 0;
10417
+ const zero = !!evt.zero;
10418
+ onEvent({ type: "balance_status", balance, zero });
10419
+ break;
10420
+ }
10421
+ case "done": {
10422
+ onEvent({ type: "done" });
10430
10423
  resolve3();
10431
- } else if (notResumable && (!emittedToCaller || allowRestartAfterRender)) {
10432
- clearInterval(heartbeatWatchdog);
10433
- stream.destroy?.();
10434
- reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
10435
- } else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
10436
- clearInterval(heartbeatWatchdog);
10437
- stream.destroy?.();
10438
- reject(tagTransient(new Error(message)));
10439
- } else {
10440
- clearInterval(heartbeatWatchdog);
10441
- stream.destroy?.();
10442
- onEvent({ type: "error", message });
10443
- reject(new Error(message));
10424
+ break;
10425
+ }
10426
+ case "error": {
10427
+ const message = typeof evt.message === "string" ? evt.message : typeof evt.error === "string" ? evt.error : "Unknown SSE error";
10428
+ const notResumable = evt.notResumable === true;
10429
+ if (haveCompleteMessage()) {
10430
+ clearInterval(heartbeatWatchdog);
10431
+ onEvent({ type: "error", message });
10432
+ resolve3();
10433
+ } else if (notResumable && (!emittedToCaller || allowRestartAfterRender)) {
10434
+ clearInterval(heartbeatWatchdog);
10435
+ stream.destroy?.();
10436
+ reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
10437
+ } else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
10438
+ clearInterval(heartbeatWatchdog);
10439
+ stream.destroy?.();
10440
+ reject(tagTransient(new Error(message)));
10441
+ } else {
10442
+ clearInterval(heartbeatWatchdog);
10443
+ stream.destroy?.();
10444
+ onEvent({ type: "error", message });
10445
+ reject(new Error(message));
10446
+ }
10447
+ break;
10444
10448
  }
10445
- break;
10446
10449
  }
10447
10450
  }
10448
- }
10449
- });
10450
- stream.on("data", (chunk) => {
10451
- lastDataAt = Date.now();
10452
- if (abortSignal?.aborted) {
10453
- stream.destroy?.();
10454
- return;
10455
- }
10456
- parser.feed(chunk.toString("utf-8"));
10457
- });
10458
- stream.on("end", () => {
10459
- clearInterval(heartbeatWatchdog);
10460
- if (!sawModelEvent && !completedMessage) {
10461
- reject(Object.assign(new Error("Connection closed before the model responded. Retrying\u2026"), { retryable: true }));
10462
- return;
10463
- }
10464
- resolve3();
10465
- });
10466
- stream.on("error", (err) => {
10467
- clearInterval(heartbeatWatchdog);
10468
- if (abortSignal?.aborted || controller.signal.aborted) {
10469
- resolve3();
10470
- return;
10471
- }
10472
- if (haveCompleteMessage()) {
10451
+ });
10452
+ stream.on("data", (chunk) => {
10453
+ lastDataAt = Date.now();
10454
+ if (abortSignal?.aborted) {
10455
+ stream.destroy?.();
10456
+ return;
10457
+ }
10458
+ parser.feed(chunk.toString("utf-8"));
10459
+ });
10460
+ stream.on("end", () => {
10461
+ clearInterval(heartbeatWatchdog);
10462
+ if (!sawModelEvent && !completedMessage) {
10463
+ reject(Object.assign(new Error("Connection closed before the model responded. Retrying\u2026"), { retryable: true }));
10464
+ return;
10465
+ }
10473
10466
  resolve3();
10474
- return;
10475
- }
10476
- reject(tagTransient(err));
10467
+ });
10468
+ stream.on("error", (err) => {
10469
+ clearInterval(heartbeatWatchdog);
10470
+ if (abortSignal?.aborted || controller.signal.aborted) {
10471
+ resolve3();
10472
+ return;
10473
+ }
10474
+ if (haveCompleteMessage()) {
10475
+ resolve3();
10476
+ return;
10477
+ }
10478
+ reject(tagTransient(err));
10479
+ });
10477
10480
  });
10478
- });
10481
+ } finally {
10482
+ if (heartbeatWatchdog !== void 0)
10483
+ clearInterval(heartbeatWatchdog);
10484
+ }
10479
10485
  if (completedMessage) {
10480
10486
  return completedMessage;
10481
10487
  }
@@ -15757,7 +15763,8 @@ var require_loop = __commonJS({
15757
15763
  };
15758
15764
  }();
15759
15765
  Object.defineProperty(exports, "__esModule", { value: true });
15760
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = void 0;
15766
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports._stallLimits = void 0;
15767
+ exports.errorRoundSignature = errorRoundSignature;
15761
15768
  exports.resolveMaxIterations = resolveMaxIterations;
15762
15769
  exports.createLimiter = createLimiter;
15763
15770
  exports.extractSubTaskText = extractSubTaskText;
@@ -15890,6 +15897,11 @@ var require_loop = __commonJS({
15890
15897
  var MAX_ITERATIONS_CEILING = 2e3;
15891
15898
  var HARD_ITERATIONS_CAP = 1e5;
15892
15899
  var STALL_LIMIT = 8;
15900
+ var REPEAT_STALL_LIMIT = 12;
15901
+ function errorRoundSignature(errored) {
15902
+ return errored.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`).sort().join("|");
15903
+ }
15904
+ exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
15893
15905
  function resolveMaxIterations(optionValue, settingsRaw) {
15894
15906
  const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
15895
15907
  const fromSettings = Number(settingsRaw.maxIterations);
@@ -16266,6 +16278,8 @@ ${partial}` : "",
16266
16278
  }
16267
16279
  var PRUNE_MIN_RECLAIM_BYTES = 256 * 1024;
16268
16280
  var COMPACT_KEEP_MIN = 6;
16281
+ var COMPACT_MIN_RECLAIM_BYTES = 32 * 1024;
16282
+ var COMPACT_MAX_FAILURES = 3;
16269
16283
  var MAX_BODY_BYTES = 8 * 1024 * 1024;
16270
16284
  function estimateBodyBytes2(messages) {
16271
16285
  try {
@@ -16347,7 +16361,7 @@ ${tail}`;
16347
16361
  var LEDGER_MAX_FILES = 60;
16348
16362
  var LEDGER_MAX_NOTES = 20;
16349
16363
  function createLedger() {
16350
- return { filesTouched: /* @__PURE__ */ new Map(), verifications: [], testIntegrity: [], epoch: 0 };
16364
+ return { filesTouched: /* @__PURE__ */ new Map(), filesTouchedTotal: 0, verifications: [], testIntegrity: [], testIntegrityTotal: 0, epoch: 0 };
16351
16365
  }
16352
16366
  function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
16353
16367
  if (exports.WRITE_TOOL_NAMES.has(toolName)) {
@@ -16357,7 +16371,18 @@ ${tail}`;
16357
16371
  const p = typeof input?.path === "string" ? input.path : void 0;
16358
16372
  if (p) {
16359
16373
  const prev = ledger.filesTouched.get(p);
16374
+ if (!prev)
16375
+ ledger.filesTouchedTotal++;
16376
+ ledger.filesTouched.delete(p);
16360
16377
  ledger.filesTouched.set(p, { tool: toolName, edits: (prev?.edits ?? 0) + 1 });
16378
+ if (ledger.filesTouched.size > LEDGER_MAX_FILES * 4) {
16379
+ for (const key of ledger.filesTouched.keys()) {
16380
+ if (ledger.filesTouched.size <= LEDGER_MAX_FILES * 2)
16381
+ break;
16382
+ if (key !== p)
16383
+ ledger.filesTouched.delete(key);
16384
+ }
16385
+ }
16361
16386
  }
16362
16387
  const reasons = [];
16363
16388
  const markerReasons = toolName === "write_file" ? (0, testIntegrity_1.decodeTestIntegrityMarker)(output) : [];
@@ -16371,6 +16396,7 @@ ${tail}`;
16371
16396
  if (reasons.length && p) {
16372
16397
  for (const reason of reasons) {
16373
16398
  ledger.testIntegrity.push({ path: p, reason });
16399
+ ledger.testIntegrityTotal++;
16374
16400
  }
16375
16401
  if (ledger.testIntegrity.length > LEDGER_MAX_NOTES * 2) {
16376
16402
  ledger.testIntegrity.splice(0, ledger.testIntegrity.length - LEDGER_MAX_NOTES);
@@ -16391,8 +16417,8 @@ ${tail}`;
16391
16417
  const lines = [];
16392
16418
  if (ledger.filesTouched.size) {
16393
16419
  const files = [...ledger.filesTouched.entries()];
16394
- const shown = files.slice(0, LEDGER_MAX_FILES);
16395
- lines.push(`FILES CHANGED THIS SESSION (${ledger.filesTouched.size}):`);
16420
+ const shown = files.slice(-LEDGER_MAX_FILES);
16421
+ lines.push(`FILES CHANGED THIS SESSION (${ledger.filesTouchedTotal || ledger.filesTouched.size}):`);
16396
16422
  for (const [p, meta] of shown) {
16397
16423
  lines.push(` \u2022 ${p} (${meta.tool}${meta.edits > 1 ? ` \xD7${meta.edits}` : ""})`);
16398
16424
  }
@@ -16629,6 +16655,9 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16629
16655
  let completedRounds = 0;
16630
16656
  let stalledOut = false;
16631
16657
  let consecutiveErrorRounds = 0;
16658
+ let repeatedErrorRounds = 0;
16659
+ let lastErrorSignature = "";
16660
+ let stalledRepeatError = null;
16632
16661
  let budget = maxIterations;
16633
16662
  let iteration = 0;
16634
16663
  let filesMutatedSinceVerify = false;
@@ -16638,6 +16667,8 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16638
16667
  const flakyNudgedCmds = /* @__PURE__ */ new Set();
16639
16668
  let claimEvidenceNudged = false;
16640
16669
  const ledger = createLedger();
16670
+ let compactFailures = 0;
16671
+ let compactDisabled = false;
16641
16672
  try {
16642
16673
  for (; iteration < budget; iteration++) {
16643
16674
  if (options.abortSignal?.aborted)
@@ -16654,14 +16685,25 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16654
16685
  (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.`);
16655
16686
  }
16656
16687
  }
16657
- if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
16688
+ if (autoCompact && !compactDisabled && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
16658
16689
  compacting = true;
16659
16690
  try {
16691
+ const bytesBefore = estimateBodyBytes2(messages);
16660
16692
  const did = await autoCompactMessages(messages, options, ledger);
16693
+ const bytesAfter = did ? estimateBodyBytes2(messages) : bytesBefore;
16694
+ const reclaimed = bytesBefore - bytesAfter;
16661
16695
  if (did) {
16662
16696
  lastPromptTokens = 0;
16663
16697
  const reason = bytePressure ? `body ~${(bodyBytes / (1024 * 1024)).toFixed(1)}MB` : "context window";
16664
16698
  (options.onNotice ?? options.onText)(`\u267B\uFE0F Auto-compacted earlier conversation to stay within the ${reason}.`);
16699
+ bodyBytes = bytesAfter;
16700
+ bytePressure = bodyBytes > MAX_BODY_BYTES;
16701
+ }
16702
+ if (did && reclaimed >= COMPACT_MIN_RECLAIM_BYTES) {
16703
+ compactFailures = 0;
16704
+ } else if (++compactFailures >= COMPACT_MAX_FAILURES) {
16705
+ compactDisabled = true;
16706
+ (options.onNotice ?? options.onText)(`\u26A0\uFE0F Auto-compaction isn't reducing this conversation any further, so it's been switched off for the rest of this run to avoid repeated summarising. If the context fills up, start a fresh chat or run /compact manually.`);
16665
16707
  }
16666
16708
  } finally {
16667
16709
  compacting = false;
@@ -16776,9 +16818,10 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16776
16818
  messages.push({ role: "user", content: [{ type: "text", text }] });
16777
16819
  continue;
16778
16820
  }
16779
- if (ledger.testIntegrity.length > testIntegrityNudgedCount) {
16780
- const fresh = ledger.testIntegrity.slice(testIntegrityNudgedCount);
16781
- testIntegrityNudgedCount = ledger.testIntegrity.length;
16821
+ if (ledger.testIntegrityTotal > testIntegrityNudgedCount) {
16822
+ const newCount = Math.min(ledger.testIntegrityTotal - testIntegrityNudgedCount, ledger.testIntegrity.length);
16823
+ const fresh = ledger.testIntegrity.slice(ledger.testIntegrity.length - newCount);
16824
+ testIntegrityNudgedCount = ledger.testIntegrityTotal;
16782
16825
  const bullet = fresh.map((t2) => ` \u2022 ${t2.path}: ${t2.reason}`).join("\n");
16783
16826
  messages.push({
16784
16827
  role: "user",
@@ -16959,12 +17002,25 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
16959
17002
  messages.push(toolResultMessage);
16960
17003
  completedRounds++;
16961
17004
  options.onProgress?.(messages);
16962
- const allErrored = toolResults.length > 0 && toolResults.every(({ result }) => result.error !== void 0);
17005
+ const errored = toolResults.filter(({ result }) => result.error !== void 0);
17006
+ const allErrored = toolResults.length > 0 && errored.length === toolResults.length;
16963
17007
  consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
16964
17008
  if (consecutiveErrorRounds >= STALL_LIMIT) {
16965
17009
  stalledOut = true;
16966
17010
  break;
16967
17011
  }
17012
+ const errSignature = errorRoundSignature(errored.map(({ block, result }) => ({ name: block.name, error: String(result.error) })));
17013
+ if (errSignature && errSignature === lastErrorSignature) {
17014
+ repeatedErrorRounds++;
17015
+ } else {
17016
+ repeatedErrorRounds = 0;
17017
+ lastErrorSignature = errSignature;
17018
+ }
17019
+ if (repeatedErrorRounds >= REPEAT_STALL_LIMIT) {
17020
+ stalledOut = true;
17021
+ stalledRepeatError = errored[0] ? String(errored[0].result.error).slice(0, 300) : null;
17022
+ break;
17023
+ }
16968
17024
  if (autoContinue && iteration + 1 >= budget && budget < hardCap) {
16969
17025
  budget = Math.min(budget + maxIterations, hardCap);
16970
17026
  options.onText(`
@@ -16974,11 +17030,15 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
16974
17030
  }
16975
17031
  if (!options.abortSignal?.aborted && !completedCleanly) {
16976
17032
  if (stalledOut) {
16977
- options.onText(`
17033
+ (options.onNotice ?? options.onText)(stalledRepeatError ? `
17034
+ \u{1F6D1} Stopped: the same tool error repeated ${REPEAT_STALL_LIMIT} rounds in a row, so the agent was looping without making progress. The recurring error was:
17035
+ ${stalledRepeatError}
17036
+ Fix that underlying cause (or grant the needed permission) and send "continue".
17037
+ ` : `
16978
17038
  \u{1F6D1} Stopped: the last ${STALL_LIMIT} tool rounds all failed, so the agent looked stuck. Fix the underlying error (or grant the needed permission) and send "continue".
16979
17039
  `);
16980
17040
  } else if (iteration >= budget) {
16981
- options.onText(`
17041
+ (options.onNotice ?? options.onText)(`
16982
17042
  \u23F8\uFE0F Stopped at the ${budget}-step safety limit \u2014 the task may be incomplete. Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json (or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.
16983
17043
  `);
16984
17044
  }
@@ -63944,7 +64004,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
63944
64004
  };
63945
64005
 
63946
64006
  // src/commands/chat.ts
63947
- var CLI_VERSION = "0.5.51";
64007
+ var CLI_VERSION = "0.5.52";
63948
64008
  var MODEL_LABELS = {
63949
64009
  turbo: "Nexrall Turbo",
63950
64010
  pro: "Nexrall Pro",
@@ -65611,7 +65671,7 @@ function pluginSourceRemoveCommand(name, opts) {
65611
65671
 
65612
65672
  // src/index.ts
65613
65673
  var program2 = new Command();
65614
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.51").enablePositionalOptions();
65674
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.52").enablePositionalOptions();
65615
65675
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
65616
65676
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
65617
65677
  program2.command("update").description("Update nex to the latest version").option("-c, --check", "Check for updates without installing").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.51",
3
+ "version": "0.5.52",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",
@@ -41,7 +41,7 @@
41
41
  "react": "^19.2.8",
42
42
  "readline": "^1.3.0",
43
43
  "string-width": "^7.2.0",
44
- "@nexrall/code-core": "1.4.26"
44
+ "@nexrall/code-core": "1.4.27"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",