@nexrall/code-core 1.4.66 → 1.4.68

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 (91) hide show
  1. package/dist/agent/agentRegistry.d.ts +27 -0
  2. package/dist/agent/agentRegistry.d.ts.map +1 -1
  3. package/dist/agent/agentRegistry.js +45 -0
  4. package/dist/agent/agentTypes.d.ts +6 -2
  5. package/dist/agent/agentTypes.d.ts.map +1 -1
  6. package/dist/agent/agentTypes.js +13 -4
  7. package/dist/agent/askOnce.d.ts +12 -0
  8. package/dist/agent/askOnce.d.ts.map +1 -0
  9. package/dist/agent/askOnce.js +41 -0
  10. package/dist/agent/compaction.d.ts +244 -0
  11. package/dist/agent/compaction.d.ts.map +1 -0
  12. package/dist/agent/compaction.js +976 -0
  13. package/dist/agent/fileLocks.d.ts +34 -0
  14. package/dist/agent/fileLocks.d.ts.map +1 -0
  15. package/dist/agent/fileLocks.js +114 -0
  16. package/dist/agent/hooks.d.ts +331 -0
  17. package/dist/agent/hooks.d.ts.map +1 -0
  18. package/dist/agent/hooks.js +1239 -0
  19. package/dist/agent/iterationPolicy.d.ts +121 -0
  20. package/dist/agent/iterationPolicy.d.ts.map +1 -0
  21. package/dist/agent/iterationPolicy.js +297 -0
  22. package/dist/agent/lifecycleHost.d.ts +55 -0
  23. package/dist/agent/lifecycleHost.d.ts.map +1 -0
  24. package/dist/agent/lifecycleHost.js +294 -0
  25. package/dist/agent/loop.d.ts +39 -491
  26. package/dist/agent/loop.d.ts.map +1 -1
  27. package/dist/agent/loop.js +538 -3069
  28. package/dist/agent/planMode.d.ts.map +1 -1
  29. package/dist/agent/planMode.js +1 -0
  30. package/dist/agent/sharedTasks.d.ts +7 -0
  31. package/dist/agent/sharedTasks.d.ts.map +1 -1
  32. package/dist/agent/sharedTasks.js +16 -0
  33. package/dist/agent/subAgentBudget.d.ts +65 -0
  34. package/dist/agent/subAgentBudget.d.ts.map +1 -0
  35. package/dist/agent/subAgentBudget.js +269 -0
  36. package/dist/agent/subTask.d.ts +15 -0
  37. package/dist/agent/subTask.d.ts.map +1 -0
  38. package/dist/agent/subTask.js +730 -0
  39. package/dist/agent/subTaskSupport.d.ts +167 -0
  40. package/dist/agent/subTaskSupport.d.ts.map +1 -0
  41. package/dist/agent/subTaskSupport.js +425 -0
  42. package/dist/agent/toolDescriptions.d.ts +3 -0
  43. package/dist/agent/toolDescriptions.d.ts.map +1 -0
  44. package/dist/agent/toolDescriptions.js +116 -0
  45. package/dist/api/client.d.ts +41 -0
  46. package/dist/api/client.d.ts.map +1 -1
  47. package/dist/api/client.js +173 -1
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +5 -0
  51. package/dist/mcp/client.d.ts +104 -0
  52. package/dist/mcp/client.d.ts.map +1 -1
  53. package/dist/mcp/client.js +136 -2
  54. package/dist/mcp/httpClient.d.ts +17 -1
  55. package/dist/mcp/httpClient.d.ts.map +1 -1
  56. package/dist/mcp/httpClient.js +120 -19
  57. package/dist/mcp/manager.d.ts +77 -2
  58. package/dist/mcp/manager.d.ts.map +1 -1
  59. package/dist/mcp/manager.js +275 -9
  60. package/dist/mcp/server.d.ts +39 -0
  61. package/dist/mcp/server.d.ts.map +1 -0
  62. package/dist/mcp/server.js +189 -0
  63. package/dist/mcp/sseClient.d.ts +7 -1
  64. package/dist/mcp/sseClient.d.ts.map +1 -1
  65. package/dist/mcp/sseClient.js +45 -1
  66. package/dist/mcp/stats.d.ts +41 -0
  67. package/dist/mcp/stats.d.ts.map +1 -0
  68. package/dist/mcp/stats.js +108 -0
  69. package/dist/permissions/destructive.d.ts +2 -0
  70. package/dist/permissions/destructive.d.ts.map +1 -1
  71. package/dist/permissions/destructive.js +6 -2
  72. package/dist/permissions/destructiveTokens.d.ts +5 -0
  73. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  74. package/dist/permissions/destructiveTokens.js +9 -3
  75. package/dist/permissions/modePolicy.d.ts.map +1 -1
  76. package/dist/permissions/modePolicy.js +5 -2
  77. package/dist/permissions/rules.d.ts +4 -1
  78. package/dist/permissions/rules.d.ts.map +1 -1
  79. package/dist/permissions/rules.js +29 -0
  80. package/dist/plugins/data.d.ts +23 -0
  81. package/dist/plugins/data.d.ts.map +1 -0
  82. package/dist/plugins/data.js +141 -0
  83. package/dist/plugins/index.d.ts +14 -2
  84. package/dist/plugins/index.d.ts.map +1 -1
  85. package/dist/plugins/index.js +45 -4
  86. package/dist/plugins/installer.d.ts +83 -0
  87. package/dist/plugins/installer.d.ts.map +1 -1
  88. package/dist/plugins/installer.js +207 -1
  89. package/dist/types.d.ts +45 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ import type { ToolResult } from '../types';
