nexrall-code 0.5.52 → 0.5.54

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 +244 -34
  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.
@@ -14832,6 +14923,9 @@ var require_agentTypes = __commonJS({
14832
14923
  "stock_photo",
14833
14924
  "open_in_browser",
14834
14925
  "task",
14926
+ // Granted by a `memory:` scope rather than listed in a `tools:` line, but a user may
14927
+ // still name it explicitly — so it must validate rather than be flagged as a typo.
14928
+ "agent_memory_write",
14835
14929
  "get_symbols",
14836
14930
  "get_workspace_symbols",
14837
14931
  "find_references",
@@ -14845,7 +14939,8 @@ var require_agentTypes = __commonJS({
14845
14939
  "tools",
14846
14940
  "model",
14847
14941
  "test_files_only",
14848
- "testfilesonly"
14942
+ "testfilesonly",
14943
+ "memory"
14849
14944
  ]);
14850
14945
  var VALID_MODELS = ["turbo", "pro", "ultra"];
14851
14946
  function parseFrontmatter(raw) {
@@ -14918,6 +15013,13 @@ var require_agentTypes = __commonJS({
14918
15013
  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
15014
  });
14920
15015
  }
15016
+ if (meta.memory !== void 0 && parseMemoryScope(meta.memory) === void 0) {
15017
+ warnings.push({
15018
+ file: full,
15019
+ agent: name,
15020
+ 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.`
15021
+ });
15022
+ }
14921
15023
  if (meta.model !== void 0 && parseModel(meta.model) === void 0) {
14922
15024
  warnings.push({
14923
15025
  file: full,
@@ -14960,7 +15062,13 @@ var require_agentTypes = __commonJS({
14960
15062
  // `testFilesOnly`) lets anyone build a test-writing agent that genuinely
14961
15063
  // cannot touch production source, rather than only the builtin getting
14962
15064
  // that guarantee.
14963
- ...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}
15065
+ ...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {},
15066
+ // The `ok &&` is defensive, not load-bearing: parseFrontmatter already returns an
15067
+ // EMPTY meta when it cannot find a `---` block, so `meta.memory` is undefined on
15068
+ // that path regardless. It stays because the guarantee we want — an unreadable
15069
+ // definition never receives a writable store — should survive parseFrontmatter
15070
+ // being changed to salvage partial metadata, which is a plausible future edit.
15071
+ ...ok && parseMemoryScope(meta.memory) ? { memory: parseMemoryScope(meta.memory) } : {}
14964
15072
  });
14965
15073
  }
14966
15074
  }
@@ -15763,8 +15871,10 @@ var require_loop = __commonJS({
15763
15871
  };
15764
15872
  }();
15765
15873
  Object.defineProperty(exports, "__esModule", { value: true });
15766
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports._stallLimits = void 0;
15874
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = void 0;
15767
15875
  exports.errorRoundSignature = errorRoundSignature;
15876
+ exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
15877
+ exports.stopReasonNotice = stopReasonNotice;
15768
15878
  exports.resolveMaxIterations = resolveMaxIterations;
15769
15879
  exports.createLimiter = createLimiter;
15770
15880
  exports.extractSubTaskText = extractSubTaskText;
@@ -15902,6 +16012,72 @@ var require_loop = __commonJS({
15902
16012
  return errored.map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`).sort().join("|");
15903
16013
  }
15904
16014
  exports._stallLimits = { STALL_LIMIT, REPEAT_STALL_LIMIT };
16015
+ exports.AGENT_MEMORY_TOOL = "agent_memory_write";
16016
+ exports.AGENT_MEMORY_TOOL_SCHEMA = {
16017
+ name: exports.AGENT_MEMORY_TOOL,
16018
+ 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.",
16019
+ input_schema: {
16020
+ type: "object",
16021
+ properties: {
16022
+ content: { type: "string", description: "The single fact to remember, 1-2 sentences." }
16023
+ },
16024
+ required: ["content"]
16025
+ }
16026
+ };
16027
+ async function executeAgentMemoryWrite(input, binding, workDir) {
16028
+ if (!binding) {
16029
+ return {
16030
+ 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.`
16031
+ };
16032
+ }
16033
+ const content = typeof input.content === "string" ? input.content.trim() : "";
16034
+ if (!content)
16035
+ return { error: `${exports.AGENT_MEMORY_TOOL} requires a non-empty \`content\` string.` };
16036
+ const res = await (0, memory_1.writeAgentMemory)(binding.agentName, binding.scope, content, workDir);
16037
+ if (!res.ok) {
16038
+ return {
16039
+ 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.`
16040
+ };
16041
+ }
16042
+ return {
16043
+ 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.`
16044
+ };
16045
+ }
16046
+ function stopReasonNotice(reason, ctx = {}) {
16047
+ switch (reason) {
16048
+ case "clean":
16049
+ case "aborted":
16050
+ case "reported-elsewhere":
16051
+ return null;
16052
+ case "empty-response":
16053
+ return `
16054
+ \u26A0\uFE0F The model returned an empty response, so nothing was done. This is usually a transient upstream hiccup \u2014 send "continue" to retry.
16055
+ `;
16056
+ case "no-balance":
16057
+ return `
16058
+ \u{1F4B3} Stopped: your balance is empty, so the request was rejected before it started. Top up and send "continue" \u2014 no tokens were used for this turn.
16059
+ `;
16060
+ case "stalled":
16061
+ return `
16062
+ \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".
16063
+ `;
16064
+ case "stalled-repeat":
16065
+ return `
16066
+ \u{1F6D1} Stopped: the same tool error repeated ${REPEAT_STALL_LIMIT} rounds in a row, so the agent was looping without making progress.` + (ctx.repeatError ? ` The recurring error was:
16067
+ ${ctx.repeatError}
16068
+ ` : "\n") + `Fix that underlying cause (or grant the needed permission) and send "continue".
16069
+ `;
16070
+ case "budget":
16071
+ return `
16072
+ \u23F8\uFE0F Stopped at the ${ctx.budget}-step safety limit \u2014 the task may be incomplete. Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json (or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.
16073
+ `;
16074
+ case "unknown":
16075
+ default:
16076
+ return `
16077
+ \u26A0\uFE0F The run ended unexpectedly without completing. Your work so far is preserved \u2014 send "continue" to resume.
16078
+ `;
16079
+ }
16080
+ }
15905
16081
  function resolveMaxIterations(optionValue, settingsRaw) {
15906
16082
  const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
15907
16083
  const fromSettings = Number(settingsRaw.maxIterations);
@@ -16146,14 +16322,25 @@ ${tail}`;
16146
16322
  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."
16147
16323
  };
16148
16324
  }
16325
+ const memoryScope = agent?.memory;
16326
+ const agentMemoryNotes = agent && memoryScope ? (0, memory_1.agentMemoryPreamble)(agent.name, memoryScope, options.workDir) : "";
16149
16327
  const subNexrallMd = agent ? `# Sub-agent role: ${agent.name}
16150
- ${agent.prompt}` + (options.nexrallMd ? `
16328
+ ${agent.prompt}` + (agentMemoryNotes ? `
16329
+
16330
+ ---
16331
+
16332
+ ${agentMemoryNotes}` : "") + (options.nexrallMd ? `
16151
16333
 
16152
16334
  ---
16153
16335
 
16154
16336
  ${options.nexrallMd}` : "") : options.nexrallMd;
16155
16337
  const allowed = agent?.tools ? new Set(agent.tools) : null;
16338
+ if (allowed && memoryScope)
16339
+ allowed.add(exports.AGENT_MEMORY_TOOL);
16156
16340
  const gatedPermission = async (req) => {
16341
+ if (req.tool === exports.AGENT_MEMORY_TOOL && !(agent && memoryScope)) {
16342
+ 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.`);
16343
+ }
16157
16344
  if (allowed && !allowed.has(req.tool)) {
16158
16345
  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.`);
16159
16346
  }
@@ -16177,6 +16364,11 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16177
16364
  _depth: depth + 1,
16178
16365
  _agentScope: `sub_${++_subTaskCounter}`,
16179
16366
  // isolated todo store per sub-agent
16367
+ // Only set when the agent opted in, so the tool is inert for everyone else.
16368
+ ...agent && memoryScope ? { _agentMemory: { agentName: agent.name, scope: memoryScope } } : {},
16369
+ // The same set `gatedPermission` enforces above, so prompt and permission agree
16370
+ // by construction instead of by two people remembering to update both.
16371
+ ...allowed ? { _allowedTools: allowed } : {},
16180
16372
  editorContext: null,
16181
16373
  // fresh isolated context for sub-agent
16182
16374
  model: agent?.model ?? options.model,
@@ -16651,9 +16843,8 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16651
16843
  let lastPromptTokens = 0;
16652
16844
  let compacting = false;
16653
16845
  const hardCap = autoContinue ? Math.max(maxIterations, MAX_ITERATIONS_CEILING) : maxIterations;
16654
- let completedCleanly = false;
16846
+ let stopReason = "unknown";
16655
16847
  let completedRounds = 0;
16656
- let stalledOut = false;
16657
16848
  let consecutiveErrorRounds = 0;
16658
16849
  let repeatedErrorRounds = 0;
16659
16850
  let lastErrorSignature = "";
@@ -16671,8 +16862,10 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16671
16862
  let compactDisabled = false;
16672
16863
  try {
16673
16864
  for (; iteration < budget; iteration++) {
16674
- if (options.abortSignal?.aborted)
16865
+ if (options.abortSignal?.aborted) {
16866
+ stopReason = "aborted";
16675
16867
  break;
16868
+ }
16676
16869
  let bodyBytes = estimateBodyBytes2(messages);
16677
16870
  const prunePressure = lastPromptTokens > contextWindow * AUTO_PRUNE_THRESHOLD;
16678
16871
  const tokenPressure = lastPromptTokens > contextWindow * AUTO_COMPACT_THRESHOLD;
@@ -16768,7 +16961,22 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16768
16961
  nexrallMd: planAwareNexrallMd,
16769
16962
  clientType: options.clientType,
16770
16963
  abortSignal: options.abortSignal,
16771
- extraTools: options.mcpManager?.getAnthropicTools(),
16964
+ // MCP tools, plus the per-agent memory tool when (and only when) this run
16965
+ // is a sub-agent that declared a `memory:` scope. Declaring it through
16966
+ // extraTools rather than the backend's static catalogue keeps it invisible
16967
+ // to every other run: a tool the model cannot see is one it cannot try,
16968
+ // which is better than advertising it everywhere and refusing it at the gate.
16969
+ extraTools: [
16970
+ ...options.mcpManager?.getAnthropicTools() ?? [],
16971
+ ...options._agentMemory ? [exports.AGENT_MEMORY_TOOL_SCHEMA] : []
16972
+ ],
16973
+ // Derived from the run's ACTUAL allowlist rather than asserted separately,
16974
+ // so the prompt's memory instructions cannot drift from what is permitted.
16975
+ // That drift is the bug being fixed: sub-agents were told they MUST call
16976
+ // memory_write, which no sub-agent allowlist contains.
16977
+ canWriteSharedMemory: options._allowedTools ? options._allowedTools.has("memory_write") : true,
16978
+ // no allowlist = main agent = may write
16979
+ hasOwnAgentStore: !!options._agentMemory,
16772
16980
  agents: agentsCatalogue || void 0,
16773
16981
  skills: skillsCatalogue || void 0,
16774
16982
  // Only allow a post-render restart when the caller actually implements the
@@ -16778,19 +16986,24 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16778
16986
  allowRestartAfterRender: !!options.onStreamRestart
16779
16987
  }, onEvent);
16780
16988
  } catch (err) {
16781
- if (options.abortSignal?.aborted || err.name === "AbortError")
16989
+ if (options.abortSignal?.aborted || err.name === "AbortError") {
16990
+ stopReason = "aborted";
16782
16991
  break;
16992
+ }
16783
16993
  const status = err.status;
16784
16994
  if (status === 402) {
16785
16995
  const balance = err.balance ?? 0;
16786
16996
  options.onBalanceStatus?.(balance, true);
16997
+ stopReason = options.onBalanceStatus ? "reported-elsewhere" : "no-balance";
16787
16998
  break;
16788
16999
  }
16789
17000
  runSimpleHooks(hooks.OnError, options.workDir);
16790
17001
  throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
16791
17002
  }
16792
- if (options.abortSignal?.aborted)
17003
+ if (options.abortSignal?.aborted) {
17004
+ stopReason = "aborted";
16793
17005
  break;
17006
+ }
16794
17007
  assistantMessage.content = assistantMessage.content.filter((b) => b.type !== "thinking" && b.type !== "redacted_thinking");
16795
17008
  if (assistantMessage.content.length === 0) {
16796
17009
  const queued = options.takePendingInput?.() ?? [];
@@ -16801,7 +17014,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16801
17014
  continue;
16802
17015
  }
16803
17016
  runSimpleHooks(hooks.PostMessageComplete, options.workDir);
16804
- completedCleanly = true;
17017
+ stopReason = "empty-response";
16805
17018
  break;
16806
17019
  }
16807
17020
  const { stopReason: _stopReason, ...historyMessage } = assistantMessage;
@@ -16872,7 +17085,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16872
17085
  }
16873
17086
  }
16874
17087
  runSimpleHooks(hooks.PostMessageComplete, options.workDir);
16875
- completedCleanly = true;
17088
+ stopReason = "clean";
16876
17089
  break;
16877
17090
  }
16878
17091
  const toolResults = await Promise.all(toolUseBlocks.map(async (block) => {
@@ -16914,6 +17127,11 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
16914
17127
  result = { error: `Blocked by PreToolUse hook: ${pre.reason}` };
16915
17128
  } else {
16916
17129
  try {
17130
+ if (name === exports.AGENT_MEMORY_TOOL) {
17131
+ result = await executeAgentMemoryWrite(input, options._agentMemory, options.workDir);
17132
+ options.onToolResult(name, result);
17133
+ return { block: { ...block, id }, result };
17134
+ }
16917
17135
  const external = options.executeExternalTool ? await options.executeExternalTool(name, input) : null;
16918
17136
  if (external !== null && external !== void 0) {
16919
17137
  result = external;
@@ -17006,7 +17224,7 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
17006
17224
  const allErrored = toolResults.length > 0 && errored.length === toolResults.length;
17007
17225
  consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
17008
17226
  if (consecutiveErrorRounds >= STALL_LIMIT) {
17009
- stalledOut = true;
17227
+ stopReason = "stalled";
17010
17228
  break;
17011
17229
  }
17012
17230
  const errSignature = errorRoundSignature(errored.map(({ block, result }) => ({ name: block.name, error: String(result.error) })));
@@ -17017,7 +17235,7 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
17017
17235
  lastErrorSignature = errSignature;
17018
17236
  }
17019
17237
  if (repeatedErrorRounds >= REPEAT_STALL_LIMIT) {
17020
- stalledOut = true;
17238
+ stopReason = "stalled-repeat";
17021
17239
  stalledRepeatError = errored[0] ? String(errored[0].result.error).slice(0, 300) : null;
17022
17240
  break;
17023
17241
  }
@@ -17028,21 +17246,13 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
17028
17246
  `);
17029
17247
  }
17030
17248
  }
17031
- if (!options.abortSignal?.aborted && !completedCleanly) {
17032
- if (stalledOut) {
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
- ` : `
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".
17039
- `);
17040
- } else if (iteration >= budget) {
17041
- (options.onNotice ?? options.onText)(`
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.
17043
- `);
17044
- }
17045
- }
17249
+ if (stopReason === "unknown" && iteration >= budget)
17250
+ stopReason = "budget";
17251
+ if (options.abortSignal?.aborted)
17252
+ stopReason = "aborted";
17253
+ const notice = stopReasonNotice(stopReason, { budget, repeatError: stalledRepeatError });
17254
+ if (notice)
17255
+ (options.onNotice ?? options.onText)(notice);
17046
17256
  } catch (err) {
17047
17257
  if (err instanceof types_1.AgentTurnError)
17048
17258
  throw err;
@@ -64004,7 +64214,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
64004
64214
  };
64005
64215
 
64006
64216
  // src/commands/chat.ts
64007
- var CLI_VERSION = "0.5.52";
64217
+ var CLI_VERSION = "0.5.54";
64008
64218
  var MODEL_LABELS = {
64009
64219
  turbo: "Nexrall Turbo",
64010
64220
  pro: "Nexrall Pro",
@@ -65671,7 +65881,7 @@ function pluginSourceRemoveCommand(name, opts) {
65671
65881
 
65672
65882
  // src/index.ts
65673
65883
  var program2 = new Command();
65674
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.52").enablePositionalOptions();
65884
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.54").enablePositionalOptions();
65675
65885
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
65676
65886
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
65677
65887
  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.52",
3
+ "version": "0.5.54",
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.27"
44
+ "@nexrall/code-core": "1.4.29"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",