nexrall-code 0.5.50 → 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 +689 -236
  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
  }
@@ -15192,6 +15198,9 @@ var require_rules = __commonJS({
15192
15198
  case "stock_photo":
15193
15199
  vals.push(str(input.path));
15194
15200
  break;
15201
+ case "task":
15202
+ vals.push(str(input.subagent_type));
15203
+ break;
15195
15204
  default:
15196
15205
  pushPath(str(input.path));
15197
15206
  break;
@@ -15232,6 +15241,350 @@ var require_rules = __commonJS({
15232
15241
  }
15233
15242
  });
15234
15243
 
15244
+ // ../core/dist/agent/planMode.js
15245
+ var require_planMode = __commonJS({
15246
+ "../core/dist/agent/planMode.js"(exports) {
15247
+ "use strict";
15248
+ Object.defineProperty(exports, "__esModule", { value: true });
15249
+ exports.PLAN_MODE_INSTRUCTIONS = void 0;
15250
+ exports.isReadOnlyCommand = isReadOnlyCommand;
15251
+ exports.checkPlanMode = checkPlanMode;
15252
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
15253
+ "read_file",
15254
+ "search_files",
15255
+ "glob",
15256
+ "list_directory",
15257
+ "notebook_read",
15258
+ // Planning bookkeeping. todo_write persists only to the session's own todo
15259
+ // list, not the repo, so it stays available — a plan mode that cannot draft
15260
+ // a checklist is missing the point of plan mode.
15261
+ "todo_write",
15262
+ "todo_read",
15263
+ // Reading memory is fine; memory_write is NOT here on purpose. Memory is
15264
+ // durable state that survives the session, so writing it is a real mutation
15265
+ // even though no file in the repo changes.
15266
+ "memory_read",
15267
+ "use_skill",
15268
+ "fetch_url",
15269
+ "web_search",
15270
+ // Delegating research is ALLOWED, and this is safe rather than a hole:
15271
+ // runSubTask passes planMode down, so the sub-agent is under the same lock and
15272
+ // its own write tools are refused by this very function one level deeper.
15273
+ //
15274
+ // Worth allowing rather than blanket-refusing, because plan mode exists to
15275
+ // support deep research and the explorer agent is the cheapest way to do bulk
15276
+ // searching without flooding the main context. Blocking `task` would have made
15277
+ // the mode's headline use case worse, for no security gain.
15278
+ "task",
15279
+ // VS Code language server — all pure queries.
15280
+ "get_symbols",
15281
+ "get_workspace_symbols",
15282
+ "find_references",
15283
+ "go_to_definition",
15284
+ "get_hover",
15285
+ "get_diagnostics",
15286
+ // Reading output from an ALREADY-running background shell. Starting one is
15287
+ // gated with the rest of bash below; draining a buffer is not a new effect.
15288
+ "bash_output"
15289
+ ]);
15290
+ var READ_ONLY_BASH = /* @__PURE__ */ new Set([
15291
+ "ls",
15292
+ "cat",
15293
+ "head",
15294
+ "tail",
15295
+ "wc",
15296
+ "file",
15297
+ "stat",
15298
+ "du",
15299
+ "df",
15300
+ "pwd",
15301
+ "which",
15302
+ "type",
15303
+ "echo",
15304
+ "printf",
15305
+ "basename",
15306
+ "dirname",
15307
+ "realpath",
15308
+ "readlink",
15309
+ "grep",
15310
+ "egrep",
15311
+ "fgrep",
15312
+ "rg",
15313
+ "ag",
15314
+ "ack",
15315
+ "find",
15316
+ "fd",
15317
+ "locate",
15318
+ "diff",
15319
+ "comm",
15320
+ "cmp",
15321
+ "sort",
15322
+ "uniq",
15323
+ "cut",
15324
+ "tr",
15325
+ "column",
15326
+ "jq",
15327
+ "yq",
15328
+ "date",
15329
+ "whoami",
15330
+ "hostname",
15331
+ "uname",
15332
+ "env",
15333
+ "printenv",
15334
+ "id",
15335
+ "groups",
15336
+ "ps",
15337
+ "top",
15338
+ "uptime",
15339
+ "free",
15340
+ "node",
15341
+ "python",
15342
+ "python3",
15343
+ "tree",
15344
+ "less",
15345
+ "more",
15346
+ "nl",
15347
+ "tac",
15348
+ "strings",
15349
+ "md5sum",
15350
+ "sha256sum",
15351
+ "true",
15352
+ "false",
15353
+ "test",
15354
+ "sleep",
15355
+ "seq",
15356
+ "expr"
15357
+ ]);
15358
+ var READ_ONLY_SUBCOMMANDS = {
15359
+ git: /* @__PURE__ */ new Set([
15360
+ "log",
15361
+ "show",
15362
+ "diff",
15363
+ "status",
15364
+ "blame",
15365
+ "branch",
15366
+ "tag",
15367
+ "remote",
15368
+ "describe",
15369
+ "rev-parse",
15370
+ "rev-list",
15371
+ "ls-files",
15372
+ "ls-tree",
15373
+ "ls-remote",
15374
+ "cat-file",
15375
+ "shortlog",
15376
+ "reflog",
15377
+ "whatchanged",
15378
+ "grep",
15379
+ "config",
15380
+ "check-ignore",
15381
+ "merge-base",
15382
+ "name-rev",
15383
+ "count-objects",
15384
+ "verify-commit"
15385
+ ]),
15386
+ // Package managers: only their query verbs.
15387
+ npm: /* @__PURE__ */ new Set(["ls", "list", "view", "info", "outdated", "why", "config", "ping", "search"]),
15388
+ pnpm: /* @__PURE__ */ new Set(["ls", "list", "view", "info", "outdated", "why", "config"]),
15389
+ yarn: /* @__PURE__ */ new Set(["list", "info", "why", "config"]),
15390
+ cargo: /* @__PURE__ */ new Set(["tree", "metadata", "search"]),
15391
+ go: /* @__PURE__ */ new Set(["list", "version", "env", "vet", "doc"]),
15392
+ docker: /* @__PURE__ */ new Set(["ps", "images", "logs", "inspect", "version", "info", "top", "port", "diff", "stats"]),
15393
+ kubectl: /* @__PURE__ */ new Set(["get", "describe", "logs", "explain", "top", "version", "api-resources", "api-versions"]),
15394
+ // gh: read verbs only. `gh run rerun`, `gh pr merge` etc. are excluded, and
15395
+ // the nested-verb check in isReadOnlyCommand handles `gh pr view` vs `gh pr merge`.
15396
+ gh: /* @__PURE__ */ new Set(["browse"])
15397
+ };
15398
+ var READ_ONLY_NESTED = {
15399
+ gh: /* @__PURE__ */ new Set(["view", "list", "status", "diff", "checks"])
15400
+ };
15401
+ var SHELL_CONTROL = /[\n\r;&|`]|\$\(|>>|>|<\(/;
15402
+ function firstWords(command) {
15403
+ return command.trim().split(/\s+/).filter(Boolean);
15404
+ }
15405
+ function isReadOnlyCommand(command) {
15406
+ const cmd = command.trim();
15407
+ if (!cmd)
15408
+ return false;
15409
+ if (SHELL_CONTROL.test(cmd))
15410
+ return false;
15411
+ const words = firstWords(cmd);
15412
+ if (!words.length)
15413
+ return false;
15414
+ let i2 = 0;
15415
+ while (i2 < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i2]))
15416
+ i2++;
15417
+ if (i2 >= words.length)
15418
+ return false;
15419
+ let verb = words[i2];
15420
+ const slash = verb.lastIndexOf("/");
15421
+ if (slash >= 0)
15422
+ verb = verb.slice(slash + 1);
15423
+ if (verb === "sudo" || verb === "doas" || verb === "su")
15424
+ return false;
15425
+ const rest = words.slice(i2 + 1).filter((w) => !w.startsWith("-"));
15426
+ const subs = READ_ONLY_SUBCOMMANDS[verb];
15427
+ if (subs) {
15428
+ const sub = rest[0];
15429
+ if (!sub) {
15430
+ return true;
15431
+ }
15432
+ if (subs.has(sub)) {
15433
+ if (verb === "git" && sub === "config") {
15434
+ const args = words.slice(i2 + 2).filter((w) => !w.startsWith("-"));
15435
+ return args.length <= 1;
15436
+ }
15437
+ return true;
15438
+ }
15439
+ const nested = READ_ONLY_NESTED[verb];
15440
+ if (nested && rest[1] && nested.has(rest[1]))
15441
+ return true;
15442
+ return false;
15443
+ }
15444
+ if (!READ_ONLY_TOOLS.has(verb) && !READ_ONLY_BASH.has(verb))
15445
+ return false;
15446
+ if (verb === "node" || verb === "python" || verb === "python3") {
15447
+ return words.slice(i2 + 1).every((w) => w === "--version" || w === "-V" || w === "-v");
15448
+ }
15449
+ if (verb === "find" && words.some((w) => w === "-exec" || w === "-execdir" || w === "-delete")) {
15450
+ return false;
15451
+ }
15452
+ return true;
15453
+ }
15454
+ function checkPlanMode(tool, input) {
15455
+ if (tool === "bash") {
15456
+ const command = typeof input.command === "string" ? input.command : "";
15457
+ if (input.run_in_background === true) {
15458
+ return {
15459
+ reason: "bash-not-read-only",
15460
+ message: "Plan mode is ON: background processes cannot be started. Finish researching and propose your plan; the user will leave plan mode if they want it carried out."
15461
+ };
15462
+ }
15463
+ if (isReadOnlyCommand(command))
15464
+ return null;
15465
+ return {
15466
+ reason: "bash-not-read-only",
15467
+ message: `Plan mode is ON, so \`bash\` is limited to provably read-only commands and this one was not recognised as such (chained commands, redirection and command substitution are always refused). Do NOT ask the user to approve it and do NOT try a variation to get around this \u2014 it is a session-wide lock, not a per-call prompt. Use read-only inspection instead, and put the command in your plan as a step to run once plan mode is off.`
15468
+ };
15469
+ }
15470
+ if (READ_ONLY_TOOLS.has(tool))
15471
+ return null;
15472
+ return {
15473
+ reason: "mutating-tool",
15474
+ message: `Plan mode is ON, so \`${tool}\` is unavailable \u2014 this session may not modify anything yet. Do NOT ask for approval: no answer the user gives at this prompt can unlock it, because it is a session-wide lock they control directly. Research with read-only tools and finish by proposing a concrete plan (files to change, in order, and how to verify). The user will exit plan mode to let you carry it out.`
15475
+ };
15476
+ }
15477
+ exports.PLAN_MODE_INSTRUCTIONS = [
15478
+ "# PLAN MODE IS ACTIVE",
15479
+ "",
15480
+ "This session is READ-ONLY. You cannot edit files, write files, run mutating commands, or start",
15481
+ "background processes. This is enforced outside your control \u2014 it is not a permission prompt, and",
15482
+ "the user cannot approve an exception from inside this turn.",
15483
+ "",
15484
+ "Therefore:",
15485
+ '- NEVER ask "shall I go ahead and make this change?" \u2014 you cannot, whatever the answer.',
15486
+ '- NEVER attempt a mutating tool "just to see" \u2014 it will be refused and wastes the turn.',
15487
+ "- NEVER try to reach a blocked command another way (a different flag, a script, an interpreter).",
15488
+ "",
15489
+ "Your job is to research thoroughly, then deliver a plan:",
15490
+ "1. What you found \u2014 the specific files, functions and line numbers that matter.",
15491
+ "2. What you propose to change \u2014 file by file, in dependency order.",
15492
+ "3. Risks and unknowns \u2014 what could break, what you could not verify, what you assumed.",
15493
+ "4. How it will be verified \u2014 the exact build/test/lint command that proves it works.",
15494
+ "",
15495
+ 'Be concrete. "Update the auth logic" is not a plan; "add a `refreshToken` field to AuthConfig in',
15496
+ 'auth/index.ts:42, then thread it through client.ts:118" is. The user will review the plan and',
15497
+ "leave plan mode to have it carried out."
15498
+ ].join("\n");
15499
+ }
15500
+ });
15501
+
15502
+ // ../core/dist/agent/agentRegistry.js
15503
+ var require_agentRegistry = __commonJS({
15504
+ "../core/dist/agent/agentRegistry.js"(exports) {
15505
+ "use strict";
15506
+ Object.defineProperty(exports, "__esModule", { value: true });
15507
+ exports._limits = void 0;
15508
+ exports.rememberAgent = rememberAgent;
15509
+ exports.updateAgent = updateAgent;
15510
+ exports.getAgent = getAgent;
15511
+ exports.listAgents = listAgents;
15512
+ exports._resetAgentRegistry = _resetAgentRegistry;
15513
+ var MAX_AGENTS = 20;
15514
+ var MAX_TOTAL_BYTES = 8 * 1024 * 1024;
15515
+ var _agents = /* @__PURE__ */ new Map();
15516
+ var _counter = 0;
15517
+ var _seq = 0;
15518
+ function totalBytes() {
15519
+ let n = 0;
15520
+ for (const a of _agents.values())
15521
+ n += a.bytes;
15522
+ return n;
15523
+ }
15524
+ function evictUntilWithinLimits() {
15525
+ while (_agents.size > MAX_AGENTS) {
15526
+ const oldest = _agents.keys().next().value;
15527
+ if (oldest === void 0)
15528
+ break;
15529
+ _agents.delete(oldest);
15530
+ }
15531
+ while (totalBytes() > MAX_TOTAL_BYTES && _agents.size > 1) {
15532
+ const oldest = _agents.keys().next().value;
15533
+ if (oldest === void 0)
15534
+ break;
15535
+ _agents.delete(oldest);
15536
+ }
15537
+ }
15538
+ function sizeOf(messages) {
15539
+ try {
15540
+ return JSON.stringify(messages).length;
15541
+ } catch {
15542
+ return MAX_TOTAL_BYTES;
15543
+ }
15544
+ }
15545
+ function rememberAgent(agentName, description, messages) {
15546
+ const id = `agent_${++_counter}`;
15547
+ const now = Date.now();
15548
+ _agents.set(id, {
15549
+ id,
15550
+ agentName,
15551
+ description,
15552
+ messages,
15553
+ createdAt: now,
15554
+ updatedAt: now,
15555
+ seq: ++_seq,
15556
+ bytes: sizeOf(messages)
15557
+ });
15558
+ evictUntilWithinLimits();
15559
+ return id;
15560
+ }
15561
+ function updateAgent(id, messages) {
15562
+ const existing = _agents.get(id);
15563
+ if (!existing)
15564
+ return;
15565
+ existing.messages = messages;
15566
+ existing.updatedAt = Date.now();
15567
+ existing.seq = ++_seq;
15568
+ existing.bytes = sizeOf(messages);
15569
+ _agents.delete(id);
15570
+ _agents.set(id, existing);
15571
+ evictUntilWithinLimits();
15572
+ }
15573
+ function getAgent(id) {
15574
+ return _agents.get(id);
15575
+ }
15576
+ function listAgents() {
15577
+ return [..._agents.values()].sort((a, b) => b.seq - a.seq);
15578
+ }
15579
+ function _resetAgentRegistry() {
15580
+ _agents.clear();
15581
+ _counter = 0;
15582
+ _seq = 0;
15583
+ }
15584
+ exports._limits = { MAX_AGENTS, MAX_TOTAL_BYTES };
15585
+ }
15586
+ });
15587
+
15235
15588
  // ../core/dist/agent/flaky.js
