@nexrall/code-core 1.4.66 → 1.4.67
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.
- package/dist/agent/agentTypes.d.ts +6 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +13 -4
- package/dist/agent/askOnce.d.ts +12 -0
- package/dist/agent/askOnce.d.ts.map +1 -0
- package/dist/agent/askOnce.js +41 -0
- package/dist/agent/compaction.d.ts +244 -0
- package/dist/agent/compaction.d.ts.map +1 -0
- package/dist/agent/compaction.js +976 -0
- package/dist/agent/fileLocks.d.ts +34 -0
- package/dist/agent/fileLocks.d.ts.map +1 -0
- package/dist/agent/fileLocks.js +114 -0
- package/dist/agent/hooks.d.ts +324 -0
- package/dist/agent/hooks.d.ts.map +1 -0
- package/dist/agent/hooks.js +1228 -0
- package/dist/agent/iterationPolicy.d.ts +121 -0
- package/dist/agent/iterationPolicy.d.ts.map +1 -0
- package/dist/agent/iterationPolicy.js +297 -0
- package/dist/agent/lifecycleHost.d.ts +55 -0
- package/dist/agent/lifecycleHost.d.ts.map +1 -0
- package/dist/agent/lifecycleHost.js +294 -0
- package/dist/agent/loop.d.ts +11 -491
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +417 -3031
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +1 -0
- package/dist/agent/sharedTasks.d.ts +7 -0
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +16 -0
- package/dist/agent/subAgentBudget.d.ts +65 -0
- package/dist/agent/subAgentBudget.d.ts.map +1 -0
- package/dist/agent/subAgentBudget.js +269 -0
- package/dist/agent/subTask.d.ts +6 -0
- package/dist/agent/subTask.d.ts.map +1 -0
- package/dist/agent/subTask.js +713 -0
- package/dist/agent/subTaskSupport.d.ts +156 -0
- package/dist/agent/subTaskSupport.d.ts.map +1 -0
- package/dist/agent/subTaskSupport.js +409 -0
- package/dist/agent/toolDescriptions.d.ts +3 -0
- package/dist/agent/toolDescriptions.d.ts.map +1 -0
- package/dist/agent/toolDescriptions.js +116 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/mcp/client.d.ts +104 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +136 -2
- package/dist/mcp/httpClient.d.ts +17 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +120 -19
- package/dist/mcp/manager.d.ts +77 -2
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +275 -9
- package/dist/mcp/sseClient.d.ts +7 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +45 -1
- package/dist/mcp/stats.d.ts +41 -0
- package/dist/mcp/stats.d.ts.map +1 -0
- package/dist/mcp/stats.js +108 -0
- package/dist/permissions/destructive.d.ts +2 -0
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +6 -2
- package/dist/permissions/destructiveTokens.d.ts +5 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +9 -3
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +5 -2
- package/dist/permissions/rules.d.ts +4 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +29 -0
- package/dist/types.d.ts +45 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"planMode.d.ts","sourceRoot":"","sources":["../../src/agent/planMode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"planMode.d.ts","sourceRoot":"","sources":["../../src/agent/planMode.ts"],"names":[],"mappings":"AAiJA,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,MAAM,EAAE,eAAe,GAAG,oBAAoB,CAAC;IAC/C,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;CACjB;AAqFD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CA8D1D;AAQD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO3D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,eAAe,GAAG,IAAI,CAoCxB;AAED,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,QAqBvB,CAAC"}
|
package/dist/agent/planMode.js
CHANGED
|
@@ -36,6 +36,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
36
36
|
// list, not the repo, so it stays available — a plan mode that cannot draft
|
|
37
37
|
// a checklist is missing the point of plan mode.
|
|
38
38
|
'todo_write', 'todo_read',
|
|
39
|
+
'cron_list', 'mcp_list_resources', 'mcp_read_resource',
|
|
39
40
|
// Reading memory is fine; memory_write is NOT here on purpose. Memory is
|
|
40
41
|
// durable state that survives the session, so writing it is a real mutation
|
|
41
42
|
// even though no file in the repo changes.
|
|
@@ -59,5 +59,12 @@ export declare const _sharedTasksInternal: {
|
|
|
59
59
|
CLAIM_STALE_MS: number;
|
|
60
60
|
MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS: number;
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Remove a task outright — the veto path for a `TaskCreated` hook. Claude Code creates the
|
|
64
|
+
* task, and when a hook refuses it the task is DELETED and the reason goes back to the
|
|
65
|
+
* model as the tool's error; so "no task remains" is the observable contract of a veto,
|
|
66
|
+
* and this is the only place that needs to make it true.
|
|
67
|
+
*/
|
|
68
|
+
export declare function deleteSharedTask(workDir: string, taskId: string): boolean;
|
|
62
69
|
export {};
|
|
63
70
|
//# sourceMappingURL=sharedTasks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharedTasks.d.ts","sourceRoot":"","sources":["../../src/agent/sharedTasks.ts"],"names":[],"mappings":"AA6DA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,4HAA4H;IAC5H,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD,iBAAS,SAAS,IAAI,MAAM,CAE3B;AAED,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGzC;AAED,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzD;AA4BD,0HAA0H;AAC1H,iBAAS,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAUnD;AAED,4GAA4G;AAC5G,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAG/D;AAED,0IAA0I;AAC1I,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAW7D;AAED,sJAAsJ;AACtJ,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC/C,UAAU,CAUZ;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,GAAG,SAAS,GAAG,aAAa,CAAA;CAAE,CAAC;AAEvF;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BxG;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrF,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED,eAAO,MAAM,oBAAoB;;;;;;;;CAAqH,CAAC"}
|
|
1
|
+
{"version":3,"file":"sharedTasks.d.ts","sourceRoot":"","sources":["../../src/agent/sharedTasks.ts"],"names":[],"mappings":"AA6DA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,4HAA4H;IAC5H,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD,iBAAS,SAAS,IAAI,MAAM,CAE3B;AAED,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGzC;AAED,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzD;AA4BD,0HAA0H;AAC1H,iBAAS,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAUnD;AAED,4GAA4G;AAC5G,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAG/D;AAED,0IAA0I;AAC1I,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAW7D;AAED,sJAAsJ;AACtJ,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC/C,UAAU,CAUZ;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,GAAG,SAAS,GAAG,aAAa,CAAA;CAAE,CAAC;AAEvF;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BxG;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrF,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED,eAAO,MAAM,oBAAoB;;;;;;;;CAAqH,CAAC;AAEvJ;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzE"}
|
|
@@ -38,6 +38,7 @@ exports.listSharedTasks = listSharedTasks;
|
|
|
38
38
|
exports.createSharedTask = createSharedTask;
|
|
39
39
|
exports.claimTask = claimTask;
|
|
40
40
|
exports.updateSharedTaskStatus = updateSharedTaskStatus;
|
|
41
|
+
exports.deleteSharedTask = deleteSharedTask;
|
|
41
42
|
/**
|
|
42
43
|
* Shared task list — lets MULTIPLE independent sessions (two CLI windows, or
|
|
43
44
|
* CLI + VS Code + Desktop, all working the same project) coordinate who is
|
|
@@ -281,4 +282,19 @@ async function updateSharedTaskStatus(workDir, taskId, status, opts = {}) {
|
|
|
281
282
|
}, workDir);
|
|
282
283
|
}
|
|
283
284
|
exports._sharedTasksInternal = { tasksRoot, scopeDir, taskPath, isClaimAbandoned, isUnblocked, CLAIM_STALE_MS, MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS };
|
|
285
|
+
/**
|
|
286
|
+
* Remove a task outright — the veto path for a `TaskCreated` hook. Claude Code creates the
|
|
287
|
+
* task, and when a hook refuses it the task is DELETED and the reason goes back to the
|
|
288
|
+
* model as the tool's error; so "no task remains" is the observable contract of a veto,
|
|
289
|
+
* and this is the only place that needs to make it true.
|
|
290
|
+
*/
|
|
291
|
+
function deleteSharedTask(workDir, taskId) {
|
|
292
|
+
try {
|
|
293
|
+
fs.unlinkSync(taskPath(workDir, taskId));
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
284
300
|
//# sourceMappingURL=sharedTasks.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the fan-out limit: env → settings.json → default.
|
|
3
|
+
*
|
|
4
|
+
* Was env-ONLY, the same gap as the sub-task timeout: a project on a small machine that
|
|
5
|
+
* wanted 2, or a big one that wanted 6, had to set a shell variable, and nobody reading
|
|
6
|
+
* settings.json could see what the limit even was. Capped at HARD_MAX_CONCURRENT_SUBTASKS
|
|
7
|
+
* (32 — above Claude Code's default 20, so a user who wants that width can have it)
|
|
8
|
+
* because this bounds real shared resources (CPU, the API rate limit, file handles) and a
|
|
9
|
+
* typo like 400 should degrade to "a lot" rather than fork-bomb the machine.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveMaxConcurrentSubtasks(settingsRaw?: Record<string, unknown>): number;
|
|
12
|
+
/** Resolve the session-total sub-agent ceiling: env → settings.json → default. */
|
|
13
|
+
export declare function resolveMaxSubagentsPerSession(settingsRaw?: Record<string, unknown>): number;
|
|
14
|
+
/**
|
|
15
|
+
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
16
|
+
* the CLI ships as a single esbuild bundle with no node_modules, and this is a
|
|
17
|
+
* dozen lines.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
20
|
+
/** In-flight count per depth — used only to detect queueing, so the notice is accurate. */
|
|
21
|
+
export declare const _inFlightByDepth: Map<number, number>;
|
|
22
|
+
export declare function subTaskLimiter(depth: number, workDir?: string): {
|
|
23
|
+
run: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
24
|
+
max: number;
|
|
25
|
+
};
|
|
26
|
+
/** Test-only: forget the memoised limiter so a new limit can take effect. */
|
|
27
|
+
export declare function _resetSubTaskLimiter(): void;
|
|
28
|
+
export declare const PROCESS_BUDGET_KEY = "\0process";
|
|
29
|
+
/**
|
|
30
|
+
* Start a fresh sub-agent budget. Call when a NEW conversation begins.
|
|
31
|
+
*
|
|
32
|
+
* Exported for clients that reuse one process across conversations (the VS Code extension
|
|
33
|
+
* host). A client that never calls it gets process-lifetime semantics, which is correct
|
|
34
|
+
* for a one-shot CLI invocation.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resetSessionSubAgentBudget(sessionKey?: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Claim one slot against the session total. Returns an error string when exhausted.
|
|
39
|
+
*
|
|
40
|
+
* `notify` surfaces exhaustion to the HUMAN exactly once. Without it only the model is
|
|
41
|
+
* told, and a model instructed to "do the remaining work directly" complies silently — so
|
|
42
|
+
* the user never learns delegation was capped, which is the one thing a runaway guard has
|
|
43
|
+
* to make visible.
|
|
44
|
+
*/
|
|
45
|
+
export declare function claimSessionSubAgentSlot(workDir?: string, notify?: (msg: string) => void, sessionKey?: string): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Give back a slot claimed for a spawn that never started an agent.
|
|
48
|
+
*
|
|
49
|
+
* The claim happens at the dispatch site, BEFORE the concurrency limiter, so that a spawn
|
|
50
|
+
* which is already over budget is refused immediately instead of queueing behind running
|
|
51
|
+
* siblings. That ordering is what makes the guard useful in a runaway — but it also means
|
|
52
|
+
* the claim precedes runSubTask's own validation, which rejects on several paths without
|
|
53
|
+
* ever starting a loop (empty prompt, a `deny` rule, an unusable resume id).
|
|
54
|
+
*
|
|
55
|
+
* Without a refund those rejections spend budget: a model retrying against a deny rule would
|
|
56
|
+
* silently burn the whole allowance on spawns that never ran, then lose delegation for the
|
|
57
|
+
* session with a notice blaming a runaway that never happened. The counter's contract is
|
|
58
|
+
* "sub-agents STARTED", and this is what keeps that true while preserving fast refusal.
|
|
59
|
+
*
|
|
60
|
+
* Floored at 0 so a double refund can never manufacture budget.
|
|
61
|
+
*/
|
|
62
|
+
export declare function refundSessionSubAgentSlot(sessionKey?: string): void;
|
|
63
|
+
/** Test-only: observe the session counter without exporting the mutable binding. */
|
|
64
|
+
export declare function _sessionSubAgentCount(sessionKey?: string): number;
|
|
65
|
+
//# sourceMappingURL=subAgentBudget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subAgentBudget.d.ts","sourceRoot":"","sources":["../../src/agent/subAgentBudget.ts"],"names":[],"mappings":"AAyCA;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAW9F;AAoBD,kFAAkF;AAClF,wBAAgB,6BAA6B,CAAC,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAa/F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAgBlF;AA8BD,2FAA2F;AAC3F,eAAO,MAAM,gBAAgB,qBAA4B,CAAC;AAE1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG;IAC/D,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7C,GAAG,EAAE,MAAM,CAAC;CACb,CAuBA;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,IAAI,IAAI,CAM3C;AAkBD,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAQ9C;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAGpE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7H;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAGnE;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROCESS_BUDGET_KEY = exports._inFlightByDepth = void 0;
|
|
4
|
+
exports.resolveMaxConcurrentSubtasks = resolveMaxConcurrentSubtasks;
|
|
5
|
+
exports.resolveMaxSubagentsPerSession = resolveMaxSubagentsPerSession;
|
|
6
|
+
exports.createLimiter = createLimiter;
|
|
7
|
+
exports.subTaskLimiter = subTaskLimiter;
|
|
8
|
+
exports._resetSubTaskLimiter = _resetSubTaskLimiter;
|
|
9
|
+
exports.resetSessionSubAgentBudget = resetSessionSubAgentBudget;
|
|
10
|
+
exports.claimSessionSubAgentSlot = claimSessionSubAgentSlot;
|
|
11
|
+
exports.refundSessionSubAgentSlot = refundSessionSubAgentSlot;
|
|
12
|
+
exports._sessionSubAgentCount = _sessionSubAgentCount;
|
|
13
|
+
const rules_1 = require("../permissions/rules");
|
|
14
|
+
// ─── Sub-agent fan-out limiter ────────────────────────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// Tool calls in one turn run via Promise.all with no ceiling. For ordinary tools
|
|
17
|
+
// that is right — they're cheap and mostly I/O — but a `task` call spawns a WHOLE
|
|
18
|
+
// nested agent loop: its own model stream, its own tool executions, its own
|
|
19
|
+
// sub-process spawns. A model that emits ten `task` blocks in one turn therefore
|
|
20
|
+
// starts ten concurrent agents, each billing tokens and competing for the same
|
|
21
|
+
// CPU, file handles and API rate limit. The practical symptoms are the ones users
|
|
22
|
+
// report as "it got slow and then stalled": every sub-agent's stream slows, some
|
|
23
|
+
// trip their own stall watchdog, and one shared rate limit is spread across ten
|
|
24
|
+
// callers.
|
|
25
|
+
//
|
|
26
|
+
// Anthropic hit the same wall and capped Claude Code's concurrent subagents at 20
|
|
27
|
+
// (v2.1.217, July 2026, CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS); community guidance for
|
|
28
|
+
// everyday work settles around 3-5 because past that the synthesis overhead cancels
|
|
29
|
+
// the parallelism.
|
|
30
|
+
//
|
|
31
|
+
// Default 10, raised from 4 (2026-09-29), with a hard ceiling of 32 (see below). Measured
|
|
32
|
+
// on a 12-core dev box against the Nexrall repo, the per-agent latency of a
|
|
33
|
+
// search_files + glob pair was 116 ms at N=4, 176 ms at N=10, 300 ms at N=20 and 415 ms
|
|
34
|
+
// at N=30. Local CPU is NOT what limits fan-out; the model API is (rounds are seconds
|
|
35
|
+
// long, tools are milliseconds). What did limit it was the SERVER: /api/code was IP
|
|
36
|
+
// rate-limited at 100 req/15 min, which even 4 agents exhausted. That is now per-user
|
|
37
|
+
// (backend shared/utils/codeRateLimit.js), so the old "4" no longer protects anything
|
|
38
|
+
// that 10 does not.
|
|
39
|
+
//
|
|
40
|
+
// Why not default 20/30 like the headline number: the default applies to EVERY user, on
|
|
41
|
+
// every model, including ones with a low per-key TPM, and each concurrent agent is its
|
|
42
|
+
// own bill. Wide fan-out is opt-in (maxConcurrentSubtasks / NEXRALL_MAX_CONCURRENT_SUBTASKS
|
|
43
|
+
// up to 32), narrow fan-out is the safe default.
|
|
44
|
+
//
|
|
45
|
+
// This is a QUEUE, not a rejection: every sub-task still runs, just at most N at a
|
|
46
|
+
// time. Failing the excess would be worse than serialising it.
|
|
47
|
+
const DEFAULT_MAX_CONCURRENT_SUBTASKS = 10;
|
|
48
|
+
/** Hard ceiling on maxConcurrentSubtasks, whatever settings.json (repo-controlled) says. */
|
|
49
|
+
const HARD_MAX_CONCURRENT_SUBTASKS = 32;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the fan-out limit: env → settings.json → default.
|
|
52
|
+
*
|
|
53
|
+
* Was env-ONLY, the same gap as the sub-task timeout: a project on a small machine that
|
|
54
|
+
* wanted 2, or a big one that wanted 6, had to set a shell variable, and nobody reading
|
|
55
|
+
* settings.json could see what the limit even was. Capped at HARD_MAX_CONCURRENT_SUBTASKS
|
|
56
|
+
* (32 — above Claude Code's default 20, so a user who wants that width can have it)
|
|
57
|
+
* because this bounds real shared resources (CPU, the API rate limit, file handles) and a
|
|
58
|
+
* typo like 400 should degrade to "a lot" rather than fork-bomb the machine.
|
|
59
|
+
*/
|
|
60
|
+
function resolveMaxConcurrentSubtasks(settingsRaw = {}) {
|
|
61
|
+
// `Math.max(1, …)` matters: a fractional value like 0.5 passes the `> 0` guard, then floors
|
|
62
|
+
// to 0, and createLimiter(0) queues every task with nothing left to ever release them — a
|
|
63
|
+
// silent permanent hang with no timeout and no error. Harmless when only depth 0 used the
|
|
64
|
+
// limiter; now that every level does, it would wedge the whole tree.
|
|
65
|
+
const clamp = (n) => Math.max(1, Math.min(Math.floor(n), HARD_MAX_CONCURRENT_SUBTASKS));
|
|
66
|
+
const fromEnv = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
|
|
67
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0)
|
|
68
|
+
return clamp(fromEnv);
|
|
69
|
+
const fromSettings = Number(settingsRaw.maxConcurrentSubtasks);
|
|
70
|
+
if (Number.isFinite(fromSettings) && fromSettings > 0)
|
|
71
|
+
return clamp(fromSettings);
|
|
72
|
+
return DEFAULT_MAX_CONCURRENT_SUBTASKS;
|
|
73
|
+
}
|
|
74
|
+
// ── Session TOTAL, distinct from the per-moment concurrency gate ─────────────
|
|
75
|
+
//
|
|
76
|
+
// maxConcurrentSubtasks bounds how many run AT ONCE; it does not bound how many run
|
|
77
|
+
// IN TOTAL. With a queue rather than a rejection, "4 at a time" and "unbounded" are the
|
|
78
|
+
// same thing given enough turns — the limiter just meters the spend, it never stops it.
|
|
79
|
+
// A model in a retry loop could spawn sub-agents indefinitely and the only signal would
|
|
80
|
+
// be the bill.
|
|
81
|
+
//
|
|
82
|
+
// This gap did not matter much while sub-agents were leaves: only the main agent could
|
|
83
|
+
// spawn, so the count grew linearly with its own turns. With nesting it grows like a
|
|
84
|
+
// tree, which is exactly why Anthropic added a per-session subagent ceiling alongside
|
|
85
|
+
// their concurrency cap rather than relying on concurrency alone.
|
|
86
|
+
//
|
|
87
|
+
// 100 is chosen to be invisible in real work (a heavy orchestration session uses a few
|
|
88
|
+
// dozen) and decisive in a runaway. Unlike the concurrency gate this REJECTS rather than
|
|
89
|
+
// queues: a queue that never drains is a hang, and the point here is to stop.
|
|
90
|
+
const DEFAULT_MAX_SUBAGENTS_PER_SESSION = 100;
|
|
91
|
+
/** Resolve the session-total sub-agent ceiling: env → settings.json → default. */
|
|
92
|
+
function resolveMaxSubagentsPerSession(settingsRaw = {}) {
|
|
93
|
+
// Clamped, unlike the first draft of this function. The argument for leaving it unbounded
|
|
94
|
+
// was that it only moves a counter — but `.nexrall/settings.json` is REPO-CONTROLLED and
|
|
95
|
+
// merged last, so a cloned repo could set 999999 and neutralise the one ceiling that makes
|
|
96
|
+
// a default depth > 1 defensible, turning `depth 5 x 16 wide` into an unbounded spend on a
|
|
97
|
+
// machine whose owner only opened a project. Depth and concurrency were already clamped for
|
|
98
|
+
// exactly this reason; this was the gap between them.
|
|
99
|
+
const floor = (n) => Math.max(1, Math.min(Math.floor(n), 10000));
|
|
100
|
+
const fromEnv = Number(process.env.NEXRALL_MAX_SUBAGENTS_PER_SESSION);
|
|
101
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0)
|
|
102
|
+
return floor(fromEnv);
|
|
103
|
+
const fromSettings = Number(settingsRaw.maxSubagentsPerSession);
|
|
104
|
+
if (Number.isFinite(fromSettings) && fromSettings > 0)
|
|
105
|
+
return floor(fromSettings);
|
|
106
|
+
return DEFAULT_MAX_SUBAGENTS_PER_SESSION;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
110
|
+
* the CLI ships as a single esbuild bundle with no node_modules, and this is a
|
|
111
|
+
* dozen lines.
|
|
112
|
+
*/
|
|
113
|
+
function createLimiter(max) {
|
|
114
|
+
let active = 0;
|
|
115
|
+
const queue = [];
|
|
116
|
+
const release = () => {
|
|
117
|
+
active--;
|
|
118
|
+
queue.shift()?.();
|
|
119
|
+
};
|
|
120
|
+
return async (fn) => {
|
|
121
|
+
if (active >= max)
|
|
122
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
123
|
+
active++;
|
|
124
|
+
try {
|
|
125
|
+
return await fn();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
release();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Process-wide, deliberately: the limit exists to protect shared resources (CPU,
|
|
133
|
+
// the API rate limit, file handles), and those are shared across every concurrent
|
|
134
|
+
// turn in this process, not just the tool calls of one message.
|
|
135
|
+
//
|
|
136
|
+
// Built LAZILY on first use rather than at module load, because the limit can now come
|
|
137
|
+
// from settings.json and the workspace is not known when this module is imported.
|
|
138
|
+
// Once created it is reused for the process lifetime — rebuilding it per turn would
|
|
139
|
+
// reset `active` and let the ceiling be exceeded, which is worse than not honouring a
|
|
140
|
+
// mid-session settings change.
|
|
141
|
+
// ONE LIMITER PER DEPTH, which is what makes nesting safe to gate at all.
|
|
142
|
+
//
|
|
143
|
+
// The old design gated only `depth === 0` and left nested spawns ungated — deliberately,
|
|
144
|
+
// because a single shared limiter deadlocks the moment a slot-holder re-enters it: a
|
|
145
|
+
// parent holding one of N slots waits for a child that can only start when a slot frees,
|
|
146
|
+
// and if all N are held by such parents the run wedges forever. While sub-agents were
|
|
147
|
+
// leaves that could not happen, so "gate the top, leave the rest" cost nothing.
|
|
148
|
+
//
|
|
149
|
+
// With nesting it costs everything: nested fan-out becomes completely unbounded, which is
|
|
150
|
+
// worse than the deadlock it was avoiding.
|
|
151
|
+
//
|
|
152
|
+
// Keying the limiter by depth fixes both at once. A depth-D run only ever waits on the
|
|
153
|
+
// depth-(D+1) limiter, never its own, so the wait-for graph is strictly ordered by depth —
|
|
154
|
+
// a DAG, and a DAG cannot deadlock. Every level is independently bounded, so worst-case
|
|
155
|
+
// concurrency is bounded per level rather than unbounded below level 1.
|
|
156
|
+
const _subTaskLimiters = new Map();
|
|
157
|
+
/** The tapered ceiling actually applied at each depth, so the queue notice can report it. */
|
|
158
|
+
const _subTaskLimitMaxByDepth = new Map();
|
|
159
|
+
let _subTaskLimitMax = 0;
|
|
160
|
+
/** In-flight count per depth — used only to detect queueing, so the notice is accurate. */
|
|
161
|
+
exports._inFlightByDepth = new Map();
|
|
162
|
+
function subTaskLimiter(depth, workDir) {
|
|
163
|
+
if (!_subTaskLimitMax) {
|
|
164
|
+
_subTaskLimitMax = resolveMaxConcurrentSubtasks(workDir ? (0, rules_1.loadSettings)(workDir).raw : {});
|
|
165
|
+
}
|
|
166
|
+
let run = _subTaskLimiters.get(depth);
|
|
167
|
+
let max = _subTaskLimitMaxByDepth.get(depth) ?? 0;
|
|
168
|
+
if (!run) {
|
|
169
|
+
// TAPERED per level, not `max` at every level.
|
|
170
|
+
//
|
|
171
|
+
// Giving each depth the full `max` multiplies total concurrency by the depth limit: 10
|
|
172
|
+
// becomes 30 by default and 32×5 = 160 at the configured maxima. The value is justified by
|
|
173
|
+
// shared resources — CPU, file handles, ONE API rate limit — none of which care which
|
|
174
|
+
// level a loop is running at, so honouring `4` per level silently abandons the limit the
|
|
175
|
+
// user set. Halving per level bounds the total at ~2× `max` (10+5+3 = 18 by default) while keeping the
|
|
176
|
+
// per-depth structure that makes the wait-for graph acyclic.
|
|
177
|
+
//
|
|
178
|
+
// Never below 1: a level with 0 slots is a permanent hang, not a restriction.
|
|
179
|
+
max = Math.max(1, Math.ceil(_subTaskLimitMax / 2 ** Math.max(0, depth - 1)));
|
|
180
|
+
run = createLimiter(max);
|
|
181
|
+
_subTaskLimiters.set(depth, run);
|
|
182
|
+
_subTaskLimitMaxByDepth.set(depth, max);
|
|
183
|
+
}
|
|
184
|
+
return { run, max };
|
|
185
|
+
}
|
|
186
|
+
/** Test-only: forget the memoised limiter so a new limit can take effect. */
|
|
187
|
+
function _resetSubTaskLimiter() {
|
|
188
|
+
_subTaskLimiters.clear();
|
|
189
|
+
_subTaskLimitMaxByDepth.clear();
|
|
190
|
+
exports._inFlightByDepth.clear();
|
|
191
|
+
_subTaskLimitMax = 0;
|
|
192
|
+
_subAgentBudgets.clear();
|
|
193
|
+
}
|
|
194
|
+
const _subAgentBudgets = new Map();
|
|
195
|
+
exports.PROCESS_BUDGET_KEY = '\0process';
|
|
196
|
+
function budgetFor(sessionKey) {
|
|
197
|
+
const key = sessionKey || exports.PROCESS_BUDGET_KEY;
|
|
198
|
+
let b = _subAgentBudgets.get(key);
|
|
199
|
+
if (!b) {
|
|
200
|
+
b = { used: 0, max: 0, notified: false };
|
|
201
|
+
_subAgentBudgets.set(key, b);
|
|
202
|
+
}
|
|
203
|
+
return b;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Start a fresh sub-agent budget. Call when a NEW conversation begins.
|
|
207
|
+
*
|
|
208
|
+
* Exported for clients that reuse one process across conversations (the VS Code extension
|
|
209
|
+
* host). A client that never calls it gets process-lifetime semantics, which is correct
|
|
210
|
+
* for a one-shot CLI invocation.
|
|
211
|
+
*/
|
|
212
|
+
function resetSessionSubAgentBudget(sessionKey) {
|
|
213
|
+
// No key: the legacy "new conversation" call — reset the process-wide budget only.
|
|
214
|
+
_subAgentBudgets.delete(sessionKey || exports.PROCESS_BUDGET_KEY);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Claim one slot against the session total. Returns an error string when exhausted.
|
|
218
|
+
*
|
|
219
|
+
* `notify` surfaces exhaustion to the HUMAN exactly once. Without it only the model is
|
|
220
|
+
* told, and a model instructed to "do the remaining work directly" complies silently — so
|
|
221
|
+
* the user never learns delegation was capped, which is the one thing a runaway guard has
|
|
222
|
+
* to make visible.
|
|
223
|
+
*/
|
|
224
|
+
function claimSessionSubAgentSlot(workDir, notify, sessionKey) {
|
|
225
|
+
const b = budgetFor(sessionKey);
|
|
226
|
+
if (!b.max) {
|
|
227
|
+
b.max = resolveMaxSubagentsPerSession(workDir ? (0, rules_1.loadSettings)(workDir).raw : {});
|
|
228
|
+
}
|
|
229
|
+
const _sessionCapMax = b.max;
|
|
230
|
+
if (b.used >= b.max) {
|
|
231
|
+
if (!b.notified) {
|
|
232
|
+
b.notified = true;
|
|
233
|
+
notify?.(`\u26a0\ufe0f Sub-agent budget reached (${_sessionCapMax} this session) \u2014 further delegation is ` +
|
|
234
|
+
'blocked and the agent will continue without it. Raise "maxSubagentsPerSession" in ' +
|
|
235
|
+
'.nexrall/settings.json if this was legitimate work.');
|
|
236
|
+
}
|
|
237
|
+
return (`Sub-agent budget for this session is exhausted (${_sessionCapMax} started). This is a ` +
|
|
238
|
+
'runaway-delegation guard, not a per-task limit: do the remaining work directly, and say ' +
|
|
239
|
+
'in your final message that you hit the delegation cap.');
|
|
240
|
+
}
|
|
241
|
+
b.used++;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Give back a slot claimed for a spawn that never started an agent.
|
|
246
|
+
*
|
|
247
|
+
* The claim happens at the dispatch site, BEFORE the concurrency limiter, so that a spawn
|
|
248
|
+
* which is already over budget is refused immediately instead of queueing behind running
|
|
249
|
+
* siblings. That ordering is what makes the guard useful in a runaway — but it also means
|
|
250
|
+
* the claim precedes runSubTask's own validation, which rejects on several paths without
|
|
251
|
+
* ever starting a loop (empty prompt, a `deny` rule, an unusable resume id).
|
|
252
|
+
*
|
|
253
|
+
* Without a refund those rejections spend budget: a model retrying against a deny rule would
|
|
254
|
+
* silently burn the whole allowance on spawns that never ran, then lose delegation for the
|
|
255
|
+
* session with a notice blaming a runaway that never happened. The counter's contract is
|
|
256
|
+
* "sub-agents STARTED", and this is what keeps that true while preserving fast refusal.
|
|
257
|
+
*
|
|
258
|
+
* Floored at 0 so a double refund can never manufacture budget.
|
|
259
|
+
*/
|
|
260
|
+
function refundSessionSubAgentSlot(sessionKey) {
|
|
261
|
+
const b = budgetFor(sessionKey);
|
|
262
|
+
if (b.used > 0)
|
|
263
|
+
b.used--;
|
|
264
|
+
}
|
|
265
|
+
/** Test-only: observe the session counter without exporting the mutable binding. */
|
|
266
|
+
function _sessionSubAgentCount(sessionKey) {
|
|
267
|
+
return budgetFor(sessionKey).used;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=subAgentBudget.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Message, ToolResult, AgentLoopOptions } from '../types';
|
|
2
|
+
import { type AgentType } from './agentTypes';
|
|
3
|
+
export declare function runSubTask(input: Record<string, unknown>, options: AgentLoopOptions, agentTypes: AgentType[], started: {
|
|
4
|
+
value: boolean;
|
|
5
|
+
} | undefined, runChild: (messages: Message[], options: AgentLoopOptions) => Promise<Message[]>): Promise<ToolResult>;
|
|
6
|
+
//# sourceMappingURL=subTask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subTask.d.ts","sourceRoot":"","sources":["../../src/agent/subTask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAiB,MAAM,UAAU,CAAC;AAGrF,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAezF,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,SAAS,EAAE,EAQvB,OAAO,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EAEvC,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,GAC/E,OAAO,CAAC,UAAU,CAAC,CAsuBrB"}
|