@wrongstack/core 0.280.0 → 0.281.0
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-subagent-runner-PoqNKiR4.d.ts → agent-subagent-runner-DyOSK2Wy.d.ts} +39 -8
- package/dist/{compactor-U3agvUIG.d.ts → compactor-BxddyCFu.d.ts} +1 -1
- package/dist/{config-Cr3312zc.d.ts → config-CN-vdDEI.d.ts} +82 -2
- package/dist/coordination/index.d.ts +203 -20
- package/dist/coordination/index.js +981 -52
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -30
- package/dist/defaults/index.js +382 -30
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-Bs2fmldo.d.ts → events-C5hLU0AT.d.ts} +38 -2
- package/dist/execution/index.d.ts +16 -16
- package/dist/execution/index.js +161 -18
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +1 -1
- package/dist/execution/prompt-enhancer.js +12 -3
- package/dist/execution/prompt-enhancer.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{global-mailbox-Ct7IorLJ.d.ts → global-mailbox-CHM1QkCi.d.ts} +5 -2
- package/dist/{goal-store-C4F6DjC0.d.ts → goal-store-BLGZq16t.d.ts} +1 -1
- package/dist/hq/index.d.ts +6 -6
- package/dist/hq/index.js +35 -13
- package/dist/hq/index.js.map +1 -1
- package/dist/{index-nP09-oP2.d.ts → index-BEFWNZYs.d.ts} +1 -2
- package/dist/{index-kidebiDh.d.ts → index-C1E-_71T.d.ts} +6 -6
- package/dist/index.d.ts +44 -48
- package/dist/index.js +14855 -13944
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +60 -8
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +11 -11
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-D910X5_r.d.ts → mcp-servers-EKRAcJDi.d.ts} +97 -4
- package/dist/models/index.d.ts +4 -5
- package/dist/models/index.js +22 -5
- package/dist/models/index.js.map +1 -1
- package/dist/{multi-agent-coordinator-CieyUoEL.d.ts → multi-agent-coordinator-IpHCuHUa.d.ts} +29 -1
- package/dist/{null-fleet-bus-DkdmZJ_W.d.ts → null-fleet-bus-ZBkKGR9j.d.ts} +105 -7
- package/dist/observability/index.d.ts +3 -3
- package/dist/{tool-executor-SiE1wlZo.d.ts → parallel-eternal-engine-Cc2CNgmv.d.ts} +6 -202
- package/dist/path-resolver-CMS5307d.d.ts +13 -0
- package/dist/{permission-Dx6dIqS2.d.ts → permission-B6YAtNFz.d.ts} +1 -1
- package/dist/{pipeline-BwAP21_4.d.ts → pipeline-E7_jqGyV.d.ts} +2 -2
- package/dist/{provider-model-resolve-CwQNZWt_.d.ts → provider-model-resolve-KgIdiEQ6.d.ts} +118 -5
- package/dist/{provider-runner-CYHFImzV.d.ts → provider-runner-DxWXNHL1.d.ts} +3 -3
- package/dist/{retry-policy-D4feSLk3.d.ts → retry-policy-V67lBOJz.d.ts} +2 -2
- package/dist/sdd/index.d.ts +9 -9
- package/dist/sdd/index.js +94 -10
- package/dist/sdd/index.js.map +1 -1
- package/dist/{permission-policy-C8vJcnX5.d.ts → secret-vault-D7jeyXUQ.d.ts} +119 -3
- package/dist/security/index.d.ts +5 -6
- package/dist/security/index.js +4 -1
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-Dy-MzKp1.d.ts → selector-Bx2HgUkU.d.ts} +1 -1
- package/dist/{session-event-bridge-CqdiGnfU.d.ts → session-event-bridge-vA2fLV58.d.ts} +1 -1
- package/dist/{session-reader-Hk0WbNm9.d.ts → session-reader-B-Fi9L6I.d.ts} +1 -1
- package/dist/session-registry-Dvg7i_IA.d.ts +129 -0
- package/dist/storage/index.d.ts +71 -142
- package/dist/storage/index.js +161 -13
- package/dist/storage/index.js.map +1 -1
- package/dist/{todos-checkpoint-Bk2uP7Ex.d.ts → todos-checkpoint-B1QVkoa8.d.ts} +23 -5
- package/dist/{tool-BkOgs_KL.d.ts → tool-LVSLiE1s.d.ts} +18 -4
- package/dist/{strategy-compactor-CQwhbErd.d.ts → tool-executor-BWtYwuPI.d.ts} +210 -12
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/types/index.d.ts +17 -41
- package/dist/types/index.js +182 -5085
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/index.js +6 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/{worktree-manager-BjOFF6bt.d.ts → worktree-manager-C6CIilTT.d.ts} +1 -1
- package/dist/{wstack-paths-CMl_cYgq.d.ts → wstack-paths-C3K40Qst.d.ts} +2 -0
- package/instructions/agents/shadow-agent.md +23 -84
- package/instructions/coordination/director-preamble.md +12 -3
- package/instructions/coordination/subagent-baseline.md +39 -46
- package/instructions/leader-after-task.md +10 -28
- package/instructions/llm/llm-selector.md +1 -2
- package/instructions/modes/brief-system.md +7 -33
- package/instructions/modes/brief.md +5 -20
- package/instructions/modes/teach-system.md +12 -72
- package/instructions/modes/teach.md +12 -49
- package/instructions/sections/tool/commit-hygiene.md +5 -5
- package/instructions/sections/tool/context-management-full.md +6 -6
- package/instructions/sections/tool/delegation-compact.md +1 -1
- package/instructions/sections/tool/delegation-full.md +19 -23
- package/instructions/sections/tool/mailbox-full.md +11 -31
- package/instructions/sections/tool/mcp-full-control.md +5 -7
- package/instructions/sections/tool/mcp-full-use.md +7 -7
- package/instructions/system.md +22 -38
- package/package.json +1 -29
- package/dist/agent-bridge-DXC6QDJ4.d.ts +0 -35
- package/dist/logger-D3lV0cUZ.d.ts +0 -101
- package/dist/models-registry-CLkoOcHk.d.ts +0 -108
- package/dist/path-resolver-XfZ9eLxG.d.ts +0 -67
- package/dist/secret-vault-CImt2XrR.d.ts +0 -118
|
@@ -371,6 +371,27 @@ function safeParse(input, maxBytes = 5e6) {
|
|
|
371
371
|
};
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
|
+
function safeStringify(value, pretty = false) {
|
|
375
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
376
|
+
const replacer = (_k, v) => {
|
|
377
|
+
if (typeof v === "bigint") return v.toString();
|
|
378
|
+
if (v instanceof Error) {
|
|
379
|
+
return { name: v.name, message: v.message, stack: v.stack };
|
|
380
|
+
}
|
|
381
|
+
if (typeof v === "object" && v !== null) {
|
|
382
|
+
if (seen.has(v)) return "[Circular]";
|
|
383
|
+
seen.add(v);
|
|
384
|
+
}
|
|
385
|
+
return v;
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
return JSON.stringify(value, replacer, pretty ? 2 : void 0) ?? "null";
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return JSON.stringify({
|
|
391
|
+
__serialization_error: toErrorMessage(err)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
374
395
|
function sessionScopedPath(dir, sessionId, suffix) {
|
|
375
396
|
if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
376
397
|
throw invalid(sessionId);
|
|
@@ -1385,20 +1406,32 @@ var TOOLS = {
|
|
|
1385
1406
|
/** Web research. */
|
|
1386
1407
|
research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
|
|
1387
1408
|
};
|
|
1409
|
+
var promptCache = /* @__PURE__ */ new Map();
|
|
1410
|
+
var candidateCache = /* @__PURE__ */ new Map();
|
|
1388
1411
|
function agentPrompt(id) {
|
|
1412
|
+
const envDir = process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"] ?? "";
|
|
1413
|
+
const cacheKey = `${envDir}\0${id}`;
|
|
1414
|
+
const cached = promptCache.get(cacheKey);
|
|
1415
|
+
if (cached !== void 0) return cached;
|
|
1389
1416
|
const fileName = `${id}.md`;
|
|
1390
|
-
|
|
1417
|
+
let resolved = "";
|
|
1418
|
+
for (const dir of agentPromptDirCandidates(envDir)) {
|
|
1391
1419
|
try {
|
|
1392
|
-
|
|
1420
|
+
resolved = readFileSync(path9.join(dir, fileName), "utf8").trimEnd();
|
|
1421
|
+
break;
|
|
1393
1422
|
} catch {
|
|
1394
1423
|
}
|
|
1395
1424
|
}
|
|
1396
|
-
|
|
1425
|
+
promptCache.set(cacheKey, resolved);
|
|
1426
|
+
return resolved;
|
|
1397
1427
|
}
|
|
1398
|
-
function agentPromptDirCandidates() {
|
|
1399
|
-
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
1400
|
-
const explicitDir = process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"];
|
|
1428
|
+
function agentPromptDirCandidates(envDir) {
|
|
1401
1429
|
const globalRoot = process.env["WRONGSTACK_HOME"] || path9.join(os.homedir(), ".wrongstack");
|
|
1430
|
+
const candKey = `${envDir}\0${globalRoot}`;
|
|
1431
|
+
const cached = candidateCache.get(candKey);
|
|
1432
|
+
if (cached !== void 0) return cached;
|
|
1433
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
1434
|
+
const explicitDir = envDir || void 0;
|
|
1402
1435
|
const candidates = [
|
|
1403
1436
|
...explicitDir ? [path9.resolve(explicitDir)] : [],
|
|
1404
1437
|
path9.join(globalRoot, "instructions", "agents"),
|
|
@@ -1408,7 +1441,9 @@ function agentPromptDirCandidates() {
|
|
|
1408
1441
|
path9.resolve(here, "../instructions/agents"),
|
|
1409
1442
|
path9.resolve(here, "instructions/agents")
|
|
1410
1443
|
];
|
|
1411
|
-
|
|
1444
|
+
const ordered = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
|
|
1445
|
+
candidateCache.set(candKey, ordered);
|
|
1446
|
+
return ordered;
|
|
1412
1447
|
}
|
|
1413
1448
|
function isDirectory(candidate) {
|
|
1414
1449
|
try {
|
|
@@ -4670,14 +4705,21 @@ var DirectorStateCheckpoint = class {
|
|
|
4670
4705
|
}
|
|
4671
4706
|
}
|
|
4672
4707
|
};
|
|
4708
|
+
var textCache = /* @__PURE__ */ new Map();
|
|
4709
|
+
var rootCandidates;
|
|
4673
4710
|
function readBundledInstructionText(relativePath) {
|
|
4711
|
+
const cached = textCache.get(relativePath);
|
|
4712
|
+
if (cached !== void 0) return cached;
|
|
4713
|
+
let resolved = "";
|
|
4674
4714
|
for (const root of instructionRootCandidates()) {
|
|
4675
4715
|
try {
|
|
4676
|
-
|
|
4716
|
+
resolved = readFileSync(path9.join(root, relativePath), "utf8").trimEnd();
|
|
4717
|
+
break;
|
|
4677
4718
|
} catch {
|
|
4678
4719
|
}
|
|
4679
4720
|
}
|
|
4680
|
-
|
|
4721
|
+
textCache.set(relativePath, resolved);
|
|
4722
|
+
return resolved;
|
|
4681
4723
|
}
|
|
4682
4724
|
function renderInstructionTemplate(template, values) {
|
|
4683
4725
|
return template.replace(
|
|
@@ -4686,13 +4728,15 @@ function renderInstructionTemplate(template, values) {
|
|
|
4686
4728
|
);
|
|
4687
4729
|
}
|
|
4688
4730
|
function instructionRootCandidates() {
|
|
4731
|
+
if (rootCandidates !== void 0) return rootCandidates;
|
|
4689
4732
|
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
4690
4733
|
const candidates = [
|
|
4691
4734
|
path9.resolve(here, "../../instructions"),
|
|
4692
4735
|
path9.resolve(here, "../instructions"),
|
|
4693
4736
|
path9.resolve(here, "instructions")
|
|
4694
4737
|
];
|
|
4695
|
-
|
|
4738
|
+
rootCandidates = candidates.sort((a, b) => Number(!isDirectory2(a)) - Number(!isDirectory2(b)));
|
|
4739
|
+
return rootCandidates;
|
|
4696
4740
|
}
|
|
4697
4741
|
function isDirectory2(candidate) {
|
|
4698
4742
|
try {
|
|
@@ -5012,13 +5056,31 @@ function makeAssignTool(director) {
|
|
|
5012
5056
|
function makeAwaitTasksTool(director) {
|
|
5013
5057
|
return {
|
|
5014
5058
|
name: "await_tasks",
|
|
5015
|
-
description: "
|
|
5059
|
+
description: 'Wait for assigned tasks. mode:"all" (default) blocks until EVERY named task completes and returns all results. mode:"any" returns as soon as AT LEAST ONE completes \u2014 use it for independent tasks so you can handle each finisher immediately (reassign work, spawn helpers) instead of idling on the slowest; call again with the returned `pending` ids to pick up the next finisher.',
|
|
5016
5060
|
permission: "auto",
|
|
5017
5061
|
mutating: false,
|
|
5018
5062
|
capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
|
|
5019
|
-
inputSchema: {
|
|
5063
|
+
inputSchema: {
|
|
5064
|
+
type: "object",
|
|
5065
|
+
properties: {
|
|
5066
|
+
taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." },
|
|
5067
|
+
mode: { type: "string", enum: ["all", "any"], description: '"all" (default): block until every task completes. "any": return on the first completion with the rest listed as pending.' },
|
|
5068
|
+
timeoutMs: { type: "number", minimum: 1, description: 'mode:"any" only \u2014 return {timedOut:true, completed:[]} if nothing completes within this window instead of blocking.' }
|
|
5069
|
+
},
|
|
5070
|
+
required: ["taskIds"]
|
|
5071
|
+
},
|
|
5020
5072
|
async execute(input) {
|
|
5021
5073
|
const i = input;
|
|
5074
|
+
if (i.mode === "any") {
|
|
5075
|
+
const r = await director.awaitTasksAny(i.taskIds, i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : void 0);
|
|
5076
|
+
return {
|
|
5077
|
+
mode: "any",
|
|
5078
|
+
completed: r.completed,
|
|
5079
|
+
pending: r.pending,
|
|
5080
|
+
...r.timedOut ? { timedOut: true } : {},
|
|
5081
|
+
...r.pending.length > 0 ? { hint: 'Handle the completed results now. Re-call await_tasks with the pending ids (mode:"any") for the next finisher \u2014 or assign new work to the now-idle subagent first.' } : {}
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5022
5084
|
const results = await director.awaitTasks(i.taskIds);
|
|
5023
5085
|
return { results };
|
|
5024
5086
|
}
|
|
@@ -6104,6 +6166,30 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6104
6166
|
results() {
|
|
6105
6167
|
return this.completedResults;
|
|
6106
6168
|
}
|
|
6169
|
+
/** Defensive snapshot of the still-queued (not yet dispatched) tasks. */
|
|
6170
|
+
listPendingTasks() {
|
|
6171
|
+
return this.pendingTasks.map((t) => ({ ...t }));
|
|
6172
|
+
}
|
|
6173
|
+
/**
|
|
6174
|
+
* Re-pin a still-PENDING task to a different subagent (`undefined` =
|
|
6175
|
+
* unpin, any idle worker may take it), then try to dispatch. Returns
|
|
6176
|
+
* `false` when the task is not in the pending queue — already
|
|
6177
|
+
* dispatched, completed, or unknown. Running tasks can never be pulled;
|
|
6178
|
+
* they can only be steered or terminated. The mutation is synchronous
|
|
6179
|
+
* (same tick as the check), so it cannot race a concurrent dispatch:
|
|
6180
|
+
* `tryDispatchNext` runs on this same call stack or a later one.
|
|
6181
|
+
*
|
|
6182
|
+
* The task keeps its id, so waiters, the report-back notifier, and
|
|
6183
|
+
* checkpoint bookkeeping resolve unchanged when it eventually completes.
|
|
6184
|
+
*/
|
|
6185
|
+
retargetPendingTask(taskId, subagentId) {
|
|
6186
|
+
const task = this.pendingTasks.find((t) => t.id === taskId);
|
|
6187
|
+
if (!task) return false;
|
|
6188
|
+
if (subagentId !== void 0 && !this.subagents.has(subagentId)) return false;
|
|
6189
|
+
task.subagentId = subagentId;
|
|
6190
|
+
this.tryDispatchNext();
|
|
6191
|
+
return true;
|
|
6192
|
+
}
|
|
6107
6193
|
/**
|
|
6108
6194
|
* Wait for one or more tasks to complete and return their results.
|
|
6109
6195
|
* If a task is already done when called, returns immediately.
|
|
@@ -6131,6 +6217,43 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6131
6217
|
})
|
|
6132
6218
|
);
|
|
6133
6219
|
}
|
|
6220
|
+
/**
|
|
6221
|
+
* Wait until AT LEAST ONE of the named tasks completes. Returns every
|
|
6222
|
+
* requested result already available at that moment (drain-what's-done)
|
|
6223
|
+
* plus the ids still outstanding, so callers can loop "handle finishers,
|
|
6224
|
+
* re-await the remainder" instead of blocking on the whole batch.
|
|
6225
|
+
*
|
|
6226
|
+
* Unlike `awaitTasks`, this never rejects and deliberately does NOT
|
|
6227
|
+
* inherit `config.timeoutMs` — a "return whatever is done" call has no
|
|
6228
|
+
* business timing out unless the caller asks for a window explicitly.
|
|
6229
|
+
*/
|
|
6230
|
+
async awaitTasksAny(taskIds, opts) {
|
|
6231
|
+
const ids = new Set(taskIds);
|
|
6232
|
+
const completed = this.completedResults.filter((r) => ids.has(r.taskId));
|
|
6233
|
+
if (completed.length > 0 || ids.size === 0) {
|
|
6234
|
+
const done = new Set(completed.map((r) => r.taskId));
|
|
6235
|
+
return { completed, pending: taskIds.filter((id) => !done.has(id)) };
|
|
6236
|
+
}
|
|
6237
|
+
return new Promise((resolve6) => {
|
|
6238
|
+
let timer;
|
|
6239
|
+
const handler = ({ result }) => {
|
|
6240
|
+
if (!ids.has(result.taskId)) return;
|
|
6241
|
+
if (timer) clearTimeout(timer);
|
|
6242
|
+
this.off("task.completed", handler);
|
|
6243
|
+
resolve6({
|
|
6244
|
+
completed: [result],
|
|
6245
|
+
pending: taskIds.filter((id) => id !== result.taskId)
|
|
6246
|
+
});
|
|
6247
|
+
};
|
|
6248
|
+
if (opts?.timeoutMs !== void 0) {
|
|
6249
|
+
timer = setTimeout(() => {
|
|
6250
|
+
this.off("task.completed", handler);
|
|
6251
|
+
resolve6({ completed: [], pending: [...taskIds], timedOut: true });
|
|
6252
|
+
}, opts.timeoutMs);
|
|
6253
|
+
}
|
|
6254
|
+
this.on("task.completed", handler);
|
|
6255
|
+
});
|
|
6256
|
+
}
|
|
6134
6257
|
/**
|
|
6135
6258
|
* Manual completion — for callers that drive subagents without a runner
|
|
6136
6259
|
* (e.g. external orchestrators). When a runner is configured the coordinator
|
|
@@ -6755,6 +6878,13 @@ var Director = class _Director {
|
|
|
6755
6878
|
* coordinator emits `task.completed` for a given task id. Each entry
|
|
6756
6879
|
* is created lazily on first poll/await and cleared once consumed. */
|
|
6757
6880
|
taskWaiters = /* @__PURE__ */ new Map();
|
|
6881
|
+
/** First-completion waiters registered by `awaitTasksAny`. Kept separate
|
|
6882
|
+
* from `taskWaiters` on purpose: a `taskWaiters` entry marks a result as
|
|
6883
|
+
* "will be delivered in-band" and suppresses the fire-and-forget
|
|
6884
|
+
* report-back — but an any-await only consumes ONE of its ids, so the
|
|
6885
|
+
* losers must stay eligible for the mailbox notifier. Entries carry an
|
|
6886
|
+
* optional timeout timer so `shutdown()` can clear them. */
|
|
6887
|
+
anyWaiters = /* @__PURE__ */ new Set();
|
|
6758
6888
|
/** Cache of completed results in case the consumer asks AFTER the
|
|
6759
6889
|
* coordinator already fired the event — `awaitTasks(['t-1'])` after
|
|
6760
6890
|
* t-1 finished should resolve immediately, not hang. */
|
|
@@ -6776,6 +6906,8 @@ var Director = class _Director {
|
|
|
6776
6906
|
roster;
|
|
6777
6907
|
directorPreamble;
|
|
6778
6908
|
subagentBaseline;
|
|
6909
|
+
/** See {@link DirectorOptions.taskResultNotifier}. */
|
|
6910
|
+
taskResultNotifier;
|
|
6779
6911
|
/** Absolute path to the fleet's shared scratchpad directory, or null
|
|
6780
6912
|
* when none was configured. Exposed as a readonly getter for callers
|
|
6781
6913
|
* that need to surface the path to the user (e.g. the CLI logging
|
|
@@ -6873,6 +7005,7 @@ var Director = class _Director {
|
|
|
6873
7005
|
this.roster = opts.roster;
|
|
6874
7006
|
this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
6875
7007
|
this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
7008
|
+
this.taskResultNotifier = opts.taskResultNotifier;
|
|
6876
7009
|
this.sharedScratchpadPath = opts.sharedScratchpadPath ?? null;
|
|
6877
7010
|
this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
|
|
6878
7011
|
this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
|
|
@@ -6945,8 +7078,38 @@ var Director = class _Director {
|
|
|
6945
7078
|
waiter.resolve(r);
|
|
6946
7079
|
this.taskWaiters.delete(r.taskId);
|
|
6947
7080
|
}
|
|
7081
|
+
let anyConsumed = false;
|
|
7082
|
+
for (const aw of [...this.anyWaiters]) {
|
|
7083
|
+
if (!aw.ids.has(r.taskId)) continue;
|
|
7084
|
+
if (aw.timer) clearTimeout(aw.timer);
|
|
7085
|
+
this.anyWaiters.delete(aw);
|
|
7086
|
+
aw.resolve(r);
|
|
7087
|
+
anyConsumed = true;
|
|
7088
|
+
}
|
|
7089
|
+
const consumedInBand = !!waiter || anyConsumed;
|
|
6948
7090
|
if (internalTask) return;
|
|
6949
7091
|
const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
|
|
7092
|
+
if (!consumedInBand && this.taskResultNotifier) {
|
|
7093
|
+
const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
|
|
7094
|
+
try {
|
|
7095
|
+
void Promise.resolve(
|
|
7096
|
+
this.taskResultNotifier({
|
|
7097
|
+
taskId: r.taskId,
|
|
7098
|
+
title,
|
|
7099
|
+
status: r.status,
|
|
7100
|
+
subagentId: r.subagentId,
|
|
7101
|
+
subagentName: this.manifestEntries.get(r.subagentId)?.name,
|
|
7102
|
+
resultText,
|
|
7103
|
+
errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
|
|
7104
|
+
iterations: r.iterations,
|
|
7105
|
+
toolCalls: r.toolCalls,
|
|
7106
|
+
durationMs: r.durationMs
|
|
7107
|
+
})
|
|
7108
|
+
).catch(() => {
|
|
7109
|
+
});
|
|
7110
|
+
} catch {
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
6950
7113
|
const failed = r.status !== "success";
|
|
6951
7114
|
const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
|
|
6952
7115
|
this.stateCheckpoint?.recordTaskStatus(r.taskId, {
|
|
@@ -7532,6 +7695,10 @@ var Director = class _Director {
|
|
|
7532
7695
|
this.coordinator.off("task.completed", this.taskCompletedListener);
|
|
7533
7696
|
this.taskCompletedListener = null;
|
|
7534
7697
|
}
|
|
7698
|
+
for (const aw of this.anyWaiters) {
|
|
7699
|
+
if (aw.timer) clearTimeout(aw.timer);
|
|
7700
|
+
}
|
|
7701
|
+
this.anyWaiters.clear();
|
|
7535
7702
|
if (this.toolExecFilter) {
|
|
7536
7703
|
this.toolExecFilter();
|
|
7537
7704
|
this.toolExecFilter = null;
|
|
@@ -7600,6 +7767,12 @@ var Director = class _Director {
|
|
|
7600
7767
|
waiter.resolve(synthetic);
|
|
7601
7768
|
this.taskWaiters.delete(taskWithId.id);
|
|
7602
7769
|
}
|
|
7770
|
+
for (const aw of [...this.anyWaiters]) {
|
|
7771
|
+
if (!aw.ids.has(taskWithId.id)) continue;
|
|
7772
|
+
if (aw.timer) clearTimeout(aw.timer);
|
|
7773
|
+
this.anyWaiters.delete(aw);
|
|
7774
|
+
aw.resolve(synthetic);
|
|
7775
|
+
}
|
|
7603
7776
|
return taskWithId.id;
|
|
7604
7777
|
}
|
|
7605
7778
|
if (task.subagentId) {
|
|
@@ -7670,6 +7843,85 @@ var Director = class _Director {
|
|
|
7670
7843
|
})
|
|
7671
7844
|
);
|
|
7672
7845
|
}
|
|
7846
|
+
/**
|
|
7847
|
+
* Wait until AT LEAST ONE of the named tasks completes, then return every
|
|
7848
|
+
* requested result already cached plus the still-pending ids. This is the
|
|
7849
|
+
* leader's "handle whichever finishes next" primitive — unlike
|
|
7850
|
+
* `awaitTasks` it never blocks on the slowest sibling, and the siblings
|
|
7851
|
+
* that complete later still reach the leader through the fire-and-forget
|
|
7852
|
+
* report-back (see the consumed-in-band rule in `taskCompletedListener`).
|
|
7853
|
+
*
|
|
7854
|
+
* Deliberately does NOT register `taskWaiters` entries for the pending
|
|
7855
|
+
* ids: a `taskWaiters` entry would mark those results as in-band-consumed
|
|
7856
|
+
* and silence the mailbox notifier for results this caller never receives.
|
|
7857
|
+
*
|
|
7858
|
+
* With `timeoutMs`, resolves (never rejects) `{timedOut: true}` and zero
|
|
7859
|
+
* completions when the window elapses first.
|
|
7860
|
+
*/
|
|
7861
|
+
awaitTasksAny(taskIds, opts) {
|
|
7862
|
+
const completed = taskIds.map((id) => this.completed.get(id)).filter((r) => r !== void 0);
|
|
7863
|
+
if (completed.length > 0 || taskIds.length === 0) {
|
|
7864
|
+
const done = new Set(completed.map((r) => r.taskId));
|
|
7865
|
+
return Promise.resolve({
|
|
7866
|
+
completed,
|
|
7867
|
+
pending: taskIds.filter((id) => !done.has(id))
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
return new Promise((resolve6) => {
|
|
7871
|
+
const entry = {
|
|
7872
|
+
ids: new Set(taskIds),
|
|
7873
|
+
resolve: (r) => resolve6({
|
|
7874
|
+
completed: [r],
|
|
7875
|
+
pending: taskIds.filter((id) => id !== r.taskId)
|
|
7876
|
+
})
|
|
7877
|
+
};
|
|
7878
|
+
if (opts?.timeoutMs !== void 0) {
|
|
7879
|
+
entry.timer = setTimeout(() => {
|
|
7880
|
+
this.anyWaiters.delete(entry);
|
|
7881
|
+
resolve6({ completed: [], pending: [...taskIds], timedOut: true });
|
|
7882
|
+
}, opts.timeoutMs);
|
|
7883
|
+
}
|
|
7884
|
+
this.anyWaiters.add(entry);
|
|
7885
|
+
});
|
|
7886
|
+
}
|
|
7887
|
+
/** Defensive snapshot of tasks still queued (not yet dispatched). */
|
|
7888
|
+
listPendingTasks() {
|
|
7889
|
+
return this.coordinator.listPendingTasks();
|
|
7890
|
+
}
|
|
7891
|
+
/**
|
|
7892
|
+
* Re-pin a still-pending task to a different subagent (`undefined` =
|
|
7893
|
+
* unpin) — the rebalancing primitive used by the FleetSupervisor and
|
|
7894
|
+
* available to hosts. Only queued tasks can move; a dispatched task
|
|
7895
|
+
* returns `false` (steer or terminate its worker instead). Keeps
|
|
7896
|
+
* ownership bookkeeping (taskOwners, checkpoint, fleet manifest) in
|
|
7897
|
+
* sync with the coordinator's queue.
|
|
7898
|
+
*/
|
|
7899
|
+
retargetPendingTask(taskId, subagentId) {
|
|
7900
|
+
const ok = this.coordinator.retargetPendingTask(taskId, subagentId);
|
|
7901
|
+
if (!ok) return false;
|
|
7902
|
+
if (subagentId) {
|
|
7903
|
+
this.taskOwners.set(taskId, subagentId);
|
|
7904
|
+
if (this.fleetManager) {
|
|
7905
|
+
this.fleetManager.addTaskToSubagent(subagentId, taskId);
|
|
7906
|
+
} else {
|
|
7907
|
+
const entry = this.manifestEntries.get(subagentId);
|
|
7908
|
+
if (entry && !entry.taskIds.includes(taskId)) entry.taskIds.push(taskId);
|
|
7909
|
+
}
|
|
7910
|
+
} else {
|
|
7911
|
+
this.taskOwners.delete(taskId);
|
|
7912
|
+
}
|
|
7913
|
+
const description = this.taskDescriptions.get(taskId) ?? taskId;
|
|
7914
|
+
this.fleetManager?.addPendingTask(taskId, subagentId ?? "", description);
|
|
7915
|
+
this.stateCheckpoint?.recordTaskAssigned({
|
|
7916
|
+
taskId,
|
|
7917
|
+
subagentId,
|
|
7918
|
+
description,
|
|
7919
|
+
status: "running",
|
|
7920
|
+
assignedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7921
|
+
});
|
|
7922
|
+
this.scheduleManifest();
|
|
7923
|
+
return true;
|
|
7924
|
+
}
|
|
7673
7925
|
async terminate(subagentId) {
|
|
7674
7926
|
await this.coordinator.stop(subagentId);
|
|
7675
7927
|
}
|
|
@@ -8595,6 +8847,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8595
8847
|
dir;
|
|
8596
8848
|
events;
|
|
8597
8849
|
secretScrubber;
|
|
8850
|
+
isSessionInUse;
|
|
8598
8851
|
/**
|
|
8599
8852
|
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
8600
8853
|
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
@@ -8614,6 +8867,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8614
8867
|
this.dir = opts.dir;
|
|
8615
8868
|
this.events = opts.events;
|
|
8616
8869
|
this.secretScrubber = opts.secretScrubber;
|
|
8870
|
+
this.isSessionInUse = opts.isSessionInUse;
|
|
8617
8871
|
}
|
|
8618
8872
|
/**
|
|
8619
8873
|
* Clear the load() cache. Useful for testing or when the caller knows
|
|
@@ -9505,19 +9759,74 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
9505
9759
|
});
|
|
9506
9760
|
await this.writeTombstone(id);
|
|
9507
9761
|
}
|
|
9762
|
+
/**
|
|
9763
|
+
* Read the session id currently marked active in `active.json`, or `null`
|
|
9764
|
+
* when the lock is absent/unreadable. Shared by {@link delete} and
|
|
9765
|
+
* {@link prune} to avoid clobbering a session a live process is writing to.
|
|
9766
|
+
*/
|
|
9767
|
+
async readActiveSessionId() {
|
|
9768
|
+
try {
|
|
9769
|
+
const raw = await fsp7.readFile(path9.join(this.dir, "active.json"), "utf8");
|
|
9770
|
+
const active = JSON.parse(raw);
|
|
9771
|
+
return active.sessionId ?? null;
|
|
9772
|
+
} catch {
|
|
9773
|
+
return null;
|
|
9774
|
+
}
|
|
9775
|
+
}
|
|
9508
9776
|
async delete(id) {
|
|
9777
|
+
const activeId = await this.readActiveSessionId();
|
|
9778
|
+
if (activeId && id === activeId) {
|
|
9779
|
+
throw new Error(
|
|
9780
|
+
`Session ${id} is currently active in this project and cannot be deleted. Resume or start another session first.`
|
|
9781
|
+
);
|
|
9782
|
+
}
|
|
9783
|
+
if (this.isSessionInUse) {
|
|
9784
|
+
const reason = await this.isSessionInUse(id);
|
|
9785
|
+
if (reason) {
|
|
9786
|
+
throw new Error(`Session ${id} is in use (${reason}) and cannot be deleted.`);
|
|
9787
|
+
}
|
|
9788
|
+
}
|
|
9509
9789
|
await this.deleteSession(id);
|
|
9510
9790
|
}
|
|
9791
|
+
async rename(id, name) {
|
|
9792
|
+
const trimmed = name.trim();
|
|
9793
|
+
const manifest = this.sessionPath(id, ".summary.json");
|
|
9794
|
+
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
9795
|
+
try {
|
|
9796
|
+
await fsp7.stat(jsonlPath);
|
|
9797
|
+
} catch (err) {
|
|
9798
|
+
const code = err?.code;
|
|
9799
|
+
if (code === "ENOENT") {
|
|
9800
|
+
throw new Error(`Session not found: ${id}`);
|
|
9801
|
+
}
|
|
9802
|
+
throw err;
|
|
9803
|
+
}
|
|
9804
|
+
const summary = await this.summaryFor(id);
|
|
9805
|
+
const { name: _drop, ...rest } = summary;
|
|
9806
|
+
const updated = trimmed ? { ...rest, name: trimmed } : rest;
|
|
9807
|
+
const t0 = Date.now();
|
|
9808
|
+
let outcome = "success";
|
|
9809
|
+
let errorMsg;
|
|
9810
|
+
try {
|
|
9811
|
+
await atomicWrite(manifest, JSON.stringify(updated), { mode: 384 });
|
|
9812
|
+
} catch (err) {
|
|
9813
|
+
outcome = "failure";
|
|
9814
|
+
errorMsg = toErrorMessage(err);
|
|
9815
|
+
this.emitError(id, manifest, "rename", errorMsg, false);
|
|
9816
|
+
throw err;
|
|
9817
|
+
} finally {
|
|
9818
|
+
this.emitWrite(id, manifest, "close", outcome, Date.now() - t0, void 0, errorMsg);
|
|
9819
|
+
}
|
|
9820
|
+
await this.appendToIndex(updated);
|
|
9821
|
+
this.invalidateShardManifestBySessionId(id);
|
|
9822
|
+
this._indexCache = null;
|
|
9823
|
+
this.clearLoadCache(id);
|
|
9824
|
+
return updated;
|
|
9825
|
+
}
|
|
9511
9826
|
async prune(maxAgeDays = 30) {
|
|
9512
9827
|
const cutoff = Date.now() - maxAgeDays * 864e5;
|
|
9513
9828
|
let deleted = 0;
|
|
9514
|
-
|
|
9515
|
-
try {
|
|
9516
|
-
const raw = await fsp7.readFile(path9.join(this.dir, "active.json"), "utf8");
|
|
9517
|
-
const active = JSON.parse(raw);
|
|
9518
|
-
activeSessionId = active.sessionId ?? null;
|
|
9519
|
-
} catch {
|
|
9520
|
-
}
|
|
9829
|
+
const activeSessionId = await this.readActiveSessionId();
|
|
9521
9830
|
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
9522
9831
|
const pruneFile = async (dir, name, prefix) => {
|
|
9523
9832
|
const jsonlPath = path9.join(dir, name);
|
|
@@ -11317,6 +11626,651 @@ function makeMailInboxTool(opts = {}) {
|
|
|
11317
11626
|
}
|
|
11318
11627
|
};
|
|
11319
11628
|
}
|
|
11629
|
+
|
|
11630
|
+
// src/coordination/fleet-status-tool.ts
|
|
11631
|
+
var TASK_SNIPPET_CHARS = 120;
|
|
11632
|
+
function snippet(s) {
|
|
11633
|
+
if (!s) return void 0;
|
|
11634
|
+
return s.length > TASK_SNIPPET_CHARS ? `${s.slice(0, TASK_SNIPPET_CHARS)}\u2026` : s;
|
|
11635
|
+
}
|
|
11636
|
+
function makeFleetStatusTool(opts = {}) {
|
|
11637
|
+
const resolveMailbox = opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
11638
|
+
return {
|
|
11639
|
+
name: "fleet_status",
|
|
11640
|
+
description: "Live snapshot of every agent working on this project (this process AND other terminals/TUIs/WebUIs): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. To talk to a peer, use mail_send with the returned id.",
|
|
11641
|
+
usageHint: "fleet_status (optionally: onlineOnly=false to include recently-offline agents)",
|
|
11642
|
+
category: "Coordination",
|
|
11643
|
+
permission: "auto",
|
|
11644
|
+
mutating: false,
|
|
11645
|
+
capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
|
|
11646
|
+
inputSchema: {
|
|
11647
|
+
type: "object",
|
|
11648
|
+
properties: {
|
|
11649
|
+
onlineOnly: {
|
|
11650
|
+
type: "boolean",
|
|
11651
|
+
description: "Only agents with a live heartbeat (default true)."
|
|
11652
|
+
}
|
|
11653
|
+
}
|
|
11654
|
+
},
|
|
11655
|
+
async execute(input, ctx) {
|
|
11656
|
+
const i = input ?? {};
|
|
11657
|
+
const onlineOnly = i.onlineOnly ?? true;
|
|
11658
|
+
const mb = resolveMailbox(ctx);
|
|
11659
|
+
const identity = resolveMailboxIdentity(ctx);
|
|
11660
|
+
try {
|
|
11661
|
+
await mb.registerAgent({
|
|
11662
|
+
agentId: identity.callerId,
|
|
11663
|
+
sessionId: identity.sessionId,
|
|
11664
|
+
name: identity.name,
|
|
11665
|
+
role: identity.role,
|
|
11666
|
+
pid: process.pid,
|
|
11667
|
+
source: ctx.meta["source"] ?? "cli"
|
|
11668
|
+
});
|
|
11669
|
+
await mb.heartbeat({ agentId: identity.callerId });
|
|
11670
|
+
} catch {
|
|
11671
|
+
}
|
|
11672
|
+
const statuses = await mb.getAgentStatuses().catch(() => []);
|
|
11673
|
+
const local = /* @__PURE__ */ new Map();
|
|
11674
|
+
try {
|
|
11675
|
+
for (const e of opts.getLocalAgents?.() ?? []) local.set(e.name, e);
|
|
11676
|
+
} catch {
|
|
11677
|
+
}
|
|
11678
|
+
const rows = statuses.filter((s) => onlineOnly ? s.online : true).map((s) => {
|
|
11679
|
+
const rich = local.get(s.name);
|
|
11680
|
+
return {
|
|
11681
|
+
id: s.agentId,
|
|
11682
|
+
name: s.name,
|
|
11683
|
+
role: s.role,
|
|
11684
|
+
you: s.agentId === identity.callerId,
|
|
11685
|
+
status: s.status,
|
|
11686
|
+
online: s.online,
|
|
11687
|
+
currentTask: snippet(s.currentTask),
|
|
11688
|
+
currentTool: rich?.currentTool ?? s.currentTool,
|
|
11689
|
+
iterations: rich?.iterations ?? s.iterations,
|
|
11690
|
+
toolCalls: rich?.toolCalls ?? s.toolCalls,
|
|
11691
|
+
lastSeenAt: s.lastSeenAt,
|
|
11692
|
+
...rich?.ctxPct !== void 0 ? { ctxPct: rich.ctxPct } : {},
|
|
11693
|
+
...rich?.costUsd !== void 0 ? { costUsd: rich.costUsd } : {}
|
|
11694
|
+
};
|
|
11695
|
+
});
|
|
11696
|
+
const peers = rows.filter((r) => !r.you);
|
|
11697
|
+
return {
|
|
11698
|
+
ok: true,
|
|
11699
|
+
you: identity.callerId,
|
|
11700
|
+
onlineCount: rows.filter((r) => r.online).length,
|
|
11701
|
+
agents: rows,
|
|
11702
|
+
summary: peers.length === 0 ? "No other agents are active on this project right now." : `${peers.length} peer agent(s): ${peers.map((p) => `${p.name} (${p.status}${p.currentTask ? ` \u2014 ${p.currentTask}` : ""})`).join("; ")}`
|
|
11703
|
+
};
|
|
11704
|
+
}
|
|
11705
|
+
};
|
|
11706
|
+
}
|
|
11707
|
+
var COLLAB_ID_PREFIXES = ["bug-hunter-", "refactor-planner-", "critic-"];
|
|
11708
|
+
var DEFAULTS = {
|
|
11709
|
+
intervalMs: 2e4,
|
|
11710
|
+
cooldownMs: 12e4,
|
|
11711
|
+
maxInterventionsPerSubagent: 3,
|
|
11712
|
+
pinnedWaitMs: 6e4,
|
|
11713
|
+
overloadPinnedThreshold: 2,
|
|
11714
|
+
backlogFactor: 2,
|
|
11715
|
+
stuckMs: 18e4,
|
|
11716
|
+
failureStreak: 2
|
|
11717
|
+
};
|
|
11718
|
+
var HISTORY_MAX = 100;
|
|
11719
|
+
var FleetSupervisor = class {
|
|
11720
|
+
constructor(opts) {
|
|
11721
|
+
this.opts = opts;
|
|
11722
|
+
const c = opts.config ?? {};
|
|
11723
|
+
this.cfg = {
|
|
11724
|
+
enabled: c.enabled !== false,
|
|
11725
|
+
intervalMs: c.intervalMs ?? DEFAULTS.intervalMs,
|
|
11726
|
+
cooldownMs: c.cooldownMs ?? DEFAULTS.cooldownMs,
|
|
11727
|
+
maxInterventionsPerSubagent: c.maxInterventionsPerSubagent ?? DEFAULTS.maxInterventionsPerSubagent,
|
|
11728
|
+
pinnedWaitMs: c.pinnedWaitMs ?? DEFAULTS.pinnedWaitMs,
|
|
11729
|
+
overloadPinnedThreshold: c.overloadPinnedThreshold ?? DEFAULTS.overloadPinnedThreshold,
|
|
11730
|
+
backlogFactor: c.backlogFactor ?? DEFAULTS.backlogFactor,
|
|
11731
|
+
stuckMs: c.stuckMs ?? DEFAULTS.stuckMs,
|
|
11732
|
+
failureStreak: c.failureStreak ?? DEFAULTS.failureStreak,
|
|
11733
|
+
allowSpawn: c.allowSpawn !== false,
|
|
11734
|
+
allowTerminate: c.allowTerminate === true
|
|
11735
|
+
};
|
|
11736
|
+
this.now = opts.now ?? Date.now;
|
|
11737
|
+
}
|
|
11738
|
+
opts;
|
|
11739
|
+
cfg;
|
|
11740
|
+
now;
|
|
11741
|
+
timer = null;
|
|
11742
|
+
offHandles = [];
|
|
11743
|
+
engaging = false;
|
|
11744
|
+
stopped = false;
|
|
11745
|
+
// ── observation state ─────────────────────────────────────────────────
|
|
11746
|
+
/** taskId → first tick timestamp we saw it pending. */
|
|
11747
|
+
pendingSince = /* @__PURE__ */ new Map();
|
|
11748
|
+
/** subagentId → last tool.executed timestamp (fleet-wide activity map). */
|
|
11749
|
+
lastActivityAt = /* @__PURE__ */ new Map();
|
|
11750
|
+
/** subagentId → consecutive non-success completions. */
|
|
11751
|
+
failStreaks = /* @__PURE__ */ new Map();
|
|
11752
|
+
/** Consecutive ticks the backlog condition held. */
|
|
11753
|
+
backlogTicks = 0;
|
|
11754
|
+
// ── rate-limit state ──────────────────────────────────────────────────
|
|
11755
|
+
/** `${kind}|${subject}` → last engagement ts. */
|
|
11756
|
+
lastEngagedAt = /* @__PURE__ */ new Map();
|
|
11757
|
+
interventionsBySubagent = /* @__PURE__ */ new Map();
|
|
11758
|
+
/** Tasks already retargeted once — never bounced twice. */
|
|
11759
|
+
retargetedTasks = /* @__PURE__ */ new Set();
|
|
11760
|
+
log = [];
|
|
11761
|
+
/** Idempotent; also re-arms after a stop() so `/supervisor on` works. */
|
|
11762
|
+
start() {
|
|
11763
|
+
if (!this.cfg.enabled || this.timer) return;
|
|
11764
|
+
this.stopped = false;
|
|
11765
|
+
this.offHandles.push(
|
|
11766
|
+
this.opts.fleet.filter("tool.executed", (e) => {
|
|
11767
|
+
this.lastActivityAt.set(e.subagentId, this.now());
|
|
11768
|
+
})
|
|
11769
|
+
);
|
|
11770
|
+
this.offHandles.push(
|
|
11771
|
+
this.opts.events.on("subagent.task_completed", (e) => {
|
|
11772
|
+
if (this.isCollab(e.subagentId)) return;
|
|
11773
|
+
if (e.status === "success") {
|
|
11774
|
+
this.failStreaks.delete(e.subagentId);
|
|
11775
|
+
} else {
|
|
11776
|
+
this.failStreaks.set(e.subagentId, (this.failStreaks.get(e.subagentId) ?? 0) + 1);
|
|
11777
|
+
}
|
|
11778
|
+
this.scheduleNudge();
|
|
11779
|
+
})
|
|
11780
|
+
);
|
|
11781
|
+
this.timer = setInterval(() => {
|
|
11782
|
+
void this.evaluate();
|
|
11783
|
+
}, this.cfg.intervalMs);
|
|
11784
|
+
this.timer.unref?.();
|
|
11785
|
+
}
|
|
11786
|
+
stop() {
|
|
11787
|
+
this.stopped = true;
|
|
11788
|
+
if (this.timer) clearInterval(this.timer);
|
|
11789
|
+
this.timer = null;
|
|
11790
|
+
if (this.nudgeTimer) clearTimeout(this.nudgeTimer);
|
|
11791
|
+
this.nudgeTimer = null;
|
|
11792
|
+
for (const off of this.offHandles.splice(0)) off();
|
|
11793
|
+
}
|
|
11794
|
+
history() {
|
|
11795
|
+
return this.log;
|
|
11796
|
+
}
|
|
11797
|
+
/** True while the evaluation loop is armed. */
|
|
11798
|
+
isRunning() {
|
|
11799
|
+
return this.timer !== null;
|
|
11800
|
+
}
|
|
11801
|
+
/** Resolved (defaulted) configuration — for status surfaces. */
|
|
11802
|
+
configSnapshot() {
|
|
11803
|
+
return { ...this.cfg };
|
|
11804
|
+
}
|
|
11805
|
+
nudgeTimer = null;
|
|
11806
|
+
scheduleNudge() {
|
|
11807
|
+
if (this.nudgeTimer || this.stopped || !this.cfg.enabled) return;
|
|
11808
|
+
this.nudgeTimer = setTimeout(() => {
|
|
11809
|
+
this.nudgeTimer = null;
|
|
11810
|
+
void this.evaluate();
|
|
11811
|
+
}, 5e3);
|
|
11812
|
+
this.nudgeTimer.unref?.();
|
|
11813
|
+
}
|
|
11814
|
+
isCollab(subagentId) {
|
|
11815
|
+
return COLLAB_ID_PREFIXES.some((p) => subagentId.startsWith(p));
|
|
11816
|
+
}
|
|
11817
|
+
/** One evaluation pass. Public for tests; production runs it on the tick. */
|
|
11818
|
+
async evaluate() {
|
|
11819
|
+
if (this.stopped || !this.cfg.enabled || this.engaging) return;
|
|
11820
|
+
if (this.opts.source.isWorkComplete()) return;
|
|
11821
|
+
const now = this.now();
|
|
11822
|
+
const subagents = this.opts.source.subagents().filter((s) => !this.isCollab(s.id));
|
|
11823
|
+
const pending = this.opts.source.listPendingTasks().filter((t) => !t.subagentId || !this.isCollab(t.subagentId));
|
|
11824
|
+
const pendingIds = new Set(pending.map((t) => t.id));
|
|
11825
|
+
for (const id of this.pendingSince.keys()) {
|
|
11826
|
+
if (!pendingIds.has(id)) this.pendingSince.delete(id);
|
|
11827
|
+
}
|
|
11828
|
+
for (const t of pending) {
|
|
11829
|
+
if (!this.pendingSince.has(t.id)) this.pendingSince.set(t.id, now);
|
|
11830
|
+
}
|
|
11831
|
+
const idle = subagents.filter((s) => s.status === "idle");
|
|
11832
|
+
const running = subagents.filter((s) => s.status === "running");
|
|
11833
|
+
const live = idle.length + running.length;
|
|
11834
|
+
for (const [subagentId, streak] of this.failStreaks) {
|
|
11835
|
+
if (streak >= this.cfg.failureStreak) {
|
|
11836
|
+
this.failStreaks.delete(subagentId);
|
|
11837
|
+
await this.engageFailureStreak(subagentId, streak);
|
|
11838
|
+
return;
|
|
11839
|
+
}
|
|
11840
|
+
}
|
|
11841
|
+
if (idle.length > 0) {
|
|
11842
|
+
const runningIds = new Set(running.map((s) => s.id));
|
|
11843
|
+
const pinnedByWorker = /* @__PURE__ */ new Map();
|
|
11844
|
+
for (const t of pending) {
|
|
11845
|
+
if (!t.subagentId || this.retargetedTasks.has(t.id)) continue;
|
|
11846
|
+
if (!runningIds.has(t.subagentId)) continue;
|
|
11847
|
+
const list = pinnedByWorker.get(t.subagentId) ?? [];
|
|
11848
|
+
list.push(t);
|
|
11849
|
+
pinnedByWorker.set(t.subagentId, list);
|
|
11850
|
+
}
|
|
11851
|
+
for (const [workerId, tasks] of pinnedByWorker) {
|
|
11852
|
+
const overloaded = tasks.length >= this.cfg.overloadPinnedThreshold;
|
|
11853
|
+
const starved = tasks.filter(
|
|
11854
|
+
(t) => now - (this.pendingSince.get(t.id) ?? now) >= this.cfg.pinnedWaitMs
|
|
11855
|
+
);
|
|
11856
|
+
if (overloaded || starved.length > 0) {
|
|
11857
|
+
const movable = overloaded ? tasks.slice(1) : starved;
|
|
11858
|
+
if (movable.length > 0) {
|
|
11859
|
+
await this.engageRebalance(
|
|
11860
|
+
overloaded ? "overloaded_worker" : "pinned_starvation",
|
|
11861
|
+
workerId,
|
|
11862
|
+
movable,
|
|
11863
|
+
idle.map((s) => s.id)
|
|
11864
|
+
);
|
|
11865
|
+
return;
|
|
11866
|
+
}
|
|
11867
|
+
}
|
|
11868
|
+
}
|
|
11869
|
+
}
|
|
11870
|
+
const backlogged = live > 0 && pending.length > this.cfg.backlogFactor * live;
|
|
11871
|
+
this.backlogTicks = backlogged ? this.backlogTicks + 1 : 0;
|
|
11872
|
+
if (backlogged && this.backlogTicks >= 2 && this.cfg.allowSpawn) {
|
|
11873
|
+
this.backlogTicks = 0;
|
|
11874
|
+
await this.engageBacklog(pending.length, live);
|
|
11875
|
+
return;
|
|
11876
|
+
}
|
|
11877
|
+
for (const s of running) {
|
|
11878
|
+
const lastSeen = this.lastActivityAt.get(s.id);
|
|
11879
|
+
if (lastSeen !== void 0 && now - lastSeen >= this.cfg.stuckMs) {
|
|
11880
|
+
this.lastActivityAt.set(s.id, now);
|
|
11881
|
+
await this.engageStuck(s);
|
|
11882
|
+
return;
|
|
11883
|
+
}
|
|
11884
|
+
}
|
|
11885
|
+
if (idle.length > 0 && pending.length > 0) {
|
|
11886
|
+
const oldest = Math.min(...pending.map((t) => this.pendingSince.get(t.id) ?? now));
|
|
11887
|
+
if (now - oldest >= this.cfg.pinnedWaitMs) {
|
|
11888
|
+
await this.engageIdleWithWork(idle.length, pending.length);
|
|
11889
|
+
}
|
|
11890
|
+
}
|
|
11891
|
+
}
|
|
11892
|
+
// ── engagements ────────────────────────────────────────────────────────
|
|
11893
|
+
cooldownOk(kind, subject) {
|
|
11894
|
+
const key = `${kind}|${subject}`;
|
|
11895
|
+
const last = this.lastEngagedAt.get(key) ?? 0;
|
|
11896
|
+
if (this.now() - last < this.cfg.cooldownMs) return false;
|
|
11897
|
+
this.lastEngagedAt.set(key, this.now());
|
|
11898
|
+
return true;
|
|
11899
|
+
}
|
|
11900
|
+
interventionBudgetOk(subagentId) {
|
|
11901
|
+
return (this.interventionsBySubagent.get(subagentId) ?? 0) < this.cfg.maxInterventionsPerSubagent;
|
|
11902
|
+
}
|
|
11903
|
+
recordIntervention(subagentId) {
|
|
11904
|
+
this.interventionsBySubagent.set(
|
|
11905
|
+
subagentId,
|
|
11906
|
+
(this.interventionsBySubagent.get(subagentId) ?? 0) + 1
|
|
11907
|
+
);
|
|
11908
|
+
}
|
|
11909
|
+
emitSignal(kind, detail, subagentId, taskId) {
|
|
11910
|
+
this.opts.events.emit("fleet.supervisor.signal", {
|
|
11911
|
+
sessionId: this.opts.sessionId?.(),
|
|
11912
|
+
kind,
|
|
11913
|
+
subagentId,
|
|
11914
|
+
taskId,
|
|
11915
|
+
detail
|
|
11916
|
+
});
|
|
11917
|
+
}
|
|
11918
|
+
record(entry) {
|
|
11919
|
+
this.log.push({ at: this.now(), ...entry });
|
|
11920
|
+
if (this.log.length > HISTORY_MAX) this.log.splice(0, this.log.length - HISTORY_MAX);
|
|
11921
|
+
this.opts.events.emit("fleet.supervisor.decision", {
|
|
11922
|
+
sessionId: this.opts.sessionId?.(),
|
|
11923
|
+
kind: entry.kind,
|
|
11924
|
+
proposedAction: entry.proposedAction,
|
|
11925
|
+
via: "brain",
|
|
11926
|
+
outcome: entry.outcome === "error" ? "skipped" : entry.outcome,
|
|
11927
|
+
rationale: entry.detail
|
|
11928
|
+
});
|
|
11929
|
+
}
|
|
11930
|
+
emitAction(action, ok, detail, subagentId, taskId) {
|
|
11931
|
+
this.opts.events.emit("fleet.supervisor.action", {
|
|
11932
|
+
sessionId: this.opts.sessionId?.(),
|
|
11933
|
+
action,
|
|
11934
|
+
subagentId,
|
|
11935
|
+
taskId,
|
|
11936
|
+
ok,
|
|
11937
|
+
detail
|
|
11938
|
+
});
|
|
11939
|
+
}
|
|
11940
|
+
/**
|
|
11941
|
+
* Ask the brain to approve a proposal. Same contract as BrainMonitor:
|
|
11942
|
+
* `source:'system'`, the rule's proposal `recommended:true`, fallback
|
|
11943
|
+
* 'ask_human' (routes to the LLM tier under the risk ceiling before any
|
|
11944
|
+
* human). Returns the chosen option id, or null for deny/escalate.
|
|
11945
|
+
*/
|
|
11946
|
+
async decide(question, context, options, risk) {
|
|
11947
|
+
const request = {
|
|
11948
|
+
id: `fleetsup-${randomUUID()}`,
|
|
11949
|
+
sessionId: this.opts.sessionId?.(),
|
|
11950
|
+
source: "system",
|
|
11951
|
+
question,
|
|
11952
|
+
context,
|
|
11953
|
+
options,
|
|
11954
|
+
risk,
|
|
11955
|
+
fallback: "ask_human"
|
|
11956
|
+
};
|
|
11957
|
+
const decision = await this.opts.brain.decide(request);
|
|
11958
|
+
if (decision.type !== "answer") return { choice: null, decision };
|
|
11959
|
+
const choice = decision.optionId ?? options.find((o) => o.recommended)?.id ?? null;
|
|
11960
|
+
return { choice, decision };
|
|
11961
|
+
}
|
|
11962
|
+
async engageRebalance(kind, fromWorkerId, tasks, idleWorkerIds) {
|
|
11963
|
+
if (!this.cooldownOk(kind, fromWorkerId) || !this.interventionBudgetOk(fromWorkerId)) return;
|
|
11964
|
+
this.engaging = true;
|
|
11965
|
+
try {
|
|
11966
|
+
const taskList = tasks.map((t) => `- ${t.id}: ${t.description.slice(0, 80)}`).join("\n");
|
|
11967
|
+
this.emitSignal(
|
|
11968
|
+
kind,
|
|
11969
|
+
`${tasks.length} pending task(s) pinned to busy worker ${fromWorkerId} while ${idleWorkerIds.length} worker(s) idle`,
|
|
11970
|
+
fromWorkerId,
|
|
11971
|
+
tasks[0]?.id
|
|
11972
|
+
);
|
|
11973
|
+
const { choice, decision } = await this.decide(
|
|
11974
|
+
`Worker ${fromWorkerId} is busy with ${tasks.length} more task(s) queued behind it while ${idleWorkerIds.length} sibling worker(s) sit idle. Rebalance the queued tasks to the idle workers?`,
|
|
11975
|
+
`Queued behind ${fromWorkerId}:
|
|
11976
|
+
${taskList}
|
|
11977
|
+
Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
11978
|
+
[
|
|
11979
|
+
{
|
|
11980
|
+
id: "rebalance",
|
|
11981
|
+
label: "Move the queued task(s) to idle worker(s)",
|
|
11982
|
+
consequence: "Only unstarted tasks move; the running task is untouched.",
|
|
11983
|
+
risk: "low",
|
|
11984
|
+
recommended: true
|
|
11985
|
+
},
|
|
11986
|
+
{ id: "wait", label: "Leave the queue as is", risk: "low" }
|
|
11987
|
+
],
|
|
11988
|
+
"low"
|
|
11989
|
+
);
|
|
11990
|
+
if (choice !== "rebalance") {
|
|
11991
|
+
this.record({
|
|
11992
|
+
kind,
|
|
11993
|
+
subagentId: fromWorkerId,
|
|
11994
|
+
proposedAction: "retarget",
|
|
11995
|
+
outcome: decision.type === "answer" ? "denied" : "escalated",
|
|
11996
|
+
detail: decision.type === "answer" ? decision.rationale ?? decision.text : decision.type
|
|
11997
|
+
});
|
|
11998
|
+
return;
|
|
11999
|
+
}
|
|
12000
|
+
this.recordIntervention(fromWorkerId);
|
|
12001
|
+
const moved = [];
|
|
12002
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
12003
|
+
const task = tasks[i];
|
|
12004
|
+
const target = idleWorkerIds[i % idleWorkerIds.length];
|
|
12005
|
+
if (!task || !target) continue;
|
|
12006
|
+
const ok = await this.opts.actions.retargetPendingTask(task.id, target);
|
|
12007
|
+
this.emitAction("retarget", ok, `\u2192 ${target}`, fromWorkerId, task.id);
|
|
12008
|
+
if (ok) {
|
|
12009
|
+
this.retargetedTasks.add(task.id);
|
|
12010
|
+
moved.push(`${task.id} \u2192 ${target}`);
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
12013
|
+
this.record({
|
|
12014
|
+
kind,
|
|
12015
|
+
subagentId: fromWorkerId,
|
|
12016
|
+
taskId: tasks[0]?.id,
|
|
12017
|
+
proposedAction: "retarget",
|
|
12018
|
+
outcome: "approved",
|
|
12019
|
+
detail: moved.length > 0 ? `moved ${moved.join(", ")}` : "nothing movable (already dispatched)"
|
|
12020
|
+
});
|
|
12021
|
+
if (moved.length > 0) {
|
|
12022
|
+
await this.opts.actions.steerAgent(
|
|
12023
|
+
fromWorkerId,
|
|
12024
|
+
"Workload reduced by fleet supervisor",
|
|
12025
|
+
`The fleet supervisor moved ${moved.length} of your queued task(s) to idle workers: ${moved.join(
|
|
12026
|
+
", "
|
|
12027
|
+
)}. Do not start them \u2014 focus on your current task.`
|
|
12028
|
+
).catch(() => {
|
|
12029
|
+
});
|
|
12030
|
+
await this.opts.actions.notifyLeader(
|
|
12031
|
+
"Fleet rebalanced",
|
|
12032
|
+
`Supervisor moved ${moved.length} queued task(s) off busy worker ${fromWorkerId}: ${moved.join(", ")}.`
|
|
12033
|
+
).catch(() => {
|
|
12034
|
+
});
|
|
12035
|
+
}
|
|
12036
|
+
} catch {
|
|
12037
|
+
} finally {
|
|
12038
|
+
this.engaging = false;
|
|
12039
|
+
}
|
|
12040
|
+
}
|
|
12041
|
+
async engageBacklog(pendingCount, liveWorkers) {
|
|
12042
|
+
if (!this.cooldownOk("backlog", "fleet")) return;
|
|
12043
|
+
this.engaging = true;
|
|
12044
|
+
try {
|
|
12045
|
+
this.emitSignal(
|
|
12046
|
+
"backlog",
|
|
12047
|
+
`${pendingCount} pending tasks vs ${liveWorkers} live worker(s)`
|
|
12048
|
+
);
|
|
12049
|
+
const { choice, decision } = await this.decide(
|
|
12050
|
+
`The task queue is deep: ${pendingCount} pending tasks for ${liveWorkers} live worker(s). Spawn one additional helper worker to drain it?`,
|
|
12051
|
+
`Pending: ${pendingCount}, live workers: ${liveWorkers}, factor threshold: ${this.cfg.backlogFactor}x`,
|
|
12052
|
+
[
|
|
12053
|
+
{
|
|
12054
|
+
id: "spawn",
|
|
12055
|
+
label: "Spawn one helper worker",
|
|
12056
|
+
consequence: "One more subagent joins the fleet and takes queued work.",
|
|
12057
|
+
risk: "medium",
|
|
12058
|
+
recommended: true
|
|
12059
|
+
},
|
|
12060
|
+
{ id: "wait", label: "Let the current fleet drain the queue", risk: "low" }
|
|
12061
|
+
],
|
|
12062
|
+
"medium"
|
|
12063
|
+
);
|
|
12064
|
+
if (choice !== "spawn") {
|
|
12065
|
+
this.record({
|
|
12066
|
+
kind: "backlog",
|
|
12067
|
+
proposedAction: "spawn_helper",
|
|
12068
|
+
outcome: decision.type === "answer" ? "denied" : "escalated",
|
|
12069
|
+
detail: decision.type === "answer" ? decision.rationale ?? decision.text : decision.type
|
|
12070
|
+
});
|
|
12071
|
+
return;
|
|
12072
|
+
}
|
|
12073
|
+
const res = await this.opts.actions.spawnHelper({
|
|
12074
|
+
reason: `queue backlog: ${pendingCount} pending / ${liveWorkers} workers`
|
|
12075
|
+
});
|
|
12076
|
+
if ("subagentId" in res) {
|
|
12077
|
+
this.emitAction("spawn_helper", true, res.subagentId, res.subagentId);
|
|
12078
|
+
this.record({
|
|
12079
|
+
kind: "backlog",
|
|
12080
|
+
subagentId: res.subagentId,
|
|
12081
|
+
proposedAction: "spawn_helper",
|
|
12082
|
+
outcome: "approved",
|
|
12083
|
+
detail: `spawned ${res.subagentId}`
|
|
12084
|
+
});
|
|
12085
|
+
await this.opts.actions.notifyLeader(
|
|
12086
|
+
"Fleet helper spawned",
|
|
12087
|
+
`Supervisor spawned helper ${res.subagentId} to drain a ${pendingCount}-task backlog.`
|
|
12088
|
+
).catch(() => {
|
|
12089
|
+
});
|
|
12090
|
+
} else {
|
|
12091
|
+
this.emitAction("spawn_helper", false, res.error);
|
|
12092
|
+
this.record({
|
|
12093
|
+
kind: "backlog",
|
|
12094
|
+
proposedAction: "spawn_helper",
|
|
12095
|
+
outcome: "error",
|
|
12096
|
+
detail: res.error
|
|
12097
|
+
});
|
|
12098
|
+
await this.opts.actions.notifyLeader(
|
|
12099
|
+
"Fleet backlog needs attention",
|
|
12100
|
+
`Queue is ${pendingCount} deep for ${liveWorkers} worker(s); supervisor could not spawn a helper (${res.error}). Consider rebalancing or finishing tasks before assigning more.`
|
|
12101
|
+
).catch(() => {
|
|
12102
|
+
});
|
|
12103
|
+
}
|
|
12104
|
+
} catch {
|
|
12105
|
+
} finally {
|
|
12106
|
+
this.engaging = false;
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
async engageStuck(s) {
|
|
12110
|
+
if (!this.cooldownOk("stuck_agent", s.id) || !this.interventionBudgetOk(s.id)) return;
|
|
12111
|
+
this.engaging = true;
|
|
12112
|
+
try {
|
|
12113
|
+
this.emitSignal(
|
|
12114
|
+
"stuck_agent",
|
|
12115
|
+
`no tool activity for \u2265${Math.round(this.cfg.stuckMs / 1e3)}s`,
|
|
12116
|
+
s.id
|
|
12117
|
+
);
|
|
12118
|
+
const { choice, decision } = await this.decide(
|
|
12119
|
+
`Worker ${s.name} (${s.id}) is marked running but produced no tool activity for over ${Math.round(this.cfg.stuckMs / 1e3)}s. Nudge it with a steer message?`,
|
|
12120
|
+
`Current task: ${s.currentTask ?? "unknown"}`,
|
|
12121
|
+
[
|
|
12122
|
+
{
|
|
12123
|
+
id: "steer",
|
|
12124
|
+
label: "Send a corrective steer to the worker",
|
|
12125
|
+
consequence: "A steer mail is injected before its next step.",
|
|
12126
|
+
risk: "low",
|
|
12127
|
+
recommended: true
|
|
12128
|
+
},
|
|
12129
|
+
{ id: "wait", label: "Give it more time", risk: "low" }
|
|
12130
|
+
],
|
|
12131
|
+
"medium"
|
|
12132
|
+
);
|
|
12133
|
+
if (choice !== "steer") {
|
|
12134
|
+
this.record({
|
|
12135
|
+
kind: "stuck_agent",
|
|
12136
|
+
subagentId: s.id,
|
|
12137
|
+
proposedAction: "steer",
|
|
12138
|
+
outcome: decision.type === "answer" ? "denied" : "escalated",
|
|
12139
|
+
detail: decision.type === "answer" ? decision.rationale ?? decision.text : decision.type
|
|
12140
|
+
});
|
|
12141
|
+
return;
|
|
12142
|
+
}
|
|
12143
|
+
this.recordIntervention(s.id);
|
|
12144
|
+
await this.opts.actions.steerAgent(
|
|
12145
|
+
s.id,
|
|
12146
|
+
"Progress check from fleet supervisor",
|
|
12147
|
+
`You appear stalled (no tool activity for ${Math.round(this.cfg.stuckMs / 1e3)}s on: ${s.currentTask ?? "your current task"}). If you are blocked, say so via mail_send to the leader and either narrow the scope or hand the task back \u2014 do not keep silently retrying the same step.`
|
|
12148
|
+
);
|
|
12149
|
+
this.emitAction("steer", true, "stuck nudge", s.id);
|
|
12150
|
+
await this.opts.actions.notifyLeader(
|
|
12151
|
+
`Worker ${s.name} may be stuck`,
|
|
12152
|
+
`No tool activity for ${Math.round(this.cfg.stuckMs / 1e3)}s. Supervisor nudged it; consider terminate_subagent + reassigning if it stays silent.`
|
|
12153
|
+
).catch(() => {
|
|
12154
|
+
});
|
|
12155
|
+
this.record({
|
|
12156
|
+
kind: "stuck_agent",
|
|
12157
|
+
subagentId: s.id,
|
|
12158
|
+
proposedAction: "steer",
|
|
12159
|
+
outcome: "approved",
|
|
12160
|
+
detail: "steered + leader notified"
|
|
12161
|
+
});
|
|
12162
|
+
} catch {
|
|
12163
|
+
} finally {
|
|
12164
|
+
this.engaging = false;
|
|
12165
|
+
}
|
|
12166
|
+
}
|
|
12167
|
+
async engageFailureStreak(subagentId, streak) {
|
|
12168
|
+
if (!this.cooldownOk("failure_streak", subagentId) || !this.interventionBudgetOk(subagentId)) {
|
|
12169
|
+
return;
|
|
12170
|
+
}
|
|
12171
|
+
this.engaging = true;
|
|
12172
|
+
try {
|
|
12173
|
+
this.emitSignal("failure_streak", `${streak} consecutive failed/timeout tasks`, subagentId);
|
|
12174
|
+
const options = [
|
|
12175
|
+
{
|
|
12176
|
+
id: "steer",
|
|
12177
|
+
label: "Steer the worker to change approach",
|
|
12178
|
+
risk: "low",
|
|
12179
|
+
recommended: true
|
|
12180
|
+
},
|
|
12181
|
+
{ id: "observe", label: "Keep observing", risk: "low" }
|
|
12182
|
+
];
|
|
12183
|
+
if (this.cfg.allowTerminate) {
|
|
12184
|
+
options.push({
|
|
12185
|
+
id: "terminate",
|
|
12186
|
+
label: "Terminate the worker",
|
|
12187
|
+
consequence: "Its current task fails; pending pinned tasks need reassignment.",
|
|
12188
|
+
risk: "high"
|
|
12189
|
+
});
|
|
12190
|
+
}
|
|
12191
|
+
const { choice, decision } = await this.decide(
|
|
12192
|
+
`Worker ${subagentId} produced ${streak} consecutive failed/timed-out tasks. Intervene?`,
|
|
12193
|
+
`Consecutive failures: ${streak}`,
|
|
12194
|
+
options,
|
|
12195
|
+
"high"
|
|
12196
|
+
);
|
|
12197
|
+
if (choice === "terminate" && this.cfg.allowTerminate) {
|
|
12198
|
+
this.recordIntervention(subagentId);
|
|
12199
|
+
await this.opts.actions.terminate(subagentId);
|
|
12200
|
+
this.emitAction("terminate", true, `${streak} consecutive failures`, subagentId);
|
|
12201
|
+
await this.opts.actions.notifyLeader(
|
|
12202
|
+
`Worker ${subagentId} terminated`,
|
|
12203
|
+
`Supervisor terminated ${subagentId} after ${streak} consecutive failures. Reassign its pending work.`
|
|
12204
|
+
).catch(() => {
|
|
12205
|
+
});
|
|
12206
|
+
this.record({
|
|
12207
|
+
kind: "failure_streak",
|
|
12208
|
+
subagentId,
|
|
12209
|
+
proposedAction: "terminate",
|
|
12210
|
+
outcome: "approved",
|
|
12211
|
+
detail: `terminated after ${streak} failures`
|
|
12212
|
+
});
|
|
12213
|
+
return;
|
|
12214
|
+
}
|
|
12215
|
+
if (choice === "steer") {
|
|
12216
|
+
this.recordIntervention(subagentId);
|
|
12217
|
+
await this.opts.actions.steerAgent(
|
|
12218
|
+
subagentId,
|
|
12219
|
+
"Change of approach required",
|
|
12220
|
+
`Your last ${streak} tasks failed. Stop, re-read the task, and take a different approach \u2014 if the task itself is impossible or under-specified, report that to the leader via mail_send instead of retrying.`
|
|
12221
|
+
);
|
|
12222
|
+
this.emitAction("steer", true, "failure streak", subagentId);
|
|
12223
|
+
await this.opts.actions.notifyLeader(
|
|
12224
|
+
`Worker ${subagentId} failing repeatedly`,
|
|
12225
|
+
`${streak} consecutive failures. Supervisor steered it; consider reassigning its work if the next task also fails.`
|
|
12226
|
+
).catch(() => {
|
|
12227
|
+
});
|
|
12228
|
+
this.record({
|
|
12229
|
+
kind: "failure_streak",
|
|
12230
|
+
subagentId,
|
|
12231
|
+
proposedAction: "steer",
|
|
12232
|
+
outcome: "approved",
|
|
12233
|
+
detail: `steered after ${streak} failures`
|
|
12234
|
+
});
|
|
12235
|
+
return;
|
|
12236
|
+
}
|
|
12237
|
+
this.record({
|
|
12238
|
+
kind: "failure_streak",
|
|
12239
|
+
subagentId,
|
|
12240
|
+
proposedAction: this.cfg.allowTerminate ? "steer|terminate" : "steer",
|
|
12241
|
+
outcome: decision.type === "answer" ? "denied" : "escalated",
|
|
12242
|
+
detail: decision.type === "answer" ? decision.rationale ?? decision.text : decision.type
|
|
12243
|
+
});
|
|
12244
|
+
} catch {
|
|
12245
|
+
} finally {
|
|
12246
|
+
this.engaging = false;
|
|
12247
|
+
}
|
|
12248
|
+
}
|
|
12249
|
+
async engageIdleWithWork(idleCount, pendingCount) {
|
|
12250
|
+
if (!this.cooldownOk("idle_with_work", "fleet")) return;
|
|
12251
|
+
this.engaging = true;
|
|
12252
|
+
try {
|
|
12253
|
+
this.emitSignal(
|
|
12254
|
+
"idle_with_work",
|
|
12255
|
+
`${idleCount} idle worker(s) while ${pendingCount} task(s) cannot dispatch`
|
|
12256
|
+
);
|
|
12257
|
+
await this.opts.actions.notifyLeader(
|
|
12258
|
+
"Idle workers with undispatchable tasks",
|
|
12259
|
+
`${idleCount} worker(s) idle while ${pendingCount} task(s) stay pending (likely pinned to stopped/errored workers). Reassign them with assign_task or let the supervisor retarget by unpinning.`
|
|
12260
|
+
);
|
|
12261
|
+
this.emitAction("notify_leader", true, "idle_with_work");
|
|
12262
|
+
this.record({
|
|
12263
|
+
kind: "idle_with_work",
|
|
12264
|
+
proposedAction: "notify_leader",
|
|
12265
|
+
outcome: "approved",
|
|
12266
|
+
detail: `${idleCount} idle / ${pendingCount} pending`
|
|
12267
|
+
});
|
|
12268
|
+
} catch {
|
|
12269
|
+
} finally {
|
|
12270
|
+
this.engaging = false;
|
|
12271
|
+
}
|
|
12272
|
+
}
|
|
12273
|
+
};
|
|
11320
12274
|
var MAILBOX_FILE2 = "_mailbox.jsonl";
|
|
11321
12275
|
var LINE_SEPARATOR2 = "\n";
|
|
11322
12276
|
var MESSAGE_CACHE_MAX_ENTRIES2 = 1e4;
|
|
@@ -12041,7 +12995,8 @@ function detectEcosystem(manifestPath) {
|
|
|
12041
12995
|
if (name === "package.json") return "npm";
|
|
12042
12996
|
if (name === "go.mod") return "go";
|
|
12043
12997
|
if (name === "cargo.toml") return "cargo";
|
|
12044
|
-
if (name === "pyproject.toml" || name === "requirements.txt" || name === "pipfile" || name === "pipfile.lock")
|
|
12998
|
+
if (name === "pyproject.toml" || name === "requirements.txt" || name === "pipfile" || name === "pipfile.lock")
|
|
12999
|
+
return "pip";
|
|
12045
13000
|
if (name === "gemfile" || name === "gemfile.lock") return "gem";
|
|
12046
13001
|
if (name === "composer.json" || name === "composer.lock") return "composer";
|
|
12047
13002
|
if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
|
|
@@ -12050,7 +13005,7 @@ function detectEcosystem(manifestPath) {
|
|
|
12050
13005
|
if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
|
|
12051
13006
|
if (name === "vcpkg.json") return "vcpkg";
|
|
12052
13007
|
if (name === "conanfile.txt" || name === "conanfile.py") return "conan";
|
|
12053
|
-
if (name === "
|
|
13008
|
+
if (name === "cmakelists.txt") return "cmake";
|
|
12054
13009
|
return "unknown";
|
|
12055
13010
|
}
|
|
12056
13011
|
async function recordPackageAction(opts, entry) {
|
|
@@ -12081,9 +13036,7 @@ async function getPackageAuthor(opts, manifestPath, packageName) {
|
|
|
12081
13036
|
async function getManifestPackages(opts, manifestPath) {
|
|
12082
13037
|
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
12083
13038
|
const normalizedManifest = manifestPath.replace(/\\/g, "/");
|
|
12084
|
-
return log.entries.filter(
|
|
12085
|
-
(e) => e.manifestPath.replace(/\\/g, "/") === normalizedManifest
|
|
12086
|
-
);
|
|
13039
|
+
return log.entries.filter((e) => e.manifestPath.replace(/\\/g, "/") === normalizedManifest);
|
|
12087
13040
|
}
|
|
12088
13041
|
async function getPackagesByAgent(opts, agentId) {
|
|
12089
13042
|
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
@@ -12130,7 +13083,7 @@ function parseOutdatedPackages(body) {
|
|
|
12130
13083
|
latestVersion: cols[2] ?? "",
|
|
12131
13084
|
wantedVersion: cols[3] ?? "",
|
|
12132
13085
|
manifestPath: cols[4] ?? "",
|
|
12133
|
-
ecosystem:
|
|
13086
|
+
ecosystem: detectEcosystem(cols[4] ?? "")
|
|
12134
13087
|
});
|
|
12135
13088
|
}
|
|
12136
13089
|
}
|
|
@@ -12151,20 +13104,6 @@ function parseOutdatedPackages(body) {
|
|
|
12151
13104
|
}
|
|
12152
13105
|
return results;
|
|
12153
13106
|
}
|
|
12154
|
-
function detectEcosystem2(manifestPath) {
|
|
12155
|
-
const name = manifestPath.split("/").pop()?.split("\\").pop() ?? manifestPath;
|
|
12156
|
-
if (name === "package.json") return "npm";
|
|
12157
|
-
if (name === "go.mod") return "go";
|
|
12158
|
-
if (name === "cargo.toml") return "cargo";
|
|
12159
|
-
if (name === "pyproject.toml" || name === "requirements.txt") return "pip";
|
|
12160
|
-
if (name === "gemfile" || name === "gemfile.lock") return "gem";
|
|
12161
|
-
if (name === "composer.json" || name === "composer.lock") return "composer";
|
|
12162
|
-
if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
|
|
12163
|
-
if (name === "mix.exs" || name === "mix.lock") return "elixir";
|
|
12164
|
-
if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
|
|
12165
|
-
if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
|
|
12166
|
-
return "unknown";
|
|
12167
|
-
}
|
|
12168
13107
|
function startPackageOutdatedWatcher(opts) {
|
|
12169
13108
|
const {
|
|
12170
13109
|
mailbox,
|
|
@@ -12214,11 +13153,7 @@ function startPackageOutdatedWatcher(opts) {
|
|
|
12214
13153
|
log(`[pkg-outdated-watcher] Processing ${entries.length} outdated package(s) from ${msg.from}`);
|
|
12215
13154
|
for (const entry of entries) {
|
|
12216
13155
|
try {
|
|
12217
|
-
const author = await getPackageAuthor(
|
|
12218
|
-
packageTrackerOpts,
|
|
12219
|
-
entry.manifestPath,
|
|
12220
|
-
entry.name
|
|
12221
|
-
);
|
|
13156
|
+
const author = await getPackageAuthor(packageTrackerOpts, entry.manifestPath, entry.name);
|
|
12222
13157
|
const notifyTarget = author?.agentId ?? "*";
|
|
12223
13158
|
const notifyBody = buildNotifyBody(entry, author?.agentName);
|
|
12224
13159
|
const notifyMsg = {
|
|
@@ -12273,13 +13208,7 @@ function buildNotifyBody(entry, authorName) {
|
|
|
12273
13208
|
`This package appears to have been added by an agent no longer on record. Consider reviewing and updating it.`
|
|
12274
13209
|
);
|
|
12275
13210
|
}
|
|
12276
|
-
lines.push(
|
|
12277
|
-
"",
|
|
12278
|
-
`Update with:`,
|
|
12279
|
-
`\`\`\``,
|
|
12280
|
-
`${getUpdateCommand(entry)}`,
|
|
12281
|
-
`\`\`\``
|
|
12282
|
-
);
|
|
13211
|
+
lines.push("", `Update with:`, `\`\`\``, `${getUpdateCommand(entry)}`, `\`\`\``);
|
|
12283
13212
|
return lines.join("\n");
|
|
12284
13213
|
}
|
|
12285
13214
|
function getUpdateCommand(entry) {
|
|
@@ -15343,6 +16272,6 @@ ${input.detail}`
|
|
|
15343
16272
|
}
|
|
15344
16273
|
};
|
|
15345
16274
|
|
|
15346
|
-
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AdaptiveConcurrencyController, AgentMonitorService, AutonomousBrain, AutonomousCoordinator, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, ChangeManager, CollabSession, ConsensusProtocol, DECISION_TIMEOUT_MS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_QUALITY_CHECKS, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, KnowledgeGraph, LIGHT_BUDGET, LargeAnswerStore, MAILBOX_BRIDGE_LOCK_FILENAME, MAILBOX_BRIDGE_TOKEN_FILENAME, MAILBOX_HEALTH_DEFAULT_FAILURE_THRESHOLD, MAILBOX_HEALTH_DEFAULT_FROM, MAILBOX_HEALTH_DEFAULT_INTERVAL_MS, MAILBOX_HEALTH_DEFAULT_TIMEOUT_MS, MEDIUM_BUDGET, META_AGENTS, MailboxHealthWatchdog, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TaskAuctioneer, TaskDAG, VERIFY_AGENTS, acquireOrJoin, actionToAckInput, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, buildDownAlert, buildRecoveryAlert, composeDirectorPrompt, composeSubagentPrompt, createAgentMonitorService, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, finalize, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetTool as makeFleetHealthTool, makeFleetTool as makeFleetSessionTool,
|
|
16275
|
+
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AdaptiveConcurrencyController, AgentMonitorService, AutonomousBrain, AutonomousCoordinator, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, ChangeManager, CollabSession, ConsensusProtocol, DECISION_TIMEOUT_MS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_QUALITY_CHECKS, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetSupervisor, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, KnowledgeGraph, LIGHT_BUDGET, LargeAnswerStore, MAILBOX_BRIDGE_LOCK_FILENAME, MAILBOX_BRIDGE_TOKEN_FILENAME, MAILBOX_HEALTH_DEFAULT_FAILURE_THRESHOLD, MAILBOX_HEALTH_DEFAULT_FROM, MAILBOX_HEALTH_DEFAULT_INTERVAL_MS, MAILBOX_HEALTH_DEFAULT_TIMEOUT_MS, MEDIUM_BUDGET, META_AGENTS, MailboxHealthWatchdog, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TaskAuctioneer, TaskDAG, VERIFY_AGENTS, acquireOrJoin, actionToAckInput, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, buildDownAlert, buildRecoveryAlert, composeDirectorPrompt, composeSubagentPrompt, createAgentMonitorService, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, finalize, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetTool as makeFleetHealthTool, makeFleetTool as makeFleetSessionTool, makeFleetStatusTool, makeFleetTool, makeFleetTool as makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateAllTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, readLiveLock, recordPackageAction, release, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, validateWatchdogOptions, withDisabledToolFiltering };
|
|
15347
16276
|
//# sourceMappingURL=index.js.map
|
|
15348
16277
|
//# sourceMappingURL=index.js.map
|