15236
15589
  var require_flaky = __commonJS({
15237
15590
  "../core/dist/agent/flaky.js"(exports) {
@@ -15410,7 +15763,8 @@ var require_loop = __commonJS({
15410
15763
  };
15411
15764
  }();
15412
15765
  Object.defineProperty(exports, "__esModule", { value: true });
15413
- 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;
15414
15768
  exports.resolveMaxIterations = resolveMaxIterations;
15415
15769
  exports.createLimiter = createLimiter;
15416
15770
  exports.extractSubTaskText = extractSubTaskText;
@@ -15436,6 +15790,8 @@ var require_loop = __commonJS({
15436
15790
  var agentTypes_1 = require_agentTypes();
15437
15791
  var skills_1 = require_skills();
15438
15792
  var rules_1 = require_rules();
15793
+ var planMode_1 = require_planMode();
15794
+ var agentRegistry_1 = require_agentRegistry();
15439
15795
  var sandbox_1 = require_sandbox();
15440
15796
  var index_1 = require_plugins();
15441
15797
  var testIntegrity_1 = require_testIntegrity();
@@ -15541,6 +15897,11 @@ var require_loop = __commonJS({
15541
15897
  var MAX_ITERATIONS_CEILING = 2e3;
15542
15898
  var HARD_ITERATIONS_CAP = 1e5;
15543
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 };
15544
15905
  function resolveMaxIterations(optionValue, settingsRaw) {
15545
15906
  const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
15546
15907
  const fromSettings = Number(settingsRaw.maxIterations);
@@ -15756,7 +16117,22 @@ ${tail}`;
15756
16117
  if (depth >= MAX_TASK_DEPTH) {
15757
16118
  return { error: "Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it." };
15758
16119
  }
15759
- const requestedType = typeof input.subagent_type === "string" ? input.subagent_type : "";
16120
+ const resumeId = typeof input.resume_agent_id === "string" ? input.resume_agent_id.trim() : "";
16121
+ const resumed = resumeId ? (0, agentRegistry_1.getAgent)(resumeId) : void 0;
16122
+ if (resumeId && !resumed) {
16123
+ return {
16124
+ error: `No resumable sub-agent with id "${resumeId}". Ids live only for the current session and the oldest are dropped when too many accumulate, so this one has expired or never existed. Start a fresh sub-task with a self-contained prompt instead.`
16125
+ };
16126
+ }
16127
+ const requestedType = resumed ? resumed.agentName ?? "" : typeof input.subagent_type === "string" ? input.subagent_type : "";
16128
+ if (requestedType) {
16129
+ const decision = (0, rules_1.evaluatePermission)((0, rules_1.loadSettings)(options.workDir).permissions, "task", { subagent_type: requestedType }, options.workDir);
16130
+ if (decision === "deny") {
16131
+ return {
16132
+ error: `The sub-agent "${requestedType}" is disabled by a permission rule in this project (permissions.deny in settings.json). This is a deliberate policy choice, not a missing file \u2014 do not create it and do not retry. Do the work yourself, or use a different sub-agent.`
16133
+ };
16134
+ }
16135
+ }
15760
16136
  let agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
15761
16137
  let knownTypes = agentTypes;
15762
16138
  if (requestedType && !agent) {
@@ -15786,9 +16162,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
15786
16162
  }
15787
16163
  return options.requestPermission(req);
15788
16164
  };
15789
- const subMessages = [
15790
- { role: "user", content: [{ type: "text", text: prompt2 }] }
15791
- ];
16165
+ const subMessages = resumed ? [...resumed.messages, { role: "user", content: [{ type: "text", text: prompt2 }] }] : [{ role: "user", content: [{ type: "text", text: prompt2 }] }];
15792
16166
  const subAbort = { aborted: false };
15793
16167
  const timer = setTimeout(() => {
15794
16168
  subAbort.aborted = true;
@@ -15806,6 +16180,9 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
15806
16180
  editorContext: null,
15807
16181
  // fresh isolated context for sub-agent
15808
16182
  model: agent?.model ?? options.model,
16183
+ // Plan mode is inherited, never relaxed. If the main agent could spawn a
16184
+ // sub-agent that writes, the lock would be one `task` call from useless.
16185
+ planMode: options.planMode,
15809
16186
  nexrallMd: subNexrallMd,
15810
16187
  abortSignal: subAbort,
15811
16188
  requestPermission: gatedPermission,
@@ -15854,7 +16231,13 @@ ${partial}` : "",
15854
16231
  return { error: sections.join("\n\n") };
15855
16232
  }
15856
16233
  const text = capSubTaskText(extractSubTaskText(result, true));
15857
- return { output: text || "(sub-task completed with no text output)" };
16234
+ const agentId = resumed ? ((0, agentRegistry_1.updateAgent)(resumed.id, result), resumed.id) : (0, agentRegistry_1.rememberAgent)(agent?.name ?? null, typeof input.description === "string" && input.description.trim() || prompt2.slice(0, 80), result);
16235
+ const body = text || "(sub-task completed with no text output)";
16236
+ return {
16237
+ output: `${body}
16238
+
16239
+ [resumable: this sub-agent is "${agentId}". To ask IT a follow-up \u2014 keeping everything it already read and concluded \u2014 call task again with resume_agent_id="` + agentId + '" instead of writing a new prompt from scratch.]'
16240
+ };
15858
16241
  } catch (err) {
15859
16242
  const salvaged = (0, types_1.salvageHistory)(err);
15860
16243
  if (salvaged) {
@@ -15895,6 +16278,8 @@ ${partial}` : "",
15895
16278
  }
15896
16279
  var PRUNE_MIN_RECLAIM_BYTES = 256 * 1024;
15897
16280
  var COMPACT_KEEP_MIN = 6;
16281
+ var COMPACT_MIN_RECLAIM_BYTES = 32 * 1024;
16282
+ var COMPACT_MAX_FAILURES = 3;
15898
16283
  var MAX_BODY_BYTES = 8 * 1024 * 1024;
15899
16284
  function estimateBodyBytes2(messages) {
15900
16285
  try {
@@ -15976,7 +16361,7 @@ ${tail}`;
15976
16361
  var LEDGER_MAX_FILES = 60;
15977
16362
  var LEDGER_MAX_NOTES = 20;
15978
16363
  function createLedger() {
15979
- return { filesTouched: /* @__PURE__ */ new Map(), verifications: [], testIntegrity: [], epoch: 0 };
16364
+ return { filesTouched: /* @__PURE__ */ new Map(), filesTouchedTotal: 0, verifications: [], testIntegrity: [], testIntegrityTotal: 0, epoch: 0 };
15980
16365
  }
15981
16366
  function ledgerRecord(ledger, toolName, input, ok, output, exitCode) {
15982
16367
  if (exports.WRITE_TOOL_NAMES.has(toolName)) {
@@ -15986,7 +16371,18 @@ ${tail}`;
15986
16371
  const p = typeof input?.path === "string" ? input.path : void 0;
15987
16372
  if (p) {
15988
16373
  const prev = ledger.filesTouched.get(p);
16374
+ if (!prev)
16375
+ ledger.filesTouchedTotal++;
16376
+ ledger.filesTouched.delete(p);
15989
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
+ }
15990
16386
  }
15991
16387
  const reasons = [];
15992
16388
  const markerReasons = toolName === "write_file" ? (0, testIntegrity_1.decodeTestIntegrityMarker)(output) : [];
@@ -16000,6 +16396,7 @@ ${tail}`;
16000
16396
  if (reasons.length && p) {
16001
16397
  for (const reason of reasons) {
16002
16398
  ledger.testIntegrity.push({ path: p, reason });
16399
+ ledger.testIntegrityTotal++;
16003
16400
  }
16004
16401
  if (ledger.testIntegrity.length > LEDGER_MAX_NOTES * 2) {
16005
16402
  ledger.testIntegrity.splice(0, ledger.testIntegrity.length - LEDGER_MAX_NOTES);
@@ -16020,8 +16417,8 @@ ${tail}`;
16020
16417
  const lines = [];
16021
16418
  if (ledger.filesTouched.size) {
16022
16419
  const files = [...ledger.filesTouched.entries()];
16023
- const shown = files.slice(0, LEDGER_MAX_FILES);
16024
- 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}):`);
16025
16422
  for (const [p, meta] of shown) {
16026
16423
  lines.push(` \u2022 ${p} (${meta.tool}${meta.edits > 1 ? ` \xD7${meta.edits}` : ""})`);
16027
16424
  }
@@ -16236,10 +16633,15 @@ Continue the work from here.` }] });
16236
16633
  const hooks = loadHooks(options.workDir);
16237
16634
  const depth = options._depth ?? 0;
16238
16635
  const agentScope = options._agentScope ?? "root";
16239
- const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
16636
+ const settings = (0, rules_1.loadSettings)(options.workDir);
16637
+ const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir).filter((t2) => (0, rules_1.evaluatePermission)(settings.permissions, "task", { subagent_type: t2.name }, options.workDir) !== "deny");
16240
16638
  const agentsCatalogue = depth === 0 ? (0, agentTypes_1.summariseAgents)(agentTypes) : "";
16241
16639
  const skillsCatalogue = (0, skills_1.summariseSkills)((0, skills_1.loadSkills)(options.workDir));
16242
- const settings = (0, rules_1.loadSettings)(options.workDir);
16640
+ const planAwareNexrallMd = options.planMode ? planMode_1.PLAN_MODE_INSTRUCTIONS + (options.nexrallMd ? `
16641
+
16642
+ ---
16643
+
16644
+ ${options.nexrallMd}` : "") : options.nexrallMd;
16243
16645
  const sandboxCfg = (0, sandbox_1.parseSandboxConfig)(settings.raw.sandbox) ?? void 0;
16244
16646
  const maxIterations = resolveMaxIterations(options.maxIterations, settings.raw);
16245
16647
  const autoContinue = resolveAutoContinue(options.autoContinue, settings.raw);
@@ -16253,6 +16655,9 @@ Continue the work from here.` }] });
16253
16655
  let completedRounds = 0;
16254
16656
  let stalledOut = false;
16255
16657
  let consecutiveErrorRounds = 0;
16658
+ let repeatedErrorRounds = 0;
16659
+ let lastErrorSignature = "";
16660
+ let stalledRepeatError = null;
16256
16661
  let budget = maxIterations;
16257
16662
  let iteration = 0;
16258
16663
  let filesMutatedSinceVerify = false;
@@ -16262,6 +16667,8 @@ Continue the work from here.` }] });
16262
16667
  const flakyNudgedCmds = /* @__PURE__ */ new Set();
16263
16668
  let claimEvidenceNudged = false;
16264
16669
  const ledger = createLedger();
16670
+ let compactFailures = 0;
16671
+ let compactDisabled = false;
16265
16672
  try {
16266
16673
  for (; iteration < budget; iteration++) {
16267
16674
  if (options.abortSignal?.aborted)
@@ -16278,14 +16685,25 @@ Continue the work from here.` }] });
16278
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.`);
16279
16686
  }
16280
16687
  }
16281
- if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
16688
+ if (autoCompact && !compactDisabled && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
16282
16689
  compacting = true;
16283
16690
  try {
16691
+ const bytesBefore = estimateBodyBytes2(messages);
16284
16692
  const did = await autoCompactMessages(messages, options, ledger);
16693
+ const bytesAfter = did ? estimateBodyBytes2(messages) : bytesBefore;
16694
+ const reclaimed = bytesBefore - bytesAfter;
16285
16695
  if (did) {
16286
16696
  lastPromptTokens = 0;
16287
16697
  const reason = bytePressure ? `body ~${(bodyBytes / (1024 * 1024)).toFixed(1)}MB` : "context window";
16288
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.`);
16289
16707
  }
