@wrongstack/core 0.260.0 → 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-BbskZ7HH.d.ts → agent-bridge-D8sa1vtv.d.ts} +1 -1
- package/dist/{agent-subagent-runner-BNIGZx18.d.ts → agent-subagent-runner-c9DLkaas.d.ts} +11 -9
- package/dist/{brain-C2yDd7Lw.d.ts → brain-O1IdKPaK.d.ts} +2 -2
- package/dist/{compactor-t0R_AIt_.d.ts → compactor-BBy0rCtB.d.ts} +1 -1
- package/dist/{config-FG6As4H5.d.ts → config-Dz2F3H2K.d.ts} +7 -1
- package/dist/{context-JFOVvu6z.d.ts → context-BGSpZNSE.d.ts} +11 -0
- package/dist/coordination/index.d.ts +1681 -15
- package/dist/coordination/index.js +2648 -388
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +25 -25
- package/dist/defaults/index.js +1414 -1387
- 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 +410 -388
- 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-B1IXJtLX.d.ts → goal-preamble-DzjFuN3p.d.ts} +21 -9
- package/dist/{goal-store-CPXz6Mml.d.ts → goal-store-CxWmCGbH.d.ts} +1 -1
- package/dist/{index-CebbJB94.d.ts → index-CYIQrXVF.d.ts} +8 -8
- package/dist/{index-BPcg4N3M.d.ts → index-CbLSI66_.d.ts} +5 -5
- package/dist/index.d.ts +45 -91
- package/dist/index.js +14976 -12551
- 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-DXxI2tlu.d.ts → llm-selector-DzxuZnNz.d.ts} +2 -2
- package/dist/{mcp-servers-OwNHo43-.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-Djlmq4uB.d.ts → models-registry-B_siPxqN.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-CEmrSCMJ.d.ts → multi-agent-coordinator-CK5Jdj9K.d.ts} +2 -2
- package/dist/{null-fleet-bus-DT92xqgJ.d.ts → null-fleet-bus-DgvD4SCO.d.ts} +6 -6
- package/dist/observability/index.d.ts +2 -2
- package/dist/observability/index.js +8 -3
- package/dist/observability/index.js.map +1 -1
- package/dist/{parallel-eternal-engine-0SItuq5r.d.ts → parallel-eternal-engine-bK0JQBR_.d.ts} +9 -9
- package/dist/{path-resolver-DKBh6Jlo.d.ts → path-resolver-BPEDlN38.d.ts} +3 -3
- package/dist/{permission-BJ7eO9Vl.d.ts → permission-4yvGmMRB.d.ts} +1 -1
- package/dist/{permission-policy-DEXOfnpm.d.ts → permission-policy-C6XpsBOy.d.ts} +2 -2
- package/dist/{pipeline-zflkI2dp.d.ts → pipeline-CXCeMz8J.d.ts} +58 -3
- package/dist/{plan-templates-BFXyRkEK.d.ts → plan-templates-BvzRBkJc.d.ts} +5 -5
- package/dist/{provider-runner-BC-uywtT.d.ts → provider-runner-C5aQpDWE.d.ts} +3 -3
- package/dist/{retry-policy-Cavrzmtk.d.ts → retry-policy-CFhdtRzz.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +39 -29
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-CDvDYXWX.d.ts → secret-vault-CxiVLbt1.d.ts} +1 -1
- package/dist/security/index.d.ts +4 -4
- package/dist/security/index.js +208 -203
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-B7AivHsu.d.ts → selector-gIuhRTkN.d.ts} +1 -1
- package/dist/{session-event-bridge-BmIDxdJd.d.ts → session-event-bridge-DkvvrpDt.d.ts} +8 -2
- package/dist/{session-reader-DtofsB-2.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 +31 -12
- package/dist/storage/index.js +441 -360
- 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 +703 -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/dist/package-outdated-watcher-C70ag2G9.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
|
}
|
|
@@ -4329,7 +4447,7 @@ function makeSpawnTool(director, roster) {
|
|
|
4329
4447
|
if (err instanceof FleetCostCapError) {
|
|
4330
4448
|
return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
|
|
4331
4449
|
}
|
|
4332
|
-
return { error:
|
|
4450
|
+
return { error: toErrorMessage(err) };
|
|
4333
4451
|
}
|
|
4334
4452
|
}
|
|
4335
4453
|
};
|
|
@@ -4411,7 +4529,7 @@ function makeAskTool(director) {
|
|
|
4411
4529
|
_hint: "Response was large and stored. Use ask_result with the key to retrieve it."
|
|
4412
4530
|
};
|
|
4413
4531
|
} catch (err) {
|
|
4414
|
-
return { ok: false, error:
|
|
4532
|
+
return { ok: false, error: toErrorMessage(err) };
|
|
4415
4533
|
}
|
|
4416
4534
|
}
|
|
4417
4535
|
};
|
|
@@ -4637,7 +4755,7 @@ function makeCollabDebugTool(director) {
|
|
|
4637
4755
|
evaluations: report.evaluations
|
|
4638
4756
|
};
|
|
4639
4757
|
} catch (err) {
|
|
4640
|
-
const msg =
|
|
4758
|
+
const msg = toErrorMessage(err);
|
|
4641
4759
|
return { error: "collab_debug failed: " + msg };
|
|
4642
4760
|
}
|
|
4643
4761
|
}
|
|
@@ -5313,11 +5431,6 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5313
5431
|
}
|
|
5314
5432
|
};
|
|
5315
5433
|
|
|
5316
|
-
// src/utils/string.ts
|
|
5317
|
-
function truncate(s, max) {
|
|
5318
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
5319
|
-
}
|
|
5320
|
-
|
|
5321
5434
|
// src/types/provider.ts
|
|
5322
5435
|
var ProviderError = class extends WrongStackError {
|
|
5323
5436
|
status;
|
|
@@ -5397,7 +5510,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
5397
5510
|
const baseMessage2 = err.describe();
|
|
5398
5511
|
return providerErrorToSubagentError(err, baseMessage2, cause);
|
|
5399
5512
|
}
|
|
5400
|
-
const baseMessage =
|
|
5513
|
+
const baseMessage = toErrorMessage(err);
|
|
5401
5514
|
if (err instanceof BudgetExceededError) {
|
|
5402
5515
|
const map = {
|
|
5403
5516
|
iterations: "budget_iterations",
|
|
@@ -7383,7 +7496,7 @@ var Director = class _Director {
|
|
|
7383
7496
|
})),
|
|
7384
7497
|
usage: this.usage.snapshot()
|
|
7385
7498
|
};
|
|
7386
|
-
await fsp6.mkdir(
|
|
7499
|
+
await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
|
|
7387
7500
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
7388
7501
|
return this.manifestPath;
|
|
7389
7502
|
}
|
|
@@ -7433,7 +7546,7 @@ var Director = class _Director {
|
|
|
7433
7546
|
* listener for structured collection, and never affects exit code.
|
|
7434
7547
|
*/
|
|
7435
7548
|
logShutdownError(phase, err) {
|
|
7436
|
-
const detail =
|
|
7549
|
+
const detail = toErrorMessage(err);
|
|
7437
7550
|
process.emitWarning(
|
|
7438
7551
|
`Director shutdown phase "${phase}" failed: ${detail}`,
|
|
7439
7552
|
"DirectorShutdownWarning"
|
|
@@ -7597,7 +7710,7 @@ var Director = class _Director {
|
|
|
7597
7710
|
*/
|
|
7598
7711
|
async readSession(subagentId, tail) {
|
|
7599
7712
|
if (!this.sessionsRoot) return null;
|
|
7600
|
-
const filePath =
|
|
7713
|
+
const filePath = path5.join(this.sessionsRoot, this.directorRunId, `${subagentId}.jsonl`);
|
|
7601
7714
|
let raw;
|
|
7602
7715
|
try {
|
|
7603
7716
|
raw = await fsp6.readFile(filePath, "utf8");
|
|
@@ -8026,7 +8139,7 @@ function createDelegateTool(opts) {
|
|
|
8026
8139
|
summary
|
|
8027
8140
|
};
|
|
8028
8141
|
} catch (err) {
|
|
8029
|
-
const message =
|
|
8142
|
+
const message = toErrorMessage(err);
|
|
8030
8143
|
opts.events?.emit("delegate.completed", {
|
|
8031
8144
|
target,
|
|
8032
8145
|
task: i.task,
|
|
@@ -8127,13 +8240,13 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
8127
8240
|
if (!opts.sessionsRoot) return void 0;
|
|
8128
8241
|
const candidates = [];
|
|
8129
8242
|
if (opts.directorRunId) {
|
|
8130
|
-
candidates.push(
|
|
8243
|
+
candidates.push(path5.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
|
|
8131
8244
|
} else {
|
|
8132
8245
|
try {
|
|
8133
8246
|
const entries = await fsp6.readdir(opts.sessionsRoot, { withFileTypes: true });
|
|
8134
8247
|
for (const entry of entries) {
|
|
8135
8248
|
if (entry.isDirectory()) {
|
|
8136
|
-
candidates.push(
|
|
8249
|
+
candidates.push(path5.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
|
|
8137
8250
|
}
|
|
8138
8251
|
}
|
|
8139
8252
|
} catch {
|
|
@@ -8389,100 +8502,6 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8389
8502
|
function defaultFormatTaskInput(task) {
|
|
8390
8503
|
return task.description ?? "";
|
|
8391
8504
|
}
|
|
8392
|
-
|
|
8393
|
-
// src/utils/message-invariants.ts
|
|
8394
|
-
function repairToolUseAdjacency(messages) {
|
|
8395
|
-
const removedToolUses = [];
|
|
8396
|
-
const removedToolResults = [];
|
|
8397
|
-
let removedMessages = 0;
|
|
8398
|
-
let changed = false;
|
|
8399
|
-
const out = [];
|
|
8400
|
-
for (let i = 0; i < messages.length; i++) {
|
|
8401
|
-
const original = expectDefined(messages[i]);
|
|
8402
|
-
let msg = original;
|
|
8403
|
-
if (hasToolUse(msg)) {
|
|
8404
|
-
const nextIds = toolResultIds(messages[i + 1]);
|
|
8405
|
-
const filtered = mapContent(msg, (blocks) => {
|
|
8406
|
-
const next = [];
|
|
8407
|
-
for (const block of blocks) {
|
|
8408
|
-
if (block.type === "tool_use" && !nextIds.has(block.id)) {
|
|
8409
|
-
removedToolUses.push(block.id);
|
|
8410
|
-
changed = true;
|
|
8411
|
-
continue;
|
|
8412
|
-
}
|
|
8413
|
-
next.push(block);
|
|
8414
|
-
}
|
|
8415
|
-
return next;
|
|
8416
|
-
});
|
|
8417
|
-
msg = filtered ?? msg;
|
|
8418
|
-
}
|
|
8419
|
-
if (hasToolResult(msg)) {
|
|
8420
|
-
const allowed = toolUseIds(out[out.length - 1]);
|
|
8421
|
-
const filtered = mapContent(msg, (blocks) => {
|
|
8422
|
-
const next = [];
|
|
8423
|
-
for (const block of blocks) {
|
|
8424
|
-
if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
|
|
8425
|
-
removedToolResults.push(block.tool_use_id);
|
|
8426
|
-
changed = true;
|
|
8427
|
-
continue;
|
|
8428
|
-
}
|
|
8429
|
-
next.push(block);
|
|
8430
|
-
}
|
|
8431
|
-
return next;
|
|
8432
|
-
});
|
|
8433
|
-
msg = filtered ?? msg;
|
|
8434
|
-
}
|
|
8435
|
-
if (isEmptyMessage(msg)) {
|
|
8436
|
-
removedMessages++;
|
|
8437
|
-
changed = true;
|
|
8438
|
-
continue;
|
|
8439
|
-
}
|
|
8440
|
-
out.push(msg);
|
|
8441
|
-
}
|
|
8442
|
-
return {
|
|
8443
|
-
messages: changed ? out : messages,
|
|
8444
|
-
report: { changed, removedToolUses, removedToolResults, removedMessages }
|
|
8445
|
-
};
|
|
8446
|
-
}
|
|
8447
|
-
function hasToolUse(msg) {
|
|
8448
|
-
return contentBlocks(msg).some((b) => b.type === "tool_use");
|
|
8449
|
-
}
|
|
8450
|
-
function hasToolResult(msg) {
|
|
8451
|
-
return contentBlocks(msg).some((b) => b.type === "tool_result");
|
|
8452
|
-
}
|
|
8453
|
-
function toolUseIds(msg) {
|
|
8454
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8455
|
-
if (!msg || msg.role !== "assistant") return ids;
|
|
8456
|
-
for (const block of contentBlocks(msg)) {
|
|
8457
|
-
if (block.type === "tool_use") ids.add(block.id);
|
|
8458
|
-
}
|
|
8459
|
-
return ids;
|
|
8460
|
-
}
|
|
8461
|
-
function toolResultIds(msg) {
|
|
8462
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8463
|
-
if (!msg || msg.role !== "user") return ids;
|
|
8464
|
-
for (const block of contentBlocks(msg)) {
|
|
8465
|
-
if (block.type === "tool_result") ids.add(block.tool_use_id);
|
|
8466
|
-
}
|
|
8467
|
-
return ids;
|
|
8468
|
-
}
|
|
8469
|
-
function contentBlocks(msg) {
|
|
8470
|
-
return msg && Array.isArray(msg.content) ? msg.content : [];
|
|
8471
|
-
}
|
|
8472
|
-
function mapContent(msg, fn) {
|
|
8473
|
-
if (!Array.isArray(msg.content)) return msg;
|
|
8474
|
-
const next = fn(msg.content);
|
|
8475
|
-
if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
|
|
8476
|
-
return msg;
|
|
8477
|
-
}
|
|
8478
|
-
return { ...msg, content: next };
|
|
8479
|
-
}
|
|
8480
|
-
function isEmptyMessage(msg) {
|
|
8481
|
-
if (typeof msg.content === "string") return msg.content.trim().length === 0;
|
|
8482
|
-
return msg.content.length === 0;
|
|
8483
|
-
}
|
|
8484
|
-
|
|
8485
|
-
// src/storage/session-store.ts
|
|
8486
8505
|
function sanitizeModel(model) {
|
|
8487
8506
|
return model.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
8488
8507
|
}
|
|
@@ -8538,11 +8557,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8538
8557
|
}
|
|
8539
8558
|
/** Absolute path to the session index file. */
|
|
8540
8559
|
get indexFile() {
|
|
8541
|
-
return
|
|
8560
|
+
return path5.join(this.dir, "_index.jsonl");
|
|
8542
8561
|
}
|
|
8543
8562
|
/** Join session ID to its absolute path within the store directory. */
|
|
8544
8563
|
sessionPath(id, ext) {
|
|
8545
|
-
return
|
|
8564
|
+
return path5.join(this.dir, `${id}${ext}`);
|
|
8546
8565
|
}
|
|
8547
8566
|
/**
|
|
8548
8567
|
* Ensure the directory implied by the session ID exists. When the ID
|
|
@@ -8550,7 +8569,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8550
8569
|
* subdirectory so sessions group naturally by day.
|
|
8551
8570
|
*/
|
|
8552
8571
|
async ensureShardDir(id) {
|
|
8553
|
-
const dirPath =
|
|
8572
|
+
const dirPath = path5.dirname(path5.join(this.dir, id));
|
|
8554
8573
|
await ensureDir(dirPath);
|
|
8555
8574
|
return dirPath;
|
|
8556
8575
|
}
|
|
@@ -8558,15 +8577,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8558
8577
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8559
8578
|
const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt, meta.model ?? meta.provider);
|
|
8560
8579
|
const shardDir = await this.ensureShardDir(id);
|
|
8561
|
-
const file =
|
|
8580
|
+
const file = path5.join(shardDir, `${path5.basename(id)}.jsonl`);
|
|
8562
8581
|
const t0 = Date.now();
|
|
8563
8582
|
let handle;
|
|
8564
8583
|
try {
|
|
8565
8584
|
handle = await fsp6.open(file, "a", 384);
|
|
8566
8585
|
} catch (err) {
|
|
8567
|
-
this.emitError(id, file, "create",
|
|
8586
|
+
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
8568
8587
|
throw new Error(
|
|
8569
|
-
`Failed to open session file: ${
|
|
8588
|
+
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
8570
8589
|
{ cause: err }
|
|
8571
8590
|
);
|
|
8572
8591
|
}
|
|
@@ -8586,7 +8605,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8586
8605
|
message: e instanceof Error ? e.message : String(e),
|
|
8587
8606
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8588
8607
|
})));
|
|
8589
|
-
this.emitError(id, file, "create",
|
|
8608
|
+
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
8590
8609
|
throw err;
|
|
8591
8610
|
}
|
|
8592
8611
|
}
|
|
@@ -8598,9 +8617,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8598
8617
|
try {
|
|
8599
8618
|
handle = await fsp6.open(file, "a", 384);
|
|
8600
8619
|
} catch (err) {
|
|
8601
|
-
this.emitError(id, file, "resume",
|
|
8620
|
+
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
8602
8621
|
throw new Error(
|
|
8603
|
-
`Failed to open session "${id}" for append: ${
|
|
8622
|
+
`Failed to open session "${id}" for append: ${toErrorMessage(err)}`,
|
|
8604
8623
|
{ cause: err }
|
|
8605
8624
|
);
|
|
8606
8625
|
}
|
|
@@ -8620,7 +8639,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8620
8639
|
// Shard directory (sessions/<date>/) — must match create() so the
|
|
8621
8640
|
// .summary.json sidecar lands next to the JSONL instead of the
|
|
8622
8641
|
// sessions root (where summaryFor() would never find it).
|
|
8623
|
-
dir:
|
|
8642
|
+
dir: path5.dirname(file),
|
|
8624
8643
|
filePath: file,
|
|
8625
8644
|
secretScrubber: this.secretScrubber,
|
|
8626
8645
|
onClose: (s) => this.appendToIndex(s)
|
|
@@ -8635,7 +8654,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8635
8654
|
message: e instanceof Error ? e.message : String(e),
|
|
8636
8655
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8637
8656
|
})));
|
|
8638
|
-
this.emitError(id, file, "resume",
|
|
8657
|
+
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
8639
8658
|
throw err;
|
|
8640
8659
|
}
|
|
8641
8660
|
}
|
|
@@ -8663,7 +8682,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8663
8682
|
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
8664
8683
|
} catch (err) {
|
|
8665
8684
|
outcome = "failure";
|
|
8666
|
-
errorMsg =
|
|
8685
|
+
errorMsg = toErrorMessage(err);
|
|
8667
8686
|
throw err;
|
|
8668
8687
|
} finally {
|
|
8669
8688
|
this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
|
|
@@ -8746,7 +8765,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8746
8765
|
await fsp6.rename(tmp, this.indexFile);
|
|
8747
8766
|
} catch (err) {
|
|
8748
8767
|
outcome = "failure";
|
|
8749
|
-
errorMsg =
|
|
8768
|
+
errorMsg = toErrorMessage(err);
|
|
8750
8769
|
} finally {
|
|
8751
8770
|
this.emitWrite("~compact~", this.indexFile, "compact", outcome, Date.now() - t0, void 0, errorMsg);
|
|
8752
8771
|
}
|
|
@@ -8814,7 +8833,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8814
8833
|
continue;
|
|
8815
8834
|
if (entry.isDirectory()) {
|
|
8816
8835
|
const childPrefix = depth === 0 ? entry.name : `${prefix}/${entry.name}`;
|
|
8817
|
-
ids.push(...await this.collectSessionIds(
|
|
8836
|
+
ids.push(...await this.collectSessionIds(path5.join(dir, entry.name), childPrefix, depth + 1));
|
|
8818
8837
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
8819
8838
|
if (entry.name === "_index.jsonl") continue;
|
|
8820
8839
|
const base = entry.name.replace(/\.jsonl$/, "");
|
|
@@ -8834,10 +8853,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8834
8853
|
return JSON.parse(raw);
|
|
8835
8854
|
} catch {
|
|
8836
8855
|
const full = this.sessionPath(id, ".jsonl");
|
|
8837
|
-
const
|
|
8838
|
-
const summary = await this.summarize(id,
|
|
8856
|
+
const stat6 = await fsp6.stat(full);
|
|
8857
|
+
const summary = await this.summarize(id, stat6.mtime.toISOString());
|
|
8839
8858
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
8840
|
-
const msg =
|
|
8859
|
+
const msg = toErrorMessage(err);
|
|
8841
8860
|
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
8842
8861
|
console.warn(JSON.stringify({
|
|
8843
8862
|
level: "warn",
|
|
@@ -8865,14 +8884,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8865
8884
|
async deleteSession(id) {
|
|
8866
8885
|
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
8867
8886
|
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
8868
|
-
const shardDir =
|
|
8869
|
-
const base =
|
|
8870
|
-
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);
|
|
8871
8890
|
const deletions = [
|
|
8872
8891
|
fsp6.unlink(jsonlPath),
|
|
8873
8892
|
fsp6.unlink(summaryPath),
|
|
8874
|
-
fsp6.unlink(
|
|
8875
|
-
fsp6.unlink(
|
|
8893
|
+
fsp6.unlink(path5.join(shardDir, `${base}.plan.json`)),
|
|
8894
|
+
fsp6.unlink(path5.join(shardDir, `${base}.todos.json`))
|
|
8876
8895
|
];
|
|
8877
8896
|
const results = await Promise.allSettled(deletions);
|
|
8878
8897
|
for (const r of results) {
|
|
@@ -8894,7 +8913,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8894
8913
|
level: "warn",
|
|
8895
8914
|
event: "session_store.rmdir_failed",
|
|
8896
8915
|
sessionId: id,
|
|
8897
|
-
message:
|
|
8916
|
+
message: toErrorMessage(err),
|
|
8898
8917
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8899
8918
|
}));
|
|
8900
8919
|
});
|
|
@@ -8908,17 +8927,17 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8908
8927
|
let deleted = 0;
|
|
8909
8928
|
let activeSessionId = null;
|
|
8910
8929
|
try {
|
|
8911
|
-
const raw = await fsp6.readFile(
|
|
8930
|
+
const raw = await fsp6.readFile(path5.join(this.dir, "active.json"), "utf8");
|
|
8912
8931
|
const active = JSON.parse(raw);
|
|
8913
8932
|
activeSessionId = active.sessionId ?? null;
|
|
8914
8933
|
} catch {
|
|
8915
8934
|
}
|
|
8916
8935
|
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
8917
8936
|
const pruneFile = async (dir, name, prefix) => {
|
|
8918
|
-
const jsonlPath =
|
|
8937
|
+
const jsonlPath = path5.join(dir, name);
|
|
8919
8938
|
try {
|
|
8920
|
-
const
|
|
8921
|
-
if (
|
|
8939
|
+
const stat6 = await fsp6.stat(jsonlPath);
|
|
8940
|
+
if (stat6.mtimeMs >= cutoff) return;
|
|
8922
8941
|
} catch {
|
|
8923
8942
|
return;
|
|
8924
8943
|
}
|
|
@@ -8935,7 +8954,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8935
8954
|
continue;
|
|
8936
8955
|
}
|
|
8937
8956
|
if (!entry.isDirectory()) continue;
|
|
8938
|
-
const dateDir =
|
|
8957
|
+
const dateDir = path5.join(this.dir, entry.name);
|
|
8939
8958
|
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
8940
8959
|
for (const file of files) {
|
|
8941
8960
|
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
@@ -8947,7 +8966,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8947
8966
|
}
|
|
8948
8967
|
for (const entry of entries) {
|
|
8949
8968
|
if (!entry.isDirectory()) continue;
|
|
8950
|
-
const dateDir =
|
|
8969
|
+
const dateDir = path5.join(this.dir, entry.name);
|
|
8951
8970
|
try {
|
|
8952
8971
|
const remaining = await fsp6.readdir(dateDir);
|
|
8953
8972
|
if (remaining.length === 0) {
|
|
@@ -9122,7 +9141,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9122
9141
|
this.meta = meta;
|
|
9123
9142
|
this.events = events;
|
|
9124
9143
|
this.resumed = opts.resumed ?? false;
|
|
9125
|
-
this.manifestFile = opts.dir ?
|
|
9144
|
+
this.manifestFile = opts.dir ? path5.join(opts.dir, `${path5.basename(id)}.summary.json`) : "";
|
|
9126
9145
|
this.filePath = opts.filePath ?? "";
|
|
9127
9146
|
this.secretScrubber = opts.secretScrubber;
|
|
9128
9147
|
this.onCloseCb = opts.onClose;
|
|
@@ -9329,7 +9348,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9329
9348
|
await this.enqueueWrite(batch);
|
|
9330
9349
|
} catch (err) {
|
|
9331
9350
|
outcome = "failure";
|
|
9332
|
-
errorMsg =
|
|
9351
|
+
errorMsg = toErrorMessage(err);
|
|
9333
9352
|
this.appendFailCount += eventCount;
|
|
9334
9353
|
const now = Date.now();
|
|
9335
9354
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
@@ -9337,7 +9356,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9337
9356
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
9338
9357
|
console.warn(
|
|
9339
9358
|
"[session] flush failed:",
|
|
9340
|
-
|
|
9359
|
+
toErrorMessage(err),
|
|
9341
9360
|
tail
|
|
9342
9361
|
);
|
|
9343
9362
|
this.lastAppendWarnAt = now;
|
|
@@ -9427,7 +9446,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9427
9446
|
await atomicWrite(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
|
|
9428
9447
|
} catch (err) {
|
|
9429
9448
|
outcome = "failure";
|
|
9430
|
-
errorMsg =
|
|
9449
|
+
errorMsg = toErrorMessage(err);
|
|
9431
9450
|
} finally {
|
|
9432
9451
|
this.events?.emit("storage.write", {
|
|
9433
9452
|
sessionId: this.id,
|
|
@@ -9448,7 +9467,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
9448
9467
|
await this.onCloseCb?.(this.summary);
|
|
9449
9468
|
} catch (err) {
|
|
9450
9469
|
idxOutcome = "failure";
|
|
9451
|
-
idxError =
|
|
9470
|
+
idxError = toErrorMessage(err);
|
|
9452
9471
|
} finally {
|
|
9453
9472
|
this.events?.emit("storage.write", {
|
|
9454
9473
|
sessionId: this.summary.id,
|
|
@@ -9625,9 +9644,9 @@ function makeDirectorSessionFactory(opts) {
|
|
|
9625
9644
|
let dir;
|
|
9626
9645
|
if (opts.store) {
|
|
9627
9646
|
store = opts.store;
|
|
9628
|
-
dir = opts.sessionsRoot ?
|
|
9647
|
+
dir = opts.sessionsRoot ? path5.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
9629
9648
|
} else if (opts.sessionsRoot) {
|
|
9630
|
-
dir =
|
|
9649
|
+
dir = path5.join(opts.sessionsRoot, runId);
|
|
9631
9650
|
store = new DefaultSessionStore({ dir });
|
|
9632
9651
|
} else {
|
|
9633
9652
|
throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
|
|
@@ -9935,7 +9954,7 @@ var FleetManager = class {
|
|
|
9935
9954
|
})),
|
|
9936
9955
|
usage: this.usage.snapshot()
|
|
9937
9956
|
};
|
|
9938
|
-
await fsp6.mkdir(
|
|
9957
|
+
await fsp6.mkdir(path5.dirname(this.manifestPath), { recursive: true });
|
|
9939
9958
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
9940
9959
|
return this.manifestPath;
|
|
9941
9960
|
}
|
|
@@ -9956,7 +9975,7 @@ var FleetManager = class {
|
|
|
9956
9975
|
if (!this.manifestPath) return;
|
|
9957
9976
|
if (this.manifestDebounceMs === 0) {
|
|
9958
9977
|
void this.writeManifest().catch((err) => {
|
|
9959
|
-
const detail =
|
|
9978
|
+
const detail = toErrorMessage(err);
|
|
9960
9979
|
process.emitWarning(
|
|
9961
9980
|
`FleetManager manifest write failed: ${detail}`,
|
|
9962
9981
|
"FleetManagerWarning"
|
|
@@ -9969,7 +9988,7 @@ var FleetManager = class {
|
|
|
9969
9988
|
this.manifestTimer = setTimeout(() => {
|
|
9970
9989
|
this.manifestTimer = null;
|
|
9971
9990
|
void this.writeManifest().catch((err) => {
|
|
9972
|
-
const detail =
|
|
9991
|
+
const detail = toErrorMessage(err);
|
|
9973
9992
|
process.emitWarning(
|
|
9974
9993
|
`FleetManager manifest write failed: ${detail}`,
|
|
9975
9994
|
"FleetManagerWarning"
|
|
@@ -9988,7 +10007,7 @@ var FleetManager = class {
|
|
|
9988
10007
|
this.manifestTimer = null;
|
|
9989
10008
|
}
|
|
9990
10009
|
await this.writeManifest().catch((err) => {
|
|
9991
|
-
const detail =
|
|
10010
|
+
const detail = toErrorMessage(err);
|
|
9992
10011
|
process.emitWarning(
|
|
9993
10012
|
`FleetManager manifest write failed: ${detail}`,
|
|
9994
10013
|
"FleetManagerWarning"
|
|
@@ -10083,7 +10102,7 @@ var LINE_SEPARATOR = "\n";
|
|
|
10083
10102
|
var DefaultMailbox = class {
|
|
10084
10103
|
filePath;
|
|
10085
10104
|
constructor(sessionDir) {
|
|
10086
|
-
this.filePath =
|
|
10105
|
+
this.filePath = path5.join(sessionDir, MAILBOX_FILE);
|
|
10087
10106
|
}
|
|
10088
10107
|
get mailboxPath() {
|
|
10089
10108
|
return this.filePath;
|
|
@@ -10108,7 +10127,7 @@ var DefaultMailbox = class {
|
|
|
10108
10127
|
taskContext: input.taskContext
|
|
10109
10128
|
};
|
|
10110
10129
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
10111
|
-
await fsp6.mkdir(
|
|
10130
|
+
await fsp6.mkdir(path5.dirname(this.filePath), { recursive: true });
|
|
10112
10131
|
await withFileLock(this.filePath, async () => {
|
|
10113
10132
|
await fsp6.appendFile(this.filePath, line, "utf8");
|
|
10114
10133
|
});
|
|
@@ -10148,29 +10167,43 @@ var DefaultMailbox = class {
|
|
|
10148
10167
|
}
|
|
10149
10168
|
// ── Ack ───────────────────────────────────────────────────────────────
|
|
10150
10169
|
async ack(input) {
|
|
10151
|
-
|
|
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);
|
|
10152
10178
|
await withFileLock(this.filePath, async () => {
|
|
10153
10179
|
const all = await this._readAll();
|
|
10154
|
-
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10155
|
-
if (idx === -1) return;
|
|
10156
|
-
const msg = all[idx];
|
|
10157
10180
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
msg
|
|
10163
|
-
|
|
10164
|
-
|
|
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
|
+
}
|
|
10165
10200
|
}
|
|
10166
|
-
if (
|
|
10167
|
-
|
|
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");
|
|
10168
10204
|
}
|
|
10169
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
10170
|
-
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
10171
|
-
result = msg;
|
|
10172
10205
|
});
|
|
10173
|
-
return
|
|
10206
|
+
return updated;
|
|
10174
10207
|
}
|
|
10175
10208
|
// ── Agent statuses ────────────────────────────────────────────────────
|
|
10176
10209
|
async getAgentStatuses() {
|
|
@@ -10222,6 +10255,43 @@ var DefaultMailbox = class {
|
|
|
10222
10255
|
await fsp6.writeFile(this.filePath, "", "utf8");
|
|
10223
10256
|
});
|
|
10224
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
|
+
}
|
|
10225
10295
|
// ── Client registry stubs (not applicable per-session) ─────────────────
|
|
10226
10296
|
async registerClient(_input) {
|
|
10227
10297
|
}
|
|
@@ -10390,21 +10460,6 @@ var BrainMonitor = class {
|
|
|
10390
10460
|
}
|
|
10391
10461
|
}
|
|
10392
10462
|
};
|
|
10393
|
-
function projectSlug(absRoot) {
|
|
10394
|
-
const base = slugify(path4.basename(absRoot));
|
|
10395
|
-
const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
10396
|
-
return `${base}-${hash}`;
|
|
10397
|
-
}
|
|
10398
|
-
function slugify(name) {
|
|
10399
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
10400
|
-
}
|
|
10401
|
-
function wstackGlobalRoot() {
|
|
10402
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
10403
|
-
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
10404
|
-
return path4.join(os.homedir(), ".wrongstack");
|
|
10405
|
-
}
|
|
10406
|
-
|
|
10407
|
-
// src/coordination/global-mailbox.ts
|
|
10408
10463
|
var MAILBOX_FILE2 = "_mailbox.jsonl";
|
|
10409
10464
|
var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
|
|
10410
10465
|
var AGENT_STALE_MS = 6e4;
|
|
@@ -10412,8 +10467,9 @@ var CLIENT_STALE_MS = 6e4;
|
|
|
10412
10467
|
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
10413
10468
|
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
10414
10469
|
var LINE_SEPARATOR2 = "\n";
|
|
10470
|
+
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
10415
10471
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
10416
|
-
return
|
|
10472
|
+
return path5.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
10417
10473
|
}
|
|
10418
10474
|
var GlobalMailbox = class {
|
|
10419
10475
|
/** Path to the JSONL message file. */
|
|
@@ -10444,14 +10500,28 @@ var GlobalMailbox = class {
|
|
|
10444
10500
|
_lastHeartbeat = /* @__PURE__ */ new Map();
|
|
10445
10501
|
/** Last time each local client sent a heartbeat (throttle). */
|
|
10446
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;
|
|
10447
10517
|
/**
|
|
10448
10518
|
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
10449
10519
|
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
10450
10520
|
*/
|
|
10451
10521
|
constructor(projectDir, events) {
|
|
10452
|
-
this.messagePath =
|
|
10453
|
-
this.registryPath =
|
|
10454
|
-
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);
|
|
10455
10525
|
this._events = events;
|
|
10456
10526
|
}
|
|
10457
10527
|
// ── Messages ────────────────────────────────────────────────────────────
|
|
@@ -10474,73 +10544,89 @@ var GlobalMailbox = class {
|
|
|
10474
10544
|
taskContext: input.taskContext
|
|
10475
10545
|
};
|
|
10476
10546
|
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
10477
|
-
await fsp6.mkdir(
|
|
10547
|
+
await fsp6.mkdir(path5.dirname(this.messagePath), { recursive: true });
|
|
10478
10548
|
await withFileLock(this.messagePath, async () => {
|
|
10479
10549
|
await fsp6.appendFile(this.messagePath, line, "utf8");
|
|
10550
|
+
this._pushToCache(msg);
|
|
10480
10551
|
});
|
|
10481
10552
|
return msg;
|
|
10482
10553
|
}
|
|
10483
10554
|
async query(q) {
|
|
10484
|
-
const all = await this.
|
|
10555
|
+
const all = await this._readMessagesCached();
|
|
10485
10556
|
const limit = q.limit ?? 50;
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
}
|
|
10502
|
-
if (q.minPriority !== void 0) {
|
|
10503
|
-
const order = { low: 0, normal: 1, high: 2 };
|
|
10504
|
-
const min = order[q.minPriority];
|
|
10505
|
-
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
10506
|
-
}
|
|
10507
|
-
if (q.since !== void 0) {
|
|
10508
|
-
const since = q.since;
|
|
10509
|
-
filtered = filtered.filter((m) => m.timestamp > since);
|
|
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);
|
|
10510
10572
|
}
|
|
10511
|
-
|
|
10512
|
-
return
|
|
10573
|
+
out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
10574
|
+
return out.slice(0, limit).map((m) => ({ ...m, readBy: { ...m.readBy } }));
|
|
10513
10575
|
}
|
|
10514
10576
|
async ack(input) {
|
|
10515
|
-
|
|
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;
|
|
10516
10588
|
await withFileLock(this.messagePath, async () => {
|
|
10517
|
-
const all = await this.
|
|
10518
|
-
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10519
|
-
if (idx === -1) return;
|
|
10520
|
-
const msg = all[idx];
|
|
10589
|
+
const all = await this._readMessagesFresh();
|
|
10521
10590
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
msg
|
|
10527
|
-
|
|
10528
|
-
|
|
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
|
+
}
|
|
10529
10610
|
}
|
|
10530
|
-
if (
|
|
10531
|
-
|
|
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");
|
|
10532
10614
|
}
|
|
10533
|
-
|
|
10534
|
-
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10535
|
-
result = msg;
|
|
10615
|
+
cacheSnapshot = all;
|
|
10536
10616
|
});
|
|
10537
|
-
|
|
10617
|
+
if (cacheSnapshot) this._setMessageCache(cacheSnapshot);
|
|
10618
|
+
return updated;
|
|
10538
10619
|
}
|
|
10539
10620
|
async unreadCount(forAgentId) {
|
|
10540
|
-
const all = await this.
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
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;
|
|
10544
10630
|
}
|
|
10545
10631
|
// ── Agent registry ──────────────────────────────────────────────────────
|
|
10546
10632
|
async registerAgent(input) {
|
|
@@ -10701,13 +10787,62 @@ var GlobalMailbox = class {
|
|
|
10701
10787
|
async close() {
|
|
10702
10788
|
this._registryCache = null;
|
|
10703
10789
|
this._clientRegistryCache = null;
|
|
10790
|
+
this._messageCache = null;
|
|
10791
|
+
this._messageCacheMtime = -1;
|
|
10792
|
+
this._messageCacheSize = -1;
|
|
10704
10793
|
}
|
|
10705
10794
|
async clearAll() {
|
|
10706
10795
|
await withFileLock(this.messagePath, async () => {
|
|
10707
10796
|
await fsp6.writeFile(this.messagePath, "", "utf8");
|
|
10708
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
|
+
};
|
|
10709
10838
|
}
|
|
10710
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
|
+
*/
|
|
10711
10846
|
async _readMessages() {
|
|
10712
10847
|
try {
|
|
10713
10848
|
const raw = await fsp6.readFile(this.messagePath, "utf8");
|
|
@@ -10735,8 +10870,85 @@ var GlobalMailbox = class {
|
|
|
10735
10870
|
throw err;
|
|
10736
10871
|
}
|
|
10737
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
|
+
}
|
|
10738
10950
|
async _ensureRegistry() {
|
|
10739
|
-
await fsp6.mkdir(
|
|
10951
|
+
await fsp6.mkdir(path5.dirname(this.registryPath), { recursive: true });
|
|
10740
10952
|
}
|
|
10741
10953
|
async _readRegistry(opts) {
|
|
10742
10954
|
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
@@ -10781,7 +10993,7 @@ var GlobalMailbox = class {
|
|
|
10781
10993
|
}
|
|
10782
10994
|
// ── Client registry internals ───────────────────────────────────────────
|
|
10783
10995
|
async _ensureClientRegistry() {
|
|
10784
|
-
await fsp6.mkdir(
|
|
10996
|
+
await fsp6.mkdir(path5.dirname(this.clientRegistryPath), { recursive: true });
|
|
10785
10997
|
}
|
|
10786
10998
|
async _readClientRegistry(opts) {
|
|
10787
10999
|
if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
@@ -11421,7 +11633,7 @@ function createMailboxHooks(opts) {
|
|
|
11421
11633
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
11422
11634
|
var LOG_FILENAME = "package-authors.json";
|
|
11423
11635
|
function logPath(storageDir) {
|
|
11424
|
-
return
|
|
11636
|
+
return path5.join(storageDir, LOG_FILENAME);
|
|
11425
11637
|
}
|
|
11426
11638
|
async function loadLog(storageDir, projectRoot) {
|
|
11427
11639
|
try {
|
|
@@ -11445,7 +11657,7 @@ async function saveLog(storageDir, log) {
|
|
|
11445
11657
|
await fsp6.rename(tmp, logPath(storageDir));
|
|
11446
11658
|
}
|
|
11447
11659
|
function detectEcosystem(manifestPath) {
|
|
11448
|
-
const name =
|
|
11660
|
+
const name = path5.basename(manifestPath).toLowerCase();
|
|
11449
11661
|
if (name === "package.json") return "npm";
|
|
11450
11662
|
if (name === "go.mod") return "go";
|
|
11451
11663
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -11642,7 +11854,7 @@ function startPackageOutdatedWatcher(opts) {
|
|
|
11642
11854
|
);
|
|
11643
11855
|
} catch (err) {
|
|
11644
11856
|
handleError(err);
|
|
11645
|
-
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${
|
|
11857
|
+
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${toErrorMessage(err)}`);
|
|
11646
11858
|
}
|
|
11647
11859
|
}
|
|
11648
11860
|
}
|
|
@@ -11715,7 +11927,2055 @@ mvn versions:use-latest-versions`;
|
|
|
11715
11927
|
return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
|
|
11716
11928
|
}
|
|
11717
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
|
+
};
|
|
11718
13978
|
|
|
11719
|
-
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
|
|
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 };
|
|
11720
13980
|
//# sourceMappingURL=index.js.map
|
|
11721
13981
|
//# sourceMappingURL=index.js.map
|