nexrall-code 0.5.51 → 0.5.53

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 +339 -245
  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;
10313
- }
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;
10280
+ {
10281
+ const frameId = event.id;
10282
+ if (frameId) {
10283
+ const n = Number(frameId);
10284
+ if (Number.isFinite(n) && n > lastEventId)
10285
+ lastEventId = n;
10334
10286
  }
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,9 @@ 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;
15768
+ exports.stopReasonNotice = stopReasonNotice;
15761
15769
  exports.resolveMaxIterations = resolveMaxIterations;
15762
15770
  exports.createLimiter = createLimiter;
15763
15771
  exports.extractSubTaskText = extractSubTaskText;
@@ -15890,6 +15898,46 @@ var require_loop = __commonJS({
15890
15898
  var MAX_ITERATIONS_CEILING = 2e3;
15891
15899
  var HARD_ITERATIONS_CAP = 1e5;
15892
15900
  var STALL_LIMIT = 8;
15901
+ var REPEAT_STALL_LIMIT = 12;
15902
+ function errorRoundSignature(errored) {
15903
+ return errored.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`).sort().join("|");
15904
+ }
15905
+ exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
15906
+ function stopReasonNotice(reason, ctx = {}) {
15907
+ switch (reason) {
15908
+ case "clean":
15909
+ case "aborted":
15910
+ case "reported-elsewhere":
15911
+ return null;
15912
+ case "empty-response":
15913
+ return `
15914
+ \u26A0\uFE0F The model returned an empty response, so nothing was done. This is usually a transient upstream hiccup \u2014 send "continue" to retry.
15915
+ `;
15916
+ case "no-balance":
15917
+ return `
15918
+ \u{1F4B3} Stopped: your balance is empty, so the request was rejected before it started. Top up and send "continue" \u2014 no tokens were used for this turn.
15919
+ `;
15920
+ case "stalled":
15921
+ return `
15922
+ \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".
15923
+ `;
15924
+ case "stalled-repeat":
15925
+ return `
15926
+ \u{1F6D1} Stopped: the same tool error repeated ${REPEAT_STALL_LIMIT} rounds in a row, so the agent was looping without making progress.` + (ctx.repeatError ? ` The recurring error was:
15927
+ ${ctx.repeatError}
15928
+ ` : "\n") + `Fix that underlying cause (or grant the needed permission) and send "continue".
15929
+ `;
15930
+ case "budget":
15931
+ return `
15932
+ \u23F8\uFE0F Stopped at the ${ctx.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.
15933
+ `;
15934
+ case "unknown":
15935
+ default:
15936
+ return `
15937
+ \u26A0\uFE0F The run ended unexpectedly without completing. Your work so far is preserved \u2014 send "continue" to resume.
15938
+ `;
15939
+ }
15940
+ }
15893
15941
  function resolveMaxIterations(optionValue, settingsRaw) {
15894
15942
  const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
15895
15943
  const fromSettings = Number(settingsRaw.maxIterations);
@@ -16266,6 +16314,8 @@ ${partial}` : "",
16266
16314
  }
16267
16315
  var PRUNE_MIN_RECLAIM_BYTES = 256 * 1024;
16268
16316
  var COMPACT_KEEP_MIN = 6;
16317
+ var COMPACT_MIN_RECLAIM_BYTES = 32 * 1024;
16318
+ var COMPACT_MAX_FAILURES = 3;
16269
16319
  var MAX_BODY_BYTES = 8 * 1024 * 1024;
16270
16320
  function estimateBodyBytes2(messages) {
16271
16321
  try {
@@ -16347,7 +16397,7 @@ ${tail}`;
16347
16397
  var LEDGER_MAX_FILES = 60;
16348
16398
  var LEDGER_MAX_NOTES = 20;
16349
16399
  function createLedger() {
16350
- return { filesTouched: /* @__PURE__ */ new Map(), verifications: [], testIntegrity: [], epoch: 0 };
16400
+ return { filesTouched: /* @__PURE__ */ new Map(), filesTouchedTotal: 0, verifications: [], testIntegrity: [], testIntegrityTotal: 0, epoch: 0 };
16351
16401
  }
16352
16402
  function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
16353
16403
  if (exports.WRITE_TOOL_NAMES.has(toolName)) {
@@ -16357,7 +16407,18 @@ ${tail}`;
16357
16407
  const p = typeof input?.path === "string" ? input.path : void 0;
16358
16408
  if (p) {
16359
16409
  const prev = ledger.filesTouched.get(p);
16410
+ if (!prev)
16411
+ ledger.filesTouchedTotal++;
16412
+ ledger.filesTouched.delete(p);
16360
16413
  ledger.filesTouched.set(p, { tool: toolName, edits: (prev?.edits ?? 0) + 1 });
16414
+ if (ledger.filesTouched.size > LEDGER_MAX_FILES * 4) {
16415
+ for (const key of ledger.filesTouched.keys()) {
16416
+ if (ledger.filesTouched.size <= LEDGER_MAX_FILES * 2)
16417
+ break;
16418
+ if (key !== p)
16419
+ ledger.filesTouched.delete(key);
16420
+ }
16421
+ }
16361
16422
  }
16362
16423
  const reasons = [];
16363
16424
  const markerReasons = toolName === "write_file" ? (0, testIntegrity_1.decodeTestIntegrityMarker)(output) : [];
@@ -16371,6 +16432,7 @@ ${tail}`;
16371
16432
  if (reasons.length && p) {
16372
16433
  for (const reason of reasons) {
16373
16434
  ledger.testIntegrity.push({ path: p, reason });
16435
+ ledger.testIntegrityTotal++;
16374
16436
  }
16375
16437
  if (ledger.testIntegrity.length > LEDGER_MAX_NOTES * 2) {
16376
16438
  ledger.testIntegrity.splice(0, ledger.testIntegrity.length - LEDGER_MAX_NOTES);
@@ -16391,8 +16453,8 @@ ${tail}`;
16391
16453
  const lines = [];
16392
16454
  if (ledger.filesTouched.size) {
16393
16455
  const files = [...ledger.filesTouched.entries()];
16394
- const shown = files.slice(0, LEDGER_MAX_FILES);
16395
- lines.push(`FILES CHANGED THIS SESSION (${ledger.filesTouched.size}):`);
16456
+ const shown = files.slice(-LEDGER_MAX_FILES);
16457
+ lines.push(`FILES CHANGED THIS SESSION (${ledger.filesTouchedTotal || ledger.filesTouched.size}):`);
16396
16458
  for (const [p, meta] of shown) {
16397
16459
  lines.push(` \u2022 ${p} (${meta.tool}${meta.edits > 1 ? ` \xD7${meta.edits}` : ""})`);
16398
16460
  }
@@ -16625,10 +16687,12 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16625
16687
  let lastPromptTokens = 0;
16626
16688
  let compacting = false;
16627
16689
  const hardCap = autoContinue ? Math.max(maxIterations, MAX_ITERATIONS_CEILING) : maxIterations;
16628
- let completedCleanly = false;
16690
+ let stopReason = "unknown";
16629
16691
  let completedRounds = 0;
16630
- let stalledOut = false;
16631
16692
  let consecutiveErrorRounds = 0;
16693
+ let repeatedErrorRounds = 0;
16694
+ let lastErrorSignature = "";
16695
+ let stalledRepeatError = null;
16632
16696
  let budget = maxIterations;
16633
16697
  let iteration = 0;
16634
16698
  let filesMutatedSinceVerify = false;
@@ -16638,10 +16702,14 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16638
16702
  const flakyNudgedCmds = /* @__PURE__ */ new Set();
16639
16703
  let claimEvidenceNudged = false;
16640
16704
  const ledger = createLedger();
16705
+ let compactFailures = 0;
16706
+ let compactDisabled = false;
16641
16707
  try {
16642
16708
  for (; iteration < budget; iteration++) {
16643
- if (options.abortSignal?.aborted)
16709
+ if (options.abortSignal?.aborted) {
16710
+ stopReason = "aborted";
16644
16711
  break;
16712
+ }
16645
16713
  let bodyBytes = estimateBodyBytes2(messages);
16646
16714
  const prunePressure = lastPromptTokens > contextWindow * AUTO_PRUNE_THRESHOLD;
16647
16715
  const tokenPressure = lastPromptTokens > contextWindow * AUTO_COMPACT_THRESHOLD;
@@ -16654,14 +16722,25 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16654
16722
  (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
16723
  }
16656
16724
  }
16657
- if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
16725
+ if (autoCompact && !compactDisabled && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
16658
16726
  compacting = true;
16659
16727
  try {
16728
+ const bytesBefore = estimateBodyBytes2(messages);
16660
16729
  const did = await autoCompactMessages(messages, options, ledger);
16730
+ const bytesAfter = did ? estimateBodyBytes2(messages) : bytesBefore;
16731
+ const reclaimed = bytesBefore - bytesAfter;
16661
16732
  if (did) {
16662
16733
  lastPromptTokens = 0;
16663
16734
  const reason = bytePressure ? `body ~${(bodyBytes / (1024 * 1024)).toFixed(1)}MB` : "context window";
16664
16735
  (options.onNotice ?? options.onText)(`\u267B\uFE0F Auto-compacted earlier conversation to stay within the ${reason}.`);
16736
+ bodyBytes = bytesAfter;
16737
+ bytePressure = bodyBytes > MAX_BODY_BYTES;
16738
+ }
16739
+ if (did && reclaimed >= COMPACT_MIN_RECLAIM_BYTES) {
16740
+ compactFailures = 0;
16741
+ } else if (++compactFailures >= COMPACT_MAX_FAILURES) {
16742
+ compactDisabled = true;
16743
+ (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
16744
  }
16666
16745
  } finally {
16667
16746
  compacting = false;
@@ -16736,19 +16815,24 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16736
16815
  allowRestartAfterRender: !!options.onStreamRestart
16737
16816
  }, onEvent);
16738
16817
  } catch (err) {
16739
- if (options.abortSignal?.aborted || err.name === "AbortError")
16818
+ if (options.abortSignal?.aborted || err.name === "AbortError") {
16819
+ stopReason = "aborted";
16740
16820
  break;
16821
+ }
16741
16822
  const status = err.status;
16742
16823
  if (status === 402) {
16743
16824
  const balance = err.balance ?? 0;
16744
16825
  options.onBalanceStatus?.(balance, true);
16826
+ stopReason = options.onBalanceStatus ? "reported-elsewhere" : "no-balance";
16745
16827
  break;
16746
16828
  }
16747
16829
  runSimpleHooks(hooks.OnError, options.workDir);
16748
16830
  throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
16749
16831
  }
16750
- if (options.abortSignal?.aborted)
16832
+ if (options.abortSignal?.aborted) {
16833
+ stopReason = "aborted";
16751
16834
  break;
16835
+ }
16752
16836
  assistantMessage.content = assistantMessage.content.filter((b) => b.type !== "thinking" && b.type !== "redacted_thinking");
16753
16837
  if (assistantMessage.content.length === 0) {
16754
16838
  const queued = options.takePendingInput?.() ?? [];
@@ -16759,7 +16843,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16759
16843
  continue;
16760
16844
  }
16761
16845
  runSimpleHooks(hooks.PostMessageComplete, options.workDir);
16762
- completedCleanly = true;
16846
+ stopReason = "empty-response";
16763
16847
  break;
16764
16848
  }
16765
16849
  const { stopReason: _stopReason, ...historyMessage } = assistantMessage;
@@ -16776,9 +16860,10 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16776
16860
  messages.push({ role: "user", content: [{ type: "text", text }] });
16777
16861
  continue;
16778
16862
  }
16779
- if (ledger.testIntegrity.length > testIntegrityNudgedCount) {
16780
- const fresh = ledger.testIntegrity.slice(testIntegrityNudgedCount);
16781
- testIntegrityNudgedCount = ledger.testIntegrity.length;
16863
+ if (ledger.testIntegrityTotal > testIntegrityNudgedCount) {
16864
+ const newCount = Math.min(ledger.testIntegrityTotal - testIntegrityNudgedCount, ledger.testIntegrity.length);
16865
+ const fresh = ledger.testIntegrity.slice(ledger.testIntegrity.length - newCount);
16866
+ testIntegrityNudgedCount = ledger.testIntegrityTotal;
16782
16867
  const bullet = fresh.map((t2) => ` \u2022 ${t2.path}: ${t2.reason}`).join("\n");
16783
16868
  messages.push({
16784
16869
  role: "user",
@@ -16829,7 +16914,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16829
16914
  }
16830
16915
  }
16831
16916
  runSimpleHooks(hooks.PostMessageComplete, options.workDir);
16832
- completedCleanly = true;
16917
+ stopReason = "clean";
16833
16918
  break;
16834
16919
  }
16835
16920
  const toolResults = await Promise.all(toolUseBlocks.map(async (block) => {
@@ -16959,10 +17044,23 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
16959
17044
  messages.push(toolResultMessage);
16960
17045
  completedRounds++;
16961
17046
  options.onProgress?.(messages);
16962
- const allErrored = toolResults.length > 0 && toolResults.every(({ result }) => result.error !== void 0);
17047
+ const errored = toolResults.filter(({ result }) => result.error !== void 0);
17048
+ const allErrored = toolResults.length > 0 && errored.length === toolResults.length;
16963
17049
  consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
16964
17050
  if (consecutiveErrorRounds >= STALL_LIMIT) {
16965
- stalledOut = true;
17051
+ stopReason = "stalled";
17052
+ break;
17053
+ }
17054
+ const errSignature = errorRoundSignature(errored.map(({ block, result }) => ({ name: block.name, error: String(result.error) })));
17055
+ if (errSignature && errSignature === lastErrorSignature) {
17056
+ repeatedErrorRounds++;
17057
+ } else {
17058
+ repeatedErrorRounds = 0;
17059
+ lastErrorSignature = errSignature;
17060
+ }
17061
+ if (repeatedErrorRounds >= REPEAT_STALL_LIMIT) {
17062
+ stopReason = "stalled-repeat";
17063
+ stalledRepeatError = errored[0] ? String(errored[0].result.error).slice(0, 300) : null;
16966
17064
  break;
16967
17065
  }
16968
17066
  if (autoContinue && iteration + 1 >= budget && budget < hardCap) {
@@ -16972,17 +17070,13 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
16972
17070
  `);
16973
17071
  }
16974
17072
  }
16975
- if (!options.abortSignal?.aborted && !completedCleanly) {
16976
- if (stalledOut) {
16977
- options.onText(`
16978
- \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
- `);
16980
- } else if (iteration >= budget) {
16981
- options.onText(`
16982
- \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
- `);
16984
- }
16985
- }
17073
+ if (stopReason === "unknown" && iteration >= budget)
17074
+ stopReason = "budget";
17075
+ if (options.abortSignal?.aborted)
17076
+ stopReason = "aborted";
17077
+ const notice = stopReasonNotice(stopReason, { budget, repeatError: stalledRepeatError });
17078
+ if (notice)
17079
+ (options.onNotice ?? options.onText)(notice);
16986
17080
  } catch (err) {
16987
17081
  if (err instanceof types_1.AgentTurnError)
16988
17082
  throw err;
@@ -63944,7 +64038,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
63944
64038
  };
63945
64039
 
63946
64040
  // src/commands/chat.ts
63947
- var CLI_VERSION = "0.5.51";
64041
+ var CLI_VERSION = "0.5.53";
63948
64042
  var MODEL_LABELS = {
63949
64043
  turbo: "Nexrall Turbo",
63950
64044
  pro: "Nexrall Pro",
@@ -65611,7 +65705,7 @@ function pluginSourceRemoveCommand(name, opts) {
65611
65705
 
65612
65706
  // src/index.ts
65613
65707
  var program2 = new Command();
65614
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.51").enablePositionalOptions();
65708
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.53").enablePositionalOptions();
65615
65709
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
65616
65710
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
65617
65711
  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.53",
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.28"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",