nexrall-code 0.5.53 → 0.5.55

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 +337 -26
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -9982,7 +9982,7 @@ var require_client = __commonJS({
9982
9982
  }
9983
9983
  var MAX_TOTAL_ATTEMPTS = (MAX_RETRIES + 1) * (MAX_RETRIES + 1);
9984
9984
  async function streamChat(messages, options, onEvent) {
9985
- const { model, env: env3, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents, skills, allowRestartAfterRender } = options;
9985
+ const { model, env: env3, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents, skills, allowRestartAfterRender, canWriteSharedMemory, hasOwnAgentStore } = options;
9986
9986
  let turnId = (0, crypto_1.randomUUID)();
9987
9987
  const controller = new AbortController();
9988
9988
  if (abortSignal?.aborted)
@@ -10023,7 +10023,7 @@ var require_client = __commonJS({
10023
10023
  {
10024
10024
  method: "POST",
10025
10025
  headers: authHeaders(),
10026
- body: JSON.stringify({ messages, model, env: env3, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills, turnId }),
10026
+ body: JSON.stringify({ messages, model, env: env3, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills, turnId, canWriteSharedMemory, hasOwnAgentStore }),
10027
10027
  signal: controller.signal
10028
10028
  }
10029
10029
  ];
@@ -11012,13 +11012,18 @@ var require_memory = __commonJS({
11012
11012
  };
11013
11013
  }();
11014
11014
  Object.defineProperty(exports, "__esModule", { value: true });
11015
- exports.MEMORY_HARD_CAP_BYTES = exports.MEMORY_COMPACT_TRIGGER_BYTES = exports.MEMORY_MAX_BYTES = exports.MEMORY_ENTRY_MAX_CHARS = void 0;
11015
+ exports.AGENT_MEMORY_MAX_BYTES = exports.AGENT_MEMORY_INJECT_MAX = exports.MEMORY_HARD_CAP_BYTES = exports.MEMORY_COMPACT_TRIGGER_BYTES = exports.MEMORY_MAX_BYTES = exports.MEMORY_ENTRY_MAX_CHARS = void 0;
11016
11016
  exports.memoryFilePath = memoryFilePath;
11017
11017
  exports.writeMemory = writeMemory;
11018
11018
  exports.readMemory = readMemory2;
11019
11019
  exports.readAllMemory = readAllMemory2;
11020
11020
  exports.clearMemory = clearMemory2;
11021
11021
  exports.memoryStats = memoryStats2;
11022
+ exports.isSafeAgentName = isSafeAgentName;
11023
+ exports.agentMemoryPath = agentMemoryPath;
11024
+ exports.readAgentMemory = readAgentMemory;
11025
+ exports.writeAgentMemory = writeAgentMemory;
11026
+ exports.agentMemoryPreamble = agentMemoryPreamble;
11022
11027
  exports.compactMemoryIfNeeded = compactMemoryIfNeeded2;
11023
11028
  var fs9 = __importStar(__require("fs"));
11024
11029
  var os6 = __importStar(__require("os"));
@@ -11127,6 +11132,71 @@ ${projMem}`);
11127
11132
  const entries = content.split("\n").filter((l) => /^-\s\[\d{4}-\d{2}-\d{2}\]/.test(l)).length;
11128
11133
  return { file, bytes: Buffer.byteLength(content, "utf-8"), entries };
11129
11134
  }
11135
+ exports.AGENT_MEMORY_INJECT_MAX = 8e3;
11136
+ exports.AGENT_MEMORY_MAX_BYTES = 24e3;
11137
+ function isSafeAgentName(name) {
11138
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && !name.includes("..");
11139
+ }
11140
+ function agentMemoryPath(agentName, scope, workDir) {
11141
+ if (!isSafeAgentName(agentName))
11142
+ return null;
11143
+ const file = `${agentName}.md`;
11144
+ let dir;
11145
+ if (scope === "user") {
11146
+ dir = path6.join(MEMORY_ROOT, "agent-memory");
11147
+ } else {
11148
+ if (!workDir)
11149
+ return null;
11150
+ dir = path6.join(path6.resolve(workDir), ".nexrall", scope === "local" ? "agent-memory-local" : "agent-memory");
11151
+ }
11152
+ const full = path6.join(dir, file);
11153
+ const rel = path6.relative(dir, full);
11154
+ if (rel.startsWith("..") || path6.isAbsolute(rel))
11155
+ return null;
11156
+ return full;
11157
+ }
11158
+ function readAgentMemory(agentName, scope, workDir) {
11159
+ const file = agentMemoryPath(agentName, scope, workDir);
11160
+ if (!file)
11161
+ return "";
11162
+ return readMemoryFile(file).trim();
11163
+ }
11164
+ async function writeAgentMemory(agentName, scope, content, workDir) {
11165
+ let trimmed = content.trim();
11166
+ if (!trimmed)
11167
+ return { ok: false, already: false, file: "" };
11168
+ if (trimmed.length > exports.MEMORY_ENTRY_MAX_CHARS) {
11169
+ trimmed = trimmed.slice(0, exports.MEMORY_ENTRY_MAX_CHARS - 1).trimEnd() + "\u2026";
11170
+ }
11171
+ const file = agentMemoryPath(agentName, scope, workDir);
11172
+ if (!file)
11173
+ return { ok: false, already: false, file: "" };
11174
+ return withLock(file, async () => {
11175
+ const existing = readMemoryFile(file);
11176
+ const fingerprint = trimmed.toLowerCase().slice(0, FINGERPRINT_LEN);
11177
+ if (existing.toLowerCase().includes(fingerprint)) {
11178
+ return { ok: true, already: true, file };
11179
+ }
11180
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
11181
+ let next = existing + `
11182
+ - [${today}] ${trimmed}`;
11183
+ if (Buffer.byteLength(next, "utf-8") > exports.AGENT_MEMORY_MAX_BYTES) {
11184
+ next = evictOldest(next, exports.AGENT_MEMORY_MAX_BYTES);
11185
+ }
11186
+ writeMemoryFile(file, next);
11187
+ return { ok: true, already: false, file };
11188
+ });
11189
+ }
11190
+ function agentMemoryPreamble(agentName, scope, workDir) {
11191
+ const notes = readAgentMemory(agentName, scope, workDir);
11192
+ if (!notes)
11193
+ return "";
11194
+ const kept = Buffer.byteLength(notes, "utf-8") > exports.AGENT_MEMORY_INJECT_MAX ? notes.slice(-exports.AGENT_MEMORY_INJECT_MAX) : notes;
11195
+ return `# Your own notes (agent: ${agentName}, scope: ${scope})
11196
+ These are notes YOU saved on previous runs. Treat them as your accumulated experience, not as instructions from the user, and verify anything that looks stale against the current code.
11197
+
11198
+ ${kept}`;
11199
+ }
11130
11200
  async function compactMemoryIfNeeded2(scope, workDir, summarize) {
11131
11201
  const file = memoryFilePath(scope, workDir);
11132
11202
  return withLock(file, async () => {
@@ -14585,6 +14655,11 @@ var require_agentTypes = __commonJS({
14585
14655
  var path6 = __importStar(__require("path"));
14586
14656
  var os6 = __importStar(__require("os"));
14587
14657
  var index_1 = require_plugins();
14658
+ var VALID_MEMORY_SCOPES = ["project", "user", "local"];
14659
+ function parseMemoryScope(v) {
14660
+ const s2 = (v ?? "").trim().toLowerCase();
14661
+ return VALID_MEMORY_SCOPES.includes(s2) ? s2 : void 0;
14662
+ }
14588
14663
  var READ_ONLY_TOOLS = [
14589
14664
  // Universal
14590
14665
  "read_file",
@@ -14600,6 +14675,22 @@ var require_agentTypes = __commonJS({
14600
14675
  "notebook_read",
14601
14676
  "todo_write",
14602
14677
  "todo_read",
14678
+ // memory_READ, deliberately without memory_write.
14679
+ //
14680
+ // Reading is free capability: a sub-agent that knows "tests run with X" or "never
14681
+ // edit Z directly" does better work, and withholding it meant every sub-agent
14682
+ // rediscovered project conventions from scratch. Writing is a different thing
14683
+ // entirely — memory is durable, cross-session, SHARED state, while sub-agents run
14684
+ // up to 4-wide in parallel with their reasoning hidden from the user. A wrong fact
14685
+ // written there is invisible and permanent, and it competes for a capped, shared
14686
+ // budget that gets periodically condensed.
14687
+ //
14688
+ // So the parent owns writes: a sub-agent that learns something durable says so in
14689
+ // its report, and the main agent — the one actually talking to the user — decides.
14690
+ // A sub-agent that needs its own persistent notes gets the per-agent store instead
14691
+ // (see `memory:` in the frontmatter), which is scoped to itself and cannot pollute
14692
+ // the shared file.
14693
+ "memory_read",
14603
14694
  // Skills are reusable prompt playbooks, and loop.ts advertises the skills
14604
14695
  // catalogue to sub-agents at EVERY depth — so withholding the tool that loads
14605
14696
  // one meant showing every sub-agent a menu it could not order from.
@@ -14627,6 +14718,29 @@ var require_agentTypes = __commonJS({
14627
14718
  "notebook_edit"
14628
14719
  ];
14629
14720
  var BUILTIN_AGENTS = [
14721
+ {
14722
+ // The catch-all, matching Claude Code's `general-purpose`.
14723
+ //
14724
+ // This capability already existed — omitting `subagent_type` gives an unrestricted
14725
+ // sub-agent — but it had no NAME, and that had two consequences worth fixing:
14726
+ //
14727
+ // 1. `permissions.deny: ["task(...)"]` matches on the agent name, so the ONE
14728
+ // sub-agent that can write files and run bash was the one variant a project
14729
+ // could not disable individually. Only a blanket `deny: ["task"]` reached it.
14730
+ // 2. The model had to infer that leaving the field blank was even an option, so
14731
+ // it would sometimes pick a specialist that fitted badly (an unrestricted
14732
+ // explorer) rather than the general worker it actually wanted.
14733
+ //
14734
+ // `tools` is deliberately UNDEFINED, which means "no allowlist" — full access,
14735
+ // 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.
14739
+ name: "general-purpose",
14740
+ 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
+ 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.",
14742
+ source: "builtin"
14743
+ },
14630
14744
  {
14631
14745
  name: "reviewer",
14632
14746
  description: "Read-only code reviewer \u2014 finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.",
@@ -14689,6 +14803,9 @@ var require_agentTypes = __commonJS({
14689
14803
  // frontier model, and this is the agent most likely to be spawned in bulk.
14690
14804
  {
14691
14805
  name: "explorer",
14806
+ // Lean prompt: this agent exists to keep bulk searching cheap, and it reports
14807
+ // findings for the MAIN agent to interpret with full project context.
14808
+ lightPrompt: true,
14692
14809
  description: "Fast read-only codebase explorer \u2014 locates files, symbols, and call sites and reports concise findings. Use to keep bulk searching out of the main context. Cannot modify files.",
14693
14810
  tools: READ_ONLY_TOOLS,
14694
14811
  model: "turbo",
@@ -14832,6 +14949,9 @@ var require_agentTypes = __commonJS({
14832
14949
  "stock_photo",
14833
14950
  "open_in_browser",
14834
14951
  "task",
14952
+ // Granted by a `memory:` scope rather than listed in a `tools:` line, but a user may
14953
+ // still name it explicitly — so it must validate rather than be flagged as a typo.
14954
+ "agent_memory_write",
14835
14955
  "get_symbols",
14836
14956
  "get_workspace_symbols",
14837
14957
  "find_references",
@@ -14845,7 +14965,8 @@ var require_agentTypes = __commonJS({
14845
14965
  "tools",
14846
14966
  "model",
14847
14967
  "test_files_only",
14848
- "testfilesonly"
14968
+ "testfilesonly",
14969
+ "memory"
14849
14970
  ]);
14850
14971
  var VALID_MODELS = ["turbo", "pro", "ultra"];
14851
14972
  function parseFrontmatter(raw) {
@@ -14918,6 +15039,13 @@ var require_agentTypes = __commonJS({
14918
15039
  message: "has no `description:` \u2014 that text is the ONLY thing the model uses to decide when to delegate to this agent, so it will rarely be picked."
14919
15040
  });
14920
15041
  }
15042
+ if (meta.memory !== void 0 && parseMemoryScope(meta.memory) === void 0) {
15043
+ warnings.push({
15044
+ file: full,
15045
+ agent: name,
15046
+ message: `has memory: "${meta.memory}", which is not a valid scope \u2014 use one of ${VALID_MEMORY_SCOPES.join(", ")}, or omit the line to give this agent no persistent notes.`
15047
+ });
15048
+ }
14921
15049
  if (meta.model !== void 0 && parseModel(meta.model) === void 0) {
14922
15050
  warnings.push({
14923
15051
  file: full,
@@ -14960,7 +15088,13 @@ var require_agentTypes = __commonJS({
14960
15088
  // `testFilesOnly`) lets anyone build a test-writing agent that genuinely
14961
15089
  // cannot touch production source, rather than only the builtin getting
14962
15090
  // that guarantee.
14963
- ...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}
15091
+ ...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {},
15092
+ // The `ok &&` is defensive, not load-bearing: parseFrontmatter already returns an
15093
+ // EMPTY meta when it cannot find a `---` block, so `meta.memory` is undefined on
15094
+ // that path regardless. It stays because the guarantee we want — an unreadable
15095
+ // definition never receives a writable store — should survive parseFrontmatter
15096
+ // being changed to salvage partial metadata, which is a plausible future edit.
15097
+ ...ok && parseMemoryScope(meta.memory) ? { memory: parseMemoryScope(meta.memory) } : {}
14964
15098
  });
14965
15099
  }
14966
15100
  }
@@ -15763,14 +15897,17 @@ var require_loop = __commonJS({
15763
15897
  };
15764
15898
  }();
15765
15899
  Object.defineProperty(exports, "__esModule", { value: true });
15766
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports._stallLimits = void 0;
15900
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = void 0;
15767
15901
  exports.errorRoundSignature = errorRoundSignature;
15902
+ exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
15768
15903
  exports.stopReasonNotice = stopReasonNotice;
15769
15904
  exports.resolveMaxIterations = resolveMaxIterations;
15770
15905
  exports.createLimiter = createLimiter;
15906
+ exports.resolveSubtaskTimeoutMs = resolveSubtaskTimeoutMs;
15771
15907
  exports.extractSubTaskText = extractSubTaskText;
15772
15908
  exports.capSubTaskText = capSubTaskText;
15773
15909
  exports.summariseSubTaskProgress = summariseSubTaskProgress;
15910
+ exports.lastToolResults = lastToolResults;
15774
15911
  exports.contextWindowFor = contextWindowFor2;
15775
15912
  exports.compactionThresholds = compactionThresholds2;
15776
15913
  exports.estimateBodyBytes = estimateBodyBytes2;
@@ -15903,6 +16040,37 @@ var require_loop = __commonJS({
15903
16040
  return errored.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`).sort().join("|");
15904
16041
  }
15905
16042
  exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
16043
+ exports.AGENT_MEMORY_TOOL = "agent_memory_write";
16044
+ exports.AGENT_MEMORY_TOOL_SCHEMA = {
16045
+ name: exports.AGENT_MEMORY_TOOL,
16046
+ description: "Save a durable note to YOUR OWN persistent notes, which are injected into your prompt on future runs. Use this for lessons that will still be true next time \u2014 a convention this repo follows, a recurring bug pattern, a command that works, a dead end not worth retrying. These notes are PRIVATE to you: they are NOT shown to the user and NOT read by the main agent, so anything the user or the main agent needs to know must still go in your final message. One self-contained fact per call, a sentence or two. Do not save transient task details.",
16047
+ input_schema: {
16048
+ type: "object",
16049
+ properties: {
16050
+ content: { type: "string", description: "The single fact to remember, 1-2 sentences." }
16051
+ },
16052
+ required: ["content"]
16053
+ }
16054
+ };
16055
+ async function executeAgentMemoryWrite(input, binding, workDir) {
16056
+ if (!binding) {
16057
+ return {
16058
+ error: `${exports.AGENT_MEMORY_TOOL} is only available to a sub-agent whose definition declares a \`memory:\` scope. Put anything worth remembering in your final message instead.`
16059
+ };
16060
+ }
16061
+ const content = typeof input.content === "string" ? input.content.trim() : "";
16062
+ if (!content)
16063
+ return { error: `${exports.AGENT_MEMORY_TOOL} requires a non-empty \`content\` string.` };
16064
+ const res = await (0, memory_1.writeAgentMemory)(binding.agentName, binding.scope, content, workDir);
16065
+ if (!res.ok) {
16066
+ return {
16067
+ error: `Could not save to the "${binding.agentName}" agent's ${binding.scope} notes. Either this scope needs a project directory (use \`memory: user\` for a store that works anywhere) or the agent name is not usable as a filename.`
16068
+ };
16069
+ }
16070
+ return {
16071
+ output: res.already ? "Already saved (a near-identical note exists) \u2014 nothing added." : `Saved to your ${binding.scope} notes. It will be in your prompt on your next run.`
16072
+ };
16073
+ }
15906
16074
  function stopReasonNotice(reason, ctx = {}) {
15907
16075
  switch (reason) {
15908
16076
  case "clean":
@@ -16082,7 +16250,17 @@ ${ctx.repeatError}
16082
16250
  }
16083
16251
  var MAX_TASK_DEPTH = 1;
16084
16252
  var _subTaskCounter = 0;
16085
- var SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0 ? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) : 10 * 60 * 1e3;
16253
+ var DEFAULT_SUBTASK_TIMEOUT_MS = 10 * 60 * 1e3;
16254
+ function resolveSubtaskTimeoutMs(settingsRaw) {
16255
+ const fromEnv = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS);
16256
+ if (Number.isFinite(fromEnv) && fromEnv > 0)
16257
+ return Math.floor(fromEnv);
16258
+ const raw = settingsRaw.subtaskTimeoutMs;
16259
+ const fromSettings = Number(raw);
16260
+ if (Number.isFinite(fromSettings) && fromSettings > 0)
16261
+ return Math.floor(fromSettings);
16262
+ return DEFAULT_SUBTASK_TIMEOUT_MS;
16263
+ }
16086
16264
  var SUBTASK_MAX = 48e3;
