nexrall-code 0.5.58 → 0.5.60

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 +241 -92
  2. package/package.json +11 -11
package/dist/index.js CHANGED
@@ -12663,6 +12663,34 @@ ${doc}` : sig || "No type information." };
12663
12663
  }
12664
12664
  });
12665
12665
 
12666
+ // ../core/dist/util/safeSlice.js
12667
+ var require_safeSlice = __commonJS({
12668
+ "../core/dist/util/safeSlice.js"(exports) {
12669
+ "use strict";
12670
+ Object.defineProperty(exports, "__esModule", { value: true });
12671
+ exports.sliceSafeEnd = sliceSafeEnd;
12672
+ exports.sliceSafeStart = sliceSafeStart;
12673
+ function sliceSafeEnd(s2, max) {
12674
+ if (s2.length <= max)
12675
+ return s2;
12676
+ let end = max;
12677
+ const code = s2.charCodeAt(end - 1);
12678
+ if (code >= 55296 && code <= 56319)
12679
+ end--;
12680
+ return s2.slice(0, end);
12681
+ }
12682
+ function sliceSafeStart(s2, from) {
12683
+ if (from <= 0)
12684
+ return s2;
12685
+ let start = from;
12686
+ const code = s2.charCodeAt(start);
12687
+ if (code >= 56320 && code <= 57343)
12688
+ start++;
12689
+ return s2.slice(start);
12690
+ }
12691
+ }
12692
+ });
12693
+
12666
12694
  // ../core/dist/tools/executor.js
12667
12695
  var require_executor = __commonJS({
12668
12696
  "../core/dist/tools/executor.js"(exports) {
@@ -12731,6 +12759,7 @@ var require_executor = __commonJS({
12731
12759
  var client_1 = require_client();
12732
12760
  var symbols_1 = require_symbols();
12733
12761
  var tsLangService_1 = require_tsLangService();
12762
+ var safeSlice_1 = require_safeSlice();
12734
12763
  var DEFAULT_TIMEOUT_MS = 6e4;
12735
12764
  var MAX_FETCH_BYTES = 200 * 1024;
12736
12765
  var MAX_READ_BYTES = 500 * 1024;
@@ -13024,7 +13053,7 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
13024
13053
  }
13025
13054
  let output = lines.join("\n");
13026
13055
  if (output.length > MAX_OUTPUT_CHARS) {
13027
- output = output.slice(0, MAX_OUTPUT_CHARS) + `
13056
+ output = (0, safeSlice_1.sliceSafeEnd)(output, MAX_OUTPUT_CHARS) + `
13028
13057
  [Truncated \u2014 ${entries.length} total entries]`;
13029
13058
  }
13030
13059
  return { output };
@@ -13496,7 +13525,7 @@ Update these call-sites (or restore the symbol), then run a build/typecheck to c
13496
13525
  return { output: "No matching files found." };
13497
13526
  let out = matches.join("\n");
13498
13527
  if (out.length > MAX_OUTPUT_CHARS)
13499
- out = out.slice(0, MAX_OUTPUT_CHARS) + "\n[Truncated]";
13528
+ out = (0, safeSlice_1.sliceSafeEnd)(out, MAX_OUTPUT_CHARS) + "\n[Truncated]";
13500
13529
  return { output: out };
13501
13530
  } else {
13502
13531
  const SEARCH_MAX_BUFFER = 64 * 1024 * 1024;
@@ -13566,7 +13595,7 @@ ${globalCapMatches(output)}` : "";
13566
13595
  [Showing first ${GLOBAL_MATCH_CAP} of ${lines.length} matched lines \u2014 narrow the pattern/path for the rest.]`;
13567
13596
  }
13568
13597
  if (capped.length > MAX_OUTPUT_CHARS) {
13569
- capped = capped.slice(0, MAX_OUTPUT_CHARS);
13598
+ capped = (0, safeSlice_1.sliceSafeEnd)(capped, MAX_OUTPUT_CHARS);
13570
13599
  note = `
13571
13600
  [Output truncated at ${MAX_OUTPUT_CHARS / 1024}KB.]`;
13572
13601
  }
@@ -14102,7 +14131,7 @@ ${d}`).join("\n") + xfile + sec
14102
14131
  return { output: `No files matched pattern: ${pattern}` };
14103
14132
  let out = matches.join("\n");
14104
14133
  if (out.length > MAX_OUTPUT_CHARS)
14105
- out = out.slice(0, MAX_OUTPUT_CHARS) + "\n[Truncated]";
14134
+ out = (0, safeSlice_1.sliceSafeEnd)(out, MAX_OUTPUT_CHARS) + "\n[Truncated]";
14106
14135
  return { output: `${matches.length} file(s) matched "${pattern}":