2
+ import { type AgentMemoryScope } from './agentTypes';
3
+ export declare const MAX_ITERATIONS_CEILING = 2000;
4
+ export declare const STALL_LIMIT = 8;
5
+ export declare const REPEAT_STALL_LIMIT = 12;
6
+ export declare const EMPTY_TURN_RETRY_LIMIT = 3;
7
+ /** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
8
+ export declare function emptyTurnBackoffMs(attempt: number): number;
9
+ /**
10
+ * Should an empty assistant turn be retried automatically, rather than surfaced?
11
+ *
12
+ * Split out as a pure function because the two "empty" causes look IDENTICAL at the
13
+ * call site (both are `content.length === 0`) and telling them apart is the entire
14
+ * point — the previous bug in this area was treating a deterministic truncation as a
15
+ * transient hiccup and advising a retry that reproduced it verbatim.
16
+ *
17
+ * • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
18
+ * OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
19
+ * behaviour, so it fails the same way while billing again. Never retried; the user
20
+ * is told to lower effort or split the task (stopReasonNotice 'output-limit').
21
+ * • anything else → a genuinely transient empty turn. Retried, up to the limit.
22
+ *
23
+ * The attempt cap matters as much as the classification: a model that has decided to
24
+ * return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
25
+ * on a paid endpoint. After the cap we fall back to the old visible notice.
26
+ */
27
+ export declare function shouldRetryEmptyTurn(stopReason: string | null | undefined, attemptsSoFar: number): boolean;
28
+ /** Empty-turn retry limits, exposed for tests. */
29
+ export declare const _emptyTurnRetry: {
30
+ EMPTY_TURN_RETRY_LIMIT: number;
31
+ EMPTY_TURN_RETRY_BASE_MS: number;
32
+ };
33
+ /**
34
+ * Fingerprint one round's tool failures, for the repeated-failure runaway guard.
35
+ *
36
+ * Exported (with the limits) purely as a test seam: the guard's whole value is in the
37
+ * edge cases — that a DIFFERENT error each round must NOT trip it, that call order
38
+ * within a round is irrelevant, that a long error body doesn't make every occurrence
39
+ * look unique — and none of that is reachable without driving a live model loop.
40
+ *
41
+ * Sorted so parallel tool calls completing in a different order still compare equal;
42
+ * truncated because errors often embed a varying path or timestamp late in the string.
43
+ */
44
+ export declare function errorRoundSignature(errored: Array<{
45
+ name: string;
46
+ error: string;
47
+ }>): string;
48
+ /** Runaway-guard limits, exposed for tests. */
49
+ export declare const _stallLimits: {
50
+ STALL_LIMIT: number;
51
+ REPEAT_STALL_LIMIT: number;
52
+ };
53
+ /**
54
+ * The single tool granted by a `memory:` frontmatter scope.
55
+ *
56
+ * Named distinctly from `memory_write` on purpose: they write to DIFFERENT stores, and
57
+ * a model that saw one name for both would reasonably assume its notes were visible to
58
+ * the main agent. They are not.
59
+ */
60
+ export declare const AGENT_MEMORY_TOOL = "agent_memory_write";
61
+ /**
62
+ * Schema advertised to the model, only for a sub-agent with a declared memory scope.
63
+ *
64
+ * The description does the load-bearing work of keeping the two stores apart in the
65
+ * model's head: it must not believe these notes reach the user or the main agent.
66
+ */
67
+ export declare const AGENT_MEMORY_TOOL_SCHEMA: {
68
+ readonly name: "agent_memory_write";
69
+ readonly description: string;
70
+ readonly input_schema: {
71
+ readonly type: "object";
72
+ readonly properties: {
73
+ readonly content: {
74
+ readonly type: "string";
75
+ readonly description: "The single fact to remember, 1-2 sentences.";
76
+ };
77
+ };
78
+ readonly required: readonly ["content"];
79
+ };
80
+ };
81
+ /** Identifies whose notes an agent_memory_write call may touch. */
82
+ export interface AgentMemoryBinding {
83
+ agentName: string;
84
+ scope: AgentMemoryScope;
85
+ }
86
+ /**
87
+ * Execute an `agent_memory_write` call.
88
+ *
89
+ * Pure-ish and exported so the refusal paths are testable without spawning a real
90
+ * sub-agent: the binding is data, so "no binding" and "unsafe agent name" can both be
91
+ * exercised directly.
92
+ */
93
+ export declare function executeAgentMemoryWrite(input: Record<string, unknown>, binding: AgentMemoryBinding | undefined, workDir?: string): Promise<ToolResult>;
94
+ /**
95
+ * Why an agent run stopped.
96
+ *
97
+ * `'clean'` and `'aborted'` are the two silent-by-design outcomes: the model gave a
98
+ * final answer, or the user pressed Ctrl+C and already knows why it stopped.
99
+ * EVERYTHING else owes the user an explanation, which is what stopReasonNotice covers.
100
+ */
101
+ export type StopReason = 'clean' | 'aborted' | 'reported-elsewhere' | 'empty-response' | 'output-limit' | 'no-balance' | 'no-team-budget' | 'team-unavailable' | 'stalled' | 'stalled-repeat' | 'budget' | 'hook-blocked' | 'unknown';
102
+ /**
103
+ * The message shown when a run ends for any reason other than a clean finish.
104
+ *
105
+ * Pure and exported so every branch is testable: reaching some of these for real needs
106
+ * an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
107
+ * the two outcomes that are deliberately silent.
108
+ *
109
+ * `'unknown'` deliberately produces a message rather than nothing. If a future `break`
110
+ * forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
111
+ * annoying and reportable — not the silent stop that made this refactor necessary.
112
+ */
113
+ export declare function stopReasonNotice(reason: StopReason, ctx?: {
114
+ budget?: number;
115
+ repeatError?: string | null;
116
+ hookReason?: string | null;
117
+ }): string | null;
118
+ export declare const TEAM_SCOPE_ERROR_CODES: ReadonlySet<string>;
119
+ export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
120
+ export declare function resolveAutoContinue(optionValue: boolean | undefined, settingsRaw: Record<string, unknown>): boolean;
121
+ //# sourceMappingURL=iterationPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iterationPolicy.d.ts","sourceRoot":"","sources":["../../src/agent/iterationPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAYrD,eAAO,MAAM,sBAAsB,OAAQ,CAAC;AAU5C,eAAO,MAAM,WAAW,IAAgB,CAAC;AAKzC,eAAO,MAAM,kBAAkB,KAAU,CAAC;AAuB1C,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAKxC,yFAAyF;AACzF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,aAAa,EAAE,MAAM,GACpB,OAAO,CAGT;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe;;;CAAuD,CAAC;AAEpF;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9C,MAAM,CAKR;AAED,+CAA+C;AAC/C,eAAO,MAAM,YAAY;;;CAAsC,CAAC;AAEhE;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;CAgB3B,CAAC;AAEX,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CA8BrB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,SAAS,GACT,oBAAoB,GACpB,gBAAgB,GAChB,cAAc,GACd,YAAY,GACZ,gBAAgB,GAChB,kBAAkB,GAClB,SAAS,GACT,gBAAgB,GAChB,QAAQ,GACR,cAAc,GACd,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACrF,MAAM,GAAG,IAAI,CA4Df;AAMD,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAMrD,CAAC;AAaH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAYR;AAKD,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,OAAO,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAQT"}
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEAM_SCOPE_ERROR_CODES = exports.AGENT_MEMORY_TOOL_SCHEMA = exports.AGENT_MEMORY_TOOL = exports._stallLimits = exports._emptyTurnRetry = exports.EMPTY_TURN_RETRY_LIMIT = exports.REPEAT_STALL_LIMIT = exports.STALL_LIMIT = exports.MAX_ITERATIONS_CEILING = void 0;
4
+ exports.emptyTurnBackoffMs = emptyTurnBackoffMs;
5
+ exports.shouldRetryEmptyTurn = shouldRetryEmptyTurn;
6
+ exports.errorRoundSignature = errorRoundSignature;
7
+ exports.executeAgentMemoryWrite = executeAgentMemoryWrite;
8
+ exports.stopReasonNotice = stopReasonNotice;
9
+ exports.resolveMaxIterations = resolveMaxIterations;
10
+ exports.resolveAutoContinue = resolveAutoContinue;
11
+ const memory_1 = require("./memory");
12
+ // ─── Iteration cap ──────────────────────────────────────────────────────────
13
+ // Each iteration is one model response + one round of tool execution. The cap is
14
+ // a runaway-loop backstop, NOT a task-size limit — on a large project a single
15
+ // task can legitimately need well over 50 rounds (read → search → edit → test →
16
+ // fix → …). A too-low cap makes the agent appear to "freeze" mid-task. Keep the
17
+ // default high and let projects raise it further via settings / env.
18
+ const DEFAULT_MAX_ITERATIONS = 500;
19
+ exports.MAX_ITERATIONS_CEILING = 2000; // default auto-continue backstop (no explicit opt-in)
20
+ // There is deliberately NO hard ceiling on an explicit opt-in anymore: a task meant to run
21
+ // for days/weeks/months (an unattended agent loop) must not die at an arbitrary iteration
22
+ // count just because someone picked a big-but-finite safety number in the past. The actual
23
+ // protection against a runaway session burning cost forever is STALL_LIMIT /
24
+ // REPEAT_STALL_LIMIT below — those catch "stuck", not "long", and fire in a handful of
25
+ // rounds regardless of how high maxIterations is set. `Infinity` here is a real, intentional
26
+ // value (not a bug) — resolveMaxIterations() below returns it whenever the caller does not
27
+ // explicitly opt in to a finite number, matching the wording "unbounded unless you cap it".
28
+ const HARD_ITERATIONS_CAP = Infinity; // no absolute cap — bounded only by the stall guards
29
+ exports.STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runaway guard)
30
+ // Consecutive rounds producing the IDENTICAL error(s) → give up, even if other calls in
31
+ // those rounds succeeded. Higher than STALL_LIMIT because a repeat is weaker evidence of
32
+ // being stuck than a total failure: legitimately retrying one failing command a few times
33
+ // while making progress elsewhere is normal, twelve times is not.
34
+ exports.REPEAT_STALL_LIMIT = 12;
35
+ // ─── Empty-turn auto-retry ────────────────────────────────────────────────────
36
+ //
37
+ // A model turn can come back STRUCTURALLY FINE (the stream completed, `message_complete`
38
+ // arrived) and still contain nothing usable once thinking blocks are stripped for
39
+ // history. Anthropic models hit this materially more often than the OpenAI-compatible
40
+ // ones, because only they emit `thinking`/`redacted_thinking` blocks — a turn that
41
+ // reasons and then ends without committing text or a tool call strips down to `[]`.
42
+ //
43
+ // The transport layer cannot fix this. client.ts only retries when the stream produced
44
+ // NOTHING; here the stream produced a valid message that happens to be empty, so it
45
+ // resolves normally and the loop used to break immediately and tell the user to type
46
+ // "continue" — asking a human to press a button the loop can press itself.
47
+ //
48
+ // Retrying here is safe for a specific, checkable reason, not a hopeful one:
49
+ // • tools are dispatched from `assistantMessage.content` AFTER this check, and this
50
+ // content is empty, so no tool ran and no file changed;
51
+ // • nothing was pushed to `messages` (the push happens below this branch), so the
52
+ // history is byte-identical to the previous attempt — a retry is a clean re-ask,
53
+ // not a resend of a corrupted turn;
54
+ // • no round was counted, no hook fired, no checkpoint was taken.
55
+ // Contrast the output-limit case, which is NOT retried: see shouldRetryEmptyTurn.
56
+ exports.EMPTY_TURN_RETRY_LIMIT = 3;
57
+ // Short, escalating pause (1s, 2s, 4s). Long enough to ride out the upstream blip that
58
+ // causes this; short enough that three failures cost ~7s rather than a visible stall.
59
+ const EMPTY_TURN_RETRY_BASE_MS = 1000;
60
+ /** Backoff for the Nth (0-based) empty-turn retry. Pure, so the schedule is testable. */
61
+ function emptyTurnBackoffMs(attempt) {
62
+ return EMPTY_TURN_RETRY_BASE_MS * Math.pow(2, Math.max(0, attempt));
63
+ }
64
+ /**
65
+ * Should an empty assistant turn be retried automatically, rather than surfaced?
66
+ *
67
+ * Split out as a pure function because the two "empty" causes look IDENTICAL at the
68
+ * call site (both are `content.length === 0`) and telling them apart is the entire
69
+ * point — the previous bug in this area was treating a deterministic truncation as a
70
+ * transient hiccup and advising a retry that reproduced it verbatim.
71
+ *
72
+ * • `max_tokens` → the model burned its whole output budget on reasoning and was CUT
73
+ * OFF. Retrying re-runs the same prompt with the same budget and the same reasoning
74
+ * behaviour, so it fails the same way while billing again. Never retried; the user
75
+ * is told to lower effort or split the task (stopReasonNotice 'output-limit').
76
+ * • anything else → a genuinely transient empty turn. Retried, up to the limit.
77
+ *
78
+ * The attempt cap matters as much as the classification: a model that has decided to
79
+ * return nothing (e.g. a prompt it refuses to continue) would otherwise loop forever
80
+ * on a paid endpoint. After the cap we fall back to the old visible notice.
81
+ */
82
+ function shouldRetryEmptyTurn(stopReason, attemptsSoFar) {
83
+ if (stopReason === 'max_tokens')
84
+ return false;
85
+ return attemptsSoFar < exports.EMPTY_TURN_RETRY_LIMIT;
86
+ }
87
+ /** Empty-turn retry limits, exposed for tests. */
88
+ exports._emptyTurnRetry = { EMPTY_TURN_RETRY_LIMIT: exports.EMPTY_TURN_RETRY_LIMIT, EMPTY_TURN_RETRY_BASE_MS };
89
+ /**
90
+ * Fingerprint one round's tool failures, for the repeated-failure runaway guard.
91
+ *
92
+ * Exported (with the limits) purely as a test seam: the guard's whole value is in the
93
+ * edge cases — that a DIFFERENT error each round must NOT trip it, that call order
94
+ * within a round is irrelevant, that a long error body doesn't make every occurrence
95
+ * look unique — and none of that is reachable without driving a live model loop.
96
+ *
97
+ * Sorted so parallel tool calls completing in a different order still compare equal;
98
+ * truncated because errors often embed a varying path or timestamp late in the string.
99
+ */
100
+ function errorRoundSignature(errored) {
101
+ return errored
102
+ .map(({ name, error }) => `${name}:${String(error).slice(0, 200)}`)
103
+ .sort()
104
+ .join('|');
105
+ }
106
+ /** Runaway-guard limits, exposed for tests. */
107
+ exports._stallLimits = { STALL_LIMIT: exports.STALL_LIMIT, REPEAT_STALL_LIMIT: exports.REPEAT_STALL_LIMIT };
108
+ /**
109
+ * The single tool granted by a `memory:` frontmatter scope.
110
+ *
111
+ * Named distinctly from `memory_write` on purpose: they write to DIFFERENT stores, and
112
+ * a model that saw one name for both would reasonably assume its notes were visible to
113
+ * the main agent. They are not.
114
+ */
115
+ exports.AGENT_MEMORY_TOOL = 'agent_memory_write';
116
+ /**
117
+ * Schema advertised to the model, only for a sub-agent with a declared memory scope.
118
+ *
119
+ * The description does the load-bearing work of keeping the two stores apart in the
120
+ * model's head: it must not believe these notes reach the user or the main agent.
121
+ */
122
+ exports.AGENT_MEMORY_TOOL_SCHEMA = {
123
+ name: exports.AGENT_MEMORY_TOOL,
124
+ description: 'Save a durable note to YOUR OWN persistent notes, which are injected into your prompt on ' +
125
+ 'future runs. Use this for lessons that will still be true next time — a convention this repo ' +
126
+ 'follows, a recurring bug pattern, a command that works, a dead end not worth retrying. ' +
127
+ 'These notes are PRIVATE to you: they are NOT shown to the user and NOT read by the main agent, ' +
128
+ 'so anything the user or the main agent needs to know must still go in your final message. ' +
129
+ 'One self-contained fact per call, a sentence or two. Do not save transient task details.',
130
+ input_schema: {
131
+ type: 'object',
132
+ properties: {
133
+ content: { type: 'string', description: 'The single fact to remember, 1-2 sentences.' },
134
+ },
135
+ required: ['content'],
136
+ },
137
+ };
138
+ /**
139
+ * Execute an `agent_memory_write` call.
140
+ *
141
+ * Pure-ish and exported so the refusal paths are testable without spawning a real
142
+ * sub-agent: the binding is data, so "no binding" and "unsafe agent name" can both be
143
+ * exercised directly.
144
+ */
145
+ async function executeAgentMemoryWrite(input, binding, workDir) {
146
+ // Belt-and-braces: the permission gate already refuses this tool without a binding.
147
+ // Re-checked because this is the function that actually touches the filesystem, and
148
+ // it must not depend on a caller elsewhere having got the check right.
149
+ if (!binding) {
150
+ return {
151
+ error: `${exports.AGENT_MEMORY_TOOL} is only available to a sub-agent whose definition declares a \`memory:\` ` +
152
+ 'scope. Put anything worth remembering in your final message instead.',
153
+ };
154
+ }
155
+ const content = typeof input.content === 'string' ? input.content.trim() : '';
156
+ if (!content)
157
+ return { error: `${exports.AGENT_MEMORY_TOOL} requires a non-empty \`content\` string.` };
158
+ const res = await (0, memory_1.writeAgentMemory)(binding.agentName, binding.scope, content, workDir);
159
+ if (!res.ok) {
160
+ // The realistic cause is a repo-scoped store with no workDir, or an agent name that
161
+ // is not a safe single path segment. Say which, rather than a bare failure.
162
+ return {
163
+ error: `Could not save to the "${binding.agentName}" agent's ${binding.scope} notes. ` +
164
+ 'Either this scope needs a project directory (use `memory: user` for a store that ' +
165
+ 'works anywhere) or the agent name is not usable as a filename.',
166
+ };
167
+ }
168
+ return {
169
+ output: res.already
170
+ ? 'Already saved (a near-identical note exists) — nothing added.'
171
+ : `Saved to your ${binding.scope} notes. It will be in your prompt on your next run.`,
172
+ };
173
+ }
174
+ /**
175
+ * The message shown when a run ends for any reason other than a clean finish.
176
+ *
177
+ * Pure and exported so every branch is testable: reaching some of these for real needs
178
+ * an empty wallet, a dead upstream, or hundreds of iterations. Returns null only for
179
+ * the two outcomes that are deliberately silent.
180
+ *
181
+ * `'unknown'` deliberately produces a message rather than nothing. If a future `break`
182
+ * forgets to set a reason, the symptom should be a visible "ended unexpectedly" line —
183
+ * annoying and reportable — not the silent stop that made this refactor necessary.
184
+ */
185
+ function stopReasonNotice(reason, ctx = {}) {
186
+ switch (reason) {
187
+ case 'clean':
188
+ case 'aborted':
189
+ // A dedicated channel (e.g. the zero-balance bubble via onBalanceStatus) has already
190
+ // told the user why this stopped. Naming this case explicitly — rather than reusing
191
+ // 'aborted' — keeps "the user cancelled" from silently coming to mean two things.
192
+ case 'reported-elsewhere':
193
+ return null;
194
+ // Reaching this notice now means the loop ALREADY retried automatically and the
195
+ // model came back empty every time (see shouldRetryEmptyTurn). Saying "send
196
+ // continue to retry" without that context reads as if nothing was tried, and the
197
+ // user's manual retry is then the fourth identical attempt — so name the attempts.
198
+ case 'empty-response':
199
+ return `\n\u26a0\ufe0f The model returned an empty response ${exports.EMPTY_TURN_RETRY_LIMIT} times in a row, so nothing was done. ` +
200
+ `This is usually a transient upstream hiccup that the agent retries by itself; it did not clear this time. ` +
201
+ `Send "continue" to try again, or switch model with /model if it persists.\n`;
202
+ // Deliberately NOT folded into 'empty-response'. Both arrive as an assistant
203
+ // message with no content once thinking blocks are stripped, so they used to
204
+ // be indistinguishable — and the user was told the truncation case was "a
205
+ // transient upstream hiccup" they should retry with "continue". Both halves
206
+ // were wrong: nothing was transient, and continuing re-runs the same prompt
207
+ // with the same budget and the same reasoning behaviour, reproducing it
208
+ // exactly. Naming the cause is the fix; the advice has to change with it.
209
+ case 'output-limit':
210
+ return `\n\u26a0\ufe0f The model used its entire output budget on internal reasoning and was cut off ` +
211
+ `before it could reply, so nothing was done. This will repeat identically if you just retry \u2014 ` +
212
+ `lower the reasoning effort (/effort) or split the task into smaller steps.\n`;
213
+ case 'no-balance':
214
+ return `\n\ud83d\udcb3 Stopped: your balance is empty, so the request was rejected before it started. ` +
215
+ `Top up and send "continue" \u2014 no tokens were used for this turn.\n`;
216
+ case 'no-team-budget':
217
+ return `\n\ud83d\udcb3 Stopped: the team budget you are billing to is empty, so the request was rejected ` +
218
+ `before it started. Ask your team admin to add funds \u2014 or switch "Bill to" back to Personal \u2014 ` +
219
+ `and send "continue". No tokens were used for this turn.\n`;
220
+ case 'team-unavailable':
221
+ return `\n\u26a0\ufe0f Stopped: that team is no longer available for billing (you may have been removed, or the ` +
222
+ `team was suspended). "Bill to" has been reset to your Personal account, so sending "continue" will ` +
223
+ `run on your own wallet.\n`;
224
+ case 'stalled':
225
+ return `\n\ud83d\uded1 Stopped: the last ${exports.STALL_LIMIT} tool rounds all failed, so the agent looked stuck. ` +
226
+ `Fix the underlying error (or grant the needed permission) and send "continue".\n`;
227
+ case 'stalled-repeat':
228
+ return `\n\ud83d\uded1 Stopped: the same tool error repeated ${exports.REPEAT_STALL_LIMIT} rounds in a row, so the agent ` +
229
+ `was looping without making progress.` +
230
+ (ctx.repeatError ? ` The recurring error was:\n${ctx.repeatError}\n` : '\n') +
231
+ `Fix that underlying cause (or grant the needed permission) and send "continue".\n`;
232
+ case 'hook-blocked':
233
+ return `\n\u26d4 A PostToolBatch hook stopped the turn before the next model call` +
234
+ (ctx.hookReason ? `: ${ctx.hookReason}` : '.') +
235
+ `\nThe reason is in the conversation, so send "continue" once the cause is handled.\n`;
236
+ case 'budget':
237
+ return `\n\u23f8\ufe0f Stopped at the ${ctx.budget}-step safety limit \u2014 the task may be incomplete. ` +
238
+ `Send "continue" to resume, or raise the limit via "maxIterations" in .nexrall/settings.json ` +
239
+ `(or the NEXRALL_MAX_ITERATIONS env var). Auto-continue can be disabled with "autoContinue": false.\n`;
240
+ case 'unknown':
241
+ default:
242
+ return `\n\u26a0\ufe0f The run ended unexpectedly without completing. Your work so far is preserved \u2014 ` +
243
+ `send "continue" to resume.\n`;
244
+ }
245
+ }
246
+ // Error codes that mean "this team cannot pay for anything right now" — the
247
+ // pre-flight refusals routes/code.js returns when a request names a team
248
+ // (migration 139). Kept as one list so the loop's auto-fallback and the server's
249
+ // vocabulary cannot drift apart silently.
250
+ exports.TEAM_SCOPE_ERROR_CODES = new Set([
251
+ 'team_unavailable',
252
+ 'team_not_found',
253
+ 'not_a_member',
254
+ 'member_suspended',
255
+ 'team_suspended',
256
+ ]);
257
+ // Resolve the soft iteration budget. Precedence:
258
+ // options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
259
+ //
260
+ // The DEFAULT (nothing set) is clamped to MAX_ITERATIONS_CEILING so an ordinary
261
+ // run can never spin past 2000 rounds by accident. But an EXPLICIT value from
262
+ // any of the three opt-in channels is honoured with NO upper bound (HARD_ITERATIONS_CAP
263
+ // is Infinity) — this is what lets a genuinely unattended, long-lived task (an agent meant
264
+ // to keep working for days or longer) run for as many rounds as it needs once the user has
265
+ // deliberately asked for that, instead of dying at a hidden ceiling while the error message
266
+ // misleadingly tells them to "raise the limit". A stuck/looping run is still caught by the
267
+ // STALL_LIMIT / REPEAT_STALL_LIMIT guards below, independent of this budget.
268
+ function resolveMaxIterations(optionValue, settingsRaw) {
269
+ const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
270
+ const fromSettings = Number(settingsRaw.maxIterations);
271
+ const explicit = (typeof optionValue === 'number' && optionValue > 0) ? optionValue
272
+ : Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv
273
+ : Number.isFinite(fromSettings) && fromSettings > 0 ? fromSettings
274
+ : undefined;
275
+ if (explicit === undefined)
276
+ return DEFAULT_MAX_ITERATIONS;
277
+ // Explicit opt-in: honour it verbatim (floor for a fractional value from JSON/env), no
278
+ // longer bounded by an absolute safety cap — see HARD_ITERATIONS_CAP's comment above.
279
+ return Math.min(Math.floor(explicit), HARD_ITERATIONS_CAP);
280
+ }
281
+ // When the soft budget is exhausted with work still pending, keep going instead
282
+ // of stopping. Precedence: options.autoContinue → env NEXRALL_AUTO_CONTINUE →
283
+ // settings.autoContinue → default (on). Bounded by STALL_LIMIT and the ceiling.
284
+ function resolveAutoContinue(optionValue, settingsRaw) {
285
+ if (typeof optionValue === 'boolean')
286
+ return optionValue;
287
+ const env = process.env.NEXRALL_AUTO_CONTINUE;
288
+ if (env === '0' || env === 'false')
289
+ return false;
290
+ if (env === '1' || env === 'true')
291
+ return true;
292
+ const s = settingsRaw.autoContinue;
293
+ if (typeof s === 'boolean')
294
+ return s;
295
+ return true;
296
+ }
297
+ //# sourceMappingURL=iterationPolicy.js.map
@@ -0,0 +1,55 @@
1
+ /** One instruction file that was loaded, told to the InstructionsLoaded hooks. */
2
+ export interface LoadedInstruction {
3
+ file_path: string;
4
+ /** Claude Code's memory_type vocabulary, mapped onto nex's files (see the CLI caller). */
5
+ memory_type: 'User' | 'Project' | 'Local' | 'Managed';
6
+ /** Claude Code's load_reason vocabulary: session_start | compact | include | nested_traversal | path_glob_match. */
7
+ load_reason: string;
8
+ globs?: string[];
9
+ }
10
+ /**
11
+ * Tell the InstructionsLoaded hooks which instruction files this session loaded.
12
+ * Observer: output and exit code are ignored (Claude Code says the same for this event).
13
+ *
14
+ * Called by the host at the moment the files are actually read — a `--bare` run reads
15
+ * none and therefore fires nothing, which is the honest behaviour.
16
+ */
17
+ export declare function fireInstructionsLoadedHooks(workDir: string, sessionId: string, files: LoadedInstruction[]): Promise<void>;
18
+ export interface FileChangedWatcher {
19
+ /** Stop watching and drop any pending debounce timer. Safe to call twice. */
20
+ stop(): void;
21
+ /** Replace the DYNAMIC watch list (a hook's watchPaths answer). Static matcher names stay. */
22
+ updateWatchPaths(paths: string[]): void;
23
+ /** What the watcher is currently listening for — exposed for tests and `nex doctor`. */
24
+ watchedFiles(): string[];
25
+ }
26
+ /**
27
+ * Start watching the files named by the FileChanged hooks' matchers ("a.txt|b.txt").
28
+ *
29
+ * - Returns null when no FileChanged hook is configured, so callers can wire it
30
+ * unconditionally.
31
+ * - `fs.watch` on the WORKING DIRECTORY (not the files themselves: editors replace
32
+ * files, which kills a per-file watch on rename) filtered to the watched basenames,
33
+ * with a short per-file debounce — one save is several fs events.
34
+ * - A hook's `{"watchPaths": […]}` answer replaces the dynamic list; `{"systemMessage": "…"}`
35
+ * is handed to `onSystemMessage` for the host to display.
36
+ * - Never throws into the host: watching is best-effort (an OS without inotify, a
37
+ * directory that vanishes) and a broken watch must not break the session.
38
+ */
39
+ export declare function startFileChangedWatcher(workDir: string, sessionId: string, opts?: {
40
+ onSystemMessage?: (message: string) => void;
41
+ /**
42
+ * How long to wait before the arming reconcile pass looks for a write the OS stream
43
+ * may have missed because it was not armed yet (see `stamp` below). 0 disables it;
44
+ * tests set it low, the default is tuned for a real save landing right at session start.
45
+ */
46
+ armGraceMs?: number;
47
+ }): FileChangedWatcher | null;
48
+ /**
49
+ * Which watched files differ from the snapshot taken when the watcher armed. This is the
50
+ * whole substance of the arming reconcile: fs.watch arms asynchronously, so a write that
51
+ * lands before the stream is listening is not queued — it is gone. Exported so the
52
+ * comparison can be tested without racing an OS event.
53
+ */
54
+ export declare function filesChangedSinceSnapshot(names: Iterable<string>, seen: Map<string, string>, stamp: (name: string) => string): string[];
55
+ //# sourceMappingURL=lifecycleHost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycleHost.d.ts","sourceRoot":"","sources":["../../src/agent/lifecycleHost.ts"],"names":[],"mappings":"AAeA,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACtD,oHAAoH;IACpH,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,iBAAiB,EAAE,GACzB,OAAO,CAAC,IAAI,CAAC,CAef;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,IAAI,IAAI,IAAI,CAAC;IACb,8FAA8F;IAC9F,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,wFAAwF;IACxF,YAAY,IAAI,MAAM,EAAE,CAAC;CAC1B;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IACL,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,kBAAkB,GAAG,IAAI,CA+H3B;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAC9B,MAAM,EAAE,CAIV"}