16087
16265
  var ToolNotAllowedError = class extends Error {
16088
16266
  constructor(message) {
@@ -16139,11 +16317,59 @@ ${tail}`;
16139
16317
  }
16140
16318
  if (toolNames.length === 0)
16141
16319
  return "";
16320
+ const recentFindings = lastToolResults(messages, SALVAGE_RESULT_COUNT, SALVAGE_RESULT_CHARS);
16142
16321
  const counts = /* @__PURE__ */ new Map();
16143
16322
  for (const n of toolNames)
16144
16323
  counts.set(n, (counts.get(n) ?? 0) + 1);
16145
16324
  const inventory = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
16146
- return `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
16325
+ const header = `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
16326
+ return recentFindings ? `${header}
16327
+
16328
+ What its most recent tool calls actually returned (use this instead of repeating them):
16329
+ ${recentFindings}` : header;
16330
+ }
16331
+ var SALVAGE_RESULT_COUNT = 4;
16332
+ var SALVAGE_RESULT_CHARS = 2e3;
16333
+ function lastToolResults(messages, count, maxChars) {
16334
+ const nameById = /* @__PURE__ */ new Map();
16335
+ for (const m2 of messages) {
16336
+ if (m2.role !== "assistant" || !Array.isArray(m2.content))
16337
+ continue;
16338
+ for (const b of m2.content) {
16339
+ if (b?.type === "tool_use" && b.id && typeof b.name === "string")
16340
+ nameById.set(b.id, b.name);
16341
+ }
16342
+ }
16343
+ const out = [];
16344
+ for (let i2 = messages.length - 1; i2 >= 0 && out.length < count; i2--) {
16345
+ const m2 = messages[i2];
16346
+ if (m2.role !== "user" || !Array.isArray(m2.content))
16347
+ continue;
16348
+ for (const b of [...m2.content].reverse()) {
16349
+ if (out.length >= count)
16350
+ break;
16351
+ if (b?.type !== "tool_result")
16352
+ continue;
16353
+ const text = toolResultText(b);
16354
+ if (!text)
16355
+ continue;
16356
+ const name = nameById.get(String(b.tool_use_id ?? "")) ?? "tool";
16357
+ const body = text.length > maxChars ? `${sliceSafeEnd(text, maxChars)}
16358
+ \u2026 [truncated]` : text;
16359
+ out.push(`\u2022 ${name}:
16360
+ ${body}`);
16361
+ }
16362
+ }
16363
+ return out.reverse().join("\n\n");
16364
+ }
16365
+ function toolResultText(block) {
16366
+ const c = block.content;
16367
+ if (typeof c === "string")
16368
+ return c.trim();
16369
+ if (Array.isArray(c)) {
16370
+ return c.filter((x2) => x2?.type === "text" && typeof x2.text === "string").map((x2) => x2.text).join("\n").trim();
16371
+ }
16372
+ return "";
16147
16373
  }
16148
16374
  async function runSubTask(input, options, agentTypes) {
16149
16375
  const prompt2 = typeof input.prompt === "string" ? input.prompt.trim() : "";
@@ -16182,14 +16408,26 @@ ${tail}`;
16182
16408
  If you just created .nexrall/agents/` + requestedType + ".md, make sure the write finished in an EARLIER tool call than this one \u2014 a file written in the same batch may not be on disk yet."
16183
16409
  };
16184
16410
  }
16411
+ const memoryScope = agent?.memory;
16412
+ const agentMemoryNotes = agent && memoryScope ? (0, memory_1.agentMemoryPreamble)(agent.name, memoryScope, options.workDir) : "";
16413
+ const inheritedMd = agent?.lightPrompt ? "" : options.nexrallMd ?? "";
16185
16414
  const subNexrallMd = agent ? `# Sub-agent role: ${agent.name}
16186
- ${agent.prompt}` + (options.nexrallMd ? `
16415
+ ${agent.prompt}` + (agentMemoryNotes ? `
16187
16416
 
16188
16417
  ---
16189
16418
 
16190
- ${options.nexrallMd}` : "") : options.nexrallMd;
16419
+ ${agentMemoryNotes}` : "") + (inheritedMd ? `
16420
+
16421
+ ---
16422
+
16423
+ ${inheritedMd}` : "") : options.nexrallMd;
16191
16424
  const allowed = agent?.tools ? new Set(agent.tools) : null;
16425
+ if (allowed && memoryScope)
16426
+ allowed.add(exports.AGENT_MEMORY_TOOL);
16192
16427
  const gatedPermission = async (req) => {
16428
+ if (req.tool === exports.AGENT_MEMORY_TOOL && !(agent && memoryScope)) {
16429
+ 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.`);
16430
+ }
16193
16431
  if (allowed && !allowed.has(req.tool)) {
16194
16432
  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.`);
16195
16433
  }
@@ -16200,9 +16438,18 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16200
16438
  };