16290
16708
  } finally {
16291
16709
  compacting = false;
@@ -16347,7 +16765,7 @@ Continue the work from here.` }] });
16347
16765
  effort: options.effort,
16348
16766
  env: options.env,
16349
16767
  editorContext: options.editorContext,
16350
- nexrallMd: options.nexrallMd,
16768
+ nexrallMd: planAwareNexrallMd,
16351
16769
  clientType: options.clientType,
16352
16770
  abortSignal: options.abortSignal,
16353
16771
  extraTools: options.mcpManager?.getAnthropicTools(),
@@ -16400,9 +16818,10 @@ Continue the work from here.` }] });
16400
16818
  messages.push({ role: "user", content: [{ type: "text", text }] });
16401
16819
  continue;
16402
16820
  }
16403
- if (ledger.testIntegrity.length > testIntegrityNudgedCount) {
16404
- const fresh = ledger.testIntegrity.slice(testIntegrityNudgedCount);
16405
- 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;
16406
16825
  const bullet = fresh.map((t2) => ` \u2022 ${t2.path}: ${t2.reason}`).join("\n");
16407
16826
  messages.push({
16408
16827
  role: "user",
@@ -16467,6 +16886,14 @@ Continue the work from here.` }] });
16467
16886
  options.onToolResult(name, result);
16468
16887
  return { block: { ...block, id }, result };
16469
16888
  }
16889
+ if (options.planMode) {
16890
+ const refusal = (0, planMode_1.checkPlanMode)(name, input);
16891
+ if (refusal) {
16892
+ result = { error: refusal.message };
16893
+ options.onToolResult(name, result);
16894
+ return { block: { ...block, id }, result };
16895
+ }
16896
+ }
16470
16897
  const description = humanDescription(name, input);
16471
16898
  let permitted;
16472
16899
  let deniedReason = null;
@@ -16575,12 +17002,25 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
16575
17002
  messages.push(toolResultMessage);
16576
17003
  completedRounds++;
16577
17004
  options.onProgress?.(messages);
16578
- 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;
16579
17007
  consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
16580
17008
  if (consecutiveErrorRounds >= STALL_LIMIT) {
16581
17009
  stalledOut = true;
16582
17010
  break;
16583
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
+ }
16584
17024
  if (autoContinue && iteration + 1 >= budget && budget < hardCap) {
16585
17025
  budget = Math.min(budget + maxIterations, hardCap);
16586
17026
  options.onText(`
@@ -16590,11 +17030,15 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
16590
17030
  }
16591
17031
  if (!options.abortSignal?.aborted && !completedCleanly) {
16592
17032
  if (stalledOut) {
16593
- 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
+ ` : `
16594
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".
16595
17039
  `);
16596
17040
  } else if (iteration >= budget) {
16597
- options.onText(`
17041
+ (options.onNotice ?? options.onText)(`
16598
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.
16599
17043
  `);
16600
17044
  }
@@ -18482,6 +18926,8 @@ var require_dist2 = __commonJS({
18482
18926
  __exportStar(require_manager2(), exports);
18483
18927
  __exportStar(require_loader(), exports);
18484
18928
  __exportStar(require_agentTypes(), exports);
18929
+ __exportStar(require_planMode(), exports);
18930
+ __exportStar(require_agentRegistry(), exports);
18485
18931
  __exportStar(require_rules(), exports);
18486
18932
  __exportStar(require_destructive(), exports);
18487
18933
  __exportStar(require_plugins(), exports);
@@ -63558,7 +64004,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
63558
64004
  };
63559
64005
 
63560
64006
  // src/commands/chat.ts
63561
- var CLI_VERSION = "0.5.50";
64007
+ var CLI_VERSION = "0.5.52";
63562
64008
  var MODEL_LABELS = {
63563
64009
  turbo: "Nexrall Turbo",
63564
64010
  pro: "Nexrall Pro",
@@ -63829,6 +64275,12 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
63829
64275
  env: env3,
63830
64276
  nexrallMd,
63831
64277
  mode,
64278
+ // `/mode plan` previously only hinted to the backend — the footer said "plan
64279
+ // mode on" while every write tool stayed live, so the one mode whose entire
64280
+ // promise is "I will not touch anything" was the one not enforced. This
64281
+ // makes it real: the loop refuses mutating tools before the permission
64282
+ // prompt, and sub-agents inherit the lock.
64283
+ planMode: mode === "plan",
63832
64284
  effort,
63833
64285
  // The backend streams a cumulative output-token count (routes/code.js's
63834
64286
  // sendProgress, throttled to ≤5/s) covering thinking, visible text AND
@@ -63982,6 +64434,7 @@ async function runTurnHeadless(messages, modelAlias, workDir, _abortSignal, env3
63982
64434
  env: env3,
63983
64435
  nexrallMd,
63984
64436
  mode,
64437
+ planMode: mode === "plan",
63985
64438
  effort,
63986
64439
  abortSignal: _abortSignal,
63987
64440
  onText: (text) => {
@@ -65218,7 +65671,7 @@ function pluginSourceRemoveCommand(name, opts) {
65218
65671
 
65219
65672
  // src/index.ts
65220
65673
  var program2 = new Command();
65221
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.50").enablePositionalOptions();
65674
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.52").enablePositionalOptions();
65222
65675
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
65223
65676
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
65224
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.50",
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.25"
44
+ "@nexrall/code-core": "1.4.27"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",