14107
14136
  ${out}` };
14108
14137
  } catch (err) {
@@ -14158,7 +14187,7 @@ ${lines.join("\n")}`;
14158
14187
  switch (out.output_type) {
14159
14188
  case "stream": {
14160
14189
  const txt = nbJoin(out.text ?? "");
14161
- lines.push(`[${out.name === "stderr" ? "stderr" : "stdout"}] ${txt.slice(0, 3e3)}${txt.length > 3e3 ? "\n[truncated]" : ""}`);
14190
+ lines.push(`[${out.name === "stderr" ? "stderr" : "stdout"}] ${(0, safeSlice_1.sliceSafeEnd)(txt, 3e3)}${txt.length > 3e3 ? "\n[truncated]" : ""}`);
14162
14191
  break;
14163
14192
  }
14164
14193
  case "execute_result":
@@ -14168,9 +14197,9 @@ ${lines.join("\n")}`;
14168
14197
  const html = d["text/html"] ? nbJoin(d["text/html"]) : null;
14169
14198
  const hasImg = "image/png" in d || "image/jpeg" in d || "image/svg+xml" in d;
14170
14199
  if (plain)
14171
- lines.push(plain.slice(0, 2e3) + (plain.length > 2e3 ? "\n[truncated]" : ""));
14200
+ lines.push((0, safeSlice_1.sliceSafeEnd)(plain, 2e3) + (plain.length > 2e3 ? "\n[truncated]" : ""));
14172
14201
  else if (html)
14173
- lines.push(stripHtml(html).slice(0, 1e3));
14202
+ lines.push((0, safeSlice_1.sliceSafeEnd)(stripHtml(html), 1e3));
14174
14203
  if (hasImg)
14175
14204
  lines.push("[image output \u2014 not shown in text mode]");
14176
14205
  break;
@@ -14179,7 +14208,7 @@ ${lines.join("\n")}`;
14179
14208
  lines.push(`[error] ${out.ename ?? "Error"}: ${out.evalue ?? ""}`);
14180
14209
  if (out.traceback) {
14181
14210
  const clean = out.traceback.map((l) => l.replace(/\x1b\[[0-9;]*[mGKH]/g, "")).join("\n");
14182
- lines.push(clean.slice(0, 1500) + (clean.length > 1500 ? "\n[truncated]" : ""));
14211
+ lines.push((0, safeSlice_1.sliceSafeEnd)(clean, 1500) + (clean.length > 1500 ? "\n[truncated]" : ""));
14183
14212
  }
14184
14213
  break;
14185
14214
  }
@@ -14222,7 +14251,7 @@ ${lines.join("\n")}`;
14222
14251
  });
14223
14252
  let output = lines.join("\n");
14224
14253
  if (output.length > MAX_OUTPUT_CHARS) {
14225
- output = output.slice(0, MAX_OUTPUT_CHARS) + "\n\n[Notebook output truncated at 100KB]";
14254
+ output = (0, safeSlice_1.sliceSafeEnd)(output, MAX_OUTPUT_CHARS) + "\n\n[Notebook output truncated at 100KB]";
14226
14255
  }
14227
14256
  return { output };
14228
14257
  } catch (err) {
@@ -14733,9 +14762,17 @@ var require_agentTypes = __commonJS({
14733
14762
  //
14734
14763
  // `tools` is deliberately UNDEFINED, which means "no allowlist" — full access,
14735
14764
  // inheriting whatever the session permits. That is the same power an unnamed
14736
- // sub-task always had; naming it changes only who can see and deny it. The safety
14737
- // properties elsewhere still apply: plan mode is inherited, the user's permission
14738
- // gate still runs on every call, and it cannot spawn further sub-agents.
14765
+ // sub-task always had; naming it changes only who can see and deny it.
14766
+ //
14767
+ // It also makes this the ONLY builtin that can nest, since every specialist omits
14768
+ // `task` on purpose. So the tree this enables is up to `maxSubagentDepth` generations
14769
+ // of unrestricted agents — accepted deliberately, because the alternative (a catch-all
14770
+ // that cannot delegate) removes the one agent suited to orchestration.
14771
+ //
14772
+ // The safety properties that bound it: plan mode is inherited, the user's permission
14773
+ // gate runs on every call, a child's allowlist is INTERSECTED with its parent's so a
14774
+ // restricted ancestor cannot be escaped through this agent, `test_files_only` is
14775
+ // likewise sticky, and both the depth ceiling and the session budget still apply.
14739
14776
  name: "general-purpose",
14740
14777
  description: "General-purpose worker for a multi-step task that needs BOTH exploration and changes (edit files, run commands) and that no specialist above fits. Inherits the session model and full tool access, so prefer a narrower agent when one matches.",
14741
14778
  prompt: "You are a general-purpose engineering sub-agent. Work the task end to end: explore what you need, make the changes, and verify them with the project's own build/test commands.\n\nRules:\n- Mirror existing conventions; make the smallest correct change.\n- Verify before you claim success. If you could not verify, say so explicitly.\n- Your FINAL MESSAGE is the only thing that reaches the main agent: state what you changed (with file paths), what you ran and its outcome, and anything you deliberately left undone.",
@@ -15061,11 +15098,11 @@ var require_agentTypes = __commonJS({
15061
15098
  message: `lists unknown tool name(s): ${unknown.join(", ")}. An allowlist only grants, so these silently do nothing and the agent cannot use them. Tool names are lower_snake_case (read_file, search_files, glob, bash).`
15062
15099
  });
15063
15100
  }
15064
- if ((tools ?? []).includes("task")) {
15101
+ if ((tools ?? []).length === 1 && tools?.[0] === "task") {
15065
15102
  warnings.push({
15066
15103
  file: full,
15067
15104
  agent: name,
15068
- message: "lists `task`, which cannot be granted: sub-agents are never sent that tool because they cannot spawn further sub-agents. Remove it \u2014 delegation stays with the main agent."
15105
+ message: "lists `task` as its ONLY tool, so it can delegate but cannot read, write or run anything itself \u2014 it has no way to do or verify work. Add the tools it needs, or drop the `tools:` line to inherit its parent's."
15069
15106
  });
15070
15107
  }
15071
15108
  const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
@@ -15704,10 +15741,12 @@ var require_agentRegistry = __commonJS({
15704
15741
  Object.defineProperty(exports, "__esModule", { value: true });
15705
15742
  exports._limits = void 0;
15706
15743
  exports.rememberAgent = rememberAgent;
15744
+ exports.canResume = canResume;
15707
15745
  exports.updateAgent = updateAgent;
15708
15746
  exports.getAgent = getAgent;
15709
15747
  exports.listAgents = listAgents;
15710
15748
  exports._resetAgentRegistry = _resetAgentRegistry;
15749
+ var node_crypto_1 = __require("node:crypto");
15711
15750
  var MAX_AGENTS = 20;
15712
15751
  var MAX_TOTAL_BYTES = 8 * 1024 * 1024;
15713
15752
  var _agents = /* @__PURE__ */ new Map();
@@ -15740,12 +15779,13 @@ var require_agentRegistry = __commonJS({
15740
15779
  return MAX_TOTAL_BYTES;
15741
15780
  }
15742
15781
  }
15743
- function rememberAgent(agentName, description, messages) {
15744
- const id = `agent_${++_counter}`;
15782
+ function rememberAgent(agentName, description, messages, ownerScope = "root") {
15783
+ const id = `agent_${++_counter}_${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
15745
15784
  const now = Date.now();
15746
15785
  _agents.set(id, {
15747
15786
  id,
15748
15787
  agentName,
15788
+ ownerScope,
15749
15789
  description,
15750
15790
  messages,
15751
15791
  createdAt: now,
@@ -15756,10 +15796,15 @@ var require_agentRegistry = __commonJS({
15756
15796
  evictUntilWithinLimits();
15757
15797
  return id;
15758
15798
  }
15759
- function updateAgent(id, messages) {
15799
+ function canResume(stored, scope) {
15800
+ return stored.ownerScope === scope || scope === "root";
15801
+ }
15802
+ function updateAgent(id, messages, scope = "root") {
15760
15803
  const existing = _agents.get(id);
15761
15804
  if (!existing)
15762
15805
  return;
15806
+ if (!canResume(existing, scope))
15807
+ return;
15763
15808
  existing.messages = messages;
15764
15809
  existing.updatedAt = Date.now();
15765
15810
  existing.seq = ++_seq;
@@ -15961,15 +16006,21 @@ var require_loop = __commonJS({
15961
16006
  };
15962
16007
  }();
15963
16008
  Object.defineProperty(exports, "__esModule", { value: true });
15964
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.NO_NESTED_SUBAGENTS_MSG = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports.bashNeedsRepoLock = void 0;
16009
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports.bashNeedsRepoLock = void 0;
15965
16010
  exports.errorRoundSignature = errorRoundSignature;
15966
16011
  exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
15967
16012
  exports.stopReasonNotice = stopReasonNotice;
15968
16013
  exports.resolveMaxIterations = resolveMaxIterations;
15969
16014
  exports.lockPathsFor = lockPathsFor;
15970
16015
  exports.resolveMaxConcurrentSubtasks = resolveMaxConcurrentSubtasks;
16016
+ exports.resolveMaxSubagentsPerSession = resolveMaxSubagentsPerSession;
15971
16017
  exports.createLimiter = createLimiter;
15972
16018
  exports._resetSubTaskLimiter = _resetSubTaskLimiter;
16019
+ exports.resetSessionSubAgentBudget = resetSessionSubAgentBudget2;
16020
+ exports._sessionSubAgentCount = _sessionSubAgentCount;
16021
+ exports.resolveMaxSubagentDepth = resolveMaxSubagentDepth;
16022
+ exports.noSpawnReason = noSpawnReason;
16023
+ exports.intersectAllowlists = intersectAllowlists;
15973
16024
  exports.canSpawnSubAgents = canSpawnSubAgents;
15974
16025
  exports.resolveSubtaskTimeoutMs = resolveSubtaskTimeoutMs;
15975
16026
  exports.extractSubTaskText = extractSubTaskText;
@@ -16008,6 +16059,7 @@ var require_loop = __commonJS({
16008
16059
  var flaky_1 = require_flaky();
16009
16060
  var claimEvidence_1 = require_claimEvidence();
16010
16061
  var memory_1 = require_memory();
16062
+ var safeSlice_1 = require_safeSlice();
16011
16063
  var fs9 = __importStar(__require("fs"));
16012
16064
  var path6 = __importStar(__require("path"));
16013
16065
  var child_process_1 = __require("child_process");
@@ -16242,7 +16294,7 @@ ${ctx.repeatError}
16242
16294
  }
16243
16295
  var DEFAULT_MAX_CONCURRENT_SUBTASKS = 4;
16244
16296
  function resolveMaxConcurrentSubtasks(settingsRaw = {}) {
16245
- const clamp = (n) => Math.min(Math.floor(n), 16);
16297
+ const clamp = (n) => Math.max(1, Math.min(Math.floor(n), 16));
16246
16298
  const fromEnv = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
16247
16299
  if (Number.isFinite(fromEnv) && fromEnv > 0)
16248
16300
  return clamp(fromEnv);
@@ -16251,6 +16303,17 @@ ${ctx.repeatError}
16251
16303
  return clamp(fromSettings);
16252
16304
  return DEFAULT_MAX_CONCURRENT_SUBTASKS;
16253
16305
  }
16306
+ var DEFAULT_MAX_SUBAGENTS_PER_SESSION = 100;
16307
+ function resolveMaxSubagentsPerSession(settingsRaw = {}) {
16308
+ const floor = (n) => Math.max(1, Math.min(Math.floor(n), 1e4));
16309
+ const fromEnv = Number(process.env.NEXRALL_MAX_SUBAGENTS_PER_SESSION);
16310
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
16311
+ return floor(fromEnv);
16312
+ const fromSettings = Number(settingsRaw.maxSubagentsPerSession);
16313
+ if (Number.isFinite(fromSettings) && fromSettings > 0)
16314
+ return floor(fromSettings);
16315
+ return DEFAULT_MAX_SUBAGENTS_PER_SESSION;
16316
+ }
16254
16317
  function createLimiter(max) {
16255
16318
  let active = 0;
16256
16319
  const queue = [];
@@ -16269,19 +16332,61 @@ ${ctx.repeatError}
16269
16332
  }
16270
16333
  };
16271
16334
  }
16272
- var _subTaskLimitInstance = null;
16335
+ var _subTaskLimiters = /* @__PURE__ */ new Map();
16336
+ var _subTaskLimitMaxByDepth = /* @__PURE__ */ new Map();
16273
16337
  var _subTaskLimitMax = 0;
16274
- var _inFlightSubTasks = 0;
16275
- function subTaskLimiter(workDir) {
16276
- if (!_subTaskLimitInstance) {
16338
+ var _inFlightByDepth = /* @__PURE__ */ new Map();
16339
+ function subTaskLimiter(depth, workDir) {
16340
+ if (!_subTaskLimitMax) {
16277
16341
  _subTaskLimitMax = resolveMaxConcurrentSubtasks(workDir ? (0, rules_1.loadSettings)(workDir).raw : {});
16278
- _subTaskLimitInstance = createLimiter(_subTaskLimitMax);
16279
16342
  }
16280
- return { run: _subTaskLimitInstance, max: _subTaskLimitMax };
16343
+ let run = _subTaskLimiters.get(depth);
16344
+ let max = _subTaskLimitMaxByDepth.get(depth) ?? 0;
16345
+ if (!run) {
16346
+ max = Math.max(1, Math.ceil(_subTaskLimitMax / 2 ** Math.max(0, depth - 1)));
16347
+ run = createLimiter(max);
16348
+ _subTaskLimiters.set(depth, run);
16349
+ _subTaskLimitMaxByDepth.set(depth, max);
16350
+ }
16351
+ return { run, max };
16281
16352
  }
16282
16353
  function _resetSubTaskLimiter() {
16283
- _subTaskLimitInstance = null;
16354
+ _subTaskLimiters.clear();
16355
+ _subTaskLimitMaxByDepth.clear();
16356
+ _inFlightByDepth.clear();
16284
16357
  _subTaskLimitMax = 0;
16358
+ _subAgentsThisSession = 0;
16359
+ _sessionCapMax = 0;
16360
+ _sessionCapNotified = false;
16361
+ }
16362
+ var _subAgentsThisSession = 0;
16363
+ var _sessionCapMax = 0;
16364
+ var _sessionCapNotified = false;
16365
+ function resetSessionSubAgentBudget2() {
16366
+ _subAgentsThisSession = 0;
16367
+ _sessionCapMax = 0;
16368
+ _sessionCapNotified = false;
16369
+ }
16370
+ function claimSessionSubAgentSlot(workDir, notify) {
16371
+ if (!_sessionCapMax) {
16372
+ _sessionCapMax = resolveMaxSubagentsPerSession(workDir ? (0, rules_1.loadSettings)(workDir).raw : {});
16373
+ }
16374
+ if (_subAgentsThisSession >= _sessionCapMax) {
16375
+ if (!_sessionCapNotified) {
16376
+ _sessionCapNotified = true;
16377
+ notify?.(`\u26A0\uFE0F Sub-agent budget reached (${_sessionCapMax} this session) \u2014 further delegation is blocked and the agent will continue without it. Raise "maxSubagentsPerSession" in .nexrall/settings.json if this was legitimate work.`);
16378
+ }
16379
+ return `Sub-agent budget for this session is exhausted (${_sessionCapMax} started). This is a runaway-delegation guard, not a per-task limit: do the remaining work directly, and say in your final message that you hit the delegation cap.`;
16380
+ }
16381
+ _subAgentsThisSession++;
16382
+ return null;
16383
+ }
16384
+ function refundSessionSubAgentSlot() {
16385
+ if (_subAgentsThisSession > 0)
16386
+ _subAgentsThisSession--;
16387
+ }
16388
+ function _sessionSubAgentCount() {
16389
+ return _subAgentsThisSession;
16285
16390
  }
16286
16391
  function humanDescription(name, input) {
16287
16392
  switch (name) {
@@ -16364,10 +16469,33 @@ ${ctx.repeatError}
16364
16469
  return `Use tool: ${name}`;
16365
16470
  }
16366
16471
  }
16367
- var MAX_TASK_DEPTH = 1;
16368
- exports.NO_NESTED_SUBAGENTS_MSG = "Sub-agents cannot spawn further sub-agents \u2014 this is a structural limit, not a permission, so no other agent type and no approval can lift it. Do this work directly with the tools you have, or report back so the main agent can delegate it.";
16369
- function canSpawnSubAgents(depth, allowedTools) {
16370
- if (depth >= MAX_TASK_DEPTH)
16472
+ var DEFAULT_MAX_TASK_DEPTH = 3;
16473
+ var HARD_MAX_TASK_DEPTH = 5;
16474
+ function resolveMaxSubagentDepth(settingsRaw = {}) {
16475
+ const clamp = (n) => Math.max(1, Math.min(Math.floor(n), HARD_MAX_TASK_DEPTH));
16476
+ const fromEnv = Number(process.env.NEXRALL_MAX_SUBAGENT_DEPTH);
16477
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
16478
+ return clamp(fromEnv);
16479
+ const fromSettings = Number(settingsRaw.maxSubagentDepth);
16480
+ if (Number.isFinite(fromSettings) && fromSettings > 0)
16481
+ return clamp(fromSettings);
16482
+ return DEFAULT_MAX_TASK_DEPTH;
16483
+ }
16484
+ function noSpawnReason(kind, limit) {
16485
+ if (kind === "denied") {
16486
+ return "This sub-agent's definition does not grant `task`, so it may not delegate. That is a deliberate restriction on this agent type \u2014 do not ask for approval and do not look for a way around it. Do the work with the tools you have, or report back what is missing.";
16487
+ }
16488
+ return `Maximum sub-agent nesting depth (${limit ?? DEFAULT_MAX_TASK_DEPTH}) reached, so this run is a leaf and cannot delegate further. Depth is a budget, not a bug: finish this work yourself, or report back so a shallower frame can decide. (The ceiling is "maxSubagentDepth" in .nexrall/settings.json, but raising it mid-task will not help you \u2014 it applies from the next session.)`;
16489
+ }
16490
+ function intersectAllowlists(own, parent) {
16491
+ if (own && parent)
16492
+ return new Set([...own].filter((t2) => parent.has(t2)));
16493
+ if (own)
16494
+ return own;
16495
+ return parent ? new Set(parent) : null;
16496
+ }
16497
+ function canSpawnSubAgents(depth, allowedTools, limit = DEFAULT_MAX_TASK_DEPTH) {
16498
+ if (depth >= limit)
16371
16499
  return false;
16372
16500
  return allowedTools ? allowedTools.has("task") : true;
16373
16501
  }
@@ -16391,24 +16519,6 @@ ${ctx.repeatError}
16391
16519
  }
16392
16520
  };
16393
16521
  exports.ToolNotAllowedError = ToolNotAllowedError;
16394
- function sliceSafeEnd(s2, max) {
16395
- if (s2.length <= max)
16396
- return s2;
16397
- let end = max;
16398
- const code = s2.charCodeAt(end - 1);
16399
- if (code >= 55296 && code <= 56319)
16400
- end--;
16401
- return s2.slice(0, end);
16402
- }
16403
- function sliceSafeStart(s2, from) {
16404
- if (from <= 0)
16405
- return s2;
16406
- let start = from;
16407
- const code = s2.charCodeAt(start);
16408
- if (code >= 56320 && code <= 57343)
16409
- start++;
16410
- return s2.slice(start);
16411
- }
16412
16522
  function extractSubTaskText(messages, preferLast = true) {
16413
16523
  const assistants = messages.filter((m2) => m2.role === "assistant");
16414
16524
  const textOf = (m2) => (m2?.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim();
@@ -16419,8 +16529,8 @@ ${ctx.repeatError}
16419
16529
  function capSubTaskText(text, max = SUBTASK_MAX) {
16420
16530
  if (text.length <= max)
16421
16531
  return text;
16422
- const head = sliceSafeEnd(text, Math.floor(max * 0.6));
16423
- const tail = sliceSafeStart(text, text.length - Math.floor(max * 0.4));
16532
+ const head = (0, safeSlice_1.sliceSafeEnd)(text, Math.floor(max * 0.6));
16533
+ const tail = (0, safeSlice_1.sliceSafeStart)(text, text.length - Math.floor(max * 0.4));
16424
16534
  return `${head}
16425
16535
 
16426
16536
  [\u2026 sub-task output truncated (${text.length} chars) \u2014 kept the beginning and end \u2026]
@@ -16476,7 +16586,7 @@ ${recentFindings}` : header;
16476
16586
  if (!text)
16477
16587
  continue;
16478
16588
  const name = nameById.get(String(b.tool_use_id ?? "")) ?? "tool";
16479
- const body = text.length > maxChars ? `${sliceSafeEnd(text, maxChars)}
16589
+ const body = text.length > maxChars ? `${(0, safeSlice_1.sliceSafeEnd)(text, maxChars)}
16480
16590
  \u2026 [truncated]` : text;
16481
16591
  out.push(`\u2022 ${name}:
16482
16592
  ${body}`);
@@ -16493,29 +16603,35 @@ ${body}`);
16493
16603
  }
16494
16604
  return "";
16495
16605
  }
16496
- async function runSubTask(input, options, agentTypes) {
16606
+ async function runSubTask(input, options, agentTypes, started) {
16497
16607
  const prompt2 = typeof input.prompt === "string" ? input.prompt.trim() : "";
16498
16608
  if (!prompt2)
16499
16609
  return { error: "task tool requires a non-empty prompt" };
16610
+ const agentScope = options._agentScope ?? "root";
16500
16611
  const depth = options._depth ?? 0;
16501
- if (depth >= MAX_TASK_DEPTH) {
16502
- return { error: exports.NO_NESTED_SUBAGENTS_MSG };
16612
+ const depthLimit = resolveMaxSubagentDepth(options.workDir ? (0, rules_1.loadSettings)(options.workDir).raw : {});
16613
+ if (depth >= depthLimit) {
16614
+ return { error: noSpawnReason("depth", depthLimit) };
16503
16615
  }
16504
16616
  const resumeId = typeof input.resume_agent_id === "string" ? input.resume_agent_id.trim() : "";
16505
16617
  const resumed = resumeId ? (0, agentRegistry_1.getAgent)(resumeId) : void 0;
16618
+ if (resumed && !(0, agentRegistry_1.canResume)(resumed, agentScope)) {
16619
+ return {
16620
+ error: `No resumable sub-agent with id "${resumeId}" is available to this run. Start a fresh sub-task with a self-contained prompt instead.`
16621
+ };
16622
+ }
16506
16623
  if (resumeId && !resumed) {
16507
16624
  return {
16508
16625
  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.`
16509
16626
  };
16510
16627
  }
16511
16628
  const requestedType = resumed ? resumed.agentName ?? "" : typeof input.subagent_type === "string" ? input.subagent_type : "";
16512
- if (requestedType) {
16513
- const decision = (0, rules_1.evaluatePermission)((0, rules_1.loadSettings)(options.workDir).permissions, "task", { subagent_type: requestedType }, options.workDir);
16514
- if (decision === "deny") {
16515
- return {
16516
- 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.`
16517
- };
16518
- }
16629
+ const denyKey = requestedType || "general-purpose";
16630
+ const decision = (0, rules_1.evaluatePermission)((0, rules_1.loadSettings)(options.workDir).permissions, "task", { subagent_type: denyKey }, options.workDir);
16631
+ if (decision === "deny") {
16632
+ return {
16633
+ error: `The sub-agent "${denyKey}" 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.`
16634
+ };
16519
16635
  }
16520
16636
  let agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
16521
16637
  let knownTypes = agentTypes;
@@ -16543,21 +16659,31 @@ ${agentMemoryNotes}` : "") + (inheritedMd ? `
16543
16659
  ---
16544
16660
 
16545
16661
  ${inheritedMd}` : "") : options.nexrallMd;
16546
- const allowed = agent?.tools ? new Set(agent.tools) : null;
16662
+ const testFilesOnly = !!agent?.testFilesOnly || !!options._testFilesOnly;
16663
+ const allowed = intersectAllowlists(agent?.tools ? new Set(agent.tools) : null, options._allowedTools);
16547
16664
  if (allowed && memoryScope)
16548
16665
  allowed.add(exports.AGENT_MEMORY_TOOL);
16549
16666
  const gatedPermission = async (req) => {
16550
16667
  if (req.tool === exports.AGENT_MEMORY_TOOL && !(agent && memoryScope)) {
16551
16668
  throw new ToolNotAllowedError(`\`${exports.AGENT_MEMORY_TOOL}\` is only available to a sub-agent whose definition declares a \`memory:\` scope (project, user or local). Report anything worth remembering in your final message instead \u2014 the main agent decides what to persist.`);
16552
16669
  }
16553
- if (req.tool === "task") {
16554
- throw new ToolNotAllowedError(exports.NO_NESTED_SUBAGENTS_MSG);
16670
+ if (req.tool === "task" && !canSpawnSubAgents(depth + 1, allowed ?? void 0, depthLimit)) {
16671
+ throw new ToolNotAllowedError(allowed && !allowed.has("task") ? noSpawnReason("denied") : noSpawnReason("depth", depthLimit));
16555
16672
  }
16556
16673
  if (allowed && !allowed.has(req.tool)) {
16557
- throw new ToolNotAllowedError(`The "${agent.name}" sub-agent is not allowed to use \`${req.tool}\` \u2014 it is not in that agent's tool allowlist. This is a restriction of the agent definition, NOT a user decision: do not ask for approval, use one of the tools you do have, or report back that the task needs a different agent.`);
16674
+ throw new ToolNotAllowedError(
16675
+ // `agent?.name`, NOT `agent!.name`. The non-null assertion held only while `allowed`
16676
+ // was derived solely from `agent?.tools` (non-null allowlist ⇒ named agent). The
16677
+ // parent-intersection broke that invariant: a RESTRICTED parent dispatching `task`
16678
+ // with no subagent_type yields a non-null inherited allowlist with `agent`
16679
+ // undefined, and this line then threw a TypeError instead of ToolNotAllowedError —
16680
+ // which the dispatch site does not recognise, so it laundered the refusal into the
16681
+ // generic "Permission denied by user" this very message exists to avoid.
16682
+ `The "${agent?.name ?? "general-purpose"}" sub-agent is not allowed to use \`${req.tool}\` \u2014 it is not in that agent's tool allowlist. This is a restriction of the agent definition, NOT a user decision: do not ask for approval, use one of the tools you do have, or report back that the task needs a different agent.`
16683
+ );
16558
16684
  }
16559
- if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input)) {
16560
- throw new ToolNotAllowedError(`The "${agent.name}" sub-agent may only write to TEST files, so \`${req.tool}\` was refused for this path. Do not try to work around it: if production code must change, say so in your report instead.`);
16685
+ if (testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input)) {
16686
+ throw new ToolNotAllowedError(`The "${agent?.name ?? "general-purpose"}" sub-agent may only write to TEST files, so \`${req.tool}\` was refused for this path. Do not try to work around it: if production code must change, say so in your report instead.`);
16561
16687
  }
16562
16688
  return options.requestPermission(req);
16563
16689
  };
@@ -16580,6 +16706,8 @@ ${inheritedMd}` : "") : options.nexrallMd;
16580
16706
  subAbort.aborted = true;
16581
16707
  }, 250);
16582
16708
  try {
16709
+ if (started)
16710
+ started.value = true;
16583
16711
  const result = await runAgentLoop2(subMessages, {
16584
16712
  ...options,
16585
16713
  _depth: depth + 1,
@@ -16597,15 +16725,21 @@ ${inheritedMd}` : "") : options.nexrallMd;
16597
16725
  // `tools:` line inherited the parent's allowlist, making the prompt's capability
16598
16726
  // claim disagree with its real one.
16599
16727
  //
16600
- // MAX_TASK_DEPTH === 1 means no nested spawn can reach this today, so it is
16601
- // latent rather than live but the limiter comment below explicitly contemplates
16602
- // raising that depth, and this is exactly the kind of leak that would come back
16603
- // as a security bug rather than a visible error. Explicit undefined makes the
16604
- // child's identity independent of the parent's by construction.
16728
+ // This is now LIVE, not latent: nesting is enabled by default, so a grandchild really
16729
+ // can be spawned by an agent that has a memory binding. The explicit `undefined` is
16730
+ // what stops it inheriting that binding and appending to its grandparent's private
16731
+ // notes a silent cross-agent write rather than a visible error. Note the allowlist
16732
+ // takes the opposite direction on purpose (inherited, because it RESTRICTS); identity
16733
+ // must not be inherited, capability must.
16605
16734
  _agentMemory: agent && memoryScope ? { agentName: agent.name, scope: memoryScope } : void 0,
16606
16735
  // The same set `gatedPermission` enforces above, so prompt and permission agree
16607
16736
  // by construction instead of by two people remembering to update both.
16608
16737
  _allowedTools: allowed ?? void 0,
16738
+ // Propagated so a grandchild inherits it too — see _testFilesOnly. Assigned
16739
+ // unconditionally (not by conditional spread) for the same reason as _agentMemory
16740
+ // above: a conditional spread leaves the parent's value in place instead of clearing
16741
+ // it, and here that direction is at least safe, whereas forgetting to propagate is not.
16742
+ _testFilesOnly: testFilesOnly,
16609
16743
  editorContext: null,
16610
16744
  // fresh isolated context for sub-agent
16611
16745
  model: agent?.model ?? options.model,
@@ -16671,7 +16805,7 @@ ${inheritedMd}` : "") : options.nexrallMd;
16671
16805
  const mins = Math.round(subtaskTimeoutMs / 6e4);
16672
16806
  const partial = capSubTaskText(extractSubTaskText(result, false));
16673
16807
  const progress = summariseSubTaskProgress(result);
16674
- const partialId = (0, agentRegistry_1.rememberAgent)(agent?.name ?? null, typeof input.description === "string" && input.description.trim() || prompt2.slice(0, 80), result);
16808
+ const partialId = (0, agentRegistry_1.rememberAgent)(agent?.name ?? null, typeof input.description === "string" && input.description.trim() || prompt2.slice(0, 80), result, agentScope);
16675
16809
  const sections = [
16676
16810
  `Sub-task STOPPED after ${mins} minutes with NO PROGRESS (it was not making tool calls or producing output) \u2014 treat everything below as PARTIAL, unverified work, not a finished answer.`,
16677
16811
  progress,
@@ -16683,7 +16817,7 @@ ${partial}` : "",
16683
16817
  return { error: sections.join("\n\n") };
16684
16818
  }
16685
16819
  const text = capSubTaskText(extractSubTaskText(result, true));
16686
- 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);
16820
+ const agentId = resumed ? ((0, agentRegistry_1.updateAgent)(resumed.id, result, agentScope), resumed.id) : (0, agentRegistry_1.rememberAgent)(agent?.name ?? null, typeof input.description === "string" && input.description.trim() || prompt2.slice(0, 80), result, agentScope);
16687
16821
  const body = text || "(sub-task completed with no text output)";
16688
16822
  return {
16689
16823
  output: `${body}
@@ -16788,11 +16922,11 @@ ${partial}` : "",
16788
16922
  for (const m2 of messages) {
16789
16923
  for (const b of m2.content) {
16790
16924
  if (b.type === "text" && b.text) {
16791
- parts.push(`${m2.role.toUpperCase()}: ${b.text.slice(0, 2e3)}`);
16925
+ parts.push(`${m2.role.toUpperCase()}: ${(0, safeSlice_1.sliceSafeEnd)(b.text, 2e3)}`);
16792
16926
  } else if (b.type === "tool_use") {
16793
- parts.push(`${m2.role.toUpperCase()} [tool: ${b.name}]: ${JSON.stringify(b.input ?? {}).slice(0, 400)}`);
16927
+ parts.push(`${m2.role.toUpperCase()} [tool: ${b.name}]: ${(0, safeSlice_1.sliceSafeEnd)(JSON.stringify(b.input ?? {}), 400)}`);
16794
16928
  } else if (b.type === "tool_result") {
16795
- parts.push(`TOOL RESULT: ${String(b.content ?? "").slice(0, 600)}`);
16929
+ parts.push(`TOOL RESULT: ${(0, safeSlice_1.sliceSafeEnd)(String(b.content ?? ""), 600)}`);
16796
16930
  }
16797
16931
  }
16798
16932
  }
@@ -16801,8 +16935,8 @@ ${partial}` : "",
16801
16935
  return full;
16802
16936
  const headBudget = Math.floor(MAX_TRANSCRIPT_CHARS * 0.4);
16803
16937
  const tailBudget = MAX_TRANSCRIPT_CHARS - headBudget;
16804
- const head = full.slice(0, headBudget);
16805
- const tail = full.slice(full.length - tailBudget);
16938
+ const head = (0, safeSlice_1.sliceSafeEnd)(full, headBudget);
16939
+ const tail = (0, safeSlice_1.sliceSafeStart)(full, full.length - tailBudget);
16806
16940
  const dropped = full.length - head.length - tail.length;
16807
16941
  return `${head}
16808
16942
 
@@ -17087,7 +17221,8 @@ Continue the work from here.` }] });
17087
17221
  const agentScope = options._agentScope ?? "root";
17088
17222
  const settings = (0, rules_1.loadSettings)(options.workDir);
17089
17223
  const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir).filter((t2) => (0, rules_1.evaluatePermission)(settings.permissions, "task", { subagent_type: t2.name }, options.workDir) !== "deny");
17090
- const maySpawn = canSpawnSubAgents(depth, options._allowedTools);
17224
+ const depthLimit = resolveMaxSubagentDepth(settings.raw);
17225
+ const maySpawn = canSpawnSubAgents(depth, options._allowedTools, depthLimit);
17091
17226
  const agentsCatalogue = maySpawn ? (0, agentTypes_1.summariseAgents)(agentTypes) : "";
17092
17227
  const skillsCatalogue = (0, skills_1.summariseSkills)((0, skills_1.loadSkills)(options.workDir));
17093
17228
  const planAwareNexrallMd = options.planMode ? planMode_1.PLAN_MODE_INSTRUCTIONS + (options.nexrallMd ? `
@@ -17384,19 +17519,29 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
17384
17519
  if (!permitted) {
17385
17520
  result = { error: deniedReason ?? "Permission denied by user" };
17386
17521
  } else if (name === "task") {
17387
- if (depth === 0) {
17388
- const { run: limitRun, max: limitMax } = subTaskLimiter(options.workDir);
17389
- if (_inFlightSubTasks >= limitMax) {
17390
- (options.onNotice ?? options.onText)(`\u23F3 Queued: ${limitMax} sub-agents are already running, so this one starts when a slot frees up (raise "maxConcurrentSubtasks" in .nexrall/settings.json to widen it).`);
17522
+ const overBudget = claimSessionSubAgentSlot(options.workDir, options.onNotice ?? options.onText);
17523
+ if (overBudget) {
17524
+ result = { error: overBudget };
17525
+ } else {
17526
+ const childDepth = depth + 1;
17527
+ const { run: limitRun, max: limitMax } = subTaskLimiter(childDepth, options.workDir);
17528
+ const inFlight = _inFlightByDepth.get(childDepth) ?? 0;
17529
+ if (inFlight >= limitMax) {
17530
+ (options.onNotice ?? options.onText)(`\u23F3 Queued: ${limitMax} sub-agent(s) already running at this level, so this one starts when a slot frees up (raise "maxConcurrentSubtasks" in .nexrall/settings.json to widen it).`);
17391
17531
  }
17392
- _inFlightSubTasks++;
17532
+ _inFlightByDepth.set(childDepth, inFlight + 1);
17533
+ const started = { value: false };
17393
17534
  try {
17394
- result = await limitRun(() => runSubTask(input, options, agentTypes));
17535
+ result = await limitRun(() => runSubTask(input, options, agentTypes, started));
17395
17536
  } finally {
17396
- _inFlightSubTasks--;
17537
+ if (!started.value)
17538
+ refundSessionSubAgentSlot();
17539
+ const n = (_inFlightByDepth.get(childDepth) ?? 1) - 1;
17540
+ if (n > 0)
17541
+ _inFlightByDepth.set(childDepth, n);
17542
+ else
17543
+ _inFlightByDepth.delete(childDepth);
17397
17544
  }
17398
- } else {
17399
- result = await runSubTask(input, options, agentTypes);
17400
17545
  }
17401
17546
  } else {
17402
17547
  const pre = runToolHooks(hooks.PreToolUse, "PreToolUse", name, input, options.workDir);
@@ -64372,7 +64517,7 @@ function TrustPrompt({ workDir, signals, onDecide }) {
64372
64517
  decide(false);
64373
64518
  return;
64374
64519
  }
64375
- if (key.return || input === "\r" || input === "\n") {
64520
+ if (key.return || /[\r\n]/.test(input)) {
64376
64521
  decide(indexRef.current === 0);
64377
64522
  }
64378
64523
  });
@@ -64575,7 +64720,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
64575
64720
  };
64576
64721
 
64577
64722
  // src/commands/chat.ts
64578
- var CLI_VERSION = "0.5.58";
64723
+ var CLI_VERSION = "0.5.60";
64579
64724
  var MODEL_LABELS = {
64580
64725
  turbo: "Nexrall Turbo",
64581
64726
  pro: "Nexrall Pro",
@@ -65158,6 +65303,9 @@ Set ${TRUST_ENV_VAR}=1 to confirm non-interactively, or run \`nex\` interactivel
65158
65303
  process.stdout.write("Not trusted \u2014 exiting without loading this folder.\n");
65159
65304
  process.exit(0);
65160
65305
  }
65306
+ await new Promise((resolve3) => setImmediate(resolve3));
65307
+ if (process.stdin.isTTY)
65308
+ process.stdin.setRawMode(true);
65161
65309
  }
65162
65310
  const env3 = collectEnv(workDir);
65163
65311
  const nexrallMd = readNexrallMd(workDir);
@@ -65387,6 +65535,7 @@ ${text}` : "");
65387
65535
  messages = [];
65388
65536
  sessionId = crypto.randomBytes(8).toString("hex");
65389
65537
  sessionTitle = "";
65538
+ (0, import_code_core4.resetSessionSubAgentBudget)();
65390
65539
  console.log(source_default.dim(" Conversation cleared. New session started."));
65391
65540
  rl.prompt();
65392
65541
  return;
@@ -66246,7 +66395,7 @@ function pluginSourceRemoveCommand(name, opts) {
66246
66395
 
66247
66396
  // src/index.ts
66248
66397
  var program2 = new Command();
66249
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.58").enablePositionalOptions();
66398
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.60").enablePositionalOptions();
66250
66399
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
66251
66400
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
66252
66401
  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.58",
3
+ "version": "0.5.60",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",
@@ -31,7 +31,15 @@
31
31
  "engines": {
32
32
  "node": ">=18"
33
33
  },
34
+ "scripts": {
35
+ "build": "node build.js",
36
+ "dev": "tsx src/index.ts",
37
+ "start": "node dist/index.js",
38
+ "test": "tsc --noEmit && tsc --noEmit -p test/tsconfig.json && node --import tsx --test test/*.test.ts test/*.test.mts",
39
+ "release": "node build.js && node scripts/upload-release.cjs"
40
+ },
34
41
  "dependencies": {
42
+ "@nexrall/code-core": "workspace:*",
35
43
  "chalk": "^5.3.0",
36
44
  "commander": "^12.0.0",
37
45
  "diff": "^5.2.0",
@@ -40,8 +48,7 @@
40
48
  "prompts": "^2.4.2",
41
49
  "react": "^19.2.8",
42
50
  "readline": "^1.3.0",
43
- "string-width": "^7.2.0",
44
- "@nexrall/code-core": "1.4.33"
51
+ "string-width": "^7.2.0"
45
52
  },
46
53
  "devDependencies": {
47
54
  "@aws-sdk/client-s3": "^3.600.0",
@@ -52,12 +59,5 @@
52
59
  "esbuild": "^0.20.0",
53
60
  "tsx": "^4.7.0",
54
61
  "typescript": "^6.0.3"
55
- },
56
- "scripts": {
57
- "build": "node build.js",
58
- "dev": "tsx src/index.ts",
59
- "start": "node dist/index.js",
60
- "test": "tsc --noEmit && tsc --noEmit -p test/tsconfig.json && node --import tsx --test test/*.test.ts test/*.test.mts",
61
- "release": "node build.js && node scripts/upload-release.cjs"
62
62
  }
63
- }
63
+ }