@wrongstack/core 0.260.0 → 0.265.1
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-bridge-BbskZ7HH.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
- package/dist/{agent-subagent-runner-BNIGZx18.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +116 -12
- package/dist/{brain-C2yDd7Lw.d.ts → brain-BXd_61kQ.d.ts} +32 -3
- package/dist/{compactor-t0R_AIt_.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
- package/dist/{config-FG6As4H5.d.ts → config-BMCj_XDs.d.ts} +86 -12
- package/dist/{context-JFOVvu6z.d.ts → context-MRk5PhNv.d.ts} +26 -1
- package/dist/coordination/index.d.ts +1737 -15
- package/dist/coordination/index.js +3152 -494
- package/dist/coordination/index.js.map +1 -1
- package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
- package/dist/defaults/index.d.ts +28 -28
- package/dist/defaults/index.js +1804 -1363
- package/dist/defaults/index.js.map +1 -1
- package/dist/dispatcher-types.d-BBeXBQgS.d.ts +66 -0
- package/dist/execution/index.d.ts +16 -16
- package/dist/execution/index.js +933 -672
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +1 -1
- package/dist/execution/prompt-enhancer.js +7 -1
- package/dist/execution/prompt-enhancer.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js.map +1 -1
- package/dist/{goal-preamble-B1IXJtLX.d.ts → goal-preamble-DvHDSKSe.d.ts} +26 -10
- package/dist/{goal-store-CPXz6Mml.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
- package/dist/{index-CebbJB94.d.ts → index-B-ch8K9C.d.ts} +8 -8
- package/dist/{index-BPcg4N3M.d.ts → index-CEDeNodM.d.ts} +5 -5
- package/dist/index.d.ts +189 -104
- package/dist/index.js +24693 -21162
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/infrastructure/index.js +12 -8
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +7 -2
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-DXxI2tlu.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
- package/dist/{mcp-servers-OwNHo43-.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +80 -31
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-Djlmq4uB.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-CEmrSCMJ.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +2 -2
- package/dist/{null-fleet-bus-DT92xqgJ.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/observability/index.js +8 -3
- package/dist/observability/index.js.map +1 -1
- package/dist/{parallel-eternal-engine-0SItuq5r.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
- package/dist/{path-resolver-DKBh6Jlo.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
- package/dist/{permission-BJ7eO9Vl.d.ts → permission-B9SB45lp.d.ts} +1 -1
- package/dist/{permission-policy-DEXOfnpm.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
- package/dist/{pipeline-zflkI2dp.d.ts → pipeline-DPDxH_7m.d.ts} +59 -4
- package/dist/{plan-templates-BFXyRkEK.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
- package/dist/{provider-runner-BC-uywtT.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
- package/dist/{retry-policy-Cavrzmtk.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +313 -122
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-CDvDYXWX.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
- package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
- package/dist/security/index.d.ts +5 -5
- package/dist/security/index.js +411 -225
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-B7AivHsu.d.ts → selector-CzHh_igB.d.ts} +1 -1
- package/dist/{session-event-bridge-BmIDxdJd.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +8 -2
- package/dist/{session-reader-DtofsB-2.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
- package/dist/skills/index.js +67 -64
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +132 -16
- package/dist/storage/index.js +851 -432
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +57 -0
- package/dist/tools/index.js +411 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/index.d.ts +21 -21
- package/dist/types/index.js +928 -711
- package/dist/types/index.js.map +1 -1
- package/dist/utils/error.d.ts +7 -0
- package/dist/utils/error.js +8 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/index.d.ts +8 -68
- package/dist/utils/index.js +20 -10
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
- package/package.json +5 -1
- package/skills/api-design/SKILL.md +1 -1
- package/skills/audit-log/SKILL.md +6 -6
- package/skills/bug-hunter/SKILL.md +5 -5
- package/skills/chimera/SKILL.md +4 -4
- package/skills/docker-deploy/SKILL.md +1 -1
- package/skills/git-flow/SKILL.md +3 -3
- package/skills/multi-agent/SKILL.md +3 -3
- package/skills/node-modern/SKILL.md +1 -0
- package/skills/observability/SKILL.md +2 -2
- package/skills/output-standards/SKILL.md +51 -28
- package/skills/refactor-planner/SKILL.md +3 -3
- package/skills/security-scanner/SKILL.md +4 -3
- package/skills/tech-stack/SKILL.md +1 -2
- package/dist/package-outdated-watcher-C70ag2G9.d.ts +0 -581
- package/dist/secret-vault-BJDY28ev.d.ts +0 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID, createHash, randomBytes } from 'crypto';
|
|
2
2
|
import * as fsp6 from 'fs/promises';
|
|
3
|
-
import * as
|
|
3
|
+
import * as path5 from 'path';
|
|
4
4
|
import { isAbsolute, resolve } from 'path';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { hostname } from 'os';
|
|
@@ -152,9 +152,9 @@ function formatHumanPrompt(request) {
|
|
|
152
152
|
return lines.join("\n");
|
|
153
153
|
}
|
|
154
154
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
155
|
-
const dir =
|
|
155
|
+
const dir = path5.dirname(targetPath);
|
|
156
156
|
await fsp6.mkdir(dir, { recursive: true });
|
|
157
|
-
const tmp =
|
|
157
|
+
const tmp = path5.join(dir, `.${path5.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
158
158
|
try {
|
|
159
159
|
if (typeof content === "string") {
|
|
160
160
|
await fsp6.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
@@ -172,8 +172,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
172
172
|
}
|
|
173
173
|
let mode;
|
|
174
174
|
try {
|
|
175
|
-
const
|
|
176
|
-
mode =
|
|
175
|
+
const stat6 = await fsp6.stat(targetPath);
|
|
176
|
+
mode = stat6.mode & 511;
|
|
177
177
|
} catch {
|
|
178
178
|
mode = opts.mode;
|
|
179
179
|
}
|
|
@@ -193,9 +193,9 @@ async function ensureDir(dir) {
|
|
|
193
193
|
await fsp6.mkdir(dir, { recursive: true });
|
|
194
194
|
}
|
|
195
195
|
async function withFileLock(targetPath, fn, opts = {}) {
|
|
196
|
-
const dir =
|
|
196
|
+
const dir = path5.dirname(targetPath);
|
|
197
197
|
await fsp6.mkdir(dir, { recursive: true });
|
|
198
|
-
const lockPath =
|
|
198
|
+
const lockPath = path5.join(dir, `.${path5.basename(targetPath)}.lock`);
|
|
199
199
|
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
200
200
|
const staleMs = opts.staleMs ?? 3e4;
|
|
201
201
|
const started = Date.now();
|
|
@@ -206,10 +206,15 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
206
206
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
207
207
|
break;
|
|
208
208
|
} catch (err) {
|
|
209
|
-
|
|
209
|
+
const code = err.code;
|
|
210
|
+
if (code === "ENOENT") {
|
|
211
|
+
await fsp6.mkdir(dir, { recursive: true });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (code !== "EEXIST") throw err;
|
|
210
215
|
try {
|
|
211
|
-
const
|
|
212
|
-
if (Date.now() -
|
|
216
|
+
const stat6 = await fsp6.stat(lockPath);
|
|
217
|
+
if (Date.now() - stat6.mtimeMs > staleMs) {
|
|
213
218
|
await fsp6.unlink(lockPath);
|
|
214
219
|
continue;
|
|
215
220
|
}
|
|
@@ -259,6 +264,11 @@ async function renameWithRetry(from, to) {
|
|
|
259
264
|
throw lastErr;
|
|
260
265
|
}
|
|
261
266
|
|
|
267
|
+
// src/utils/error.ts
|
|
268
|
+
function toErrorMessage(err) {
|
|
269
|
+
return err instanceof Error ? err.message : String(err);
|
|
270
|
+
}
|
|
271
|
+
|
|
262
272
|
// src/storage/director-state.ts
|
|
263
273
|
async function acquireDirectorStateLock(lockPath, processId = process.pid) {
|
|
264
274
|
let existing;
|
|
@@ -412,7 +422,7 @@ var DirectorStateCheckpoint = class {
|
|
|
412
422
|
} catch (err) {
|
|
413
423
|
console.warn(
|
|
414
424
|
"[director-state] checkpoint write failed:",
|
|
415
|
-
|
|
425
|
+
toErrorMessage(err)
|
|
416
426
|
);
|
|
417
427
|
} finally {
|
|
418
428
|
this.writing = false;
|
|
@@ -434,11 +444,246 @@ function safeParse(input, maxBytes = 5e6) {
|
|
|
434
444
|
} catch (err) {
|
|
435
445
|
return {
|
|
436
446
|
ok: false,
|
|
437
|
-
error:
|
|
447
|
+
error: toErrorMessage(err)
|
|
438
448
|
};
|
|
439
449
|
}
|
|
440
450
|
}
|
|
441
451
|
|
|
452
|
+
// src/utils/string.ts
|
|
453
|
+
function truncate(s, max) {
|
|
454
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/utils/expect-defined.ts
|
|
458
|
+
function expectDefined(value, label) {
|
|
459
|
+
if (value === null || value === void 0) {
|
|
460
|
+
const err = new Error("Expected value to be defined");
|
|
461
|
+
err.name = "ExpectDefinedError";
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
function projectSlug(absRoot) {
|
|
467
|
+
const base = slugify(path5.basename(absRoot));
|
|
468
|
+
const hash = createHash("sha256").update(path5.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
469
|
+
return `${base}-${hash}`;
|
|
470
|
+
}
|
|
471
|
+
function slugify(name) {
|
|
472
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
473
|
+
}
|
|
474
|
+
function wstackGlobalRoot() {
|
|
475
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
476
|
+
if (fromEnv && fromEnv.trim().length > 0) return path5.resolve(fromEnv);
|
|
477
|
+
return path5.join(os.homedir(), ".wrongstack");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/utils/message-invariants.ts
|
|
481
|
+
function repairToolUseAdjacency(messages) {
|
|
482
|
+
const removedToolUses = [];
|
|
483
|
+
const removedToolResults = [];
|
|
484
|
+
let removedMessages = 0;
|
|
485
|
+
let changed = false;
|
|
486
|
+
const out = [];
|
|
487
|
+
for (let i = 0; i < messages.length; i++) {
|
|
488
|
+
const original = expectDefined(messages[i]);
|
|
489
|
+
let msg = original;
|
|
490
|
+
if (hasToolUse(msg)) {
|
|
491
|
+
const nextIds = toolResultIds(messages[i + 1]);
|
|
492
|
+
const filtered = mapContent(msg, (blocks) => {
|
|
493
|
+
const next = [];
|
|
494
|
+
for (const block of blocks) {
|
|
495
|
+
if (block.type === "tool_use" && !nextIds.has(block.id)) {
|
|
496
|
+
removedToolUses.push(block.id);
|
|
497
|
+
changed = true;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
next.push(block);
|
|
501
|
+
}
|
|
502
|
+
return next;
|
|
503
|
+
});
|
|
504
|
+
msg = filtered ?? msg;
|
|
505
|
+
}
|
|
506
|
+
if (hasToolResult(msg)) {
|
|
507
|
+
const allowed = toolUseIds(out[out.length - 1]);
|
|
508
|
+
const filtered = mapContent(msg, (blocks) => {
|
|
509
|
+
const next = [];
|
|
510
|
+
for (const block of blocks) {
|
|
511
|
+
if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
|
|
512
|
+
removedToolResults.push(block.tool_use_id);
|
|
513
|
+
changed = true;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
next.push(block);
|
|
517
|
+
}
|
|
518
|
+
return next;
|
|
519
|
+
});
|
|
520
|
+
msg = filtered ?? msg;
|
|
521
|
+
}
|
|
522
|
+
if (isEmptyMessage(msg)) {
|
|
523
|
+
removedMessages++;
|
|
524
|
+
changed = true;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
out.push(msg);
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
messages: changed ? out : messages,
|
|
531
|
+
report: { changed, removedToolUses, removedToolResults, removedMessages }
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function hasToolUse(msg) {
|
|
535
|
+
return contentBlocks(msg).some((b) => b.type === "tool_use");
|
|
536
|
+
}
|
|
537
|
+
function hasToolResult(msg) {
|
|
538
|
+
return contentBlocks(msg).some((b) => b.type === "tool_result");
|
|
539
|
+
}
|
|
540
|
+
function toolUseIds(msg) {
|
|
541
|
+
const ids = /* @__PURE__ */ new Set();
|
|
542
|
+
if (!msg || msg.role !== "assistant") return ids;
|
|
543
|
+
for (const block of contentBlocks(msg)) {
|
|
544
|
+
if (block.type === "tool_use") ids.add(block.id);
|
|
545
|
+
}
|
|
546
|
+
return ids;
|
|
547
|
+
}
|
|
548
|
+
function toolResultIds(msg) {
|
|
549
|
+
const ids = /* @__PURE__ */ new Set();
|
|
550
|
+
if (!msg || msg.role !== "user") return ids;
|
|
551
|
+
for (const block of contentBlocks(msg)) {
|
|
552
|
+
if (block.type === "tool_result") ids.add(block.tool_use_id);
|
|
553
|
+
}
|
|
554
|
+
return ids;
|
|
555
|
+
}
|
|
556
|
+
function contentBlocks(msg) {
|
|
557
|
+
return msg && Array.isArray(msg.content) ? msg.content : [];
|
|
558
|
+
}
|
|
559
|
+
function mapContent(msg, fn) {
|
|
560
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
561
|
+
const next = fn(msg.content);
|
|
562
|
+
if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
|
|
563
|
+
return msg;
|
|
564
|
+
}
|
|
565
|
+
return { ...msg, content: next };
|
|
566
|
+
}
|
|
567
|
+
function isEmptyMessage(msg) {
|
|
568
|
+
if (typeof msg.content === "string") return msg.content.trim().length === 0;
|
|
569
|
+
return msg.content.length === 0;
|
|
570
|
+
}
|
|
571
|
+
var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
|
|
572
|
+
var IS_WINDOWS = process.platform === "win32";
|
|
573
|
+
var SEP = IS_WINDOWS ? "\\" : "/";
|
|
574
|
+
function isGlob(p) {
|
|
575
|
+
for (const c of p) {
|
|
576
|
+
if (GLOB_CHARS.has(c)) return true;
|
|
577
|
+
}
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
function globToRegex(pat) {
|
|
581
|
+
let i = 0;
|
|
582
|
+
let re = "^";
|
|
583
|
+
while (i < pat.length) {
|
|
584
|
+
const c = expectDefined(pat[i]);
|
|
585
|
+
if (c === "*") {
|
|
586
|
+
if (pat[i + 1] === "*") {
|
|
587
|
+
re += ".*";
|
|
588
|
+
i += 2;
|
|
589
|
+
if (pat[i] === "/") i++;
|
|
590
|
+
} else {
|
|
591
|
+
re += "[^/\\\\]*";
|
|
592
|
+
i++;
|
|
593
|
+
}
|
|
594
|
+
} else if (c === "?") {
|
|
595
|
+
re += "[^/\\\\]";
|
|
596
|
+
i++;
|
|
597
|
+
} else if (c === "[") {
|
|
598
|
+
let cls = "[";
|
|
599
|
+
i++;
|
|
600
|
+
if (pat[i] === "!" || pat[i] === "^") {
|
|
601
|
+
cls += "^";
|
|
602
|
+
i++;
|
|
603
|
+
}
|
|
604
|
+
while (i < pat.length && pat[i] !== "]") {
|
|
605
|
+
const ch = pat[i] ?? "";
|
|
606
|
+
if (ch === "\\") cls += "\\\\";
|
|
607
|
+
else if (ch === "]" || ch === "^") cls += `\\${ch}`;
|
|
608
|
+
else cls += ch;
|
|
609
|
+
i++;
|
|
610
|
+
}
|
|
611
|
+
cls += "]";
|
|
612
|
+
re += cls;
|
|
613
|
+
i++;
|
|
614
|
+
} else {
|
|
615
|
+
re += c.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
616
|
+
i++;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return new RegExp(re + "$");
|
|
620
|
+
}
|
|
621
|
+
function baseDir(pat) {
|
|
622
|
+
let i = pat.length - 1;
|
|
623
|
+
while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
|
|
624
|
+
const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
|
|
625
|
+
return cut < 0 ? "." : pat.slice(0, cut);
|
|
626
|
+
}
|
|
627
|
+
async function expandGlob(pattern) {
|
|
628
|
+
if (!isGlob(pattern)) return [pattern];
|
|
629
|
+
const results = /* @__PURE__ */ new Set();
|
|
630
|
+
const abs = isAbsolute(pattern);
|
|
631
|
+
const base = abs ? baseDir(pattern) : baseDir(pattern);
|
|
632
|
+
const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
|
|
633
|
+
async function walk(dir, pat) {
|
|
634
|
+
let entries;
|
|
635
|
+
try {
|
|
636
|
+
entries = await fsp6.readdir(dir);
|
|
637
|
+
} catch {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const firstGlob = pat.search(/[*?[[]/);
|
|
641
|
+
if (firstGlob < 0) {
|
|
642
|
+
const re = globToRegex(pat);
|
|
643
|
+
for (const e of entries) {
|
|
644
|
+
if (re.test(e)) {
|
|
645
|
+
const full = `${dir}${SEP}${e}`;
|
|
646
|
+
results.add(abs ? resolve(full) : full);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const before = pat.slice(0, firstGlob);
|
|
652
|
+
const rest = pat.slice(firstGlob);
|
|
653
|
+
if (before.endsWith("**")) {
|
|
654
|
+
await walk(dir, rest);
|
|
655
|
+
for (const e of entries) {
|
|
656
|
+
const full = `${dir}${SEP}${e}`;
|
|
657
|
+
try {
|
|
658
|
+
const stat6 = await fsp6.stat(full);
|
|
659
|
+
if (stat6.isDirectory()) await walk(full, rest);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} else if (before === "") {
|
|
664
|
+
const re = globToRegex(rest);
|
|
665
|
+
for (const e of entries) {
|
|
666
|
+
if (re.test(e)) {
|
|
667
|
+
const full = `${dir}${SEP}${e}`;
|
|
668
|
+
results.add(abs ? resolve(full) : full);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
|
|
673
|
+
if (entries.includes(seg)) {
|
|
674
|
+
const full = `${dir}${SEP}${seg}`;
|
|
675
|
+
try {
|
|
676
|
+
const stat6 = await fsp6.stat(full);
|
|
677
|
+
if (stat6.isDirectory()) await walk(full, rest);
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
await walk(base === "." ? "." : base, relPat);
|
|
684
|
+
return [...results];
|
|
685
|
+
}
|
|
686
|
+
|
|
442
687
|
// src/types/errors.ts
|
|
443
688
|
var ERROR_CODES = {
|
|
444
689
|
// Provider
|
|
@@ -657,133 +902,6 @@ function createMessage(type, from, payload, to) {
|
|
|
657
902
|
priority: "normal"
|
|
658
903
|
};
|
|
659
904
|
}
|
|
660
|
-
|
|
661
|
-
// src/utils/expect-defined.ts
|
|
662
|
-
function expectDefined(value, label) {
|
|
663
|
-
if (value === null || value === void 0) {
|
|
664
|
-
const err = new Error("Expected value to be defined");
|
|
665
|
-
err.name = "ExpectDefinedError";
|
|
666
|
-
throw err;
|
|
667
|
-
}
|
|
668
|
-
return value;
|
|
669
|
-
}
|
|
670
|
-
var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
|
|
671
|
-
var IS_WINDOWS = process.platform === "win32";
|
|
672
|
-
var SEP = IS_WINDOWS ? "\\" : "/";
|
|
673
|
-
function isGlob(p) {
|
|
674
|
-
for (const c of p) {
|
|
675
|
-
if (GLOB_CHARS.has(c)) return true;
|
|
676
|
-
}
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
function globToRegex(pat) {
|
|
680
|
-
let i = 0;
|
|
681
|
-
let re = "^";
|
|
682
|
-
while (i < pat.length) {
|
|
683
|
-
const c = expectDefined(pat[i]);
|
|
684
|
-
if (c === "*") {
|
|
685
|
-
if (pat[i + 1] === "*") {
|
|
686
|
-
re += ".*";
|
|
687
|
-
i += 2;
|
|
688
|
-
if (pat[i] === "/") i++;
|
|
689
|
-
} else {
|
|
690
|
-
re += "[^/\\\\]*";
|
|
691
|
-
i++;
|
|
692
|
-
}
|
|
693
|
-
} else if (c === "?") {
|
|
694
|
-
re += "[^/\\\\]";
|
|
695
|
-
i++;
|
|
696
|
-
} else if (c === "[") {
|
|
697
|
-
let cls = "[";
|
|
698
|
-
i++;
|
|
699
|
-
if (pat[i] === "!" || pat[i] === "^") {
|
|
700
|
-
cls += "^";
|
|
701
|
-
i++;
|
|
702
|
-
}
|
|
703
|
-
while (i < pat.length && pat[i] !== "]") {
|
|
704
|
-
const ch = pat[i] ?? "";
|
|
705
|
-
if (ch === "\\") cls += "\\\\";
|
|
706
|
-
else if (ch === "]" || ch === "^") cls += `\\${ch}`;
|
|
707
|
-
else cls += ch;
|
|
708
|
-
i++;
|
|
709
|
-
}
|
|
710
|
-
cls += "]";
|
|
711
|
-
re += cls;
|
|
712
|
-
i++;
|
|
713
|
-
} else {
|
|
714
|
-
re += c.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
715
|
-
i++;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return new RegExp(re + "$");
|
|
719
|
-
}
|
|
720
|
-
function baseDir(pat) {
|
|
721
|
-
let i = pat.length - 1;
|
|
722
|
-
while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
|
|
723
|
-
const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
|
|
724
|
-
return cut < 0 ? "." : pat.slice(0, cut);
|
|
725
|
-
}
|
|
726
|
-
async function expandGlob(pattern) {
|
|
727
|
-
if (!isGlob(pattern)) return [pattern];
|
|
728
|
-
const results = /* @__PURE__ */ new Set();
|
|
729
|
-
const abs = isAbsolute(pattern);
|
|
730
|
-
const base = abs ? baseDir(pattern) : baseDir(pattern);
|
|
731
|
-
const relPat = base === "." ? pattern : pattern.slice(base.length + 1);
|
|
732
|
-
async function walk(dir, pat) {
|
|
733
|
-
let entries;
|
|
734
|
-
try {
|
|
735
|
-
entries = await fsp6.readdir(dir);
|
|
736
|
-
} catch {
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
const firstGlob = pat.search(/[*?[[]/);
|
|
740
|
-
if (firstGlob < 0) {
|
|
741
|
-
const re = globToRegex(pat);
|
|
742
|
-
for (const e of entries) {
|
|
743
|
-
if (re.test(e)) {
|
|
744
|
-
const full = `${dir}${SEP}${e}`;
|
|
745
|
-
results.add(abs ? resolve(full) : full);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
const before = pat.slice(0, firstGlob);
|
|
751
|
-
const rest = pat.slice(firstGlob);
|
|
752
|
-
if (before.endsWith("**")) {
|
|
753
|
-
await walk(dir, rest);
|
|
754
|
-
for (const e of entries) {
|
|
755
|
-
const full = `${dir}${SEP}${e}`;
|
|
756
|
-
try {
|
|
757
|
-
const stat5 = await fsp6.stat(full);
|
|
758
|
-
if (stat5.isDirectory()) await walk(full, rest);
|
|
759
|
-
} catch {
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
} else if (before === "") {
|
|
763
|
-
const re = globToRegex(rest);
|
|
764
|
-
for (const e of entries) {
|
|
765
|
-
if (re.test(e)) {
|
|
766
|
-
const full = `${dir}${SEP}${e}`;
|
|
767
|
-
results.add(abs ? resolve(full) : full);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
} else {
|
|
771
|
-
const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
|
|
772
|
-
if (entries.includes(seg)) {
|
|
773
|
-
const full = `${dir}${SEP}${seg}`;
|
|
774
|
-
try {
|
|
775
|
-
const stat5 = await fsp6.stat(full);
|
|
776
|
-
if (stat5.isDirectory()) await walk(full, rest);
|
|
777
|
-
} catch {
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
await walk(base === "." ? "." : base, relPat);
|
|
783
|
-
return [...results];
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// src/coordination/collab-debug.ts
|
|
787
905
|
var DEFAULT_MAX_TARGET_FILES = 30;
|
|
788
906
|
var DirectorAlertLevel = /* @__PURE__ */ ((DirectorAlertLevel2) => {
|
|
789
907
|
DirectorAlertLevel2["WARNING"] = "warning";
|
|
@@ -878,7 +996,7 @@ var CollabSession = class extends EventEmitter {
|
|
|
878
996
|
}
|
|
879
997
|
for (const filePath of allFiles) {
|
|
880
998
|
try {
|
|
881
|
-
const [content,
|
|
999
|
+
const [content, stat6] = await Promise.all([
|
|
882
1000
|
fsp6.readFile(filePath, "utf8"),
|
|
883
1001
|
fsp6.stat(filePath)
|
|
884
1002
|
]);
|
|
@@ -888,8 +1006,8 @@ var CollabSession = class extends EventEmitter {
|
|
|
888
1006
|
path: filePath,
|
|
889
1007
|
content,
|
|
890
1008
|
language,
|
|
891
|
-
snapshotMtimeMs:
|
|
892
|
-
snapshotSizeBytes:
|
|
1009
|
+
snapshotMtimeMs: stat6.mtimeMs,
|
|
1010
|
+
snapshotSizeBytes: stat6.size
|
|
893
1011
|
});
|
|
894
1012
|
} catch {
|
|
895
1013
|
this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
|
|
@@ -1302,9 +1420,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
|
|
|
1302
1420
|
for (const file of this.snapshot.files) {
|
|
1303
1421
|
if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
|
|
1304
1422
|
try {
|
|
1305
|
-
const
|
|
1306
|
-
const mtimeChanged = file.snapshotMtimeMs !== void 0 &&
|
|
1307
|
-
const sizeChanged = file.snapshotSizeBytes !== void 0 &&
|
|
1423
|
+
const stat6 = await fsp6.stat(file.path);
|
|
1424
|
+
const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat6.mtimeMs > file.snapshotMtimeMs + 1;
|
|
1425
|
+
const sizeChanged = file.snapshotSizeBytes !== void 0 && stat6.size !== file.snapshotSizeBytes;
|
|
1308
1426
|
if (mtimeChanged || sizeChanged) {
|
|
1309
1427
|
warnings.push(`${file.path} changed after the collab snapshot was captured.`);
|
|
1310
1428
|
}
|
|
@@ -4329,7 +4447,7 @@ function makeSpawnTool(director, roster) {
|
|
|
4329
4447
|
if (err instanceof FleetCostCapError) {
|
|
4330
4448
|
return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
|
|
4331
4449
|
}
|
|
4332
|
-
return { error:
|
|
4450
|
+
return { error: toErrorMessage(err) };
|
|
4333
4451
|
}
|
|
4334
4452
|
}
|
|
4335
4453
|
};
|
|
@@ -4411,7 +4529,7 @@ function makeAskTool(director) {
|
|
|
4411
4529
|
_hint: "Response was large and stored. Use ask_result with the key to retrieve it."
|
|
4412
4530
|
};
|
|
4413
4531
|
} catch (err) {
|
|
4414
|
-
return { ok: false, error:
|
|
4532
|
+
return { ok: false, error: toErrorMessage(err) };
|
|
4415
4533
|
}
|
|
4416
4534
|
}
|
|
4417
4535
|
};
|
|
@@ -4637,7 +4755,7 @@ function makeCollabDebugTool(director) {
|
|
|
4637
4755
|
evaluations: report.evaluations
|
|
4638
4756
|
};
|
|
4639
4757
|
} catch (err) {
|
|
4640
|
-
const msg =
|
|
4758
|
+
const msg = toErrorMessage(err);
|
|
4641
4759
|
return { error: "collab_debug failed: " + msg };
|
|
4642
4760
|
}
|
|
4643
4761
|
}
|
|
@@ -4971,6 +5089,7 @@ function resolveModelMatrix(matrix, role) {
|
|
|
4971
5089
|
|
|
4972
5090
|
// src/coordination/subagent-budget.ts
|
|
4973
5091
|
var TIMEOUT_PREEMPT_FRACTION = 0.85;
|
|
5092
|
+
var DECISION_TIMEOUT_MS = 6e4;
|
|
4974
5093
|
var BudgetExceededError = class extends Error {
|
|
4975
5094
|
kind;
|
|
4976
5095
|
limit;
|
|
@@ -5000,6 +5119,31 @@ var BudgetThresholdSignal = class extends Error {
|
|
|
5000
5119
|
};
|
|
5001
5120
|
var SubagentBudget = class _SubagentBudget {
|
|
5002
5121
|
limits;
|
|
5122
|
+
/** Patch one or more budget limits in-place after construction.
|
|
5123
|
+
* Used by the coordinator watchdog when granting an extension.
|
|
5124
|
+
* All fields are optional — only provided fields are updated.
|
|
5125
|
+
* This is the single write path for limit mutations so that future
|
|
5126
|
+
* validation or side-effects live in one place (M1). */
|
|
5127
|
+
patchLimits(ext) {
|
|
5128
|
+
if (ext.maxIterations !== void 0) {
|
|
5129
|
+
this.limits.maxIterations = ext.maxIterations;
|
|
5130
|
+
}
|
|
5131
|
+
if (ext.maxToolCalls !== void 0) {
|
|
5132
|
+
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
5133
|
+
}
|
|
5134
|
+
if (ext.maxTokens !== void 0) {
|
|
5135
|
+
this.limits.maxTokens = ext.maxTokens;
|
|
5136
|
+
}
|
|
5137
|
+
if (ext.maxCostUsd !== void 0) {
|
|
5138
|
+
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
5139
|
+
}
|
|
5140
|
+
if (ext.timeoutMs !== void 0) {
|
|
5141
|
+
this.limits.timeoutMs = ext.timeoutMs;
|
|
5142
|
+
}
|
|
5143
|
+
if (ext.idleTimeoutMs !== void 0) {
|
|
5144
|
+
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5003
5147
|
iterations = 0;
|
|
5004
5148
|
toolCalls = 0;
|
|
5005
5149
|
tokenInput = 0;
|
|
@@ -5020,12 +5164,44 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5020
5164
|
* or hung listener (Director not built / event filter detached mid-run)
|
|
5021
5165
|
* leaves the budget over-limit and never enforces anything.
|
|
5022
5166
|
*/
|
|
5023
|
-
static DECISION_TIMEOUT_MS =
|
|
5167
|
+
static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
|
|
5024
5168
|
/**
|
|
5025
5169
|
* Injected by the runner when wiring the budget to its EventBus.
|
|
5026
5170
|
* Used to emit `budget.threshold_reached` events in `'auto'` mode.
|
|
5027
5171
|
*/
|
|
5028
5172
|
_events;
|
|
5173
|
+
/**
|
|
5174
|
+
* Guard against dual-path races between the coordinator watchdog
|
|
5175
|
+
* (`executeWithTimeout`) and the budget's own `checkTimeout()`.
|
|
5176
|
+
* Both paths detect `elapsed >= timeoutMs` and can emit
|
|
5177
|
+
* `budget.threshold_reached` for kind `'timeout'` simultaneously.
|
|
5178
|
+
* Set to the current `timeoutMs` ceiling by the coordinator BEFORE
|
|
5179
|
+
* calling `onThreshold`, and cleared after the negotiation resolves.
|
|
5180
|
+
* `checkTimeout()` skips its wall-clock check while this is set so
|
|
5181
|
+
* the coordinator's watchdog is the sole source of wall-clock timeout
|
|
5182
|
+
* events — `checkTimeout()` focuses exclusively on `idle_timeout`.
|
|
5183
|
+
*/
|
|
5184
|
+
_watchdogActive;
|
|
5185
|
+
/** Returns the timeout ceiling currently being negotiated by the watchdog,
|
|
5186
|
+
* or `undefined` when no wall-clock negotiation is in flight.
|
|
5187
|
+
* Used by `executeWithTimeout` to detect a stale lock (M3). */
|
|
5188
|
+
get watchdogActive() {
|
|
5189
|
+
return this._watchdogActive;
|
|
5190
|
+
}
|
|
5191
|
+
/** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
|
|
5192
|
+
* `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
|
|
5193
|
+
* the budget's own `checkTimeout()` from emitting a second
|
|
5194
|
+
* `budget.threshold_reached` event while the watchdog is already
|
|
5195
|
+
* negotiating the same wall-clock deadline (C1). */
|
|
5196
|
+
setWatchdogNegotiation(timeoutMs) {
|
|
5197
|
+
this._watchdogActive = timeoutMs;
|
|
5198
|
+
}
|
|
5199
|
+
/** Clears the watchdog guard after negotiation resolves. Called in the
|
|
5200
|
+
* `finally` block of both the pre-empt and deadline branches so it fires
|
|
5201
|
+
* on every exit path: grant, deny, throw, or error. */
|
|
5202
|
+
clearWatchdogNegotiation() {
|
|
5203
|
+
this._watchdogActive = void 0;
|
|
5204
|
+
}
|
|
5029
5205
|
/**
|
|
5030
5206
|
* Negotiation mode — controls whether a threshold hit tries to emit
|
|
5031
5207
|
* `budget.threshold_reached` and wait for a coordinator decision, or
|
|
@@ -5126,7 +5302,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5126
5302
|
if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
|
|
5127
5303
|
exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
|
|
5128
5304
|
}
|
|
5129
|
-
|
|
5305
|
+
const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
|
|
5306
|
+
if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
|
|
5130
5307
|
exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
|
|
5131
5308
|
}
|
|
5132
5309
|
}
|
|
@@ -5140,19 +5317,99 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5140
5317
|
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
5141
5318
|
}
|
|
5142
5319
|
const bus = this._events;
|
|
5143
|
-
if (!bus
|
|
5320
|
+
if (!bus) {
|
|
5144
5321
|
const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
5145
5322
|
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
5146
5323
|
}
|
|
5324
|
+
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
5325
|
+
if (bus.hasListenerFor("budget.threshold_reached")) {
|
|
5326
|
+
for (const entry of exceeded) {
|
|
5327
|
+
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
5328
|
+
this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
|
|
5329
|
+
}
|
|
5330
|
+
const decision = this._pendingNegotiations.get(first.kind);
|
|
5331
|
+
if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
|
|
5332
|
+
throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
|
|
5333
|
+
}
|
|
5334
|
+
let hardStop = null;
|
|
5147
5335
|
for (const entry of exceeded) {
|
|
5148
5336
|
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
5149
|
-
const
|
|
5150
|
-
this._pendingNegotiations.set(entry.kind,
|
|
5337
|
+
const marker = Promise.resolve("stop");
|
|
5338
|
+
this._pendingNegotiations.set(entry.kind, marker);
|
|
5339
|
+
void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
|
|
5340
|
+
const sync = this._invokeHandlerSync(entry);
|
|
5341
|
+
if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
|
|
5151
5342
|
}
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5343
|
+
if (hardStop) throw hardStop;
|
|
5344
|
+
return exceeded;
|
|
5345
|
+
}
|
|
5346
|
+
/**
|
|
5347
|
+
* Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
|
|
5348
|
+
* whether it decided synchronously. Returns `true` when the handler returned
|
|
5349
|
+
* a synchronous decision (already honored — an `extend` patched the limits),
|
|
5350
|
+
* or `false` when it returned a Promise (async; the caller hard-stops, since
|
|
5351
|
+
* there is no listener to resolve the negotiation). The handler is given the
|
|
5352
|
+
* full info shape (`requestDecision` plus direct `extend`/`deny`) so both
|
|
5353
|
+
* recording handlers and policy handlers work without a wired listener.
|
|
5354
|
+
*/
|
|
5355
|
+
_invokeHandlerSync(entry) {
|
|
5356
|
+
const handler = this._onThreshold;
|
|
5357
|
+
if (!handler) return false;
|
|
5358
|
+
let extendArg;
|
|
5359
|
+
const result = handler({
|
|
5360
|
+
kind: entry.kind,
|
|
5361
|
+
used: entry.used,
|
|
5362
|
+
limit: entry.limit,
|
|
5363
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
5364
|
+
// Direct hooks for synchronous policy/recording handlers.
|
|
5365
|
+
extend: (extra) => {
|
|
5366
|
+
extendArg = extra;
|
|
5367
|
+
},
|
|
5368
|
+
deny: () => {
|
|
5369
|
+
}
|
|
5370
|
+
});
|
|
5371
|
+
if (result && typeof result.then === "function") return false;
|
|
5372
|
+
if (result === "throw") return false;
|
|
5373
|
+
if (result && typeof result === "object" && "extend" in result) {
|
|
5374
|
+
extendArg = result.extend;
|
|
5375
|
+
}
|
|
5376
|
+
if (extendArg) this.patchLimits(extendArg);
|
|
5377
|
+
return true;
|
|
5378
|
+
}
|
|
5379
|
+
/**
|
|
5380
|
+
* Emit `budget.threshold_reached` and resolve to the listener's verdict.
|
|
5381
|
+
* Resolves to `'stop'` immediately when there is no listener (or no bus) so
|
|
5382
|
+
* no negotiation can hang and no fallback timer leaks. Mirrors the
|
|
5383
|
+
* coordinator watchdog's own request path so both agree on the no-listener
|
|
5384
|
+
* default.
|
|
5385
|
+
*/
|
|
5386
|
+
_busRequestDecision(entry) {
|
|
5387
|
+
const bus = this._events;
|
|
5388
|
+
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
5389
|
+
return Promise.resolve("stop");
|
|
5390
|
+
}
|
|
5391
|
+
return new Promise((resolve3) => {
|
|
5392
|
+
let resolved = false;
|
|
5393
|
+
const respond = (d) => {
|
|
5394
|
+
if (resolved) return;
|
|
5395
|
+
resolved = true;
|
|
5396
|
+
clearTimeout(fallback);
|
|
5397
|
+
resolve3(d);
|
|
5398
|
+
};
|
|
5399
|
+
const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
|
|
5400
|
+
bus.emit("budget.threshold_reached", {
|
|
5401
|
+
kind: entry.kind,
|
|
5402
|
+
used: entry.used,
|
|
5403
|
+
limit: entry.limit,
|
|
5404
|
+
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
5405
|
+
// deny() wins over a same-dispatch extend(): a listener that both grants
|
|
5406
|
+
// and denies (or two listeners disagreeing) is resolved as a stop. The
|
|
5407
|
+
// grant is deferred a microtask so a synchronous deny in the same emit
|
|
5408
|
+
// pre-empts it; async grants still resolve normally.
|
|
5409
|
+
extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
|
|
5410
|
+
deny: () => respond("stop")
|
|
5411
|
+
});
|
|
5412
|
+
});
|
|
5156
5413
|
}
|
|
5157
5414
|
/**
|
|
5158
5415
|
* Per-kind in-flight negotiation Promises. Each budget kind can have its
|
|
@@ -5172,77 +5429,33 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5172
5429
|
* `{ extend: {} }` — keep going without patching; next overrun fires
|
|
5173
5430
|
* a fresh signal.
|
|
5174
5431
|
*/
|
|
5175
|
-
async _negotiateExtension(
|
|
5432
|
+
async _negotiateExtension(entry) {
|
|
5176
5433
|
if (!this._onThreshold) {
|
|
5177
5434
|
return "stop";
|
|
5178
5435
|
}
|
|
5179
5436
|
try {
|
|
5180
|
-
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
5181
5437
|
const result = this._onThreshold({
|
|
5182
|
-
kind:
|
|
5183
|
-
used:
|
|
5184
|
-
limit:
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
if (resolved) return;
|
|
5194
|
-
resolved = true;
|
|
5195
|
-
resolve3(d);
|
|
5196
|
-
};
|
|
5197
|
-
const fallback = setTimeout(
|
|
5198
|
-
() => respond("stop"),
|
|
5199
|
-
_SubagentBudget.DECISION_TIMEOUT_MS
|
|
5200
|
-
);
|
|
5201
|
-
for (const { kind: kind2, used, limit } of exceeded) {
|
|
5202
|
-
bus.emit("budget.threshold_reached", {
|
|
5203
|
-
kind: kind2,
|
|
5204
|
-
used,
|
|
5205
|
-
limit,
|
|
5206
|
-
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
5207
|
-
extend: (extra) => {
|
|
5208
|
-
clearTimeout(fallback);
|
|
5209
|
-
respond({ extend: extra });
|
|
5210
|
-
},
|
|
5211
|
-
deny: () => {
|
|
5212
|
-
clearTimeout(fallback);
|
|
5213
|
-
respond("stop");
|
|
5214
|
-
}
|
|
5215
|
-
});
|
|
5216
|
-
}
|
|
5217
|
-
});
|
|
5438
|
+
kind: entry.kind,
|
|
5439
|
+
used: entry.used,
|
|
5440
|
+
limit: entry.limit,
|
|
5441
|
+
// One event for THIS kind only — each exceeded kind has its own
|
|
5442
|
+
// negotiation (and its own resolve), so there is no cross-kind
|
|
5443
|
+
// first-wins drop and no O(N^2) re-emission.
|
|
5444
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
5445
|
+
extend: (extra) => {
|
|
5446
|
+
this.patchLimits(extra);
|
|
5447
|
+
},
|
|
5448
|
+
deny: () => {
|
|
5218
5449
|
}
|
|
5219
5450
|
});
|
|
5220
5451
|
if (result === "throw") return "stop";
|
|
5221
5452
|
if (result === "continue") return { extend: {} };
|
|
5222
5453
|
const decision = await result;
|
|
5223
5454
|
if (decision === "stop") return "stop";
|
|
5224
|
-
|
|
5225
|
-
if (ext.maxIterations !== void 0) {
|
|
5226
|
-
this.limits.maxIterations = ext.maxIterations;
|
|
5227
|
-
}
|
|
5228
|
-
if (ext.maxToolCalls !== void 0) {
|
|
5229
|
-
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
5230
|
-
}
|
|
5231
|
-
if (ext.maxTokens !== void 0) {
|
|
5232
|
-
this.limits.maxTokens = ext.maxTokens;
|
|
5233
|
-
}
|
|
5234
|
-
if (ext.maxCostUsd !== void 0) {
|
|
5235
|
-
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
5236
|
-
}
|
|
5237
|
-
if (ext.timeoutMs !== void 0) {
|
|
5238
|
-
this.limits.timeoutMs = ext.timeoutMs;
|
|
5239
|
-
}
|
|
5240
|
-
if (ext.idleTimeoutMs !== void 0) {
|
|
5241
|
-
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
5242
|
-
}
|
|
5455
|
+
this.patchLimits(decision.extend);
|
|
5243
5456
|
return decision;
|
|
5244
5457
|
} finally {
|
|
5245
|
-
this._pendingNegotiations.delete(kind);
|
|
5458
|
+
this._pendingNegotiations.delete(entry.kind);
|
|
5246
5459
|
}
|
|
5247
5460
|
}
|
|
5248
5461
|
recordIteration() {
|
|
@@ -5285,7 +5498,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5285
5498
|
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
5286
5499
|
if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
|
|
5287
5500
|
const elapsed = Date.now() - this.startTime;
|
|
5288
|
-
const
|
|
5501
|
+
const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
|
|
5502
|
+
const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
5289
5503
|
const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
|
|
5290
5504
|
if (!wallTripped && !idleTripped) return;
|
|
5291
5505
|
void this.checkLimits(elapsed);
|
|
@@ -5313,11 +5527,6 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5313
5527
|
}
|
|
5314
5528
|
};
|
|
5315
5529
|
|
|
5316
|
-
// src/utils/string.ts
|
|
5317
|
-
function truncate(s, max) {
|
|
5318
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
5319
|
-
}
|
|
5320
|
-
|
|
5321
5530
|
// src/types/provider.ts
|
|
5322
5531
|
var ProviderError = class extends WrongStackError {
|
|
5323
5532
|
status;
|
|
@@ -5397,7 +5606,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
5397
5606
|
const baseMessage2 = err.describe();
|
|
5398
5607
|
return providerErrorToSubagentError(err, baseMessage2, cause);
|
|
5399
5608
|
}
|
|
5400
|
-
const baseMessage =
|
|
5609
|
+
const baseMessage = toErrorMessage(err);
|
|
5401
5610
|
if (err instanceof BudgetExceededError) {
|
|
5402
5611
|
const map = {
|
|
5403
5612
|
iterations: "budget_iterations",
|
|
@@ -5912,6 +6121,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
5912
6121
|
terminating = /* @__PURE__ */ new Set();
|
|
5913
6122
|
constructor(config, options = {}) {
|
|
5914
6123
|
super();
|
|
6124
|
+
this.setMaxListeners(0);
|
|
5915
6125
|
this.coordinatorId = config.coordinatorId;
|
|
5916
6126
|
this.config = config;
|
|
5917
6127
|
this.runner = options.runner;
|
|
@@ -6306,7 +6516,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6306
6516
|
let result;
|
|
6307
6517
|
budget.start();
|
|
6308
6518
|
try {
|
|
6309
|
-
const outcome = await this.executeWithTimeout(
|
|
6519
|
+
const outcome = await this.executeWithTimeout(
|
|
6520
|
+
this.runner,
|
|
6521
|
+
task,
|
|
6522
|
+
runCtx,
|
|
6523
|
+
budget,
|
|
6524
|
+
subagent.config.preemptFraction
|
|
6525
|
+
);
|
|
6310
6526
|
result = {
|
|
6311
6527
|
subagentId,
|
|
6312
6528
|
taskId: task.id,
|
|
@@ -6333,7 +6549,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6333
6549
|
}
|
|
6334
6550
|
this.recordCompletion(result);
|
|
6335
6551
|
}
|
|
6336
|
-
async executeWithTimeout(runner, task, ctx, budget) {
|
|
6552
|
+
async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
|
|
6337
6553
|
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
6338
6554
|
const idleLimitMs = budget.limits.idleTimeoutMs;
|
|
6339
6555
|
if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
|
|
@@ -6341,8 +6557,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6341
6557
|
}
|
|
6342
6558
|
const start = Date.now();
|
|
6343
6559
|
let timer = null;
|
|
6344
|
-
let
|
|
6560
|
+
let PreemptState;
|
|
6561
|
+
((PreemptState2) => {
|
|
6562
|
+
PreemptState2["ACTIVE"] = "active";
|
|
6563
|
+
PreemptState2["LOCKED"] = "locked";
|
|
6564
|
+
})(PreemptState || (PreemptState = {}));
|
|
6565
|
+
let preemptedCeiling = null;
|
|
6566
|
+
let preemptState = "active" /* ACTIVE */;
|
|
6567
|
+
let lastGrantActivityTs = -1;
|
|
6345
6568
|
const timeoutPromise = new Promise((_, reject) => {
|
|
6569
|
+
const terminate = (kind, limit, used) => {
|
|
6570
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
6571
|
+
reject(
|
|
6572
|
+
budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
|
|
6573
|
+
);
|
|
6574
|
+
};
|
|
6346
6575
|
const armFor = (ms) => {
|
|
6347
6576
|
if (timer) clearTimeout(timer);
|
|
6348
6577
|
timer = setTimeout(onTick, Math.max(0, ms));
|
|
@@ -6351,7 +6580,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6351
6580
|
const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
6352
6581
|
const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
|
|
6353
6582
|
const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
6354
|
-
const preemptRemaining = initialTimeoutMs === void 0 ||
|
|
6583
|
+
const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
|
|
6355
6584
|
armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
|
|
6356
6585
|
};
|
|
6357
6586
|
const negotiateTimeout = async (used, limit) => {
|
|
@@ -6361,16 +6590,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6361
6590
|
kind: "timeout",
|
|
6362
6591
|
used,
|
|
6363
6592
|
limit,
|
|
6364
|
-
requestDecision: () =>
|
|
6365
|
-
budget._events?.
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6593
|
+
requestDecision: () => {
|
|
6594
|
+
if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
|
|
6595
|
+
return Promise.resolve("stop");
|
|
6596
|
+
}
|
|
6597
|
+
return new Promise((resolveDecision) => {
|
|
6598
|
+
let settled = false;
|
|
6599
|
+
const resolve3 = (d) => {
|
|
6600
|
+
if (settled) return;
|
|
6601
|
+
settled = true;
|
|
6602
|
+
resolveDecision(d);
|
|
6603
|
+
};
|
|
6604
|
+
const fallback = setTimeout(() => resolve3("stop"), DECISION_TIMEOUT_MS);
|
|
6605
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
6606
|
+
kind: "timeout",
|
|
6607
|
+
used,
|
|
6608
|
+
limit,
|
|
6609
|
+
// Informational: the budget's own decision deadline. Listeners may use
|
|
6610
|
+
// this to display a countdown. The coordinator does NOT enforce it —
|
|
6611
|
+
// it is the budget's own `setTimeout(fallback)` that races against
|
|
6612
|
+
// the listener's `extend()`/`deny()` call to guarantee progress.
|
|
6613
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
6614
|
+
// deny() wins over a same-dispatch extend(): defer the grant a
|
|
6615
|
+
// microtask so a synchronous deny in the same emit pre-empts it
|
|
6616
|
+
// (a listener that both grants and denies, or two listeners
|
|
6617
|
+
// disagreeing, resolves as a stop). Async grants still resolve.
|
|
6618
|
+
extend: (extra) => {
|
|
6619
|
+
clearTimeout(fallback);
|
|
6620
|
+
queueMicrotask(() => resolve3({ extend: extra }));
|
|
6621
|
+
},
|
|
6622
|
+
deny: () => {
|
|
6623
|
+
clearTimeout(fallback);
|
|
6624
|
+
resolve3("stop");
|
|
6625
|
+
}
|
|
6626
|
+
});
|
|
6372
6627
|
});
|
|
6373
|
-
}
|
|
6628
|
+
}
|
|
6374
6629
|
});
|
|
6375
6630
|
return typeof result === "string" ? result : await result;
|
|
6376
6631
|
};
|
|
@@ -6381,21 +6636,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6381
6636
|
const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
|
|
6382
6637
|
const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
|
|
6383
6638
|
if (idleExceeded && !wallExceeded) {
|
|
6639
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
6640
|
+
kind: "idle_timeout",
|
|
6641
|
+
used: budget.idleMs(),
|
|
6642
|
+
limit: idleLimit ?? 0,
|
|
6643
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
6644
|
+
extend: () => {
|
|
6645
|
+
},
|
|
6646
|
+
deny: () => {
|
|
6647
|
+
}
|
|
6648
|
+
});
|
|
6384
6649
|
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
6385
|
-
reject(new BudgetExceededError("
|
|
6650
|
+
reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
|
|
6386
6651
|
return;
|
|
6387
6652
|
}
|
|
6388
|
-
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold &&
|
|
6653
|
+
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
|
|
6654
|
+
const activityTs = Date.now() - budget.idleMs();
|
|
6655
|
+
if (activityTs <= lastGrantActivityTs) {
|
|
6656
|
+
preemptState = "locked" /* LOCKED */;
|
|
6657
|
+
preemptedCeiling = wallLimit;
|
|
6658
|
+
scheduleNext();
|
|
6659
|
+
return;
|
|
6660
|
+
}
|
|
6661
|
+
budget.setWatchdogNegotiation(wallLimit);
|
|
6389
6662
|
try {
|
|
6390
6663
|
const decision = await negotiateTimeout(elapsed, wallLimit);
|
|
6391
6664
|
if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
|
|
6392
|
-
budget.
|
|
6393
|
-
|
|
6665
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
6666
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
6667
|
+
preemptState = "active" /* ACTIVE */;
|
|
6668
|
+
preemptedCeiling = null;
|
|
6394
6669
|
} else {
|
|
6395
|
-
|
|
6670
|
+
preemptState = "locked" /* LOCKED */;
|
|
6671
|
+
preemptedCeiling = wallLimit;
|
|
6396
6672
|
}
|
|
6397
6673
|
} catch {
|
|
6398
|
-
|
|
6674
|
+
preemptState = "locked" /* LOCKED */;
|
|
6675
|
+
preemptedCeiling = wallLimit;
|
|
6676
|
+
} finally {
|
|
6677
|
+
budget.clearWatchdogNegotiation();
|
|
6399
6678
|
}
|
|
6400
6679
|
scheduleNext();
|
|
6401
6680
|
return;
|
|
@@ -6410,26 +6689,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
6410
6689
|
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
6411
6690
|
return;
|
|
6412
6691
|
}
|
|
6692
|
+
budget.setWatchdogNegotiation(limit);
|
|
6413
6693
|
try {
|
|
6414
6694
|
const decision = await negotiateTimeout(elapsed, limit);
|
|
6415
|
-
if (decision === "
|
|
6416
|
-
|
|
6695
|
+
if (decision === "throw") {
|
|
6696
|
+
terminate("timeout", limit, elapsed);
|
|
6697
|
+
return;
|
|
6698
|
+
}
|
|
6699
|
+
if (decision === "continue") {
|
|
6700
|
+
preemptState = "locked" /* LOCKED */;
|
|
6701
|
+
preemptedCeiling = wallLimit;
|
|
6417
6702
|
armFor(Math.max(1e3, limit));
|
|
6418
6703
|
return;
|
|
6419
6704
|
}
|
|
6705
|
+
if (decision === "stop") {
|
|
6706
|
+
terminate("timeout", limit, elapsed);
|
|
6707
|
+
return;
|
|
6708
|
+
}
|
|
6420
6709
|
if (decision.extend.timeoutMs !== void 0) {
|
|
6421
|
-
budget.
|
|
6422
|
-
|
|
6710
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
6711
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
6712
|
+
preemptState = "active" /* ACTIVE */;
|
|
6713
|
+
preemptedCeiling = null;
|
|
6423
6714
|
scheduleNext();
|
|
6424
6715
|
return;
|
|
6425
6716
|
}
|
|
6426
|
-
|
|
6427
|
-
|
|
6717
|
+
terminate("timeout", limit, elapsed);
|
|
6718
|
+
return;
|
|
6428
6719
|
} catch (err) {
|
|
6429
6720
|
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
6430
6721
|
reject(
|
|
6431
6722
|
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
6432
6723
|
);
|
|
6724
|
+
return;
|
|
6725
|
+
} finally {
|
|
6726
|
+
budget.clearWatchdogNegotiation();
|
|
6433
6727
|
}
|
|
6434
6728
|
};
|
|
6435
6729
|
scheduleNext();
|
|
@@ -7383,7 +7677,7 @@ var Director = class _Director {
|
|
|
7383
7677
|
})),
|
|
7384
7678
|
usage: this.usage.snapshot()
|
|
7385
7679
|
};
|
|
7386
|
-
await fsp6.mkdir(
|
|
7680
|
+
await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
|
|
7387
7681
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
7388
7682
|
return this.manifestPath;
|
|
7389
7683
|
}
|
|
@@ -7433,7 +7727,7 @@ var Director = class _Director {
|
|
|
7433
7727
|
* listener for structured collection, and never affects exit code.
|
|
7434
7728
|
*/
|
|
7435
7729
|
logShutdownError(phase, err) {
|
|
7436
|
-
const detail =
|
|
7730
|
+
const detail = toErrorMessage(err);
|
|
7437
7731
|
process.emitWarning(
|
|
7438
7732
|
`Director shutdown phase "${phase}" failed: ${detail}`,
|
|
7439
7733
|
"DirectorShutdownWarning"
|
|
@@ -7597,7 +7891,7 @@ var Director = class _Director {
|
|
|
7597
7891
|
*/
|
|
7598
7892
|
async readSession(subagentId, tail) {
|
|
7599
7893
|
if (!this.sessionsRoot) return null;
|
|
7600
|
-
const filePath =
|
|
7894
|
+
const filePath = path5.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
|
|
7601
7895
|
let raw;
|
|
7602
7896
|
try {
|
|
7603
7897
|
raw = await fsp6.readFile(filePath, "utf8");
|
|
@@ -8026,7 +8320,7 @@ function createDelegateTool(opts) {
|
|
|
8026
8320
|
summary
|
|
8027
8321
|
};
|
|
8028
8322
|
} catch (err) {
|
|
8029
|
-
const message =
|
|
8323
|
+
const message = toErrorMessage(err);
|
|
8030
8324
|
opts.events?.emit("delegate.completed", {
|
|
8031
8325
|
target,
|
|
8032
8326
|
task: i.task,
|
|
@@ -8127,13 +8421,13 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
8127
8421
|
if (!opts.sessionsRoot) return void 0;
|
|
8128
8422
|
const candidates = [];
|
|
8129
8423
|
if (opts.directorRunId) {
|
|
8130
|
-
candidates.push(
|
|
8424
|
+
candidates.push(path5.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
|
|
8131
8425
|
} else {
|
|
8132
8426
|
try {
|
|
8133
8427
|
const entries = await fsp6.readdir(opts.sessionsRoot, { withFileTypes: true });
|
|
8134
8428
|
for (const entry of entries) {
|
|
8135
8429
|
if (entry.isDirectory()) {
|
|
8136
|
-
candidates.push(
|
|
8430
|
+
candidates.push(path5.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
|
|
8137
8431
|
}
|
|
8138
8432
|
}
|
|
8139
8433
|
} catch {
|
|
@@ -8389,119 +8683,48 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8389
8683
|
function defaultFormatTaskInput(task) {
|
|
8390
8684
|
return task.description ?? "";
|
|
8391
8685
|
}
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
function repairToolUseAdjacency(messages) {
|
|
8395
|
-
const removedToolUses = [];
|
|
8396
|
-
const removedToolResults = [];
|
|
8397
|
-
let removedMessages = 0;
|
|
8398
|
-
let changed = false;
|
|
8399
|
-
const out = [];
|
|
8400
|
-
for (let i = 0; i < messages.length; i++) {
|
|
8401
|
-
const original = expectDefined(messages[i]);
|
|
8402
|
-
let msg = original;
|
|
8403
|
-
if (hasToolUse(msg)) {
|
|
8404
|
-
const nextIds = toolResultIds(messages[i + 1]);
|
|
8405
|
-
const filtered = mapContent(msg, (blocks) => {
|
|
8406
|
-
const next = [];
|
|
8407
|
-
for (const block of blocks) {
|
|
8408
|
-
if (block.type === "tool_use" && !nextIds.has(block.id)) {
|
|
8409
|
-
removedToolUses.push(block.id);
|
|
8410
|
-
changed = true;
|
|
8411
|
-
continue;
|
|
8412
|
-
}
|
|
8413
|
-
next.push(block);
|
|
8414
|
-
}
|
|
8415
|
-
return next;
|
|
8416
|
-
});
|
|
8417
|
-
msg = filtered ?? msg;
|
|
8418
|
-
}
|
|
8419
|
-
if (hasToolResult(msg)) {
|
|
8420
|
-
const allowed = toolUseIds(out[out.length - 1]);
|
|
8421
|
-
const filtered = mapContent(msg, (blocks) => {
|
|
8422
|
-
const next = [];
|
|
8423
|
-
for (const block of blocks) {
|
|
8424
|
-
if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
|
|
8425
|
-
removedToolResults.push(block.tool_use_id);
|
|
8426
|
-
changed = true;
|
|
8427
|
-
continue;
|
|
8428
|
-
}
|
|
8429
|
-
next.push(block);
|
|
8430
|
-
}
|
|
8431
|
-
return next;
|
|
8432
|
-
});
|
|
8433
|
-
msg = filtered ?? msg;
|
|
8434
|
-
}
|
|
8435
|
-
if (isEmptyMessage(msg)) {
|
|
8436
|
-
removedMessages++;
|
|
8437
|
-
changed = true;
|
|
8438
|
-
continue;
|
|
8439
|
-
}
|
|
8440
|
-
out.push(msg);
|
|
8441
|
-
}
|
|
8442
|
-
return {
|
|
8443
|
-
messages: changed ? out : messages,
|
|
8444
|
-
report: { changed, removedToolUses, removedToolResults, removedMessages }
|
|
8445
|
-
};
|
|
8446
|
-
}
|
|
8447
|
-
function hasToolUse(msg) {
|
|
8448
|
-
return contentBlocks(msg).some((b) => b.type === "tool_use");
|
|
8686
|
+
function sanitizeModel(model) {
|
|
8687
|
+
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
8449
8688
|
}
|
|
8450
|
-
function
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
const
|
|
8455
|
-
|
|
8456
|
-
for (const block of contentBlocks(msg)) {
|
|
8457
|
-
if (block.type === "tool_use") ids.add(block.id);
|
|
8458
|
-
}
|
|
8459
|
-
return ids;
|
|
8460
|
-
}
|
|
8461
|
-
function toolResultIds(msg) {
|
|
8462
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8463
|
-
if (!msg || msg.role !== "user") return ids;
|
|
8464
|
-
for (const block of contentBlocks(msg)) {
|
|
8465
|
-
if (block.type === "tool_result") ids.add(block.tool_use_id);
|
|
8466
|
-
}
|
|
8467
|
-
return ids;
|
|
8468
|
-
}
|
|
8469
|
-
function contentBlocks(msg) {
|
|
8470
|
-
return msg && Array.isArray(msg.content) ? msg.content : [];
|
|
8471
|
-
}
|
|
8472
|
-
function mapContent(msg, fn) {
|
|
8473
|
-
if (!Array.isArray(msg.content)) return msg;
|
|
8474
|
-
const next = fn(msg.content);
|
|
8475
|
-
if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
|
|
8476
|
-
return msg;
|
|
8477
|
-
}
|
|
8478
|
-
return { ...msg, content: next };
|
|
8479
|
-
}
|
|
8480
|
-
function isEmptyMessage(msg) {
|
|
8481
|
-
if (typeof msg.content === "string") return msg.content.trim().length === 0;
|
|
8482
|
-
return msg.content.length === 0;
|
|
8483
|
-
}
|
|
8484
|
-
|
|
8485
|
-
// src/storage/session-store.ts
|
|
8486
|
-
function sanitizeModel(model) {
|
|
8487
|
-
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
8488
|
-
}
|
|
8489
|
-
function generateSessionId(startedAt, model) {
|
|
8490
|
-
const date = startedAt.slice(0, 10);
|
|
8491
|
-
const time = startedAt.slice(11, 19).replace(/:/g, "-");
|
|
8492
|
-
const suffix = randomBytes(2).toString("hex");
|
|
8493
|
-
const modelPart = model ? `_${sanitizeModel(model)}` : "";
|
|
8494
|
-
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
8689
|
+
function generateSessionId(startedAt, model) {
|
|
8690
|
+
const date = startedAt.slice(0, 10);
|
|
8691
|
+
const time = startedAt.slice(11, 19).replace(/:/g, "-");
|
|
8692
|
+
const suffix = randomBytes(2).toString("hex");
|
|
8693
|
+
const modelPart = model ? `_${sanitizeModel(model)}` : "";
|
|
8694
|
+
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
8495
8695
|
}
|
|
8496
8696
|
var DefaultSessionStore = class _DefaultSessionStore {
|
|
8497
8697
|
dir;
|
|
8498
8698
|
events;
|
|
8499
8699
|
secretScrubber;
|
|
8700
|
+
/**
|
|
8701
|
+
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
8702
|
+
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
8703
|
+
* file was written to). This eliminates redundant full-file reads and
|
|
8704
|
+
* JSON parses when the same session is loaded multiple times within the
|
|
8705
|
+
* store's lifetime (e.g., webui session detail views, list() fallbacks).
|
|
8706
|
+
*
|
|
8707
|
+
* Max size is capped to prevent unbounded memory growth in long-running
|
|
8708
|
+
* processes. When the limit is reached, the oldest entry is evicted.
|
|
8709
|
+
*/
|
|
8710
|
+
_loadCache = /* @__PURE__ */ new Map();
|
|
8711
|
+
static LOAD_CACHE_MAX_ENTRIES = 50;
|
|
8500
8712
|
constructor(opts) {
|
|
8501
8713
|
this.dir = opts.dir;
|
|
8502
8714
|
this.events = opts.events;
|
|
8503
8715
|
this.secretScrubber = opts.secretScrubber;
|
|
8504
8716
|
}
|
|
8717
|
+
/**
|
|
8718
|
+
* Clear the load() cache. Useful for testing or when the caller knows
|
|
8719
|
+
* the file has changed externally (e.g., another process wrote to it).
|
|
8720
|
+
*/
|
|
8721
|
+
clearLoadCache(sessionId) {
|
|
8722
|
+
if (sessionId !== void 0) {
|
|
8723
|
+
this._loadCache.delete(sessionId);
|
|
8724
|
+
} else {
|
|
8725
|
+
this._loadCache.clear();
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8505
8728
|
// ── Storage event helpers ───────────────────────────────────────────────────
|
|
8506
8729
|
emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
|
|
8507
8730
|
this.events?.emit("storage.read", {
|
|
@@ -8538,11 +8761,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8538
8761
|
}
|
|
8539
8762
|
/** Absolute path to the session index file. */
|
|
8540
8763
|
get indexFile() {
|
|
8541
|
-
return
|
|
8764
|
+
return path5.join(this.dir, "_index.jsonl");
|
|
8542
8765
|
}
|
|
8543
8766
|
/** Join session ID to its absolute path within the store directory. */
|
|
8544
8767
|
sessionPath(id, ext) {
|
|
8545
|
-
return
|
|
8768
|
+
return path5.join(this.dir, `${id}${ext}`);
|
|
8546
8769
|
}
|
|
8547
8770
|
/**
|
|
8548
8771
|
* Ensure the directory implied by the session ID exists. When the ID
|
|
@@ -8550,7 +8773,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8550
8773
|
* subdirectory so sessions group naturally by day.
|
|
8551
8774
|
*/
|
|
8552
8775
|
async ensureShardDir(id) {
|
|
8553
|
-
const dirPath =
|
|
8776
|
+
const dirPath = path5.dirname(path5.join(this.dir, id));
|
|
8554
8777
|
await ensureDir(dirPath);
|
|
8555
8778
|
return dirPath;
|
|
8556
8779
|
}
|
|
@@ -8558,15 +8781,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8558
8781
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8559
8782
|
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
8560
8783
|
const shardDir = await this.ensureShardDir(id);
|
|
8561
|
-
const file =
|
|
8784
|
+
const file = path5.join(shardDir, `${path5.basename(id)}.jsonl`);
|
|
8562
8785
|
const t0 = Date.now();
|
|
8563
8786
|
let handle;
|
|
8564
8787
|
try {
|
|
8565
8788
|
handle = await fsp6.open(file, "a", 384);
|
|
8566
8789
|
} catch (err) {
|
|
8567
|
-
this.emitError(id, file, "create",
|
|
8790
|
+
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
8568
8791
|
throw new Error(
|
|
8569
|
-
`Failed to open session file: ${
|
|
8792
|
+
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
8570
8793
|
{ cause: err }
|
|
8571
8794
|
);
|
|
8572
8795
|
}
|
|
@@ -8586,7 +8809,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8586
8809
|
message: e instanceof Error ? e.message : String(e),
|
|
8587
8810
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8588
8811
|
})));
|
|
8589
|
-
this.emitError(id, file, "create",
|
|
8812
|
+
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
8590
8813
|
throw err;
|
|
8591
8814
|
}
|
|
8592
8815
|
}
|
|
@@ -8598,9 +8821,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8598
8821
|
try {
|
|
8599
8822
|
handle = await fsp6.open(file, "a", 384);
|
|
8600
8823
|
} catch (err) {
|
|
8601
|
-
this.emitError(id, file, "resume",
|
|
8824
|
+
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
8602
8825
|
throw new Error(
|
|
8603
|
-
`Failed to open session "${id}" for append: ${
|
|
8826
|
+
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
8604
8827
|
{ cause: err }
|
|
8605
8828
|
);
|
|
8606
8829
|
}
|
|
@@ -8620,7 +8843,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8620
8843
|
// Shard directory (sessions/<date>/) — must match create() so the
|
|
8621
8844
|
// .summary.json sidecar lands next to the JSONL instead of the
|
|
8622
8845
|
// sessions root (where summaryFor() would never find it).
|
|
8623
|
-
dir:
|
|
8846
|
+
dir: path5.dirname(file),
|
|
8624
8847
|
filePath: file,
|
|
8625
8848
|
secretScrubber: this.secretScrubber,
|
|
8626
8849
|
onClose: (s) => this.appendToIndex(s)
|
|
@@ -8635,7 +8858,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8635
8858
|
message: e instanceof Error ? e.message : String(e),
|
|
8636
8859
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8637
8860
|
})));
|
|
8638
|
-
this.emitError(id, file, "resume",
|
|
8861
|
+
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
8639
8862
|
throw err;
|
|
8640
8863
|
}
|
|
8641
8864
|
}
|
|
@@ -8644,7 +8867,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8644
8867
|
const t0 = Date.now();
|
|
8645
8868
|
let outcome = "success";
|
|
8646
8869
|
let errorMsg;
|
|
8870
|
+
let cacheHit = false;
|
|
8647
8871
|
try {
|
|
8872
|
+
let stat6;
|
|
8873
|
+
try {
|
|
8874
|
+
const s = await fsp6.stat(file);
|
|
8875
|
+
stat6 = { mtimeMs: s.mtimeMs, size: s.size };
|
|
8876
|
+
} catch (err) {
|
|
8877
|
+
throw err;
|
|
8878
|
+
}
|
|
8879
|
+
const cached = this._loadCache.get(id);
|
|
8880
|
+
if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
|
|
8881
|
+
cacheHit = true;
|
|
8882
|
+
return cached.data;
|
|
8883
|
+
}
|
|
8648
8884
|
const raw = await fsp6.readFile(file, "utf8");
|
|
8649
8885
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
8650
8886
|
const events = [];
|
|
@@ -8660,13 +8896,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8660
8896
|
const meta = this.metaFromEvents(id, events);
|
|
8661
8897
|
const { messages, usage } = this.replay(events, id);
|
|
8662
8898
|
const toolCallEnds = extractToolCallEnds(events);
|
|
8663
|
-
|
|
8899
|
+
const data = { metadata: meta, events, messages, usage, toolCallEnds };
|
|
8900
|
+
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
8901
|
+
const oldest = this._loadCache.keys().next().value;
|
|
8902
|
+
if (oldest !== void 0) {
|
|
8903
|
+
this._loadCache.delete(oldest);
|
|
8904
|
+
}
|
|
8905
|
+
}
|
|
8906
|
+
this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
|
|
8907
|
+
return data;
|
|
8664
8908
|
} catch (err) {
|
|
8665
8909
|
outcome = "failure";
|
|
8666
|
-
errorMsg =
|
|
8910
|
+
errorMsg = toErrorMessage(err);
|
|
8667
8911
|
throw err;
|
|
8668
8912
|
} finally {
|
|
8669
8913
|
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
8914
|
+
if (cacheHit) {
|
|
8915
|
+
this.events?.emit("storage.cache_hit", {
|
|
8916
|
+
sessionId: id,
|
|
8917
|
+
store: "session",
|
|
8918
|
+
filePath: file,
|
|
8919
|
+
operation: "load",
|
|
8920
|
+
durationMs: Date.now() - t0
|
|
8921
|
+
});
|
|
8922
|
+
}
|
|
8670
8923
|
}
|
|
8671
8924
|
}
|
|
8672
8925
|
async list(limit = 20) {
|
|
@@ -8746,7 +8999,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8746
8999
|
await fsp6.rename(tmp, this.indexFile);
|
|
8747
9000
|
} catch (err) {
|
|
8748
9001
|
outcome = "failure";
|
|
8749
|
-
errorMsg =
|
|
9002
|
+
errorMsg = toErrorMessage(err);
|
|
8750
9003
|
} finally {
|
|
8751
9004
|
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
8752
9005
|
}
|
|
@@ -8814,7 +9067,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8814
9067
|
continue;
|
|
8815
9068
|
if (entry.isDirectory()) {
|
|
8816
9069
|
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
8817
|
-
ids.push(...await this.collectSessionIds(
|
|
9070
|
+
ids.push(...await this.collectSessionIds(path5.join(dir, entry.name), childPrefix, depth + 1));
|
|
8818
9071
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
8819
9072
|
if (entry.name === "_index.jsonl") continue;
|
|
8820
9073
|
const base = entry.name.replace(/\.jsonl$/, "");
|
|
@@ -8834,10 +9087,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8834
9087
|
return JSON.parse(raw);
|
|
8835
9088
|
} catch {
|
|
8836
9089
|
const full = this.sessionPath(id, ".jsonl");
|
|
8837
|
-
const
|
|
8838
|
-
const summary = await this.summarize(id,
|
|
9090
|
+
const stat6 = await fsp6.stat(full);
|
|
9091
|
+
const summary = await this.summarize(id, stat6.mtime.toISOString());
|
|
8839
9092
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
8840
|
-
const msg =
|
|
9093
|
+
const msg = toErrorMessage(err);
|
|
8841
9094
|
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
8842
9095
|
console.warn(JSON.stringify({
|
|
8843
9096
|
level: "warn",
|
|
@@ -8865,14 +9118,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8865
9118
|
async deleteSession(id) {
|
|
8866
9119
|
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
8867
9120
|
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
8868
|
-
const shardDir =
|
|
8869
|
-
const base =
|
|
8870
|
-
const sessDir =
|
|
9121
|
+
const shardDir = path5.dirname(path5.join(this.dir, id));
|
|
9122
|
+
const base = path5.basename(id);
|
|
9123
|
+
const sessDir = path5.join(shardDir, base);
|
|
8871
9124
|
const deletions = [
|
|
8872
9125
|
fsp6.unlink(jsonlPath),
|
|
8873
9126
|
fsp6.unlink(summaryPath),
|
|
8874
|
-
fsp6.unlink(
|
|
8875
|
-
fsp6.unlink(
|
|
9127
|
+
fsp6.unlink(path5.join(shardDir, `${base}.plan.json`)),
|
|
9128
|
+
fsp6.unlink(path5.join(shardDir, `${base}.todos.json`))
|
|
8876
9129
|
];
|
|
8877
9130
|
const results = await Promise.allSettled(deletions);
|
|
8878
9131
|
for (const r of results) {
|
|
@@ -8894,7 +9147,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8894
9147
|
level: "warn",
|
|
8895
9148
|
event: "session_store.rmdir_failed",
|
|
8896
9149
|
sessionId: id,
|
|
8897
|
-
message:
|
|
9150
|
+
message: toErrorMessage(err),
|
|
8898
9151
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8899
9152
|
}));
|
|
8900
9153
|
});
|
|
@@ -8908,17 +9161,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8908
9161
|
let deleted = 0;
|
|
8909
9162
|
let activeSessionId = null;
|
|
8910
9163
|
try {
|
|
8911
|
-
const raw = await fsp6.readFile(
|
|
9164
|
+
const raw = await fsp6.readFile(path5.join(this.dir, "active.json"), "utf8");
|
|
8912
9165
|
const active = JSON.parse(raw);
|
|
8913
9166
|
activeSessionId = active.sessionId ?? null;
|
|
8914
9167
|
} catch {
|
|
8915
9168
|
}
|
|
8916
9169
|
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
8917
9170
|
const pruneFile = async (dir, name, prefix) => {
|
|
8918
|
-
const jsonlPath =
|
|
9171
|
+
const jsonlPath = path5.join(dir, name);
|
|
8919
9172
|
try {
|
|
8920
|
-
const
|
|
8921
|
-
if (
|
|
9173
|
+
const stat6 = await fsp6.stat(jsonlPath);
|
|
9174
|
+
if (stat6.mtimeMs >= cutoff) return;
|
|
8922
9175
|
} catch {
|
|
8923
9176
|
return;
|
|
8924
9177
|
}
|
|
@@ -8935,7 +9188,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8935
9188
|
continue;
|
|
8936
9189
|
}
|
|
8937
9190
|
if (!entry.isDirectory()) continue;
|
|
8938
|
-
const dateDir =
|
|
9191
|
+
const dateDir = path5.join(this.dir, entry.name);
|
|
8939
9192
|
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
8940
9193
|
for (const file of files) {
|
|
8941
9194
|
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
@@ -8947,7 +9200,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8947
9200
|
}
|
|
8948
9201
|
for (const entry of entries) {
|
|
8949
9202
|
if (!entry.isDirectory()) continue;
|
|
8950
|
-
const dateDir =
|
|
9203
|
+
const dateDir = path5.join(this.dir, entry.name);
|
|
8951
9204
|
try {
|
|
8952
9205
|
const remaining = await fsp6.readdir(dateDir);
|
|
8953
9206
|
if (remaining.length === 0) {
|
|
@@ -9122,7 +9375,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9122
9375
|
this.meta = meta;
|
|
9123
9376
|
this.events = events;
|
|
9124
9377
|
this.resumed = opts.resumed ?? false;
|
|
9125
|
-
this.manifestFile = opts.dir ?
|
|
9378
|
+
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
9126
9379
|
this.filePath = opts.filePath ?? "";
|
|
9127
9380
|
this.secretScrubber = opts.secretScrubber;
|
|
9128
9381
|
this.onCloseCb = opts.onClose;
|
|
@@ -9329,7 +9582,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9329
9582
|
await this.enqueueWrite(batch);
|
|
9330
9583
|
} catch (err) {
|
|
9331
9584
|
outcome = "failure";
|
|
9332
|
-
errorMsg =
|
|
9585
|
+
errorMsg = toErrorMessage(err);
|
|
9333
9586
|
this.appendFailCount += eventCount;
|
|
9334
9587
|
const now = Date.now();
|
|
9335
9588
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
@@ -9337,7 +9590,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9337
9590
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
9338
9591
|
console.warn(
|
|
9339
9592
|
"[session] flush failed:",
|
|
9340
|
-
|
|
9593
|
+
toErrorMessage(err),
|
|
9341
9594
|
tail
|
|
9342
9595
|
);
|
|
9343
9596
|
this.lastAppendWarnAt = now;
|
|
@@ -9427,7 +9680,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9427
9680
|
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
9428
9681
|
} catch (err) {
|
|
9429
9682
|
outcome = "failure";
|
|
9430
|
-
errorMsg =
|
|
9683
|
+
errorMsg = toErrorMessage(err);
|
|
9431
9684
|
} finally {
|
|
9432
9685
|
this.events?.emit("storage.write", {
|
|
9433
9686
|
sessionId: this.id,
|
|
@@ -9448,7 +9701,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9448
9701
|
await this.onCloseCb?.(this.summary);
|
|
9449
9702
|
} catch (err) {
|
|
9450
9703
|
idxOutcome = "failure";
|
|
9451
|
-
idxError =
|
|
9704
|
+
idxError = toErrorMessage(err);
|
|
9452
9705
|
} finally {
|
|
9453
9706
|
this.events?.emit("storage.write", {
|
|
9454
9707
|
sessionId: this.summary.id,
|
|
@@ -9625,9 +9878,9 @@ function makeDirectorSessionFactory(opts) {
|
|
|
9625
9878
|
let dir;
|
|
9626
9879
|
if (opts.store) {
|
|
9627
9880
|
store = opts.store;
|
|
9628
|
-
dir = opts.sessionsRoot ?
|
|
9881
|
+
dir = opts.sessionsRoot ? path5.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
9629
9882
|
} else if (opts.sessionsRoot) {
|
|
9630
|
-
dir =
|
|
9883
|
+
dir = path5.join(opts.sessionsRoot, runId);
|
|
9631
9884
|
store = new DefaultSessionStore({ dir });
|
|
9632
9885
|
} else {
|
|
9633
9886
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -9673,6 +9926,7 @@ function attachAutoExtend(events, policy = {}) {
|
|
|
9673
9926
|
const extendCounts = /* @__PURE__ */ new Map();
|
|
9674
9927
|
let progress = 0;
|
|
9675
9928
|
let lastTimeoutProgress = -1;
|
|
9929
|
+
let lastSeenKey = null;
|
|
9676
9930
|
const unsubs = [
|
|
9677
9931
|
events.on("tool.executed", () => {
|
|
9678
9932
|
progress++;
|
|
@@ -9682,6 +9936,9 @@ function attachAutoExtend(events, policy = {}) {
|
|
|
9682
9936
|
}),
|
|
9683
9937
|
events.on("budget.threshold_reached", (e) => {
|
|
9684
9938
|
const { kind, limit, extend, deny } = e;
|
|
9939
|
+
const key = `${kind}:${limit}`;
|
|
9940
|
+
if (key === lastSeenKey) return;
|
|
9941
|
+
lastSeenKey = key;
|
|
9685
9942
|
if (kind === "timeout" || kind === "idle_timeout") {
|
|
9686
9943
|
if (progress > lastTimeoutProgress) {
|
|
9687
9944
|
lastTimeoutProgress = progress;
|
|
@@ -9935,7 +10192,7 @@ var FleetManager = class {
|
|
|
9935
10192
|
})),
|
|
9936
10193
|
usage: this.usage.snapshot()
|
|
9937
10194
|
};
|
|
9938
|
-
await fsp6.mkdir(
|
|
10195
|
+
await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
|
|
9939
10196
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
9940
10197
|
return this.manifestPath;
|
|
9941
10198
|
}
|
|
@@ -9956,7 +10213,7 @@ var FleetManager = class {
|
|
|
9956
10213
|
if (!this.manifestPath) return;
|
|
9957
10214
|
if (this.manifestDebounceMs === 0) {
|
|
9958
10215
|
void this.writeManifest().catch((err) => {
|
|
9959
|
-
const detail =
|
|
10216
|
+
const detail = toErrorMessage(err);
|
|
9960
10217
|
process.emitWarning(
|
|
9961
10218
|
`FleetManager manifest write failed: ${detail}`,
|
|
9962
10219
|
"FleetManagerWarning"
|
|
@@ -9969,7 +10226,7 @@ var FleetManager = class {
|
|
|
9969
10226
|
this.manifestTimer = setTimeout(() => {
|
|
9970
10227
|
this.manifestTimer = null;
|
|
9971
10228
|
void this.writeManifest().catch((err) => {
|
|
9972
|
-
const detail =
|
|
10229
|
+
const detail = toErrorMessage(err);
|
|
9973
10230
|
process.emitWarning(
|
|
9974
10231
|
`FleetManager manifest write failed: ${detail}`,
|
|
9975
10232
|
"FleetManagerWarning"
|
|
@@ -9988,7 +10245,7 @@ var FleetManager = class {
|
|
|
9988
10245
|
this.manifestTimer = null;
|
|
9989
10246
|
}
|
|
9990
10247
|
await this.writeManifest().catch((err) => {
|
|
9991
|
-
const detail =
|
|
10248
|
+
const detail = toErrorMessage(err);
|
|
9992
10249
|
process.emitWarning(
|
|
9993
10250
|
`FleetManager manifest write failed: ${detail}`,
|
|
9994
10251
|
"FleetManagerWarning"
|
|
@@ -10083,7 +10340,7 @@ var LINE_SEPARATOR = "\n";
|
|
|
10083
10340
|
var DefaultMailbox = class {
|
|
10084
10341
|
filePath;
|
|
10085
10342
|
constructor(sessionDir) {
|
|
10086
|
-
this.filePath =
|
|
10343
|
+
this.filePath = path5.join(sessionDir, MAILBOX_FILE);
|
|
10087
10344
|
}
|
|
10088
10345
|
get mailboxPath() {
|
|
10089
10346
|
return this.filePath;
|
|
@@ -10108,7 +10365,7 @@ var DefaultMailbox = class {
|
|
|
10108
10365
|
taskContext: input.taskContext
|
|
10109
10366
|
};
|
|
10110
10367
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
10111
|
-
await fsp6.mkdir(
|
|
10368
|
+
await fsp6.mkdir(path5.dirname(this.filePath), { recursive: true });
|
|
10112
10369
|
await withFileLock(this.filePath, async () => {
|
|
10113
10370
|
await fsp6.appendFile(this.filePath, line, "utf8");
|
|
10114
10371
|
});
|
|
@@ -10148,29 +10405,43 @@ var DefaultMailbox = class {
|
|
|
10148
10405
|
}
|
|
10149
10406
|
// ── Ack ───────────────────────────────────────────────────────────────
|
|
10150
10407
|
async ack(input) {
|
|
10151
|
-
|
|
10408
|
+
const updated = await this.ackMany({ acks: [input] });
|
|
10409
|
+
return updated.length > 0 ? updated[0] : null;
|
|
10410
|
+
}
|
|
10411
|
+
async ackMany(input) {
|
|
10412
|
+
if (input.acks.length === 0) return [];
|
|
10413
|
+
const updated = [];
|
|
10414
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10415
|
+
for (const a of input.acks) byId.set(a.messageId, a);
|
|
10152
10416
|
await withFileLock(this.filePath, async () => {
|
|
10153
10417
|
const all = await this._readAll();
|
|
10154
|
-
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10155
|
-
if (idx === -1) return;
|
|
10156
|
-
const msg = all[idx];
|
|
10157
10418
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
msg
|
|
10163
|
-
|
|
10164
|
-
|
|
10419
|
+
let changed = false;
|
|
10420
|
+
for (const msg of all) {
|
|
10421
|
+
const a = byId.get(msg.id);
|
|
10422
|
+
if (!a) continue;
|
|
10423
|
+
updated.push(msg);
|
|
10424
|
+
if (a.read !== false && !(a.readerId in msg.readBy)) {
|
|
10425
|
+
msg.readBy[a.readerId] = now;
|
|
10426
|
+
changed = true;
|
|
10427
|
+
}
|
|
10428
|
+
if (a.completed && !msg.completed) {
|
|
10429
|
+
msg.completed = true;
|
|
10430
|
+
msg.completedBy = a.readerId;
|
|
10431
|
+
msg.completedAt = now;
|
|
10432
|
+
changed = true;
|
|
10433
|
+
}
|
|
10434
|
+
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
10435
|
+
msg.outcome = a.outcome;
|
|
10436
|
+
changed = true;
|
|
10437
|
+
}
|
|
10165
10438
|
}
|
|
10166
|
-
if (
|
|
10167
|
-
|
|
10439
|
+
if (changed) {
|
|
10440
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10441
|
+
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
10168
10442
|
}
|
|
10169
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10170
|
-
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
10171
|
-
result = msg;
|
|
10172
10443
|
});
|
|
10173
|
-
return
|
|
10444
|
+
return updated;
|
|
10174
10445
|
}
|
|
10175
10446
|
// ── Agent statuses ────────────────────────────────────────────────────
|
|
10176
10447
|
async getAgentStatuses() {
|
|
@@ -10222,6 +10493,43 @@ var DefaultMailbox = class {
|
|
|
10222
10493
|
await fsp6.writeFile(this.filePath, "", "utf8");
|
|
10223
10494
|
});
|
|
10224
10495
|
}
|
|
10496
|
+
async purgeStale(opts) {
|
|
10497
|
+
const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
|
|
10498
|
+
const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
10499
|
+
let completedPurged = 0;
|
|
10500
|
+
let incompletePurged = 0;
|
|
10501
|
+
await withFileLock(this.filePath, async () => {
|
|
10502
|
+
const all2 = await this._readAll();
|
|
10503
|
+
const now = Date.now();
|
|
10504
|
+
const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
|
|
10505
|
+
const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
|
|
10506
|
+
const kept = [];
|
|
10507
|
+
for (const msg of all2) {
|
|
10508
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
10509
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
10510
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
10511
|
+
completedPurged++;
|
|
10512
|
+
continue;
|
|
10513
|
+
}
|
|
10514
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
10515
|
+
incompletePurged++;
|
|
10516
|
+
continue;
|
|
10517
|
+
}
|
|
10518
|
+
kept.push(msg);
|
|
10519
|
+
}
|
|
10520
|
+
if (kept.length < all2.length) {
|
|
10521
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10522
|
+
await fsp6.writeFile(this.filePath, content, "utf8");
|
|
10523
|
+
}
|
|
10524
|
+
});
|
|
10525
|
+
const all = await this._readAll();
|
|
10526
|
+
return {
|
|
10527
|
+
completedPurged,
|
|
10528
|
+
incompletePurged,
|
|
10529
|
+
totalPurged: completedPurged + incompletePurged,
|
|
10530
|
+
remaining: all.length
|
|
10531
|
+
};
|
|
10532
|
+
}
|
|
10225
10533
|
// ── Client registry stubs (not applicable per-session) ─────────────────
|
|
10226
10534
|
async registerClient(_input) {
|
|
10227
10535
|
}
|
|
@@ -10340,7 +10648,8 @@ var BrainMonitor = class {
|
|
|
10340
10648
|
id: "steer",
|
|
10341
10649
|
label: "Steer the agent with corrective guidance",
|
|
10342
10650
|
consequence: "A steer message is injected before its next step.",
|
|
10343
|
-
risk: "low"
|
|
10651
|
+
risk: "low",
|
|
10652
|
+
recommended: true
|
|
10344
10653
|
},
|
|
10345
10654
|
{
|
|
10346
10655
|
id: "continue",
|
|
@@ -10349,9 +10658,9 @@ var BrainMonitor = class {
|
|
|
10349
10658
|
}
|
|
10350
10659
|
],
|
|
10351
10660
|
risk: "medium",
|
|
10352
|
-
//
|
|
10353
|
-
//
|
|
10354
|
-
fallback: "
|
|
10661
|
+
// 'ask_human' routes to the LLM-backed autonomous layer via
|
|
10662
|
+
// createTieredBrainArbiter before any human escalation.
|
|
10663
|
+
fallback: "ask_human"
|
|
10355
10664
|
};
|
|
10356
10665
|
const decision = await this.opts.brain.decide(request);
|
|
10357
10666
|
const intervened = await this.maybeIntervene(kind, request, decision);
|
|
@@ -10390,21 +10699,6 @@ var BrainMonitor = class {
|
|
|
10390
10699
|
}
|
|
10391
10700
|
}
|
|
10392
10701
|
};
|
|
10393
|
-
function projectSlug(absRoot) {
|
|
10394
|
-
const base = slugify(path4.basename(absRoot));
|
|
10395
|
-
const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
10396
|
-
return `${base}-${hash}`;
|
|
10397
|
-
}
|
|
10398
|
-
function slugify(name) {
|
|
10399
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
10400
|
-
}
|
|
10401
|
-
function wstackGlobalRoot() {
|
|
10402
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
10403
|
-
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
10404
|
-
return path4.join(os.homedir(), ".wrongstack");
|
|
10405
|
-
}
|
|
10406
|
-
|
|
10407
|
-
// src/coordination/global-mailbox.ts
|
|
10408
10702
|
var MAILBOX_FILE2 = "_mailbox.jsonl";
|
|
10409
10703
|
var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
|
|
10410
10704
|
var AGENT_STALE_MS = 6e4;
|
|
@@ -10412,8 +10706,9 @@ var CLIENT_STALE_MS = 6e4;
|
|
|
10412
10706
|
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
10413
10707
|
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
10414
10708
|
var LINE_SEPARATOR2 = "\n";
|
|
10709
|
+
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
10415
10710
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
10416
|
-
return
|
|
10711
|
+
return path5.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
10417
10712
|
}
|
|
10418
10713
|
var GlobalMailbox = class {
|
|
10419
10714
|
/** Path to the JSONL message file. */
|
|
@@ -10444,14 +10739,28 @@ var GlobalMailbox = class {
|
|
|
10444
10739
|
_lastHeartbeat = /* @__PURE__ */ new Map();
|
|
10445
10740
|
/** Last time each local client sent a heartbeat (throttle). */
|
|
10446
10741
|
_lastClientHeartbeat = /* @__PURE__ */ new Map();
|
|
10742
|
+
/**
|
|
10743
|
+
* In-memory mirror of the JSONL message file. The mailbox is shared
|
|
10744
|
+
* ACROSS PROCESSES, so reads cannot trust the cache blindly — we pair it
|
|
10745
|
+
* with an mtime check. The file lock serializes every write, so a
|
|
10746
|
+
* changed mtimeMs is a definitive signal that another process (or this
|
|
10747
|
+
* one) wrote; an unchanged mtimeMs guarantees no write happened and the
|
|
10748
|
+
* cache is current. This collapses the per-iteration `query()` cost from
|
|
10749
|
+
* O(file_size) disk + parse to O(messages) in memory.
|
|
10750
|
+
*/
|
|
10751
|
+
_messageCache = null;
|
|
10752
|
+
/** mtimeMs of the file when `_messageCache` was populated. */
|
|
10753
|
+
_messageCacheMtime = -1;
|
|
10754
|
+
/** Size of the file when `_messageCache` was populated (extra guard). */
|
|
10755
|
+
_messageCacheSize = -1;
|
|
10447
10756
|
/**
|
|
10448
10757
|
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
10449
10758
|
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
10450
10759
|
*/
|
|
10451
10760
|
constructor(projectDir, events) {
|
|
10452
|
-
this.messagePath =
|
|
10453
|
-
this.registryPath =
|
|
10454
|
-
this.clientRegistryPath =
|
|
10761
|
+
this.messagePath = path5.join(projectDir, MAILBOX_FILE2);
|
|
10762
|
+
this.registryPath = path5.join(projectDir, "_mailbox.registry.json");
|
|
10763
|
+
this.clientRegistryPath = path5.join(projectDir, CLIENT_REGISTRY_FILE);
|
|
10455
10764
|
this._events = events;
|
|
10456
10765
|
}
|
|
10457
10766
|
// ── Messages ────────────────────────────────────────────────────────────
|
|
@@ -10474,73 +10783,89 @@ var GlobalMailbox = class {
|
|
|
10474
10783
|
taskContext: input.taskContext
|
|
10475
10784
|
};
|
|
10476
10785
|
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
10477
|
-
await fsp6.mkdir(
|
|
10786
|
+
await fsp6.mkdir(path5.dirname(this.messagePath), { recursive: true });
|
|
10478
10787
|
await withFileLock(this.messagePath, async () => {
|
|
10479
10788
|
await fsp6.appendFile(this.messagePath, line, "utf8");
|
|
10789
|
+
this._pushToCache(msg);
|
|
10480
10790
|
});
|
|
10481
10791
|
return msg;
|
|
10482
10792
|
}
|
|
10483
10793
|
async query(q) {
|
|
10484
|
-
const all = await this.
|
|
10794
|
+
const all = await this._readMessagesCached();
|
|
10485
10795
|
const limit = q.limit ?? 50;
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
}
|
|
10502
|
-
if (q.minPriority !== void 0) {
|
|
10503
|
-
const order = { low: 0, normal: 1, high: 2 };
|
|
10504
|
-
const min = order[q.minPriority];
|
|
10505
|
-
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
10506
|
-
}
|
|
10507
|
-
if (q.since !== void 0) {
|
|
10508
|
-
const since = q.since;
|
|
10509
|
-
filtered = filtered.filter((m) => m.timestamp > since);
|
|
10796
|
+
const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
|
|
10797
|
+
const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
|
|
10798
|
+
const out = [];
|
|
10799
|
+
for (let i = 0; i < all.length; i++) {
|
|
10800
|
+
const m = all[i];
|
|
10801
|
+
if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
|
|
10802
|
+
if (q.from !== void 0 && m.from !== q.from) continue;
|
|
10803
|
+
if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
|
|
10804
|
+
if (q.incompleteOnly && m.completed) continue;
|
|
10805
|
+
if (q.type !== void 0 && m.type !== q.type) continue;
|
|
10806
|
+
if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
|
|
10807
|
+
continue;
|
|
10808
|
+
}
|
|
10809
|
+
if (q.since !== void 0 && m.timestamp <= q.since) continue;
|
|
10810
|
+
out.push(m);
|
|
10510
10811
|
}
|
|
10511
|
-
|
|
10512
|
-
return
|
|
10812
|
+
out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
10813
|
+
return out.slice(0, limit).map((m) => ({ ...m, readBy: { ...m.readBy } }));
|
|
10513
10814
|
}
|
|
10514
10815
|
async ack(input) {
|
|
10515
|
-
|
|
10816
|
+
const updated = await this.ackMany({ acks: [input] });
|
|
10817
|
+
return updated.length > 0 ? updated[0] : null;
|
|
10818
|
+
}
|
|
10819
|
+
async ackMany(input) {
|
|
10820
|
+
if (input.acks.length === 0) return [];
|
|
10821
|
+
const updated = [];
|
|
10822
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10823
|
+
for (const a of input.acks) {
|
|
10824
|
+
byId.set(a.messageId, a);
|
|
10825
|
+
}
|
|
10826
|
+
let cacheSnapshot = null;
|
|
10516
10827
|
await withFileLock(this.messagePath, async () => {
|
|
10517
|
-
const all = await this.
|
|
10518
|
-
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10519
|
-
if (idx === -1) return;
|
|
10520
|
-
const msg = all[idx];
|
|
10828
|
+
const all = await this._readMessagesFresh();
|
|
10521
10829
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
msg
|
|
10527
|
-
|
|
10528
|
-
|
|
10830
|
+
let changed = false;
|
|
10831
|
+
for (const msg of all) {
|
|
10832
|
+
const a = byId.get(msg.id);
|
|
10833
|
+
if (!a) continue;
|
|
10834
|
+
updated.push(msg);
|
|
10835
|
+
if (a.read !== false && !(a.readerId in msg.readBy)) {
|
|
10836
|
+
msg.readBy[a.readerId] = now;
|
|
10837
|
+
changed = true;
|
|
10838
|
+
}
|
|
10839
|
+
if (a.completed && !msg.completed) {
|
|
10840
|
+
msg.completed = true;
|
|
10841
|
+
msg.completedBy = a.readerId;
|
|
10842
|
+
msg.completedAt = now;
|
|
10843
|
+
changed = true;
|
|
10844
|
+
}
|
|
10845
|
+
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
10846
|
+
msg.outcome = a.outcome;
|
|
10847
|
+
changed = true;
|
|
10848
|
+
}
|
|
10529
10849
|
}
|
|
10530
|
-
if (
|
|
10531
|
-
|
|
10850
|
+
if (changed) {
|
|
10851
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
10852
|
+
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10532
10853
|
}
|
|
10533
|
-
|
|
10534
|
-
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10535
|
-
result = msg;
|
|
10854
|
+
cacheSnapshot = all;
|
|
10536
10855
|
});
|
|
10537
|
-
|
|
10856
|
+
if (cacheSnapshot) this._setMessageCache(cacheSnapshot);
|
|
10857
|
+
return updated;
|
|
10538
10858
|
}
|
|
10539
10859
|
async unreadCount(forAgentId) {
|
|
10540
|
-
const all = await this.
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10860
|
+
const all = await this._readMessagesCached();
|
|
10861
|
+
let count = 0;
|
|
10862
|
+
for (let i = 0; i < all.length; i++) {
|
|
10863
|
+
const m = all[i];
|
|
10864
|
+
if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
|
|
10865
|
+
count++;
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
return count;
|
|
10544
10869
|
}
|
|
10545
10870
|
// ── Agent registry ──────────────────────────────────────────────────────
|
|
10546
10871
|
async registerAgent(input) {
|
|
@@ -10701,13 +11026,62 @@ var GlobalMailbox = class {
|
|
|
10701
11026
|
async close() {
|
|
10702
11027
|
this._registryCache = null;
|
|
10703
11028
|
this._clientRegistryCache = null;
|
|
11029
|
+
this._messageCache = null;
|
|
11030
|
+
this._messageCacheMtime = -1;
|
|
11031
|
+
this._messageCacheSize = -1;
|
|
10704
11032
|
}
|
|
10705
11033
|
async clearAll() {
|
|
10706
11034
|
await withFileLock(this.messagePath, async () => {
|
|
10707
11035
|
await fsp6.writeFile(this.messagePath, "", "utf8");
|
|
10708
11036
|
});
|
|
11037
|
+
this._setMessageCache([]);
|
|
11038
|
+
}
|
|
11039
|
+
async purgeStale(opts) {
|
|
11040
|
+
const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
|
|
11041
|
+
const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
11042
|
+
let completedPurged = 0;
|
|
11043
|
+
let incompletePurged = 0;
|
|
11044
|
+
let remaining = 0;
|
|
11045
|
+
await withFileLock(this.messagePath, async () => {
|
|
11046
|
+
const all = await this._readMessagesFresh();
|
|
11047
|
+
const now = Date.now();
|
|
11048
|
+
const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
|
|
11049
|
+
const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
|
|
11050
|
+
const kept = [];
|
|
11051
|
+
for (const msg of all) {
|
|
11052
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
11053
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
11054
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
11055
|
+
completedPurged++;
|
|
11056
|
+
continue;
|
|
11057
|
+
}
|
|
11058
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
11059
|
+
incompletePurged++;
|
|
11060
|
+
continue;
|
|
11061
|
+
}
|
|
11062
|
+
kept.push(msg);
|
|
11063
|
+
}
|
|
11064
|
+
remaining = kept.length;
|
|
11065
|
+
if (kept.length < all.length) {
|
|
11066
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
11067
|
+
await fsp6.writeFile(this.messagePath, content, "utf8");
|
|
11068
|
+
}
|
|
11069
|
+
this._setMessageCache(kept);
|
|
11070
|
+
});
|
|
11071
|
+
return {
|
|
11072
|
+
completedPurged,
|
|
11073
|
+
incompletePurged,
|
|
11074
|
+
totalPurged: completedPurged + incompletePurged,
|
|
11075
|
+
remaining
|
|
11076
|
+
};
|
|
10709
11077
|
}
|
|
10710
11078
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
11079
|
+
/**
|
|
11080
|
+
* Read all messages from the JSONL file. Always reads + parses the file.
|
|
11081
|
+
* Callers that can tolerate a stale-by-mtime view should use
|
|
11082
|
+
* {@link _readMessagesCached}; writers that need the post-lock truth
|
|
11083
|
+
* should call this directly (it's what {@link _readMessagesFresh} aliases).
|
|
11084
|
+
*/
|
|
10711
11085
|
async _readMessages() {
|
|
10712
11086
|
try {
|
|
10713
11087
|
const raw = await fsp6.readFile(this.messagePath, "utf8");
|
|
@@ -10735,8 +11109,85 @@ var GlobalMailbox = class {
|
|
|
10735
11109
|
throw err;
|
|
10736
11110
|
}
|
|
10737
11111
|
}
|
|
11112
|
+
/**
|
|
11113
|
+
* Read messages, then adopt the result as the in-memory cache. Use this
|
|
11114
|
+
* from writers that just took the file lock — the read reflects the
|
|
11115
|
+
* authoritative post-lock state and should be served to subsequent
|
|
11116
|
+
* queries without re-reading.
|
|
11117
|
+
*/
|
|
11118
|
+
async _readMessagesFresh() {
|
|
11119
|
+
const all = await this._readMessages();
|
|
11120
|
+
this._setMessageCache(all);
|
|
11121
|
+
return all;
|
|
11122
|
+
}
|
|
11123
|
+
/**
|
|
11124
|
+
* Read messages, consulting the mtime-bounded in-memory cache first.
|
|
11125
|
+
* The mailbox file is shared across processes; every `send`/`ack`/
|
|
11126
|
+
* `clearAll`/`purgeStale` takes the file lock, so writes are serialized
|
|
11127
|
+
* and a changed mtimeMs is a definitive freshness signal. When the
|
|
11128
|
+
* stat matches the cached mtime+size we return the cached array — no
|
|
11129
|
+
* file read and no JSON.parse — collapsing the per-iteration query
|
|
11130
|
+
* cost on the mailbox-loop hot path.
|
|
11131
|
+
*/
|
|
11132
|
+
async _readMessagesCached() {
|
|
11133
|
+
try {
|
|
11134
|
+
const st = await fsp6.stat(this.messagePath);
|
|
11135
|
+
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
11136
|
+
return this._messageCache;
|
|
11137
|
+
}
|
|
11138
|
+
const all = await this._readMessages();
|
|
11139
|
+
this._setMessageCache(all, st.mtimeMs, st.size);
|
|
11140
|
+
return all;
|
|
11141
|
+
} catch (err) {
|
|
11142
|
+
if (err.code === "ENOENT") {
|
|
11143
|
+
this._setMessageCache([], -1, -1);
|
|
11144
|
+
return [];
|
|
11145
|
+
}
|
|
11146
|
+
throw err;
|
|
11147
|
+
}
|
|
11148
|
+
}
|
|
11149
|
+
/**
|
|
11150
|
+
* Replace the in-memory cache. Caller is responsible for guaranteeing
|
|
11151
|
+
* that `messages` reflects the current on-disk state (e.g. they just
|
|
11152
|
+
* read or wrote it under the file lock).
|
|
11153
|
+
*/
|
|
11154
|
+
_setMessageCache(messages, mtime, size) {
|
|
11155
|
+
if (messages.length > MESSAGE_CACHE_MAX_ENTRIES) {
|
|
11156
|
+
this._messageCache = null;
|
|
11157
|
+
this._messageCacheMtime = -1;
|
|
11158
|
+
this._messageCacheSize = -1;
|
|
11159
|
+
return;
|
|
11160
|
+
}
|
|
11161
|
+
this._messageCache = messages;
|
|
11162
|
+
if (mtime !== void 0 && size !== void 0) {
|
|
11163
|
+
this._messageCacheMtime = mtime;
|
|
11164
|
+
this._messageCacheSize = size;
|
|
11165
|
+
} else {
|
|
11166
|
+
void fsp6.stat(this.messagePath).then((st) => {
|
|
11167
|
+
this._messageCacheMtime = st.mtimeMs;
|
|
11168
|
+
this._messageCacheSize = st.size;
|
|
11169
|
+
}).catch(() => {
|
|
11170
|
+
});
|
|
11171
|
+
}
|
|
11172
|
+
}
|
|
11173
|
+
/**
|
|
11174
|
+
* Append a single just-sent message to the in-memory cache without
|
|
11175
|
+
* re-reading the file. The caller must hold the file lock (or have
|
|
11176
|
+
* just released it after a successful append) so the cache stays
|
|
11177
|
+
* consistent with on-disk state.
|
|
11178
|
+
*/
|
|
11179
|
+
_pushToCache(msg) {
|
|
11180
|
+
if (this._messageCache === null) return;
|
|
11181
|
+
if (this._messageCache.length >= MESSAGE_CACHE_MAX_ENTRIES) {
|
|
11182
|
+
this._messageCache = null;
|
|
11183
|
+
this._messageCacheMtime = -1;
|
|
11184
|
+
this._messageCacheSize = -1;
|
|
11185
|
+
return;
|
|
11186
|
+
}
|
|
11187
|
+
this._messageCache.push(msg);
|
|
11188
|
+
}
|
|
10738
11189
|
async _ensureRegistry() {
|
|
10739
|
-
await fsp6.mkdir(
|
|
11190
|
+
await fsp6.mkdir(path5.dirname(this.registryPath), { recursive: true });
|
|
10740
11191
|
}
|
|
10741
11192
|
async _readRegistry(opts) {
|
|
10742
11193
|
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
@@ -10781,7 +11232,7 @@ var GlobalMailbox = class {
|
|
|
10781
11232
|
}
|
|
10782
11233
|
// ── Client registry internals ───────────────────────────────────────────
|
|
10783
11234
|
async _ensureClientRegistry() {
|
|
10784
|
-
await fsp6.mkdir(
|
|
11235
|
+
await fsp6.mkdir(path5.dirname(this.clientRegistryPath), { recursive: true });
|
|
10785
11236
|
}
|
|
10786
11237
|
async _readClientRegistry(opts) {
|
|
10787
11238
|
if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
@@ -11306,6 +11757,10 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
11306
11757
|
return {
|
|
11307
11758
|
watchPaths: unique,
|
|
11308
11759
|
debounceMs,
|
|
11760
|
+
dispose() {
|
|
11761
|
+
for (const t of pending.values()) clearTimeout(t);
|
|
11762
|
+
pending.clear();
|
|
11763
|
+
},
|
|
11309
11764
|
async onChange(entry) {
|
|
11310
11765
|
if (entry.event === "delete") return;
|
|
11311
11766
|
if (!matchesPattern(entry.path)) return;
|
|
@@ -11421,7 +11876,7 @@ function createMailboxHooks(opts) {
|
|
|
11421
11876
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
11422
11877
|
var LOG_FILENAME = "package-authors.json";
|
|
11423
11878
|
function logPath(storageDir) {
|
|
11424
|
-
return
|
|
11879
|
+
return path5.join(storageDir, LOG_FILENAME);
|
|
11425
11880
|
}
|
|
11426
11881
|
async function loadLog(storageDir, projectRoot) {
|
|
11427
11882
|
try {
|
|
@@ -11445,7 +11900,7 @@ async function saveLog(storageDir, log) {
|
|
|
11445
11900
|
await fsp6.rename(tmp, logPath(storageDir));
|
|
11446
11901
|
}
|
|
11447
11902
|
function detectEcosystem(manifestPath) {
|
|
11448
|
-
const name =
|
|
11903
|
+
const name = path5.basename(manifestPath).toLowerCase();
|
|
11449
11904
|
if (name === "package.json") return "npm";
|
|
11450
11905
|
if (name === "go.mod") return "go";
|
|
11451
11906
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -11642,7 +12097,7 @@ function startPackageOutdatedWatcher(opts) {
|
|
|
11642
12097
|
);
|
|
11643
12098
|
} catch (err) {
|
|
11644
12099
|
handleError(err);
|
|
11645
|
-
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${
|
|
12100
|
+
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${toErrorMessage(err)}`);
|
|
11646
12101
|
}
|
|
11647
12102
|
}
|
|
11648
12103
|
}
|
|
@@ -11715,7 +12170,2210 @@ mvn versions:use-latest-versions`;
|
|
|
11715
12170
|
return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
|
|
11716
12171
|
}
|
|
11717
12172
|
}
|
|
12173
|
+
var KnowledgeGraph = class {
|
|
12174
|
+
nodes = /* @__PURE__ */ new Map();
|
|
12175
|
+
index = /* @__PURE__ */ new Map();
|
|
12176
|
+
// tag/field → node ids
|
|
12177
|
+
subs = /* @__PURE__ */ new Map();
|
|
12178
|
+
pendingDeliveries = /* @__PURE__ */ new Map();
|
|
12179
|
+
filePath;
|
|
12180
|
+
graphFilePath;
|
|
12181
|
+
/** Exposed for unit-testing only: read current index contents. */
|
|
12182
|
+
getIndex() {
|
|
12183
|
+
return this.index;
|
|
12184
|
+
}
|
|
12185
|
+
constructor(sessionDir) {
|
|
12186
|
+
this.filePath = path5.join(sessionDir, "_knowledge_graph");
|
|
12187
|
+
this.graphFilePath = path5.join(this.filePath, "graph.jsonl");
|
|
12188
|
+
}
|
|
12189
|
+
// ── Write ──────────────────────────────────────────────────────────────
|
|
12190
|
+
/**
|
|
12191
|
+
* Add a node. Fires to all matching subscriptions synchronously.
|
|
12192
|
+
* Returns the node with its assigned id.
|
|
12193
|
+
*/
|
|
12194
|
+
async add(node) {
|
|
12195
|
+
const full = { id: randomUUID(), ...node };
|
|
12196
|
+
this.nodes.set(full.id, full);
|
|
12197
|
+
this._addToIndex(full, this._indexKeys(full));
|
|
12198
|
+
await this._persist(full);
|
|
12199
|
+
this._deliver(full);
|
|
12200
|
+
return full;
|
|
12201
|
+
}
|
|
12202
|
+
/** Update an existing node by id. Returns updated node or null if not found. */
|
|
12203
|
+
async update(id, patch) {
|
|
12204
|
+
const existing = this.nodes.get(id);
|
|
12205
|
+
if (!existing) return null;
|
|
12206
|
+
this._removeFromIndex(existing, this._indexKeys(existing));
|
|
12207
|
+
const updated = { ...existing, ...patch };
|
|
12208
|
+
this.nodes.set(id, updated);
|
|
12209
|
+
this._addToIndex(updated, this._indexKeys(updated));
|
|
12210
|
+
this._deliver(updated);
|
|
12211
|
+
await this._append(updated);
|
|
12212
|
+
return updated;
|
|
12213
|
+
}
|
|
12214
|
+
// ── Read ───────────────────────────────────────────────────────────────
|
|
12215
|
+
get(id) {
|
|
12216
|
+
return this.nodes.get(id);
|
|
12217
|
+
}
|
|
12218
|
+
getAll(filter) {
|
|
12219
|
+
return Array.from(this.nodes.values()).filter((n) => this._matches(n, filter ?? {}));
|
|
12220
|
+
}
|
|
12221
|
+
getGoals(filter) {
|
|
12222
|
+
return this.getAll({ type: "goal", ...filter });
|
|
12223
|
+
}
|
|
12224
|
+
getFacts(filter) {
|
|
12225
|
+
return this.getAll({ type: "fact", ...filter });
|
|
12226
|
+
}
|
|
12227
|
+
getChanges(filter) {
|
|
12228
|
+
return this.getAll({ type: "change", ...filter });
|
|
12229
|
+
}
|
|
12230
|
+
getOpenGoals() {
|
|
12231
|
+
return this.getGoals({ status: "pending" }).concat(
|
|
12232
|
+
this.getGoals({ status: "in_progress" })
|
|
12233
|
+
);
|
|
12234
|
+
}
|
|
12235
|
+
getTopLevelGoals() {
|
|
12236
|
+
return this.getGoals({}).filter((g) => !g.parentGoal);
|
|
12237
|
+
}
|
|
12238
|
+
getBlockedGoals() {
|
|
12239
|
+
return this.getGoals({ status: "blocked" });
|
|
12240
|
+
}
|
|
12241
|
+
getPendingChanges() {
|
|
12242
|
+
return this.getChanges({ status: "proposed" });
|
|
12243
|
+
}
|
|
12244
|
+
getDecisions(since) {
|
|
12245
|
+
return this.getAll({ type: "decision", since });
|
|
12246
|
+
}
|
|
12247
|
+
// ── Search ─────────────────────────────────────────────────────────────
|
|
12248
|
+
searchFacts(query) {
|
|
12249
|
+
const q = query.toLowerCase();
|
|
12250
|
+
return this.getFacts().filter(
|
|
12251
|
+
(f) => f.subject.toLowerCase().includes(q) || f.detail.toLowerCase().includes(q) || f.file?.toLowerCase().includes(q) || f.tags.some((t) => t.toLowerCase().includes(q))
|
|
12252
|
+
);
|
|
12253
|
+
}
|
|
12254
|
+
getRelatedFacts(factId) {
|
|
12255
|
+
const fact = this.nodes.get(factId);
|
|
12256
|
+
if (!fact) return [];
|
|
12257
|
+
return fact.related.map((id) => this.nodes.get(id)).filter((n) => n?.type === "fact");
|
|
12258
|
+
}
|
|
12259
|
+
// ── Subscriptions ──────────────────────────────────────────────────────
|
|
12260
|
+
/**
|
|
12261
|
+
* Subscribe to nodes matching a filter. Returns a channel id that can be
|
|
12262
|
+
* used to poll for new nodes since the last check.
|
|
12263
|
+
*/
|
|
12264
|
+
subscribe(agentId, filter) {
|
|
12265
|
+
const channel = randomUUID();
|
|
12266
|
+
const sub = { id: randomUUID(), agentId, filter, channel };
|
|
12267
|
+
this.subs.set(channel, sub);
|
|
12268
|
+
this.pendingDeliveries.set(channel, []);
|
|
12269
|
+
return channel;
|
|
12270
|
+
}
|
|
12271
|
+
/**
|
|
12272
|
+
* Poll for new nodes delivered to a channel since last check.
|
|
12273
|
+
* Clears the delivery buffer after reading.
|
|
12274
|
+
*/
|
|
12275
|
+
poll(channel) {
|
|
12276
|
+
const pending = this.pendingDeliveries.get(channel);
|
|
12277
|
+
if (!pending) return [];
|
|
12278
|
+
const delivered = [...pending];
|
|
12279
|
+
pending.length = 0;
|
|
12280
|
+
return delivered;
|
|
12281
|
+
}
|
|
12282
|
+
unsubscribe(channel) {
|
|
12283
|
+
this.subs.delete(channel);
|
|
12284
|
+
this.pendingDeliveries.delete(channel);
|
|
12285
|
+
}
|
|
12286
|
+
// ── Quality gate helpers ───────────────────────────────────────────────
|
|
12287
|
+
/**
|
|
12288
|
+
* Create a quality gate result. Call this when a change is being proposed
|
|
12289
|
+
* so the change node carries the gate result.
|
|
12290
|
+
*/
|
|
12291
|
+
static makeQualityGate(checks) {
|
|
12292
|
+
return { passed: checks.every((c) => c.passed), checks };
|
|
12293
|
+
}
|
|
12294
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
12295
|
+
/** Pure: compute the set of index keys a node would belong to. */
|
|
12296
|
+
_indexKeys(node) {
|
|
12297
|
+
const keys = /* @__PURE__ */ new Set();
|
|
12298
|
+
const add = (key) => keys.add(key);
|
|
12299
|
+
add(`type:${node.type}`);
|
|
12300
|
+
if (node.type === "fact") {
|
|
12301
|
+
const f = node;
|
|
12302
|
+
add(`cat:${f.category}`);
|
|
12303
|
+
if (f.severity) add(`sev:${f.severity}`);
|
|
12304
|
+
add(`by:${f.discoveredBy}`);
|
|
12305
|
+
for (const tag of f.tags) add(`tag:${tag}`);
|
|
12306
|
+
add(`key:${f.key}`);
|
|
12307
|
+
add(`subject:${f.subject}`);
|
|
12308
|
+
if (f.detail) add(`detail:${f.detail}`);
|
|
12309
|
+
}
|
|
12310
|
+
if (node.type === "goal") {
|
|
12311
|
+
const g = node;
|
|
12312
|
+
add(`status:${g.status}`);
|
|
12313
|
+
add(`prio:${g.priority}`);
|
|
12314
|
+
if (g.assignee) add(`assign:${g.assignee}`);
|
|
12315
|
+
for (const tag of g.tags) add(`tag:${tag}`);
|
|
12316
|
+
}
|
|
12317
|
+
if (node.type === "change") {
|
|
12318
|
+
const c = node;
|
|
12319
|
+
add(`change:${c.status}`);
|
|
12320
|
+
add(`by:${c.proposedBy}`);
|
|
12321
|
+
for (const g of c.satisfiesGoals) add(`goal:${g}`);
|
|
12322
|
+
}
|
|
12323
|
+
return keys;
|
|
12324
|
+
}
|
|
12325
|
+
/** Mutate the index: add a node's id to every set for the given keys. */
|
|
12326
|
+
_addToIndex(node, keys) {
|
|
12327
|
+
for (const key of keys) {
|
|
12328
|
+
let set = this.index.get(key);
|
|
12329
|
+
if (!set) {
|
|
12330
|
+
set = /* @__PURE__ */ new Set();
|
|
12331
|
+
this.index.set(key, set);
|
|
12332
|
+
}
|
|
12333
|
+
set.add(node.id);
|
|
12334
|
+
}
|
|
12335
|
+
}
|
|
12336
|
+
/** Remove a node's id from all index sets for the given keys. */
|
|
12337
|
+
_removeFromIndex(node, keys) {
|
|
12338
|
+
for (const key of keys) {
|
|
12339
|
+
this.index.get(key)?.delete(node.id);
|
|
12340
|
+
}
|
|
12341
|
+
}
|
|
12342
|
+
_matches(node, f) {
|
|
12343
|
+
if (f.type && node.type !== f.type) return false;
|
|
12344
|
+
if (f.category && node.category !== f.category) return false;
|
|
12345
|
+
if (f.status) {
|
|
12346
|
+
if (node.type === "goal" && node.status !== f.status) return false;
|
|
12347
|
+
if (node.type === "change" && node.status !== f.status) return false;
|
|
12348
|
+
}
|
|
12349
|
+
if (f.assignee && node.assignee !== f.assignee) return false;
|
|
12350
|
+
if (f.discoveredBy && node.discoveredBy !== f.discoveredBy) return false;
|
|
12351
|
+
if (f.proposedBy && node.proposedBy !== f.proposedBy) return false;
|
|
12352
|
+
if (f.tags?.length) {
|
|
12353
|
+
const nodeTags = node.tags ?? node.tags ?? [];
|
|
12354
|
+
if (!f.tags.some((t) => nodeTags.includes(t))) return false;
|
|
12355
|
+
}
|
|
12356
|
+
if (f.since && node.id > f.since) ;
|
|
12357
|
+
return true;
|
|
12358
|
+
}
|
|
12359
|
+
_deliver(node) {
|
|
12360
|
+
for (const sub of this.subs.values()) {
|
|
12361
|
+
if (this._matches(node, sub.filter)) {
|
|
12362
|
+
const pending = this.pendingDeliveries.get(sub.channel);
|
|
12363
|
+
if (pending) pending.push(node);
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
12367
|
+
async _persist(node) {
|
|
12368
|
+
await fsp6.mkdir(this.filePath, { recursive: true });
|
|
12369
|
+
const line = JSON.stringify(node) + "\n";
|
|
12370
|
+
await withFileLock(this.graphFilePath, async () => {
|
|
12371
|
+
await fsp6.appendFile(this.graphFilePath, line, "utf8");
|
|
12372
|
+
});
|
|
12373
|
+
}
|
|
12374
|
+
async _append(node) {
|
|
12375
|
+
await fsp6.mkdir(this.filePath, { recursive: true });
|
|
12376
|
+
const line = JSON.stringify({ op: "update", node }) + "\n";
|
|
12377
|
+
await withFileLock(this.graphFilePath, async () => {
|
|
12378
|
+
await fsp6.appendFile(this.graphFilePath, line, "utf8");
|
|
12379
|
+
});
|
|
12380
|
+
}
|
|
12381
|
+
/** Rebuild in-memory state from the log file. Call on startup. */
|
|
12382
|
+
async load() {
|
|
12383
|
+
try {
|
|
12384
|
+
const content = await fsp6.readFile(this.graphFilePath, "utf8");
|
|
12385
|
+
const lines = content.split("\n").filter(Boolean);
|
|
12386
|
+
for (const line of lines) {
|
|
12387
|
+
try {
|
|
12388
|
+
const parsed = JSON.parse(line);
|
|
12389
|
+
if (parsed.op === "update") {
|
|
12390
|
+
const oldNode = this.nodes.get(parsed.node.id);
|
|
12391
|
+
if (oldNode) {
|
|
12392
|
+
this._removeFromIndex(oldNode, this._indexKeys(oldNode));
|
|
12393
|
+
}
|
|
12394
|
+
this.nodes.set(parsed.node.id, parsed.node);
|
|
12395
|
+
this._addToIndex(parsed.node, this._indexKeys(parsed.node));
|
|
12396
|
+
} else {
|
|
12397
|
+
this.nodes.set(parsed.id, parsed);
|
|
12398
|
+
this._addToIndex(parsed, this._indexKeys(parsed));
|
|
12399
|
+
}
|
|
12400
|
+
} catch {
|
|
12401
|
+
}
|
|
12402
|
+
}
|
|
12403
|
+
} catch {
|
|
12404
|
+
}
|
|
12405
|
+
}
|
|
12406
|
+
/** Snapshot for serialization. */
|
|
12407
|
+
snapshot() {
|
|
12408
|
+
return {
|
|
12409
|
+
nodes: Array.from(this.nodes.values()),
|
|
12410
|
+
subs: this.subs.size
|
|
12411
|
+
};
|
|
12412
|
+
}
|
|
12413
|
+
};
|
|
12414
|
+
|
|
12415
|
+
// src/coordination/task-dag.ts
|
|
12416
|
+
var TaskDAG = class {
|
|
12417
|
+
nodes = /* @__PURE__ */ new Map();
|
|
12418
|
+
handlers = /* @__PURE__ */ new Set();
|
|
12419
|
+
runnablesHandlers = /* @__PURE__ */ new Set();
|
|
12420
|
+
runnableCache = null;
|
|
12421
|
+
// ── Node management ───────────────────────────────────────────────────
|
|
12422
|
+
/**
|
|
12423
|
+
* Add a task node. Dependencies are validated for cycles.
|
|
12424
|
+
* Throws if adding a dep would create a cycle.
|
|
12425
|
+
*/
|
|
12426
|
+
addNode(id, description, deps = [], opts = {}) {
|
|
12427
|
+
if (this.nodes.has(id)) return;
|
|
12428
|
+
for (const depId of deps) {
|
|
12429
|
+
if (!this.nodes.has(depId)) {
|
|
12430
|
+
throw new Error(`TaskDAG.addNode: unknown dependency "${depId}" for task "${id}". Add the dep first.`);
|
|
12431
|
+
}
|
|
12432
|
+
}
|
|
12433
|
+
if (this._wouldCycle(id, deps)) {
|
|
12434
|
+
throw new Error(`TaskDAG.addNode: adding deps [${deps.join(", ")}] to "${id}" would create a cycle.`);
|
|
12435
|
+
}
|
|
12436
|
+
const node = {
|
|
12437
|
+
id,
|
|
12438
|
+
description,
|
|
12439
|
+
deps: [...deps],
|
|
12440
|
+
status: deps.length === 0 ? "ready" : "pending",
|
|
12441
|
+
role: opts.role,
|
|
12442
|
+
priority: opts.priority ?? 5,
|
|
12443
|
+
dependents: [],
|
|
12444
|
+
tags: opts.tags ?? []
|
|
12445
|
+
};
|
|
12446
|
+
this.nodes.set(id, node);
|
|
12447
|
+
for (const depId of deps) {
|
|
12448
|
+
this.nodes.get(depId).dependents.push(id);
|
|
12449
|
+
}
|
|
12450
|
+
this.invalidateCache();
|
|
12451
|
+
this._emitReady();
|
|
12452
|
+
}
|
|
12453
|
+
/**
|
|
12454
|
+
* Remove a node and all edges to/from it.
|
|
12455
|
+
* Skips any dependents that would become dangling.
|
|
12456
|
+
*/
|
|
12457
|
+
removeNode(id) {
|
|
12458
|
+
const node = this.nodes.get(id);
|
|
12459
|
+
if (!node) return;
|
|
12460
|
+
for (const depId of node.deps) {
|
|
12461
|
+
const dep = this.nodes.get(depId);
|
|
12462
|
+
if (dep) {
|
|
12463
|
+
dep.dependents = dep.dependents.filter((d) => d !== id);
|
|
12464
|
+
}
|
|
12465
|
+
}
|
|
12466
|
+
for (const depId of node.dependents) {
|
|
12467
|
+
const dep = this.nodes.get(depId);
|
|
12468
|
+
if (dep && dep.deps.every((d) => !this.nodes.has(d) || this.nodes.get(d).status === "done")) {
|
|
12469
|
+
this._transition(depId, "pending", "ready");
|
|
12470
|
+
}
|
|
12471
|
+
}
|
|
12472
|
+
this.nodes.delete(id);
|
|
12473
|
+
this.invalidateCache();
|
|
12474
|
+
}
|
|
12475
|
+
// ── State transitions ──────────────────────────────────────────────────
|
|
12476
|
+
/**
|
|
12477
|
+
* Mark a task as running. Returns true if the transition was valid
|
|
12478
|
+
* (task was in 'ready' state), false otherwise.
|
|
12479
|
+
*/
|
|
12480
|
+
start(id, assignedTo) {
|
|
12481
|
+
const node = this.nodes.get(id);
|
|
12482
|
+
if (!node) return false;
|
|
12483
|
+
if (node.status !== "ready") return false;
|
|
12484
|
+
node.status = "running";
|
|
12485
|
+
node.assignedTo = assignedTo;
|
|
12486
|
+
node.spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12487
|
+
this.invalidateCache();
|
|
12488
|
+
this._emit({ type: "node:started", nodeId: id, assignedTo });
|
|
12489
|
+
return true;
|
|
12490
|
+
}
|
|
12491
|
+
/**
|
|
12492
|
+
* Mark a task as completed. Unblocks all dependents; they become 'ready'
|
|
12493
|
+
* if all their deps are done.
|
|
12494
|
+
*/
|
|
12495
|
+
complete(id, result) {
|
|
12496
|
+
const node = this.nodes.get(id);
|
|
12497
|
+
if (!node) return;
|
|
12498
|
+
node.status = "done";
|
|
12499
|
+
node.result = result;
|
|
12500
|
+
node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12501
|
+
this.invalidateCache();
|
|
12502
|
+
const blocked = [];
|
|
12503
|
+
for (const depId of node.dependents) {
|
|
12504
|
+
const dep = this.nodes.get(depId);
|
|
12505
|
+
if (!dep) continue;
|
|
12506
|
+
const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => this.nodes.get(d).status === "done");
|
|
12507
|
+
if (allDone) {
|
|
12508
|
+
this._transition(depId, "pending", "ready");
|
|
12509
|
+
} else {
|
|
12510
|
+
blocked.push(depId);
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
this._emit({ type: "node:completed", nodeId: id, result, blockers: blocked });
|
|
12514
|
+
if (blocked.length === 0) this._emitReady();
|
|
12515
|
+
}
|
|
12516
|
+
/**
|
|
12517
|
+
* Mark a task as failed. Unblocks dependents but they remain 'pending'
|
|
12518
|
+
* (they may still be runnable if other deps succeeded).
|
|
12519
|
+
*/
|
|
12520
|
+
fail(id, error) {
|
|
12521
|
+
const node = this.nodes.get(id);
|
|
12522
|
+
if (!node) return;
|
|
12523
|
+
node.status = "failed";
|
|
12524
|
+
node.error = error;
|
|
12525
|
+
node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12526
|
+
this.invalidateCache();
|
|
12527
|
+
const blocked = [];
|
|
12528
|
+
for (const depId of node.dependents) {
|
|
12529
|
+
const dep = this.nodes.get(depId);
|
|
12530
|
+
if (!dep) continue;
|
|
12531
|
+
const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => {
|
|
12532
|
+
const s = this.nodes.get(d).status;
|
|
12533
|
+
return s === "done" || s === "skipped";
|
|
12534
|
+
});
|
|
12535
|
+
if (allDone) {
|
|
12536
|
+
this._transition(depId, "pending", "ready");
|
|
12537
|
+
} else {
|
|
12538
|
+
blocked.push(depId);
|
|
12539
|
+
}
|
|
12540
|
+
}
|
|
12541
|
+
this._emit({ type: "node:failed", nodeId: id, error, blockers: blocked });
|
|
12542
|
+
if (blocked.length === 0) this._emitReady();
|
|
12543
|
+
}
|
|
12544
|
+
/**
|
|
12545
|
+
* Skip a task (e.g., it was deemed unnecessary by an earlier step).
|
|
12546
|
+
* Treats it as done for dependency purposes.
|
|
12547
|
+
*/
|
|
12548
|
+
skip(id, reason) {
|
|
12549
|
+
const node = this.nodes.get(id);
|
|
12550
|
+
if (!node) return;
|
|
12551
|
+
node.status = "skipped";
|
|
12552
|
+
node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12553
|
+
this.invalidateCache();
|
|
12554
|
+
for (const depId of node.dependents) {
|
|
12555
|
+
const dep = this.nodes.get(depId);
|
|
12556
|
+
if (!dep) continue;
|
|
12557
|
+
const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => {
|
|
12558
|
+
const s = this.nodes.get(d).status;
|
|
12559
|
+
return s === "done" || s === "skipped";
|
|
12560
|
+
});
|
|
12561
|
+
if (allDone) this._transition(depId, "pending", "ready");
|
|
12562
|
+
}
|
|
12563
|
+
this._emit({ type: "node:skipped", nodeId: id, reason });
|
|
12564
|
+
}
|
|
12565
|
+
// ── Queries ────────────────────────────────────────────────────────────
|
|
12566
|
+
getNode(id) {
|
|
12567
|
+
return this.nodes.get(id);
|
|
12568
|
+
}
|
|
12569
|
+
getAll() {
|
|
12570
|
+
return Array.from(this.nodes.values());
|
|
12571
|
+
}
|
|
12572
|
+
getReady() {
|
|
12573
|
+
if (this.runnableCache) return this.runnableCache;
|
|
12574
|
+
const runnable = Array.from(this.nodes.values()).filter((n) => n.status === "ready").sort((a, b) => a.priority - b.priority);
|
|
12575
|
+
this.runnableCache = runnable;
|
|
12576
|
+
return runnable;
|
|
12577
|
+
}
|
|
12578
|
+
getRunning() {
|
|
12579
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "running");
|
|
12580
|
+
}
|
|
12581
|
+
getPending() {
|
|
12582
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "pending");
|
|
12583
|
+
}
|
|
12584
|
+
getDone() {
|
|
12585
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "done");
|
|
12586
|
+
}
|
|
12587
|
+
getFailed() {
|
|
12588
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "failed");
|
|
12589
|
+
}
|
|
12590
|
+
getCompleted() {
|
|
12591
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "done" || n.status === "skipped");
|
|
12592
|
+
}
|
|
12593
|
+
isDone() {
|
|
12594
|
+
return Array.from(this.nodes.values()).every(
|
|
12595
|
+
(n) => n.status === "done" || n.status === "failed" || n.status === "skipped"
|
|
12596
|
+
);
|
|
12597
|
+
}
|
|
12598
|
+
isFailed() {
|
|
12599
|
+
return Array.from(this.nodes.values()).some((n) => n.status === "failed");
|
|
12600
|
+
}
|
|
12601
|
+
/** All tasks that are currently blocked (pending but not ready). */
|
|
12602
|
+
getBlocked() {
|
|
12603
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "pending");
|
|
12604
|
+
}
|
|
12605
|
+
/** Topological sort — tasks in dependency order. */
|
|
12606
|
+
getTopologicalOrder() {
|
|
12607
|
+
const visited = /* @__PURE__ */ new Set();
|
|
12608
|
+
const result = [];
|
|
12609
|
+
const visit = (id) => {
|
|
12610
|
+
if (visited.has(id)) return;
|
|
12611
|
+
visited.add(id);
|
|
12612
|
+
const node = this.nodes.get(id);
|
|
12613
|
+
if (!node) return;
|
|
12614
|
+
for (const depId of node.deps) visit(depId);
|
|
12615
|
+
result.push(node);
|
|
12616
|
+
};
|
|
12617
|
+
for (const id of this.nodes.keys()) visit(id);
|
|
12618
|
+
return result;
|
|
12619
|
+
}
|
|
12620
|
+
/** Check for deadlock: no runnable tasks but not done. */
|
|
12621
|
+
hasDeadlock() {
|
|
12622
|
+
if (this.isDone()) return false;
|
|
12623
|
+
return this.getReady().length === 0 && this.getRunning().length === 0;
|
|
12624
|
+
}
|
|
12625
|
+
/** Stats snapshot for reporting. */
|
|
12626
|
+
stats() {
|
|
12627
|
+
const all = Array.from(this.nodes.values());
|
|
12628
|
+
const done = all.filter((n) => n.status === "done" || n.status === "skipped").length;
|
|
12629
|
+
return {
|
|
12630
|
+
total: all.length,
|
|
12631
|
+
pending: all.filter((n) => n.status === "pending").length,
|
|
12632
|
+
ready: all.filter((n) => n.status === "ready").length,
|
|
12633
|
+
running: all.filter((n) => n.status === "running").length,
|
|
12634
|
+
done: all.filter((n) => n.status === "done").length,
|
|
12635
|
+
failed: all.filter((n) => n.status === "failed").length,
|
|
12636
|
+
skipped: all.filter((n) => n.status === "skipped").length,
|
|
12637
|
+
progress: all.length ? done / all.length : 0
|
|
12638
|
+
};
|
|
12639
|
+
}
|
|
12640
|
+
// ── Events ────────────────────────────────────────────────────────────
|
|
12641
|
+
onEvent(handler) {
|
|
12642
|
+
this.handlers.add(handler);
|
|
12643
|
+
return () => void this.handlers.delete(handler);
|
|
12644
|
+
}
|
|
12645
|
+
onRunnable(handler) {
|
|
12646
|
+
this.runnablesHandlers.add(handler);
|
|
12647
|
+
return () => void this.runnablesHandlers.delete(handler);
|
|
12648
|
+
}
|
|
12649
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
12650
|
+
_transition(id, from, to) {
|
|
12651
|
+
const node = this.nodes.get(id);
|
|
12652
|
+
if (!node || node.status !== from) return;
|
|
12653
|
+
node.status = to;
|
|
12654
|
+
this.invalidateCache();
|
|
12655
|
+
if (to === "ready") {
|
|
12656
|
+
this._emit({ type: "node:ready", nodeId: id, deps: node.deps });
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
12659
|
+
_emit(event) {
|
|
12660
|
+
for (const h of this.handlers) {
|
|
12661
|
+
try {
|
|
12662
|
+
h(event);
|
|
12663
|
+
} catch {
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
}
|
|
12667
|
+
_emitReady() {
|
|
12668
|
+
const runnable = this.getReady();
|
|
12669
|
+
if (this.hasDeadlock()) {
|
|
12670
|
+
this._emit({ type: "deadlock", blocked: this.getBlocked().map((n) => n.id) });
|
|
12671
|
+
} else {
|
|
12672
|
+
this._emit({ type: "graph:done", allDone: this.isDone() });
|
|
12673
|
+
}
|
|
12674
|
+
if (runnable.length > 0) {
|
|
12675
|
+
for (const h of this.runnablesHandlers) {
|
|
12676
|
+
try {
|
|
12677
|
+
h(runnable);
|
|
12678
|
+
} catch {
|
|
12679
|
+
}
|
|
12680
|
+
}
|
|
12681
|
+
}
|
|
12682
|
+
}
|
|
12683
|
+
invalidateCache() {
|
|
12684
|
+
this.runnableCache = null;
|
|
12685
|
+
}
|
|
12686
|
+
/**
|
|
12687
|
+
* DFS cycle detection. Adding edge (id → dep) creates a cycle if
|
|
12688
|
+
* there already exists a path from dep to id.
|
|
12689
|
+
*/
|
|
12690
|
+
_wouldCycle(id, newDeps) {
|
|
12691
|
+
const visited = /* @__PURE__ */ new Set();
|
|
12692
|
+
const stack = [...newDeps];
|
|
12693
|
+
while (stack.length > 0) {
|
|
12694
|
+
const current = stack.pop();
|
|
12695
|
+
if (current === id) return true;
|
|
12696
|
+
if (visited.has(current)) continue;
|
|
12697
|
+
visited.add(current);
|
|
12698
|
+
const node = this.nodes.get(current);
|
|
12699
|
+
if (node) stack.push(...node.deps);
|
|
12700
|
+
}
|
|
12701
|
+
return false;
|
|
12702
|
+
}
|
|
12703
|
+
};
|
|
12704
|
+
|
|
12705
|
+
// src/coordination/consensus-protocol.ts
|
|
12706
|
+
var ConsensusProtocol = class {
|
|
12707
|
+
graph;
|
|
12708
|
+
fleet;
|
|
12709
|
+
rules;
|
|
12710
|
+
voters;
|
|
12711
|
+
// agentId → config
|
|
12712
|
+
constructor(opts) {
|
|
12713
|
+
this.graph = opts.graph;
|
|
12714
|
+
this.fleet = opts.fleet ?? void 0;
|
|
12715
|
+
this.voters = new Map(opts.voters.map((v) => [v.agentId, v]));
|
|
12716
|
+
this.rules = {
|
|
12717
|
+
quorumFraction: opts.rules?.quorumFraction ?? 0.5,
|
|
12718
|
+
approvalFraction: opts.rules?.approvalFraction ?? 0.6,
|
|
12719
|
+
vetoRoles: opts.rules?.vetoRoles ?? [],
|
|
12720
|
+
approvalWeightFraction: opts.rules?.approvalWeightFraction
|
|
12721
|
+
};
|
|
12722
|
+
}
|
|
12723
|
+
// ── Vote lifecycle ────────────────────────────────────────────────────
|
|
12724
|
+
/**
|
|
12725
|
+
* Initiate a vote on a proposed change. Updates the change node's status
|
|
12726
|
+
* to 'proposed' and notifies eligible voters via FleetBus.
|
|
12727
|
+
*/
|
|
12728
|
+
initiateVote(changeId) {
|
|
12729
|
+
const change = this.graph.get(changeId);
|
|
12730
|
+
if (!change || change.type !== "change") {
|
|
12731
|
+
throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
|
|
12732
|
+
}
|
|
12733
|
+
this.graph.update(changeId, { status: "proposed", votes: [] });
|
|
12734
|
+
const eligible = this._eligibleVoters(change);
|
|
12735
|
+
this._notifyVoters(change, eligible, "vote_initiated");
|
|
12736
|
+
}
|
|
12737
|
+
/**
|
|
12738
|
+
* Cast a vote. Updates the change node in the graph and re-evaluates
|
|
12739
|
+
* consensus. If the vote triggers a resolution, updates the change status.
|
|
12740
|
+
*/
|
|
12741
|
+
castVote(changeId, voterId, value, rationale) {
|
|
12742
|
+
const change = this.graph.get(changeId);
|
|
12743
|
+
if (!change || change.type !== "change") {
|
|
12744
|
+
throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
|
|
12745
|
+
}
|
|
12746
|
+
const voter = this.voters.get(voterId);
|
|
12747
|
+
if (!voter) {
|
|
12748
|
+
throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
|
|
12749
|
+
}
|
|
12750
|
+
const eligible = this._eligibleVoters(change);
|
|
12751
|
+
if (!eligible.includes(voterId)) {
|
|
12752
|
+
throw new Error(`ConsensusProtocol: voter "${voterId}" is not eligible for this vote`);
|
|
12753
|
+
}
|
|
12754
|
+
const vote = {
|
|
12755
|
+
agentId: voterId,
|
|
12756
|
+
agentName: voter.agentName,
|
|
12757
|
+
value,
|
|
12758
|
+
rationale,
|
|
12759
|
+
votedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12760
|
+
};
|
|
12761
|
+
const existingIdx = change.votes.findIndex((v) => v.agentId === voterId);
|
|
12762
|
+
const newVotes = existingIdx >= 0 ? change.votes.with(existingIdx, vote) : [...change.votes, vote];
|
|
12763
|
+
const result = this._resolve(changeId, newVotes, eligible);
|
|
12764
|
+
this.graph.update(changeId, {
|
|
12765
|
+
votes: newVotes,
|
|
12766
|
+
...result.outcome !== "pending" ? { status: this._toChangeStatus(result.outcome) } : {}
|
|
12767
|
+
});
|
|
12768
|
+
this._notifyVoters(change, eligible, "vote_cast", { voterId, value, result });
|
|
12769
|
+
return result;
|
|
12770
|
+
}
|
|
12771
|
+
/**
|
|
12772
|
+
* Resolve the current vote without waiting for all eligible voters.
|
|
12773
|
+
* Useful when a timeout fires or an agent decides to finalize early.
|
|
12774
|
+
*/
|
|
12775
|
+
resolveNow(changeId) {
|
|
12776
|
+
const change = this.graph.get(changeId);
|
|
12777
|
+
if (!change) throw new Error(`ConsensusProtocol: unknown change "${changeId}"`);
|
|
12778
|
+
const eligible = this._eligibleVoters(change);
|
|
12779
|
+
const result = this._resolve(changeId, change.votes, eligible);
|
|
12780
|
+
if (result.outcome !== "pending") {
|
|
12781
|
+
this.graph.update(changeId, { status: this._toChangeStatus(result.outcome) });
|
|
12782
|
+
this._notifyVoters(change, eligible, "vote_resolved", { result });
|
|
12783
|
+
}
|
|
12784
|
+
return result;
|
|
12785
|
+
}
|
|
12786
|
+
/**
|
|
12787
|
+
* Register or update a voter's configuration.
|
|
12788
|
+
*/
|
|
12789
|
+
registerVoter(config) {
|
|
12790
|
+
this.voters.set(config.agentId, config);
|
|
12791
|
+
}
|
|
12792
|
+
/**
|
|
12793
|
+
* Get the current vote status for a change.
|
|
12794
|
+
*/
|
|
12795
|
+
getStatus(changeId) {
|
|
12796
|
+
const change = this.graph.get(changeId);
|
|
12797
|
+
if (!change || change.type !== "change") return null;
|
|
12798
|
+
const eligible = this._eligibleVoters(change);
|
|
12799
|
+
return this._resolve(changeId, change.votes, eligible);
|
|
12800
|
+
}
|
|
12801
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
12802
|
+
_eligibleVoters(change) {
|
|
12803
|
+
return Array.from(this.voters.keys()).filter(
|
|
12804
|
+
(agentId) => agentId !== change.proposedBy
|
|
12805
|
+
);
|
|
12806
|
+
}
|
|
12807
|
+
_resolve(changeId, votes, eligible) {
|
|
12808
|
+
const totalEligible = eligible.length;
|
|
12809
|
+
const approve = votes.filter((v) => v.value === "approve");
|
|
12810
|
+
const reject = votes.filter((v) => v.value === "reject");
|
|
12811
|
+
const abstain = votes.filter((v) => v.value === "abstain");
|
|
12812
|
+
const totalWeightApprove = approve.reduce(
|
|
12813
|
+
(sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
|
|
12814
|
+
0
|
|
12815
|
+
);
|
|
12816
|
+
const totalWeightReject = reject.reduce(
|
|
12817
|
+
(sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
|
|
12818
|
+
0
|
|
12819
|
+
);
|
|
12820
|
+
const totalWeight = Array.from(this.voters.values()).reduce(
|
|
12821
|
+
(sum, v) => sum + v.weight,
|
|
12822
|
+
0
|
|
12823
|
+
);
|
|
12824
|
+
const castCount = votes.length;
|
|
12825
|
+
const quorumRequired = Math.ceil(totalEligible * this.rules.quorumFraction);
|
|
12826
|
+
const quorumMet = castCount >= quorumRequired;
|
|
12827
|
+
for (const v of reject) {
|
|
12828
|
+
const config = this.voters.get(v.agentId);
|
|
12829
|
+
if (config?.veto && this.rules.vetoRoles.includes(config.role)) {
|
|
12830
|
+
return {
|
|
12831
|
+
changeId,
|
|
12832
|
+
outcome: "vetoed",
|
|
12833
|
+
votes,
|
|
12834
|
+
approveCount: approve.length,
|
|
12835
|
+
rejectCount: reject.length,
|
|
12836
|
+
abstainCount: abstain.length,
|
|
12837
|
+
totalWeightApprove,
|
|
12838
|
+
totalWeightReject,
|
|
12839
|
+
eligibleVoters: eligible,
|
|
12840
|
+
quorumMet,
|
|
12841
|
+
approvalMet: false,
|
|
12842
|
+
vetoedBy: config.agentId,
|
|
12843
|
+
rationale: `Hard veto from role "${config.role}" (${config.agentName}).`
|
|
12844
|
+
};
|
|
12845
|
+
}
|
|
12846
|
+
}
|
|
12847
|
+
if (!quorumMet) {
|
|
12848
|
+
return {
|
|
12849
|
+
changeId,
|
|
12850
|
+
outcome: "quorum_not_met",
|
|
12851
|
+
votes,
|
|
12852
|
+
approveCount: approve.length,
|
|
12853
|
+
rejectCount: reject.length,
|
|
12854
|
+
abstainCount: abstain.length,
|
|
12855
|
+
totalWeightApprove,
|
|
12856
|
+
totalWeightReject,
|
|
12857
|
+
eligibleVoters: eligible,
|
|
12858
|
+
quorumMet: false,
|
|
12859
|
+
approvalMet: false,
|
|
12860
|
+
rationale: `Quorum not met: ${castCount}/${quorumRequired} required.`
|
|
12861
|
+
};
|
|
12862
|
+
}
|
|
12863
|
+
const approvalRequired = Math.ceil(castCount * this.rules.approvalFraction);
|
|
12864
|
+
const approvalMet = approve.length >= approvalRequired;
|
|
12865
|
+
if (this.rules.approvalWeightFraction !== void 0 && approvalMet) {
|
|
12866
|
+
const weightRequired = totalWeight * this.rules.approvalWeightFraction;
|
|
12867
|
+
if (totalWeightApprove < weightRequired) {
|
|
12868
|
+
return {
|
|
12869
|
+
changeId,
|
|
12870
|
+
outcome: "rejected",
|
|
12871
|
+
votes,
|
|
12872
|
+
approveCount: approve.length,
|
|
12873
|
+
rejectCount: reject.length,
|
|
12874
|
+
abstainCount: abstain.length,
|
|
12875
|
+
totalWeightApprove,
|
|
12876
|
+
totalWeightReject,
|
|
12877
|
+
eligibleVoters: eligible,
|
|
12878
|
+
quorumMet: true,
|
|
12879
|
+
approvalMet: false,
|
|
12880
|
+
rationale: `Weight threshold not met: ${totalWeightApprove.toFixed(2)}/${weightRequired.toFixed(2)} required.`
|
|
12881
|
+
};
|
|
12882
|
+
}
|
|
12883
|
+
}
|
|
12884
|
+
if (approvalMet) {
|
|
12885
|
+
return {
|
|
12886
|
+
changeId,
|
|
12887
|
+
outcome: "approved",
|
|
12888
|
+
votes,
|
|
12889
|
+
approveCount: approve.length,
|
|
12890
|
+
rejectCount: reject.length,
|
|
12891
|
+
abstainCount: abstain.length,
|
|
12892
|
+
totalWeightApprove,
|
|
12893
|
+
totalWeightReject,
|
|
12894
|
+
eligibleVoters: eligible,
|
|
12895
|
+
quorumMet: true,
|
|
12896
|
+
approvalMet: true,
|
|
12897
|
+
rationale: `Approved: ${approve.length}/${castCount} votes (threshold: ${approvalRequired}).`
|
|
12898
|
+
};
|
|
12899
|
+
}
|
|
12900
|
+
return {
|
|
12901
|
+
changeId,
|
|
12902
|
+
outcome: "rejected",
|
|
12903
|
+
votes,
|
|
12904
|
+
approveCount: approve.length,
|
|
12905
|
+
rejectCount: reject.length,
|
|
12906
|
+
abstainCount: abstain.length,
|
|
12907
|
+
totalWeightApprove,
|
|
12908
|
+
totalWeightReject,
|
|
12909
|
+
eligibleVoters: eligible,
|
|
12910
|
+
quorumMet: true,
|
|
12911
|
+
approvalMet: false,
|
|
12912
|
+
rationale: `Rejected: ${approve.length}/${castCount} approve votes (threshold: ${approvalRequired}).`
|
|
12913
|
+
};
|
|
12914
|
+
}
|
|
12915
|
+
_toChangeStatus(outcome) {
|
|
12916
|
+
switch (outcome) {
|
|
12917
|
+
case "approved":
|
|
12918
|
+
return "approved";
|
|
12919
|
+
case "rejected":
|
|
12920
|
+
return "rejected";
|
|
12921
|
+
case "vetoed":
|
|
12922
|
+
return "rejected";
|
|
12923
|
+
case "quorum_not_met":
|
|
12924
|
+
return "proposed";
|
|
12925
|
+
default:
|
|
12926
|
+
return "proposed";
|
|
12927
|
+
}
|
|
12928
|
+
}
|
|
12929
|
+
_notifyVoters(change, eligible, event, extra) {
|
|
12930
|
+
if (!this.fleet) return;
|
|
12931
|
+
this.fleet.emit({
|
|
12932
|
+
subagentId: "consensus",
|
|
12933
|
+
ts: Date.now(),
|
|
12934
|
+
type: `consensus:${event}`,
|
|
12935
|
+
payload: { changeId: change.id, changeTitle: change.title, eligible, ...extra }
|
|
12936
|
+
});
|
|
12937
|
+
}
|
|
12938
|
+
};
|
|
12939
|
+
|
|
12940
|
+
// src/coordination/change-manager.ts
|
|
12941
|
+
var DEFAULT_QUALITY_CHECKS = {
|
|
12942
|
+
runTests: true,
|
|
12943
|
+
runTypecheck: true,
|
|
12944
|
+
runLint: false,
|
|
12945
|
+
// warning only
|
|
12946
|
+
runSecurityScan: true,
|
|
12947
|
+
checkTestCoverage: false,
|
|
12948
|
+
minCoveragePercent: 70
|
|
12949
|
+
};
|
|
12950
|
+
var ChangeManager = class {
|
|
12951
|
+
graph;
|
|
12952
|
+
consensus;
|
|
12953
|
+
fleet;
|
|
12954
|
+
checks;
|
|
12955
|
+
/** Track applied changes for rollback lookup. */
|
|
12956
|
+
appliedChanges = /* @__PURE__ */ new Map();
|
|
12957
|
+
// changeId → rollbackId
|
|
12958
|
+
constructor(opts) {
|
|
12959
|
+
this.graph = opts.graph;
|
|
12960
|
+
this.consensus = opts.consensus;
|
|
12961
|
+
this.fleet = opts.fleet ?? void 0;
|
|
12962
|
+
this.checks = { ...DEFAULT_QUALITY_CHECKS, ...opts.checks };
|
|
12963
|
+
}
|
|
12964
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
12965
|
+
/**
|
|
12966
|
+
* Propose a new code change. Creates a ChangeNode in the knowledge graph.
|
|
12967
|
+
* Does NOT automatically initiate voting — call `submitForReview()` for that.
|
|
12968
|
+
*/
|
|
12969
|
+
async propose(input) {
|
|
12970
|
+
const node = await this.graph.add({
|
|
12971
|
+
type: "change",
|
|
12972
|
+
title: input.title,
|
|
12973
|
+
description: input.description,
|
|
12974
|
+
files: input.files.map((f) => ({
|
|
12975
|
+
path: f.path,
|
|
12976
|
+
action: f.action
|
|
12977
|
+
})),
|
|
12978
|
+
status: "proposed",
|
|
12979
|
+
proposedBy: input.proposedBy,
|
|
12980
|
+
proposedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12981
|
+
approvedBy: [],
|
|
12982
|
+
rejectedBy: [],
|
|
12983
|
+
votes: [],
|
|
12984
|
+
qualityGate: { passed: false, checks: [] },
|
|
12985
|
+
// filled after quality gate
|
|
12986
|
+
satisfiesGoals: input.satisfiesGoals
|
|
12987
|
+
});
|
|
12988
|
+
void this._runQualityGate(node.id, input.files).then((gate) => {
|
|
12989
|
+
void this.graph.update(node.id, { qualityGate: gate });
|
|
12990
|
+
});
|
|
12991
|
+
this._emit("change:proposed", { changeId: node.id, title: node.title });
|
|
12992
|
+
return node;
|
|
12993
|
+
}
|
|
12994
|
+
/**
|
|
12995
|
+
* Submit an approved change for application.
|
|
12996
|
+
* Returns the change node — actual file mutations are performed by agents
|
|
12997
|
+
* acting on this node's data from the knowledge graph.
|
|
12998
|
+
*/
|
|
12999
|
+
async submitForReview(changeId) {
|
|
13000
|
+
const change = this.graph.get(changeId);
|
|
13001
|
+
if (!change || change.type !== "change") {
|
|
13002
|
+
throw new Error(`ChangeManager: no change found "${changeId}"`);
|
|
13003
|
+
}
|
|
13004
|
+
if (change.status !== "proposed") {
|
|
13005
|
+
throw new Error(`ChangeManager: change "${changeId}" is not in 'proposed' state`);
|
|
13006
|
+
}
|
|
13007
|
+
this.consensus.initiateVote(changeId);
|
|
13008
|
+
this._emit("change:submitted_for_review", { changeId, title: change.title });
|
|
13009
|
+
}
|
|
13010
|
+
/**
|
|
13011
|
+
* Apply an approved change. Updates the change node to 'applied'.
|
|
13012
|
+
* Agents should watch for 'applied' status and perform the actual file mutations.
|
|
13013
|
+
*/
|
|
13014
|
+
async markApplied(changeId, appliedAt) {
|
|
13015
|
+
const change = this.graph.get(changeId);
|
|
13016
|
+
if (!change) return null;
|
|
13017
|
+
const updated = await this.graph.update(changeId, {
|
|
13018
|
+
status: "applied",
|
|
13019
|
+
appliedAt
|
|
13020
|
+
});
|
|
13021
|
+
if (updated) {
|
|
13022
|
+
this.appliedChanges.set(changeId, "");
|
|
13023
|
+
this._emit("change:applied", { changeId, title: updated.title, files: updated.files });
|
|
13024
|
+
}
|
|
13025
|
+
return updated;
|
|
13026
|
+
}
|
|
13027
|
+
/**
|
|
13028
|
+
* Mark a change as applied and trigger rollback for any satisfied goal
|
|
13029
|
+
* that turns out to be broken.
|
|
13030
|
+
*/
|
|
13031
|
+
async markAppliedWithVerification(changeId, verify) {
|
|
13032
|
+
const change = this.graph.get(changeId);
|
|
13033
|
+
if (!change) throw new Error(`ChangeManager: unknown change "${changeId}"`);
|
|
13034
|
+
const appliedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13035
|
+
await this.markApplied(changeId, appliedAt);
|
|
13036
|
+
const verificationResult = await verify();
|
|
13037
|
+
if (!verificationResult.passed) {
|
|
13038
|
+
const rollbackResult = await this.proposeRollback(changeId, "Quality gate failed after apply");
|
|
13039
|
+
return {
|
|
13040
|
+
changeId,
|
|
13041
|
+
success: false,
|
|
13042
|
+
appliedAt,
|
|
13043
|
+
filesTouched: change.files.map((f) => f.path),
|
|
13044
|
+
verificationResult,
|
|
13045
|
+
rollbackChangeId: rollbackResult?.id,
|
|
13046
|
+
error: `Quality gate failed: ${verificationResult.checks.filter((c) => !c.passed).map((c) => c.name).join(", ")}`
|
|
13047
|
+
};
|
|
13048
|
+
}
|
|
13049
|
+
return {
|
|
13050
|
+
changeId,
|
|
13051
|
+
success: true,
|
|
13052
|
+
appliedAt,
|
|
13053
|
+
filesTouched: change.files.map((f) => f.path),
|
|
13054
|
+
verificationResult
|
|
13055
|
+
};
|
|
13056
|
+
}
|
|
13057
|
+
/**
|
|
13058
|
+
* Propose a rollback for an applied change. Creates a new change that
|
|
13059
|
+
* reverses the original. Goes through full consensus.
|
|
13060
|
+
*/
|
|
13061
|
+
async proposeRollback(appliedChangeId, reason) {
|
|
13062
|
+
const original = this.graph.get(appliedChangeId);
|
|
13063
|
+
if (!original || original.type !== "change") return null;
|
|
13064
|
+
const rollbackFiles = original.files.map((f) => ({
|
|
13065
|
+
path: f.path,
|
|
13066
|
+
action: f.action === "create" ? "delete" : f.action === "delete" ? "create" : "modify"
|
|
13067
|
+
}));
|
|
13068
|
+
const rollback = await this.propose({
|
|
13069
|
+
title: `Rollback: ${original.title}`,
|
|
13070
|
+
description: `Rollback of "${original.title}" applied at ${original.appliedAt}. Reason: ${reason}`,
|
|
13071
|
+
files: rollbackFiles,
|
|
13072
|
+
proposedBy: "change-manager",
|
|
13073
|
+
satisfiesGoals: [],
|
|
13074
|
+
tags: ["rollback", `original:${appliedChangeId}`]
|
|
13075
|
+
});
|
|
13076
|
+
this.appliedChanges.set(appliedChangeId, rollback.id);
|
|
13077
|
+
await this.graph.update(appliedChangeId, {
|
|
13078
|
+
rolledBackAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13079
|
+
rollbackReason: reason
|
|
13080
|
+
});
|
|
13081
|
+
this._emit("change:rollback_proposed", {
|
|
13082
|
+
originalChangeId: appliedChangeId,
|
|
13083
|
+
rollbackChangeId: rollback.id,
|
|
13084
|
+
reason
|
|
13085
|
+
});
|
|
13086
|
+
return rollback;
|
|
13087
|
+
}
|
|
13088
|
+
/**
|
|
13089
|
+
* Mark a change as rolled back.
|
|
13090
|
+
*/
|
|
13091
|
+
async markRolledBack(changeId, rolledBackAt) {
|
|
13092
|
+
const updated = await this.graph.update(changeId, {
|
|
13093
|
+
status: "rolled_back",
|
|
13094
|
+
rolledBackAt
|
|
13095
|
+
});
|
|
13096
|
+
if (updated) {
|
|
13097
|
+
this._emit("change:rolled_back", { changeId, title: updated.title });
|
|
13098
|
+
}
|
|
13099
|
+
return updated;
|
|
13100
|
+
}
|
|
13101
|
+
// ── Queries ───────────────────────────────────────────────────────────
|
|
13102
|
+
getPendingReviews() {
|
|
13103
|
+
return this.graph.getChanges({ status: "proposed" });
|
|
13104
|
+
}
|
|
13105
|
+
getAppliedChanges() {
|
|
13106
|
+
return this.graph.getChanges({ status: "applied" });
|
|
13107
|
+
}
|
|
13108
|
+
getChange(id) {
|
|
13109
|
+
return this.graph.get(id);
|
|
13110
|
+
}
|
|
13111
|
+
getChangesForGoal(goalId) {
|
|
13112
|
+
return this.graph.getChanges({}).filter(
|
|
13113
|
+
(c) => c.satisfiesGoals.includes(goalId)
|
|
13114
|
+
);
|
|
13115
|
+
}
|
|
13116
|
+
// ── Quality gate ──────────────────────────────────────────────────────
|
|
13117
|
+
/**
|
|
13118
|
+
* Run quality gate checks. This is informational — actual test/lint/typecheck
|
|
13119
|
+
* execution is done by agents spawned for this purpose. This method stores
|
|
13120
|
+
* the result in the change node.
|
|
13121
|
+
*/
|
|
13122
|
+
async _runQualityGate(_changeId, _files) {
|
|
13123
|
+
const checks = [];
|
|
13124
|
+
if (this.checks.runTests) {
|
|
13125
|
+
checks.push({ name: "tests", passed: false, detail: "Tests must be run by a verify agent" });
|
|
13126
|
+
}
|
|
13127
|
+
if (this.checks.runTypecheck) {
|
|
13128
|
+
checks.push({ name: "typecheck", passed: false, detail: "TypeScript must compile" });
|
|
13129
|
+
}
|
|
13130
|
+
if (this.checks.runSecurityScan) {
|
|
13131
|
+
checks.push({ name: "security", passed: false, detail: "Security scan must pass" });
|
|
13132
|
+
}
|
|
13133
|
+
if (this.checks.runLint) {
|
|
13134
|
+
checks.push({ name: "lint", passed: false, detail: "Lint check" });
|
|
13135
|
+
}
|
|
13136
|
+
const result = {
|
|
13137
|
+
passed: checks.length === 0,
|
|
13138
|
+
checks
|
|
13139
|
+
};
|
|
13140
|
+
await this.graph.update(_changeId, { qualityGate: result });
|
|
13141
|
+
return result;
|
|
13142
|
+
}
|
|
13143
|
+
/**
|
|
13144
|
+
* Update quality gate result for a change. Called by verify agents
|
|
13145
|
+
* after running their checks.
|
|
13146
|
+
*/
|
|
13147
|
+
async updateQualityGate(changeId, checkName, result) {
|
|
13148
|
+
const change = this.graph.get(changeId);
|
|
13149
|
+
if (!change) return;
|
|
13150
|
+
const checks = change.qualityGate.checks.map(
|
|
13151
|
+
(c) => c.name === checkName ? { ...c, ...result } : c
|
|
13152
|
+
);
|
|
13153
|
+
const allPassed = checks.every((c) => c.passed);
|
|
13154
|
+
await this.graph.update(changeId, {
|
|
13155
|
+
qualityGate: { passed: allPassed, checks }
|
|
13156
|
+
});
|
|
13157
|
+
this._emit("quality_gate:updated", { changeId, checkName, passed: result.passed });
|
|
13158
|
+
}
|
|
13159
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
13160
|
+
_emit(type, payload) {
|
|
13161
|
+
if (!this.fleet) return;
|
|
13162
|
+
this.fleet.emit({ subagentId: "change-manager", ts: Date.now(), type, payload });
|
|
13163
|
+
}
|
|
13164
|
+
};
|
|
13165
|
+
var AutonomousBrain = class {
|
|
13166
|
+
graph;
|
|
13167
|
+
// Fleet bus for emitting decisions — null-safe, no-op if not provided
|
|
13168
|
+
fleetBus;
|
|
13169
|
+
llmProvider;
|
|
13170
|
+
maxRetries;
|
|
13171
|
+
consensusRiskThreshold;
|
|
13172
|
+
selfImprove;
|
|
13173
|
+
/** Decision history for self-improvement and audit. */
|
|
13174
|
+
decisionHistory = [];
|
|
13175
|
+
/** Tracks failure patterns for self-improvement. */
|
|
13176
|
+
failurePatterns = /* @__PURE__ */ new Map();
|
|
13177
|
+
RISK_ORDER = ["low", "medium", "high", "critical"];
|
|
13178
|
+
// ── Fleet bus integration ─────────────────────────────────────────────
|
|
13179
|
+
_emit(type, payload) {
|
|
13180
|
+
if (!this.fleetBus) return;
|
|
13181
|
+
this.fleetBus.emit({ subagentId: "brain", ts: Date.now(), type, payload });
|
|
13182
|
+
}
|
|
13183
|
+
constructor(opts) {
|
|
13184
|
+
this.graph = opts.graph;
|
|
13185
|
+
this.fleetBus = opts.fleet ?? void 0;
|
|
13186
|
+
this.llmProvider = opts.llmProvider;
|
|
13187
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
13188
|
+
this.consensusRiskThreshold = opts.consensusRiskThreshold ?? "high";
|
|
13189
|
+
this.selfImprove = opts.selfImprove ?? true;
|
|
13190
|
+
}
|
|
13191
|
+
// ── BrainArbiter interface ────────────────────────────────────────────
|
|
13192
|
+
/** Implements BrainArbiter — bridges standard brain.ts interface to autonomous engine. */
|
|
13193
|
+
async decide(request) {
|
|
13194
|
+
return this.decideAuto(this._toAutonomous(request));
|
|
13195
|
+
}
|
|
13196
|
+
// ── Main entry point ──────────────────────────────────────────────────
|
|
13197
|
+
/**
|
|
13198
|
+
* Primary autonomous decision engine — receives AutonomousDecisionRequest,
|
|
13199
|
+
* queries the LLM, records the decision, and returns a BrainDecision.
|
|
13200
|
+
*
|
|
13201
|
+
* Specialized methods (decideSpawn, decideApproval, etc.) should call this
|
|
13202
|
+
* directly with a pre-built AutonomousDecisionRequest.
|
|
13203
|
+
*/
|
|
13204
|
+
async decideAuto(request) {
|
|
13205
|
+
const { id, decisionType, question, context, options, risk, requiresConsensus } = request;
|
|
13206
|
+
const history = this.selfImprove ? this._loadHistory(decisionType, risk) : [];
|
|
13207
|
+
const hints = this.selfImprove ? this._getSelfImproveHints(decisionType) : [];
|
|
13208
|
+
const prompt = {
|
|
13209
|
+
decisionType,
|
|
13210
|
+
question,
|
|
13211
|
+
context: this._serializeContext(context),
|
|
13212
|
+
options,
|
|
13213
|
+
risk,
|
|
13214
|
+
decisionHistory: history,
|
|
13215
|
+
selfImproveHints: hints
|
|
13216
|
+
};
|
|
13217
|
+
let result;
|
|
13218
|
+
try {
|
|
13219
|
+
result = await this.llmProvider.decide(prompt);
|
|
13220
|
+
} catch (err) {
|
|
13221
|
+
const recommended = options.find((o) => o.recommended);
|
|
13222
|
+
if (recommended && risk === "low") {
|
|
13223
|
+
return { type: "answer", optionId: recommended.id, text: recommended.label };
|
|
13224
|
+
}
|
|
13225
|
+
return { type: "deny", reason: `Brain LLM failed: ${String(err)}` };
|
|
13226
|
+
}
|
|
13227
|
+
this._recordDecision({
|
|
13228
|
+
id,
|
|
13229
|
+
decisionType,
|
|
13230
|
+
question,
|
|
13231
|
+
options,
|
|
13232
|
+
chosen: result.optionId,
|
|
13233
|
+
rationale: result.rationale,
|
|
13234
|
+
madeBy: "autonomous-brain",
|
|
13235
|
+
context: JSON.stringify(context)
|
|
13236
|
+
}).catch(() => {
|
|
13237
|
+
});
|
|
13238
|
+
if (requiresConsensus) {
|
|
13239
|
+
this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: true });
|
|
13240
|
+
return {
|
|
13241
|
+
type: "answer",
|
|
13242
|
+
optionId: result.optionId,
|
|
13243
|
+
text: options.find((o) => o.id === result.optionId)?.label ?? result.optionId,
|
|
13244
|
+
rationale: `${result.rationale}
|
|
13245
|
+
|
|
13246
|
+
\u26A0\uFE0F This decision requires consensus approval before execution.`
|
|
13247
|
+
};
|
|
13248
|
+
}
|
|
13249
|
+
this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: false });
|
|
13250
|
+
return {
|
|
13251
|
+
type: "answer",
|
|
13252
|
+
optionId: result.optionId,
|
|
13253
|
+
text: options.find((o) => o.id === result.optionId)?.label ?? result.optionId,
|
|
13254
|
+
rationale: result.rationale
|
|
13255
|
+
};
|
|
13256
|
+
}
|
|
13257
|
+
// ── Specialized decision methods ────────────────────────────────────
|
|
13258
|
+
/**
|
|
13259
|
+
* Decide whether to spawn a subagent, which role to use, and what budget.
|
|
13260
|
+
*/
|
|
13261
|
+
async decideSpawn(source, taskDescription, availableFacts, fleetStatus) {
|
|
13262
|
+
const roleHints = this._inferRoles(taskDescription);
|
|
13263
|
+
const risk = roleHints.length > 1 ? "medium" : "low";
|
|
13264
|
+
const options = roleHints.map((role, i) => ({
|
|
13265
|
+
id: `spawn:${role}`,
|
|
13266
|
+
label: `Spawn ${role} agent`,
|
|
13267
|
+
risk: i === 0 ? "low" : "medium",
|
|
13268
|
+
recommended: i === 0,
|
|
13269
|
+
consequence: i === 0 ? `Spawn the most appropriate agent for: ${taskDescription.slice(0, 80)}` : `Spawn an alternative agent for the same task`
|
|
13270
|
+
}));
|
|
13271
|
+
return this.decideAuto({
|
|
13272
|
+
id: randomUUID(),
|
|
13273
|
+
source,
|
|
13274
|
+
decisionType: "spawn",
|
|
13275
|
+
question: `Should we spawn a subagent for this task?`,
|
|
13276
|
+
context: {
|
|
13277
|
+
facts: availableFacts,
|
|
13278
|
+
fleetStatus,
|
|
13279
|
+
taskDescription
|
|
13280
|
+
},
|
|
13281
|
+
options,
|
|
13282
|
+
risk,
|
|
13283
|
+
requiresConsensus: false
|
|
13284
|
+
});
|
|
13285
|
+
}
|
|
13286
|
+
/**
|
|
13287
|
+
* Decide whether to approve a proposed change.
|
|
13288
|
+
*/
|
|
13289
|
+
async decideApproval(source, change, relevantFacts) {
|
|
13290
|
+
const risk = this._changeRisk(change);
|
|
13291
|
+
const options = [
|
|
13292
|
+
{
|
|
13293
|
+
id: "approve",
|
|
13294
|
+
label: "Approve change",
|
|
13295
|
+
recommended: change.qualityGate.passed && relevantFacts.filter((f) => f.severity === "critical").length === 0,
|
|
13296
|
+
risk,
|
|
13297
|
+
consequence: `Apply changes to: ${change.files.map((f) => f.path).join(", ")}`
|
|
13298
|
+
},
|
|
13299
|
+
{
|
|
13300
|
+
id: "reject",
|
|
13301
|
+
label: "Reject change",
|
|
13302
|
+
recommended: false,
|
|
13303
|
+
risk: "medium",
|
|
13304
|
+
consequence: "Return change to proposer with feedback"
|
|
13305
|
+
},
|
|
13306
|
+
{
|
|
13307
|
+
id: "request_changes",
|
|
13308
|
+
label: "Request specific changes",
|
|
13309
|
+
recommended: false,
|
|
13310
|
+
risk: "low",
|
|
13311
|
+
consequence: "Send back for revision with conditions"
|
|
13312
|
+
}
|
|
13313
|
+
];
|
|
13314
|
+
return this.decideAuto({
|
|
13315
|
+
id: randomUUID(),
|
|
13316
|
+
source,
|
|
13317
|
+
decisionType: "approve_change",
|
|
13318
|
+
question: `Should we approve the change "${change.title}"?`,
|
|
13319
|
+
context: {
|
|
13320
|
+
facts: relevantFacts,
|
|
13321
|
+
change
|
|
13322
|
+
},
|
|
13323
|
+
options,
|
|
13324
|
+
risk,
|
|
13325
|
+
requiresConsensus: risk === "critical" || risk === "high"
|
|
13326
|
+
});
|
|
13327
|
+
}
|
|
13328
|
+
/**
|
|
13329
|
+
* Decide how to handle a failed task.
|
|
13330
|
+
*/
|
|
13331
|
+
async decideEscalation(source, taskId, error, attempts) {
|
|
13332
|
+
const retryCount = this.failurePatterns.get(taskId)?.failures ?? attempts;
|
|
13333
|
+
const options = [];
|
|
13334
|
+
if (retryCount < this.maxRetries) {
|
|
13335
|
+
options.push({
|
|
13336
|
+
id: "retry",
|
|
13337
|
+
label: `Retry task (attempt ${retryCount + 1}/${this.maxRetries})`,
|
|
13338
|
+
recommended: retryCount < 2,
|
|
13339
|
+
risk: "medium",
|
|
13340
|
+
consequence: `Restart the task with same or adjusted budget`
|
|
13341
|
+
});
|
|
13342
|
+
options.push({
|
|
13343
|
+
id: "retry_with_adjustment",
|
|
13344
|
+
label: `Retry with more budget`,
|
|
13345
|
+
recommended: retryCount >= 1,
|
|
13346
|
+
risk: "medium",
|
|
13347
|
+
consequence: `Increase timeout or iterations before retrying`
|
|
13348
|
+
});
|
|
13349
|
+
}
|
|
13350
|
+
options.push({
|
|
13351
|
+
id: "delegate",
|
|
13352
|
+
label: "Delegate to different role",
|
|
13353
|
+
recommended: retryCount >= 1,
|
|
13354
|
+
risk: "medium",
|
|
13355
|
+
consequence: "Try a different agent role for the same task"
|
|
13356
|
+
});
|
|
13357
|
+
if (retryCount >= this.maxRetries) {
|
|
13358
|
+
options.push({
|
|
13359
|
+
id: "mark_failed",
|
|
13360
|
+
label: "Mark task as failed",
|
|
13361
|
+
recommended: true,
|
|
13362
|
+
risk: "high",
|
|
13363
|
+
consequence: "Stop retrying, propagate failure upward"
|
|
13364
|
+
});
|
|
13365
|
+
}
|
|
13366
|
+
options.push({
|
|
13367
|
+
id: "decompose",
|
|
13368
|
+
label: "Decompose and retry in parts",
|
|
13369
|
+
recommended: false,
|
|
13370
|
+
risk: "low",
|
|
13371
|
+
consequence: "Break the task into smaller sub-tasks"
|
|
13372
|
+
});
|
|
13373
|
+
return this.decideAuto({
|
|
13374
|
+
id: randomUUID(),
|
|
13375
|
+
source,
|
|
13376
|
+
decisionType: "escalate_task",
|
|
13377
|
+
question: `Task failed: ${error.slice(0, 100)}. How should we proceed?`,
|
|
13378
|
+
context: { error, attempts: retryCount },
|
|
13379
|
+
options,
|
|
13380
|
+
risk: retryCount >= this.maxRetries ? "critical" : "medium",
|
|
13381
|
+
requiresConsensus: false
|
|
13382
|
+
});
|
|
13383
|
+
}
|
|
13384
|
+
// ── Self-improvement ─────────────────────────────────────────────────
|
|
13385
|
+
/**
|
|
13386
|
+
* Record the outcome of a decision for self-improvement.
|
|
13387
|
+
* Call this after a spawned agent completes or a change is applied.
|
|
13388
|
+
*/
|
|
13389
|
+
recordOutcome(decisionId, outcome, _detail) {
|
|
13390
|
+
const node = this.graph.get(decisionId);
|
|
13391
|
+
if (!node) return;
|
|
13392
|
+
const key = `decision:${node.decisionType}`;
|
|
13393
|
+
if (outcome === "failure") {
|
|
13394
|
+
const existing = this.failurePatterns.get(key) ?? { failures: 0, lastFailure: "" };
|
|
13395
|
+
existing.failures += 1;
|
|
13396
|
+
existing.lastFailure = (/* @__PURE__ */ new Date()).toISOString();
|
|
13397
|
+
this.failurePatterns.set(key, existing);
|
|
13398
|
+
} else {
|
|
13399
|
+
this.failurePatterns.delete(key);
|
|
13400
|
+
}
|
|
13401
|
+
void this.graph.update(decisionId, { decisionType: node.decisionType });
|
|
13402
|
+
}
|
|
13403
|
+
_getSelfImproveHints(decisionType) {
|
|
13404
|
+
const pattern = this.failurePatterns.get(`decision:${decisionType}`);
|
|
13405
|
+
if (!pattern || pattern.failures < 2) return [];
|
|
13406
|
+
return [
|
|
13407
|
+
`\u26A0\uFE0F ${decisionType} decisions have failed ${pattern.failures} times recently.`,
|
|
13408
|
+
"Consider alternative approaches before defaulting to this pattern."
|
|
13409
|
+
];
|
|
13410
|
+
}
|
|
13411
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
13412
|
+
_toAutonomous(req) {
|
|
13413
|
+
const decisionType = this._inferDecisionType(req);
|
|
13414
|
+
return {
|
|
13415
|
+
id: req.id,
|
|
13416
|
+
source: req.source,
|
|
13417
|
+
decisionType,
|
|
13418
|
+
question: req.question,
|
|
13419
|
+
context: {
|
|
13420
|
+
taskDescription: req.context ?? ""
|
|
13421
|
+
},
|
|
13422
|
+
options: req.options ?? [],
|
|
13423
|
+
risk: req.risk,
|
|
13424
|
+
requiresConsensus: this.RISK_ORDER.indexOf(req.risk) >= this.RISK_ORDER.indexOf(this.consensusRiskThreshold)
|
|
13425
|
+
};
|
|
13426
|
+
}
|
|
13427
|
+
_inferDecisionType(req) {
|
|
13428
|
+
if (req.question.toLowerCase().includes("spawn")) return "spawn";
|
|
13429
|
+
if (req.question.toLowerCase().includes("approve") || req.question.toLowerCase().includes("change")) return "approve_change";
|
|
13430
|
+
if (req.question.toLowerCase().includes("retry") || req.question.toLowerCase().includes("fail")) return "retry_task";
|
|
13431
|
+
if (req.question.toLowerCase().includes("priorit")) return "prioritize_goals";
|
|
13432
|
+
if (req.question.toLowerCase().includes("decompos")) return "decompose_goal";
|
|
13433
|
+
return "assign_task";
|
|
13434
|
+
}
|
|
13435
|
+
_serializeContext(ctx) {
|
|
13436
|
+
const parts = [];
|
|
13437
|
+
if (ctx.facts?.length) {
|
|
13438
|
+
parts.push(`## Relevant Facts
|
|
13439
|
+
${ctx.facts.map((f) => `- [${f.severity ?? "info"}] ${f.subject}: ${f.detail}`).join("\n")}`);
|
|
13440
|
+
}
|
|
13441
|
+
if (ctx.goals?.length) {
|
|
13442
|
+
parts.push(`## Active Goals
|
|
13443
|
+
${ctx.goals.map((g) => `- [${g.status}] ${g.priority}: ${g.title}`).join("\n")}`);
|
|
13444
|
+
}
|
|
13445
|
+
if (ctx.change) {
|
|
13446
|
+
const c = ctx.change;
|
|
13447
|
+
parts.push(`## Change Under Review
|
|
13448
|
+
- Title: ${c.title}
|
|
13449
|
+
- Status: ${c.status}
|
|
13450
|
+
- Files: ${c.files.map((f) => `${f.action} ${f.path}`).join(", ")}
|
|
13451
|
+
- Quality gate: ${c.qualityGate.passed ? "PASSED" : "FAILED"}
|
|
13452
|
+
Checks: ${c.qualityGate.checks.map((ch) => `${ch.name}:${ch.passed ? "\u2705" : "\u274C"}`).join(", ")}`);
|
|
13453
|
+
}
|
|
13454
|
+
if (ctx.fleetStatus) {
|
|
13455
|
+
parts.push(`## Fleet Status
|
|
13456
|
+
- Running: ${ctx.fleetStatus.running}, Idle: ${ctx.fleetStatus.idle}, Total: ${ctx.fleetStatus.total}
|
|
13457
|
+
- Cost so far: $${ctx.fleetStatus.costSoFar.toFixed(4)}`);
|
|
13458
|
+
}
|
|
13459
|
+
if (ctx.taskDescription) {
|
|
13460
|
+
parts.push(`## Task
|
|
13461
|
+
${ctx.taskDescription}`);
|
|
13462
|
+
}
|
|
13463
|
+
if (ctx.error) {
|
|
13464
|
+
parts.push(`## Error
|
|
13465
|
+
${ctx.error}`);
|
|
13466
|
+
}
|
|
13467
|
+
return parts.join("\n\n");
|
|
13468
|
+
}
|
|
13469
|
+
_loadHistory(type, _risk) {
|
|
13470
|
+
const all = this.graph.getDecisions().filter((d) => d.decisionType === type);
|
|
13471
|
+
return all.slice(-10);
|
|
13472
|
+
}
|
|
13473
|
+
async _recordDecision(input) {
|
|
13474
|
+
const node = await this.graph.add({
|
|
13475
|
+
type: "decision",
|
|
13476
|
+
decisionType: input.decisionType,
|
|
13477
|
+
question: input.question,
|
|
13478
|
+
options: input.options,
|
|
13479
|
+
chosen: input.chosen,
|
|
13480
|
+
rationale: input.rationale,
|
|
13481
|
+
madeBy: input.madeBy,
|
|
13482
|
+
madeAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13483
|
+
context: input.context
|
|
13484
|
+
});
|
|
13485
|
+
this.decisionHistory.push(node);
|
|
13486
|
+
return node;
|
|
13487
|
+
}
|
|
13488
|
+
_inferRoles(task) {
|
|
13489
|
+
const t = task.toLowerCase();
|
|
13490
|
+
if (t.includes("bug") || t.includes("error") || t.includes("crash")) return ["bug-hunter", "fixer"];
|
|
13491
|
+
if (t.includes("security") || t.includes("secret") || t.includes("injection")) return ["security-scanner"];
|
|
13492
|
+
if (t.includes("refactor") || t.includes("architecture") || t.includes("debt")) return ["refactor-planner", "critic"];
|
|
13493
|
+
if (t.includes("audit") || t.includes("log") || t.includes("analyze")) return ["audit-log"];
|
|
13494
|
+
if (t.includes("test") || t.includes("coverage")) return ["tester", "bug-hunter"];
|
|
13495
|
+
return ["bug-hunter", "refactor-planner"];
|
|
13496
|
+
}
|
|
13497
|
+
_changeRisk(change) {
|
|
13498
|
+
const criticalFiles = change.files.filter(
|
|
13499
|
+
(f) => f.path.includes("auth") || f.path.includes("config") || f.path.includes("schema")
|
|
13500
|
+
);
|
|
13501
|
+
if (criticalFiles.length > 0) return "high";
|
|
13502
|
+
if (change.files.length > 10) return "medium";
|
|
13503
|
+
return "low";
|
|
13504
|
+
}
|
|
13505
|
+
};
|
|
13506
|
+
var TaskAuctioneer = class {
|
|
13507
|
+
graph;
|
|
13508
|
+
fleet;
|
|
13509
|
+
mailbox;
|
|
13510
|
+
selfAgentId;
|
|
13511
|
+
bidWindowMs;
|
|
13512
|
+
maxTasksPerAgent;
|
|
13513
|
+
minConfidence;
|
|
13514
|
+
// minimum dispatcher confidence to accept a bid
|
|
13515
|
+
maxBidRetries;
|
|
13516
|
+
// max republished attempts before marking task failed
|
|
13517
|
+
/** Pending bids keyed by taskId. */
|
|
13518
|
+
pendingBids = /* @__PURE__ */ new Map();
|
|
13519
|
+
/** Active bid windows keyed by taskId. */
|
|
13520
|
+
bidTimers = /* @__PURE__ */ new Map();
|
|
13521
|
+
/** FleetBus subscription disposers, detached in dispose(). */
|
|
13522
|
+
unsubs = [];
|
|
13523
|
+
/** How many times a task has been republished with no bids received. */
|
|
13524
|
+
bidRetryCounts = /* @__PURE__ */ new Map();
|
|
13525
|
+
/** Agent → current task count (from graph + in-flight). */
|
|
13526
|
+
agentTaskCounts = /* @__PURE__ */ new Map();
|
|
13527
|
+
constructor(opts) {
|
|
13528
|
+
this.graph = opts.graph;
|
|
13529
|
+
this.fleet = opts.fleet;
|
|
13530
|
+
this.mailbox = opts.mailbox;
|
|
13531
|
+
this.selfAgentId = opts.selfAgentId ?? "auctioneer";
|
|
13532
|
+
this.bidWindowMs = opts.bidWindowMs ?? 3e4;
|
|
13533
|
+
this.maxTasksPerAgent = opts.maxTasksPerAgent ?? 3;
|
|
13534
|
+
this.minConfidence = opts.minConfidence ?? 0.3;
|
|
13535
|
+
this.maxBidRetries = opts.maxBidRetries ?? 3;
|
|
13536
|
+
const offBid = this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
|
|
13537
|
+
const offClaimed = this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
|
|
13538
|
+
if (offBid) this.unsubs.push(offBid);
|
|
13539
|
+
if (offClaimed) this.unsubs.push(offClaimed);
|
|
13540
|
+
}
|
|
13541
|
+
/**
|
|
13542
|
+
* Detach all FleetBus subscriptions and cancel any open bid-window timers.
|
|
13543
|
+
* Call when the owning coordinator stops/restarts so handlers and timers
|
|
13544
|
+
* don't accumulate across cycles.
|
|
13545
|
+
*/
|
|
13546
|
+
dispose() {
|
|
13547
|
+
for (const off of this.unsubs.splice(0)) {
|
|
13548
|
+
try {
|
|
13549
|
+
off();
|
|
13550
|
+
} catch {
|
|
13551
|
+
}
|
|
13552
|
+
}
|
|
13553
|
+
for (const t of this.bidTimers.values()) clearTimeout(t);
|
|
13554
|
+
this.bidTimers.clear();
|
|
13555
|
+
}
|
|
13556
|
+
// ── Publish a task ────────────────────────────────────────────────────
|
|
13557
|
+
/**
|
|
13558
|
+
* Publish a new task to the auction. Creates a GoalNode and broadcasts
|
|
13559
|
+
* it to all online agents. Returns the goal id.
|
|
13560
|
+
*
|
|
13561
|
+
* If `targetAgent` is specified, the task is assigned directly without auction.
|
|
13562
|
+
*/
|
|
13563
|
+
async publishTask(input) {
|
|
13564
|
+
const blockedBy = input.blockedBy ?? [];
|
|
13565
|
+
const hasOpenBlockers = blockedBy.length > 0 && blockedBy.some((id) => this.graph.get(id)?.status !== "done");
|
|
13566
|
+
const goal = await this.graph.add({
|
|
13567
|
+
type: "goal",
|
|
13568
|
+
title: input.title,
|
|
13569
|
+
description: input.description,
|
|
13570
|
+
status: input.targetAgent ? "in_progress" : hasOpenBlockers ? "blocked" : "pending",
|
|
13571
|
+
priority: input.priority ?? "medium",
|
|
13572
|
+
assignee: input.targetAgent,
|
|
13573
|
+
blockedBy,
|
|
13574
|
+
dependsOn: input.satisfiesGoals ?? [],
|
|
13575
|
+
createdBy: this.selfAgentId,
|
|
13576
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13577
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13578
|
+
tags: input.tags ?? [],
|
|
13579
|
+
children: [],
|
|
13580
|
+
parentGoal: input.parentGoal
|
|
13581
|
+
});
|
|
13582
|
+
if (input.parentGoal) {
|
|
13583
|
+
const parent = this.graph.get(input.parentGoal);
|
|
13584
|
+
if (parent) {
|
|
13585
|
+
await this.graph.update(input.parentGoal, {
|
|
13586
|
+
children: [...parent.children, goal.id]
|
|
13587
|
+
});
|
|
13588
|
+
}
|
|
13589
|
+
}
|
|
13590
|
+
for (const blockerId of blockedBy) {
|
|
13591
|
+
const blocker = this.graph.get(blockerId);
|
|
13592
|
+
if (blocker && !blocker.children.includes(goal.id)) {
|
|
13593
|
+
await this.graph.update(blockerId, { children: [...blocker.children, goal.id] });
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
if (input.targetAgent) {
|
|
13597
|
+
await this._assignDirect(goal.id, input.targetAgent);
|
|
13598
|
+
} else {
|
|
13599
|
+
await this._broadcastTask(goal);
|
|
13600
|
+
this._startBidWindow(goal.id);
|
|
13601
|
+
}
|
|
13602
|
+
this._emit("task:published", {
|
|
13603
|
+
taskId: goal.id,
|
|
13604
|
+
title: goal.title,
|
|
13605
|
+
priority: goal.priority,
|
|
13606
|
+
tags: goal.tags
|
|
13607
|
+
});
|
|
13608
|
+
return goal.id;
|
|
13609
|
+
}
|
|
13610
|
+
// ── Bid on a task ─────────────────────────────────────────────────────
|
|
13611
|
+
/**
|
|
13612
|
+
* Submit a bid for a task. Called by agents who want to work on it.
|
|
13613
|
+
* Returns true if the bid was accepted, false if the task was already claimed.
|
|
13614
|
+
*/
|
|
13615
|
+
async bid(taskId, agent, rationale) {
|
|
13616
|
+
const goal = this.graph.get(taskId);
|
|
13617
|
+
if (!goal || goal.type !== "goal") return false;
|
|
13618
|
+
if (goal.status !== "pending") return false;
|
|
13619
|
+
const currentCount = this._getAgentTaskCount(agent.agentId);
|
|
13620
|
+
if (currentCount >= this.maxTasksPerAgent) return false;
|
|
13621
|
+
const dispatchResult = await dispatchAgent(goal.description);
|
|
13622
|
+
const score = dispatchResult.confidence * (dispatchResult.role === agent.agentRole ? 1.2 : 1);
|
|
13623
|
+
if (score < this.minConfidence) return false;
|
|
13624
|
+
const bid = {
|
|
13625
|
+
id: randomUUID(),
|
|
13626
|
+
taskId,
|
|
13627
|
+
agentId: agent.agentId,
|
|
13628
|
+
agentName: agent.agentName,
|
|
13629
|
+
agentRole: agent.agentRole,
|
|
13630
|
+
score,
|
|
13631
|
+
rationale,
|
|
13632
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13633
|
+
};
|
|
13634
|
+
let bids = this.pendingBids.get(taskId);
|
|
13635
|
+
if (!bids) {
|
|
13636
|
+
bids = [];
|
|
13637
|
+
this.pendingBids.set(taskId, bids);
|
|
13638
|
+
}
|
|
13639
|
+
const existingIdx = bids.findIndex((b) => b.agentId === agent.agentId);
|
|
13640
|
+
if (existingIdx >= 0) {
|
|
13641
|
+
bids[existingIdx] = bid;
|
|
13642
|
+
} else {
|
|
13643
|
+
bids.push(bid);
|
|
13644
|
+
}
|
|
13645
|
+
this._emit("task:bid", {
|
|
13646
|
+
taskId,
|
|
13647
|
+
bid: { ...bid, score: Math.round(score * 100) / 100 },
|
|
13648
|
+
agentName: agent.agentName
|
|
13649
|
+
});
|
|
13650
|
+
await this._mailboxPublish({
|
|
13651
|
+
type: "note",
|
|
13652
|
+
subject: `[bid] ${agent.agentName} \u2192 ${goal.title}`,
|
|
13653
|
+
body: `${agent.agentName} (${agent.agentRole}) bidded on task "${goal.title}" (${goal.id})
|
|
13654
|
+
Rationale: ${rationale}
|
|
13655
|
+
Score: ${score.toFixed(2)}`
|
|
13656
|
+
});
|
|
13657
|
+
return true;
|
|
13658
|
+
}
|
|
13659
|
+
// ── Claim (award) a task ───────────────────────────────────────────────
|
|
13660
|
+
/**
|
|
13661
|
+
* Award a task to a specific agent. Called internally by the bid window
|
|
13662
|
+
* expiry, or can be called directly to force an award.
|
|
13663
|
+
*/
|
|
13664
|
+
async claim(taskId, agentId, agentName) {
|
|
13665
|
+
const goal = this.graph.get(taskId);
|
|
13666
|
+
if (!goal || goal.type !== "goal") return false;
|
|
13667
|
+
if (goal.status !== "pending") return false;
|
|
13668
|
+
this._cancelBidWindow(taskId);
|
|
13669
|
+
await this.graph.update(taskId, {
|
|
13670
|
+
status: "in_progress",
|
|
13671
|
+
assignee: agentId,
|
|
13672
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13673
|
+
});
|
|
13674
|
+
this.pendingBids.delete(taskId);
|
|
13675
|
+
this.agentTaskCount(agentId, 1);
|
|
13676
|
+
await this._notifyAgent(agentId, {
|
|
13677
|
+
type: "assign",
|
|
13678
|
+
subject: `[assigned] ${goal.title}`,
|
|
13679
|
+
body: `You have been assigned: "${goal.title}"
|
|
13680
|
+
|
|
13681
|
+
${goal.description}
|
|
13682
|
+
|
|
13683
|
+
Task ID: ${taskId}
|
|
13684
|
+
Priority: ${goal.priority}`,
|
|
13685
|
+
taskContext: {
|
|
13686
|
+
agentRole: goal.tags[0],
|
|
13687
|
+
taskId,
|
|
13688
|
+
status: "in_progress"
|
|
13689
|
+
}
|
|
13690
|
+
});
|
|
13691
|
+
this._emit("task:claimed", { taskId, agentId, agentName });
|
|
13692
|
+
return true;
|
|
13693
|
+
}
|
|
13694
|
+
// ── Complete a task ────────────────────────────────────────────────────
|
|
13695
|
+
/**
|
|
13696
|
+
* Mark a task as done. Called by the agent when it finishes.
|
|
13697
|
+
*/
|
|
13698
|
+
async complete(taskId, _result) {
|
|
13699
|
+
const goal = this.graph.get(taskId);
|
|
13700
|
+
if (!goal) return;
|
|
13701
|
+
const agentId = goal.assignee ?? "unknown";
|
|
13702
|
+
this.agentTaskCount(agentId, -1);
|
|
13703
|
+
await this.graph.update(taskId, {
|
|
13704
|
+
status: "done",
|
|
13705
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13706
|
+
});
|
|
13707
|
+
this.bidRetryCounts.delete(taskId);
|
|
13708
|
+
this.pendingBids.delete(taskId);
|
|
13709
|
+
this._cancelBidWindow(taskId);
|
|
13710
|
+
for (const childId of goal.children) {
|
|
13711
|
+
const child = this.graph.get(childId);
|
|
13712
|
+
if (child && child.status === "blocked") {
|
|
13713
|
+
const allUnblocked = child.blockedBy.every((blockedId) => {
|
|
13714
|
+
const blocked = this.graph.get(blockedId);
|
|
13715
|
+
return blocked?.status === "done";
|
|
13716
|
+
});
|
|
13717
|
+
if (allUnblocked) {
|
|
13718
|
+
await this.graph.update(childId, { status: "pending", updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13719
|
+
const unblockedGoal = this.graph.get(childId);
|
|
13720
|
+
await this._broadcastTask(unblockedGoal);
|
|
13721
|
+
this._startBidWindow(childId);
|
|
13722
|
+
}
|
|
13723
|
+
}
|
|
13724
|
+
}
|
|
13725
|
+
this._emit("task:completed", { taskId, agentId, result: _result });
|
|
13726
|
+
await this._mailboxPublish({
|
|
13727
|
+
type: "result",
|
|
13728
|
+
subject: `[done] ${goal.title}`,
|
|
13729
|
+
body: `Task completed by ${agentId}: "${goal.title}"
|
|
13730
|
+
|
|
13731
|
+
${_result ?? "No result provided."}`
|
|
13732
|
+
});
|
|
13733
|
+
}
|
|
13734
|
+
/**
|
|
13735
|
+
* Mark a task as failed. Optionally spawn a retry.
|
|
13736
|
+
*/
|
|
13737
|
+
async fail(taskId, error) {
|
|
13738
|
+
const goal = this.graph.get(taskId);
|
|
13739
|
+
if (!goal) return;
|
|
13740
|
+
const agentId = goal.assignee ?? "unknown";
|
|
13741
|
+
this.agentTaskCount(agentId, -1);
|
|
13742
|
+
await this.graph.update(taskId, {
|
|
13743
|
+
status: "failed",
|
|
13744
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13745
|
+
result: error
|
|
13746
|
+
});
|
|
13747
|
+
this.bidRetryCounts.delete(taskId);
|
|
13748
|
+
this.pendingBids.delete(taskId);
|
|
13749
|
+
this._cancelBidWindow(taskId);
|
|
13750
|
+
this._emit("task:failed", { taskId, agentId, error });
|
|
13751
|
+
await this._mailboxPublish({
|
|
13752
|
+
type: "note",
|
|
13753
|
+
subject: `[failed] ${goal.title}`,
|
|
13754
|
+
body: `Task failed: "${goal.title}"
|
|
13755
|
+
Error: ${error}
|
|
13756
|
+
Assignee: ${agentId}`
|
|
13757
|
+
});
|
|
13758
|
+
}
|
|
13759
|
+
// ── Work finding ──────────────────────────────────────────────────────
|
|
13760
|
+
/**
|
|
13761
|
+
* Find the best available tasks for an agent based on its capabilities.
|
|
13762
|
+
* Returns tasks sorted by match score (best first).
|
|
13763
|
+
*/
|
|
13764
|
+
async findWork(_agentId, agentRole, limit = 5) {
|
|
13765
|
+
const pending = this.graph.getGoals({ status: "pending" });
|
|
13766
|
+
const scored = [];
|
|
13767
|
+
for (const goal of pending) {
|
|
13768
|
+
if (goal.blockedBy.length > 0) continue;
|
|
13769
|
+
const dispatchResult = await dispatchAgent(goal.description);
|
|
13770
|
+
const roleBonus = agentRole && goal.tags.includes(agentRole) ? 1.3 : 1;
|
|
13771
|
+
const priorityBonus = goal.priority === "critical" ? 1.5 : goal.priority === "high" ? 1.2 : 1;
|
|
13772
|
+
const score = dispatchResult.confidence * roleBonus * priorityBonus;
|
|
13773
|
+
const bids = this.pendingBids.get(goal.id)?.length ?? 0;
|
|
13774
|
+
scored.push({ task: goal, score, bids });
|
|
13775
|
+
}
|
|
13776
|
+
scored.sort((a, b) => b.score - a.score);
|
|
13777
|
+
return scored.slice(0, limit);
|
|
13778
|
+
}
|
|
13779
|
+
// ── Queries ──────────────────────────────────────────────────────────
|
|
13780
|
+
/** Get all pending tasks (available for bidding). */
|
|
13781
|
+
getPendingTasks() {
|
|
13782
|
+
return this.graph.getGoals({ status: "pending" }).filter((g) => g.blockedBy.length === 0);
|
|
13783
|
+
}
|
|
13784
|
+
/** Get tasks assigned to a specific agent. */
|
|
13785
|
+
getTasksForAgent(agentId) {
|
|
13786
|
+
return this.graph.getGoals({}).filter((g) => g.assignee === agentId);
|
|
13787
|
+
}
|
|
13788
|
+
/** Get the current bid count for a task. */
|
|
13789
|
+
getBidCount(taskId) {
|
|
13790
|
+
return this.pendingBids.get(taskId)?.length ?? 0;
|
|
13791
|
+
}
|
|
13792
|
+
/** Get bids for a task. */
|
|
13793
|
+
getBids(taskId) {
|
|
13794
|
+
return this.pendingBids.get(taskId) ?? [];
|
|
13795
|
+
}
|
|
13796
|
+
/** Get task stats for a project-wide dashboard. */
|
|
13797
|
+
getStats() {
|
|
13798
|
+
const all = this.graph.getGoals({});
|
|
13799
|
+
const pending = all.filter((g) => g.status === "pending" && g.blockedBy.length === 0);
|
|
13800
|
+
const inProgress = all.filter((g) => g.status === "in_progress");
|
|
13801
|
+
const done = all.filter((g) => g.status === "done");
|
|
13802
|
+
const failed = all.filter((g) => g.status === "failed");
|
|
13803
|
+
let totalBids = 0;
|
|
13804
|
+
for (const bids of this.pendingBids.values()) totalBids += bids.length;
|
|
13805
|
+
return {
|
|
13806
|
+
total: all.length,
|
|
13807
|
+
pending: pending.length,
|
|
13808
|
+
in_progress: inProgress.length,
|
|
13809
|
+
done: done.length,
|
|
13810
|
+
failed: failed.length,
|
|
13811
|
+
totalBids,
|
|
13812
|
+
avgBidsPerTask: pending.length > 0 ? totalBids / pending.length : 0
|
|
13813
|
+
};
|
|
13814
|
+
}
|
|
13815
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
13816
|
+
_emit(type, payload) {
|
|
13817
|
+
if (!this.fleet) return;
|
|
13818
|
+
this.fleet.emit({ subagentId: this.selfAgentId, ts: Date.now(), type, payload });
|
|
13819
|
+
}
|
|
13820
|
+
_broadcastTask(goal) {
|
|
13821
|
+
this._emit("task:available", {
|
|
13822
|
+
taskId: goal.id,
|
|
13823
|
+
title: goal.title,
|
|
13824
|
+
description: goal.description,
|
|
13825
|
+
priority: goal.priority,
|
|
13826
|
+
tags: goal.tags
|
|
13827
|
+
});
|
|
13828
|
+
this._mailboxPublish({
|
|
13829
|
+
type: "broadcast",
|
|
13830
|
+
subject: `[task] ${goal.title} (${goal.priority})`,
|
|
13831
|
+
body: `New task available: "${goal.title}"
|
|
13832
|
+
Priority: ${goal.priority}
|
|
13833
|
+
Description: ${goal.description.slice(0, 200)}${goal.description.length > 200 ? "..." : ""}
|
|
13834
|
+
|
|
13835
|
+
Task ID: ${goal.id}
|
|
13836
|
+
Tags: ${goal.tags.join(", ") || "none"}
|
|
13837
|
+
|
|
13838
|
+
Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
|
|
13839
|
+
}).catch(() => {
|
|
13840
|
+
});
|
|
13841
|
+
}
|
|
13842
|
+
async _mailboxPublish(msg) {
|
|
13843
|
+
if (!this.mailbox) return;
|
|
13844
|
+
try {
|
|
13845
|
+
await this.mailbox.send({
|
|
13846
|
+
from: this.selfAgentId,
|
|
13847
|
+
to: "*",
|
|
13848
|
+
type: msg.type,
|
|
13849
|
+
subject: msg.subject,
|
|
13850
|
+
body: msg.body,
|
|
13851
|
+
priority: "normal"
|
|
13852
|
+
});
|
|
13853
|
+
} catch {
|
|
13854
|
+
}
|
|
13855
|
+
}
|
|
13856
|
+
async _notifyAgent(agentId, msg) {
|
|
13857
|
+
if (!this.mailbox) return;
|
|
13858
|
+
try {
|
|
13859
|
+
await this.mailbox.send({
|
|
13860
|
+
from: this.selfAgentId,
|
|
13861
|
+
to: agentId,
|
|
13862
|
+
type: msg.type,
|
|
13863
|
+
subject: msg.subject,
|
|
13864
|
+
body: msg.body,
|
|
13865
|
+
priority: "high",
|
|
13866
|
+
taskContext: msg.taskContext
|
|
13867
|
+
});
|
|
13868
|
+
} catch {
|
|
13869
|
+
}
|
|
13870
|
+
}
|
|
13871
|
+
_startBidWindow(taskId) {
|
|
13872
|
+
this._cancelBidWindow(taskId);
|
|
13873
|
+
const timer = setTimeout(() => {
|
|
13874
|
+
this.bidTimers.delete(taskId);
|
|
13875
|
+
void this._evaluateBids(taskId).catch(() => {
|
|
13876
|
+
});
|
|
13877
|
+
}, this.bidWindowMs);
|
|
13878
|
+
this.bidTimers.set(taskId, timer);
|
|
13879
|
+
}
|
|
13880
|
+
_cancelBidWindow(taskId) {
|
|
13881
|
+
const timer = this.bidTimers.get(taskId);
|
|
13882
|
+
if (timer) {
|
|
13883
|
+
clearTimeout(timer);
|
|
13884
|
+
this.bidTimers.delete(taskId);
|
|
13885
|
+
}
|
|
13886
|
+
}
|
|
13887
|
+
async _evaluateBids(taskId) {
|
|
13888
|
+
const bids = this.pendingBids.get(taskId);
|
|
13889
|
+
if (!bids || bids.length === 0) {
|
|
13890
|
+
const retryCount = (this.bidRetryCounts.get(taskId) ?? 0) + 1;
|
|
13891
|
+
this.bidRetryCounts.set(taskId, retryCount);
|
|
13892
|
+
if (retryCount >= this.maxBidRetries) {
|
|
13893
|
+
await this.fail(taskId, `No bids received after ${this.maxBidRetries} attempts`);
|
|
13894
|
+
this.bidRetryCounts.delete(taskId);
|
|
13895
|
+
return;
|
|
13896
|
+
}
|
|
13897
|
+
const goal = this.graph.get(taskId);
|
|
13898
|
+
if (goal) {
|
|
13899
|
+
await this._broadcastTask(goal);
|
|
13900
|
+
this._startBidWindow(taskId);
|
|
13901
|
+
}
|
|
13902
|
+
return;
|
|
13903
|
+
}
|
|
13904
|
+
bids.sort((a, b) => b.score - a.score);
|
|
13905
|
+
const winner = bids.find((b) => this._getAgentTaskCount(b.agentId) < this.maxTasksPerAgent);
|
|
13906
|
+
if (!winner) {
|
|
13907
|
+
const goal = this.graph.get(taskId);
|
|
13908
|
+
if (goal) {
|
|
13909
|
+
await this._broadcastTask(goal);
|
|
13910
|
+
this._startBidWindow(taskId);
|
|
13911
|
+
}
|
|
13912
|
+
return;
|
|
13913
|
+
}
|
|
13914
|
+
await this.claim(taskId, winner.agentId, winner.agentName);
|
|
13915
|
+
}
|
|
13916
|
+
async _assignDirect(taskId, agentId) {
|
|
13917
|
+
const goal = this.graph.get(taskId);
|
|
13918
|
+
if (!goal) return;
|
|
13919
|
+
await this.graph.update(taskId, {
|
|
13920
|
+
status: "in_progress",
|
|
13921
|
+
assignee: agentId,
|
|
13922
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13923
|
+
});
|
|
13924
|
+
this.agentTaskCount(agentId, 1);
|
|
13925
|
+
await this._notifyAgent(agentId, {
|
|
13926
|
+
type: "assign",
|
|
13927
|
+
subject: `[assigned] ${goal.title}`,
|
|
13928
|
+
body: `You have been directly assigned: "${goal.title}"
|
|
13929
|
+
|
|
13930
|
+
${goal.description}`,
|
|
13931
|
+
taskContext: { taskId, status: "in_progress" }
|
|
13932
|
+
});
|
|
13933
|
+
}
|
|
13934
|
+
_onBidEvent(_e) {
|
|
13935
|
+
}
|
|
13936
|
+
_onClaimedEvent(e) {
|
|
13937
|
+
const { taskId } = e.payload;
|
|
13938
|
+
this.pendingBids.delete(taskId);
|
|
13939
|
+
this._cancelBidWindow(taskId);
|
|
13940
|
+
}
|
|
13941
|
+
_getAgentTaskCount(agentId) {
|
|
13942
|
+
return this.agentTaskCounts.get(agentId) ?? 0;
|
|
13943
|
+
}
|
|
13944
|
+
agentTaskCount(agentId, delta) {
|
|
13945
|
+
const current = this._getAgentTaskCount(agentId);
|
|
13946
|
+
const next = Math.max(0, current + delta);
|
|
13947
|
+
this.agentTaskCounts.set(agentId, next);
|
|
13948
|
+
}
|
|
13949
|
+
};
|
|
13950
|
+
var AutonomousCoordinator = class {
|
|
13951
|
+
graph;
|
|
13952
|
+
dag;
|
|
13953
|
+
auction;
|
|
13954
|
+
consensus;
|
|
13955
|
+
changes;
|
|
13956
|
+
brain;
|
|
13957
|
+
selfAgentId;
|
|
13958
|
+
fleet;
|
|
13959
|
+
fleetManager;
|
|
13960
|
+
director;
|
|
13961
|
+
mailbox;
|
|
13962
|
+
events;
|
|
13963
|
+
onCoordinatorEvent;
|
|
13964
|
+
running = false;
|
|
13965
|
+
iterationCount = 0;
|
|
13966
|
+
/** Tasks already handled by _onSubagentTerminated (to avoid double goal:failed on fleet event). */
|
|
13967
|
+
_handledBySubagent = /* @__PURE__ */ new Set();
|
|
13968
|
+
/** FleetBus subscription disposers, detached in dispose(). */
|
|
13969
|
+
unsubs = [];
|
|
13970
|
+
constructor(opts) {
|
|
13971
|
+
this.selfAgentId = opts.selfAgentId;
|
|
13972
|
+
this.fleet = opts.fleet ?? void 0;
|
|
13973
|
+
this.fleetManager = opts.fleetManager ?? void 0;
|
|
13974
|
+
this.director = opts.director ?? void 0;
|
|
13975
|
+
this.mailbox = opts.mailbox ?? void 0;
|
|
13976
|
+
this.events = opts.events ?? void 0;
|
|
13977
|
+
this.onCoordinatorEvent = opts.onCoordinatorEvent;
|
|
13978
|
+
this.graph = new KnowledgeGraph(opts.sessionDir);
|
|
13979
|
+
this.dag = new TaskDAG();
|
|
13980
|
+
this.auction = new TaskAuctioneer({
|
|
13981
|
+
graph: this.graph,
|
|
13982
|
+
fleet: this.fleet ?? void 0,
|
|
13983
|
+
mailbox: this.mailbox ?? void 0,
|
|
13984
|
+
selfAgentId: this.selfAgentId
|
|
13985
|
+
});
|
|
13986
|
+
this.consensus = new ConsensusProtocol({
|
|
13987
|
+
graph: this.graph,
|
|
13988
|
+
fleet: this.fleet ?? void 0,
|
|
13989
|
+
voters: this._buildVoters(),
|
|
13990
|
+
rules: {
|
|
13991
|
+
quorumFraction: 0.5,
|
|
13992
|
+
approvalFraction: 0.6,
|
|
13993
|
+
vetoRoles: ["critic"],
|
|
13994
|
+
// Critic has veto power
|
|
13995
|
+
approvalWeightFraction: 0.5
|
|
13996
|
+
}
|
|
13997
|
+
});
|
|
13998
|
+
this.changes = new ChangeManager({
|
|
13999
|
+
graph: this.graph,
|
|
14000
|
+
consensus: this.consensus,
|
|
14001
|
+
fleet: this.fleet ?? void 0,
|
|
14002
|
+
checks: DEFAULT_QUALITY_CHECKS
|
|
14003
|
+
});
|
|
14004
|
+
this.brain = new AutonomousBrain({
|
|
14005
|
+
llmProvider: opts.llmProvider,
|
|
14006
|
+
graph: this.graph,
|
|
14007
|
+
fleet: this.fleet ?? void 0,
|
|
14008
|
+
selfImprove: !opts.disableSelfImprove
|
|
14009
|
+
});
|
|
14010
|
+
this.dag.onEvent((event) => {
|
|
14011
|
+
this._onDagEvent(event);
|
|
14012
|
+
});
|
|
14013
|
+
const offCompleted = this.fleet?.filter("subagent.completed", (e) => {
|
|
14014
|
+
this._onSubagentTerminated(e);
|
|
14015
|
+
});
|
|
14016
|
+
if (offCompleted) this.unsubs.push(offCompleted);
|
|
14017
|
+
const offFailed = this.fleet?.filter("task:failed", (e) => {
|
|
14018
|
+
const payload = e.payload;
|
|
14019
|
+
const taskId = payload?.taskId;
|
|
14020
|
+
if (!taskId || this._handledBySubagent.has(taskId)) return;
|
|
14021
|
+
this._handledBySubagent.add(taskId);
|
|
14022
|
+
this._emit({ type: "goal:failed", goalId: taskId, text: payload?.error ?? "Task failed" });
|
|
14023
|
+
});
|
|
14024
|
+
if (offFailed) this.unsubs.push(offFailed);
|
|
14025
|
+
this._emit({ type: "coordinator:mode", mode: this.fleet ? "fleet" : "standalone" });
|
|
14026
|
+
}
|
|
14027
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
14028
|
+
/**
|
|
14029
|
+
* Run the autonomous loop until the goal is satisfied or max iterations reached.
|
|
14030
|
+
* This is the main entry point for a fully autonomous session.
|
|
14031
|
+
*/
|
|
14032
|
+
async run(opts = {}) {
|
|
14033
|
+
if (this.running) throw new Error("AutonomousCoordinator: already running");
|
|
14034
|
+
this.running = true;
|
|
14035
|
+
this.iterationCount = 0;
|
|
14036
|
+
const maxIterations = opts.maxIterations ?? 100;
|
|
14037
|
+
const goal = opts.goal ?? "Improve the codebase";
|
|
14038
|
+
const maxCost = opts.maxCostUsd;
|
|
14039
|
+
try {
|
|
14040
|
+
await this.graph.load();
|
|
14041
|
+
const goalConfigs = await this._decomposeGoal(goal);
|
|
14042
|
+
for (const g of goalConfigs) {
|
|
14043
|
+
const goalId = await this.auction.publishTask(g);
|
|
14044
|
+
this.dag.addNode(goalId, g.description, []);
|
|
14045
|
+
this._emit({ type: "goal:added", goalId, title: g.title, text: g.description });
|
|
14046
|
+
}
|
|
14047
|
+
while (this.running) {
|
|
14048
|
+
this.iterationCount++;
|
|
14049
|
+
if (this.iterationCount >= maxIterations) break;
|
|
14050
|
+
if (maxCost !== void 0) {
|
|
14051
|
+
const cost = this.fleetManager?.snapshot()?.total?.cost ?? 0;
|
|
14052
|
+
if (cost >= maxCost) break;
|
|
14053
|
+
}
|
|
14054
|
+
if (opts.runUntilComplete && this.dag.isDone()) break;
|
|
14055
|
+
const decision = await this.brain.decideAuto({
|
|
14056
|
+
id: randomUUID(),
|
|
14057
|
+
source: "system",
|
|
14058
|
+
decisionType: "prioritize_goals",
|
|
14059
|
+
question: `What should we work on next? Open goals: ${this.auction.getPendingTasks().map((g) => g.title).join(", ") || "none"}`,
|
|
14060
|
+
context: {
|
|
14061
|
+
goals: this.auction.getPendingTasks(),
|
|
14062
|
+
fleetStatus: this._fleetStatus()
|
|
14063
|
+
},
|
|
14064
|
+
options: this._goalToOptions(this.auction.getPendingTasks()),
|
|
14065
|
+
risk: "medium",
|
|
14066
|
+
requiresConsensus: false
|
|
14067
|
+
});
|
|
14068
|
+
if (decision.type === "deny") {
|
|
14069
|
+
const blocked = this.dag.getBlocked();
|
|
14070
|
+
if (blocked.length > 0 && this.dag.hasDeadlock()) {
|
|
14071
|
+
(this.events?.emit)("autonomous:deadlock", { blocked });
|
|
14072
|
+
this._emit({ type: "deadlock:detected", goalId: blocked[0]?.id ?? "", text: `Deadlock detected: ${blocked.map((n) => n.id).join(", ")}` });
|
|
14073
|
+
this.running = false;
|
|
14074
|
+
}
|
|
14075
|
+
break;
|
|
14076
|
+
}
|
|
14077
|
+
if (decision.type === "ask_human") {
|
|
14078
|
+
(this.events?.emit)("autonomous:ask_human", { prompt: decision.prompt });
|
|
14079
|
+
break;
|
|
14080
|
+
}
|
|
14081
|
+
if (decision.optionId) {
|
|
14082
|
+
const goalNode = this._optionToGoal(decision.optionId);
|
|
14083
|
+
if (goalNode) {
|
|
14084
|
+
await this._processGoal(goalNode.id);
|
|
14085
|
+
}
|
|
14086
|
+
}
|
|
14087
|
+
const pendingChanges = this.changes.getPendingReviews();
|
|
14088
|
+
for (const change of pendingChanges) {
|
|
14089
|
+
try {
|
|
14090
|
+
await this._handlePendingChange(change);
|
|
14091
|
+
} catch (err) {
|
|
14092
|
+
this._emit({
|
|
14093
|
+
type: "goal:failed",
|
|
14094
|
+
goalId: change.id,
|
|
14095
|
+
text: `Consensus handling failed: ${err instanceof Error ? err.message : String(err)}`
|
|
14096
|
+
});
|
|
14097
|
+
}
|
|
14098
|
+
}
|
|
14099
|
+
}
|
|
14100
|
+
} finally {
|
|
14101
|
+
this.running = false;
|
|
14102
|
+
}
|
|
14103
|
+
return this.getStats();
|
|
14104
|
+
}
|
|
14105
|
+
/** Stop the autonomous loop. */
|
|
14106
|
+
stop() {
|
|
14107
|
+
if (!this.running) return;
|
|
14108
|
+
this.running = false;
|
|
14109
|
+
console.error(`[AutonomousCoordinator] stop signal received \u2014 shutting down (iteration ${this.iterationCount})`);
|
|
14110
|
+
}
|
|
14111
|
+
/**
|
|
14112
|
+
* Tear down the coordinator for good: stop the loop and detach all FleetBus
|
|
14113
|
+
* subscriptions (this coordinator's + the auctioneer's) plus any open bid
|
|
14114
|
+
* timers. Call this when discarding the instance (e.g. `/coordinator stop`
|
|
14115
|
+
* that recreates a fresh coordinator on the next start) so handlers and
|
|
14116
|
+
* timers don't accumulate across cycles. `stop()` only pauses the loop.
|
|
14117
|
+
*/
|
|
14118
|
+
dispose() {
|
|
14119
|
+
this.stop();
|
|
14120
|
+
for (const off of this.unsubs.splice(0)) {
|
|
14121
|
+
try {
|
|
14122
|
+
off();
|
|
14123
|
+
} catch {
|
|
14124
|
+
}
|
|
14125
|
+
}
|
|
14126
|
+
this.auction.dispose();
|
|
14127
|
+
}
|
|
14128
|
+
/** Get a stats snapshot. */
|
|
14129
|
+
getStats() {
|
|
14130
|
+
const dagStats = this.dag.stats();
|
|
14131
|
+
const auctionStats = this.auction.getStats();
|
|
14132
|
+
const allGoals = this.graph.getGoals({});
|
|
14133
|
+
const allChanges = this.graph.getChanges({});
|
|
14134
|
+
const allDecisions = this.graph.getDecisions();
|
|
14135
|
+
return {
|
|
14136
|
+
goals: {
|
|
14137
|
+
total: allGoals.length,
|
|
14138
|
+
done: allGoals.filter((g) => g.status === "done").length,
|
|
14139
|
+
pending: allGoals.filter((g) => g.status === "pending").length,
|
|
14140
|
+
failed: allGoals.filter((g) => g.status === "failed").length,
|
|
14141
|
+
progress: allGoals.length > 0 ? allGoals.filter((g) => g.status === "done").length / allGoals.length : 0
|
|
14142
|
+
},
|
|
14143
|
+
dag: dagStats,
|
|
14144
|
+
auction: auctionStats,
|
|
14145
|
+
changes: {
|
|
14146
|
+
proposed: allChanges.filter((c) => c.status === "proposed").length,
|
|
14147
|
+
approved: allChanges.filter((c) => c.status === "approved").length,
|
|
14148
|
+
applied: allChanges.filter((c) => c.status === "applied").length,
|
|
14149
|
+
rejected: allChanges.filter((c) => c.status === "rejected").length
|
|
14150
|
+
},
|
|
14151
|
+
decisions: allDecisions.length,
|
|
14152
|
+
costSoFar: this.fleetManager?.snapshot()?.total?.cost
|
|
14153
|
+
};
|
|
14154
|
+
}
|
|
14155
|
+
// ── Fact publishing ──────────────────────────────────────────────────
|
|
14156
|
+
/**
|
|
14157
|
+
* Publish a fact discovered by an agent. Facts are immutable and form
|
|
14158
|
+
* the basis for other agents' decisions.
|
|
14159
|
+
*/
|
|
14160
|
+
async publishFact(input) {
|
|
14161
|
+
const fact = await this.graph.add({
|
|
14162
|
+
type: "fact",
|
|
14163
|
+
category: input.category,
|
|
14164
|
+
subject: input.subject,
|
|
14165
|
+
detail: input.detail,
|
|
14166
|
+
file: input.file,
|
|
14167
|
+
line: input.line,
|
|
14168
|
+
severity: input.severity,
|
|
14169
|
+
discoveredBy: this.selfAgentId,
|
|
14170
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14171
|
+
tags: input.tags ?? [],
|
|
14172
|
+
key: `${input.category}:${input.subject}:${input.file ?? ""}:${input.line ?? ""}`,
|
|
14173
|
+
related: []
|
|
14174
|
+
});
|
|
14175
|
+
await this._mailboxBroadcast({
|
|
14176
|
+
type: "note",
|
|
14177
|
+
subject: `[${input.severity ?? "info"}] ${input.category}: ${input.subject}`,
|
|
14178
|
+
body: `**${input.category}**${input.file ? ` in ${input.file}${input.line ? `:${input.line}` : ""}` : ""}
|
|
14179
|
+
${input.detail}`
|
|
14180
|
+
});
|
|
14181
|
+
this._emit({ type: "knowledge:added", knowledgeId: fact.id, title: input.subject, text: input.detail });
|
|
14182
|
+
return fact;
|
|
14183
|
+
}
|
|
14184
|
+
// ── Goal creation helpers ────────────────────────────────────────────
|
|
14185
|
+
/**
|
|
14186
|
+
* Publish a goal and add it to the DAG.
|
|
14187
|
+
*/
|
|
14188
|
+
async createGoal(input) {
|
|
14189
|
+
const resolvedPriority = input.priority ?? "medium";
|
|
14190
|
+
const goalId = await this.auction.publishTask({
|
|
14191
|
+
title: input.title,
|
|
14192
|
+
description: input.description,
|
|
14193
|
+
priority: resolvedPriority,
|
|
14194
|
+
...input.tags ? { tags: input.tags } : {},
|
|
14195
|
+
// Mirror the dependency edges into the auction so blocked goals aren't
|
|
14196
|
+
// biddable until their deps complete (the DAG tracks the same edges).
|
|
14197
|
+
...input.deps && input.deps.length > 0 ? { blockedBy: input.deps } : {}
|
|
14198
|
+
});
|
|
14199
|
+
const goal = this.graph.get(goalId);
|
|
14200
|
+
for (const depId of input.deps ?? []) {
|
|
14201
|
+
this.dag.addNode(depId, this.graph.get(depId)?.type === "goal" ? this.graph.get(depId).title : depId);
|
|
14202
|
+
}
|
|
14203
|
+
this.dag.addNode(goalId, input.description, input.deps ?? []);
|
|
14204
|
+
return goal;
|
|
14205
|
+
}
|
|
14206
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
14207
|
+
async _decomposeGoal(goalText) {
|
|
14208
|
+
const category = this._inferCategory(goalText);
|
|
14209
|
+
const subGoals = [];
|
|
14210
|
+
if (category === "security") {
|
|
14211
|
+
subGoals.push({ title: "Audit for secrets", description: "Scan codebase for hardcoded secrets and API keys", priority: "critical", tags: ["security"] });
|
|
14212
|
+
subGoals.push({ title: "Check injection vectors", description: "Find eval, innerHTML, SQL concat, shell injection patterns", priority: "critical", tags: ["security", "injection"] });
|
|
14213
|
+
subGoals.push({ title: "Dependency audit", description: "Run npm/pnpm audit for known CVEs", priority: "high", tags: ["security", "deps"] });
|
|
14214
|
+
} else if (category === "bug") {
|
|
14215
|
+
subGoals.push({ title: "Find bugs", description: `Scan for bugs related to: ${goalText}`, priority: "high", tags: ["bug"] });
|
|
14216
|
+
subGoals.push({ title: "Fix bugs", description: "Fix discovered bugs with tests", priority: "high", tags: ["fix"] });
|
|
14217
|
+
} else if (category === "refactor") {
|
|
14218
|
+
subGoals.push({ title: "Plan refactor", description: `Analyze code structure for: ${goalText}`, priority: "medium", tags: ["refactor", "planning"] });
|
|
14219
|
+
subGoals.push({ title: "Implement refactor", description: "Apply the refactoring plan", priority: "medium", tags: ["refactor", "implementation"] });
|
|
14220
|
+
} else {
|
|
14221
|
+
subGoals.push({ title: goalText, description: goalText, priority: "medium", tags: [category] });
|
|
14222
|
+
}
|
|
14223
|
+
return subGoals;
|
|
14224
|
+
}
|
|
14225
|
+
_inferCategory(goal) {
|
|
14226
|
+
const g = goal.toLowerCase();
|
|
14227
|
+
if (g.includes("security") || g.includes("secret") || g.includes("injection")) return "security";
|
|
14228
|
+
if (g.includes("bug") || g.includes("fix") || g.includes("error")) return "bug";
|
|
14229
|
+
if (g.includes("refactor") || g.includes("debt") || g.includes("architecture")) return "architecture";
|
|
14230
|
+
if (g.includes("test") || g.includes("coverage")) return "test";
|
|
14231
|
+
if (g.includes("perf") || g.includes("speed") || g.includes("optimize")) return "perf";
|
|
14232
|
+
if (g.includes("deps") || g.includes("package") || g.includes("update")) return "deps";
|
|
14233
|
+
return "quality";
|
|
14234
|
+
}
|
|
14235
|
+
async _processGoal(goalId) {
|
|
14236
|
+
const ready = this.dag.getReady();
|
|
14237
|
+
if (ready.length === 0) return;
|
|
14238
|
+
const dagNode = ready.find((n) => n.id === goalId) ?? ready[0];
|
|
14239
|
+
this.dag.start(dagNode.id, "auctioneer");
|
|
14240
|
+
const goalNode = this.graph.get(goalId);
|
|
14241
|
+
if (!goalNode) return;
|
|
14242
|
+
const title = goalNode.title || dagNode.description;
|
|
14243
|
+
const taskId = await this.auction.publishTask({
|
|
14244
|
+
title,
|
|
14245
|
+
description: goalNode.description,
|
|
14246
|
+
priority: this._dagPriorityToGoal(dagNode.priority),
|
|
14247
|
+
tags: dagNode.tags
|
|
14248
|
+
});
|
|
14249
|
+
this._emit({ type: "task:ready", goalId, taskId, title });
|
|
14250
|
+
if (this.director) {
|
|
14251
|
+
const config = {
|
|
14252
|
+
name: `worker-${goalId.slice(0, 8)}`,
|
|
14253
|
+
role: "general",
|
|
14254
|
+
maxIterations: 100,
|
|
14255
|
+
timeoutMs: 6e5
|
|
14256
|
+
// 10 minutes per goal
|
|
14257
|
+
};
|
|
14258
|
+
const subagentId = await this.director.spawn(config);
|
|
14259
|
+
await this.auction.claim(taskId, subagentId, config.name);
|
|
14260
|
+
await this.director.assign({
|
|
14261
|
+
id: goalId,
|
|
14262
|
+
subagentId,
|
|
14263
|
+
description: goalNode.description
|
|
14264
|
+
});
|
|
14265
|
+
}
|
|
14266
|
+
}
|
|
14267
|
+
async _handlePendingChange(change) {
|
|
14268
|
+
const result = this.consensus.getStatus(change.id);
|
|
14269
|
+
if (result?.outcome !== "pending") return;
|
|
14270
|
+
if (change.qualityGate.passed) {
|
|
14271
|
+
const voteResult = await this.consensus.castVote(
|
|
14272
|
+
change.id,
|
|
14273
|
+
this.selfAgentId,
|
|
14274
|
+
"approve",
|
|
14275
|
+
`Quality gate passed: ${change.qualityGate.checks.map((c) => c.name).join(", ")}`
|
|
14276
|
+
);
|
|
14277
|
+
if (voteResult.outcome === "approved") {
|
|
14278
|
+
await this.changes.markApplied(change.id, (/* @__PURE__ */ new Date()).toISOString());
|
|
14279
|
+
this._emit({ type: "consensus:reached", goalId: change.id, text: "Change approved and applied" });
|
|
14280
|
+
}
|
|
14281
|
+
} else {
|
|
14282
|
+
const voteResult = await this.consensus.castVote(
|
|
14283
|
+
change.id,
|
|
14284
|
+
this.selfAgentId,
|
|
14285
|
+
"reject",
|
|
14286
|
+
`Quality gate failed: ${change.qualityGate.checks.map((c) => `${c.name}=${c.passed}`).join(", ")}`
|
|
14287
|
+
);
|
|
14288
|
+
if (voteResult.outcome === "rejected" || voteResult.outcome === "vetoed") {
|
|
14289
|
+
this._emit({ type: "consensus:reached", goalId: change.id, text: "Change rejected by quality gate" });
|
|
14290
|
+
}
|
|
14291
|
+
}
|
|
14292
|
+
}
|
|
14293
|
+
_onDagEvent(event) {
|
|
14294
|
+
if (event.type === "node:ready") {
|
|
14295
|
+
const node = this.dag.getNode(event.nodeId);
|
|
14296
|
+
if (node) {
|
|
14297
|
+
(this.events?.emit)("autonomous:task_ready", { taskId: event.nodeId, description: node.description });
|
|
14298
|
+
}
|
|
14299
|
+
}
|
|
14300
|
+
if (event.type === "graph:done") {
|
|
14301
|
+
(this.events?.emit)("autonomous:all_done", this.getStats());
|
|
14302
|
+
}
|
|
14303
|
+
}
|
|
14304
|
+
_onSubagentTerminated(e) {
|
|
14305
|
+
const payload = e.payload;
|
|
14306
|
+
const subagentId = payload?.subagentId ?? e.subagentId;
|
|
14307
|
+
const stopReason = payload?.stopReason ?? (payload?.status === "ok" ? "end_turn" : payload?.status ?? "unknown");
|
|
14308
|
+
const tasks = this.auction.getTasksForAgent(subagentId);
|
|
14309
|
+
for (const task of tasks) {
|
|
14310
|
+
this._handledBySubagent.add(task.id);
|
|
14311
|
+
if (stopReason === "end_turn") {
|
|
14312
|
+
void this.auction.complete(task.id, "Subagent completed successfully");
|
|
14313
|
+
this._emit({ type: "task:completed", goalId: task.id, taskId: task.id, text: "Subagent completed successfully" });
|
|
14314
|
+
} else {
|
|
14315
|
+
void this.auction.fail(task.id, `Subagent terminated: ${stopReason}`);
|
|
14316
|
+
this._emit({ type: "goal:failed", goalId: task.id, text: `Subagent terminated: ${stopReason}` });
|
|
14317
|
+
}
|
|
14318
|
+
}
|
|
14319
|
+
}
|
|
14320
|
+
_fleetStatus() {
|
|
14321
|
+
return {
|
|
14322
|
+
running: this.fleetManager?.getFleetStats().running ?? 0,
|
|
14323
|
+
idle: this.fleetManager?.getFleetStats().idle ?? 0,
|
|
14324
|
+
total: this.fleetManager?.getFleetStats().total ?? 0,
|
|
14325
|
+
costSoFar: this.fleetManager?.snapshot()?.total?.cost ?? 0
|
|
14326
|
+
};
|
|
14327
|
+
}
|
|
14328
|
+
_buildVoters() {
|
|
14329
|
+
return [
|
|
14330
|
+
// The coordinator itself casts the quality-gate auto-vote in
|
|
14331
|
+
// _handlePendingChange — it MUST be a registered, eligible voter or
|
|
14332
|
+
// castVote throws "unknown voter" and tears down the run() loop.
|
|
14333
|
+
{ agentId: this.selfAgentId, agentName: "Coordinator", role: "coordinator", weight: 1 },
|
|
14334
|
+
{ agentId: "critic", agentName: "Critic", role: "critic", weight: 2, veto: true },
|
|
14335
|
+
{ agentId: "bug-hunter", agentName: "Bug Hunter", role: "bug-hunter", weight: 1.5 },
|
|
14336
|
+
{ agentId: "security-scanner", agentName: "Security Scanner", role: "security-scanner", weight: 1.5 },
|
|
14337
|
+
{ agentId: "audit-log", agentName: "Audit Log", role: "audit-log", weight: 1 },
|
|
14338
|
+
{ agentId: "refactor-planner", agentName: "Refactor Planner", role: "refactor-planner", weight: 1 }
|
|
14339
|
+
];
|
|
14340
|
+
}
|
|
14341
|
+
_goalToOptions(goals) {
|
|
14342
|
+
return goals.slice(0, 5).map((g, i) => ({
|
|
14343
|
+
id: g.id,
|
|
14344
|
+
label: `[${g.priority}] ${g.title}`,
|
|
14345
|
+
recommended: i === 0
|
|
14346
|
+
}));
|
|
14347
|
+
}
|
|
14348
|
+
_optionToGoal(optionId) {
|
|
14349
|
+
return this.graph.get(optionId);
|
|
14350
|
+
}
|
|
14351
|
+
_dagPriorityToGoal(p) {
|
|
14352
|
+
if (p <= 1) return "critical";
|
|
14353
|
+
if (p <= 2) return "high";
|
|
14354
|
+
if (p <= 4) return "medium";
|
|
14355
|
+
return "low";
|
|
14356
|
+
}
|
|
14357
|
+
async _mailboxBroadcast(msg) {
|
|
14358
|
+
if (!this.mailbox) return;
|
|
14359
|
+
try {
|
|
14360
|
+
await this.mailbox.send({
|
|
14361
|
+
from: this.selfAgentId,
|
|
14362
|
+
to: "*",
|
|
14363
|
+
type: msg.type,
|
|
14364
|
+
subject: msg.subject,
|
|
14365
|
+
body: msg.body,
|
|
14366
|
+
priority: "normal"
|
|
14367
|
+
});
|
|
14368
|
+
} catch {
|
|
14369
|
+
}
|
|
14370
|
+
}
|
|
14371
|
+
/** Emit a CoordinatorEvent to the subscriber (e.g. TUI panel timeline). */
|
|
14372
|
+
_emit(event) {
|
|
14373
|
+
this.onCoordinatorEvent?.(event);
|
|
14374
|
+
}
|
|
14375
|
+
};
|
|
11718
14376
|
|
|
11719
|
-
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, 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, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
|
|
14377
|
+
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, 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, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TaskAuctioneer, TaskDAG, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
|
|
11720
14378
|
//# sourceMappingURL=index.js.map
|
|
11721
14379
|
//# sourceMappingURL=index.js.map
|