16201
16439
  const subMessages = resumed ? [...resumed.messages, { role: "user", content: [{ type: "text", text: prompt2 }] }] : [{ role: "user", content: [{ type: "text", text: prompt2 }] }];
16202
16440
  const subAbort = { aborted: false };
16203
- const timer = setTimeout(() => {
16204
- subAbort.aborted = true;
16205
- }, SUBTASK_TIMEOUT_MS);
16441
+ const subtaskTimeoutMs = resolveSubtaskTimeoutMs((0, rules_1.loadSettings)(options.workDir).raw);
16442
+ let lastProgressAt = Date.now();
16443
+ let stalled = false;
16444
+ const bumpProgress = () => {
16445
+ lastProgressAt = Date.now();
16446
+ };
16447
+ const stallWatchdog = setInterval(() => {
16448
+ if (Date.now() - lastProgressAt > subtaskTimeoutMs) {
16449
+ stalled = true;
16450
+ subAbort.aborted = true;
16451
+ }
16452
+ }, 1e3);
16206
16453
  const parentAbortPoll = setInterval(() => {
16207
16454
  if (options.abortSignal?.aborted)
16208
16455
  subAbort.aborted = true;
@@ -16213,6 +16460,27 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16213
16460
  _depth: depth + 1,
16214
16461
  _agentScope: `sub_${++_subTaskCounter}`,
16215
16462
  // isolated todo store per sub-agent
16463
+ // ── Assigned UNCONDITIONALLY, never by conditional spread ────────────────
16464
+ //
16465
+ // These two were previously spread in only when set:
16466
+ //
16467
+ // ...(agent && memoryScope ? { _agentMemory: … } : {}),
16468
+ //
16469
+ // which does NOT clear the key — it leaves whatever `...options` already had.
16470
+ // So a child WITHOUT its own `memory:` inherited its PARENT's binding and would
16471
+ // have appended to another agent's private notes; likewise an agent with no
16472
+ // `tools:` line inherited the parent's allowlist, making the prompt's capability
16473
+ // claim disagree with its real one.
16474
+ //
16475
+ // MAX_TASK_DEPTH === 1 means no nested spawn can reach this today, so it is
16476
+ // latent rather than live — but the limiter comment below explicitly contemplates
16477
+ // raising that depth, and this is exactly the kind of leak that would come back
16478
+ // as a security bug rather than a visible error. Explicit undefined makes the
16479
+ // child's identity independent of the parent's by construction.
16480
+ _agentMemory: agent && memoryScope ? { agentName: agent.name, scope: memoryScope } : void 0,
16481
+ // The same set `gatedPermission` enforces above, so prompt and permission agree
16482
+ // by construction instead of by two people remembering to update both.
16483
+ _allowedTools: allowed ?? void 0,
16216
16484
  editorContext: null,
16217
16485
  // fresh isolated context for sub-agent
16218
16486
  model: agent?.model ?? options.model,
@@ -16244,25 +16512,48 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16244
16512
  // Forward tool events with isSubTask=true so the UI can render a badge
16245
16513
  // instead of prepending "[sub-task]" to the tool name (which caused double-prefix
16246
16514
  // when the name was already labelled, and mixed display concerns into the data layer).
16247
- onToolUse: (n, i2) => options.onToolUse(n, i2, true),
16248
- onToolResult: (n, r2) => options.onToolResult(n, r2, true),
16515
+ // Every tool event is PROGRESS: it proves the sub-agent is still doing work, which
16516
+ // is what the stall watchdog above measures. Bumping on both use and result means a
16517
+ // single very slow tool (a long test run) resets the clock when it starts AND when
16518
+ // it finishes, so it cannot be mistaken for a hang.
16519
+ onToolUse: (n, i2) => {
16520
+ bumpProgress();
16521
+ options.onToolUse(n, i2, true);
16522
+ },
16523
+ onToolResult: (n, r2) => {
16524
+ bumpProgress();
16525
+ options.onToolResult(n, r2, true);
16526
+ },
16249
16527
  onToolStreamChunk: (n, c) => options.onToolStreamChunk?.(n, c, true),
16250
16528
  // Forward thinking so the UI shows the indicator while sub-agent reasons
16251
- onThinking: (text2) => options.onThinking?.(text2),
16252
- onThinkingDelta: (text2) => options.onThinkingDelta?.(text2),
16253
- onThinkingProgress: (tok) => options.onThinkingProgress?.(tok)
16529
+ // Thinking is progress too — a model reasoning for minutes on a hard problem is
16530
+ // working, not stalled. Without this, deep reasoning on an expensive tier would
16531
+ // trip the watchdog precisely when the sub-agent was most valuable.
16532
+ onThinking: (text2) => {
16533
+ bumpProgress();
16534
+ options.onThinking?.(text2);
16535
+ },
16536
+ onThinkingDelta: (text2) => {
16537
+ bumpProgress();
16538
+ options.onThinkingDelta?.(text2);
16539
+ },
16540
+ onThinkingProgress: (tok) => {
16541
+ bumpProgress();
16542
+ options.onThinkingProgress?.(tok);
16543
+ }
16254
16544
  });
