@wrongstack/core 0.257.2 → 0.264.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-BrxWHEOm.d.ts → agent-bridge-D8sa1vtv.d.ts} +1 -1
- package/dist/{agent-subagent-runner-US741uBH.d.ts → agent-subagent-runner-c9DLkaas.d.ts} +31 -9
- package/dist/{brain-TjEEwSpw.d.ts → brain-O1IdKPaK.d.ts} +59 -2
- package/dist/{compactor-C5sT4U7I.d.ts → compactor-BBy0rCtB.d.ts} +1 -1
- package/dist/{config-DuAu23zm.d.ts → config-Dz2F3H2K.d.ts} +7 -1
- package/dist/{context-CGdgA0q6.d.ts → context-BGSpZNSE.d.ts} +33 -0
- package/dist/coordination/index.d.ts +1681 -15
- package/dist/coordination/index.js +2826 -405
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +25 -25
- package/dist/defaults/index.js +2258 -1433
- package/dist/defaults/index.js.map +1 -1
- package/dist/dispatcher-types.d-BBeXBQgS.d.ts +66 -0
- package/dist/execution/index.d.ts +15 -15
- package/dist/execution/index.js +502 -398
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +2 -2
- 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-CznHTZqP.d.ts → goal-preamble-DzjFuN3p.d.ts} +21 -9
- package/dist/{goal-store-CV9Yz2X_.d.ts → goal-store-CxWmCGbH.d.ts} +4 -2
- package/dist/{index-CC0Mcm05.d.ts → index-CYIQrXVF.d.ts} +8 -8
- package/dist/{index-CitPrI3a.d.ts → index-CbLSI66_.d.ts} +5 -5
- package/dist/index.d.ts +50 -94
- package/dist/index.js +16009 -12406
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +6 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-CJ4SyAFE.d.ts → llm-selector-DzxuZnNz.d.ts} +2 -2
- package/dist/{mcp-servers-D8YnLaEp.d.ts → mcp-servers-DC4QRPUI.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +6 -1
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-ByZCdFuQ.d.ts → models-registry-B_siPxqN.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-DqTUEAeC.d.ts → multi-agent-coordinator-CK5Jdj9K.d.ts} +2 -2
- package/dist/{null-fleet-bus-B5mfTJXT.d.ts → null-fleet-bus-DgvD4SCO.d.ts} +13 -8
- 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-C0juOszP.d.ts → parallel-eternal-engine-bK0JQBR_.d.ts} +13 -9
- package/dist/{path-resolver-CbkT-RMU.d.ts → path-resolver-BPEDlN38.d.ts} +3 -3
- package/dist/{permission-CwBBpCoF.d.ts → permission-4yvGmMRB.d.ts} +1 -1
- package/dist/{permission-policy-B8rSu908.d.ts → permission-policy-C6XpsBOy.d.ts} +3 -2
- package/dist/{pipeline-JG8XoudC.d.ts → pipeline-CXCeMz8J.d.ts} +58 -3
- package/dist/{plan-templates-DPiQMkBz.d.ts → plan-templates-BvzRBkJc.d.ts} +32 -11
- package/dist/{provider-runner-hM7EXlLI.d.ts → provider-runner-C5aQpDWE.d.ts} +3 -3
- package/dist/{retry-policy-Tg7LXkoK.d.ts → retry-policy-CFhdtRzz.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +59 -31
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-gxtFZYBt.d.ts → secret-vault-CxiVLbt1.d.ts} +1 -1
- package/dist/security/index.d.ts +4 -4
- package/dist/security/index.js +238 -204
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-DWsqVjGf.d.ts → selector-gIuhRTkN.d.ts} +1 -1
- package/dist/{session-event-bridge-BAFWdgQ3.d.ts → session-event-bridge-DkvvrpDt.d.ts} +8 -2
- package/dist/{session-reader-CqRvaL5v.d.ts → session-reader-KdfVwkKP.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 +50 -22
- package/dist/storage/index.js +1654 -525
- 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 +19 -19
- package/dist/types/index.js +711 -694
- 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 +7 -67
- package/dist/utils/index.js +17 -5
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/output-standards/SKILL.md +14 -9
- package/skills/output-standards/SKILL.save.md +3 -2
- package/dist/package-outdated-watcher-BSgR_kK-.d.ts +0 -581
|
@@ -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
|
}
|
|
@@ -1409,7 +1527,12 @@ Working rules:
|
|
|
1409
1527
|
6. Never claim a subagent's work as your own without verifying it. If a
|
|
1410
1528
|
result looks wrong, ask_subagent for clarification before passing it
|
|
1411
1529
|
to the user.
|
|
1412
|
-
7.
|
|
1530
|
+
7. **Act on subagent mail immediately**. Subagent messages (result, ask,
|
|
1531
|
+
assign, note) are injected inline before every step \u2014 even mid-task.
|
|
1532
|
+
When you see one, address it before continuing: reply to asks, factor
|
|
1533
|
+
in results, act on assignments. Use \`mailbox action=ack\` to mark
|
|
1534
|
+
completed messages.
|
|
1535
|
+
8. Wind down when satisfied. When the results are good enough, call
|
|
1413
1536
|
work_complete \u2014 no new subagents will spawn and queued tasks complete
|
|
1414
1537
|
as aborted. Running subagents finish naturally. Call terminate_subagent
|
|
1415
1538
|
only for ones you need to stop immediately.`;
|
|
@@ -1426,6 +1549,13 @@ Bridge contract:
|
|
|
1426
1549
|
structured, and self-contained \u2014 assume the Director will paste your
|
|
1427
1550
|
output into its own context.
|
|
1428
1551
|
|
|
1552
|
+
CRITICAL CONSTRAINT \u2014 NO FURTHER DELEGATION:
|
|
1553
|
+
- You MUST NOT call the \`delegate\` tool or attempt to spawn subagents.
|
|
1554
|
+
- You MUST NOT use \`spawn_subagent\`, \`assign_task\`, or any equivalent.
|
|
1555
|
+
- Your role is to execute the assigned task yourself, not to orchestrate.
|
|
1556
|
+
- If a subtask is too complex, report back to the Director with what you
|
|
1557
|
+
found and let the Director decide how to decompose.
|
|
1558
|
+
|
|
1429
1559
|
Inter-agent mailbox (if you have the \`mail_send\`/\`mail_inbox\`/\`mailbox\` tools):
|
|
1430
1560
|
- You are part of a project-wide fleet that may span other terminals and
|
|
1431
1561
|
WebUIs. Your mailbox identity is \`<your-name>@<session-tag>\` (unique
|
|
@@ -1440,7 +1570,12 @@ Inter-agent mailbox (if you have the \`mail_send\`/\`mail_inbox\`/\`mailbox\` to
|
|
|
1440
1570
|
their exact id instead of doing everything yourself. Discover ids with
|
|
1441
1571
|
\`mailbox action=online\`.
|
|
1442
1572
|
- Answer your mail: reply to the sender's exact \`from\` id. When done with
|
|
1443
|
-
an assigned task, post a \`result\` back to whoever assigned it
|
|
1573
|
+
an assigned task, post a \`result\` back to whoever assigned it.
|
|
1574
|
+
- **Mail to the leader is always seen**: when you send \`ask\`, \`result\`,
|
|
1575
|
+
or \`assign\` to the director/leader, the message is injected inline into
|
|
1576
|
+
the leader's conversation before their next step \u2014 even if the leader is
|
|
1577
|
+
mid-task. Use \`mail_send\` to reliably reach the leader instead of
|
|
1578
|
+
waiting for them to check in.`;
|
|
1444
1579
|
function composeDirectorPrompt(parts = {}) {
|
|
1445
1580
|
const sections = [];
|
|
1446
1581
|
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
@@ -4312,7 +4447,7 @@ function makeSpawnTool(director, roster) {
|
|
|
4312
4447
|
if (err instanceof FleetCostCapError) {
|
|
4313
4448
|
return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
|
|
4314
4449
|
}
|
|
4315
|
-
return { error:
|
|
4450
|
+
return { error: toErrorMessage(err) };
|
|
4316
4451
|
}
|
|
4317
4452
|
}
|
|
4318
4453
|
};
|
|
@@ -4394,7 +4529,7 @@ function makeAskTool(director) {
|
|
|
4394
4529
|
_hint: "Response was large and stored. Use ask_result with the key to retrieve it."
|
|
4395
4530
|
};
|
|
4396
4531
|
} catch (err) {
|
|
4397
|
-
return { ok: false, error:
|
|
4532
|
+
return { ok: false, error: toErrorMessage(err) };
|
|
4398
4533
|
}
|
|
4399
4534
|
}
|
|
4400
4535
|
};
|
|
@@ -4620,7 +4755,7 @@ function makeCollabDebugTool(director) {
|
|
|
4620
4755
|
evaluations: report.evaluations
|
|
4621
4756
|
};
|
|
4622
4757
|
} catch (err) {
|
|
4623
|
-
const msg =
|
|
4758
|
+
const msg = toErrorMessage(err);
|
|
4624
4759
|
return { error: "collab_debug failed: " + msg };
|
|
4625
4760
|
}
|
|
4626
4761
|
}
|
|
@@ -5296,11 +5431,6 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5296
5431
|
}
|
|
5297
5432
|
};
|
|
5298
5433
|
|
|
5299
|
-
// src/utils/string.ts
|
|
5300
|
-
function truncate(s, max) {
|
|
5301
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
5302
|
-
}
|
|
5303
|
-
|
|
5304
5434
|
// src/types/provider.ts
|
|
5305
5435
|
var ProviderError = class extends WrongStackError {
|
|
5306
5436
|
status;
|
|
@@ -5380,7 +5510,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
5380
5510
|
const baseMessage2 = err.describe();
|
|
5381
5511
|
return providerErrorToSubagentError(err, baseMessage2, cause);
|
|
5382
5512
|
}
|
|
5383
|
-
const baseMessage =
|
|
5513
|
+
const baseMessage = toErrorMessage(err);
|
|
5384
5514
|
if (err instanceof BudgetExceededError) {
|
|
5385
5515
|
const map = {
|
|
5386
5516
|
iterations: "budget_iterations",
|
|
@@ -7366,7 +7496,7 @@ var Director = class _Director {
|
|
|
7366
7496
|
})),
|
|
7367
7497
|
usage: this.usage.snapshot()
|
|
7368
7498
|
};
|
|
7369
|
-
await fsp6.mkdir(
|
|
7499
|
+
await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
|
|
7370
7500
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
7371
7501
|
return this.manifestPath;
|
|
7372
7502
|
}
|
|
@@ -7416,7 +7546,7 @@ var Director = class _Director {
|
|
|
7416
7546
|
* listener for structured collection, and never affects exit code.
|
|
7417
7547
|
*/
|
|
7418
7548
|
logShutdownError(phase, err) {
|
|
7419
|
-
const detail =
|
|
7549
|
+
const detail = toErrorMessage(err);
|
|
7420
7550
|
process.emitWarning(
|
|
7421
7551
|
`Director shutdown phase "${phase}" failed: ${detail}`,
|
|
7422
7552
|
"DirectorShutdownWarning"
|
|
@@ -7580,7 +7710,7 @@ var Director = class _Director {
|
|
|
7580
7710
|
*/
|
|
7581
7711
|
async readSession(subagentId, tail) {
|
|
7582
7712
|
if (!this.sessionsRoot) return null;
|
|
7583
|
-
const filePath =
|
|
7713
|
+
const filePath = path5.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
|
|
7584
7714
|
let raw;
|
|
7585
7715
|
try {
|
|
7586
7716
|
raw = await fsp6.readFile(filePath, "utf8");
|
|
@@ -8009,7 +8139,7 @@ function createDelegateTool(opts) {
|
|
|
8009
8139
|
summary
|
|
8010
8140
|
};
|
|
8011
8141
|
} catch (err) {
|
|
8012
|
-
const message =
|
|
8142
|
+
const message = toErrorMessage(err);
|
|
8013
8143
|
opts.events?.emit("delegate.completed", {
|
|
8014
8144
|
target,
|
|
8015
8145
|
task: i.task,
|
|
@@ -8110,13 +8240,13 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
8110
8240
|
if (!opts.sessionsRoot) return void 0;
|
|
8111
8241
|
const candidates = [];
|
|
8112
8242
|
if (opts.directorRunId) {
|
|
8113
|
-
candidates.push(
|
|
8243
|
+
candidates.push(path5.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
|
|
8114
8244
|
} else {
|
|
8115
8245
|
try {
|
|
8116
8246
|
const entries = await fsp6.readdir(opts.sessionsRoot, { withFileTypes: true });
|
|
8117
8247
|
for (const entry of entries) {
|
|
8118
8248
|
if (entry.isDirectory()) {
|
|
8119
|
-
candidates.push(
|
|
8249
|
+
candidates.push(path5.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
|
|
8120
8250
|
}
|
|
8121
8251
|
}
|
|
8122
8252
|
} catch {
|
|
@@ -8161,6 +8291,20 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
8161
8291
|
}
|
|
8162
8292
|
|
|
8163
8293
|
// src/coordination/agent-subagent-runner.ts
|
|
8294
|
+
function withDisabledToolFiltering(factory) {
|
|
8295
|
+
return async (config) => {
|
|
8296
|
+
const result = await factory(config);
|
|
8297
|
+
const disabled = config.disabledTools ?? [];
|
|
8298
|
+
if (disabled.length === 0) return result;
|
|
8299
|
+
const registry = result.agent.tools;
|
|
8300
|
+
if (registry && typeof registry.unregister === "function") {
|
|
8301
|
+
for (const toolName of disabled) {
|
|
8302
|
+
registry.unregister(toolName);
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8305
|
+
return result;
|
|
8306
|
+
};
|
|
8307
|
+
}
|
|
8164
8308
|
function makeAgentSubagentRunner(opts) {
|
|
8165
8309
|
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
8166
8310
|
return async (task, ctx) => {
|
|
@@ -8358,100 +8502,6 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8358
8502
|
function defaultFormatTaskInput(task) {
|
|
8359
8503
|
return task.description ?? "";
|
|
8360
8504
|
}
|
|
8361
|
-
|
|
8362
|
-
// src/utils/message-invariants.ts
|
|
8363
|
-
function repairToolUseAdjacency(messages) {
|
|
8364
|
-
const removedToolUses = [];
|
|
8365
|
-
const removedToolResults = [];
|
|
8366
|
-
let removedMessages = 0;
|
|
8367
|
-
let changed = false;
|
|
8368
|
-
const out = [];
|
|
8369
|
-
for (let i = 0; i < messages.length; i++) {
|
|
8370
|
-
const original = expectDefined(messages[i]);
|
|
8371
|
-
let msg = original;
|
|
8372
|
-
if (hasToolUse(msg)) {
|
|
8373
|
-
const nextIds = toolResultIds(messages[i + 1]);
|
|
8374
|
-
const filtered = mapContent(msg, (blocks) => {
|
|
8375
|
-
const next = [];
|
|
8376
|
-
for (const block of blocks) {
|
|
8377
|
-
if (block.type === "tool_use" && !nextIds.has(block.id)) {
|
|
8378
|
-
removedToolUses.push(block.id);
|
|
8379
|
-
changed = true;
|
|
8380
|
-
continue;
|
|
8381
|
-
}
|
|
8382
|
-
next.push(block);
|
|
8383
|
-
}
|
|
8384
|
-
return next;
|
|
8385
|
-
});
|
|
8386
|
-
msg = filtered ?? msg;
|
|
8387
|
-
}
|
|
8388
|
-
if (hasToolResult(msg)) {
|
|
8389
|
-
const allowed = toolUseIds(out[out.length - 1]);
|
|
8390
|
-
const filtered = mapContent(msg, (blocks) => {
|
|
8391
|
-
const next = [];
|
|
8392
|
-
for (const block of blocks) {
|
|
8393
|
-
if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
|
|
8394
|
-
removedToolResults.push(block.tool_use_id);
|
|
8395
|
-
changed = true;
|
|
8396
|
-
continue;
|
|
8397
|
-
}
|
|
8398
|
-
next.push(block);
|
|
8399
|
-
}
|
|
8400
|
-
return next;
|
|
8401
|
-
});
|
|
8402
|
-
msg = filtered ?? msg;
|
|
8403
|
-
}
|
|
8404
|
-
if (isEmptyMessage(msg)) {
|
|
8405
|
-
removedMessages++;
|
|
8406
|
-
changed = true;
|
|
8407
|
-
continue;
|
|
8408
|
-
}
|
|
8409
|
-
out.push(msg);
|
|
8410
|
-
}
|
|
8411
|
-
return {
|
|
8412
|
-
messages: changed ? out : messages,
|
|
8413
|
-
report: { changed, removedToolUses, removedToolResults, removedMessages }
|
|
8414
|
-
};
|
|
8415
|
-
}
|
|
8416
|
-
function hasToolUse(msg) {
|
|
8417
|
-
return contentBlocks(msg).some((b) => b.type === "tool_use");
|
|
8418
|
-
}
|
|
8419
|
-
function hasToolResult(msg) {
|
|
8420
|
-
return contentBlocks(msg).some((b) => b.type === "tool_result");
|
|
8421
|
-
}
|
|
8422
|
-
function toolUseIds(msg) {
|
|
8423
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8424
|
-
if (!msg || msg.role !== "assistant") return ids;
|
|
8425
|
-
for (const block of contentBlocks(msg)) {
|
|
8426
|
-
if (block.type === "tool_use") ids.add(block.id);
|
|
8427
|
-
}
|
|
8428
|
-
return ids;
|
|
8429
|
-
}
|
|
8430
|
-
function toolResultIds(msg) {
|
|
8431
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8432
|
-
if (!msg || msg.role !== "user") return ids;
|
|
8433
|
-
for (const block of contentBlocks(msg)) {
|
|
8434
|
-
if (block.type === "tool_result") ids.add(block.tool_use_id);
|
|
8435
|
-
}
|
|
8436
|
-
return ids;
|
|
8437
|
-
}
|
|
8438
|
-
function contentBlocks(msg) {
|
|
8439
|
-
return msg && Array.isArray(msg.content) ? msg.content : [];
|
|
8440
|
-
}
|
|
8441
|
-
function mapContent(msg, fn) {
|
|
8442
|
-
if (!Array.isArray(msg.content)) return msg;
|
|
8443
|
-
const next = fn(msg.content);
|
|
8444
|
-
if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
|
|
8445
|
-
return msg;
|
|
8446
|
-
}
|
|
8447
|
-
return { ...msg, content: next };
|
|
8448
|
-
}
|
|
8449
|
-
function isEmptyMessage(msg) {
|
|
8450
|
-
if (typeof msg.content === "string") return msg.content.trim().length === 0;
|
|
8451
|
-
return msg.content.length === 0;
|
|
8452
|
-
}
|
|
8453
|
-
|
|
8454
|
-
// src/storage/session-store.ts
|
|
8455
8505
|
function sanitizeModel(model) {
|
|
8456
8506
|
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
8457
8507
|
}
|
|
@@ -8471,13 +8521,47 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8471
8521
|
this.events = opts.events;
|
|
8472
8522
|
this.secretScrubber = opts.secretScrubber;
|
|
8473
8523
|
}
|
|
8524
|
+
// ── Storage event helpers ───────────────────────────────────────────────────
|
|
8525
|
+
emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
|
|
8526
|
+
this.events?.emit("storage.read", {
|
|
8527
|
+
sessionId,
|
|
8528
|
+
store: "session",
|
|
8529
|
+
filePath,
|
|
8530
|
+
operation,
|
|
8531
|
+
outcome,
|
|
8532
|
+
durationMs,
|
|
8533
|
+
...error !== void 0 ? { error } : {}
|
|
8534
|
+
});
|
|
8535
|
+
}
|
|
8536
|
+
emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
|
|
8537
|
+
this.events?.emit("storage.write", {
|
|
8538
|
+
sessionId,
|
|
8539
|
+
store: "session",
|
|
8540
|
+
filePath,
|
|
8541
|
+
operation,
|
|
8542
|
+
outcome,
|
|
8543
|
+
durationMs,
|
|
8544
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
8545
|
+
...error !== void 0 ? { error } : {}
|
|
8546
|
+
});
|
|
8547
|
+
}
|
|
8548
|
+
emitError(sessionId, filePath, operation, error, recoverable) {
|
|
8549
|
+
this.events?.emit("storage.error", {
|
|
8550
|
+
sessionId,
|
|
8551
|
+
store: "session",
|
|
8552
|
+
filePath,
|
|
8553
|
+
operation,
|
|
8554
|
+
error,
|
|
8555
|
+
recoverable
|
|
8556
|
+
});
|
|
8557
|
+
}
|
|
8474
8558
|
/** Absolute path to the session index file. */
|
|
8475
8559
|
get indexFile() {
|
|
8476
|
-
return
|
|
8560
|
+
return path5.join(this.dir, "_index.jsonl");
|
|
8477
8561
|
}
|
|
8478
8562
|
/** Join session ID to its absolute path within the store directory. */
|
|
8479
8563
|
sessionPath(id, ext) {
|
|
8480
|
-
return
|
|
8564
|
+
return path5.join(this.dir, `${id}${ext}`);
|
|
8481
8565
|
}
|
|
8482
8566
|
/**
|
|
8483
8567
|
* Ensure the directory implied by the session ID exists. When the ID
|
|
@@ -8485,7 +8569,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8485
8569
|
* subdirectory so sessions group naturally by day.
|
|
8486
8570
|
*/
|
|
8487
8571
|
async ensureShardDir(id) {
|
|
8488
|
-
const dirPath =
|
|
8572
|
+
const dirPath = path5.dirname(path5.join(this.dir, id));
|
|
8489
8573
|
await ensureDir(dirPath);
|
|
8490
8574
|
return dirPath;
|
|
8491
8575
|
}
|
|
@@ -8493,23 +8577,27 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8493
8577
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8494
8578
|
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
8495
8579
|
const shardDir = await this.ensureShardDir(id);
|
|
8496
|
-
const file =
|
|
8580
|
+
const file = path5.join(shardDir, `${path5.basename(id)}.jsonl`);
|
|
8581
|
+
const t0 = Date.now();
|
|
8497
8582
|
let handle;
|
|
8498
8583
|
try {
|
|
8499
8584
|
handle = await fsp6.open(file, "a", 384);
|
|
8500
8585
|
} catch (err) {
|
|
8586
|
+
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
8501
8587
|
throw new Error(
|
|
8502
|
-
`Failed to open session file: ${
|
|
8588
|
+
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
8503
8589
|
{ cause: err }
|
|
8504
8590
|
);
|
|
8505
8591
|
}
|
|
8506
8592
|
try {
|
|
8507
|
-
|
|
8593
|
+
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
8508
8594
|
dir: shardDir,
|
|
8509
8595
|
filePath: file,
|
|
8510
8596
|
secretScrubber: this.secretScrubber,
|
|
8511
8597
|
onClose: (s) => this.appendToIndex(s)
|
|
8512
8598
|
});
|
|
8599
|
+
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
8600
|
+
return writer;
|
|
8513
8601
|
} catch (err) {
|
|
8514
8602
|
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
8515
8603
|
level: "warn",
|
|
@@ -8517,18 +8605,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8517
8605
|
message: e instanceof Error ? e.message : String(e),
|
|
8518
8606
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8519
8607
|
})));
|
|
8608
|
+
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
8520
8609
|
throw err;
|
|
8521
8610
|
}
|
|
8522
8611
|
}
|
|
8523
8612
|
async resume(id) {
|
|
8524
8613
|
const file = this.sessionPath(id, ".jsonl");
|
|
8614
|
+
const t0 = Date.now();
|
|
8525
8615
|
const data = await this.load(id);
|
|
8526
8616
|
let handle;
|
|
8527
8617
|
try {
|
|
8528
8618
|
handle = await fsp6.open(file, "a", 384);
|
|
8529
8619
|
} catch (err) {
|
|
8620
|
+
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
8530
8621
|
throw new Error(
|
|
8531
|
-
`Failed to open session "${id}" for append: ${
|
|
8622
|
+
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
8532
8623
|
{ cause: err }
|
|
8533
8624
|
);
|
|
8534
8625
|
}
|
|
@@ -8548,12 +8639,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8548
8639
|
// Shard directory (sessions/<date>/) — must match create() so the
|
|
8549
8640
|
// .summary.json sidecar lands next to the JSONL instead of the
|
|
8550
8641
|
// sessions root (where summaryFor() would never find it).
|
|
8551
|
-
dir:
|
|
8642
|
+
dir: path5.dirname(file),
|
|
8552
8643
|
filePath: file,
|
|
8553
8644
|
secretScrubber: this.secretScrubber,
|
|
8554
8645
|
onClose: (s) => this.appendToIndex(s)
|
|
8555
8646
|
}
|
|
8556
8647
|
);
|
|
8648
|
+
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
8557
8649
|
return { writer, data };
|
|
8558
8650
|
} catch (err) {
|
|
8559
8651
|
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
@@ -8562,27 +8654,39 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8562
8654
|
message: e instanceof Error ? e.message : String(e),
|
|
8563
8655
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8564
8656
|
})));
|
|
8657
|
+
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
8565
8658
|
throw err;
|
|
8566
8659
|
}
|
|
8567
8660
|
}
|
|
8568
8661
|
async load(id) {
|
|
8569
8662
|
const file = this.sessionPath(id, ".jsonl");
|
|
8570
|
-
const
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8663
|
+
const t0 = Date.now();
|
|
8664
|
+
let outcome = "success";
|
|
8665
|
+
let errorMsg;
|
|
8666
|
+
try {
|
|
8667
|
+
const raw = await fsp6.readFile(file, "utf8");
|
|
8668
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
8669
|
+
const events = [];
|
|
8670
|
+
for (const line of lines) {
|
|
8671
|
+
try {
|
|
8672
|
+
const parsed = JSON.parse(line);
|
|
8673
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
8674
|
+
events.push(parsed);
|
|
8675
|
+
}
|
|
8676
|
+
} catch {
|
|
8578
8677
|
}
|
|
8579
|
-
} catch {
|
|
8580
8678
|
}
|
|
8679
|
+
const meta = this.metaFromEvents(id, events);
|
|
8680
|
+
const { messages, usage } = this.replay(events, id);
|
|
8681
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
8682
|
+
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
8683
|
+
} catch (err) {
|
|
8684
|
+
outcome = "failure";
|
|
8685
|
+
errorMsg = toErrorMessage(err);
|
|
8686
|
+
throw err;
|
|
8687
|
+
} finally {
|
|
8688
|
+
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
8581
8689
|
}
|
|
8582
|
-
const meta = this.metaFromEvents(id, events);
|
|
8583
|
-
const { messages, usage } = this.replay(events, id);
|
|
8584
|
-
const toolCallEnds = extractToolCallEnds(events);
|
|
8585
|
-
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
8586
8690
|
}
|
|
8587
8691
|
async list(limit = 20) {
|
|
8588
8692
|
try {
|
|
@@ -8649,12 +8753,22 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8649
8753
|
* (keep latest per session), and rewrite. Atomic via temp+rename.
|
|
8650
8754
|
*/
|
|
8651
8755
|
async compactIndex() {
|
|
8652
|
-
const
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8756
|
+
const t0 = Date.now();
|
|
8757
|
+
let outcome = "success";
|
|
8758
|
+
let errorMsg;
|
|
8759
|
+
try {
|
|
8760
|
+
const entries = await this.readIndex();
|
|
8761
|
+
if (entries.length === 0) return;
|
|
8762
|
+
const tmp = `${this.indexFile}.compact.tmp`;
|
|
8763
|
+
const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
8764
|
+
await fsp6.writeFile(tmp, lines, "utf8");
|
|
8765
|
+
await fsp6.rename(tmp, this.indexFile);
|
|
8766
|
+
} catch (err) {
|
|
8767
|
+
outcome = "failure";
|
|
8768
|
+
errorMsg = toErrorMessage(err);
|
|
8769
|
+
} finally {
|
|
8770
|
+
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
8771
|
+
}
|
|
8658
8772
|
}
|
|
8659
8773
|
/**
|
|
8660
8774
|
* Read the index file and return deduplicated session summaries.
|
|
@@ -8719,7 +8833,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8719
8833
|
continue;
|
|
8720
8834
|
if (entry.isDirectory()) {
|
|
8721
8835
|
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
8722
|
-
ids.push(...await this.collectSessionIds(
|
|
8836
|
+
ids.push(...await this.collectSessionIds(path5.join(dir, entry.name), childPrefix, depth + 1));
|
|
8723
8837
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
8724
8838
|
if (entry.name === "_index.jsonl") continue;
|
|
8725
8839
|
const base = entry.name.replace(/\.jsonl$/, "");
|
|
@@ -8730,22 +8844,31 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8730
8844
|
}
|
|
8731
8845
|
async summaryFor(id) {
|
|
8732
8846
|
const manifest = this.sessionPath(id, ".summary.json");
|
|
8847
|
+
const t0 = Date.now();
|
|
8848
|
+
let outcome = "success";
|
|
8849
|
+
let errorMsg;
|
|
8733
8850
|
try {
|
|
8734
8851
|
const raw = await fsp6.readFile(manifest, "utf8");
|
|
8852
|
+
this.emitRead(id, manifest, "summary", "success", Date.now() - t0);
|
|
8735
8853
|
return JSON.parse(raw);
|
|
8736
8854
|
} catch {
|
|
8737
8855
|
const full = this.sessionPath(id, ".jsonl");
|
|
8738
|
-
const
|
|
8739
|
-
const summary = await this.summarize(id,
|
|
8856
|
+
const stat6 = await fsp6.stat(full);
|
|
8857
|
+
const summary = await this.summarize(id, stat6.mtime.toISOString());
|
|
8740
8858
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
8859
|
+
const msg = toErrorMessage(err);
|
|
8860
|
+
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
8741
8861
|
console.warn(JSON.stringify({
|
|
8742
8862
|
level: "warn",
|
|
8743
8863
|
event: "session_store.manifest_write_failed",
|
|
8744
8864
|
sessionId: id,
|
|
8745
|
-
message:
|
|
8865
|
+
message: msg,
|
|
8746
8866
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8747
8867
|
}));
|
|
8748
8868
|
});
|
|
8869
|
+
outcome = "failure";
|
|
8870
|
+
errorMsg = "summary fallback \u2014 manifest rebuilt";
|
|
8871
|
+
this.emitRead(id, manifest, "summary", outcome, Date.now() - t0, errorMsg);
|
|
8749
8872
|
return summary;
|
|
8750
8873
|
}
|
|
8751
8874
|
}
|
|
@@ -8761,14 +8884,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8761
8884
|
async deleteSession(id) {
|
|
8762
8885
|
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
8763
8886
|
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
8764
|
-
const shardDir =
|
|
8765
|
-
const base =
|
|
8766
|
-
const sessDir =
|
|
8887
|
+
const shardDir = path5.dirname(path5.join(this.dir, id));
|
|
8888
|
+
const base = path5.basename(id);
|
|
8889
|
+
const sessDir = path5.join(shardDir, base);
|
|
8767
8890
|
const deletions = [
|
|
8768
8891
|
fsp6.unlink(jsonlPath),
|
|
8769
8892
|
fsp6.unlink(summaryPath),
|
|
8770
|
-
fsp6.unlink(
|
|
8771
|
-
fsp6.unlink(
|
|
8893
|
+
fsp6.unlink(path5.join(shardDir, `${base}.plan.json`)),
|
|
8894
|
+
fsp6.unlink(path5.join(shardDir, `${base}.todos.json`))
|
|
8772
8895
|
];
|
|
8773
8896
|
const results = await Promise.allSettled(deletions);
|
|
8774
8897
|
for (const r of results) {
|
|
@@ -8790,7 +8913,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8790
8913
|
level: "warn",
|
|
8791
8914
|
event: "session_store.rmdir_failed",
|
|
8792
8915
|
sessionId: id,
|
|
8793
|
-
message:
|
|
8916
|
+
message: toErrorMessage(err),
|
|
8794
8917
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8795
8918
|
}));
|
|
8796
8919
|
});
|
|
@@ -8804,17 +8927,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8804
8927
|
let deleted = 0;
|
|
8805
8928
|
let activeSessionId = null;
|
|
8806
8929
|
try {
|
|
8807
|
-
const raw = await fsp6.readFile(
|
|
8930
|
+
const raw = await fsp6.readFile(path5.join(this.dir, "active.json"), "utf8");
|
|
8808
8931
|
const active = JSON.parse(raw);
|
|
8809
8932
|
activeSessionId = active.sessionId ?? null;
|
|
8810
8933
|
} catch {
|
|
8811
8934
|
}
|
|
8812
8935
|
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
8813
8936
|
const pruneFile = async (dir, name, prefix) => {
|
|
8814
|
-
const jsonlPath =
|
|
8937
|
+
const jsonlPath = path5.join(dir, name);
|
|
8815
8938
|
try {
|
|
8816
|
-
const
|
|
8817
|
-
if (
|
|
8939
|
+
const stat6 = await fsp6.stat(jsonlPath);
|
|
8940
|
+
if (stat6.mtimeMs >= cutoff) return;
|
|
8818
8941
|
} catch {
|
|
8819
8942
|
return;
|
|
8820
8943
|
}
|
|
@@ -8831,7 +8954,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8831
8954
|
continue;
|
|
8832
8955
|
}
|
|
8833
8956
|
if (!entry.isDirectory()) continue;
|
|
8834
|
-
const dateDir =
|
|
8957
|
+
const dateDir = path5.join(this.dir, entry.name);
|
|
8835
8958
|
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
8836
8959
|
for (const file of files) {
|
|
8837
8960
|
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
@@ -8843,7 +8966,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8843
8966
|
}
|
|
8844
8967
|
for (const entry of entries) {
|
|
8845
8968
|
if (!entry.isDirectory()) continue;
|
|
8846
|
-
const dateDir =
|
|
8969
|
+
const dateDir = path5.join(this.dir, entry.name);
|
|
8847
8970
|
try {
|
|
8848
8971
|
const remaining = await fsp6.readdir(dateDir);
|
|
8849
8972
|
if (remaining.length === 0) {
|
|
@@ -9011,14 +9134,14 @@ function extractToolCallEnds(events) {
|
|
|
9011
9134
|
return result;
|
|
9012
9135
|
}
|
|
9013
9136
|
var FileSessionWriter = class _FileSessionWriter {
|
|
9014
|
-
constructor(id, handle, startedAt, meta, events, opts = {}) {
|
|
9137
|
+
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
9015
9138
|
this.id = id;
|
|
9016
9139
|
this.handle = handle;
|
|
9017
9140
|
this.startedAt = startedAt;
|
|
9018
9141
|
this.meta = meta;
|
|
9019
9142
|
this.events = events;
|
|
9020
9143
|
this.resumed = opts.resumed ?? false;
|
|
9021
|
-
this.manifestFile = opts.dir ?
|
|
9144
|
+
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
9022
9145
|
this.filePath = opts.filePath ?? "";
|
|
9023
9146
|
this.secretScrubber = opts.secretScrubber;
|
|
9024
9147
|
this.onCloseCb = opts.onClose;
|
|
@@ -9030,6 +9153,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9030
9153
|
provider: meta.provider ?? "unknown",
|
|
9031
9154
|
tokenTotal: 0
|
|
9032
9155
|
};
|
|
9156
|
+
this.traceId = traceId;
|
|
9033
9157
|
}
|
|
9034
9158
|
id;
|
|
9035
9159
|
handle;
|
|
@@ -9062,6 +9186,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9062
9186
|
lastAppendWarnAt = 0;
|
|
9063
9187
|
secretScrubber;
|
|
9064
9188
|
onCloseCb;
|
|
9189
|
+
/** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
|
|
9190
|
+
traceId;
|
|
9065
9191
|
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
9066
9192
|
//
|
|
9067
9193
|
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
@@ -9215,9 +9341,14 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9215
9341
|
const eventCount = this.writeBuffer.length;
|
|
9216
9342
|
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
9217
9343
|
this.writeBuffer = [];
|
|
9344
|
+
const t0 = Date.now();
|
|
9345
|
+
let outcome = "success";
|
|
9346
|
+
let errorMsg;
|
|
9218
9347
|
try {
|
|
9219
9348
|
await this.enqueueWrite(batch);
|
|
9220
9349
|
} catch (err) {
|
|
9350
|
+
outcome = "failure";
|
|
9351
|
+
errorMsg = toErrorMessage(err);
|
|
9221
9352
|
this.appendFailCount += eventCount;
|
|
9222
9353
|
const now = Date.now();
|
|
9223
9354
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
@@ -9225,12 +9356,24 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9225
9356
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
9226
9357
|
console.warn(
|
|
9227
9358
|
"[session] flush failed:",
|
|
9228
|
-
|
|
9359
|
+
toErrorMessage(err),
|
|
9229
9360
|
tail
|
|
9230
9361
|
);
|
|
9231
9362
|
this.lastAppendWarnAt = now;
|
|
9232
9363
|
this.appendFailCount = 0;
|
|
9233
9364
|
}
|
|
9365
|
+
} finally {
|
|
9366
|
+
this.events?.emit("storage.write", {
|
|
9367
|
+
sessionId: this.id,
|
|
9368
|
+
store: "session",
|
|
9369
|
+
filePath: this.filePath,
|
|
9370
|
+
operation: "flush",
|
|
9371
|
+
outcome,
|
|
9372
|
+
durationMs: Date.now() - t0,
|
|
9373
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
9374
|
+
...eventCount !== void 0 ? { eventCount } : {},
|
|
9375
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9376
|
+
});
|
|
9234
9377
|
}
|
|
9235
9378
|
}
|
|
9236
9379
|
observeForSummary(event) {
|
|
@@ -9296,14 +9439,46 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9296
9439
|
outcome: this.outcome ?? "completed"
|
|
9297
9440
|
};
|
|
9298
9441
|
if (this.manifestFile) {
|
|
9442
|
+
const t0 = Date.now();
|
|
9443
|
+
let outcome = "success";
|
|
9444
|
+
let errorMsg;
|
|
9299
9445
|
try {
|
|
9300
9446
|
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
9301
|
-
} catch {
|
|
9447
|
+
} catch (err) {
|
|
9448
|
+
outcome = "failure";
|
|
9449
|
+
errorMsg = toErrorMessage(err);
|
|
9450
|
+
} finally {
|
|
9451
|
+
this.events?.emit("storage.write", {
|
|
9452
|
+
sessionId: this.id,
|
|
9453
|
+
store: "session",
|
|
9454
|
+
filePath: this.manifestFile,
|
|
9455
|
+
operation: "close",
|
|
9456
|
+
outcome,
|
|
9457
|
+
durationMs: Date.now() - t0,
|
|
9458
|
+
...errorMsg !== void 0 ? { error: errorMsg } : {},
|
|
9459
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9460
|
+
});
|
|
9302
9461
|
}
|
|
9303
9462
|
}
|
|
9463
|
+
const idxT0 = Date.now();
|
|
9464
|
+
let idxOutcome = "success";
|
|
9465
|
+
let idxError;
|
|
9304
9466
|
try {
|
|
9305
9467
|
await this.onCloseCb?.(this.summary);
|
|
9306
|
-
} catch {
|
|
9468
|
+
} catch (err) {
|
|
9469
|
+
idxOutcome = "failure";
|
|
9470
|
+
idxError = toErrorMessage(err);
|
|
9471
|
+
} finally {
|
|
9472
|
+
this.events?.emit("storage.write", {
|
|
9473
|
+
sessionId: this.summary.id,
|
|
9474
|
+
store: "session",
|
|
9475
|
+
filePath: this.filePath,
|
|
9476
|
+
operation: "index_append",
|
|
9477
|
+
outcome: idxOutcome,
|
|
9478
|
+
durationMs: Date.now() - idxT0,
|
|
9479
|
+
...idxError !== void 0 ? { error: idxError } : {},
|
|
9480
|
+
...this.traceId !== void 0 ? { traceId: this.traceId } : {}
|
|
9481
|
+
});
|
|
9307
9482
|
}
|
|
9308
9483
|
try {
|
|
9309
9484
|
await this.handle.close();
|
|
@@ -9464,13 +9639,14 @@ function userInputTitle(content) {
|
|
|
9464
9639
|
// src/coordination/director-session.ts
|
|
9465
9640
|
function makeDirectorSessionFactory(opts) {
|
|
9466
9641
|
const runId = opts.directorRunId ?? `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-director`;
|
|
9642
|
+
const { traceId } = opts;
|
|
9467
9643
|
let store;
|
|
9468
9644
|
let dir;
|
|
9469
9645
|
if (opts.store) {
|
|
9470
9646
|
store = opts.store;
|
|
9471
|
-
dir = opts.sessionsRoot ?
|
|
9647
|
+
dir = opts.sessionsRoot ? path5.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
9472
9648
|
} else if (opts.sessionsRoot) {
|
|
9473
|
-
dir =
|
|
9649
|
+
dir = path5.join(opts.sessionsRoot, runId);
|
|
9474
9650
|
store = new DefaultSessionStore({ dir });
|
|
9475
9651
|
} else {
|
|
9476
9652
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -9479,12 +9655,16 @@ function makeDirectorSessionFactory(opts) {
|
|
|
9479
9655
|
dir,
|
|
9480
9656
|
directorRunId: runId,
|
|
9481
9657
|
async createSubagentSession({ subagentId, provider, model, title }) {
|
|
9482
|
-
|
|
9658
|
+
const writer = await store.create({
|
|
9483
9659
|
id: subagentId,
|
|
9484
9660
|
title: title ?? subagentId,
|
|
9485
9661
|
provider: provider ?? "unknown",
|
|
9486
9662
|
model: model ?? "unknown"
|
|
9487
9663
|
});
|
|
9664
|
+
if (traceId !== void 0) {
|
|
9665
|
+
writer.traceId = traceId;
|
|
9666
|
+
}
|
|
9667
|
+
return writer;
|
|
9488
9668
|
}
|
|
9489
9669
|
};
|
|
9490
9670
|
}
|
|
@@ -9774,7 +9954,7 @@ var FleetManager = class {
|
|
|
9774
9954
|
})),
|
|
9775
9955
|
usage: this.usage.snapshot()
|
|
9776
9956
|
};
|
|
9777
|
-
await fsp6.mkdir(
|
|
9957
|
+
await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
|
|
9778
9958
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
9779
9959
|
return this.manifestPath;
|
|
9780
9960
|
}
|
|
@@ -9795,7 +9975,7 @@ var FleetManager = class {
|
|
|
9795
9975
|
if (!this.manifestPath) return;
|
|
9796
9976
|
if (this.manifestDebounceMs === 0) {
|
|
9797
9977
|
void this.writeManifest().catch((err) => {
|
|
9798
|
-
const detail =
|
|
9978
|
+
const detail = toErrorMessage(err);
|
|
9799
9979
|
process.emitWarning(
|
|
9800
9980
|
`FleetManager manifest write failed: ${detail}`,
|
|
9801
9981
|
"FleetManagerWarning"
|
|
@@ -9808,7 +9988,7 @@ var FleetManager = class {
|
|
|
9808
9988
|
this.manifestTimer = setTimeout(() => {
|
|
9809
9989
|
this.manifestTimer = null;
|
|
9810
9990
|
void this.writeManifest().catch((err) => {
|
|
9811
|
-
const detail =
|
|
9991
|
+
const detail = toErrorMessage(err);
|
|
9812
9992
|
process.emitWarning(
|
|
9813
9993
|
`FleetManager manifest write failed: ${detail}`,
|
|
9814
9994
|
"FleetManagerWarning"
|
|
@@ -9827,7 +10007,7 @@ var FleetManager = class {
|
|
|
9827
10007
|
this.manifestTimer = null;
|
|
9828
10008
|
}
|
|
9829
10009
|
await this.writeManifest().catch((err) => {
|
|
9830
|
-
const detail =
|
|
10010
|
+
const detail = toErrorMessage(err);
|
|
9831
10011
|
process.emitWarning(
|
|
9832
10012
|
`FleetManager manifest write failed: ${detail}`,
|
|
9833
10013
|
"FleetManagerWarning"
|
|
@@ -9922,7 +10102,7 @@ var LINE_SEPARATOR = "\n";
|
|
|
9922
10102
|
var DefaultMailbox = class {
|
|
9923
10103
|
filePath;
|
|
9924
10104
|
constructor(sessionDir) {
|
|
9925
|
-
this.filePath =
|
|
10105
|
+
this.filePath = path5.join(sessionDir, MAILBOX_FILE);
|
|
9926
10106
|
}
|
|
9927
10107
|
get mailboxPath() {
|
|
9928
10108
|
return this.filePath;
|
|
@@ -9947,7 +10127,7 @@ var DefaultMailbox = class {
|
|
|
9947
10127
|
taskContext: input.taskContext
|
|
9948
10128
|
};
|
|
9949
10129
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
9950
|
-
await fsp6.mkdir(
|
|
10130
|
+
await fsp6.mkdir(path5.dirname(this.filePath), { recursive: true });
|
|
9951
10131
|
await withFileLock(this.filePath, async () => {
|
|
9952
10132
|
await fsp6.appendFile(this.filePath, line, "utf8");
|
|
9953
10133
|
});
|
|
@@ -9987,29 +10167,43 @@ var DefaultMailbox = class {
|
|
|
9987
10167
|
}
|
|
9988
10168
|
// ── Ack ───────────────────────────────────────────────────────────────
|
|
9989
10169
|
async ack(input) {
|
|
9990
|
-
|
|
10170
|
+
const updated = await this.ackMany({ acks: [input] });
|
|
10171
|
+
return updated.length > 0 ? updated[0] : null;
|
|
10172
|
+
}
|
|
10173
|
+
async ackMany(input) {
|
|
10174
|
+
if (input.acks.length === 0) return [];
|
|
10175
|
+
const updated = [];
|
|
10176
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10177
|
+
for (const a of input.acks) byId.set(a.messageId, a);
|
|
9991
10178
|
await withFileLock(this.filePath, async () => {
|
|
9992
10179
|
const all = await this._readAll();
|
|
9993
|
-
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
9994
|
-
if (idx === -1) return;
|
|
9995
|
-
const msg = all[idx];
|
|
9996
10180
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
msg
|
|
10002
|
-
|
|
10003
|
-
|
|
10181
|
+
let changed = false;
|
|
10182
|
+
for (const msg of all) {
|
|
10183
|
+
const a = byId.get(msg.id);
|
|
10184
|
+
if (!a) continue;
|
|
10185
|
+
updated.push(msg);
|
|
10186
|
+
if (a.read !== false && !(a.readerId in msg.readBy)) {
|
|
10187
|
+
msg.readBy[a.readerId] = now;
|
|
10188
|
+
changed = true;
|
|
10189
|
+
}
|
|
10190
|
+
if (a.completed && !msg.completed) {
|
|
10191
|
+
msg.completed = true;
|
|
10192
|
+
msg.completedBy = a.readerId;
|
|
10193
|
+
msg.completedAt = now;
|
|
10194
|
+
changed = true;
|
|
10195
|
+
}
|
|
10196
|
+
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
10197
|
+
msg.outcome = a.outcome;
|
|
10198
|
+
changed = true;
|
|
10199
|
+
}
|
|
10004
10200
|
}
|
|
10005
|
-
if (
|
|
10006
|
-
|
|
10201
|
+
if (changed) {
|
|
10202
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10203
|
+
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
10007
10204
|
}
|
|
10008
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10009
|
-
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
10010
|
-
result = msg;
|
|
10011
10205
|
});
|
|
10012
|
-
return
|
|
10206
|
+
return updated;
|
|
10013
10207
|
}
|
|
10014
10208
|
// ── Agent statuses ────────────────────────────────────────────────────
|
|
10015
10209
|
async getAgentStatuses() {
|
|
@@ -10061,6 +10255,43 @@ var DefaultMailbox = class {
|
|
|
10061
10255
|
await fsp6.writeFile(this.filePath, "", "utf8");
|
|
10062
10256
|
});
|
|
10063
10257
|
}
|
|
10258
|
+
async purgeStale(opts) {
|
|
10259
|
+
const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
|
|
10260
|
+
const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
10261
|
+
let completedPurged = 0;
|
|
10262
|
+
let incompletePurged = 0;
|
|
10263
|
+
await withFileLock(this.filePath, async () => {
|
|
10264
|
+
const all2 = await this._readAll();
|
|
10265
|
+
const now = Date.now();
|
|
10266
|
+
const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
|
|
10267
|
+
const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
|
|
10268
|
+
const kept = [];
|
|
10269
|
+
for (const msg of all2) {
|
|
10270
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
10271
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
10272
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
10273
|
+
completedPurged++;
|
|
10274
|
+
continue;
|
|
10275
|
+
}
|
|
10276
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
10277
|
+
incompletePurged++;
|
|
10278
|
+
continue;
|
|
10279
|
+
}
|
|
10280
|
+
kept.push(msg);
|
|
10281
|
+
}
|
|
10282
|
+
if (kept.length < all2.length) {
|
|
10283
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10284
|
+
await fsp6.writeFile(this.filePath, content, "utf8");
|
|
10285
|
+
}
|
|
10286
|
+
});
|
|
10287
|
+
const all = await this._readAll();
|
|
10288
|
+
return {
|
|
10289
|
+
completedPurged,
|
|
10290
|
+
incompletePurged,
|
|
10291
|
+
totalPurged: completedPurged + incompletePurged,
|
|
10292
|
+
remaining: all.length
|
|
10293
|
+
};
|
|
10294
|
+
}
|
|
10064
10295
|
// ── Client registry stubs (not applicable per-session) ─────────────────
|
|
10065
10296
|
async registerClient(_input) {
|
|
10066
10297
|
}
|
|
@@ -10229,21 +10460,6 @@ var BrainMonitor = class {
|
|
|
10229
10460
|
}
|
|
10230
10461
|
}
|
|
10231
10462
|
};
|
|
10232
|
-
function projectSlug(absRoot) {
|
|
10233
|
-
const base = slugify(path4.basename(absRoot));
|
|
10234
|
-
const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
10235
|
-
return `${base}-${hash}`;
|
|
10236
|
-
}
|
|
10237
|
-
function slugify(name) {
|
|
10238
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
10239
|
-
}
|
|
10240
|
-
function wstackGlobalRoot() {
|
|
10241
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
10242
|
-
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
10243
|
-
return path4.join(os.homedir(), ".wrongstack");
|
|
10244
|
-
}
|
|
10245
|
-
|
|
10246
|
-
// src/coordination/global-mailbox.ts
|
|
10247
10463
|
var MAILBOX_FILE2 = "_mailbox.jsonl";
|
|
10248
10464
|
var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
|
|
10249
10465
|
var AGENT_STALE_MS = 6e4;
|
|
@@ -10251,8 +10467,9 @@ var CLIENT_STALE_MS = 6e4;
|
|
|
10251
10467
|
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
10252
10468
|
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
10253
10469
|
var LINE_SEPARATOR2 = "\n";
|
|
10470
|
+
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
10254
10471
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
10255
|
-
return
|
|
10472
|
+
return path5.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
10256
10473
|
}
|
|
10257
10474
|
var GlobalMailbox = class {
|
|
10258
10475
|
/** Path to the JSONL message file. */
|
|
@@ -10283,14 +10500,28 @@ var GlobalMailbox = class {
|
|
|
10283
10500
|
_lastHeartbeat = /* @__PURE__ */ new Map();
|
|
10284
10501
|
/** Last time each local client sent a heartbeat (throttle). */
|
|
10285
10502
|
_lastClientHeartbeat = /* @__PURE__ */ new Map();
|
|
10503
|
+
/**
|
|
10504
|
+
* In-memory mirror of the JSONL message file. The mailbox is shared
|
|
10505
|
+
* ACROSS PROCESSES, so reads cannot trust the cache blindly — we pair it
|
|
10506
|
+
* with an mtime check. The file lock serializes every write, so a
|
|
10507
|
+
* changed mtimeMs is a definitive signal that another process (or this
|
|
10508
|
+
* one) wrote; an unchanged mtimeMs guarantees no write happened and the
|
|
10509
|
+
* cache is current. This collapses the per-iteration `query()` cost from
|
|
10510
|
+
* O(file_size) disk + parse to O(messages) in memory.
|
|
10511
|
+
*/
|
|
10512
|
+
_messageCache = null;
|
|
10513
|
+
/** mtimeMs of the file when `_messageCache` was populated. */
|
|
10514
|
+
_messageCacheMtime = -1;
|
|
10515
|
+
/** Size of the file when `_messageCache` was populated (extra guard). */
|
|
10516
|
+
_messageCacheSize = -1;
|
|
10286
10517
|
/**
|
|
10287
10518
|
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
10288
10519
|
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
10289
10520
|
*/
|
|
10290
10521
|
constructor(projectDir, events) {
|
|
10291
|
-
this.messagePath =
|
|
10292
|
-
this.registryPath =
|
|
10293
|
-
this.clientRegistryPath =
|
|
10522
|
+
this.messagePath = path5.join(projectDir, MAILBOX_FILE2);
|
|
10523
|
+
this.registryPath = path5.join(projectDir, "_mailbox.registry.json");
|
|
10524
|
+
this.clientRegistryPath = path5.join(projectDir, CLIENT_REGISTRY_FILE);
|
|
10294
10525
|
this._events = events;
|
|
10295
10526
|
}
|
|
10296
10527
|
// ── Messages ────────────────────────────────────────────────────────────
|
|
@@ -10313,73 +10544,89 @@ var GlobalMailbox = class {
|
|
|
10313
10544
|
taskContext: input.taskContext
|
|
10314
10545
|
};
|
|
10315
10546
|
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
10316
|
-
await fsp6.mkdir(
|
|
10547
|
+
await fsp6.mkdir(path5.dirname(this.messagePath), { recursive: true });
|
|
10317
10548
|
await withFileLock(this.messagePath, async () => {
|
|
10318
10549
|
await fsp6.appendFile(this.messagePath, line, "utf8");
|
|
10550
|
+
this._pushToCache(msg);
|
|
10319
10551
|
});
|
|
10320
10552
|
return msg;
|
|
10321
10553
|
}
|
|
10322
10554
|
async query(q) {
|
|
10323
|
-
const all = await this.
|
|
10555
|
+
const all = await this._readMessagesCached();
|
|
10324
10556
|
const limit = q.limit ?? 50;
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
}
|
|
10341
|
-
if (q.minPriority !== void 0) {
|
|
10342
|
-
const order = { low: 0, normal: 1, high: 2 };
|
|
10343
|
-
const min = order[q.minPriority];
|
|
10344
|
-
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
10345
|
-
}
|
|
10346
|
-
if (q.since !== void 0) {
|
|
10347
|
-
const since = q.since;
|
|
10348
|
-
filtered = filtered.filter((m) => m.timestamp > since);
|
|
10557
|
+
const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
|
|
10558
|
+
const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
|
|
10559
|
+
const out = [];
|
|
10560
|
+
for (let i = 0; i < all.length; i++) {
|
|
10561
|
+
const m = all[i];
|
|
10562
|
+
if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
|
|
10563
|
+
if (q.from !== void 0 && m.from !== q.from) continue;
|
|
10564
|
+
if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
|
|
10565
|
+
if (q.incompleteOnly && m.completed) continue;
|
|
10566
|
+
if (q.type !== void 0 && m.type !== q.type) continue;
|
|
10567
|
+
if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
|
|
10568
|
+
continue;
|
|
10569
|
+
}
|
|
10570
|
+
if (q.since !== void 0 && m.timestamp <= q.since) continue;
|
|
10571
|
+
out.push(m);
|
|
10349
10572
|
}
|
|
10350
|
-
|
|
10351
|
-
return
|
|
10573
|
+
out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
10574
|
+
return out.slice(0, limit).map((m) => ({ ...m, readBy: { ...m.readBy } }));
|
|
10352
10575
|
}
|
|
10353
10576
|
async ack(input) {
|
|
10354
|
-
|
|
10577
|
+
const updated = await this.ackMany({ acks: [input] });
|
|
10578
|
+
return updated.length > 0 ? updated[0] : null;
|
|
10579
|
+
}
|
|
10580
|
+
async ackMany(input) {
|
|
10581
|
+
if (input.acks.length === 0) return [];
|
|
10582
|
+
const updated = [];
|
|
10583
|
+
const byId = /* @__PURE__ */ new Map();
|
|
10584
|
+
for (const a of input.acks) {
|
|
10585
|
+
byId.set(a.messageId, a);
|
|
10586
|
+
}
|
|
10587
|
+
let cacheSnapshot = null;
|
|
10355
10588
|
await withFileLock(this.messagePath, async () => {
|
|
10356
|
-
const all = await this.
|
|
10357
|
-
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10358
|
-
if (idx === -1) return;
|
|
10359
|
-
const msg = all[idx];
|
|
10589
|
+
const all = await this._readMessagesFresh();
|
|
10360
10590
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
msg
|
|
10366
|
-
|
|
10367
|
-
|
|
10591
|
+
let changed = false;
|
|
10592
|
+
for (const msg of all) {
|
|
10593
|
+
const a = byId.get(msg.id);
|
|
10594
|
+
if (!a) continue;
|
|
10595
|
+
updated.push(msg);
|
|
10596
|
+
if (a.read !== false && !(a.readerId in msg.readBy)) {
|
|
10597
|
+
msg.readBy[a.readerId] = now;
|
|
10598
|
+
changed = true;
|
|
10599
|
+
}
|
|
10600
|
+
if (a.completed && !msg.completed) {
|
|
10601
|
+
msg.completed = true;
|
|
10602
|
+
msg.completedBy = a.readerId;
|
|
10603
|
+
msg.completedAt = now;
|
|
10604
|
+
changed = true;
|
|
10605
|
+
}
|
|
10606
|
+
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
10607
|
+
msg.outcome = a.outcome;
|
|
10608
|
+
changed = true;
|
|
10609
|
+
}
|
|
10368
10610
|
}
|
|
10369
|
-
if (
|
|
10370
|
-
|
|
10611
|
+
if (changed) {
|
|
10612
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
10613
|
+
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10371
10614
|
}
|
|
10372
|
-
|
|
10373
|
-
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10374
|
-
result = msg;
|
|
10615
|
+
cacheSnapshot = all;
|
|
10375
10616
|
});
|
|
10376
|
-
|
|
10617
|
+
if (cacheSnapshot) this._setMessageCache(cacheSnapshot);
|
|
10618
|
+
return updated;
|
|
10377
10619
|
}
|
|
10378
10620
|
async unreadCount(forAgentId) {
|
|
10379
|
-
const all = await this.
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10621
|
+
const all = await this._readMessagesCached();
|
|
10622
|
+
let count = 0;
|
|
10623
|
+
for (let i = 0; i < all.length; i++) {
|
|
10624
|
+
const m = all[i];
|
|
10625
|
+
if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
|
|
10626
|
+
count++;
|
|
10627
|
+
}
|
|
10628
|
+
}
|
|
10629
|
+
return count;
|
|
10383
10630
|
}
|
|
10384
10631
|
// ── Agent registry ──────────────────────────────────────────────────────
|
|
10385
10632
|
async registerAgent(input) {
|
|
@@ -10540,13 +10787,62 @@ var GlobalMailbox = class {
|
|
|
10540
10787
|
async close() {
|
|
10541
10788
|
this._registryCache = null;
|
|
10542
10789
|
this._clientRegistryCache = null;
|
|
10790
|
+
this._messageCache = null;
|
|
10791
|
+
this._messageCacheMtime = -1;
|
|
10792
|
+
this._messageCacheSize = -1;
|
|
10543
10793
|
}
|
|
10544
10794
|
async clearAll() {
|
|
10545
10795
|
await withFileLock(this.messagePath, async () => {
|
|
10546
10796
|
await fsp6.writeFile(this.messagePath, "", "utf8");
|
|
10547
10797
|
});
|
|
10798
|
+
this._setMessageCache([]);
|
|
10799
|
+
}
|
|
10800
|
+
async purgeStale(opts) {
|
|
10801
|
+
const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
|
|
10802
|
+
const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
10803
|
+
let completedPurged = 0;
|
|
10804
|
+
let incompletePurged = 0;
|
|
10805
|
+
let remaining = 0;
|
|
10806
|
+
await withFileLock(this.messagePath, async () => {
|
|
10807
|
+
const all = await this._readMessagesFresh();
|
|
10808
|
+
const now = Date.now();
|
|
10809
|
+
const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
|
|
10810
|
+
const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
|
|
10811
|
+
const kept = [];
|
|
10812
|
+
for (const msg of all) {
|
|
10813
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
10814
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
10815
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
10816
|
+
completedPurged++;
|
|
10817
|
+
continue;
|
|
10818
|
+
}
|
|
10819
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
10820
|
+
incompletePurged++;
|
|
10821
|
+
continue;
|
|
10822
|
+
}
|
|
10823
|
+
kept.push(msg);
|
|
10824
|
+
}
|
|
10825
|
+
remaining = kept.length;
|
|
10826
|
+
if (kept.length < all.length) {
|
|
10827
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
10828
|
+
await fsp6.writeFile(this.messagePath, content, "utf8");
|
|
10829
|
+
}
|
|
10830
|
+
this._setMessageCache(kept);
|
|
10831
|
+
});
|
|
10832
|
+
return {
|
|
10833
|
+
completedPurged,
|
|
10834
|
+
incompletePurged,
|
|
10835
|
+
totalPurged: completedPurged + incompletePurged,
|
|
10836
|
+
remaining
|
|
10837
|
+
};
|
|
10548
10838
|
}
|
|
10549
10839
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
10840
|
+
/**
|
|
10841
|
+
* Read all messages from the JSONL file. Always reads + parses the file.
|
|
10842
|
+
* Callers that can tolerate a stale-by-mtime view should use
|
|
10843
|
+
* {@link _readMessagesCached}; writers that need the post-lock truth
|
|
10844
|
+
* should call this directly (it's what {@link _readMessagesFresh} aliases).
|
|
10845
|
+
*/
|
|
10550
10846
|
async _readMessages() {
|
|
10551
10847
|
try {
|
|
10552
10848
|
const raw = await fsp6.readFile(this.messagePath, "utf8");
|
|
@@ -10574,8 +10870,85 @@ var GlobalMailbox = class {
|
|
|
10574
10870
|
throw err;
|
|
10575
10871
|
}
|
|
10576
10872
|
}
|
|
10873
|
+
/**
|
|
10874
|
+
* Read messages, then adopt the result as the in-memory cache. Use this
|
|
10875
|
+
* from writers that just took the file lock — the read reflects the
|
|
10876
|
+
* authoritative post-lock state and should be served to subsequent
|
|
10877
|
+
* queries without re-reading.
|
|
10878
|
+
*/
|
|
10879
|
+
async _readMessagesFresh() {
|
|
10880
|
+
const all = await this._readMessages();
|
|
10881
|
+
this._setMessageCache(all);
|
|
10882
|
+
return all;
|
|
10883
|
+
}
|
|
10884
|
+
/**
|
|
10885
|
+
* Read messages, consulting the mtime-bounded in-memory cache first.
|
|
10886
|
+
* The mailbox file is shared across processes; every `send`/`ack`/
|
|
10887
|
+
* `clearAll`/`purgeStale` takes the file lock, so writes are serialized
|
|
10888
|
+
* and a changed mtimeMs is a definitive freshness signal. When the
|
|
10889
|
+
* stat matches the cached mtime+size we return the cached array — no
|
|
10890
|
+
* file read and no JSON.parse — collapsing the per-iteration query
|
|
10891
|
+
* cost on the mailbox-loop hot path.
|
|
10892
|
+
*/
|
|
10893
|
+
async _readMessagesCached() {
|
|
10894
|
+
try {
|
|
10895
|
+
const st = await fsp6.stat(this.messagePath);
|
|
10896
|
+
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
10897
|
+
return this._messageCache;
|
|
10898
|
+
}
|
|
10899
|
+
const all = await this._readMessages();
|
|
10900
|
+
this._setMessageCache(all, st.mtimeMs, st.size);
|
|
10901
|
+
return all;
|
|
10902
|
+
} catch (err) {
|
|
10903
|
+
if (err.code === "ENOENT") {
|
|
10904
|
+
this._setMessageCache([], -1, -1);
|
|
10905
|
+
return [];
|
|
10906
|
+
}
|
|
10907
|
+
throw err;
|
|
10908
|
+
}
|
|
10909
|
+
}
|
|
10910
|
+
/**
|
|
10911
|
+
* Replace the in-memory cache. Caller is responsible for guaranteeing
|
|
10912
|
+
* that `messages` reflects the current on-disk state (e.g. they just
|
|
10913
|
+
* read or wrote it under the file lock).
|
|
10914
|
+
*/
|
|
10915
|
+
_setMessageCache(messages, mtime, size) {
|
|
10916
|
+
if (messages.length > MESSAGE_CACHE_MAX_ENTRIES) {
|
|
10917
|
+
this._messageCache = null;
|
|
10918
|
+
this._messageCacheMtime = -1;
|
|
10919
|
+
this._messageCacheSize = -1;
|
|
10920
|
+
return;
|
|
10921
|
+
}
|
|
10922
|
+
this._messageCache = messages;
|
|
10923
|
+
if (mtime !== void 0 && size !== void 0) {
|
|
10924
|
+
this._messageCacheMtime = mtime;
|
|
10925
|
+
this._messageCacheSize = size;
|
|
10926
|
+
} else {
|
|
10927
|
+
void fsp6.stat(this.messagePath).then((st) => {
|
|
10928
|
+
this._messageCacheMtime = st.mtimeMs;
|
|
10929
|
+
this._messageCacheSize = st.size;
|
|
10930
|
+
}).catch(() => {
|
|
10931
|
+
});
|
|
10932
|
+
}
|
|
10933
|
+
}
|
|
10934
|
+
/**
|
|
10935
|
+
* Append a single just-sent message to the in-memory cache without
|
|
10936
|
+
* re-reading the file. The caller must hold the file lock (or have
|
|
10937
|
+
* just released it after a successful append) so the cache stays
|
|
10938
|
+
* consistent with on-disk state.
|
|
10939
|
+
*/
|
|
10940
|
+
_pushToCache(msg) {
|
|
10941
|
+
if (this._messageCache === null) return;
|
|
10942
|
+
if (this._messageCache.length >= MESSAGE_CACHE_MAX_ENTRIES) {
|
|
10943
|
+
this._messageCache = null;
|
|
10944
|
+
this._messageCacheMtime = -1;
|
|
10945
|
+
this._messageCacheSize = -1;
|
|
10946
|
+
return;
|
|
10947
|
+
}
|
|
10948
|
+
this._messageCache.push(msg);
|
|
10949
|
+
}
|
|
10577
10950
|
async _ensureRegistry() {
|
|
10578
|
-
await fsp6.mkdir(
|
|
10951
|
+
await fsp6.mkdir(path5.dirname(this.registryPath), { recursive: true });
|
|
10579
10952
|
}
|
|
10580
10953
|
async _readRegistry(opts) {
|
|
10581
10954
|
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
@@ -10620,7 +10993,7 @@ var GlobalMailbox = class {
|
|
|
10620
10993
|
}
|
|
10621
10994
|
// ── Client registry internals ───────────────────────────────────────────
|
|
10622
10995
|
async _ensureClientRegistry() {
|
|
10623
|
-
await fsp6.mkdir(
|
|
10996
|
+
await fsp6.mkdir(path5.dirname(this.clientRegistryPath), { recursive: true });
|
|
10624
10997
|
}
|
|
10625
10998
|
async _readClientRegistry(opts) {
|
|
10626
10999
|
if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
@@ -11260,7 +11633,7 @@ function createMailboxHooks(opts) {
|
|
|
11260
11633
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
11261
11634
|
var LOG_FILENAME = "package-authors.json";
|
|
11262
11635
|
function logPath(storageDir) {
|
|
11263
|
-
return
|
|
11636
|
+
return path5.join(storageDir, LOG_FILENAME);
|
|
11264
11637
|
}
|
|
11265
11638
|
async function loadLog(storageDir, projectRoot) {
|
|
11266
11639
|
try {
|
|
@@ -11284,7 +11657,7 @@ async function saveLog(storageDir, log) {
|
|
|
11284
11657
|
await fsp6.rename(tmp, logPath(storageDir));
|
|
11285
11658
|
}
|
|
11286
11659
|
function detectEcosystem(manifestPath) {
|
|
11287
|
-
const name =
|
|
11660
|
+
const name = path5.basename(manifestPath).toLowerCase();
|
|
11288
11661
|
if (name === "package.json") return "npm";
|
|
11289
11662
|
if (name === "go.mod") return "go";
|
|
11290
11663
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -11481,7 +11854,7 @@ function startPackageOutdatedWatcher(opts) {
|
|
|
11481
11854
|
);
|
|
11482
11855
|
} catch (err) {
|
|
11483
11856
|
handleError(err);
|
|
11484
|
-
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${
|
|
11857
|
+
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${toErrorMessage(err)}`);
|
|
11485
11858
|
}
|
|
11486
11859
|
}
|
|
11487
11860
|
}
|
|
@@ -11554,7 +11927,2055 @@ mvn versions:use-latest-versions`;
|
|
|
11554
11927
|
return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
|
|
11555
11928
|
}
|
|
11556
11929
|
}
|
|
11930
|
+
var KnowledgeGraph = class {
|
|
11931
|
+
nodes = /* @__PURE__ */ new Map();
|
|
11932
|
+
index = /* @__PURE__ */ new Map();
|
|
11933
|
+
// tag/field → node ids
|
|
11934
|
+
subs = /* @__PURE__ */ new Map();
|
|
11935
|
+
pendingDeliveries = /* @__PURE__ */ new Map();
|
|
11936
|
+
filePath;
|
|
11937
|
+
graphFilePath;
|
|
11938
|
+
constructor(sessionDir) {
|
|
11939
|
+
this.filePath = path5.join(sessionDir, "_knowledge_graph");
|
|
11940
|
+
this.graphFilePath = path5.join(this.filePath, "graph.jsonl");
|
|
11941
|
+
}
|
|
11942
|
+
// ── Write ──────────────────────────────────────────────────────────────
|
|
11943
|
+
/**
|
|
11944
|
+
* Add a node. Fires to all matching subscriptions synchronously.
|
|
11945
|
+
* Returns the node with its assigned id.
|
|
11946
|
+
*/
|
|
11947
|
+
async add(node) {
|
|
11948
|
+
const full = { id: randomUUID(), ...node };
|
|
11949
|
+
this.nodes.set(full.id, full);
|
|
11950
|
+
this._index(full);
|
|
11951
|
+
await this._persist(full);
|
|
11952
|
+
this._deliver(full);
|
|
11953
|
+
return full;
|
|
11954
|
+
}
|
|
11955
|
+
/** Update an existing node by id. Returns updated node or null if not found. */
|
|
11956
|
+
async update(id, patch) {
|
|
11957
|
+
const existing = this.nodes.get(id);
|
|
11958
|
+
if (!existing) return null;
|
|
11959
|
+
const updated = { ...existing, ...patch };
|
|
11960
|
+
this.nodes.set(id, updated);
|
|
11961
|
+
this._deliver(updated);
|
|
11962
|
+
await this._append(updated);
|
|
11963
|
+
return updated;
|
|
11964
|
+
}
|
|
11965
|
+
// ── Read ───────────────────────────────────────────────────────────────
|
|
11966
|
+
get(id) {
|
|
11967
|
+
return this.nodes.get(id);
|
|
11968
|
+
}
|
|
11969
|
+
getAll(filter) {
|
|
11970
|
+
return Array.from(this.nodes.values()).filter((n) => this._matches(n, filter ?? {}));
|
|
11971
|
+
}
|
|
11972
|
+
getGoals(filter) {
|
|
11973
|
+
return this.getAll({ type: "goal", ...filter });
|
|
11974
|
+
}
|
|
11975
|
+
getFacts(filter) {
|
|
11976
|
+
return this.getAll({ type: "fact", ...filter });
|
|
11977
|
+
}
|
|
11978
|
+
getChanges(filter) {
|
|
11979
|
+
return this.getAll({ type: "change", ...filter });
|
|
11980
|
+
}
|
|
11981
|
+
getOpenGoals() {
|
|
11982
|
+
return this.getGoals({ status: "pending" }).concat(
|
|
11983
|
+
this.getGoals({ status: "in_progress" })
|
|
11984
|
+
);
|
|
11985
|
+
}
|
|
11986
|
+
getTopLevelGoals() {
|
|
11987
|
+
return this.getGoals({}).filter((g) => !g.parentGoal);
|
|
11988
|
+
}
|
|
11989
|
+
getBlockedGoals() {
|
|
11990
|
+
return this.getGoals({ status: "blocked" });
|
|
11991
|
+
}
|
|
11992
|
+
getPendingChanges() {
|
|
11993
|
+
return this.getChanges({ status: "proposed" });
|
|
11994
|
+
}
|
|
11995
|
+
getDecisions(since) {
|
|
11996
|
+
return this.getAll({ type: "decision", since });
|
|
11997
|
+
}
|
|
11998
|
+
// ── Search ─────────────────────────────────────────────────────────────
|
|
11999
|
+
searchFacts(query) {
|
|
12000
|
+
const q = query.toLowerCase();
|
|
12001
|
+
return this.getFacts().filter(
|
|
12002
|
+
(f) => f.subject.toLowerCase().includes(q) || f.detail.toLowerCase().includes(q) || f.file?.toLowerCase().includes(q) || f.tags.some((t) => t.toLowerCase().includes(q))
|
|
12003
|
+
);
|
|
12004
|
+
}
|
|
12005
|
+
getRelatedFacts(factId) {
|
|
12006
|
+
const fact = this.nodes.get(factId);
|
|
12007
|
+
if (!fact) return [];
|
|
12008
|
+
return fact.related.map((id) => this.nodes.get(id)).filter((n) => n?.type === "fact");
|
|
12009
|
+
}
|
|
12010
|
+
// ── Subscriptions ──────────────────────────────────────────────────────
|
|
12011
|
+
/**
|
|
12012
|
+
* Subscribe to nodes matching a filter. Returns a channel id that can be
|
|
12013
|
+
* used to poll for new nodes since the last check.
|
|
12014
|
+
*/
|
|
12015
|
+
subscribe(agentId, filter) {
|
|
12016
|
+
const channel = randomUUID();
|
|
12017
|
+
const sub = { id: randomUUID(), agentId, filter, channel };
|
|
12018
|
+
this.subs.set(channel, sub);
|
|
12019
|
+
this.pendingDeliveries.set(channel, []);
|
|
12020
|
+
return channel;
|
|
12021
|
+
}
|
|
12022
|
+
/**
|
|
12023
|
+
* Poll for new nodes delivered to a channel since last check.
|
|
12024
|
+
* Clears the delivery buffer after reading.
|
|
12025
|
+
*/
|
|
12026
|
+
poll(channel) {
|
|
12027
|
+
const pending = this.pendingDeliveries.get(channel);
|
|
12028
|
+
if (!pending) return [];
|
|
12029
|
+
const delivered = [...pending];
|
|
12030
|
+
pending.length = 0;
|
|
12031
|
+
return delivered;
|
|
12032
|
+
}
|
|
12033
|
+
unsubscribe(channel) {
|
|
12034
|
+
this.subs.delete(channel);
|
|
12035
|
+
this.pendingDeliveries.delete(channel);
|
|
12036
|
+
}
|
|
12037
|
+
// ── Quality gate helpers ───────────────────────────────────────────────
|
|
12038
|
+
/**
|
|
12039
|
+
* Create a quality gate result. Call this when a change is being proposed
|
|
12040
|
+
* so the change node carries the gate result.
|
|
12041
|
+
*/
|
|
12042
|
+
static makeQualityGate(checks) {
|
|
12043
|
+
return { passed: checks.every((c) => c.passed), checks };
|
|
12044
|
+
}
|
|
12045
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
12046
|
+
_index(node) {
|
|
12047
|
+
const add = (key) => {
|
|
12048
|
+
let set = this.index.get(key);
|
|
12049
|
+
if (!set) {
|
|
12050
|
+
set = /* @__PURE__ */ new Set();
|
|
12051
|
+
this.index.set(key, set);
|
|
12052
|
+
}
|
|
12053
|
+
set.add(node.id);
|
|
12054
|
+
};
|
|
12055
|
+
add(`type:${node.type}`);
|
|
12056
|
+
if (node.type === "fact") {
|
|
12057
|
+
const f = node;
|
|
12058
|
+
add(`cat:${f.category}`);
|
|
12059
|
+
if (f.severity) add(`sev:${f.severity}`);
|
|
12060
|
+
add(`by:${f.discoveredBy}`);
|
|
12061
|
+
for (const tag of f.tags) add(`tag:${tag}`);
|
|
12062
|
+
add(`key:${f.key}`);
|
|
12063
|
+
}
|
|
12064
|
+
if (node.type === "goal") {
|
|
12065
|
+
const g = node;
|
|
12066
|
+
add(`status:${g.status}`);
|
|
12067
|
+
add(`prio:${g.priority}`);
|
|
12068
|
+
if (g.assignee) add(`assign:${g.assignee}`);
|
|
12069
|
+
for (const tag of g.tags) add(`tag:${tag}`);
|
|
12070
|
+
}
|
|
12071
|
+
if (node.type === "change") {
|
|
12072
|
+
const c = node;
|
|
12073
|
+
add(`change:${c.status}`);
|
|
12074
|
+
add(`by:${c.proposedBy}`);
|
|
12075
|
+
for (const g of c.satisfiesGoals) add(`goal:${g}`);
|
|
12076
|
+
}
|
|
12077
|
+
}
|
|
12078
|
+
_matches(node, f) {
|
|
12079
|
+
if (f.type && node.type !== f.type) return false;
|
|
12080
|
+
if (f.category && node.category !== f.category) return false;
|
|
12081
|
+
if (f.status) {
|
|
12082
|
+
if (node.type === "goal" && node.status !== f.status) return false;
|
|
12083
|
+
if (node.type === "change" && node.status !== f.status) return false;
|
|
12084
|
+
}
|
|
12085
|
+
if (f.assignee && node.assignee !== f.assignee) return false;
|
|
12086
|
+
if (f.discoveredBy && node.discoveredBy !== f.discoveredBy) return false;
|
|
12087
|
+
if (f.proposedBy && node.proposedBy !== f.proposedBy) return false;
|
|
12088
|
+
if (f.tags?.length) {
|
|
12089
|
+
const nodeTags = node.tags ?? node.tags ?? [];
|
|
12090
|
+
if (!f.tags.some((t) => nodeTags.includes(t))) return false;
|
|
12091
|
+
}
|
|
12092
|
+
if (f.since && node.id > f.since) ;
|
|
12093
|
+
return true;
|
|
12094
|
+
}
|
|
12095
|
+
_deliver(node) {
|
|
12096
|
+
for (const sub of this.subs.values()) {
|
|
12097
|
+
if (this._matches(node, sub.filter)) {
|
|
12098
|
+
const pending = this.pendingDeliveries.get(sub.channel);
|
|
12099
|
+
if (pending) pending.push(node);
|
|
12100
|
+
}
|
|
12101
|
+
}
|
|
12102
|
+
}
|
|
12103
|
+
async _persist(node) {
|
|
12104
|
+
await fsp6.mkdir(this.filePath, { recursive: true });
|
|
12105
|
+
const line = JSON.stringify(node) + "\n";
|
|
12106
|
+
await withFileLock(this.graphFilePath, async () => {
|
|
12107
|
+
await fsp6.appendFile(this.graphFilePath, line, "utf8");
|
|
12108
|
+
});
|
|
12109
|
+
}
|
|
12110
|
+
async _append(node) {
|
|
12111
|
+
await fsp6.mkdir(this.filePath, { recursive: true });
|
|
12112
|
+
const line = JSON.stringify({ op: "update", node }) + "\n";
|
|
12113
|
+
await withFileLock(this.graphFilePath, async () => {
|
|
12114
|
+
await fsp6.appendFile(this.graphFilePath, line, "utf8");
|
|
12115
|
+
});
|
|
12116
|
+
}
|
|
12117
|
+
/** Rebuild in-memory state from the log file. Call on startup. */
|
|
12118
|
+
async load() {
|
|
12119
|
+
try {
|
|
12120
|
+
const content = await fsp6.readFile(this.graphFilePath, "utf8");
|
|
12121
|
+
const lines = content.split("\n").filter(Boolean);
|
|
12122
|
+
for (const line of lines) {
|
|
12123
|
+
try {
|
|
12124
|
+
const parsed = JSON.parse(line);
|
|
12125
|
+
if (parsed.op === "update") {
|
|
12126
|
+
this.nodes.set(parsed.node.id, parsed.node);
|
|
12127
|
+
this._index(parsed.node);
|
|
12128
|
+
} else {
|
|
12129
|
+
this.nodes.set(parsed.id, parsed);
|
|
12130
|
+
this._index(parsed);
|
|
12131
|
+
}
|
|
12132
|
+
} catch {
|
|
12133
|
+
}
|
|
12134
|
+
}
|
|
12135
|
+
} catch {
|
|
12136
|
+
}
|
|
12137
|
+
}
|
|
12138
|
+
/** Snapshot for serialization. */
|
|
12139
|
+
snapshot() {
|
|
12140
|
+
return {
|
|
12141
|
+
nodes: Array.from(this.nodes.values()),
|
|
12142
|
+
subs: this.subs.size
|
|
12143
|
+
};
|
|
12144
|
+
}
|
|
12145
|
+
};
|
|
12146
|
+
|
|
12147
|
+
// src/coordination/task-dag.ts
|
|
12148
|
+
var TaskDAG = class {
|
|
12149
|
+
nodes = /* @__PURE__ */ new Map();
|
|
12150
|
+
handlers = /* @__PURE__ */ new Set();
|
|
12151
|
+
runnablesHandlers = /* @__PURE__ */ new Set();
|
|
12152
|
+
runnableCache = null;
|
|
12153
|
+
// ── Node management ───────────────────────────────────────────────────
|
|
12154
|
+
/**
|
|
12155
|
+
* Add a task node. Dependencies are validated for cycles.
|
|
12156
|
+
* Throws if adding a dep would create a cycle.
|
|
12157
|
+
*/
|
|
12158
|
+
addNode(id, description, deps = [], opts = {}) {
|
|
12159
|
+
if (this.nodes.has(id)) return;
|
|
12160
|
+
for (const depId of deps) {
|
|
12161
|
+
if (!this.nodes.has(depId)) {
|
|
12162
|
+
throw new Error(`TaskDAG.addNode: unknown dependency "${depId}" for task "${id}". Add the dep first.`);
|
|
12163
|
+
}
|
|
12164
|
+
}
|
|
12165
|
+
if (this._wouldCycle(id, deps)) {
|
|
12166
|
+
throw new Error(`TaskDAG.addNode: adding deps [${deps.join(", ")}] to "${id}" would create a cycle.`);
|
|
12167
|
+
}
|
|
12168
|
+
const node = {
|
|
12169
|
+
id,
|
|
12170
|
+
description,
|
|
12171
|
+
deps: [...deps],
|
|
12172
|
+
status: deps.length === 0 ? "ready" : "pending",
|
|
12173
|
+
role: opts.role,
|
|
12174
|
+
priority: opts.priority ?? 5,
|
|
12175
|
+
dependents: [],
|
|
12176
|
+
tags: opts.tags ?? []
|
|
12177
|
+
};
|
|
12178
|
+
this.nodes.set(id, node);
|
|
12179
|
+
for (const depId of deps) {
|
|
12180
|
+
this.nodes.get(depId).dependents.push(id);
|
|
12181
|
+
}
|
|
12182
|
+
this.invalidateCache();
|
|
12183
|
+
this._emitReady();
|
|
12184
|
+
}
|
|
12185
|
+
/**
|
|
12186
|
+
* Remove a node and all edges to/from it.
|
|
12187
|
+
* Skips any dependents that would become dangling.
|
|
12188
|
+
*/
|
|
12189
|
+
removeNode(id) {
|
|
12190
|
+
const node = this.nodes.get(id);
|
|
12191
|
+
if (!node) return;
|
|
12192
|
+
for (const depId of node.deps) {
|
|
12193
|
+
const dep = this.nodes.get(depId);
|
|
12194
|
+
if (dep) {
|
|
12195
|
+
dep.dependents = dep.dependents.filter((d) => d !== id);
|
|
12196
|
+
}
|
|
12197
|
+
}
|
|
12198
|
+
for (const depId of node.dependents) {
|
|
12199
|
+
const dep = this.nodes.get(depId);
|
|
12200
|
+
if (dep && dep.deps.every((d) => !this.nodes.has(d) || this.nodes.get(d).status === "done")) {
|
|
12201
|
+
this._transition(depId, "pending", "ready");
|
|
12202
|
+
}
|
|
12203
|
+
}
|
|
12204
|
+
this.nodes.delete(id);
|
|
12205
|
+
this.invalidateCache();
|
|
12206
|
+
}
|
|
12207
|
+
// ── State transitions ──────────────────────────────────────────────────
|
|
12208
|
+
/**
|
|
12209
|
+
* Mark a task as running. Returns true if the transition was valid
|
|
12210
|
+
* (task was in 'ready' state), false otherwise.
|
|
12211
|
+
*/
|
|
12212
|
+
start(id, assignedTo) {
|
|
12213
|
+
const node = this.nodes.get(id);
|
|
12214
|
+
if (!node) return false;
|
|
12215
|
+
if (node.status !== "ready") return false;
|
|
12216
|
+
node.status = "running";
|
|
12217
|
+
node.assignedTo = assignedTo;
|
|
12218
|
+
node.spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12219
|
+
this.invalidateCache();
|
|
12220
|
+
this._emit({ type: "node:started", nodeId: id, assignedTo });
|
|
12221
|
+
return true;
|
|
12222
|
+
}
|
|
12223
|
+
/**
|
|
12224
|
+
* Mark a task as completed. Unblocks all dependents; they become 'ready'
|
|
12225
|
+
* if all their deps are done.
|
|
12226
|
+
*/
|
|
12227
|
+
complete(id, result) {
|
|
12228
|
+
const node = this.nodes.get(id);
|
|
12229
|
+
if (!node) return;
|
|
12230
|
+
node.status = "done";
|
|
12231
|
+
node.result = result;
|
|
12232
|
+
node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12233
|
+
this.invalidateCache();
|
|
12234
|
+
const blocked = [];
|
|
12235
|
+
for (const depId of node.dependents) {
|
|
12236
|
+
const dep = this.nodes.get(depId);
|
|
12237
|
+
if (!dep) continue;
|
|
12238
|
+
const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => this.nodes.get(d).status === "done");
|
|
12239
|
+
if (allDone) {
|
|
12240
|
+
this._transition(depId, "pending", "ready");
|
|
12241
|
+
} else {
|
|
12242
|
+
blocked.push(depId);
|
|
12243
|
+
}
|
|
12244
|
+
}
|
|
12245
|
+
this._emit({ type: "node:completed", nodeId: id, result, blockers: blocked });
|
|
12246
|
+
if (blocked.length === 0) this._emitReady();
|
|
12247
|
+
}
|
|
12248
|
+
/**
|
|
12249
|
+
* Mark a task as failed. Unblocks dependents but they remain 'pending'
|
|
12250
|
+
* (they may still be runnable if other deps succeeded).
|
|
12251
|
+
*/
|
|
12252
|
+
fail(id, error) {
|
|
12253
|
+
const node = this.nodes.get(id);
|
|
12254
|
+
if (!node) return;
|
|
12255
|
+
node.status = "failed";
|
|
12256
|
+
node.error = error;
|
|
12257
|
+
node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12258
|
+
this.invalidateCache();
|
|
12259
|
+
const blocked = [];
|
|
12260
|
+
for (const depId of node.dependents) {
|
|
12261
|
+
const dep = this.nodes.get(depId);
|
|
12262
|
+
if (!dep) continue;
|
|
12263
|
+
const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => {
|
|
12264
|
+
const s = this.nodes.get(d).status;
|
|
12265
|
+
return s === "done" || s === "skipped";
|
|
12266
|
+
});
|
|
12267
|
+
if (allDone) {
|
|
12268
|
+
this._transition(depId, "pending", "ready");
|
|
12269
|
+
} else {
|
|
12270
|
+
blocked.push(depId);
|
|
12271
|
+
}
|
|
12272
|
+
}
|
|
12273
|
+
this._emit({ type: "node:failed", nodeId: id, error, blockers: blocked });
|
|
12274
|
+
if (blocked.length === 0) this._emitReady();
|
|
12275
|
+
}
|
|
12276
|
+
/**
|
|
12277
|
+
* Skip a task (e.g., it was deemed unnecessary by an earlier step).
|
|
12278
|
+
* Treats it as done for dependency purposes.
|
|
12279
|
+
*/
|
|
12280
|
+
skip(id, reason) {
|
|
12281
|
+
const node = this.nodes.get(id);
|
|
12282
|
+
if (!node) return;
|
|
12283
|
+
node.status = "skipped";
|
|
12284
|
+
node.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12285
|
+
this.invalidateCache();
|
|
12286
|
+
for (const depId of node.dependents) {
|
|
12287
|
+
const dep = this.nodes.get(depId);
|
|
12288
|
+
if (!dep) continue;
|
|
12289
|
+
const allDone = dep.deps.filter((d) => this.nodes.has(d)).every((d) => {
|
|
12290
|
+
const s = this.nodes.get(d).status;
|
|
12291
|
+
return s === "done" || s === "skipped";
|
|
12292
|
+
});
|
|
12293
|
+
if (allDone) this._transition(depId, "pending", "ready");
|
|
12294
|
+
}
|
|
12295
|
+
this._emit({ type: "node:skipped", nodeId: id, reason });
|
|
12296
|
+
}
|
|
12297
|
+
// ── Queries ────────────────────────────────────────────────────────────
|
|
12298
|
+
getNode(id) {
|
|
12299
|
+
return this.nodes.get(id);
|
|
12300
|
+
}
|
|
12301
|
+
getAll() {
|
|
12302
|
+
return Array.from(this.nodes.values());
|
|
12303
|
+
}
|
|
12304
|
+
getReady() {
|
|
12305
|
+
if (this.runnableCache) return this.runnableCache;
|
|
12306
|
+
const runnable = Array.from(this.nodes.values()).filter((n) => n.status === "ready").sort((a, b) => a.priority - b.priority);
|
|
12307
|
+
this.runnableCache = runnable;
|
|
12308
|
+
return runnable;
|
|
12309
|
+
}
|
|
12310
|
+
getRunning() {
|
|
12311
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "running");
|
|
12312
|
+
}
|
|
12313
|
+
getPending() {
|
|
12314
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "pending");
|
|
12315
|
+
}
|
|
12316
|
+
getDone() {
|
|
12317
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "done");
|
|
12318
|
+
}
|
|
12319
|
+
getFailed() {
|
|
12320
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "failed");
|
|
12321
|
+
}
|
|
12322
|
+
getCompleted() {
|
|
12323
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "done" || n.status === "skipped");
|
|
12324
|
+
}
|
|
12325
|
+
isDone() {
|
|
12326
|
+
return Array.from(this.nodes.values()).every(
|
|
12327
|
+
(n) => n.status === "done" || n.status === "failed" || n.status === "skipped"
|
|
12328
|
+
);
|
|
12329
|
+
}
|
|
12330
|
+
isFailed() {
|
|
12331
|
+
return Array.from(this.nodes.values()).some((n) => n.status === "failed");
|
|
12332
|
+
}
|
|
12333
|
+
/** All tasks that are currently blocked (pending but not ready). */
|
|
12334
|
+
getBlocked() {
|
|
12335
|
+
return Array.from(this.nodes.values()).filter((n) => n.status === "pending");
|
|
12336
|
+
}
|
|
12337
|
+
/** Topological sort — tasks in dependency order. */
|
|
12338
|
+
getTopologicalOrder() {
|
|
12339
|
+
const visited = /* @__PURE__ */ new Set();
|
|
12340
|
+
const result = [];
|
|
12341
|
+
const visit = (id) => {
|
|
12342
|
+
if (visited.has(id)) return;
|
|
12343
|
+
visited.add(id);
|
|
12344
|
+
const node = this.nodes.get(id);
|
|
12345
|
+
if (!node) return;
|
|
12346
|
+
for (const depId of node.deps) visit(depId);
|
|
12347
|
+
result.push(node);
|
|
12348
|
+
};
|
|
12349
|
+
for (const id of this.nodes.keys()) visit(id);
|
|
12350
|
+
return result;
|
|
12351
|
+
}
|
|
12352
|
+
/** Check for deadlock: no runnable tasks but not done. */
|
|
12353
|
+
hasDeadlock() {
|
|
12354
|
+
if (this.isDone()) return false;
|
|
12355
|
+
return this.getReady().length === 0 && this.getRunning().length === 0;
|
|
12356
|
+
}
|
|
12357
|
+
/** Stats snapshot for reporting. */
|
|
12358
|
+
stats() {
|
|
12359
|
+
const all = Array.from(this.nodes.values());
|
|
12360
|
+
const done = all.filter((n) => n.status === "done" || n.status === "skipped").length;
|
|
12361
|
+
return {
|
|
12362
|
+
total: all.length,
|
|
12363
|
+
pending: all.filter((n) => n.status === "pending").length,
|
|
12364
|
+
ready: all.filter((n) => n.status === "ready").length,
|
|
12365
|
+
running: all.filter((n) => n.status === "running").length,
|
|
12366
|
+
done: all.filter((n) => n.status === "done").length,
|
|
12367
|
+
failed: all.filter((n) => n.status === "failed").length,
|
|
12368
|
+
skipped: all.filter((n) => n.status === "skipped").length,
|
|
12369
|
+
progress: all.length ? done / all.length : 0
|
|
12370
|
+
};
|
|
12371
|
+
}
|
|
12372
|
+
// ── Events ────────────────────────────────────────────────────────────
|
|
12373
|
+
onEvent(handler) {
|
|
12374
|
+
this.handlers.add(handler);
|
|
12375
|
+
return () => void this.handlers.delete(handler);
|
|
12376
|
+
}
|
|
12377
|
+
onRunnable(handler) {
|
|
12378
|
+
this.runnablesHandlers.add(handler);
|
|
12379
|
+
return () => void this.runnablesHandlers.delete(handler);
|
|
12380
|
+
}
|
|
12381
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
12382
|
+
_transition(id, from, to) {
|
|
12383
|
+
const node = this.nodes.get(id);
|
|
12384
|
+
if (!node || node.status !== from) return;
|
|
12385
|
+
node.status = to;
|
|
12386
|
+
this.invalidateCache();
|
|
12387
|
+
if (to === "ready") {
|
|
12388
|
+
this._emit({ type: "node:ready", nodeId: id, deps: node.deps });
|
|
12389
|
+
}
|
|
12390
|
+
}
|
|
12391
|
+
_emit(event) {
|
|
12392
|
+
for (const h of this.handlers) {
|
|
12393
|
+
try {
|
|
12394
|
+
h(event);
|
|
12395
|
+
} catch {
|
|
12396
|
+
}
|
|
12397
|
+
}
|
|
12398
|
+
}
|
|
12399
|
+
_emitReady() {
|
|
12400
|
+
const runnable = this.getReady();
|
|
12401
|
+
if (this.hasDeadlock()) {
|
|
12402
|
+
this._emit({ type: "deadlock", blocked: this.getBlocked().map((n) => n.id) });
|
|
12403
|
+
} else {
|
|
12404
|
+
this._emit({ type: "graph:done", allDone: this.isDone() });
|
|
12405
|
+
}
|
|
12406
|
+
if (runnable.length > 0) {
|
|
12407
|
+
for (const h of this.runnablesHandlers) {
|
|
12408
|
+
try {
|
|
12409
|
+
h(runnable);
|
|
12410
|
+
} catch {
|
|
12411
|
+
}
|
|
12412
|
+
}
|
|
12413
|
+
}
|
|
12414
|
+
}
|
|
12415
|
+
invalidateCache() {
|
|
12416
|
+
this.runnableCache = null;
|
|
12417
|
+
}
|
|
12418
|
+
/**
|
|
12419
|
+
* DFS cycle detection. Adding edge (id → dep) creates a cycle if
|
|
12420
|
+
* there already exists a path from dep to id.
|
|
12421
|
+
*/
|
|
12422
|
+
_wouldCycle(id, newDeps) {
|
|
12423
|
+
const visited = /* @__PURE__ */ new Set();
|
|
12424
|
+
const stack = [...newDeps];
|
|
12425
|
+
while (stack.length > 0) {
|
|
12426
|
+
const current = stack.pop();
|
|
12427
|
+
if (current === id) return true;
|
|
12428
|
+
if (visited.has(current)) continue;
|
|
12429
|
+
visited.add(current);
|
|
12430
|
+
const node = this.nodes.get(current);
|
|
12431
|
+
if (node) stack.push(...node.dependents);
|
|
12432
|
+
}
|
|
12433
|
+
return false;
|
|
12434
|
+
}
|
|
12435
|
+
};
|
|
12436
|
+
|
|
12437
|
+
// src/coordination/consensus-protocol.ts
|
|
12438
|
+
var ConsensusProtocol = class {
|
|
12439
|
+
graph;
|
|
12440
|
+
fleet;
|
|
12441
|
+
rules;
|
|
12442
|
+
voters;
|
|
12443
|
+
// agentId → config
|
|
12444
|
+
constructor(opts) {
|
|
12445
|
+
this.graph = opts.graph;
|
|
12446
|
+
this.fleet = opts.fleet ?? void 0;
|
|
12447
|
+
this.voters = new Map(opts.voters.map((v) => [v.agentId, v]));
|
|
12448
|
+
this.rules = {
|
|
12449
|
+
quorumFraction: opts.rules?.quorumFraction ?? 0.5,
|
|
12450
|
+
approvalFraction: opts.rules?.approvalFraction ?? 0.6,
|
|
12451
|
+
vetoRoles: opts.rules?.vetoRoles ?? [],
|
|
12452
|
+
approvalWeightFraction: opts.rules?.approvalWeightFraction
|
|
12453
|
+
};
|
|
12454
|
+
}
|
|
12455
|
+
// ── Vote lifecycle ────────────────────────────────────────────────────
|
|
12456
|
+
/**
|
|
12457
|
+
* Initiate a vote on a proposed change. Updates the change node's status
|
|
12458
|
+
* to 'proposed' and notifies eligible voters via FleetBus.
|
|
12459
|
+
*/
|
|
12460
|
+
initiateVote(changeId) {
|
|
12461
|
+
const change = this.graph.get(changeId);
|
|
12462
|
+
if (!change || change.type !== "change") {
|
|
12463
|
+
throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
|
|
12464
|
+
}
|
|
12465
|
+
this.graph.update(changeId, { status: "proposed", votes: [] });
|
|
12466
|
+
const eligible = this._eligibleVoters(change);
|
|
12467
|
+
this._notifyVoters(change, eligible, "vote_initiated");
|
|
12468
|
+
}
|
|
12469
|
+
/**
|
|
12470
|
+
* Cast a vote. Updates the change node in the graph and re-evaluates
|
|
12471
|
+
* consensus. If the vote triggers a resolution, updates the change status.
|
|
12472
|
+
*/
|
|
12473
|
+
castVote(changeId, voterId, value, rationale) {
|
|
12474
|
+
const change = this.graph.get(changeId);
|
|
12475
|
+
if (!change || change.type !== "change") {
|
|
12476
|
+
throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
|
|
12477
|
+
}
|
|
12478
|
+
const voter = this.voters.get(voterId);
|
|
12479
|
+
if (!voter) {
|
|
12480
|
+
throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
|
|
12481
|
+
}
|
|
12482
|
+
const eligible = this._eligibleVoters(change);
|
|
12483
|
+
if (!eligible.includes(voterId)) {
|
|
12484
|
+
throw new Error(`ConsensusProtocol: voter "${voterId}" is not eligible for this vote`);
|
|
12485
|
+
}
|
|
12486
|
+
const vote = {
|
|
12487
|
+
agentId: voterId,
|
|
12488
|
+
agentName: voter.agentName,
|
|
12489
|
+
value,
|
|
12490
|
+
rationale,
|
|
12491
|
+
votedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12492
|
+
};
|
|
12493
|
+
const existingIdx = change.votes.findIndex((v) => v.agentId === voterId);
|
|
12494
|
+
const newVotes = existingIdx >= 0 ? change.votes.with(existingIdx, vote) : [...change.votes, vote];
|
|
12495
|
+
const result = this._resolve(changeId, newVotes, eligible);
|
|
12496
|
+
this.graph.update(changeId, {
|
|
12497
|
+
votes: newVotes,
|
|
12498
|
+
...result.outcome !== "pending" ? { status: this._toChangeStatus(result.outcome) } : {}
|
|
12499
|
+
});
|
|
12500
|
+
this._notifyVoters(change, eligible, "vote_cast", { voterId, value, result });
|
|
12501
|
+
return result;
|
|
12502
|
+
}
|
|
12503
|
+
/**
|
|
12504
|
+
* Resolve the current vote without waiting for all eligible voters.
|
|
12505
|
+
* Useful when a timeout fires or an agent decides to finalize early.
|
|
12506
|
+
*/
|
|
12507
|
+
resolveNow(changeId) {
|
|
12508
|
+
const change = this.graph.get(changeId);
|
|
12509
|
+
if (!change) throw new Error(`ConsensusProtocol: unknown change "${changeId}"`);
|
|
12510
|
+
const eligible = this._eligibleVoters(change);
|
|
12511
|
+
const result = this._resolve(changeId, change.votes, eligible);
|
|
12512
|
+
if (result.outcome !== "pending") {
|
|
12513
|
+
this.graph.update(changeId, { status: this._toChangeStatus(result.outcome) });
|
|
12514
|
+
this._notifyVoters(change, eligible, "vote_resolved", { result });
|
|
12515
|
+
}
|
|
12516
|
+
return result;
|
|
12517
|
+
}
|
|
12518
|
+
/**
|
|
12519
|
+
* Register or update a voter's configuration.
|
|
12520
|
+
*/
|
|
12521
|
+
registerVoter(config) {
|
|
12522
|
+
this.voters.set(config.agentId, config);
|
|
12523
|
+
}
|
|
12524
|
+
/**
|
|
12525
|
+
* Get the current vote status for a change.
|
|
12526
|
+
*/
|
|
12527
|
+
getStatus(changeId) {
|
|
12528
|
+
const change = this.graph.get(changeId);
|
|
12529
|
+
if (!change || change.type !== "change") return null;
|
|
12530
|
+
const eligible = this._eligibleVoters(change);
|
|
12531
|
+
return this._resolve(changeId, change.votes, eligible);
|
|
12532
|
+
}
|
|
12533
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
12534
|
+
_eligibleVoters(_change) {
|
|
12535
|
+
return Array.from(this.voters.keys());
|
|
12536
|
+
}
|
|
12537
|
+
_resolve(changeId, votes, eligible) {
|
|
12538
|
+
const totalEligible = eligible.length;
|
|
12539
|
+
const approve = votes.filter((v) => v.value === "approve");
|
|
12540
|
+
const reject = votes.filter((v) => v.value === "reject");
|
|
12541
|
+
const abstain = votes.filter((v) => v.value === "abstain");
|
|
12542
|
+
const totalWeightApprove = approve.reduce(
|
|
12543
|
+
(sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
|
|
12544
|
+
0
|
|
12545
|
+
);
|
|
12546
|
+
const totalWeightReject = reject.reduce(
|
|
12547
|
+
(sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
|
|
12548
|
+
0
|
|
12549
|
+
);
|
|
12550
|
+
const totalWeight = Array.from(this.voters.values()).reduce(
|
|
12551
|
+
(sum, v) => sum + v.weight,
|
|
12552
|
+
0
|
|
12553
|
+
);
|
|
12554
|
+
const castCount = votes.length;
|
|
12555
|
+
const quorumRequired = Math.ceil(totalEligible * this.rules.quorumFraction);
|
|
12556
|
+
const quorumMet = castCount >= quorumRequired;
|
|
12557
|
+
for (const v of reject) {
|
|
12558
|
+
const config = this.voters.get(v.agentId);
|
|
12559
|
+
if (config?.veto && this.rules.vetoRoles.includes(config.role)) {
|
|
12560
|
+
return {
|
|
12561
|
+
changeId,
|
|
12562
|
+
outcome: "vetoed",
|
|
12563
|
+
votes,
|
|
12564
|
+
approveCount: approve.length,
|
|
12565
|
+
rejectCount: reject.length,
|
|
12566
|
+
abstainCount: abstain.length,
|
|
12567
|
+
totalWeightApprove,
|
|
12568
|
+
totalWeightReject,
|
|
12569
|
+
eligibleVoters: eligible,
|
|
12570
|
+
quorumMet,
|
|
12571
|
+
approvalMet: false,
|
|
12572
|
+
vetoedBy: config.agentId,
|
|
12573
|
+
rationale: `Hard veto from role "${config.role}" (${config.agentName}).`
|
|
12574
|
+
};
|
|
12575
|
+
}
|
|
12576
|
+
}
|
|
12577
|
+
if (!quorumMet) {
|
|
12578
|
+
return {
|
|
12579
|
+
changeId,
|
|
12580
|
+
outcome: "pending",
|
|
12581
|
+
votes,
|
|
12582
|
+
approveCount: approve.length,
|
|
12583
|
+
rejectCount: reject.length,
|
|
12584
|
+
abstainCount: abstain.length,
|
|
12585
|
+
totalWeightApprove,
|
|
12586
|
+
totalWeightReject,
|
|
12587
|
+
eligibleVoters: eligible,
|
|
12588
|
+
quorumMet: false,
|
|
12589
|
+
approvalMet: false,
|
|
12590
|
+
rationale: `Quorum not met: ${castCount}/${quorumRequired} required.`
|
|
12591
|
+
};
|
|
12592
|
+
}
|
|
12593
|
+
const approvalRequired = Math.ceil(castCount * this.rules.approvalFraction);
|
|
12594
|
+
const approvalMet = approve.length >= approvalRequired;
|
|
12595
|
+
if (this.rules.approvalWeightFraction !== void 0 && approvalMet) {
|
|
12596
|
+
const weightRequired = totalWeight * this.rules.approvalWeightFraction;
|
|
12597
|
+
if (totalWeightApprove < weightRequired) {
|
|
12598
|
+
return {
|
|
12599
|
+
changeId,
|
|
12600
|
+
outcome: "rejected",
|
|
12601
|
+
votes,
|
|
12602
|
+
approveCount: approve.length,
|
|
12603
|
+
rejectCount: reject.length,
|
|
12604
|
+
abstainCount: abstain.length,
|
|
12605
|
+
totalWeightApprove,
|
|
12606
|
+
totalWeightReject,
|
|
12607
|
+
eligibleVoters: eligible,
|
|
12608
|
+
quorumMet: true,
|
|
12609
|
+
approvalMet: false,
|
|
12610
|
+
rationale: `Weight threshold not met: ${totalWeightApprove.toFixed(2)}/${weightRequired.toFixed(2)} required.`
|
|
12611
|
+
};
|
|
12612
|
+
}
|
|
12613
|
+
}
|
|
12614
|
+
if (approvalMet) {
|
|
12615
|
+
return {
|
|
12616
|
+
changeId,
|
|
12617
|
+
outcome: "approved",
|
|
12618
|
+
votes,
|
|
12619
|
+
approveCount: approve.length,
|
|
12620
|
+
rejectCount: reject.length,
|
|
12621
|
+
abstainCount: abstain.length,
|
|
12622
|
+
totalWeightApprove,
|
|
12623
|
+
totalWeightReject,
|
|
12624
|
+
eligibleVoters: eligible,
|
|
12625
|
+
quorumMet: true,
|
|
12626
|
+
approvalMet: true,
|
|
12627
|
+
rationale: `Approved: ${approve.length}/${castCount} votes (threshold: ${approvalRequired}).`
|
|
12628
|
+
};
|
|
12629
|
+
}
|
|
12630
|
+
return {
|
|
12631
|
+
changeId,
|
|
12632
|
+
outcome: "rejected",
|
|
12633
|
+
votes,
|
|
12634
|
+
approveCount: approve.length,
|
|
12635
|
+
rejectCount: reject.length,
|
|
12636
|
+
abstainCount: abstain.length,
|
|
12637
|
+
totalWeightApprove,
|
|
12638
|
+
totalWeightReject,
|
|
12639
|
+
eligibleVoters: eligible,
|
|
12640
|
+
quorumMet: true,
|
|
12641
|
+
approvalMet: false,
|
|
12642
|
+
rationale: `Rejected: ${approve.length}/${castCount} approve votes (threshold: ${approvalRequired}).`
|
|
12643
|
+
};
|
|
12644
|
+
}
|
|
12645
|
+
_toChangeStatus(outcome) {
|
|
12646
|
+
switch (outcome) {
|
|
12647
|
+
case "approved":
|
|
12648
|
+
return "approved";
|
|
12649
|
+
case "rejected":
|
|
12650
|
+
return "rejected";
|
|
12651
|
+
case "vetoed":
|
|
12652
|
+
return "rejected";
|
|
12653
|
+
case "quorum_not_met":
|
|
12654
|
+
return "proposed";
|
|
12655
|
+
default:
|
|
12656
|
+
return "proposed";
|
|
12657
|
+
}
|
|
12658
|
+
}
|
|
12659
|
+
_notifyVoters(change, eligible, event, extra) {
|
|
12660
|
+
if (!this.fleet) return;
|
|
12661
|
+
this.fleet.emit({
|
|
12662
|
+
subagentId: "consensus",
|
|
12663
|
+
ts: Date.now(),
|
|
12664
|
+
type: `consensus:${event}`,
|
|
12665
|
+
payload: { changeId: change.id, changeTitle: change.title, eligible, ...extra }
|
|
12666
|
+
});
|
|
12667
|
+
}
|
|
12668
|
+
};
|
|
12669
|
+
|
|
12670
|
+
// src/coordination/change-manager.ts
|
|
12671
|
+
var DEFAULT_QUALITY_CHECKS = {
|
|
12672
|
+
runTests: true,
|
|
12673
|
+
runTypecheck: true,
|
|
12674
|
+
runLint: false,
|
|
12675
|
+
// warning only
|
|
12676
|
+
runSecurityScan: true,
|
|
12677
|
+
checkTestCoverage: false,
|
|
12678
|
+
minCoveragePercent: 70
|
|
12679
|
+
};
|
|
12680
|
+
var ChangeManager = class {
|
|
12681
|
+
graph;
|
|
12682
|
+
consensus;
|
|
12683
|
+
fleet;
|
|
12684
|
+
checks;
|
|
12685
|
+
/** Track applied changes for rollback lookup. */
|
|
12686
|
+
appliedChanges = /* @__PURE__ */ new Map();
|
|
12687
|
+
// changeId → rollbackId
|
|
12688
|
+
constructor(opts) {
|
|
12689
|
+
this.graph = opts.graph;
|
|
12690
|
+
this.consensus = opts.consensus;
|
|
12691
|
+
this.fleet = opts.fleet ?? void 0;
|
|
12692
|
+
this.checks = { ...DEFAULT_QUALITY_CHECKS, ...opts.checks };
|
|
12693
|
+
}
|
|
12694
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
12695
|
+
/**
|
|
12696
|
+
* Propose a new code change. Creates a ChangeNode in the knowledge graph.
|
|
12697
|
+
* Does NOT automatically initiate voting — call `submitForReview()` for that.
|
|
12698
|
+
*/
|
|
12699
|
+
async propose(input) {
|
|
12700
|
+
const node = await this.graph.add({
|
|
12701
|
+
type: "change",
|
|
12702
|
+
title: input.title,
|
|
12703
|
+
description: input.description,
|
|
12704
|
+
files: input.files.map((f) => ({
|
|
12705
|
+
path: f.path,
|
|
12706
|
+
action: f.action
|
|
12707
|
+
})),
|
|
12708
|
+
status: "proposed",
|
|
12709
|
+
proposedBy: input.proposedBy,
|
|
12710
|
+
proposedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12711
|
+
approvedBy: [],
|
|
12712
|
+
rejectedBy: [],
|
|
12713
|
+
votes: [],
|
|
12714
|
+
qualityGate: { passed: false, checks: [] },
|
|
12715
|
+
// filled after quality gate
|
|
12716
|
+
satisfiesGoals: input.satisfiesGoals
|
|
12717
|
+
});
|
|
12718
|
+
void this._runQualityGate(node.id, input.files).then((gate) => {
|
|
12719
|
+
void this.graph.update(node.id, { qualityGate: gate });
|
|
12720
|
+
});
|
|
12721
|
+
this._emit("change:proposed", { changeId: node.id, title: node.title });
|
|
12722
|
+
return node;
|
|
12723
|
+
}
|
|
12724
|
+
/**
|
|
12725
|
+
* Submit an approved change for application.
|
|
12726
|
+
* Returns the change node — actual file mutations are performed by agents
|
|
12727
|
+
* acting on this node's data from the knowledge graph.
|
|
12728
|
+
*/
|
|
12729
|
+
async submitForReview(changeId) {
|
|
12730
|
+
const change = this.graph.get(changeId);
|
|
12731
|
+
if (!change || change.type !== "change") {
|
|
12732
|
+
throw new Error(`ChangeManager: no change found "${changeId}"`);
|
|
12733
|
+
}
|
|
12734
|
+
if (change.status !== "proposed") {
|
|
12735
|
+
throw new Error(`ChangeManager: change "${changeId}" is not in 'proposed' state`);
|
|
12736
|
+
}
|
|
12737
|
+
this.consensus.initiateVote(changeId);
|
|
12738
|
+
this._emit("change:submitted_for_review", { changeId, title: change.title });
|
|
12739
|
+
}
|
|
12740
|
+
/**
|
|
12741
|
+
* Apply an approved change. Updates the change node to 'applied'.
|
|
12742
|
+
* Agents should watch for 'applied' status and perform the actual file mutations.
|
|
12743
|
+
*/
|
|
12744
|
+
async markApplied(changeId, appliedAt) {
|
|
12745
|
+
const change = this.graph.get(changeId);
|
|
12746
|
+
if (!change) return null;
|
|
12747
|
+
const updated = await this.graph.update(changeId, {
|
|
12748
|
+
status: "applied",
|
|
12749
|
+
appliedAt
|
|
12750
|
+
});
|
|
12751
|
+
if (updated) {
|
|
12752
|
+
this.appliedChanges.set(changeId, "");
|
|
12753
|
+
this._emit("change:applied", { changeId, title: updated.title, files: updated.files });
|
|
12754
|
+
}
|
|
12755
|
+
return updated;
|
|
12756
|
+
}
|
|
12757
|
+
/**
|
|
12758
|
+
* Mark a change as applied and trigger rollback for any satisfied goal
|
|
12759
|
+
* that turns out to be broken.
|
|
12760
|
+
*/
|
|
12761
|
+
async markAppliedWithVerification(changeId, verify) {
|
|
12762
|
+
const change = this.graph.get(changeId);
|
|
12763
|
+
if (!change) throw new Error(`ChangeManager: unknown change "${changeId}"`);
|
|
12764
|
+
const appliedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12765
|
+
await this.markApplied(changeId, appliedAt);
|
|
12766
|
+
const verificationResult = await verify();
|
|
12767
|
+
if (!verificationResult.passed) {
|
|
12768
|
+
const rollbackResult = await this.proposeRollback(changeId, "Quality gate failed after apply");
|
|
12769
|
+
return {
|
|
12770
|
+
changeId,
|
|
12771
|
+
success: false,
|
|
12772
|
+
appliedAt,
|
|
12773
|
+
filesTouched: change.files.map((f) => f.path),
|
|
12774
|
+
verificationResult,
|
|
12775
|
+
rollbackChangeId: rollbackResult?.id,
|
|
12776
|
+
error: `Quality gate failed: ${verificationResult.checks.filter((c) => !c.passed).map((c) => c.name).join(", ")}`
|
|
12777
|
+
};
|
|
12778
|
+
}
|
|
12779
|
+
return {
|
|
12780
|
+
changeId,
|
|
12781
|
+
success: true,
|
|
12782
|
+
appliedAt,
|
|
12783
|
+
filesTouched: change.files.map((f) => f.path),
|
|
12784
|
+
verificationResult
|
|
12785
|
+
};
|
|
12786
|
+
}
|
|
12787
|
+
/**
|
|
12788
|
+
* Propose a rollback for an applied change. Creates a new change that
|
|
12789
|
+
* reverses the original. Goes through full consensus.
|
|
12790
|
+
*/
|
|
12791
|
+
async proposeRollback(appliedChangeId, reason) {
|
|
12792
|
+
const original = this.graph.get(appliedChangeId);
|
|
12793
|
+
if (!original || original.type !== "change") return null;
|
|
12794
|
+
const rollbackFiles = original.files.map((f) => ({
|
|
12795
|
+
path: f.path,
|
|
12796
|
+
action: f.action === "create" ? "delete" : f.action === "delete" ? "create" : "modify"
|
|
12797
|
+
}));
|
|
12798
|
+
const rollback = await this.propose({
|
|
12799
|
+
title: `Rollback: ${original.title}`,
|
|
12800
|
+
description: `Rollback of "${original.title}" applied at ${original.appliedAt}. Reason: ${reason}`,
|
|
12801
|
+
files: rollbackFiles,
|
|
12802
|
+
proposedBy: "change-manager",
|
|
12803
|
+
satisfiesGoals: [],
|
|
12804
|
+
tags: ["rollback", `original:${appliedChangeId}`]
|
|
12805
|
+
});
|
|
12806
|
+
this.appliedChanges.set(appliedChangeId, rollback.id);
|
|
12807
|
+
await this.graph.update(appliedChangeId, {
|
|
12808
|
+
rolledBackAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12809
|
+
rollbackReason: reason
|
|
12810
|
+
});
|
|
12811
|
+
this._emit("change:rollback_proposed", {
|
|
12812
|
+
originalChangeId: appliedChangeId,
|
|
12813
|
+
rollbackChangeId: rollback.id,
|
|
12814
|
+
reason
|
|
12815
|
+
});
|
|
12816
|
+
return rollback;
|
|
12817
|
+
}
|
|
12818
|
+
/**
|
|
12819
|
+
* Mark a change as rolled back.
|
|
12820
|
+
*/
|
|
12821
|
+
async markRolledBack(changeId, rolledBackAt) {
|
|
12822
|
+
const updated = await this.graph.update(changeId, {
|
|
12823
|
+
status: "rolled_back",
|
|
12824
|
+
rolledBackAt
|
|
12825
|
+
});
|
|
12826
|
+
if (updated) {
|
|
12827
|
+
this._emit("change:rolled_back", { changeId, title: updated.title });
|
|
12828
|
+
}
|
|
12829
|
+
return updated;
|
|
12830
|
+
}
|
|
12831
|
+
// ── Queries ───────────────────────────────────────────────────────────
|
|
12832
|
+
getPendingReviews() {
|
|
12833
|
+
return this.graph.getChanges({ status: "proposed" });
|
|
12834
|
+
}
|
|
12835
|
+
getAppliedChanges() {
|
|
12836
|
+
return this.graph.getChanges({ status: "applied" });
|
|
12837
|
+
}
|
|
12838
|
+
getChange(id) {
|
|
12839
|
+
return this.graph.get(id);
|
|
12840
|
+
}
|
|
12841
|
+
getChangesForGoal(goalId) {
|
|
12842
|
+
return this.graph.getChanges({}).filter(
|
|
12843
|
+
(c) => c.satisfiesGoals.includes(goalId)
|
|
12844
|
+
);
|
|
12845
|
+
}
|
|
12846
|
+
// ── Quality gate ──────────────────────────────────────────────────────
|
|
12847
|
+
/**
|
|
12848
|
+
* Run quality gate checks. This is informational — actual test/lint/typecheck
|
|
12849
|
+
* execution is done by agents spawned for this purpose. This method stores
|
|
12850
|
+
* the result in the change node.
|
|
12851
|
+
*/
|
|
12852
|
+
async _runQualityGate(_changeId, _files) {
|
|
12853
|
+
const checks = [];
|
|
12854
|
+
if (this.checks.runTests) {
|
|
12855
|
+
checks.push({ name: "tests", passed: false, detail: "Tests must be run by a verify agent" });
|
|
12856
|
+
}
|
|
12857
|
+
if (this.checks.runTypecheck) {
|
|
12858
|
+
checks.push({ name: "typecheck", passed: false, detail: "TypeScript must compile" });
|
|
12859
|
+
}
|
|
12860
|
+
if (this.checks.runSecurityScan) {
|
|
12861
|
+
checks.push({ name: "security", passed: false, detail: "Security scan must pass" });
|
|
12862
|
+
}
|
|
12863
|
+
if (this.checks.runLint) {
|
|
12864
|
+
checks.push({ name: "lint", passed: false, detail: "Lint check" });
|
|
12865
|
+
}
|
|
12866
|
+
const result = {
|
|
12867
|
+
passed: checks.length === 0,
|
|
12868
|
+
checks
|
|
12869
|
+
};
|
|
12870
|
+
await this.graph.update(_changeId, { qualityGate: result });
|
|
12871
|
+
return result;
|
|
12872
|
+
}
|
|
12873
|
+
/**
|
|
12874
|
+
* Update quality gate result for a change. Called by verify agents
|
|
12875
|
+
* after running their checks.
|
|
12876
|
+
*/
|
|
12877
|
+
async updateQualityGate(changeId, checkName, result) {
|
|
12878
|
+
const change = this.graph.get(changeId);
|
|
12879
|
+
if (!change) return;
|
|
12880
|
+
const checks = change.qualityGate.checks.map(
|
|
12881
|
+
(c) => c.name === checkName ? { ...c, ...result } : c
|
|
12882
|
+
);
|
|
12883
|
+
const allPassed = checks.every((c) => c.passed);
|
|
12884
|
+
await this.graph.update(changeId, {
|
|
12885
|
+
qualityGate: { passed: allPassed, checks }
|
|
12886
|
+
});
|
|
12887
|
+
this._emit("quality_gate:updated", { changeId, checkName, passed: result.passed });
|
|
12888
|
+
}
|
|
12889
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
12890
|
+
_emit(type, payload) {
|
|
12891
|
+
if (!this.fleet) return;
|
|
12892
|
+
this.fleet.emit({ subagentId: "change-manager", ts: Date.now(), type, payload });
|
|
12893
|
+
}
|
|
12894
|
+
};
|
|
12895
|
+
var AutonomousBrain = class {
|
|
12896
|
+
graph;
|
|
12897
|
+
// Fleet bus for emitting decisions — null-safe, no-op if not provided
|
|
12898
|
+
fleetBus;
|
|
12899
|
+
llmProvider;
|
|
12900
|
+
maxRetries;
|
|
12901
|
+
consensusRiskThreshold;
|
|
12902
|
+
selfImprove;
|
|
12903
|
+
/** Decision history for self-improvement and audit. */
|
|
12904
|
+
decisionHistory = [];
|
|
12905
|
+
/** Tracks failure patterns for self-improvement. */
|
|
12906
|
+
failurePatterns = /* @__PURE__ */ new Map();
|
|
12907
|
+
RISK_ORDER = ["low", "medium", "high", "critical"];
|
|
12908
|
+
// ── Fleet bus integration ─────────────────────────────────────────────
|
|
12909
|
+
_emit(type, payload) {
|
|
12910
|
+
if (!this.fleetBus) return;
|
|
12911
|
+
this.fleetBus.emit({ subagentId: "brain", ts: Date.now(), type, payload });
|
|
12912
|
+
}
|
|
12913
|
+
constructor(opts) {
|
|
12914
|
+
this.graph = opts.graph;
|
|
12915
|
+
this.fleetBus = opts.fleet ?? void 0;
|
|
12916
|
+
this.llmProvider = opts.llmProvider;
|
|
12917
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
12918
|
+
this.consensusRiskThreshold = opts.consensusRiskThreshold ?? "high";
|
|
12919
|
+
this.selfImprove = opts.selfImprove ?? true;
|
|
12920
|
+
}
|
|
12921
|
+
// ── BrainArbiter interface ────────────────────────────────────────────
|
|
12922
|
+
/** Implements BrainArbiter — bridges standard brain.ts interface to autonomous engine. */
|
|
12923
|
+
async decide(request) {
|
|
12924
|
+
return this.decideAuto(this._toAutonomous(request));
|
|
12925
|
+
}
|
|
12926
|
+
// ── Main entry point ──────────────────────────────────────────────────
|
|
12927
|
+
/**
|
|
12928
|
+
* Primary autonomous decision engine — receives AutonomousDecisionRequest,
|
|
12929
|
+
* queries the LLM, records the decision, and returns a BrainDecision.
|
|
12930
|
+
*
|
|
12931
|
+
* Specialized methods (decideSpawn, decideApproval, etc.) should call this
|
|
12932
|
+
* directly with a pre-built AutonomousDecisionRequest.
|
|
12933
|
+
*/
|
|
12934
|
+
async decideAuto(request) {
|
|
12935
|
+
const { id, decisionType, question, context, options, risk, requiresConsensus } = request;
|
|
12936
|
+
const history = this.selfImprove ? this._loadHistory(decisionType, risk) : [];
|
|
12937
|
+
const hints = this.selfImprove ? this._getSelfImproveHints(decisionType) : [];
|
|
12938
|
+
const prompt = {
|
|
12939
|
+
decisionType,
|
|
12940
|
+
question,
|
|
12941
|
+
context: this._serializeContext(context),
|
|
12942
|
+
options,
|
|
12943
|
+
risk,
|
|
12944
|
+
decisionHistory: history,
|
|
12945
|
+
selfImproveHints: hints
|
|
12946
|
+
};
|
|
12947
|
+
let result;
|
|
12948
|
+
try {
|
|
12949
|
+
result = await this.llmProvider.decide(prompt);
|
|
12950
|
+
} catch (err) {
|
|
12951
|
+
const recommended = options.find((o) => o.recommended);
|
|
12952
|
+
if (recommended && risk === "low") {
|
|
12953
|
+
return { type: "answer", optionId: recommended.id, text: recommended.label };
|
|
12954
|
+
}
|
|
12955
|
+
return { type: "deny", reason: `Brain LLM failed: ${String(err)}` };
|
|
12956
|
+
}
|
|
12957
|
+
void this._recordDecision({
|
|
12958
|
+
id,
|
|
12959
|
+
decisionType,
|
|
12960
|
+
question,
|
|
12961
|
+
options,
|
|
12962
|
+
chosen: result.optionId,
|
|
12963
|
+
rationale: result.rationale,
|
|
12964
|
+
madeBy: "autonomous-brain",
|
|
12965
|
+
context: JSON.stringify(context)
|
|
12966
|
+
});
|
|
12967
|
+
if (requiresConsensus) {
|
|
12968
|
+
this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: true });
|
|
12969
|
+
return {
|
|
12970
|
+
type: "answer",
|
|
12971
|
+
optionId: result.optionId,
|
|
12972
|
+
text: options.find((o) => o.id === result.optionId)?.label ?? result.optionId,
|
|
12973
|
+
rationale: `${result.rationale}
|
|
12974
|
+
|
|
12975
|
+
\u26A0\uFE0F This decision requires consensus approval before execution.`
|
|
12976
|
+
};
|
|
12977
|
+
}
|
|
12978
|
+
this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: false });
|
|
12979
|
+
return {
|
|
12980
|
+
type: "answer",
|
|
12981
|
+
optionId: result.optionId,
|
|
12982
|
+
text: options.find((o) => o.id === result.optionId)?.label ?? result.optionId,
|
|
12983
|
+
rationale: result.rationale
|
|
12984
|
+
};
|
|
12985
|
+
}
|
|
12986
|
+
// ── Specialized decision methods ────────────────────────────────────
|
|
12987
|
+
/**
|
|
12988
|
+
* Decide whether to spawn a subagent, which role to use, and what budget.
|
|
12989
|
+
*/
|
|
12990
|
+
async decideSpawn(source, taskDescription, availableFacts, fleetStatus) {
|
|
12991
|
+
const roleHints = this._inferRoles(taskDescription);
|
|
12992
|
+
const risk = roleHints.length > 1 ? "medium" : "low";
|
|
12993
|
+
const options = roleHints.map((role, i) => ({
|
|
12994
|
+
id: `spawn:${role}`,
|
|
12995
|
+
label: `Spawn ${role} agent`,
|
|
12996
|
+
risk: i === 0 ? "low" : "medium",
|
|
12997
|
+
recommended: i === 0,
|
|
12998
|
+
consequence: i === 0 ? `Spawn the most appropriate agent for: ${taskDescription.slice(0, 80)}` : `Spawn an alternative agent for the same task`
|
|
12999
|
+
}));
|
|
13000
|
+
return this.decideAuto({
|
|
13001
|
+
id: randomUUID(),
|
|
13002
|
+
source,
|
|
13003
|
+
decisionType: "spawn",
|
|
13004
|
+
question: `Should we spawn a subagent for this task?`,
|
|
13005
|
+
context: {
|
|
13006
|
+
facts: availableFacts,
|
|
13007
|
+
fleetStatus,
|
|
13008
|
+
taskDescription
|
|
13009
|
+
},
|
|
13010
|
+
options,
|
|
13011
|
+
risk,
|
|
13012
|
+
requiresConsensus: false
|
|
13013
|
+
});
|
|
13014
|
+
}
|
|
13015
|
+
/**
|
|
13016
|
+
* Decide whether to approve a proposed change.
|
|
13017
|
+
*/
|
|
13018
|
+
async decideApproval(source, change, relevantFacts) {
|
|
13019
|
+
const risk = this._changeRisk(change);
|
|
13020
|
+
const options = [
|
|
13021
|
+
{
|
|
13022
|
+
id: "approve",
|
|
13023
|
+
label: "Approve change",
|
|
13024
|
+
recommended: change.qualityGate.passed && relevantFacts.filter((f) => f.severity === "critical").length === 0,
|
|
13025
|
+
risk,
|
|
13026
|
+
consequence: `Apply changes to: ${change.files.map((f) => f.path).join(", ")}`
|
|
13027
|
+
},
|
|
13028
|
+
{
|
|
13029
|
+
id: "reject",
|
|
13030
|
+
label: "Reject change",
|
|
13031
|
+
recommended: false,
|
|
13032
|
+
risk: "medium",
|
|
13033
|
+
consequence: "Return change to proposer with feedback"
|
|
13034
|
+
},
|
|
13035
|
+
{
|
|
13036
|
+
id: "request_changes",
|
|
13037
|
+
label: "Request specific changes",
|
|
13038
|
+
recommended: false,
|
|
13039
|
+
risk: "low",
|
|
13040
|
+
consequence: "Send back for revision with conditions"
|
|
13041
|
+
}
|
|
13042
|
+
];
|
|
13043
|
+
return this.decideAuto({
|
|
13044
|
+
id: randomUUID(),
|
|
13045
|
+
source,
|
|
13046
|
+
decisionType: "approve_change",
|
|
13047
|
+
question: `Should we approve the change "${change.title}"?`,
|
|
13048
|
+
context: {
|
|
13049
|
+
facts: relevantFacts,
|
|
13050
|
+
change
|
|
13051
|
+
},
|
|
13052
|
+
options,
|
|
13053
|
+
risk,
|
|
13054
|
+
requiresConsensus: risk === "critical" || risk === "high"
|
|
13055
|
+
});
|
|
13056
|
+
}
|
|
13057
|
+
/**
|
|
13058
|
+
* Decide how to handle a failed task.
|
|
13059
|
+
*/
|
|
13060
|
+
async decideEscalation(source, taskId, error, attempts) {
|
|
13061
|
+
const retryCount = this.failurePatterns.get(taskId)?.failures ?? attempts;
|
|
13062
|
+
const options = [];
|
|
13063
|
+
if (retryCount < this.maxRetries) {
|
|
13064
|
+
options.push({
|
|
13065
|
+
id: "retry",
|
|
13066
|
+
label: `Retry task (attempt ${retryCount + 1}/${this.maxRetries})`,
|
|
13067
|
+
recommended: retryCount < 2,
|
|
13068
|
+
risk: "medium",
|
|
13069
|
+
consequence: `Restart the task with same or adjusted budget`
|
|
13070
|
+
});
|
|
13071
|
+
options.push({
|
|
13072
|
+
id: "retry_with_adjustment",
|
|
13073
|
+
label: `Retry with more budget`,
|
|
13074
|
+
recommended: retryCount >= 1,
|
|
13075
|
+
risk: "medium",
|
|
13076
|
+
consequence: `Increase timeout or iterations before retrying`
|
|
13077
|
+
});
|
|
13078
|
+
}
|
|
13079
|
+
options.push({
|
|
13080
|
+
id: "delegate",
|
|
13081
|
+
label: "Delegate to different role",
|
|
13082
|
+
recommended: retryCount >= 1,
|
|
13083
|
+
risk: "medium",
|
|
13084
|
+
consequence: "Try a different agent role for the same task"
|
|
13085
|
+
});
|
|
13086
|
+
if (retryCount >= this.maxRetries) {
|
|
13087
|
+
options.push({
|
|
13088
|
+
id: "mark_failed",
|
|
13089
|
+
label: "Mark task as failed",
|
|
13090
|
+
recommended: true,
|
|
13091
|
+
risk: "high",
|
|
13092
|
+
consequence: "Stop retrying, propagate failure upward"
|
|
13093
|
+
});
|
|
13094
|
+
}
|
|
13095
|
+
options.push({
|
|
13096
|
+
id: "decompose",
|
|
13097
|
+
label: "Decompose and retry in parts",
|
|
13098
|
+
recommended: false,
|
|
13099
|
+
risk: "low",
|
|
13100
|
+
consequence: "Break the task into smaller sub-tasks"
|
|
13101
|
+
});
|
|
13102
|
+
return this.decideAuto({
|
|
13103
|
+
id: randomUUID(),
|
|
13104
|
+
source,
|
|
13105
|
+
decisionType: "escalate_task",
|
|
13106
|
+
question: `Task failed: ${error.slice(0, 100)}. How should we proceed?`,
|
|
13107
|
+
context: { error, attempts: retryCount },
|
|
13108
|
+
options,
|
|
13109
|
+
risk: retryCount >= this.maxRetries ? "critical" : "medium",
|
|
13110
|
+
requiresConsensus: false
|
|
13111
|
+
});
|
|
13112
|
+
}
|
|
13113
|
+
// ── Self-improvement ─────────────────────────────────────────────────
|
|
13114
|
+
/**
|
|
13115
|
+
* Record the outcome of a decision for self-improvement.
|
|
13116
|
+
* Call this after a spawned agent completes or a change is applied.
|
|
13117
|
+
*/
|
|
13118
|
+
recordOutcome(decisionId, outcome, _detail) {
|
|
13119
|
+
const node = this.graph.get(decisionId);
|
|
13120
|
+
if (!node) return;
|
|
13121
|
+
const key = `decision:${node.decisionType}`;
|
|
13122
|
+
if (outcome === "failure") {
|
|
13123
|
+
const existing = this.failurePatterns.get(key) ?? { failures: 0, lastFailure: "" };
|
|
13124
|
+
existing.failures += 1;
|
|
13125
|
+
existing.lastFailure = (/* @__PURE__ */ new Date()).toISOString();
|
|
13126
|
+
this.failurePatterns.set(key, existing);
|
|
13127
|
+
} else {
|
|
13128
|
+
this.failurePatterns.delete(key);
|
|
13129
|
+
}
|
|
13130
|
+
void this.graph.update(decisionId, { decisionType: node.decisionType });
|
|
13131
|
+
}
|
|
13132
|
+
_getSelfImproveHints(decisionType) {
|
|
13133
|
+
const pattern = this.failurePatterns.get(`decision:${decisionType}`);
|
|
13134
|
+
if (!pattern || pattern.failures < 2) return [];
|
|
13135
|
+
return [
|
|
13136
|
+
`\u26A0\uFE0F ${decisionType} decisions have failed ${pattern.failures} times recently.`,
|
|
13137
|
+
"Consider alternative approaches before defaulting to this pattern."
|
|
13138
|
+
];
|
|
13139
|
+
}
|
|
13140
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
13141
|
+
_toAutonomous(req) {
|
|
13142
|
+
const decisionType = this._inferDecisionType(req);
|
|
13143
|
+
return {
|
|
13144
|
+
id: req.id,
|
|
13145
|
+
source: req.source,
|
|
13146
|
+
decisionType,
|
|
13147
|
+
question: req.question,
|
|
13148
|
+
context: {
|
|
13149
|
+
taskDescription: req.context ?? ""
|
|
13150
|
+
},
|
|
13151
|
+
options: req.options ?? [],
|
|
13152
|
+
risk: req.risk,
|
|
13153
|
+
requiresConsensus: this.RISK_ORDER.indexOf(req.risk) >= this.RISK_ORDER.indexOf(this.consensusRiskThreshold)
|
|
13154
|
+
};
|
|
13155
|
+
}
|
|
13156
|
+
_inferDecisionType(req) {
|
|
13157
|
+
if (req.question.toLowerCase().includes("spawn")) return "spawn";
|
|
13158
|
+
if (req.question.toLowerCase().includes("approve") || req.question.toLowerCase().includes("change")) return "approve_change";
|
|
13159
|
+
if (req.question.toLowerCase().includes("retry") || req.question.toLowerCase().includes("fail")) return "retry_task";
|
|
13160
|
+
if (req.question.toLowerCase().includes("priorit")) return "prioritize_goals";
|
|
13161
|
+
if (req.question.toLowerCase().includes("decompos")) return "decompose_goal";
|
|
13162
|
+
return "assign_task";
|
|
13163
|
+
}
|
|
13164
|
+
_serializeContext(ctx) {
|
|
13165
|
+
const parts = [];
|
|
13166
|
+
if (ctx.facts?.length) {
|
|
13167
|
+
parts.push(`## Relevant Facts
|
|
13168
|
+
${ctx.facts.map((f) => `- [${f.severity ?? "info"}] ${f.subject}: ${f.detail}`).join("\n")}`);
|
|
13169
|
+
}
|
|
13170
|
+
if (ctx.goals?.length) {
|
|
13171
|
+
parts.push(`## Active Goals
|
|
13172
|
+
${ctx.goals.map((g) => `- [${g.status}] ${g.priority}: ${g.title}`).join("\n")}`);
|
|
13173
|
+
}
|
|
13174
|
+
if (ctx.change) {
|
|
13175
|
+
const c = ctx.change;
|
|
13176
|
+
parts.push(`## Change Under Review
|
|
13177
|
+
- Title: ${c.title}
|
|
13178
|
+
- Status: ${c.status}
|
|
13179
|
+
- Files: ${c.files.map((f) => `${f.action} ${f.path}`).join(", ")}
|
|
13180
|
+
- Quality gate: ${c.qualityGate.passed ? "PASSED" : "FAILED"}
|
|
13181
|
+
Checks: ${c.qualityGate.checks.map((ch) => `${ch.name}:${ch.passed ? "\u2705" : "\u274C"}`).join(", ")}`);
|
|
13182
|
+
}
|
|
13183
|
+
if (ctx.fleetStatus) {
|
|
13184
|
+
parts.push(`## Fleet Status
|
|
13185
|
+
- Running: ${ctx.fleetStatus.running}, Idle: ${ctx.fleetStatus.idle}, Total: ${ctx.fleetStatus.total}
|
|
13186
|
+
- Cost so far: $${ctx.fleetStatus.costSoFar.toFixed(4)}`);
|
|
13187
|
+
}
|
|
13188
|
+
if (ctx.taskDescription) {
|
|
13189
|
+
parts.push(`## Task
|
|
13190
|
+
${ctx.taskDescription}`);
|
|
13191
|
+
}
|
|
13192
|
+
if (ctx.error) {
|
|
13193
|
+
parts.push(`## Error
|
|
13194
|
+
${ctx.error}`);
|
|
13195
|
+
}
|
|
13196
|
+
return parts.join("\n\n");
|
|
13197
|
+
}
|
|
13198
|
+
_loadHistory(type, _risk) {
|
|
13199
|
+
const all = this.graph.getDecisions().filter((d) => d.decisionType === type);
|
|
13200
|
+
return all.slice(-10);
|
|
13201
|
+
}
|
|
13202
|
+
async _recordDecision(input) {
|
|
13203
|
+
const node = await this.graph.add({
|
|
13204
|
+
type: "decision",
|
|
13205
|
+
decisionType: input.decisionType,
|
|
13206
|
+
question: input.question,
|
|
13207
|
+
options: input.options,
|
|
13208
|
+
chosen: input.chosen,
|
|
13209
|
+
rationale: input.rationale,
|
|
13210
|
+
madeBy: input.madeBy,
|
|
13211
|
+
madeAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13212
|
+
context: input.context
|
|
13213
|
+
});
|
|
13214
|
+
this.decisionHistory.push(node);
|
|
13215
|
+
return node;
|
|
13216
|
+
}
|
|
13217
|
+
_inferRoles(task) {
|
|
13218
|
+
const t = task.toLowerCase();
|
|
13219
|
+
if (t.includes("bug") || t.includes("error") || t.includes("crash")) return ["bug-hunter", "fixer"];
|
|
13220
|
+
if (t.includes("security") || t.includes("secret") || t.includes("injection")) return ["security-scanner"];
|
|
13221
|
+
if (t.includes("refactor") || t.includes("architecture") || t.includes("debt")) return ["refactor-planner", "critic"];
|
|
13222
|
+
if (t.includes("audit") || t.includes("log") || t.includes("analyze")) return ["audit-log"];
|
|
13223
|
+
if (t.includes("test") || t.includes("coverage")) return ["tester", "bug-hunter"];
|
|
13224
|
+
return ["bug-hunter", "refactor-planner"];
|
|
13225
|
+
}
|
|
13226
|
+
_changeRisk(change) {
|
|
13227
|
+
const criticalFiles = change.files.filter(
|
|
13228
|
+
(f) => f.path.includes("auth") || f.path.includes("config") || f.path.includes("schema")
|
|
13229
|
+
);
|
|
13230
|
+
if (criticalFiles.length > 0) return "high";
|
|
13231
|
+
if (change.files.length > 10) return "medium";
|
|
13232
|
+
return "low";
|
|
13233
|
+
}
|
|
13234
|
+
};
|
|
13235
|
+
var TaskAuctioneer = class {
|
|
13236
|
+
graph;
|
|
13237
|
+
fleet;
|
|
13238
|
+
mailbox;
|
|
13239
|
+
selfAgentId;
|
|
13240
|
+
bidWindowMs;
|
|
13241
|
+
maxTasksPerAgent;
|
|
13242
|
+
minConfidence;
|
|
13243
|
+
// minimum dispatcher confidence to accept a bid
|
|
13244
|
+
/** Pending bids keyed by taskId. */
|
|
13245
|
+
pendingBids = /* @__PURE__ */ new Map();
|
|
13246
|
+
/** Active bid windows keyed by taskId. */
|
|
13247
|
+
bidTimers = /* @__PURE__ */ new Map();
|
|
13248
|
+
/** Agent → current task count (from graph + in-flight). */
|
|
13249
|
+
agentTaskCounts = /* @__PURE__ */ new Map();
|
|
13250
|
+
constructor(opts) {
|
|
13251
|
+
this.graph = opts.graph;
|
|
13252
|
+
this.fleet = opts.fleet;
|
|
13253
|
+
this.mailbox = opts.mailbox;
|
|
13254
|
+
this.selfAgentId = opts.selfAgentId ?? "auctioneer";
|
|
13255
|
+
this.bidWindowMs = opts.bidWindowMs ?? 3e4;
|
|
13256
|
+
this.maxTasksPerAgent = opts.maxTasksPerAgent ?? 3;
|
|
13257
|
+
this.minConfidence = opts.minConfidence ?? 0.3;
|
|
13258
|
+
this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
|
|
13259
|
+
this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
|
|
13260
|
+
}
|
|
13261
|
+
// ── Publish a task ────────────────────────────────────────────────────
|
|
13262
|
+
/**
|
|
13263
|
+
* Publish a new task to the auction. Creates a GoalNode and broadcasts
|
|
13264
|
+
* it to all online agents. Returns the goal id.
|
|
13265
|
+
*
|
|
13266
|
+
* If `targetAgent` is specified, the task is assigned directly without auction.
|
|
13267
|
+
*/
|
|
13268
|
+
async publishTask(input) {
|
|
13269
|
+
const goal = await this.graph.add({
|
|
13270
|
+
type: "goal",
|
|
13271
|
+
title: input.title,
|
|
13272
|
+
description: input.description,
|
|
13273
|
+
status: input.targetAgent ? "in_progress" : "pending",
|
|
13274
|
+
priority: input.priority ?? "medium",
|
|
13275
|
+
assignee: input.targetAgent,
|
|
13276
|
+
blockedBy: [],
|
|
13277
|
+
dependsOn: input.satisfiesGoals ?? [],
|
|
13278
|
+
createdBy: this.selfAgentId,
|
|
13279
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13280
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13281
|
+
tags: input.tags ?? [],
|
|
13282
|
+
children: [],
|
|
13283
|
+
parentGoal: input.parentGoal
|
|
13284
|
+
});
|
|
13285
|
+
if (input.parentGoal) {
|
|
13286
|
+
const parent = this.graph.get(input.parentGoal);
|
|
13287
|
+
if (parent) {
|
|
13288
|
+
await this.graph.update(input.parentGoal, {
|
|
13289
|
+
children: [...parent.children, goal.id]
|
|
13290
|
+
});
|
|
13291
|
+
}
|
|
13292
|
+
}
|
|
13293
|
+
if (input.targetAgent) {
|
|
13294
|
+
await this._assignDirect(goal.id, input.targetAgent);
|
|
13295
|
+
} else {
|
|
13296
|
+
await this._broadcastTask(goal);
|
|
13297
|
+
this._startBidWindow(goal.id);
|
|
13298
|
+
}
|
|
13299
|
+
this._emit("task:published", {
|
|
13300
|
+
taskId: goal.id,
|
|
13301
|
+
title: goal.title,
|
|
13302
|
+
priority: goal.priority,
|
|
13303
|
+
tags: goal.tags
|
|
13304
|
+
});
|
|
13305
|
+
return goal.id;
|
|
13306
|
+
}
|
|
13307
|
+
// ── Bid on a task ─────────────────────────────────────────────────────
|
|
13308
|
+
/**
|
|
13309
|
+
* Submit a bid for a task. Called by agents who want to work on it.
|
|
13310
|
+
* Returns true if the bid was accepted, false if the task was already claimed.
|
|
13311
|
+
*/
|
|
13312
|
+
async bid(taskId, agent, rationale) {
|
|
13313
|
+
const goal = this.graph.get(taskId);
|
|
13314
|
+
if (!goal || goal.type !== "goal") return false;
|
|
13315
|
+
if (goal.status !== "pending") return false;
|
|
13316
|
+
const currentCount = this._getAgentTaskCount(agent.agentId);
|
|
13317
|
+
if (currentCount >= this.maxTasksPerAgent) return false;
|
|
13318
|
+
const dispatchResult = await dispatchAgent(goal.description);
|
|
13319
|
+
const score = dispatchResult.confidence * (dispatchResult.role === agent.agentRole ? 1.2 : 1);
|
|
13320
|
+
if (score < this.minConfidence) return false;
|
|
13321
|
+
const bid = {
|
|
13322
|
+
id: randomUUID(),
|
|
13323
|
+
taskId,
|
|
13324
|
+
agentId: agent.agentId,
|
|
13325
|
+
agentName: agent.agentName,
|
|
13326
|
+
agentRole: agent.agentRole,
|
|
13327
|
+
score,
|
|
13328
|
+
rationale,
|
|
13329
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13330
|
+
};
|
|
13331
|
+
let bids = this.pendingBids.get(taskId);
|
|
13332
|
+
if (!bids) {
|
|
13333
|
+
bids = [];
|
|
13334
|
+
this.pendingBids.set(taskId, bids);
|
|
13335
|
+
}
|
|
13336
|
+
const existingIdx = bids.findIndex((b) => b.agentId === agent.agentId);
|
|
13337
|
+
if (existingIdx >= 0) {
|
|
13338
|
+
bids[existingIdx] = bid;
|
|
13339
|
+
} else {
|
|
13340
|
+
bids.push(bid);
|
|
13341
|
+
}
|
|
13342
|
+
this._emit("task:bid", {
|
|
13343
|
+
taskId,
|
|
13344
|
+
bid: { ...bid, score: Math.round(score * 100) / 100 },
|
|
13345
|
+
agentName: agent.agentName
|
|
13346
|
+
});
|
|
13347
|
+
await this._mailboxPublish({
|
|
13348
|
+
type: "note",
|
|
13349
|
+
subject: `[bid] ${agent.agentName} \u2192 ${goal.title}`,
|
|
13350
|
+
body: `${agent.agentName} (${agent.agentRole}) bidded on task "${goal.title}" (${goal.id})
|
|
13351
|
+
Rationale: ${rationale}
|
|
13352
|
+
Score: ${score.toFixed(2)}`
|
|
13353
|
+
});
|
|
13354
|
+
return true;
|
|
13355
|
+
}
|
|
13356
|
+
// ── Claim (award) a task ───────────────────────────────────────────────
|
|
13357
|
+
/**
|
|
13358
|
+
* Award a task to a specific agent. Called internally by the bid window
|
|
13359
|
+
* expiry, or can be called directly to force an award.
|
|
13360
|
+
*/
|
|
13361
|
+
async claim(taskId, agentId, agentName) {
|
|
13362
|
+
const goal = this.graph.get(taskId);
|
|
13363
|
+
if (!goal || goal.type !== "goal") return false;
|
|
13364
|
+
if (goal.status !== "pending") return false;
|
|
13365
|
+
this._cancelBidWindow(taskId);
|
|
13366
|
+
await this.graph.update(taskId, {
|
|
13367
|
+
status: "in_progress",
|
|
13368
|
+
assignee: agentId,
|
|
13369
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13370
|
+
});
|
|
13371
|
+
this.pendingBids.delete(taskId);
|
|
13372
|
+
this.agentTaskCount(agentId, 1);
|
|
13373
|
+
await this._notifyAgent(agentId, {
|
|
13374
|
+
type: "assign",
|
|
13375
|
+
subject: `[assigned] ${goal.title}`,
|
|
13376
|
+
body: `You have been assigned: "${goal.title}"
|
|
13377
|
+
|
|
13378
|
+
${goal.description}
|
|
13379
|
+
|
|
13380
|
+
Task ID: ${taskId}
|
|
13381
|
+
Priority: ${goal.priority}`,
|
|
13382
|
+
taskContext: {
|
|
13383
|
+
agentRole: goal.tags[0],
|
|
13384
|
+
taskId,
|
|
13385
|
+
status: "in_progress"
|
|
13386
|
+
}
|
|
13387
|
+
});
|
|
13388
|
+
this._emit("task:claimed", { taskId, agentId, agentName });
|
|
13389
|
+
return true;
|
|
13390
|
+
}
|
|
13391
|
+
// ── Complete a task ────────────────────────────────────────────────────
|
|
13392
|
+
/**
|
|
13393
|
+
* Mark a task as done. Called by the agent when it finishes.
|
|
13394
|
+
*/
|
|
13395
|
+
async complete(taskId, _result) {
|
|
13396
|
+
const goal = this.graph.get(taskId);
|
|
13397
|
+
if (!goal) return;
|
|
13398
|
+
const agentId = goal.assignee ?? "unknown";
|
|
13399
|
+
this.agentTaskCount(agentId, -1);
|
|
13400
|
+
await this.graph.update(taskId, {
|
|
13401
|
+
status: "done",
|
|
13402
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13403
|
+
});
|
|
13404
|
+
for (const childId of goal.children) {
|
|
13405
|
+
const child = this.graph.get(childId);
|
|
13406
|
+
if (child) {
|
|
13407
|
+
const allUnblocked = child.blockedBy.every((blockedId) => {
|
|
13408
|
+
const blocked = this.graph.get(blockedId);
|
|
13409
|
+
return blocked?.status === "done";
|
|
13410
|
+
});
|
|
13411
|
+
if (allUnblocked) {
|
|
13412
|
+
await this.graph.update(childId, { status: "pending", updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13413
|
+
const unblockedGoal = this.graph.get(childId);
|
|
13414
|
+
await this._broadcastTask(unblockedGoal);
|
|
13415
|
+
}
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
this._emit("task:completed", { taskId, agentId, result: _result });
|
|
13419
|
+
await this._mailboxPublish({
|
|
13420
|
+
type: "result",
|
|
13421
|
+
subject: `[done] ${goal.title}`,
|
|
13422
|
+
body: `Task completed by ${agentId}: "${goal.title}"
|
|
13423
|
+
|
|
13424
|
+
${_result ?? "No result provided."}`
|
|
13425
|
+
});
|
|
13426
|
+
}
|
|
13427
|
+
/**
|
|
13428
|
+
* Mark a task as failed. Optionally spawn a retry.
|
|
13429
|
+
*/
|
|
13430
|
+
async fail(taskId, error) {
|
|
13431
|
+
const goal = this.graph.get(taskId);
|
|
13432
|
+
if (!goal) return;
|
|
13433
|
+
const agentId = goal.assignee ?? "unknown";
|
|
13434
|
+
this.agentTaskCount(agentId, -1);
|
|
13435
|
+
await this.graph.update(taskId, {
|
|
13436
|
+
status: "failed",
|
|
13437
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13438
|
+
result: error
|
|
13439
|
+
});
|
|
13440
|
+
this._emit("task:failed", { taskId, agentId, error });
|
|
13441
|
+
await this._mailboxPublish({
|
|
13442
|
+
type: "note",
|
|
13443
|
+
subject: `[failed] ${goal.title}`,
|
|
13444
|
+
body: `Task failed: "${goal.title}"
|
|
13445
|
+
Error: ${error}
|
|
13446
|
+
Assignee: ${agentId}`
|
|
13447
|
+
});
|
|
13448
|
+
}
|
|
13449
|
+
// ── Work finding ──────────────────────────────────────────────────────
|
|
13450
|
+
/**
|
|
13451
|
+
* Find the best available tasks for an agent based on its capabilities.
|
|
13452
|
+
* Returns tasks sorted by match score (best first).
|
|
13453
|
+
*/
|
|
13454
|
+
async findWork(_agentId, agentRole, limit = 5) {
|
|
13455
|
+
const pending = this.graph.getGoals({ status: "pending" });
|
|
13456
|
+
const scored = [];
|
|
13457
|
+
for (const goal of pending) {
|
|
13458
|
+
if (goal.blockedBy.length > 0) continue;
|
|
13459
|
+
const dispatchResult = await dispatchAgent(goal.description);
|
|
13460
|
+
const roleBonus = agentRole && goal.tags.includes(agentRole) ? 1.3 : 1;
|
|
13461
|
+
const priorityBonus = goal.priority === "critical" ? 1.5 : goal.priority === "high" ? 1.2 : 1;
|
|
13462
|
+
const score = dispatchResult.confidence * roleBonus * priorityBonus;
|
|
13463
|
+
const bids = this.pendingBids.get(goal.id)?.length ?? 0;
|
|
13464
|
+
scored.push({ task: goal, score, bids });
|
|
13465
|
+
}
|
|
13466
|
+
scored.sort((a, b) => b.score - a.score);
|
|
13467
|
+
return scored.slice(0, limit);
|
|
13468
|
+
}
|
|
13469
|
+
// ── Queries ──────────────────────────────────────────────────────────
|
|
13470
|
+
/** Get all pending tasks (available for bidding). */
|
|
13471
|
+
getPendingTasks() {
|
|
13472
|
+
return this.graph.getGoals({ status: "pending" }).filter((g) => g.blockedBy.length === 0);
|
|
13473
|
+
}
|
|
13474
|
+
/** Get tasks assigned to a specific agent. */
|
|
13475
|
+
getTasksForAgent(agentId) {
|
|
13476
|
+
return this.graph.getGoals({}).filter((g) => g.assignee === agentId);
|
|
13477
|
+
}
|
|
13478
|
+
/** Get the current bid count for a task. */
|
|
13479
|
+
getBidCount(taskId) {
|
|
13480
|
+
return this.pendingBids.get(taskId)?.length ?? 0;
|
|
13481
|
+
}
|
|
13482
|
+
/** Get bids for a task. */
|
|
13483
|
+
getBids(taskId) {
|
|
13484
|
+
return this.pendingBids.get(taskId) ?? [];
|
|
13485
|
+
}
|
|
13486
|
+
/** Get task stats for a project-wide dashboard. */
|
|
13487
|
+
getStats() {
|
|
13488
|
+
const all = this.graph.getGoals({});
|
|
13489
|
+
const pending = all.filter((g) => g.status === "pending" && g.blockedBy.length === 0);
|
|
13490
|
+
const inProgress = all.filter((g) => g.status === "in_progress");
|
|
13491
|
+
const done = all.filter((g) => g.status === "done");
|
|
13492
|
+
const failed = all.filter((g) => g.status === "failed");
|
|
13493
|
+
let totalBids = 0;
|
|
13494
|
+
for (const bids of this.pendingBids.values()) totalBids += bids.length;
|
|
13495
|
+
return {
|
|
13496
|
+
total: all.length,
|
|
13497
|
+
pending: pending.length,
|
|
13498
|
+
in_progress: inProgress.length,
|
|
13499
|
+
done: done.length,
|
|
13500
|
+
failed: failed.length,
|
|
13501
|
+
totalBids,
|
|
13502
|
+
avgBidsPerTask: pending.length > 0 ? totalBids / pending.length : 0
|
|
13503
|
+
};
|
|
13504
|
+
}
|
|
13505
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
13506
|
+
_emit(type, payload) {
|
|
13507
|
+
if (!this.fleet) return;
|
|
13508
|
+
this.fleet.emit({ subagentId: this.selfAgentId, ts: Date.now(), type, payload });
|
|
13509
|
+
}
|
|
13510
|
+
_broadcastTask(goal) {
|
|
13511
|
+
this._emit("task:available", {
|
|
13512
|
+
taskId: goal.id,
|
|
13513
|
+
title: goal.title,
|
|
13514
|
+
description: goal.description,
|
|
13515
|
+
priority: goal.priority,
|
|
13516
|
+
tags: goal.tags
|
|
13517
|
+
});
|
|
13518
|
+
void this._mailboxPublish({
|
|
13519
|
+
type: "broadcast",
|
|
13520
|
+
subject: `[task] ${goal.title} (${goal.priority})`,
|
|
13521
|
+
body: `New task available: "${goal.title}"
|
|
13522
|
+
Priority: ${goal.priority}
|
|
13523
|
+
Description: ${goal.description.slice(0, 200)}${goal.description.length > 200 ? "..." : ""}
|
|
13524
|
+
|
|
13525
|
+
Task ID: ${goal.id}
|
|
13526
|
+
Tags: ${goal.tags.join(", ") || "none"}
|
|
13527
|
+
|
|
13528
|
+
Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
|
|
13529
|
+
});
|
|
13530
|
+
}
|
|
13531
|
+
async _mailboxPublish(msg) {
|
|
13532
|
+
if (!this.mailbox) return;
|
|
13533
|
+
try {
|
|
13534
|
+
await this.mailbox.send({
|
|
13535
|
+
from: this.selfAgentId,
|
|
13536
|
+
to: "*",
|
|
13537
|
+
type: msg.type,
|
|
13538
|
+
subject: msg.subject,
|
|
13539
|
+
body: msg.body,
|
|
13540
|
+
priority: "normal"
|
|
13541
|
+
});
|
|
13542
|
+
} catch {
|
|
13543
|
+
}
|
|
13544
|
+
}
|
|
13545
|
+
async _notifyAgent(agentId, msg) {
|
|
13546
|
+
if (!this.mailbox) return;
|
|
13547
|
+
try {
|
|
13548
|
+
await this.mailbox.send({
|
|
13549
|
+
from: this.selfAgentId,
|
|
13550
|
+
to: agentId,
|
|
13551
|
+
type: msg.type,
|
|
13552
|
+
subject: msg.subject,
|
|
13553
|
+
body: msg.body,
|
|
13554
|
+
priority: "high",
|
|
13555
|
+
taskContext: msg.taskContext
|
|
13556
|
+
});
|
|
13557
|
+
} catch {
|
|
13558
|
+
}
|
|
13559
|
+
}
|
|
13560
|
+
_startBidWindow(taskId) {
|
|
13561
|
+
this._cancelBidWindow(taskId);
|
|
13562
|
+
const timer = setTimeout(async () => {
|
|
13563
|
+
this.bidTimers.delete(taskId);
|
|
13564
|
+
await this._evaluateBids(taskId);
|
|
13565
|
+
}, this.bidWindowMs);
|
|
13566
|
+
this.bidTimers.set(taskId, timer);
|
|
13567
|
+
}
|
|
13568
|
+
_cancelBidWindow(taskId) {
|
|
13569
|
+
const timer = this.bidTimers.get(taskId);
|
|
13570
|
+
if (timer) {
|
|
13571
|
+
clearTimeout(timer);
|
|
13572
|
+
this.bidTimers.delete(taskId);
|
|
13573
|
+
}
|
|
13574
|
+
}
|
|
13575
|
+
async _evaluateBids(taskId) {
|
|
13576
|
+
const bids = this.pendingBids.get(taskId);
|
|
13577
|
+
if (!bids || bids.length === 0) {
|
|
13578
|
+
const goal = this.graph.get(taskId);
|
|
13579
|
+
if (goal) {
|
|
13580
|
+
await this._broadcastTask(goal);
|
|
13581
|
+
this._startBidWindow(taskId);
|
|
13582
|
+
}
|
|
13583
|
+
return;
|
|
13584
|
+
}
|
|
13585
|
+
bids.sort((a, b) => b.score - a.score);
|
|
13586
|
+
const winner = bids[0];
|
|
13587
|
+
await this.claim(taskId, winner.agentId, winner.agentName);
|
|
13588
|
+
}
|
|
13589
|
+
async _assignDirect(taskId, agentId) {
|
|
13590
|
+
const goal = this.graph.get(taskId);
|
|
13591
|
+
if (!goal) return;
|
|
13592
|
+
await this.graph.update(taskId, {
|
|
13593
|
+
status: "in_progress",
|
|
13594
|
+
assignee: agentId,
|
|
13595
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13596
|
+
});
|
|
13597
|
+
this.agentTaskCount(agentId, 1);
|
|
13598
|
+
await this._notifyAgent(agentId, {
|
|
13599
|
+
type: "assign",
|
|
13600
|
+
subject: `[assigned] ${goal.title}`,
|
|
13601
|
+
body: `You have been directly assigned: "${goal.title}"
|
|
13602
|
+
|
|
13603
|
+
${goal.description}`,
|
|
13604
|
+
taskContext: { taskId, status: "in_progress" }
|
|
13605
|
+
});
|
|
13606
|
+
}
|
|
13607
|
+
_onBidEvent(_e) {
|
|
13608
|
+
}
|
|
13609
|
+
_onClaimedEvent(e) {
|
|
13610
|
+
const { taskId } = e.payload;
|
|
13611
|
+
this.pendingBids.delete(taskId);
|
|
13612
|
+
this._cancelBidWindow(taskId);
|
|
13613
|
+
}
|
|
13614
|
+
_getAgentTaskCount(agentId) {
|
|
13615
|
+
return this.agentTaskCounts.get(agentId) ?? 0;
|
|
13616
|
+
}
|
|
13617
|
+
agentTaskCount(agentId, delta) {
|
|
13618
|
+
const current = this._getAgentTaskCount(agentId);
|
|
13619
|
+
const next = Math.max(0, current + delta);
|
|
13620
|
+
this.agentTaskCounts.set(agentId, next);
|
|
13621
|
+
}
|
|
13622
|
+
};
|
|
13623
|
+
var AutonomousCoordinator = class {
|
|
13624
|
+
graph;
|
|
13625
|
+
dag;
|
|
13626
|
+
auction;
|
|
13627
|
+
consensus;
|
|
13628
|
+
changes;
|
|
13629
|
+
brain;
|
|
13630
|
+
selfAgentId;
|
|
13631
|
+
fleet;
|
|
13632
|
+
fleetManager;
|
|
13633
|
+
mailbox;
|
|
13634
|
+
events;
|
|
13635
|
+
running = false;
|
|
13636
|
+
iterationCount = 0;
|
|
13637
|
+
constructor(opts) {
|
|
13638
|
+
this.selfAgentId = opts.selfAgentId;
|
|
13639
|
+
this.fleet = opts.fleet ?? void 0;
|
|
13640
|
+
this.fleetManager = opts.fleetManager ?? void 0;
|
|
13641
|
+
this.mailbox = opts.mailbox ?? void 0;
|
|
13642
|
+
this.events = opts.events ?? void 0;
|
|
13643
|
+
this.graph = new KnowledgeGraph(opts.sessionDir);
|
|
13644
|
+
this.dag = new TaskDAG();
|
|
13645
|
+
this.auction = new TaskAuctioneer({
|
|
13646
|
+
graph: this.graph,
|
|
13647
|
+
fleet: this.fleet ?? void 0,
|
|
13648
|
+
mailbox: this.mailbox ?? void 0,
|
|
13649
|
+
selfAgentId: this.selfAgentId
|
|
13650
|
+
});
|
|
13651
|
+
this.consensus = new ConsensusProtocol({
|
|
13652
|
+
graph: this.graph,
|
|
13653
|
+
fleet: this.fleet ?? void 0,
|
|
13654
|
+
voters: this._buildVoters(),
|
|
13655
|
+
rules: {
|
|
13656
|
+
quorumFraction: 0.5,
|
|
13657
|
+
approvalFraction: 0.6,
|
|
13658
|
+
vetoRoles: ["critic"],
|
|
13659
|
+
// Critic has veto power
|
|
13660
|
+
approvalWeightFraction: 0.5
|
|
13661
|
+
}
|
|
13662
|
+
});
|
|
13663
|
+
this.changes = new ChangeManager({
|
|
13664
|
+
graph: this.graph,
|
|
13665
|
+
consensus: this.consensus,
|
|
13666
|
+
fleet: this.fleet ?? void 0,
|
|
13667
|
+
checks: DEFAULT_QUALITY_CHECKS
|
|
13668
|
+
});
|
|
13669
|
+
this.brain = new AutonomousBrain({
|
|
13670
|
+
llmProvider: opts.llmProvider,
|
|
13671
|
+
graph: this.graph,
|
|
13672
|
+
fleet: this.fleet ?? void 0,
|
|
13673
|
+
selfImprove: !opts.disableSelfImprove
|
|
13674
|
+
});
|
|
13675
|
+
this.dag.onEvent((event) => {
|
|
13676
|
+
this._onDagEvent(event);
|
|
13677
|
+
});
|
|
13678
|
+
this.fleet?.filter("subagent.terminated", (e) => {
|
|
13679
|
+
this._onSubagentTerminated(e);
|
|
13680
|
+
});
|
|
13681
|
+
}
|
|
13682
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
13683
|
+
/**
|
|
13684
|
+
* Run the autonomous loop until the goal is satisfied or max iterations reached.
|
|
13685
|
+
* This is the main entry point for a fully autonomous session.
|
|
13686
|
+
*/
|
|
13687
|
+
async run(opts = {}) {
|
|
13688
|
+
if (this.running) throw new Error("AutonomousCoordinator: already running");
|
|
13689
|
+
this.running = true;
|
|
13690
|
+
this.iterationCount = 0;
|
|
13691
|
+
const maxIterations = opts.maxIterations ?? 100;
|
|
13692
|
+
const goal = opts.goal ?? "Improve the codebase";
|
|
13693
|
+
const maxCost = opts.maxCostUsd;
|
|
13694
|
+
await this.graph.load();
|
|
13695
|
+
try {
|
|
13696
|
+
const goalConfigs = await this._decomposeGoal(goal);
|
|
13697
|
+
for (const g of goalConfigs) {
|
|
13698
|
+
await this.auction.publishTask(g);
|
|
13699
|
+
}
|
|
13700
|
+
while (this.running) {
|
|
13701
|
+
this.iterationCount++;
|
|
13702
|
+
if (this.iterationCount >= maxIterations) break;
|
|
13703
|
+
if (maxCost !== void 0) {
|
|
13704
|
+
const cost = this.fleetManager?.snapshot()?.total?.cost ?? 0;
|
|
13705
|
+
if (cost >= maxCost) break;
|
|
13706
|
+
}
|
|
13707
|
+
if (opts.runUntilComplete && this.dag.isDone()) break;
|
|
13708
|
+
const decision = await this.brain.decideAuto({
|
|
13709
|
+
id: randomUUID(),
|
|
13710
|
+
source: "system",
|
|
13711
|
+
decisionType: "prioritize_goals",
|
|
13712
|
+
question: `What should we work on next? Open goals: ${this.auction.getPendingTasks().map((g) => g.title).join(", ") || "none"}`,
|
|
13713
|
+
context: {
|
|
13714
|
+
goals: this.auction.getPendingTasks(),
|
|
13715
|
+
fleetStatus: this._fleetStatus()
|
|
13716
|
+
},
|
|
13717
|
+
options: this._goalToOptions(this.auction.getPendingTasks()),
|
|
13718
|
+
risk: "medium",
|
|
13719
|
+
requiresConsensus: false
|
|
13720
|
+
});
|
|
13721
|
+
if (decision.type === "deny") {
|
|
13722
|
+
const blocked = this.dag.getBlocked();
|
|
13723
|
+
if (blocked.length > 0 && this.dag.hasDeadlock()) {
|
|
13724
|
+
(this.events?.emit)("autonomous:deadlock", { blocked });
|
|
13725
|
+
this.running = false;
|
|
13726
|
+
}
|
|
13727
|
+
break;
|
|
13728
|
+
}
|
|
13729
|
+
if (decision.type === "ask_human") {
|
|
13730
|
+
(this.events?.emit)("autonomous:ask_human", { prompt: decision.prompt });
|
|
13731
|
+
break;
|
|
13732
|
+
}
|
|
13733
|
+
if (decision.optionId) {
|
|
13734
|
+
const goalNode = this._optionToGoal(decision.optionId);
|
|
13735
|
+
if (goalNode) {
|
|
13736
|
+
await this._processGoal(goalNode.id);
|
|
13737
|
+
}
|
|
13738
|
+
}
|
|
13739
|
+
const pendingChanges = this.changes.getPendingReviews();
|
|
13740
|
+
for (const change of pendingChanges) {
|
|
13741
|
+
await this._handlePendingChange(change);
|
|
13742
|
+
}
|
|
13743
|
+
}
|
|
13744
|
+
} finally {
|
|
13745
|
+
this.running = false;
|
|
13746
|
+
}
|
|
13747
|
+
return this.getStats();
|
|
13748
|
+
}
|
|
13749
|
+
/** Stop the autonomous loop. */
|
|
13750
|
+
stop() {
|
|
13751
|
+
this.running = false;
|
|
13752
|
+
}
|
|
13753
|
+
/** Get a stats snapshot. */
|
|
13754
|
+
getStats() {
|
|
13755
|
+
const dagStats = this.dag.stats();
|
|
13756
|
+
const auctionStats = this.auction.getStats();
|
|
13757
|
+
const allGoals = this.graph.getGoals({});
|
|
13758
|
+
const allChanges = this.graph.getChanges({});
|
|
13759
|
+
const allDecisions = this.graph.getDecisions();
|
|
13760
|
+
return {
|
|
13761
|
+
goals: {
|
|
13762
|
+
total: allGoals.length,
|
|
13763
|
+
done: allGoals.filter((g) => g.status === "done").length,
|
|
13764
|
+
pending: allGoals.filter((g) => g.status === "pending").length,
|
|
13765
|
+
failed: allGoals.filter((g) => g.status === "failed").length,
|
|
13766
|
+
progress: allGoals.length > 0 ? allGoals.filter((g) => g.status === "done").length / allGoals.length : 0
|
|
13767
|
+
},
|
|
13768
|
+
dag: dagStats,
|
|
13769
|
+
auction: auctionStats,
|
|
13770
|
+
changes: {
|
|
13771
|
+
proposed: allChanges.filter((c) => c.status === "proposed").length,
|
|
13772
|
+
approved: allChanges.filter((c) => c.status === "approved").length,
|
|
13773
|
+
applied: allChanges.filter((c) => c.status === "applied").length,
|
|
13774
|
+
rejected: allChanges.filter((c) => c.status === "rejected").length
|
|
13775
|
+
},
|
|
13776
|
+
decisions: allDecisions.length,
|
|
13777
|
+
costSoFar: this.fleetManager?.snapshot()?.total?.cost
|
|
13778
|
+
};
|
|
13779
|
+
}
|
|
13780
|
+
// ── Fact publishing ──────────────────────────────────────────────────
|
|
13781
|
+
/**
|
|
13782
|
+
* Publish a fact discovered by an agent. Facts are immutable and form
|
|
13783
|
+
* the basis for other agents' decisions.
|
|
13784
|
+
*/
|
|
13785
|
+
async publishFact(input) {
|
|
13786
|
+
const fact = await this.graph.add({
|
|
13787
|
+
type: "fact",
|
|
13788
|
+
category: input.category,
|
|
13789
|
+
subject: input.subject,
|
|
13790
|
+
detail: input.detail,
|
|
13791
|
+
file: input.file,
|
|
13792
|
+
line: input.line,
|
|
13793
|
+
severity: input.severity,
|
|
13794
|
+
discoveredBy: this.selfAgentId,
|
|
13795
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13796
|
+
tags: input.tags ?? [],
|
|
13797
|
+
key: `${input.category}:${input.subject}:${input.file ?? ""}:${input.line ?? ""}`,
|
|
13798
|
+
related: []
|
|
13799
|
+
});
|
|
13800
|
+
await this._mailboxBroadcast({
|
|
13801
|
+
type: "note",
|
|
13802
|
+
subject: `[${input.severity ?? "info"}] ${input.category}: ${input.subject}`,
|
|
13803
|
+
body: `**${input.category}**${input.file ? ` in ${input.file}${input.line ? `:${input.line}` : ""}` : ""}
|
|
13804
|
+
${input.detail}`
|
|
13805
|
+
});
|
|
13806
|
+
return fact;
|
|
13807
|
+
}
|
|
13808
|
+
// ── Goal creation helpers ────────────────────────────────────────────
|
|
13809
|
+
/**
|
|
13810
|
+
* Publish a goal and add it to the DAG.
|
|
13811
|
+
*/
|
|
13812
|
+
async createGoal(input) {
|
|
13813
|
+
const resolvedPriority = input.priority ?? "medium";
|
|
13814
|
+
const goalId = await this.auction.publishTask({
|
|
13815
|
+
title: input.title,
|
|
13816
|
+
description: input.description,
|
|
13817
|
+
priority: resolvedPriority,
|
|
13818
|
+
...input.tags ? { tags: input.tags } : {}
|
|
13819
|
+
});
|
|
13820
|
+
const goal = this.graph.get(goalId);
|
|
13821
|
+
for (const depId of input.deps ?? []) {
|
|
13822
|
+
this.dag.addNode(depId, this.graph.get(depId)?.type === "goal" ? this.graph.get(depId).title : depId);
|
|
13823
|
+
}
|
|
13824
|
+
this.dag.addNode(goalId, input.description, input.deps ?? []);
|
|
13825
|
+
return goal;
|
|
13826
|
+
}
|
|
13827
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
13828
|
+
async _decomposeGoal(goalText) {
|
|
13829
|
+
const category = this._inferCategory(goalText);
|
|
13830
|
+
const subGoals = [];
|
|
13831
|
+
if (category === "security") {
|
|
13832
|
+
subGoals.push({ title: "Audit for secrets", description: "Scan codebase for hardcoded secrets and API keys", priority: "critical", tags: ["security"] });
|
|
13833
|
+
subGoals.push({ title: "Check injection vectors", description: "Find eval, innerHTML, SQL concat, shell injection patterns", priority: "critical", tags: ["security", "injection"] });
|
|
13834
|
+
subGoals.push({ title: "Dependency audit", description: "Run npm/pnpm audit for known CVEs", priority: "high", tags: ["security", "deps"] });
|
|
13835
|
+
} else if (category === "bug") {
|
|
13836
|
+
subGoals.push({ title: "Find bugs", description: `Scan for bugs related to: ${goalText}`, priority: "high", tags: ["bug"] });
|
|
13837
|
+
subGoals.push({ title: "Fix bugs", description: "Fix discovered bugs with tests", priority: "high", tags: ["fix"] });
|
|
13838
|
+
} else if (category === "refactor") {
|
|
13839
|
+
subGoals.push({ title: "Plan refactor", description: `Analyze code structure for: ${goalText}`, priority: "medium", tags: ["refactor", "planning"] });
|
|
13840
|
+
subGoals.push({ title: "Implement refactor", description: "Apply the refactoring plan", priority: "medium", tags: ["refactor", "implementation"] });
|
|
13841
|
+
} else {
|
|
13842
|
+
subGoals.push({ title: goalText, description: goalText, priority: "medium", tags: [category] });
|
|
13843
|
+
}
|
|
13844
|
+
return subGoals;
|
|
13845
|
+
}
|
|
13846
|
+
_inferCategory(goal) {
|
|
13847
|
+
const g = goal.toLowerCase();
|
|
13848
|
+
if (g.includes("security") || g.includes("secret") || g.includes("injection")) return "security";
|
|
13849
|
+
if (g.includes("bug") || g.includes("fix") || g.includes("error")) return "bug";
|
|
13850
|
+
if (g.includes("refactor") || g.includes("debt") || g.includes("architecture")) return "architecture";
|
|
13851
|
+
if (g.includes("test") || g.includes("coverage")) return "test";
|
|
13852
|
+
if (g.includes("perf") || g.includes("speed") || g.includes("optimize")) return "perf";
|
|
13853
|
+
if (g.includes("deps") || g.includes("package") || g.includes("update")) return "deps";
|
|
13854
|
+
return "quality";
|
|
13855
|
+
}
|
|
13856
|
+
async _processGoal(goalId) {
|
|
13857
|
+
const ready = this.dag.getReady();
|
|
13858
|
+
if (ready.length === 0) return;
|
|
13859
|
+
const next = ready.find((n) => n.id === goalId) ?? ready[0];
|
|
13860
|
+
this.dag.start(next.id, "auctioneer");
|
|
13861
|
+
const existingAgent = this.auction.getTasksForAgent(this.selfAgentId).find((g) => g.id === next.id);
|
|
13862
|
+
if (!existingAgent) {
|
|
13863
|
+
await this.auction.publishTask({
|
|
13864
|
+
title: next.description,
|
|
13865
|
+
description: next.description,
|
|
13866
|
+
priority: this._dagPriorityToGoal(next.priority),
|
|
13867
|
+
tags: next.tags
|
|
13868
|
+
});
|
|
13869
|
+
}
|
|
13870
|
+
await this._waitForClaim(next.id);
|
|
13871
|
+
}
|
|
13872
|
+
async _waitForClaim(taskId) {
|
|
13873
|
+
const maxWait = 6e4;
|
|
13874
|
+
const pollInterval = 2e3;
|
|
13875
|
+
const start = Date.now();
|
|
13876
|
+
while (Date.now() - start < maxWait) {
|
|
13877
|
+
const goal = this.graph.get(taskId);
|
|
13878
|
+
if (goal?.status === "in_progress" || goal?.status === "done") {
|
|
13879
|
+
return;
|
|
13880
|
+
}
|
|
13881
|
+
await this._sleep(pollInterval);
|
|
13882
|
+
}
|
|
13883
|
+
}
|
|
13884
|
+
async _handlePendingChange(change) {
|
|
13885
|
+
const result = this.consensus.getStatus(change.id);
|
|
13886
|
+
if (result?.outcome !== "pending") return;
|
|
13887
|
+
if (change.qualityGate.passed) {
|
|
13888
|
+
const voteResult = await this.consensus.castVote(
|
|
13889
|
+
change.id,
|
|
13890
|
+
this.selfAgentId,
|
|
13891
|
+
"approve",
|
|
13892
|
+
`Quality gate passed: ${change.qualityGate.checks.map((c) => c.name).join(", ")}`
|
|
13893
|
+
);
|
|
13894
|
+
if (voteResult.outcome === "approved") {
|
|
13895
|
+
await this.changes.markApplied(change.id, (/* @__PURE__ */ new Date()).toISOString());
|
|
13896
|
+
}
|
|
13897
|
+
}
|
|
13898
|
+
}
|
|
13899
|
+
_onDagEvent(event) {
|
|
13900
|
+
if (event.type === "node:ready") {
|
|
13901
|
+
const node = this.dag.getNode(event.nodeId);
|
|
13902
|
+
if (node) {
|
|
13903
|
+
(this.events?.emit)("autonomous:task_ready", { taskId: event.nodeId, description: node.description });
|
|
13904
|
+
}
|
|
13905
|
+
}
|
|
13906
|
+
if (event.type === "deadlock") {
|
|
13907
|
+
(this.events?.emit)("autonomous:deadlock", { blocked: event.blocked });
|
|
13908
|
+
this.running = false;
|
|
13909
|
+
}
|
|
13910
|
+
if (event.type === "graph:done") {
|
|
13911
|
+
(this.events?.emit)("autonomous:all_done", this.getStats());
|
|
13912
|
+
}
|
|
13913
|
+
}
|
|
13914
|
+
_onSubagentTerminated(e) {
|
|
13915
|
+
const payload = e.payload;
|
|
13916
|
+
const subagentId = payload?.subagentId ?? e.subagentId;
|
|
13917
|
+
const stopReason = payload?.stopReason ?? "unknown";
|
|
13918
|
+
const tasks = this.auction.getTasksForAgent(subagentId);
|
|
13919
|
+
for (const task of tasks) {
|
|
13920
|
+
if (stopReason === "end_turn") {
|
|
13921
|
+
void this.auction.complete(task.id, "Subagent completed successfully");
|
|
13922
|
+
} else {
|
|
13923
|
+
void this.auction.fail(task.id, `Subagent terminated: ${stopReason}`);
|
|
13924
|
+
}
|
|
13925
|
+
}
|
|
13926
|
+
}
|
|
13927
|
+
_fleetStatus() {
|
|
13928
|
+
return {
|
|
13929
|
+
running: this.fleetManager?.getFleetStats().running ?? 0,
|
|
13930
|
+
idle: this.fleetManager?.getFleetStats().idle ?? 0,
|
|
13931
|
+
total: this.fleetManager?.getFleetStats().total ?? 0,
|
|
13932
|
+
costSoFar: this.fleetManager?.snapshot()?.total?.cost ?? 0
|
|
13933
|
+
};
|
|
13934
|
+
}
|
|
13935
|
+
_buildVoters() {
|
|
13936
|
+
return [
|
|
13937
|
+
{ agentId: "critic", agentName: "Critic", role: "critic", weight: 2, veto: true },
|
|
13938
|
+
{ agentId: "bug-hunter", agentName: "Bug Hunter", role: "bug-hunter", weight: 1.5 },
|
|
13939
|
+
{ agentId: "security-scanner", agentName: "Security Scanner", role: "security-scanner", weight: 1.5 },
|
|
13940
|
+
{ agentId: "audit-log", agentName: "Audit Log", role: "audit-log", weight: 1 },
|
|
13941
|
+
{ agentId: "refactor-planner", agentName: "Refactor Planner", role: "refactor-planner", weight: 1 }
|
|
13942
|
+
];
|
|
13943
|
+
}
|
|
13944
|
+
_goalToOptions(goals) {
|
|
13945
|
+
return goals.slice(0, 5).map((g, i) => ({
|
|
13946
|
+
id: g.id,
|
|
13947
|
+
label: `[${g.priority}] ${g.title}`,
|
|
13948
|
+
recommended: i === 0
|
|
13949
|
+
}));
|
|
13950
|
+
}
|
|
13951
|
+
_optionToGoal(optionId) {
|
|
13952
|
+
return this.graph.get(optionId);
|
|
13953
|
+
}
|
|
13954
|
+
_dagPriorityToGoal(p) {
|
|
13955
|
+
if (p <= 1) return "critical";
|
|
13956
|
+
if (p <= 2) return "high";
|
|
13957
|
+
if (p <= 4) return "medium";
|
|
13958
|
+
return "low";
|
|
13959
|
+
}
|
|
13960
|
+
async _mailboxBroadcast(msg) {
|
|
13961
|
+
if (!this.mailbox) return;
|
|
13962
|
+
try {
|
|
13963
|
+
await this.mailbox.send({
|
|
13964
|
+
from: this.selfAgentId,
|
|
13965
|
+
to: "*",
|
|
13966
|
+
type: msg.type,
|
|
13967
|
+
subject: msg.subject,
|
|
13968
|
+
body: msg.body,
|
|
13969
|
+
priority: "normal"
|
|
13970
|
+
});
|
|
13971
|
+
} catch {
|
|
13972
|
+
}
|
|
13973
|
+
}
|
|
13974
|
+
_sleep(ms) {
|
|
13975
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
13976
|
+
}
|
|
13977
|
+
};
|
|
11557
13978
|
|
|
11558
|
-
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 };
|
|
13979
|
+
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, 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, 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 };
|
|
11559
13980
|
//# sourceMappingURL=index.js.map
|
|
11560
13981
|
//# sourceMappingURL=index.js.map
|