16255
- if (subAbort.aborted && !options.abortSignal?.aborted) {
16256
- const mins = Math.round(SUBTASK_TIMEOUT_MS / 6e4);
16545
+ if (stalled && !options.abortSignal?.aborted) {
16546
+ const mins = Math.round(subtaskTimeoutMs / 6e4);
16257
16547
  const partial = capSubTaskText(extractSubTaskText(result, false));
16258
16548
  const progress = summariseSubTaskProgress(result);
16549
+ const partialId = (0, agentRegistry_1.rememberAgent)(agent?.name ?? null, typeof input.description === "string" && input.description.trim() || prompt2.slice(0, 80), result);
16259
16550
  const sections = [
16260
- `Sub-task STOPPED after ${mins} minutes without completing \u2014 treat the following as PARTIAL, unverified work, not a finished answer.`,
16551
+ `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.`,
16261
16552
  progress,
16262
16553
  partial ? `Partial output before it was stopped:
16263
16554
 
16264
16555
  ${partial}` : "",
16265
- "Do NOT simply re-run the same sub-task: build on what is above, or split the remaining work into smaller, more focused sub-tasks."
16556
+ `Do NOT re-run the same sub-task from scratch. Either build on what is above, or continue THIS run with resume_agent_id="${partialId}" (it still has everything it read), or split the remaining work into smaller, more focused sub-tasks.`
16266
16557
  ].filter(Boolean);
16267
16558
  return { error: sections.join("\n\n") };
16268
16559
  }
@@ -16291,7 +16582,7 @@ ${partial}` : "",
16291
16582
  }
16292
16583
  return { error: `Sub-task failed: ${err.message}` };
16293
16584
  } finally {
16294
- clearTimeout(timer);
16585
+ clearInterval(stallWatchdog);
16295
16586
  clearInterval(parentAbortPoll);
16296
16587
  }
16297
16588
  }
@@ -16805,7 +17096,22 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16805
17096
  nexrallMd: planAwareNexrallMd,
16806
17097
  clientType: options.clientType,
16807
17098
  abortSignal: options.abortSignal,
16808
- extraTools: options.mcpManager?.getAnthropicTools(),
17099
+ // MCP tools, plus the per-agent memory tool when (and only when) this run
17100
+ // is a sub-agent that declared a `memory:` scope. Declaring it through
17101
+ // extraTools rather than the backend's static catalogue keeps it invisible
17102
+ // to every other run: a tool the model cannot see is one it cannot try,
17103
+ // which is better than advertising it everywhere and refusing it at the gate.
17104
+ extraTools: [
17105
+ ...options.mcpManager?.getAnthropicTools() ?? [],
17106
+ ...options._agentMemory ? [exports.AGENT_MEMORY_TOOL_SCHEMA] : []
17107
+ ],
17108
+ // Derived from the run's ACTUAL allowlist rather than asserted separately,
17109
+ // so the prompt's memory instructions cannot drift from what is permitted.
17110
+ // That drift is the bug being fixed: sub-agents were told they MUST call
17111
+ // memory_write, which no sub-agent allowlist contains.
17112
+ canWriteSharedMemory: options._allowedTools ? options._allowedTools.has("memory_write") : true,
17113
+ // no allowlist = main agent = may write
17114
+ hasOwnAgentStore: !!options._agentMemory,
16809
17115
  agents: agentsCatalogue || void 0,
16810
17116
  skills: skillsCatalogue || void 0,
16811
17117
  // Only allow a post-render restart when the caller actually implements the
@@ -16956,6 +17262,11 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16956
17262
  result = { error: `Blocked by PreToolUse hook: ${pre.reason}` };
16957
17263
  } else {
16958
17264
  try {
17265
+ if (name === exports.AGENT_MEMORY_TOOL) {
17266
+ result = await executeAgentMemoryWrite(input, options._agentMemory, options.workDir);
17267
+ options.onToolResult(name, result);
17268
+ return { block: { ...block, id }, result };
17269
+ }
16959
17270
  const external = options.executeExternalTool ? await options.executeExternalTool(name, input) : null;
16960
17271
  if (external !== null && external !== void 0) {
16961
17272
  result = external;
@@ -64038,7 +64349,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
64038
64349
  };
64039
64350
 
64040
64351
  // src/commands/chat.ts
64041
- var CLI_VERSION = "0.5.53";
64352
+ var CLI_VERSION = "0.5.55";
64042
64353
  var MODEL_LABELS = {
64043
64354
  turbo: "Nexrall Turbo",
64044
64355
  pro: "Nexrall Pro",
@@ -65705,7 +66016,7 @@ function pluginSourceRemoveCommand(name, opts) {
65705
66016
 
65706
66017
  // src/index.ts
65707
66018
  var program2 = new Command();
65708
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.53").enablePositionalOptions();
66019
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.55").enablePositionalOptions();
65709
66020
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
65710
66021
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
65711
66022
  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.53",
3
+ "version": "0.5.55",
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.28"
44
+ "@nexrall/code-core": "1.4.30"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",