@ra3orblade/swarm 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/dist/swarm-hook.js +39 -0
- package/dist/swarm.js +39 -1
- package/dist/swarmd.js +1652 -117
- package/package.json +1 -1
- package/web/app.js +702 -66
- package/web/icons.js +2 -2
- package/web/index.html +101 -28
- package/web/menus.js +7 -7
- package/web/release-notes.js +1 -1
- package/web/table.js +1 -1
- package/web/viz.js +76 -2
package/dist/swarmd.js
CHANGED
|
@@ -68,6 +68,44 @@ function clearDaemonInfo() {
|
|
|
68
68
|
function daemonCommand() {
|
|
69
69
|
return resolveBin("swarmd");
|
|
70
70
|
}
|
|
71
|
+
// packages/core/src/abtrial.ts
|
|
72
|
+
var ms = (a, b) => b ? Math.max(0, new Date(b).getTime() - new Date(a).getTime()) : null;
|
|
73
|
+
function splitArmTask(id) {
|
|
74
|
+
const i = id.lastIndexOf("#");
|
|
75
|
+
return i < 0 ? { task: id, arm: null } : { task: id.slice(0, i), arm: id.slice(i + 1) };
|
|
76
|
+
}
|
|
77
|
+
var armTask = (task, label) => `${task}#${label}`;
|
|
78
|
+
function scoreTrial(task, arms) {
|
|
79
|
+
const scored = arms.map((a) => {
|
|
80
|
+
const ineligibleFor = a.state === "running" ? "still running" : a.state === "failed" ? "crashed" : a.gatesFailed > 0 ? "failed a gate" : null;
|
|
81
|
+
return {
|
|
82
|
+
...a,
|
|
83
|
+
wallMs: ms(a.startedAt, a.endedAt),
|
|
84
|
+
churn: a.insertions === null && a.deletions === null ? null : (a.insertions ?? 0) + (a.deletions ?? 0),
|
|
85
|
+
eligible: ineligibleFor === null,
|
|
86
|
+
ineligibleFor,
|
|
87
|
+
winner: false
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
const eligible = scored.filter((a) => a.eligible).sort((a, b) => a.costUsd - b.costUsd || (a.wallMs ?? Number.POSITIVE_INFINITY) - (b.wallMs ?? Number.POSITIVE_INFINITY) || a.label.localeCompare(b.label));
|
|
91
|
+
const best = eligible[0] ?? null;
|
|
92
|
+
if (best)
|
|
93
|
+
best.winner = true;
|
|
94
|
+
const finished = scored.filter((a) => a.state !== "running").length;
|
|
95
|
+
const dearest = eligible.at(-1);
|
|
96
|
+
return {
|
|
97
|
+
task,
|
|
98
|
+
arms: scored.sort((a, b) => Number(b.eligible) - Number(a.eligible) || a.costUsd - b.costUsd || a.label.localeCompare(b.label)),
|
|
99
|
+
winner: best?.label ?? null,
|
|
100
|
+
verdict: best ? "winner" : scored.some((a) => a.state === "running") ? "undecided" : "all-failed",
|
|
101
|
+
totals: {
|
|
102
|
+
arms: scored.length,
|
|
103
|
+
finished,
|
|
104
|
+
costUsd: scored.reduce((a, x) => a + x.costUsd, 0),
|
|
105
|
+
savedUsd: best && dearest ? Math.max(0, dearest.costUsd - best.costUsd) : 0
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
71
109
|
// packages/core/src/actor.ts
|
|
72
110
|
var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
73
111
|
var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
@@ -691,6 +729,38 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
|
|
|
691
729
|
payload.prompt = raw.prompt;
|
|
692
730
|
return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
|
|
693
731
|
}
|
|
732
|
+
// packages/core/src/art.ts
|
|
733
|
+
var ROBOT = [
|
|
734
|
+
" g g ",
|
|
735
|
+
" X X ",
|
|
736
|
+
" X X ",
|
|
737
|
+
" ddddddddddddd ",
|
|
738
|
+
" dXXXXXXXXXXXd ",
|
|
739
|
+
" dggXXXXXXXXXd ",
|
|
740
|
+
" dddXXddXXXddXXddd ",
|
|
741
|
+
" dddXXddXXXddXXddd ",
|
|
742
|
+
" dddXXXXXXXXXXXddd ",
|
|
743
|
+
" dddXXdddddddXXddd ",
|
|
744
|
+
" dXXXXXXXXXXXd ",
|
|
745
|
+
" dXXXXXXXXXXXd ",
|
|
746
|
+
" ddddddddddddd ",
|
|
747
|
+
" XXX ",
|
|
748
|
+
" ddddd ",
|
|
749
|
+
" ddddddddddd ",
|
|
750
|
+
" dXXdggXXXXXXXdXXd ",
|
|
751
|
+
" dXXdXggggXddddXXd ",
|
|
752
|
+
" dXXdXggggXXXXdXXd ",
|
|
753
|
+
" dXXdXXXXXXddddXXd ",
|
|
754
|
+
" dXXdXXXXXXXXXdXXd ",
|
|
755
|
+
" dXXdXdddXXXXXdXXd ",
|
|
756
|
+
" ddddXXXXXXXXXdddd ",
|
|
757
|
+
" ddddddddddd ",
|
|
758
|
+
" XX XX ",
|
|
759
|
+
" XX XX ",
|
|
760
|
+
" XXXX XXXX ",
|
|
761
|
+
" dddd dddd "
|
|
762
|
+
];
|
|
763
|
+
var ROBOT_HEAD = ROBOT.slice(0, 13);
|
|
694
764
|
// packages/core/src/audit.ts
|
|
695
765
|
var AUDIT_TYPES = new Set([
|
|
696
766
|
"session.started",
|
|
@@ -836,8 +906,8 @@ function sinceToIso(since, now = Date.now()) {
|
|
|
836
906
|
const m = /^(\d+)([dhm])$/.exec(since.trim());
|
|
837
907
|
if (m) {
|
|
838
908
|
const n = Number(m[1]);
|
|
839
|
-
const
|
|
840
|
-
return new Date(now - n *
|
|
909
|
+
const ms2 = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
|
|
910
|
+
return new Date(now - n * ms2).toISOString();
|
|
841
911
|
}
|
|
842
912
|
const t = Date.parse(since);
|
|
843
913
|
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
@@ -1231,6 +1301,238 @@ function loadConfigDetailed(opts = {}) {
|
|
|
1231
1301
|
function loadConfig(opts = {}) {
|
|
1232
1302
|
return loadConfigDetailed(opts).config;
|
|
1233
1303
|
}
|
|
1304
|
+
// packages/core/src/context.ts
|
|
1305
|
+
var CHARS_PER_TOKEN = 4;
|
|
1306
|
+
var estTokens = (chars) => Math.round(chars / CHARS_PER_TOKEN);
|
|
1307
|
+
var CONTEXT_DEFAULTS = { worstLimit: 5, toolLimit: 12 };
|
|
1308
|
+
function rereadWaste(reads) {
|
|
1309
|
+
const byPath = new Map;
|
|
1310
|
+
for (const r of reads) {
|
|
1311
|
+
const hit = byPath.get(r.path);
|
|
1312
|
+
if (hit) {
|
|
1313
|
+
hit.reads++;
|
|
1314
|
+
hit.chars += r.chars;
|
|
1315
|
+
} else {
|
|
1316
|
+
byPath.set(r.path, { reads: 1, chars: r.chars, first: r.chars });
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const out = [];
|
|
1320
|
+
for (const [path, v] of byPath) {
|
|
1321
|
+
if (v.reads < 2)
|
|
1322
|
+
continue;
|
|
1323
|
+
out.push({ path, reads: v.reads, chars: v.chars, wastedChars: v.chars - v.first });
|
|
1324
|
+
}
|
|
1325
|
+
return out.sort((a, b) => b.wastedChars - a.wastedChars || a.path.localeCompare(b.path));
|
|
1326
|
+
}
|
|
1327
|
+
function contextReport(results, reads, turns, opts = {}) {
|
|
1328
|
+
const o = { ...CONTEXT_DEFAULTS, ...opts };
|
|
1329
|
+
const ids = new Set;
|
|
1330
|
+
for (const r of results)
|
|
1331
|
+
ids.add(r.sessionId);
|
|
1332
|
+
for (const r of reads)
|
|
1333
|
+
ids.add(r.sessionId);
|
|
1334
|
+
for (const t of turns)
|
|
1335
|
+
ids.add(t.sessionId);
|
|
1336
|
+
const readsBySession = new Map;
|
|
1337
|
+
for (const r of reads) {
|
|
1338
|
+
const list = readsBySession.get(r.sessionId);
|
|
1339
|
+
if (list)
|
|
1340
|
+
list.push(r);
|
|
1341
|
+
else
|
|
1342
|
+
readsBySession.set(r.sessionId, [r]);
|
|
1343
|
+
}
|
|
1344
|
+
const charsBySession = new Map;
|
|
1345
|
+
for (const r of results)
|
|
1346
|
+
charsBySession.set(r.sessionId, (charsBySession.get(r.sessionId) ?? 0) + r.chars);
|
|
1347
|
+
const tokensBySession = new Map;
|
|
1348
|
+
for (const t of turns) {
|
|
1349
|
+
const hit = tokensBySession.get(t.sessionId);
|
|
1350
|
+
if (hit) {
|
|
1351
|
+
hit.input += t.input;
|
|
1352
|
+
hit.cacheRead += t.cacheRead;
|
|
1353
|
+
hit.cacheWrite += t.cacheWrite;
|
|
1354
|
+
hit.thinking += t.thinking;
|
|
1355
|
+
hit.output += t.output;
|
|
1356
|
+
} else
|
|
1357
|
+
tokensBySession.set(t.sessionId, { ...t });
|
|
1358
|
+
}
|
|
1359
|
+
const sessions = [];
|
|
1360
|
+
for (const sessionId of ids) {
|
|
1361
|
+
const worstAll = rereadWaste(readsBySession.get(sessionId) ?? []);
|
|
1362
|
+
const wastedChars2 = worstAll.reduce((a, w) => a + w.wastedChars, 0);
|
|
1363
|
+
const toolChars2 = charsBySession.get(sessionId) ?? 0;
|
|
1364
|
+
const tk = tokensBySession.get(sessionId);
|
|
1365
|
+
const cacheable = (tk?.cacheRead ?? 0) + (tk?.input ?? 0);
|
|
1366
|
+
sessions.push({
|
|
1367
|
+
sessionId,
|
|
1368
|
+
toolChars: toolChars2,
|
|
1369
|
+
toolTokens: estTokens(toolChars2),
|
|
1370
|
+
thinking: tk?.thinking ?? 0,
|
|
1371
|
+
cacheRead: tk?.cacheRead ?? 0,
|
|
1372
|
+
input: tk?.input ?? 0,
|
|
1373
|
+
cacheHit: cacheable ? (tk?.cacheRead ?? 0) / cacheable : 0,
|
|
1374
|
+
reads: (readsBySession.get(sessionId) ?? []).length,
|
|
1375
|
+
rereadFiles: worstAll.length,
|
|
1376
|
+
wastedChars: wastedChars2,
|
|
1377
|
+
wasteShare: toolChars2 ? wastedChars2 / toolChars2 : 0,
|
|
1378
|
+
worst: worstAll.slice(0, o.worstLimit)
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
sessions.sort((a, b) => b.wastedChars - a.wastedChars || b.toolChars - a.toolChars);
|
|
1382
|
+
const byToolMap = new Map;
|
|
1383
|
+
for (const r of results) {
|
|
1384
|
+
const hit = byToolMap.get(r.tool);
|
|
1385
|
+
if (hit) {
|
|
1386
|
+
hit.calls++;
|
|
1387
|
+
hit.chars += r.chars;
|
|
1388
|
+
} else
|
|
1389
|
+
byToolMap.set(r.tool, { tool: r.tool, calls: 1, chars: r.chars, tokens: 0 });
|
|
1390
|
+
}
|
|
1391
|
+
const byTool = [...byToolMap.values()].map((t) => ({ ...t, tokens: estTokens(t.chars) })).sort((a, b) => b.chars - a.chars || a.tool.localeCompare(b.tool)).slice(0, o.toolLimit);
|
|
1392
|
+
const toolChars = sessions.reduce((a, s) => a + s.toolChars, 0);
|
|
1393
|
+
const wastedChars = sessions.reduce((a, s) => a + s.wastedChars, 0);
|
|
1394
|
+
const cacheRead = sessions.reduce((a, s) => a + s.cacheRead, 0);
|
|
1395
|
+
const input = sessions.reduce((a, s) => a + s.input, 0);
|
|
1396
|
+
return {
|
|
1397
|
+
sessions,
|
|
1398
|
+
byTool,
|
|
1399
|
+
totals: {
|
|
1400
|
+
sessions: sessions.length,
|
|
1401
|
+
toolChars,
|
|
1402
|
+
toolTokens: estTokens(toolChars),
|
|
1403
|
+
wastedChars,
|
|
1404
|
+
wastedTokens: estTokens(wastedChars),
|
|
1405
|
+
wasteShare: toolChars ? wastedChars / toolChars : 0,
|
|
1406
|
+
rereadFiles: sessions.reduce((a, s) => a + s.rereadFiles, 0),
|
|
1407
|
+
cacheHit: cacheRead + input ? cacheRead / (cacheRead + input) : 0
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
// packages/core/src/dag.ts
|
|
1412
|
+
var DAG_DEFAULTS = { dx: 190, dy: 34, passes: 2 };
|
|
1413
|
+
var edgeKey = (from, to) => JSON.stringify([from, to]);
|
|
1414
|
+
function findBackEdges(nodes, edges) {
|
|
1415
|
+
const ids = nodes.map((n) => n.id);
|
|
1416
|
+
const known = new Set(ids);
|
|
1417
|
+
const out = new Map;
|
|
1418
|
+
for (const id of ids)
|
|
1419
|
+
out.set(id, []);
|
|
1420
|
+
for (const e of edges)
|
|
1421
|
+
if (known.has(e.from) && known.has(e.to) && e.from !== e.to)
|
|
1422
|
+
out.get(e.from)?.push(e.to);
|
|
1423
|
+
for (const [, list] of out)
|
|
1424
|
+
list.sort();
|
|
1425
|
+
const back = new Set;
|
|
1426
|
+
const state = new Map;
|
|
1427
|
+
const visit = (start) => {
|
|
1428
|
+
const stack = [{ id: start, i: 0 }];
|
|
1429
|
+
state.set(start, 1);
|
|
1430
|
+
while (stack.length) {
|
|
1431
|
+
const top = stack[stack.length - 1];
|
|
1432
|
+
const next = out.get(top.id) ?? [];
|
|
1433
|
+
if (top.i >= next.length) {
|
|
1434
|
+
state.set(top.id, 2);
|
|
1435
|
+
stack.pop();
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const to = next[top.i++];
|
|
1439
|
+
const st = state.get(to) ?? 0;
|
|
1440
|
+
if (st === 1)
|
|
1441
|
+
back.add(edgeKey(top.id, to));
|
|
1442
|
+
else if (st === 0) {
|
|
1443
|
+
state.set(to, 1);
|
|
1444
|
+
stack.push({ id: to, i: 0 });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
for (const id of [...ids].sort())
|
|
1449
|
+
if ((state.get(id) ?? 0) === 0)
|
|
1450
|
+
visit(id);
|
|
1451
|
+
return back;
|
|
1452
|
+
}
|
|
1453
|
+
function assignLayers(nodes, forward) {
|
|
1454
|
+
const layer = new Map;
|
|
1455
|
+
const parents = new Map;
|
|
1456
|
+
for (const n of nodes)
|
|
1457
|
+
parents.set(n.id, []);
|
|
1458
|
+
for (const e of forward)
|
|
1459
|
+
parents.get(e.to)?.push(e.from);
|
|
1460
|
+
const resolving = new Set;
|
|
1461
|
+
const depth = (id) => {
|
|
1462
|
+
const fixed = nodes.find((n) => n.id === id)?.layer;
|
|
1463
|
+
if (typeof fixed === "number") {
|
|
1464
|
+
layer.set(id, fixed);
|
|
1465
|
+
return fixed;
|
|
1466
|
+
}
|
|
1467
|
+
const hit = layer.get(id);
|
|
1468
|
+
if (hit !== undefined)
|
|
1469
|
+
return hit;
|
|
1470
|
+
if (resolving.has(id))
|
|
1471
|
+
return 0;
|
|
1472
|
+
resolving.add(id);
|
|
1473
|
+
const ps = parents.get(id) ?? [];
|
|
1474
|
+
const d = ps.length ? Math.max(...ps.map((p) => depth(p) + 1)) : 0;
|
|
1475
|
+
resolving.delete(id);
|
|
1476
|
+
layer.set(id, d);
|
|
1477
|
+
return d;
|
|
1478
|
+
};
|
|
1479
|
+
for (const n of [...nodes].sort((a, b) => a.id.localeCompare(b.id)))
|
|
1480
|
+
depth(n.id);
|
|
1481
|
+
return layer;
|
|
1482
|
+
}
|
|
1483
|
+
function layoutDag(nodes, edges, opts = {}) {
|
|
1484
|
+
const o = { ...DAG_DEFAULTS, ...opts };
|
|
1485
|
+
if (!nodes.length)
|
|
1486
|
+
return { nodes: [], edges: [], width: 0, height: 0, layers: 0 };
|
|
1487
|
+
const known = new Set(nodes.map((n) => n.id));
|
|
1488
|
+
const clean = edges.filter((e) => known.has(e.from) && known.has(e.to) && e.from !== e.to);
|
|
1489
|
+
const back = findBackEdges(nodes, clean);
|
|
1490
|
+
const isBack = (e) => back.has(edgeKey(e.from, e.to));
|
|
1491
|
+
const forward = clean.filter((e) => !isBack(e));
|
|
1492
|
+
const layerOf = assignLayers(nodes, forward);
|
|
1493
|
+
const maxLayer = Math.max(0, ...nodes.map((n) => layerOf.get(n.id) ?? 0));
|
|
1494
|
+
const groups = Array.from({ length: maxLayer + 1 }, () => []);
|
|
1495
|
+
for (const n of [...nodes].sort((a, b) => {
|
|
1496
|
+
const sa = o.seed?.[a.id] ?? Number.POSITIVE_INFINITY;
|
|
1497
|
+
const sb = o.seed?.[b.id] ?? Number.POSITIVE_INFINITY;
|
|
1498
|
+
return sa - sb || a.id.localeCompare(b.id);
|
|
1499
|
+
}))
|
|
1500
|
+
groups[layerOf.get(n.id) ?? 0].push(n.id);
|
|
1501
|
+
const parentsOf = new Map;
|
|
1502
|
+
for (const n of nodes)
|
|
1503
|
+
parentsOf.set(n.id, []);
|
|
1504
|
+
for (const e of forward)
|
|
1505
|
+
parentsOf.get(e.to)?.push(e.from);
|
|
1506
|
+
for (let pass = 0;pass < o.passes; pass++) {
|
|
1507
|
+
for (let l = 1;l <= maxLayer; l++) {
|
|
1508
|
+
const prev = groups[l - 1];
|
|
1509
|
+
const idx = new Map(prev.map((id, i) => [id, i]));
|
|
1510
|
+
const group = groups[l];
|
|
1511
|
+
const bary = new Map;
|
|
1512
|
+
for (const id of group) {
|
|
1513
|
+
const ps = (parentsOf.get(id) ?? []).map((p) => idx.get(p)).filter((v) => v !== undefined);
|
|
1514
|
+
bary.set(id, ps.length ? ps.reduce((a, b) => a + b, 0) / ps.length : group.indexOf(id));
|
|
1515
|
+
}
|
|
1516
|
+
group.sort((a, b) => bary.get(a) - bary.get(b) || a.localeCompare(b));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const widest = Math.max(1, ...groups.map((g) => g.length));
|
|
1520
|
+
const placed = [];
|
|
1521
|
+
groups.forEach((group, l) => {
|
|
1522
|
+
const offset = (widest - group.length) * o.dy / 2;
|
|
1523
|
+
group.forEach((id, i) => {
|
|
1524
|
+
placed.push({ id, layer: l, order: i, x: l * o.dx, y: offset + i * o.dy });
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
placed.sort((a, b) => a.layer - b.layer || a.order - b.order);
|
|
1528
|
+
return {
|
|
1529
|
+
nodes: placed,
|
|
1530
|
+
edges: clean.map((e) => ({ ...e, back: isBack(e) })),
|
|
1531
|
+
width: maxLayer * o.dx,
|
|
1532
|
+
height: Math.max(0, (widest - 1) * o.dy),
|
|
1533
|
+
layers: maxLayer + 1
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1234
1536
|
// packages/core/src/dispatch.ts
|
|
1235
1537
|
function planDispatch(tasks, wanted, opts) {
|
|
1236
1538
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
@@ -1666,6 +1968,83 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
|
|
|
1666
1968
|
return { title, body: b.join(`
|
|
1667
1969
|
`) };
|
|
1668
1970
|
}
|
|
1971
|
+
// packages/core/src/gatehealth.ts
|
|
1972
|
+
var ms2 = (iso) => new Date(iso).getTime();
|
|
1973
|
+
function percentile(xs, p) {
|
|
1974
|
+
if (!xs.length)
|
|
1975
|
+
return null;
|
|
1976
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
1977
|
+
const rank = Math.max(1, Math.ceil(p / 100 * s.length));
|
|
1978
|
+
return s[Math.min(s.length, rank) - 1];
|
|
1979
|
+
}
|
|
1980
|
+
var GATE_HEALTH_DEFAULTS = { historyLimit: 20 };
|
|
1981
|
+
function gateHealth(samples, opts = {}) {
|
|
1982
|
+
const o = { ...GATE_HEALTH_DEFAULTS, ...opts };
|
|
1983
|
+
const byGate = new Map;
|
|
1984
|
+
for (const r of samples) {
|
|
1985
|
+
const list = byGate.get(r.gate);
|
|
1986
|
+
if (list)
|
|
1987
|
+
list.push(r);
|
|
1988
|
+
else
|
|
1989
|
+
byGate.set(r.gate, [r]);
|
|
1990
|
+
}
|
|
1991
|
+
const gates = [];
|
|
1992
|
+
for (const [gate, runsUnsorted] of byGate) {
|
|
1993
|
+
const runs = [...runsUnsorted].sort((a, b) => ms2(a.at) - ms2(b.at));
|
|
1994
|
+
const passes = runs.filter((r) => r.verdict === "pass").length;
|
|
1995
|
+
const byTask = new Map;
|
|
1996
|
+
for (const r of runs) {
|
|
1997
|
+
const key = `${r.projectId ?? ""}\x00${r.task}`;
|
|
1998
|
+
const list = byTask.get(key);
|
|
1999
|
+
if (list)
|
|
2000
|
+
list.push(r);
|
|
2001
|
+
else
|
|
2002
|
+
byTask.set(key, [r]);
|
|
2003
|
+
}
|
|
2004
|
+
let flips = 0;
|
|
2005
|
+
let flakyTasks = 0;
|
|
2006
|
+
for (const list of byTask.values()) {
|
|
2007
|
+
let changed = 0;
|
|
2008
|
+
for (let i = 1;i < list.length; i++)
|
|
2009
|
+
if (list[i].verdict !== list[i - 1].verdict)
|
|
2010
|
+
changed++;
|
|
2011
|
+
flips += changed;
|
|
2012
|
+
if (list.some((r) => r.verdict === "pass") && list.some((r) => r.verdict === "fail"))
|
|
2013
|
+
flakyTasks++;
|
|
2014
|
+
}
|
|
2015
|
+
const timed = runs.map((r) => r.durationMs).filter((d) => typeof d === "number");
|
|
2016
|
+
const last = runs.at(-1);
|
|
2017
|
+
gates.push({
|
|
2018
|
+
gate,
|
|
2019
|
+
runs: runs.length,
|
|
2020
|
+
passes,
|
|
2021
|
+
fails: runs.length - passes,
|
|
2022
|
+
passRate: passes / runs.length,
|
|
2023
|
+
flips,
|
|
2024
|
+
flakyTasks,
|
|
2025
|
+
flaky: flakyTasks > 0,
|
|
2026
|
+
p50Ms: percentile(timed, 50),
|
|
2027
|
+
p95Ms: percentile(timed, 95),
|
|
2028
|
+
maxMs: timed.length ? Math.max(...timed) : null,
|
|
2029
|
+
totalMs: timed.reduce((a, b) => a + b, 0),
|
|
2030
|
+
timedRuns: timed.length,
|
|
2031
|
+
lastVerdict: last?.verdict ?? null,
|
|
2032
|
+
lastAt: last?.at ?? null,
|
|
2033
|
+
history: runs.slice(-o.historyLimit).reverse().map((r) => ({ verdict: r.verdict, at: r.at, task: r.task, durationMs: r.durationMs }))
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
gates.sort((a, b) => Number(b.flaky) - Number(a.flaky) || b.flakyTasks - a.flakyTasks || b.totalMs - a.totalMs);
|
|
2037
|
+
return {
|
|
2038
|
+
gates,
|
|
2039
|
+
totals: {
|
|
2040
|
+
gates: gates.length,
|
|
2041
|
+
runs: samples.length,
|
|
2042
|
+
fails: gates.reduce((a, g) => a + g.fails, 0),
|
|
2043
|
+
flakyGates: gates.filter((g) => g.flaky).length,
|
|
2044
|
+
totalMs: gates.reduce((a, g) => a + g.totalMs, 0)
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
1669
2048
|
// packages/core/src/gates.ts
|
|
1670
2049
|
var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
1671
2050
|
function validateGateRun(input) {
|
|
@@ -1725,7 +2104,8 @@ function executedGateInput(task, gate, cmd, outcome) {
|
|
|
1725
2104
|
gate,
|
|
1726
2105
|
verdict: outcome.exitCode === 0 && !outcome.timedOut ? "pass" : "fail",
|
|
1727
2106
|
rubric: `ran \`${cmd}\` \u2014 ${how} in ${(outcome.durationMs / 1000).toFixed(1)}s`,
|
|
1728
|
-
evidence: evidenceTail(outcome.output) || null
|
|
2107
|
+
evidence: evidenceTail(outcome.output) || null,
|
|
2108
|
+
durationMs: outcome.durationMs
|
|
1729
2109
|
};
|
|
1730
2110
|
}
|
|
1731
2111
|
// packages/core/src/graphs.ts
|
|
@@ -1763,6 +2143,159 @@ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
|
1763
2143
|
contested: out.filter((f) => f.contested).length
|
|
1764
2144
|
};
|
|
1765
2145
|
}
|
|
2146
|
+
// packages/core/src/worktree.ts
|
|
2147
|
+
import { join as join3 } from "path";
|
|
2148
|
+
function planBootstrap(cfg, repoRoot, worktree) {
|
|
2149
|
+
const seen = new Set;
|
|
2150
|
+
const copies = [];
|
|
2151
|
+
for (const raw of cfg.worktree.copy) {
|
|
2152
|
+
if (!isRepoRelative(raw))
|
|
2153
|
+
continue;
|
|
2154
|
+
const rel = raw.trim().replace(/^\.\//, "");
|
|
2155
|
+
if (seen.has(rel))
|
|
2156
|
+
continue;
|
|
2157
|
+
seen.add(rel);
|
|
2158
|
+
copies.push({ rel, from: join3(repoRoot, rel), to: join3(worktree, rel) });
|
|
2159
|
+
}
|
|
2160
|
+
return { copies, setup: cfg.worktree.setup };
|
|
2161
|
+
}
|
|
2162
|
+
var needsBootstrap = (plan) => plan.copies.length > 0 || plan.setup !== null;
|
|
2163
|
+
function summarizeBootstrap(o) {
|
|
2164
|
+
const parts = [];
|
|
2165
|
+
if (o.copied.length)
|
|
2166
|
+
parts.push(`copied ${o.copied.join(", ")}`);
|
|
2167
|
+
if (o.skipped.length)
|
|
2168
|
+
parts.push(`skipped ${o.skipped.join(", ")} (missing)`);
|
|
2169
|
+
if (o.setup)
|
|
2170
|
+
parts.push(`${o.setup.command} \u2192 ${o.setup.exitCode === 0 ? "ok" : `exit ${o.setup.exitCode}`} in ${(o.setup.durationMs / 1000).toFixed(1)}s`);
|
|
2171
|
+
return parts.join("; ") || "nothing to do";
|
|
2172
|
+
}
|
|
2173
|
+
function canRemoveWorktree(w, heldByClaim, force) {
|
|
2174
|
+
if (w.main)
|
|
2175
|
+
return { ok: false, reason: "main" };
|
|
2176
|
+
if (heldByClaim)
|
|
2177
|
+
return { ok: false, reason: "held" };
|
|
2178
|
+
if (force)
|
|
2179
|
+
return { ok: true };
|
|
2180
|
+
if (w.dirty > 0)
|
|
2181
|
+
return { ok: false, reason: "dirty" };
|
|
2182
|
+
if (w.ahead > 0)
|
|
2183
|
+
return { ok: false, reason: "unpushed" };
|
|
2184
|
+
return { ok: true };
|
|
2185
|
+
}
|
|
2186
|
+
function removeRefusalMessage(reason, path, task) {
|
|
2187
|
+
switch (reason) {
|
|
2188
|
+
case "main":
|
|
2189
|
+
return `${path} is the main checkout \u2014 it is never removed`;
|
|
2190
|
+
case "held":
|
|
2191
|
+
return `${path} is held by claim ${task ?? "?"} \u2014 release the claim instead`;
|
|
2192
|
+
case "dirty":
|
|
2193
|
+
return `${path} has uncommitted changes \u2014 commit or stash them, or --force to discard`;
|
|
2194
|
+
case "unpushed":
|
|
2195
|
+
return `${path} has unpushed commits \u2014 push them, or --force to discard`;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function planGc(worktrees, claims) {
|
|
2199
|
+
const held = new Map(claims.filter((c) => c.state === "held").map((c) => [c.worktree, c.task]));
|
|
2200
|
+
const stale = new Set(claims.filter((c) => c.state !== "held").map((c) => c.worktree));
|
|
2201
|
+
const out = [];
|
|
2202
|
+
for (const w of worktrees) {
|
|
2203
|
+
if (w.main || held.has(w.path))
|
|
2204
|
+
continue;
|
|
2205
|
+
const why = w.merged ? "merged" : stale.has(w.path) ? "released-claim" : null;
|
|
2206
|
+
if (!why)
|
|
2207
|
+
continue;
|
|
2208
|
+
const can = canRemoveWorktree(w, null, false);
|
|
2209
|
+
out.push({
|
|
2210
|
+
path: w.path,
|
|
2211
|
+
branch: w.branch,
|
|
2212
|
+
why,
|
|
2213
|
+
removable: can.ok,
|
|
2214
|
+
blocker: can.ok ? null : can.reason
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
return out;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// packages/core/src/hygiene.ts
|
|
2221
|
+
var HYGIENE_DEFAULTS = {
|
|
2222
|
+
staleDays: 7,
|
|
2223
|
+
abandonedDays: 30,
|
|
2224
|
+
hungryRssKb: 1024 * 1024,
|
|
2225
|
+
heavyKb: 2 * 1024 * 1024
|
|
2226
|
+
};
|
|
2227
|
+
var days2 = (ms3) => ms3 / 86400000;
|
|
2228
|
+
function classifyProcess(p, opts = {}) {
|
|
2229
|
+
const o = { ...HYGIENE_DEFAULTS, ...opts };
|
|
2230
|
+
if (!p.alive)
|
|
2231
|
+
return {
|
|
2232
|
+
...p,
|
|
2233
|
+
issue: "dead",
|
|
2234
|
+
note: `pid ${p.pid} is gone but the registry still lists it${p.port ? ` holding port ${p.port}` : ""}`,
|
|
2235
|
+
reclaimable: true
|
|
2236
|
+
};
|
|
2237
|
+
if (p.sessionId && !p.sessionLive)
|
|
2238
|
+
return {
|
|
2239
|
+
...p,
|
|
2240
|
+
issue: "orphaned",
|
|
2241
|
+
note: `still running after its session ended${p.port ? ` \u2014 port ${p.port} stays taken` : ""}`,
|
|
2242
|
+
reclaimable: true
|
|
2243
|
+
};
|
|
2244
|
+
if (p.rssKb !== null && p.rssKb >= o.hungryRssKb)
|
|
2245
|
+
return {
|
|
2246
|
+
...p,
|
|
2247
|
+
issue: "hungry",
|
|
2248
|
+
note: `holding ${Math.round(p.rssKb / 1024)} MB of RSS`,
|
|
2249
|
+
reclaimable: false
|
|
2250
|
+
};
|
|
2251
|
+
return { ...p, issue: null, note: null, reclaimable: false };
|
|
2252
|
+
}
|
|
2253
|
+
function classifyWorktree(w, opts = {}) {
|
|
2254
|
+
const o = { ...HYGIENE_DEFAULTS, ...opts };
|
|
2255
|
+
const kb = w.diskKb ?? 0;
|
|
2256
|
+
const done = (issue, note, reclaimable) => ({
|
|
2257
|
+
...w,
|
|
2258
|
+
issue,
|
|
2259
|
+
note,
|
|
2260
|
+
reclaimable,
|
|
2261
|
+
reclaimableKb: reclaimable ? kb : 0
|
|
2262
|
+
});
|
|
2263
|
+
if (w.main)
|
|
2264
|
+
return done(null, null, false);
|
|
2265
|
+
const removable = canRemoveWorktree(w, w.heldByClaim, false).ok;
|
|
2266
|
+
const inUse = w.liveSessions > 0 || w.heldByClaim !== null;
|
|
2267
|
+
const unsafe = w.dirty > 0 || w.ahead > 0;
|
|
2268
|
+
const idleDays = w.idleMs === null ? null : days2(w.idleMs);
|
|
2269
|
+
if (w.merged && !inUse && removable && idleDays !== null && idleDays >= o.staleDays)
|
|
2270
|
+
return done("stale", `merged and untouched for ${Math.floor(idleDays)} days${kb ? ` \u2014 ${Math.round(kb / 1024)} MB to reclaim` : ""}`, true);
|
|
2271
|
+
if (!w.merged && !inUse && idleDays !== null && idleDays >= o.abandonedDays)
|
|
2272
|
+
return done("abandoned", unsafe ? `untouched for ${Math.floor(idleDays)} days with ${w.dirty > 0 ? "uncommitted" : "unpushed"} work \u2014 review before removing` : `untouched for ${Math.floor(idleDays)} days and never merged`, false);
|
|
2273
|
+
if (kb >= o.heavyKb)
|
|
2274
|
+
return done("heavy", `${Math.round(kb / 1024)} MB on disk`, false);
|
|
2275
|
+
return done(null, null, false);
|
|
2276
|
+
}
|
|
2277
|
+
function hygieneReport(procs, worktrees, opts = {}) {
|
|
2278
|
+
const processes = procs.map((p) => classifyProcess(p, opts));
|
|
2279
|
+
const trees = worktrees.map((w) => classifyWorktree(w, opts));
|
|
2280
|
+
const issues = processes.filter((p) => p.issue).length + trees.filter((w) => w.issue).length;
|
|
2281
|
+
const rank = (i) => i ? 0 : 1;
|
|
2282
|
+
processes.sort((a, b) => rank(a.issue) - rank(b.issue) || (b.rssKb ?? 0) - (a.rssKb ?? 0));
|
|
2283
|
+
trees.sort((a, b) => rank(a.issue) - rank(b.issue) || (b.diskKb ?? 0) - (a.diskKb ?? 0));
|
|
2284
|
+
return {
|
|
2285
|
+
processes,
|
|
2286
|
+
worktrees: trees,
|
|
2287
|
+
totals: {
|
|
2288
|
+
processes: processes.length,
|
|
2289
|
+
deadProcesses: processes.filter((p) => p.issue === "dead").length,
|
|
2290
|
+
orphanedProcesses: processes.filter((p) => p.issue === "orphaned").length,
|
|
2291
|
+
worktrees: trees.length,
|
|
2292
|
+
staleWorktrees: trees.filter((w) => w.issue === "stale").length,
|
|
2293
|
+
diskKb: trees.reduce((a, w) => a + (w.diskKb ?? 0), 0),
|
|
2294
|
+
reclaimableKb: trees.reduce((a, w) => a + w.reclaimableKb, 0),
|
|
2295
|
+
issues
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
1766
2299
|
// packages/core/src/ledger.ts
|
|
1767
2300
|
var DEFAULT_LEASE_MINUTES = 45;
|
|
1768
2301
|
function isExpired(claim, now) {
|
|
@@ -1970,6 +2503,250 @@ function incidentKey(inc) {
|
|
|
1970
2503
|
return `protected_ports:${portsIn(inc.command).join(",")}`;
|
|
1971
2504
|
return inc.rule;
|
|
1972
2505
|
}
|
|
2506
|
+
// packages/core/src/lineage.ts
|
|
2507
|
+
var LINEAGE_MAX_NODES = 40;
|
|
2508
|
+
var LINEAGE_FANOUT = 4;
|
|
2509
|
+
var groupId = (parent, kind) => `group:${parent}:${kind}`;
|
|
2510
|
+
function collapseFanout(sessions, edges, fanout, expanded) {
|
|
2511
|
+
const fans = new Map;
|
|
2512
|
+
for (const e of edges) {
|
|
2513
|
+
const k = groupId(e.from, e.kind);
|
|
2514
|
+
const list = fans.get(k) ?? [];
|
|
2515
|
+
list.push(e);
|
|
2516
|
+
fans.set(k, list);
|
|
2517
|
+
}
|
|
2518
|
+
const collapse = [...fans.entries()].filter(([id, list]) => list.length > fanout && !expanded.has(id));
|
|
2519
|
+
if (!collapse.length)
|
|
2520
|
+
return { sessions, edges, sizes: new Map };
|
|
2521
|
+
const hidden = new Set;
|
|
2522
|
+
const dropped = new Set;
|
|
2523
|
+
const extraSessions = [];
|
|
2524
|
+
const extraEdges = [];
|
|
2525
|
+
const sizes = new Map;
|
|
2526
|
+
for (const [id, list] of collapse) {
|
|
2527
|
+
const first = list[0];
|
|
2528
|
+
const parent = sessions.find((x) => x.id === first.from);
|
|
2529
|
+
sizes.set(id, list.length);
|
|
2530
|
+
for (const e of list) {
|
|
2531
|
+
dropped.add(e);
|
|
2532
|
+
hidden.add(e.to);
|
|
2533
|
+
}
|
|
2534
|
+
extraSessions.push({
|
|
2535
|
+
id,
|
|
2536
|
+
projectId: parent?.projectId ?? null,
|
|
2537
|
+
title: `${list.length} ${first.kind}${list.length === 1 ? "" : "s"}`,
|
|
2538
|
+
agent: parent?.agent ?? null,
|
|
2539
|
+
kind: "group",
|
|
2540
|
+
state: "ended",
|
|
2541
|
+
startedAt: first.at,
|
|
2542
|
+
endedAt: null,
|
|
2543
|
+
costUsd: null,
|
|
2544
|
+
outcome: null
|
|
2545
|
+
});
|
|
2546
|
+
extraEdges.push({ from: first.from, to: id, kind: first.kind, at: first.at });
|
|
2547
|
+
}
|
|
2548
|
+
const stillLinked = new Set;
|
|
2549
|
+
for (const e of edges) {
|
|
2550
|
+
if (dropped.has(e))
|
|
2551
|
+
continue;
|
|
2552
|
+
stillLinked.add(e.from);
|
|
2553
|
+
stillLinked.add(e.to);
|
|
2554
|
+
}
|
|
2555
|
+
const keptSessions = sessions.filter((x) => !hidden.has(x.id) || stillLinked.has(x.id));
|
|
2556
|
+
const keptEdges = edges.filter((e) => !dropped.has(e));
|
|
2557
|
+
return {
|
|
2558
|
+
sessions: [...keptSessions, ...extraSessions],
|
|
2559
|
+
edges: [...keptEdges, ...extraEdges],
|
|
2560
|
+
sizes
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
function handoffEdges(holds) {
|
|
2564
|
+
const byTask = new Map;
|
|
2565
|
+
for (const h of holds) {
|
|
2566
|
+
if (!h.sessionId)
|
|
2567
|
+
continue;
|
|
2568
|
+
const key = `${h.projectId ?? ""}\x00${h.task}`;
|
|
2569
|
+
const list = byTask.get(key) ?? [];
|
|
2570
|
+
list.push({ sessionId: h.sessionId, at: h.at, task: h.task });
|
|
2571
|
+
byTask.set(key, list);
|
|
2572
|
+
}
|
|
2573
|
+
const out = [];
|
|
2574
|
+
for (const list of byTask.values()) {
|
|
2575
|
+
list.sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime());
|
|
2576
|
+
for (let i = 1;i < list.length; i++) {
|
|
2577
|
+
const prev = list[i - 1];
|
|
2578
|
+
const cur = list[i];
|
|
2579
|
+
if (prev.sessionId === cur.sessionId)
|
|
2580
|
+
continue;
|
|
2581
|
+
out.push({
|
|
2582
|
+
from: prev.sessionId,
|
|
2583
|
+
to: cur.sessionId,
|
|
2584
|
+
kind: "handoff",
|
|
2585
|
+
at: cur.at,
|
|
2586
|
+
label: cur.task
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
return out;
|
|
2591
|
+
}
|
|
2592
|
+
function lineageGraph(sessions, edges, opts = {}) {
|
|
2593
|
+
const maxNodes = opts.maxNodes ?? LINEAGE_MAX_NODES;
|
|
2594
|
+
const known = new Map(sessions.map((s) => [s.id, s]));
|
|
2595
|
+
const seen = new Set;
|
|
2596
|
+
const usable = [];
|
|
2597
|
+
for (const e of edges) {
|
|
2598
|
+
if (!known.has(e.from) || !known.has(e.to) || e.from === e.to)
|
|
2599
|
+
continue;
|
|
2600
|
+
const key = JSON.stringify([e.from, e.to, e.kind]);
|
|
2601
|
+
if (seen.has(key))
|
|
2602
|
+
continue;
|
|
2603
|
+
seen.add(key);
|
|
2604
|
+
usable.push(e);
|
|
2605
|
+
}
|
|
2606
|
+
const collapsed = collapseFanout(sessions, usable, opts.fanout ?? LINEAGE_FANOUT, new Set(opts.expanded ?? []));
|
|
2607
|
+
const shownSessions = collapsed.sessions;
|
|
2608
|
+
const shownEdges = collapsed.edges;
|
|
2609
|
+
const groupSizes = collapsed.sizes;
|
|
2610
|
+
const byId = new Map(shownSessions.map((x) => [x.id, x]));
|
|
2611
|
+
const involved = new Set;
|
|
2612
|
+
for (const e of shownEdges) {
|
|
2613
|
+
involved.add(e.from);
|
|
2614
|
+
involved.add(e.to);
|
|
2615
|
+
}
|
|
2616
|
+
const all = shownSessions.filter((s) => involved.has(s.id));
|
|
2617
|
+
const deg0 = new Map;
|
|
2618
|
+
for (const e of shownEdges) {
|
|
2619
|
+
deg0.set(e.from, (deg0.get(e.from) ?? 0) + 1);
|
|
2620
|
+
deg0.set(e.to, (deg0.get(e.to) ?? 0) + 1);
|
|
2621
|
+
}
|
|
2622
|
+
const nodes = all.length <= maxNodes ? all : [...all].sort((a, b) => (deg0.get(b.id) ?? 0) - (deg0.get(a.id) ?? 0) || a.id.localeCompare(b.id)).slice(0, maxNodes);
|
|
2623
|
+
const truncated = all.length - nodes.length;
|
|
2624
|
+
if (!nodes.length)
|
|
2625
|
+
return {
|
|
2626
|
+
nodes: [],
|
|
2627
|
+
edges: [],
|
|
2628
|
+
width: 0,
|
|
2629
|
+
height: 0,
|
|
2630
|
+
roots: 0,
|
|
2631
|
+
byKind: { subagent: 0, dispatch: 0, message: 0, handoff: 0 },
|
|
2632
|
+
truncated: 0
|
|
2633
|
+
};
|
|
2634
|
+
const kept = new Set(nodes.map((s) => s.id));
|
|
2635
|
+
const shown = shownEdges.filter((e) => kept.has(e.from) && kept.has(e.to));
|
|
2636
|
+
const layout = layoutDag(nodes.map((s) => ({ id: s.id })), shown.map((e) => ({ from: e.from, to: e.to })), opts);
|
|
2637
|
+
const degree = new Map;
|
|
2638
|
+
for (const e of shown) {
|
|
2639
|
+
degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
|
|
2640
|
+
degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
|
|
2641
|
+
}
|
|
2642
|
+
const backSet = new Set(layout.edges.filter((e) => e.back).map((e) => JSON.stringify([e.from, e.to])));
|
|
2643
|
+
const byKind = {
|
|
2644
|
+
subagent: 0,
|
|
2645
|
+
dispatch: 0,
|
|
2646
|
+
message: 0,
|
|
2647
|
+
handoff: 0
|
|
2648
|
+
};
|
|
2649
|
+
for (const e of shown)
|
|
2650
|
+
byKind[e.kind]++;
|
|
2651
|
+
return {
|
|
2652
|
+
nodes: layout.nodes.map((p) => {
|
|
2653
|
+
const s = byId.get(p.id);
|
|
2654
|
+
const size = groupSizes.get(p.id) ?? null;
|
|
2655
|
+
const parts = size ? p.id.split(":") : null;
|
|
2656
|
+
return {
|
|
2657
|
+
...s,
|
|
2658
|
+
layer: p.layer,
|
|
2659
|
+
order: p.order,
|
|
2660
|
+
x: p.x,
|
|
2661
|
+
y: p.y,
|
|
2662
|
+
degree: degree.get(p.id) ?? 0,
|
|
2663
|
+
groupSize: size,
|
|
2664
|
+
groupOf: parts ? parts.slice(1, -1).join(":") ?? null : null,
|
|
2665
|
+
groupKind: parts ? parts.at(-1) ?? null : null
|
|
2666
|
+
};
|
|
2667
|
+
}),
|
|
2668
|
+
edges: shown.map((e) => ({ ...e, back: backSet.has(JSON.stringify([e.from, e.to])) })),
|
|
2669
|
+
width: layout.width,
|
|
2670
|
+
height: layout.height,
|
|
2671
|
+
roots: layout.nodes.filter((p) => p.layer === 0).length,
|
|
2672
|
+
byKind,
|
|
2673
|
+
truncated
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
// packages/core/src/mcphealth.ts
|
|
2677
|
+
var BUILTIN = "builtin";
|
|
2678
|
+
var MCP_HEALTH_DEFAULTS = { toolLimit: 6 };
|
|
2679
|
+
function serverOf(tool) {
|
|
2680
|
+
const m = /^mcp__([^_]+(?:_[^_]+)*?)__/.exec(tool);
|
|
2681
|
+
return m?.[1] ?? BUILTIN;
|
|
2682
|
+
}
|
|
2683
|
+
function toolOf(tool) {
|
|
2684
|
+
return tool.replace(/^mcp__[^_]+(?:_[^_]+)*?__/, "");
|
|
2685
|
+
}
|
|
2686
|
+
var stat = (tool, calls) => {
|
|
2687
|
+
const timed = calls.map((c) => c.ms).filter((m) => typeof m === "number");
|
|
2688
|
+
return {
|
|
2689
|
+
tool,
|
|
2690
|
+
calls: calls.length,
|
|
2691
|
+
errors: calls.filter((c) => c.errored).length,
|
|
2692
|
+
p50Ms: percentile(timed, 50),
|
|
2693
|
+
p95Ms: percentile(timed, 95),
|
|
2694
|
+
maxMs: timed.length ? Math.max(...timed) : null,
|
|
2695
|
+
totalMs: timed.reduce((a, b) => a + b, 0)
|
|
2696
|
+
};
|
|
2697
|
+
};
|
|
2698
|
+
function mcpHealth(calls, opts = {}) {
|
|
2699
|
+
const o = { ...MCP_HEALTH_DEFAULTS, ...opts };
|
|
2700
|
+
const byServer = new Map;
|
|
2701
|
+
for (const c of calls) {
|
|
2702
|
+
const s = serverOf(c.tool);
|
|
2703
|
+
const list = byServer.get(s);
|
|
2704
|
+
if (list)
|
|
2705
|
+
list.push(c);
|
|
2706
|
+
else
|
|
2707
|
+
byServer.set(s, [c]);
|
|
2708
|
+
}
|
|
2709
|
+
const servers = [];
|
|
2710
|
+
for (const [server, list] of byServer) {
|
|
2711
|
+
const byTool = new Map;
|
|
2712
|
+
for (const c of list) {
|
|
2713
|
+
const t = toolOf(c.tool);
|
|
2714
|
+
const tl = byTool.get(t);
|
|
2715
|
+
if (tl)
|
|
2716
|
+
tl.push(c);
|
|
2717
|
+
else
|
|
2718
|
+
byTool.set(t, [c]);
|
|
2719
|
+
}
|
|
2720
|
+
const tools = [...byTool.entries()].map(([t, cs]) => stat(t, cs)).sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool)).slice(0, o.toolLimit);
|
|
2721
|
+
const all = stat(server, list);
|
|
2722
|
+
servers.push({
|
|
2723
|
+
server,
|
|
2724
|
+
mcp: server !== BUILTIN,
|
|
2725
|
+
calls: all.calls,
|
|
2726
|
+
errors: all.errors,
|
|
2727
|
+
errorRate: all.calls ? all.errors / all.calls : 0,
|
|
2728
|
+
unanswered: list.filter((c) => c.ms === null).length,
|
|
2729
|
+
sessions: new Set(list.map((c) => c.sessionId)).size,
|
|
2730
|
+
p50Ms: all.p50Ms,
|
|
2731
|
+
p95Ms: all.p95Ms,
|
|
2732
|
+
maxMs: all.maxMs,
|
|
2733
|
+
totalMs: all.totalMs,
|
|
2734
|
+
tools,
|
|
2735
|
+
lastAt: list.map((c) => c.at).sort().at(-1) ?? null
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
servers.sort((a, b) => Number(b.mcp) - Number(a.mcp) || b.totalMs - a.totalMs || b.calls - a.calls);
|
|
2739
|
+
return {
|
|
2740
|
+
servers,
|
|
2741
|
+
totals: {
|
|
2742
|
+
servers: servers.filter((s) => s.mcp).length,
|
|
2743
|
+
calls: calls.length,
|
|
2744
|
+
errors: servers.reduce((a, s) => a + s.errors, 0),
|
|
2745
|
+
totalMs: servers.reduce((a, s) => a + s.totalMs, 0),
|
|
2746
|
+
mcpMs: servers.filter((s) => s.mcp).reduce((a, s) => a + s.totalMs, 0)
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
1973
2750
|
// packages/core/src/memory.ts
|
|
1974
2751
|
var MEMORY_KINDS = ["handoff", "incident", "gate", "session"];
|
|
1975
2752
|
function handoffDoc(projectId, id, h, sessionId) {
|
|
@@ -2087,11 +2864,11 @@ function parseTo(to) {
|
|
|
2087
2864
|
return { kind: "session", id: t };
|
|
2088
2865
|
return { kind: "task", task: t };
|
|
2089
2866
|
}
|
|
2090
|
-
function formatMessages(
|
|
2091
|
-
if (!
|
|
2867
|
+
function formatMessages(ms3) {
|
|
2868
|
+
if (!ms3.length)
|
|
2092
2869
|
return null;
|
|
2093
|
-
const lines =
|
|
2094
|
-
return `[swarm] While you were working, message${
|
|
2870
|
+
const lines = ms3.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
|
|
2871
|
+
return `[swarm] While you were working, message${ms3.length === 1 ? "" : "s"} arrived:
|
|
2095
2872
|
${lines.join(`
|
|
2096
2873
|
`)}
|
|
2097
2874
|
Reply with swarm_send if a reply is expected.`;
|
|
@@ -2366,6 +3143,147 @@ function projectIdentity(opts) {
|
|
|
2366
3143
|
const name = parts[parts.length - 1] ?? opts.root;
|
|
2367
3144
|
return { id: `p_${fnv1a(key)}`, root: opts.root, commonDir: opts.commonDir, name };
|
|
2368
3145
|
}
|
|
3146
|
+
// packages/core/src/provenance.ts
|
|
3147
|
+
var SEVERITY = {
|
|
3148
|
+
"no-task": 0,
|
|
3149
|
+
"open-pr": 1,
|
|
3150
|
+
"no-pr": 2,
|
|
3151
|
+
"no-branch": 3,
|
|
3152
|
+
"no-session": 4,
|
|
3153
|
+
unclaimed: 5,
|
|
3154
|
+
complete: 6
|
|
3155
|
+
};
|
|
3156
|
+
var severity = (b) => SEVERITY[b ?? "complete"];
|
|
3157
|
+
function breakOf(l) {
|
|
3158
|
+
if (!l.task)
|
|
3159
|
+
return "no-task";
|
|
3160
|
+
if (!l.claim)
|
|
3161
|
+
return "unclaimed";
|
|
3162
|
+
if (!l.session)
|
|
3163
|
+
return "no-session";
|
|
3164
|
+
if (!l.branch)
|
|
3165
|
+
return "no-branch";
|
|
3166
|
+
if (!l.pr)
|
|
3167
|
+
return "no-pr";
|
|
3168
|
+
if (!l.merged)
|
|
3169
|
+
return "open-pr";
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
function provenance(tasks, claims, sessions, branches) {
|
|
3173
|
+
const claimsByTask = new Map;
|
|
3174
|
+
for (const c of claims) {
|
|
3175
|
+
const list = claimsByTask.get(c.task);
|
|
3176
|
+
if (list)
|
|
3177
|
+
list.push(c);
|
|
3178
|
+
else
|
|
3179
|
+
claimsByTask.set(c.task, [c]);
|
|
3180
|
+
}
|
|
3181
|
+
const branchRow = new Map(branches.map((b) => [b.branch, b]));
|
|
3182
|
+
const sessionsByBranch = new Map;
|
|
3183
|
+
for (const s of sessions) {
|
|
3184
|
+
if (!s.branch)
|
|
3185
|
+
continue;
|
|
3186
|
+
const list = sessionsByBranch.get(s.branch);
|
|
3187
|
+
if (list)
|
|
3188
|
+
list.push(s);
|
|
3189
|
+
else
|
|
3190
|
+
sessionsByBranch.set(s.branch, [s]);
|
|
3191
|
+
}
|
|
3192
|
+
const chains = [];
|
|
3193
|
+
for (const t of tasks) {
|
|
3194
|
+
const cs = (claimsByTask.get(t.id) ?? []).sort((a, b) => new Date(a.acquiredAt).getTime() - new Date(b.acquiredAt).getTime());
|
|
3195
|
+
const branch = cs.map((c) => c.branch).find((b) => !!b) ?? null;
|
|
3196
|
+
const worktree = cs.map((c) => c.worktree).find((w) => !!w) ?? null;
|
|
3197
|
+
const row = branch ? branchRow.get(branch) : undefined;
|
|
3198
|
+
const byId = new Map;
|
|
3199
|
+
for (const s of branch ? sessionsByBranch.get(branch) ?? [] : [])
|
|
3200
|
+
byId.set(s.id, s);
|
|
3201
|
+
for (const c of cs) {
|
|
3202
|
+
if (!c.sessionId || byId.has(c.sessionId))
|
|
3203
|
+
continue;
|
|
3204
|
+
const hit = sessions.find((s) => s.id === c.sessionId);
|
|
3205
|
+
if (hit)
|
|
3206
|
+
byId.set(hit.id, hit);
|
|
3207
|
+
}
|
|
3208
|
+
const sess = [...byId.values()];
|
|
3209
|
+
const links = {
|
|
3210
|
+
task: true,
|
|
3211
|
+
claim: cs.length > 0,
|
|
3212
|
+
session: sess.length > 0,
|
|
3213
|
+
branch: !!branch,
|
|
3214
|
+
pr: !!row?.prNumber,
|
|
3215
|
+
merged: row?.outcome === "merged"
|
|
3216
|
+
};
|
|
3217
|
+
chains.push({
|
|
3218
|
+
task: t.id,
|
|
3219
|
+
fromTask: true,
|
|
3220
|
+
title: t.title,
|
|
3221
|
+
status: t.status,
|
|
3222
|
+
url: t.url ?? null,
|
|
3223
|
+
holders: [...new Set(cs.map((c) => c.owner).filter((o) => !!o))],
|
|
3224
|
+
claimedAt: cs[0]?.acquiredAt ?? null,
|
|
3225
|
+
worktree,
|
|
3226
|
+
branch,
|
|
3227
|
+
sessions: sess,
|
|
3228
|
+
costUsd: row?.costUsd ?? sess.reduce((a, s) => a + (s.costUsd ?? 0), 0),
|
|
3229
|
+
prNumber: row?.prNumber ?? null,
|
|
3230
|
+
prUrl: row?.url ?? null,
|
|
3231
|
+
outcome: row?.outcome ?? null,
|
|
3232
|
+
mergedAt: row?.mergedAt ?? null,
|
|
3233
|
+
leadHours: row?.leadHours ?? null,
|
|
3234
|
+
links,
|
|
3235
|
+
depth: Object.values(links).filter(Boolean).length,
|
|
3236
|
+
brokenAt: breakOf(links)
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
const explained = new Set(chains.map((c) => c.branch).filter((b) => !!b));
|
|
3240
|
+
for (const b of branches) {
|
|
3241
|
+
if (explained.has(b.branch))
|
|
3242
|
+
continue;
|
|
3243
|
+
const links = {
|
|
3244
|
+
task: false,
|
|
3245
|
+
claim: false,
|
|
3246
|
+
session: b.sessions.length > 0,
|
|
3247
|
+
branch: true,
|
|
3248
|
+
pr: !!b.prNumber,
|
|
3249
|
+
merged: b.outcome === "merged"
|
|
3250
|
+
};
|
|
3251
|
+
chains.push({
|
|
3252
|
+
task: b.branch,
|
|
3253
|
+
fromTask: false,
|
|
3254
|
+
title: b.title ?? b.branch,
|
|
3255
|
+
status: b.outcome,
|
|
3256
|
+
url: null,
|
|
3257
|
+
holders: [],
|
|
3258
|
+
claimedAt: null,
|
|
3259
|
+
worktree: null,
|
|
3260
|
+
branch: b.branch,
|
|
3261
|
+
sessions: b.sessions.map((id) => sessions.find((s) => s.id === id)).filter((s) => !!s),
|
|
3262
|
+
costUsd: b.costUsd,
|
|
3263
|
+
prNumber: b.prNumber,
|
|
3264
|
+
prUrl: b.url,
|
|
3265
|
+
outcome: b.outcome,
|
|
3266
|
+
mergedAt: b.mergedAt,
|
|
3267
|
+
leadHours: b.leadHours,
|
|
3268
|
+
links,
|
|
3269
|
+
depth: Object.values(links).filter(Boolean).length,
|
|
3270
|
+
brokenAt: "no-task"
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
chains.sort((a, b) => severity(a.brokenAt) - severity(b.brokenAt) || b.costUsd - a.costUsd || a.task.localeCompare(b.task));
|
|
3274
|
+
return {
|
|
3275
|
+
chains,
|
|
3276
|
+
totals: {
|
|
3277
|
+
tasks: chains.length,
|
|
3278
|
+
complete: chains.filter((c) => c.brokenAt === null).length,
|
|
3279
|
+
broken: chains.filter((c) => c.brokenAt !== null).length,
|
|
3280
|
+
unclaimed: chains.filter((c) => c.brokenAt === "unclaimed").length,
|
|
3281
|
+
noPr: chains.filter((c) => c.brokenAt === "no-pr").length,
|
|
3282
|
+
untracked: chains.filter((c) => c.brokenAt === "no-task").length,
|
|
3283
|
+
costUsd: chains.reduce((a, c) => a + c.costUsd, 0)
|
|
3284
|
+
}
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
2369
3287
|
// packages/core/src/questions.ts
|
|
2370
3288
|
function validateQuestion(text, options) {
|
|
2371
3289
|
const t = typeof text === "string" ? text.trim() : "";
|
|
@@ -2753,88 +3671,128 @@ function clusterProjectKey(remoteUrl) {
|
|
|
2753
3671
|
return null;
|
|
2754
3672
|
return `${host}/${m[2]}`;
|
|
2755
3673
|
}
|
|
2756
|
-
// packages/core/src/
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
}
|
|
2770
|
-
return { copies, setup: cfg.worktree.setup };
|
|
2771
|
-
}
|
|
2772
|
-
var needsBootstrap = (plan) => plan.copies.length > 0 || plan.setup !== null;
|
|
2773
|
-
function summarizeBootstrap(o) {
|
|
2774
|
-
const parts = [];
|
|
2775
|
-
if (o.copied.length)
|
|
2776
|
-
parts.push(`copied ${o.copied.join(", ")}`);
|
|
2777
|
-
if (o.skipped.length)
|
|
2778
|
-
parts.push(`skipped ${o.skipped.join(", ")} (missing)`);
|
|
2779
|
-
if (o.setup)
|
|
2780
|
-
parts.push(`${o.setup.command} \u2192 ${o.setup.exitCode === 0 ? "ok" : `exit ${o.setup.exitCode}`} in ${(o.setup.durationMs / 1000).toFixed(1)}s`);
|
|
2781
|
-
return parts.join("; ") || "nothing to do";
|
|
2782
|
-
}
|
|
2783
|
-
function canRemoveWorktree(w, heldByClaim, force) {
|
|
2784
|
-
if (w.main)
|
|
2785
|
-
return { ok: false, reason: "main" };
|
|
2786
|
-
if (heldByClaim)
|
|
2787
|
-
return { ok: false, reason: "held" };
|
|
2788
|
-
if (force)
|
|
2789
|
-
return { ok: true };
|
|
2790
|
-
if (w.dirty > 0)
|
|
2791
|
-
return { ok: false, reason: "dirty" };
|
|
2792
|
-
if (w.ahead > 0)
|
|
2793
|
-
return { ok: false, reason: "unpushed" };
|
|
2794
|
-
return { ok: true };
|
|
2795
|
-
}
|
|
2796
|
-
function removeRefusalMessage(reason, path, task) {
|
|
2797
|
-
switch (reason) {
|
|
2798
|
-
case "main":
|
|
2799
|
-
return `${path} is the main checkout \u2014 it is never removed`;
|
|
2800
|
-
case "held":
|
|
2801
|
-
return `${path} is held by claim ${task ?? "?"} \u2014 release the claim instead`;
|
|
2802
|
-
case "dirty":
|
|
2803
|
-
return `${path} has uncommitted changes \u2014 commit or stash them, or --force to discard`;
|
|
2804
|
-
case "unpushed":
|
|
2805
|
-
return `${path} has unpushed commits \u2014 push them, or --force to discard`;
|
|
2806
|
-
}
|
|
3674
|
+
// packages/core/src/waiting.ts
|
|
3675
|
+
var emptyByKind = () => ({
|
|
3676
|
+
permission: { episodes: 0, blockedMs: 0 },
|
|
3677
|
+
question: { episodes: 0, blockedMs: 0 },
|
|
3678
|
+
notification: { episodes: 0, blockedMs: 0 }
|
|
3679
|
+
});
|
|
3680
|
+
var ms3 = (iso) => new Date(iso).getTime();
|
|
3681
|
+
function median2(xs) {
|
|
3682
|
+
if (!xs.length)
|
|
3683
|
+
return 0;
|
|
3684
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
3685
|
+
const mid = s.length >> 1;
|
|
3686
|
+
return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
|
|
2807
3687
|
}
|
|
2808
|
-
function
|
|
2809
|
-
const
|
|
2810
|
-
const
|
|
3688
|
+
function pairWaits(samples, now, sessionEnds = {}) {
|
|
3689
|
+
const nowMs = ms3(now);
|
|
3690
|
+
const open = new Map;
|
|
2811
3691
|
const out = [];
|
|
2812
|
-
|
|
2813
|
-
|
|
3692
|
+
const id = (s) => `${s.sessionId}\x00${s.kind}\x00${s.key}`;
|
|
3693
|
+
const rank = (p) => p === "start" ? 0 : 1;
|
|
3694
|
+
const ordered = [...samples].sort((a, b) => ms3(a.ts) - ms3(b.ts) || rank(a.phase) - rank(b.phase));
|
|
3695
|
+
for (const s of ordered) {
|
|
3696
|
+
const k = id(s);
|
|
3697
|
+
if (s.phase === "start") {
|
|
3698
|
+
if (!open.has(k))
|
|
3699
|
+
open.set(k, s);
|
|
2814
3700
|
continue;
|
|
2815
|
-
|
|
2816
|
-
|
|
3701
|
+
}
|
|
3702
|
+
const start = open.get(k);
|
|
3703
|
+
if (!start)
|
|
2817
3704
|
continue;
|
|
2818
|
-
|
|
3705
|
+
open.delete(k);
|
|
2819
3706
|
out.push({
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3707
|
+
sessionId: start.sessionId,
|
|
3708
|
+
projectId: start.projectId ?? null,
|
|
3709
|
+
kind: start.kind,
|
|
3710
|
+
key: start.key,
|
|
3711
|
+
startedAt: start.ts,
|
|
3712
|
+
endedAt: s.ts,
|
|
3713
|
+
ms: Math.max(0, ms3(s.ts) - ms3(start.ts)),
|
|
3714
|
+
open: false,
|
|
3715
|
+
label: start.label ?? s.label ?? null
|
|
2825
3716
|
});
|
|
2826
3717
|
}
|
|
2827
|
-
|
|
3718
|
+
for (const start of open.values()) {
|
|
3719
|
+
const ended = sessionEnds[start.sessionId];
|
|
3720
|
+
const until = ended ? Math.min(ms3(ended), nowMs) : nowMs;
|
|
3721
|
+
out.push({
|
|
3722
|
+
sessionId: start.sessionId,
|
|
3723
|
+
projectId: start.projectId ?? null,
|
|
3724
|
+
kind: start.kind,
|
|
3725
|
+
key: start.key,
|
|
3726
|
+
startedAt: start.ts,
|
|
3727
|
+
endedAt: ended ?? null,
|
|
3728
|
+
ms: Math.max(0, until - ms3(start.ts)),
|
|
3729
|
+
open: !ended,
|
|
3730
|
+
label: start.label ?? null
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
return out.sort((a, b) => ms3(a.startedAt) - ms3(b.startedAt));
|
|
3734
|
+
}
|
|
3735
|
+
function waitingReport(episodes) {
|
|
3736
|
+
const bySession = new Map;
|
|
3737
|
+
for (const e of episodes) {
|
|
3738
|
+
const list = bySession.get(e.sessionId);
|
|
3739
|
+
if (list)
|
|
3740
|
+
list.push(e);
|
|
3741
|
+
else
|
|
3742
|
+
bySession.set(e.sessionId, [e]);
|
|
3743
|
+
}
|
|
3744
|
+
const sessions = [];
|
|
3745
|
+
for (const [sessionId, list] of bySession) {
|
|
3746
|
+
const byKind = emptyByKind();
|
|
3747
|
+
for (const e of list) {
|
|
3748
|
+
const k = byKind[e.kind];
|
|
3749
|
+
k.episodes++;
|
|
3750
|
+
k.blockedMs += e.ms;
|
|
3751
|
+
}
|
|
3752
|
+
const openEp = list.filter((e) => e.open).sort((a, b) => ms3(a.startedAt) - ms3(b.startedAt))[0];
|
|
3753
|
+
const durations = list.map((e) => e.ms);
|
|
3754
|
+
sessions.push({
|
|
3755
|
+
sessionId,
|
|
3756
|
+
projectId: list[0]?.projectId ?? null,
|
|
3757
|
+
episodes: list.length,
|
|
3758
|
+
blockedMs: durations.reduce((a, b) => a + b, 0),
|
|
3759
|
+
longestMs: durations.length ? Math.max(...durations) : 0,
|
|
3760
|
+
medianMs: median2(durations),
|
|
3761
|
+
openSince: openEp?.startedAt ?? null,
|
|
3762
|
+
openKind: openEp?.kind ?? null,
|
|
3763
|
+
openLabel: openEp?.label ?? null,
|
|
3764
|
+
byKind
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
sessions.sort((a, b) => b.blockedMs - a.blockedMs);
|
|
3768
|
+
const all = episodes.map((e) => e.ms);
|
|
3769
|
+
const totalsByKind = emptyByKind();
|
|
3770
|
+
for (const e of episodes) {
|
|
3771
|
+
const k = totalsByKind[e.kind];
|
|
3772
|
+
k.episodes++;
|
|
3773
|
+
k.blockedMs += e.ms;
|
|
3774
|
+
}
|
|
3775
|
+
return {
|
|
3776
|
+
sessions,
|
|
3777
|
+
totals: {
|
|
3778
|
+
episodes: episodes.length,
|
|
3779
|
+
blockedMs: all.reduce((a, b) => a + b, 0),
|
|
3780
|
+
medianMs: median2(all),
|
|
3781
|
+
longestMs: all.length ? Math.max(...all) : 0,
|
|
3782
|
+
waitingNow: sessions.filter((s) => s.openSince).length,
|
|
3783
|
+
byKind: totalsByKind
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
2828
3786
|
}
|
|
2829
3787
|
// packages/daemon/src/app.ts
|
|
2830
|
-
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
3788
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
|
|
2831
3789
|
import { homedir as homedir4 } from "os";
|
|
2832
3790
|
import { dirname as dirname4, join as join10 } from "path";
|
|
2833
3791
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2834
3792
|
|
|
2835
3793
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
2836
3794
|
var compose = (middleware, onError, onNotFound) => {
|
|
2837
|
-
return (
|
|
3795
|
+
return (context2, next) => {
|
|
2838
3796
|
let index = -1;
|
|
2839
3797
|
return dispatch2(0);
|
|
2840
3798
|
async function dispatch2(i) {
|
|
@@ -2847,31 +3805,31 @@ var compose = (middleware, onError, onNotFound) => {
|
|
|
2847
3805
|
let handler;
|
|
2848
3806
|
if (middleware[i]) {
|
|
2849
3807
|
handler = middleware[i][0][0];
|
|
2850
|
-
|
|
3808
|
+
context2.req.routeIndex = i;
|
|
2851
3809
|
} else {
|
|
2852
3810
|
handler = i === middleware.length && next || undefined;
|
|
2853
3811
|
}
|
|
2854
3812
|
if (handler) {
|
|
2855
3813
|
try {
|
|
2856
|
-
res = await handler(
|
|
3814
|
+
res = await handler(context2, () => dispatch2(i + 1));
|
|
2857
3815
|
} catch (err) {
|
|
2858
3816
|
if (err instanceof Error && onError) {
|
|
2859
|
-
|
|
2860
|
-
res = await onError(err,
|
|
3817
|
+
context2.error = err;
|
|
3818
|
+
res = await onError(err, context2);
|
|
2861
3819
|
isError = true;
|
|
2862
3820
|
} else {
|
|
2863
3821
|
throw err;
|
|
2864
3822
|
}
|
|
2865
3823
|
}
|
|
2866
3824
|
} else {
|
|
2867
|
-
if (
|
|
2868
|
-
res = await onNotFound(
|
|
3825
|
+
if (context2.finalized === false && onNotFound) {
|
|
3826
|
+
res = await onNotFound(context2);
|
|
2869
3827
|
}
|
|
2870
3828
|
}
|
|
2871
|
-
if (res && (
|
|
2872
|
-
|
|
3829
|
+
if (res && (context2.finalized === false || isError)) {
|
|
3830
|
+
context2.res = res;
|
|
2873
3831
|
}
|
|
2874
|
-
return
|
|
3832
|
+
return context2;
|
|
2875
3833
|
}
|
|
2876
3834
|
};
|
|
2877
3835
|
};
|
|
@@ -3292,7 +4250,7 @@ var raw = (value, callbacks) => {
|
|
|
3292
4250
|
escapedString.callbacks = callbacks;
|
|
3293
4251
|
return escapedString;
|
|
3294
4252
|
};
|
|
3295
|
-
var resolveCallback = async (str, phase, preserveCallbacks,
|
|
4253
|
+
var resolveCallback = async (str, phase, preserveCallbacks, context2, buffer) => {
|
|
3296
4254
|
if (typeof str === "object" && !(str instanceof String)) {
|
|
3297
4255
|
if (!(str instanceof Promise)) {
|
|
3298
4256
|
str = str.toString();
|
|
@@ -3310,7 +4268,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
|
|
|
3310
4268
|
} else {
|
|
3311
4269
|
buffer = [str];
|
|
3312
4270
|
}
|
|
3313
|
-
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false,
|
|
4271
|
+
const resStr = Promise.all(callbacks.map((c) => c({ phase, buffer, context: context2 }))).then((res) => Promise.all(res.filter(Boolean).map((str2) => resolveCallback(str2, phase, false, context2, buffer))).then(() => buffer[0]));
|
|
3314
4272
|
if (preserveCallbacks) {
|
|
3315
4273
|
return raw(await resStr, callbacks);
|
|
3316
4274
|
} else {
|
|
@@ -3709,11 +4667,11 @@ var Hono = class _Hono {
|
|
|
3709
4667
|
const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
|
|
3710
4668
|
return (async () => {
|
|
3711
4669
|
try {
|
|
3712
|
-
const
|
|
3713
|
-
if (!
|
|
4670
|
+
const context2 = await composed(c);
|
|
4671
|
+
if (!context2.finalized) {
|
|
3714
4672
|
throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
|
|
3715
4673
|
}
|
|
3716
|
-
return
|
|
4674
|
+
return context2.res;
|
|
3717
4675
|
} catch (err) {
|
|
3718
4676
|
return this.#handleError(err, c);
|
|
3719
4677
|
}
|
|
@@ -3786,7 +4744,7 @@ var Node = class _Node {
|
|
|
3786
4744
|
#index;
|
|
3787
4745
|
#varIndex;
|
|
3788
4746
|
#children = /* @__PURE__ */ Object.create(null);
|
|
3789
|
-
insert(tokens, index, paramMap,
|
|
4747
|
+
insert(tokens, index, paramMap, context2, isStatic) {
|
|
3790
4748
|
let node = this;
|
|
3791
4749
|
for (let i = 0, len = tokens.length;i < len; i++) {
|
|
3792
4750
|
const token = tokens[i];
|
|
@@ -3819,7 +4777,7 @@ var Node = class _Node {
|
|
|
3819
4777
|
nextNode = node.#children[regexpStr] = new _Node;
|
|
3820
4778
|
}
|
|
3821
4779
|
if (name !== "") {
|
|
3822
|
-
nextNode.#varIndex ??=
|
|
4780
|
+
nextNode.#varIndex ??= context2.varIndex++;
|
|
3823
4781
|
paramMap.push([name, nextNode.#varIndex]);
|
|
3824
4782
|
}
|
|
3825
4783
|
} else {
|
|
@@ -4344,8 +5302,8 @@ var StreamingApi = class {
|
|
|
4344
5302
|
`);
|
|
4345
5303
|
return this;
|
|
4346
5304
|
}
|
|
4347
|
-
sleep(
|
|
4348
|
-
return new Promise((res) => setTimeout(res,
|
|
5305
|
+
sleep(ms4) {
|
|
5306
|
+
return new Promise((res) => setTimeout(res, ms4));
|
|
4349
5307
|
}
|
|
4350
5308
|
async close() {
|
|
4351
5309
|
this.closed = true;
|
|
@@ -4599,6 +5557,7 @@ class Dispatcher {
|
|
|
4599
5557
|
task: t.id,
|
|
4600
5558
|
runId: r.run.id,
|
|
4601
5559
|
worktree: r.run.worktree,
|
|
5560
|
+
by: this.opts.get(projectId)?.owner ?? null,
|
|
4602
5561
|
summary: `dispatch ${t.id} \u2192 run ${r.run.id}`
|
|
4603
5562
|
}
|
|
4604
5563
|
});
|
|
@@ -4760,6 +5719,16 @@ class ForgeService {
|
|
|
4760
5719
|
}));
|
|
4761
5720
|
}
|
|
4762
5721
|
outcomeCache = new Map;
|
|
5722
|
+
outcomeInflight = new Map;
|
|
5723
|
+
mergedCached(projectId, root) {
|
|
5724
|
+
const hit = this.outcomeCache.get(projectId);
|
|
5725
|
+
const fresh = !!hit && Date.now() - hit.at < 600000;
|
|
5726
|
+
if (!fresh && !this.outcomeInflight.has(projectId)) {
|
|
5727
|
+
const run2 = this.merged(projectId, root).finally(() => this.outcomeInflight.delete(projectId));
|
|
5728
|
+
this.outcomeInflight.set(projectId, run2);
|
|
5729
|
+
}
|
|
5730
|
+
return { merged: hit?.merged ?? [], reverted: hit?.reverted ?? [], fresh };
|
|
5731
|
+
}
|
|
4763
5732
|
async merged(projectId, root) {
|
|
4764
5733
|
const hit = this.outcomeCache.get(projectId);
|
|
4765
5734
|
if (hit && Date.now() - hit.at < 600000)
|
|
@@ -5676,7 +6645,7 @@ CREATE TABLE IF NOT EXISTS processes (
|
|
|
5676
6645
|
CREATE INDEX IF NOT EXISTS processes_live ON processes(ended_at, project_id);
|
|
5677
6646
|
CREATE TABLE IF NOT EXISTS gates (
|
|
5678
6647
|
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, gate TEXT, verdict TEXT,
|
|
5679
|
-
rubric TEXT, evidence TEXT, session_id TEXT, created_at TEXT
|
|
6648
|
+
rubric TEXT, evidence TEXT, session_id TEXT, duration_ms INTEGER, created_at TEXT
|
|
5680
6649
|
);
|
|
5681
6650
|
CREATE INDEX IF NOT EXISTS gates_task ON gates(project_id, task, created_at);
|
|
5682
6651
|
CREATE TABLE IF NOT EXISTS handoffs (
|
|
@@ -5819,7 +6788,7 @@ class Store {
|
|
|
5819
6788
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
|
|
5820
6789
|
}
|
|
5821
6790
|
}
|
|
5822
|
-
static SCHEMA_VERSION =
|
|
6791
|
+
static SCHEMA_VERSION = 2;
|
|
5823
6792
|
schemaVersion() {
|
|
5824
6793
|
return Number(this.meta("schema_version") ?? 0);
|
|
5825
6794
|
}
|
|
@@ -5857,6 +6826,16 @@ class Store {
|
|
|
5857
6826
|
fill("incident_acks", "'dashboard'", null, "acks");
|
|
5858
6827
|
fill("sessions", "NULL", "id", "sessions");
|
|
5859
6828
|
fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
|
|
6829
|
+
},
|
|
6830
|
+
(db) => {
|
|
6831
|
+
this.ensureColumn("gates", "duration_ms", "INTEGER");
|
|
6832
|
+
const rows = db.query("SELECT id, rubric FROM gates WHERE duration_ms IS NULL AND rubric LIKE '%s'").all();
|
|
6833
|
+
const upd = db.query("UPDATE gates SET duration_ms = ? WHERE id = ?");
|
|
6834
|
+
for (const r of rows) {
|
|
6835
|
+
const m = /\bin ([0-9]+(?:\.[0-9]+)?)s$/.exec(r.rubric ?? "");
|
|
6836
|
+
if (m)
|
|
6837
|
+
upd.run(Math.round(Number(m[1]) * 1000), r.id);
|
|
6838
|
+
}
|
|
5860
6839
|
}
|
|
5861
6840
|
];
|
|
5862
6841
|
for (let v = this.schemaVersion();v < steps.length; v++) {
|
|
@@ -6364,10 +7343,10 @@ class Store {
|
|
|
6364
7343
|
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
6365
7344
|
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
6366
7345
|
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
6367
|
-
const
|
|
6368
|
-
if (
|
|
6369
|
-
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${
|
|
6370
|
-
return
|
|
7346
|
+
const ms4 = rows.map(rowToMessage);
|
|
7347
|
+
if (ms4.length && !opts.peek)
|
|
7348
|
+
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${ms4.map(() => "?").join(",")})`).run(new Date().toISOString(), sessionId, ...ms4.map((m) => m.id));
|
|
7349
|
+
return ms4;
|
|
6371
7350
|
}
|
|
6372
7351
|
markMessageDelivered(id, sessionId) {
|
|
6373
7352
|
this.db.query("UPDATE messages SET delivered_at = ?, session_id = COALESCE(?, session_id) WHERE id = ? AND delivered_at IS NULL").run(new Date().toISOString(), sessionId, id);
|
|
@@ -6422,6 +7401,7 @@ class Store {
|
|
|
6422
7401
|
rubric: r.rubric,
|
|
6423
7402
|
evidence: r.evidence ?? null,
|
|
6424
7403
|
sessionId: r.session_id ?? null,
|
|
7404
|
+
durationMs: r.duration_ms ?? null,
|
|
6425
7405
|
createdAt: r.created_at
|
|
6426
7406
|
};
|
|
6427
7407
|
}
|
|
@@ -6572,10 +7552,10 @@ class Store {
|
|
|
6572
7552
|
};
|
|
6573
7553
|
const done = (async () => {
|
|
6574
7554
|
let diffText = "";
|
|
6575
|
-
let
|
|
7555
|
+
let stat2 = "";
|
|
6576
7556
|
try {
|
|
6577
7557
|
const diff = await worktreeDiff(p.root, where.worktree);
|
|
6578
|
-
|
|
7558
|
+
stat2 = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
|
|
6579
7559
|
`);
|
|
6580
7560
|
diffText = await worktreePatch(where.worktree, diff.base);
|
|
6581
7561
|
} catch (e) {
|
|
@@ -6597,7 +7577,7 @@ class Store {
|
|
|
6597
7577
|
task,
|
|
6598
7578
|
title: taskRow?.title ?? null,
|
|
6599
7579
|
branch: w?.branch ?? null,
|
|
6600
|
-
stat,
|
|
7580
|
+
stat: stat2,
|
|
6601
7581
|
patch: diffText
|
|
6602
7582
|
});
|
|
6603
7583
|
writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
|
|
@@ -6755,8 +7735,8 @@ ${err}
|
|
|
6755
7735
|
return v;
|
|
6756
7736
|
const createdAt = new Date().toISOString();
|
|
6757
7737
|
const sessionId = this.knownSession(input.sessionId);
|
|
6758
|
-
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
|
|
6759
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
|
|
7738
|
+
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, duration_ms, created_at, actor_kind, actor_id)
|
|
7739
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, typeof input.durationMs === "number" ? Math.max(0, Math.round(input.durationMs)) : null, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
|
|
6760
7740
|
const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
6761
7741
|
this.remember(gateDoc(projectId, run2.id, run2, sessionId));
|
|
6762
7742
|
this.append({
|
|
@@ -7338,9 +8318,9 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
7338
8318
|
const rows = this.db.query(`SELECT * FROM (SELECT ${WIRE_COLS} FROM events WHERE ${where.join(" AND ")} ORDER BY seq DESC LIMIT ?) ORDER BY seq`).all(...args, limit);
|
|
7339
8319
|
return rows.map((r) => auditRow(wireRowToEvent(r)));
|
|
7340
8320
|
}
|
|
7341
|
-
prune(
|
|
8321
|
+
prune(days3) {
|
|
7342
8322
|
const cfg = this.policyFor(null).config;
|
|
7343
|
-
const chatter =
|
|
8323
|
+
const chatter = days3 ?? cfg.events.retain_days;
|
|
7344
8324
|
const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
|
|
7345
8325
|
let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
|
|
7346
8326
|
if (cfg.audit.retain_days > 0) {
|
|
@@ -7869,7 +8849,8 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
7869
8849
|
acquiredAt: r.acquired_at,
|
|
7870
8850
|
expiresAt: r.expires_at,
|
|
7871
8851
|
releasedAt: r.released_at ?? null,
|
|
7872
|
-
state: r.state
|
|
8852
|
+
state: r.state,
|
|
8853
|
+
sessionId: r.actor_kind === "agent" ? r.actor_id ?? null : null
|
|
7873
8854
|
}));
|
|
7874
8855
|
const now = Date.now();
|
|
7875
8856
|
for (const c of rows)
|
|
@@ -7997,6 +8978,436 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
7997
8978
|
expiresAt: r.expires_at
|
|
7998
8979
|
}));
|
|
7999
8980
|
}
|
|
8981
|
+
waiting(projectId, days3 = 7) {
|
|
8982
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
8983
|
+
const pArgs = projectId ? [projectId] : [];
|
|
8984
|
+
const paired = this.db.query(`SELECT type, session_id, project_id, ts,
|
|
8985
|
+
COALESCE(json_extract(payload,'$.requestId'), json_extract(payload,'$.id')) AS key,
|
|
8986
|
+
COALESCE(json_extract(payload,'$.tool'), json_extract(payload,'$.text')) AS label
|
|
8987
|
+
FROM events
|
|
8988
|
+
WHERE type IN ('permission.requested','permission.resolved','question.asked','question.answered')
|
|
8989
|
+
AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
8990
|
+
const notes = this.db.query(`SELECT n.seq, n.session_id, n.project_id, n.ts,
|
|
8991
|
+
json_extract(n.payload,'$.summary') AS label,
|
|
8992
|
+
(SELECT MIN(a.ts) FROM events a
|
|
8993
|
+
WHERE a.session_id = n.session_id AND a.seq > n.seq
|
|
8994
|
+
AND a.type IN ('prompt.submitted','tool.requested')) AS resumed
|
|
8995
|
+
FROM events n
|
|
8996
|
+
WHERE n.type = 'session.notification' AND n.ts >= ?${projectId ? " AND n.project_id = ?" : ""}`).all(since, ...pArgs);
|
|
8997
|
+
const samples = [];
|
|
8998
|
+
for (const r of paired) {
|
|
8999
|
+
if (!r.session_id || r.key === null)
|
|
9000
|
+
continue;
|
|
9001
|
+
const kind = r.type.startsWith("permission") ? "permission" : "question";
|
|
9002
|
+
samples.push({
|
|
9003
|
+
sessionId: r.session_id,
|
|
9004
|
+
projectId: r.project_id,
|
|
9005
|
+
kind,
|
|
9006
|
+
key: String(r.key),
|
|
9007
|
+
phase: r.type.endsWith(".requested") || r.type.endsWith(".asked") ? "start" : "end",
|
|
9008
|
+
ts: r.ts,
|
|
9009
|
+
...r.label ? { label: r.label.slice(0, 120) } : {}
|
|
9010
|
+
});
|
|
9011
|
+
}
|
|
9012
|
+
for (const n of notes) {
|
|
9013
|
+
if (!n.session_id)
|
|
9014
|
+
continue;
|
|
9015
|
+
const key = String(n.seq);
|
|
9016
|
+
samples.push({
|
|
9017
|
+
sessionId: n.session_id,
|
|
9018
|
+
projectId: n.project_id,
|
|
9019
|
+
kind: "notification",
|
|
9020
|
+
key,
|
|
9021
|
+
phase: "start",
|
|
9022
|
+
ts: n.ts,
|
|
9023
|
+
...n.label ? { label: n.label.slice(0, 120) } : {}
|
|
9024
|
+
});
|
|
9025
|
+
if (n.resumed)
|
|
9026
|
+
samples.push({
|
|
9027
|
+
sessionId: n.session_id,
|
|
9028
|
+
projectId: n.project_id,
|
|
9029
|
+
kind: "notification",
|
|
9030
|
+
key,
|
|
9031
|
+
phase: "end",
|
|
9032
|
+
ts: n.resumed
|
|
9033
|
+
});
|
|
9034
|
+
}
|
|
9035
|
+
const ends = {};
|
|
9036
|
+
for (const r of this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all())
|
|
9037
|
+
ends[r.id] = r.ended_at;
|
|
9038
|
+
const report = waitingReport(pairWaits(samples, new Date().toISOString(), ends));
|
|
9039
|
+
const meta = new Map(this.db.query("SELECT id, title, agent, project_id FROM sessions").all().map((r) => [r.id, r]));
|
|
9040
|
+
return {
|
|
9041
|
+
...report,
|
|
9042
|
+
sessions: report.sessions.map((s) => ({
|
|
9043
|
+
...s,
|
|
9044
|
+
title: meta.get(s.sessionId)?.title ?? null,
|
|
9045
|
+
agent: meta.get(s.sessionId)?.agent ?? "claude-code",
|
|
9046
|
+
projectId: s.projectId ?? meta.get(s.sessionId)?.project_id ?? null
|
|
9047
|
+
}))
|
|
9048
|
+
};
|
|
9049
|
+
}
|
|
9050
|
+
gateHealth(projectId, days3 = 30) {
|
|
9051
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9052
|
+
const rows = this.db.query(`SELECT project_id, task, gate, verdict, duration_ms, created_at FROM gates
|
|
9053
|
+
WHERE created_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
9054
|
+
return gateHealth(rows.filter((r) => r.verdict === "pass" || r.verdict === "fail").map((r) => ({
|
|
9055
|
+
projectId: r.project_id,
|
|
9056
|
+
task: r.task,
|
|
9057
|
+
gate: r.gate,
|
|
9058
|
+
verdict: r.verdict,
|
|
9059
|
+
durationMs: r.duration_ms,
|
|
9060
|
+
at: r.created_at
|
|
9061
|
+
})));
|
|
9062
|
+
}
|
|
9063
|
+
static worktreeIdleMs(path) {
|
|
9064
|
+
let newest = 0;
|
|
9065
|
+
for (const f of [path, join8(path, ".git")]) {
|
|
9066
|
+
try {
|
|
9067
|
+
newest = Math.max(newest, statSync(f).mtimeMs);
|
|
9068
|
+
} catch {}
|
|
9069
|
+
}
|
|
9070
|
+
return newest ? Math.max(0, Date.now() - newest) : null;
|
|
9071
|
+
}
|
|
9072
|
+
duCache = new Map;
|
|
9073
|
+
duInflight = null;
|
|
9074
|
+
refreshDisk(paths, ttlMs) {
|
|
9075
|
+
if (this.duInflight)
|
|
9076
|
+
return;
|
|
9077
|
+
const stale = paths.filter((p) => {
|
|
9078
|
+
const hit = this.duCache.get(p);
|
|
9079
|
+
return !hit || Date.now() - hit.t >= ttlMs;
|
|
9080
|
+
});
|
|
9081
|
+
if (!stale.length)
|
|
9082
|
+
return;
|
|
9083
|
+
this.duInflight = (async () => {
|
|
9084
|
+
for (const path of stale) {
|
|
9085
|
+
let v = null;
|
|
9086
|
+
try {
|
|
9087
|
+
const proc = Bun.spawn(["du", "-sk", "-x", path], { stdout: "pipe", stderr: "ignore" });
|
|
9088
|
+
const out = await new Response(proc.stdout).text();
|
|
9089
|
+
if (await proc.exited === 0) {
|
|
9090
|
+
const n = Number.parseInt(out.trim().split(/\s+/)[0] ?? "", 10);
|
|
9091
|
+
if (Number.isFinite(n))
|
|
9092
|
+
v = n;
|
|
9093
|
+
}
|
|
9094
|
+
} catch {}
|
|
9095
|
+
this.duCache.set(path, { v, t: Date.now() });
|
|
9096
|
+
}
|
|
9097
|
+
})().finally(() => {
|
|
9098
|
+
this.duInflight = null;
|
|
9099
|
+
});
|
|
9100
|
+
}
|
|
9101
|
+
hygiene(projectId, diskTtlMs = 600000) {
|
|
9102
|
+
const rows = this.db.query(`SELECT * FROM processes WHERE ended_at IS NULL${projectId ? " AND project_id = ?" : ""}
|
|
9103
|
+
ORDER BY started_at DESC`).all(...projectId ? [projectId] : []).map((r) => this.rowToProcess(r));
|
|
9104
|
+
const usage = new Map;
|
|
9105
|
+
if (rows.length) {
|
|
9106
|
+
try {
|
|
9107
|
+
const out = Bun.spawnSync([
|
|
9108
|
+
"ps",
|
|
9109
|
+
"-o",
|
|
9110
|
+
"pid=,pcpu=,rss=",
|
|
9111
|
+
"-p",
|
|
9112
|
+
rows.map((r) => r.pid).join(",")
|
|
9113
|
+
]);
|
|
9114
|
+
for (const line of new TextDecoder().decode(out.stdout).split(`
|
|
9115
|
+
`)) {
|
|
9116
|
+
const [pid, cpu, rss] = line.trim().split(/\s+/);
|
|
9117
|
+
if (pid)
|
|
9118
|
+
usage.set(Number(pid), { cpu: Number(cpu) || 0, rss: Number(rss) || 0 });
|
|
9119
|
+
}
|
|
9120
|
+
} catch {}
|
|
9121
|
+
}
|
|
9122
|
+
const liveSessionIds = new Set(this.db.query("SELECT id FROM sessions WHERE ended_at IS NULL AND state IN ('active','waiting')").all().map((r) => r.id));
|
|
9123
|
+
const procs = rows.map((p) => {
|
|
9124
|
+
const u = usage.get(p.pid);
|
|
9125
|
+
return {
|
|
9126
|
+
pid: p.pid,
|
|
9127
|
+
name: p.name,
|
|
9128
|
+
kind: p.kind,
|
|
9129
|
+
projectId: p.projectId,
|
|
9130
|
+
sessionId: p.sessionId,
|
|
9131
|
+
port: p.port,
|
|
9132
|
+
startedAt: p.startedAt,
|
|
9133
|
+
alive: this.processIsOurs(p),
|
|
9134
|
+
sessionLive: !p.sessionId || liveSessionIds.has(p.sessionId),
|
|
9135
|
+
cpuPct: u ? u.cpu : null,
|
|
9136
|
+
rssKb: u ? u.rss : null
|
|
9137
|
+
};
|
|
9138
|
+
});
|
|
9139
|
+
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9140
|
+
const claims = this.claims().filter((c) => c.state !== "released");
|
|
9141
|
+
const trees = [];
|
|
9142
|
+
for (const pid of projects)
|
|
9143
|
+
for (const w of this.worktrees(pid)) {
|
|
9144
|
+
const held = claims.find((c) => c.worktree === w.path);
|
|
9145
|
+
trees.push({
|
|
9146
|
+
projectId: pid,
|
|
9147
|
+
path: w.path,
|
|
9148
|
+
branch: w.branch,
|
|
9149
|
+
main: w.main,
|
|
9150
|
+
dirty: w.dirty,
|
|
9151
|
+
ahead: w.ahead,
|
|
9152
|
+
merged: w.merged,
|
|
9153
|
+
idleMs: Store.worktreeIdleMs(w.path),
|
|
9154
|
+
diskKb: this.duCache.get(w.path)?.v ?? null,
|
|
9155
|
+
heldByClaim: held?.task ?? null,
|
|
9156
|
+
liveSessions: this.sessions().filter((s) => s.cwd?.startsWith(w.path) && !s.endedAt && s.state !== "ended").length
|
|
9157
|
+
});
|
|
9158
|
+
}
|
|
9159
|
+
this.refreshDisk(trees.map((t) => t.path), diskTtlMs);
|
|
9160
|
+
return hygieneReport(procs, trees);
|
|
9161
|
+
}
|
|
9162
|
+
lineage(projectId, days3 = 14, expanded = []) {
|
|
9163
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9164
|
+
const pArgs = projectId ? [projectId] : [];
|
|
9165
|
+
const rows = this.db.query(`SELECT id, project_id, title, agent, kind, state, parent_id, started_at, ended_at
|
|
9166
|
+
FROM sessions WHERE last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
9167
|
+
const cost = new Map(this.db.query("SELECT session_id, SUM(cost_usd) AS c FROM turns WHERE ts >= ? GROUP BY session_id").all(since).map((r) => [r.session_id, r.c]));
|
|
9168
|
+
const sessions = rows.map((r) => ({
|
|
9169
|
+
id: r.id,
|
|
9170
|
+
projectId: r.project_id,
|
|
9171
|
+
title: r.title,
|
|
9172
|
+
agent: r.agent ?? "claude-code",
|
|
9173
|
+
kind: r.kind,
|
|
9174
|
+
state: r.state,
|
|
9175
|
+
startedAt: r.started_at,
|
|
9176
|
+
endedAt: r.ended_at,
|
|
9177
|
+
costUsd: cost.get(r.id) ?? null,
|
|
9178
|
+
outcome: null
|
|
9179
|
+
}));
|
|
9180
|
+
const known = new Set(sessions.map((s) => s.id));
|
|
9181
|
+
const edges = [];
|
|
9182
|
+
for (const a of this.db.query(`SELECT t.session_id, t.agent_id, MIN(t.ts) AS first_ts, MAX(t.ts) AS last_ts,
|
|
9183
|
+
SUM(t.cost_usd) AS cost, COUNT(*) AS turns
|
|
9184
|
+
FROM turns t WHERE t.agent_id IS NOT NULL AND t.ts >= ?
|
|
9185
|
+
GROUP BY t.session_id, t.agent_id`).all(since)) {
|
|
9186
|
+
const parent = sessions.find((x) => x.id === a.session_id);
|
|
9187
|
+
if (!parent)
|
|
9188
|
+
continue;
|
|
9189
|
+
const id = `sub:${a.session_id}:${a.agent_id}`;
|
|
9190
|
+
sessions.push({
|
|
9191
|
+
id,
|
|
9192
|
+
projectId: parent.projectId,
|
|
9193
|
+
title: `subagent ${a.agent_id.slice(0, 8)} \xB7 ${a.turns} turn${a.turns === 1 ? "" : "s"}`,
|
|
9194
|
+
agent: parent.agent,
|
|
9195
|
+
kind: "subagent",
|
|
9196
|
+
state: "ended",
|
|
9197
|
+
startedAt: a.first_ts,
|
|
9198
|
+
endedAt: a.last_ts,
|
|
9199
|
+
costUsd: a.cost,
|
|
9200
|
+
outcome: null
|
|
9201
|
+
});
|
|
9202
|
+
known.add(id);
|
|
9203
|
+
edges.push({ from: a.session_id, to: id, kind: "subagent", at: a.first_ts });
|
|
9204
|
+
}
|
|
9205
|
+
for (const d of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.by') AS by, json_extract(payload,'$.task') AS task
|
|
9206
|
+
FROM events WHERE type = 'dispatch.started' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs)) {
|
|
9207
|
+
if (!d.session_id || !d.by)
|
|
9208
|
+
continue;
|
|
9209
|
+
const a = actorFrom(d.by, null);
|
|
9210
|
+
if (a.kind === "agent" && known.has(a.id) && a.id !== d.session_id)
|
|
9211
|
+
edges.push({ from: a.id, to: d.session_id, kind: "dispatch", at: d.ts, label: d.task });
|
|
9212
|
+
}
|
|
9213
|
+
for (const m of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.recipient') AS to_session,
|
|
9214
|
+
json_extract(payload,'$.text') AS text
|
|
9215
|
+
FROM events WHERE type = 'message.sent' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs))
|
|
9216
|
+
if (m.session_id && m.to_session)
|
|
9217
|
+
edges.push({
|
|
9218
|
+
from: m.session_id,
|
|
9219
|
+
to: m.to_session,
|
|
9220
|
+
kind: "message",
|
|
9221
|
+
at: m.ts,
|
|
9222
|
+
label: m.text?.slice(0, 80) ?? null
|
|
9223
|
+
});
|
|
9224
|
+
const holds = this.db.query(`SELECT task, project_id, actor_kind, actor_id, acquired_at FROM claims
|
|
9225
|
+
WHERE acquired_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs).map((c) => ({
|
|
9226
|
+
task: c.task,
|
|
9227
|
+
projectId: c.project_id,
|
|
9228
|
+
sessionId: c.actor_kind === "agent" ? c.actor_id : null,
|
|
9229
|
+
at: c.acquired_at
|
|
9230
|
+
}));
|
|
9231
|
+
edges.push(...handoffEdges(holds));
|
|
9232
|
+
return lineageGraph(sessions, edges, { expanded });
|
|
9233
|
+
}
|
|
9234
|
+
mcpHealth(projectId, days3 = 7) {
|
|
9235
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9236
|
+
const rows = this.db.query(`SELECT session_id, seq, ts, type,
|
|
9237
|
+
json_extract(payload,'$.tool') AS tool,
|
|
9238
|
+
json_extract(payload,'$.toolResponse') AS response
|
|
9239
|
+
FROM events
|
|
9240
|
+
WHERE type IN ('tool.requested','tool.completed') AND ts >= ?
|
|
9241
|
+
AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
|
|
9242
|
+
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
9243
|
+
const calls = [];
|
|
9244
|
+
const pending = new Map;
|
|
9245
|
+
const abandon = (sessionId, tool, at) => {
|
|
9246
|
+
calls.push({ sessionId, tool, ms: null, errored: false, at });
|
|
9247
|
+
};
|
|
9248
|
+
for (const r of rows) {
|
|
9249
|
+
if (!r.session_id)
|
|
9250
|
+
continue;
|
|
9251
|
+
const perSession = pending.get(r.session_id) ?? new Map;
|
|
9252
|
+
pending.set(r.session_id, perSession);
|
|
9253
|
+
if (r.type === "tool.requested") {
|
|
9254
|
+
const open = perSession.get(r.tool);
|
|
9255
|
+
if (open)
|
|
9256
|
+
abandon(r.session_id, r.tool, open);
|
|
9257
|
+
perSession.set(r.tool, r.ts);
|
|
9258
|
+
continue;
|
|
9259
|
+
}
|
|
9260
|
+
const startedAt = perSession.get(r.tool);
|
|
9261
|
+
perSession.delete(r.tool);
|
|
9262
|
+
calls.push({
|
|
9263
|
+
sessionId: r.session_id,
|
|
9264
|
+
tool: r.tool,
|
|
9265
|
+
ms: startedAt ? Math.max(0, new Date(r.ts).getTime() - new Date(startedAt).getTime()) : null,
|
|
9266
|
+
errored: toolResponseErrored(safeJson(r.response)),
|
|
9267
|
+
at: r.ts
|
|
9268
|
+
});
|
|
9269
|
+
}
|
|
9270
|
+
for (const [sessionId, perSession] of pending)
|
|
9271
|
+
for (const [tool, at] of perSession)
|
|
9272
|
+
abandon(sessionId, tool, at);
|
|
9273
|
+
return mcpHealth(calls);
|
|
9274
|
+
}
|
|
9275
|
+
context(projectId, days3 = 7) {
|
|
9276
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9277
|
+
const pArgs = projectId ? [projectId] : [];
|
|
9278
|
+
const where = projectId ? " AND project_id = ?" : "";
|
|
9279
|
+
const results = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
9280
|
+
LENGTH(json_extract(payload,'$.toolResponse')) AS chars
|
|
9281
|
+
FROM events
|
|
9282
|
+
WHERE type = 'tool.completed' AND ts >= ?${where}
|
|
9283
|
+
AND json_extract(payload,'$.tool') IS NOT NULL
|
|
9284
|
+
AND json_extract(payload,'$.toolResponse') IS NOT NULL`).all(since, ...pArgs).filter((r) => r.session_id).map((r) => ({ sessionId: r.session_id, tool: r.tool, chars: r.chars ?? 0 }));
|
|
9285
|
+
const reads = this.db.query(`SELECT req.session_id,
|
|
9286
|
+
json_extract(req.payload,'$.toolInput.file_path') AS path,
|
|
9287
|
+
(SELECT LENGTH(json_extract(done.payload,'$.toolResponse')) FROM events done
|
|
9288
|
+
WHERE done.session_id = req.session_id AND done.seq > req.seq
|
|
9289
|
+
AND done.type = 'tool.completed'
|
|
9290
|
+
AND json_extract(done.payload,'$.tool') = 'Read'
|
|
9291
|
+
ORDER BY done.seq LIMIT 1) AS chars
|
|
9292
|
+
FROM events req
|
|
9293
|
+
WHERE req.type = 'tool.requested' AND json_extract(req.payload,'$.tool') = 'Read'
|
|
9294
|
+
AND req.ts >= ?${where.replace("project_id", "req.project_id")}
|
|
9295
|
+
AND json_extract(req.payload,'$.toolInput.file_path') IS NOT NULL`).all(since, ...pArgs).filter((r) => r.session_id).map((r) => ({ sessionId: r.session_id, path: r.path, chars: r.chars ?? 0 }));
|
|
9296
|
+
const turns = this.db.query(`SELECT t.session_id, SUM(t.input) AS input, SUM(t.cache_read) AS cache_read,
|
|
9297
|
+
SUM(t.cache_write) AS cache_write, SUM(t.thinking) AS thinking, SUM(t.output) AS output
|
|
9298
|
+
FROM turns t${projectId ? " JOIN sessions s ON s.id = t.session_id" : ""}
|
|
9299
|
+
WHERE t.ts >= ?${projectId ? " AND s.project_id = ?" : ""}
|
|
9300
|
+
GROUP BY t.session_id`).all(since, ...pArgs).map((r) => ({
|
|
9301
|
+
sessionId: r.session_id,
|
|
9302
|
+
input: r.input ?? 0,
|
|
9303
|
+
cacheRead: r.cache_read ?? 0,
|
|
9304
|
+
cacheWrite: r.cache_write ?? 0,
|
|
9305
|
+
thinking: r.thinking ?? 0,
|
|
9306
|
+
output: r.output ?? 0
|
|
9307
|
+
}));
|
|
9308
|
+
const report = contextReport(results, reads, turns);
|
|
9309
|
+
const meta = new Map(this.db.query("SELECT id, title, agent FROM sessions").all().map((r) => [r.id, r]));
|
|
9310
|
+
return {
|
|
9311
|
+
...report,
|
|
9312
|
+
sessions: report.sessions.map((s) => ({
|
|
9313
|
+
...s,
|
|
9314
|
+
title: meta.get(s.sessionId)?.title ?? null,
|
|
9315
|
+
agent: meta.get(s.sessionId)?.agent ?? "claude-code"
|
|
9316
|
+
}))
|
|
9317
|
+
};
|
|
9318
|
+
}
|
|
9319
|
+
abTrials(projectId) {
|
|
9320
|
+
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9321
|
+
const out = [];
|
|
9322
|
+
for (const pid of projects) {
|
|
9323
|
+
const tasks2 = [
|
|
9324
|
+
...new Set(this.claims(pid).map((c) => splitArmTask(c.task)).filter((x) => x.arm).map((x) => x.task))
|
|
9325
|
+
];
|
|
9326
|
+
for (const t of tasks2.sort())
|
|
9327
|
+
out.push({ ...this.abTrial(pid, t), projectId: pid });
|
|
9328
|
+
}
|
|
9329
|
+
const rank = { undecided: 0, "all-failed": 1, winner: 2 };
|
|
9330
|
+
return out.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.totals.costUsd - a.totals.costUsd);
|
|
9331
|
+
}
|
|
9332
|
+
diffCache = new Map;
|
|
9333
|
+
diffInflight = null;
|
|
9334
|
+
refreshDiffs(worktrees, base, ttlMs = 30000) {
|
|
9335
|
+
if (this.diffInflight)
|
|
9336
|
+
return;
|
|
9337
|
+
const stale = worktrees.filter((w) => {
|
|
9338
|
+
const hit = this.diffCache.get(w);
|
|
9339
|
+
return !hit || Date.now() - hit.t >= ttlMs;
|
|
9340
|
+
});
|
|
9341
|
+
if (!stale.length)
|
|
9342
|
+
return;
|
|
9343
|
+
this.diffInflight = (async () => {
|
|
9344
|
+
for (const wt of stale) {
|
|
9345
|
+
let v = null;
|
|
9346
|
+
try {
|
|
9347
|
+
const proc = Bun.spawn(["git", "diff", "--shortstat", `${base}...HEAD`], {
|
|
9348
|
+
cwd: wt,
|
|
9349
|
+
stdout: "pipe",
|
|
9350
|
+
stderr: "ignore"
|
|
9351
|
+
});
|
|
9352
|
+
const out = await new Response(proc.stdout).text();
|
|
9353
|
+
if (await proc.exited === 0) {
|
|
9354
|
+
const f = /(\d+) files? changed/.exec(out)?.[1];
|
|
9355
|
+
const i = /(\d+) insertions?/.exec(out)?.[1];
|
|
9356
|
+
const d = /(\d+) deletions?/.exec(out)?.[1];
|
|
9357
|
+
v = [Number(f ?? 0), Number(i ?? 0), Number(d ?? 0)];
|
|
9358
|
+
}
|
|
9359
|
+
} catch {}
|
|
9360
|
+
this.diffCache.set(wt, { v, t: Date.now() });
|
|
9361
|
+
}
|
|
9362
|
+
})().finally(() => {
|
|
9363
|
+
this.diffInflight = null;
|
|
9364
|
+
});
|
|
9365
|
+
}
|
|
9366
|
+
abTrial(projectId, task) {
|
|
9367
|
+
const claims = this.claims(projectId).filter((c) => splitArmTask(c.task).task === task);
|
|
9368
|
+
const all = this.sessions();
|
|
9369
|
+
const sessions = new Map(all.map((s) => [s.id, s]));
|
|
9370
|
+
const byCwd = new Map;
|
|
9371
|
+
for (const s of all)
|
|
9372
|
+
if (s.cwd && !byCwd.has(s.cwd))
|
|
9373
|
+
byCwd.set(s.cwd, s);
|
|
9374
|
+
const gateRuns = this.gateRuns(projectId, undefined, 2000);
|
|
9375
|
+
const arms = [];
|
|
9376
|
+
for (const c of claims) {
|
|
9377
|
+
const label = splitArmTask(c.task).arm;
|
|
9378
|
+
if (!label)
|
|
9379
|
+
continue;
|
|
9380
|
+
const sess = (c.worktree ? byCwd.get(c.worktree) : undefined) ?? (c.sessionId ? sessions.get(c.sessionId) : undefined);
|
|
9381
|
+
const sid = sess?.id ?? c.sessionId;
|
|
9382
|
+
const g = gateRuns.filter((x) => x.task === c.task);
|
|
9383
|
+
const latest = new Map;
|
|
9384
|
+
for (const run2 of [...g].sort((a, b) => a.createdAt < b.createdAt ? -1 : 1))
|
|
9385
|
+
latest.set(run2.gate, run2);
|
|
9386
|
+
const verdicts = [...latest.values()];
|
|
9387
|
+
const diff = c.worktree ? this.diffCache.get(c.worktree)?.v : null;
|
|
9388
|
+
arms.push({
|
|
9389
|
+
label,
|
|
9390
|
+
task: c.task,
|
|
9391
|
+
model: sess?.model ?? null,
|
|
9392
|
+
agent: sess?.agent ?? "claude-code",
|
|
9393
|
+
sessionId: sid,
|
|
9394
|
+
worktree: c.worktree || null,
|
|
9395
|
+
startedAt: c.acquiredAt,
|
|
9396
|
+
endedAt: sess?.endedAt ?? null,
|
|
9397
|
+
state: sess && sess.state !== "ended" ? "running" : sess ? "done" : c.state === "held" ? "running" : "failed",
|
|
9398
|
+
costUsd: sess?.costUsd ?? 0,
|
|
9399
|
+
turns: sess?.turns ?? 0,
|
|
9400
|
+
gatesPassed: verdicts.filter((v) => v.verdict === "pass").length,
|
|
9401
|
+
gatesFailed: verdicts.filter((v) => v.verdict === "fail").length,
|
|
9402
|
+
filesChanged: diff ? diff[0] : null,
|
|
9403
|
+
insertions: diff ? diff[1] : null,
|
|
9404
|
+
deletions: diff ? diff[2] : null
|
|
9405
|
+
});
|
|
9406
|
+
}
|
|
9407
|
+
const base = this.worktrees(projectId).find((w) => w.main)?.branch ?? "main";
|
|
9408
|
+
this.refreshDiffs(arms.map((a) => a.worktree).filter((w) => !!w), base);
|
|
9409
|
+
return scoreTrial(task, arms);
|
|
9410
|
+
}
|
|
8000
9411
|
collisions(projectId) {
|
|
8001
9412
|
const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
|
|
8002
9413
|
const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
|
|
@@ -8771,6 +10182,16 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8771
10182
|
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
|
|
8772
10183
|
return r.n;
|
|
8773
10184
|
}
|
|
10185
|
+
openIncidentsByProject() {
|
|
10186
|
+
const rows = this.db.query(`SELECT e.project_id AS pid, COUNT(*) AS n FROM events e
|
|
10187
|
+
LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
10188
|
+
WHERE e.type = 'incident.opened' AND a.seq IS NULL AND e.project_id IS NOT NULL
|
|
10189
|
+
GROUP BY e.project_id`).all();
|
|
10190
|
+
const out = {};
|
|
10191
|
+
for (const r of rows)
|
|
10192
|
+
out[r.pid] = r.n;
|
|
10193
|
+
return out;
|
|
10194
|
+
}
|
|
8774
10195
|
ackIncident(seq, by) {
|
|
8775
10196
|
const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
|
|
8776
10197
|
if (!row)
|
|
@@ -9094,14 +10515,14 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9094
10515
|
const from = localDayIso(-13);
|
|
9095
10516
|
const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
|
|
9096
10517
|
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
9097
|
-
const
|
|
10518
|
+
const days3 = [];
|
|
9098
10519
|
for (let i = 13;i >= 0; i--)
|
|
9099
|
-
|
|
10520
|
+
days3.push(localDayIso(-i).slice(0, 10));
|
|
9100
10521
|
const out = {};
|
|
9101
10522
|
for (const r of rows) {
|
|
9102
10523
|
out[r.pid] ??= new Array(14).fill(0);
|
|
9103
10524
|
const arr = out[r.pid];
|
|
9104
|
-
const i =
|
|
10525
|
+
const i = days3.indexOf(r.day);
|
|
9105
10526
|
if (i >= 0)
|
|
9106
10527
|
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
9107
10528
|
}
|
|
@@ -9122,12 +10543,22 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9122
10543
|
processes: this.memoised("processes", 5000, () => this.processes()),
|
|
9123
10544
|
incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
|
|
9124
10545
|
openIncidents: this.memoised("openIncidents", 30000, () => this.openIncidents()),
|
|
10546
|
+
openIncidentsByProject: this.memoised("openIncidentsByProject", 30000, () => this.openIncidentsByProject()),
|
|
9125
10547
|
questions: this.questions({ open: true, limit: 50 }),
|
|
9126
10548
|
resources: this.resources(),
|
|
9127
10549
|
seq: this.seq()
|
|
9128
10550
|
};
|
|
9129
10551
|
}
|
|
9130
10552
|
}
|
|
10553
|
+
function safeJson(v) {
|
|
10554
|
+
if (v === null)
|
|
10555
|
+
return null;
|
|
10556
|
+
try {
|
|
10557
|
+
return JSON.parse(v);
|
|
10558
|
+
} catch {
|
|
10559
|
+
return v;
|
|
10560
|
+
}
|
|
10561
|
+
}
|
|
9131
10562
|
var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
9132
10563
|
var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
|
|
9133
10564
|
var TOOL_INPUT_MAX = 2048;
|
|
@@ -9630,7 +11061,7 @@ class WorkflowEngine {
|
|
|
9630
11061
|
}
|
|
9631
11062
|
|
|
9632
11063
|
// packages/daemon/src/app.ts
|
|
9633
|
-
var VERSION = "0.
|
|
11064
|
+
var VERSION = "0.11.1";
|
|
9634
11065
|
var WEB_DIR = (() => {
|
|
9635
11066
|
if (process.env.SWARM_WEB_DIR)
|
|
9636
11067
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -9756,12 +11187,18 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9756
11187
|
app.get("/v1/state", (c) => c.json(store.snapshot()));
|
|
9757
11188
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
9758
11189
|
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11190
|
+
app.get("/v1/graphs/lineage", (c) => c.json(store.lineage(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)), c.req.queries("expand") ?? [])));
|
|
11191
|
+
app.get("/v1/context", (c) => c.json(store.context(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
11192
|
+
app.get("/v1/mcp/health", (c) => c.json(store.mcpHealth(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
11193
|
+
app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
|
|
11194
|
+
app.get("/v1/gates/health", (c) => c.json(store.gateHealth(c.req.query("project") || undefined, Math.max(1, Math.min(365, Number(c.req.query("days") ?? 30) || 30)))));
|
|
11195
|
+
app.get("/v1/waiting", (c) => c.json(store.waiting(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
9759
11196
|
app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
|
|
9760
11197
|
open: c.req.query("open") === "1",
|
|
9761
11198
|
projectId: c.req.query("project") || undefined
|
|
9762
11199
|
})));
|
|
9763
|
-
|
|
9764
|
-
const
|
|
11200
|
+
const outcomesFor = async (project, opts = {}) => {
|
|
11201
|
+
const blocking = opts.blocking !== false;
|
|
9765
11202
|
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
9766
11203
|
id: s.id,
|
|
9767
11204
|
branch: s.branch,
|
|
@@ -9772,8 +11209,11 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9772
11209
|
}));
|
|
9773
11210
|
const prs = [];
|
|
9774
11211
|
const reverted = new Set;
|
|
11212
|
+
let stale = false;
|
|
9775
11213
|
for (const p of store.projects().filter((x) => !project || x.id === project)) {
|
|
9776
|
-
const o = await forge2.merged(p.id, p.root);
|
|
11214
|
+
const o = blocking ? { ...await forge2.merged(p.id, p.root), fresh: true } : forge2.mergedCached(p.id, p.root);
|
|
11215
|
+
if (!o.fresh)
|
|
11216
|
+
stale = true;
|
|
9777
11217
|
for (const m of o.merged)
|
|
9778
11218
|
prs.push({ ...m, state: "merged" });
|
|
9779
11219
|
for (const sha of o.reverted)
|
|
@@ -9791,7 +11231,96 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9791
11231
|
mergedAt: null,
|
|
9792
11232
|
mergeSha: null
|
|
9793
11233
|
});
|
|
9794
|
-
return
|
|
11234
|
+
return { ...outcomeReport(sessions, prs, reverted), stale };
|
|
11235
|
+
};
|
|
11236
|
+
app.get("/v1/outcomes", async (c) => c.json(await outcomesFor(c.req.query("project") || undefined)));
|
|
11237
|
+
app.get("/v1/ab", (c) => {
|
|
11238
|
+
const project = c.req.query("project") || undefined;
|
|
11239
|
+
const task = c.req.query("task");
|
|
11240
|
+
if (task) {
|
|
11241
|
+
if (!project)
|
|
11242
|
+
return c.json({ error: "project is required with task" }, 400);
|
|
11243
|
+
return c.json(store.abTrial(project, task));
|
|
11244
|
+
}
|
|
11245
|
+
return c.json({ trials: store.abTrials(project) });
|
|
11246
|
+
});
|
|
11247
|
+
app.post("/v1/ab", async (c) => {
|
|
11248
|
+
const b = await c.req.json().catch(() => ({}));
|
|
11249
|
+
if (!b.projectId || !b.task)
|
|
11250
|
+
return c.json({ error: "projectId and task are required" }, 400);
|
|
11251
|
+
const arms = (b.arms ?? []).filter((a) => a.model || a.label);
|
|
11252
|
+
if (arms.length < 2)
|
|
11253
|
+
return c.json({ error: "a trial needs at least two arms" }, 400);
|
|
11254
|
+
const cfg = store.config(b.projectId);
|
|
11255
|
+
const started = [];
|
|
11256
|
+
const failed = [];
|
|
11257
|
+
for (const a of arms) {
|
|
11258
|
+
const label = (a.label ?? a.model ?? "").replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
11259
|
+
const id = armTask(b.task, label);
|
|
11260
|
+
const r = await runner.start({
|
|
11261
|
+
projectId: b.projectId,
|
|
11262
|
+
task: id,
|
|
11263
|
+
prompt: taskPrompt({ id: b.task, title: b.title ?? b.task }, {
|
|
11264
|
+
requiredGates: cfg.gates.required,
|
|
11265
|
+
executableGates: cfg.gates.required.filter((g) => cfg.gates.defs[g]),
|
|
11266
|
+
openPr: false
|
|
11267
|
+
}),
|
|
11268
|
+
owner: `ab:${label}`,
|
|
11269
|
+
permissionMode: cfg.dispatch.permission_mode ?? "acceptEdits",
|
|
11270
|
+
...a.model ? { model: a.model } : {},
|
|
11271
|
+
...cfg.dispatch.max_turns ? { maxTurns: cfg.dispatch.max_turns } : {}
|
|
11272
|
+
});
|
|
11273
|
+
if (r.ok)
|
|
11274
|
+
started.push(label);
|
|
11275
|
+
else
|
|
11276
|
+
failed.push({ arm: label, reason: r.reason });
|
|
11277
|
+
}
|
|
11278
|
+
store.append({
|
|
11279
|
+
ts: new Date().toISOString(),
|
|
11280
|
+
type: "dispatch.queued",
|
|
11281
|
+
projectId: b.projectId,
|
|
11282
|
+
sessionId: null,
|
|
11283
|
+
payload: {
|
|
11284
|
+
task: b.task,
|
|
11285
|
+
arms: arms.map((a) => a.label ?? a.model),
|
|
11286
|
+
summary: `A/B ${b.task}: ${started.length} arm${started.length === 1 ? "" : "s"} started`
|
|
11287
|
+
}
|
|
11288
|
+
});
|
|
11289
|
+
return c.json({ ok: failed.length === 0, started, failed }, failed.length ? 207 : 200);
|
|
11290
|
+
});
|
|
11291
|
+
app.get("/v1/provenance", async (c) => {
|
|
11292
|
+
const project = c.req.query("project") || undefined;
|
|
11293
|
+
const limit = Math.max(1, Math.min(500, Number(c.req.query("limit") ?? 50) || 50));
|
|
11294
|
+
const offset = Math.max(0, Number(c.req.query("offset") ?? 0) || 0);
|
|
11295
|
+
const { branches, stale } = await outcomesFor(project, { blocking: false });
|
|
11296
|
+
const projects = store.projects().filter((p) => !project || p.id === project);
|
|
11297
|
+
const tasks2 = [];
|
|
11298
|
+
for (const p of projects)
|
|
11299
|
+
for (const t of store.tasks(p.id)?.tasks ?? [])
|
|
11300
|
+
tasks2.push({ id: t.id, title: t.title, status: t.statusText, url: null });
|
|
11301
|
+
const claims = store.claims(project).map((cl) => ({
|
|
11302
|
+
task: cl.task,
|
|
11303
|
+
sessionId: cl.sessionId,
|
|
11304
|
+
owner: cl.owner || null,
|
|
11305
|
+
worktree: cl.worktree || null,
|
|
11306
|
+
branch: cl.branch || null,
|
|
11307
|
+
acquiredAt: cl.acquiredAt,
|
|
11308
|
+
state: cl.state
|
|
11309
|
+
}));
|
|
11310
|
+
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
11311
|
+
id: s.id,
|
|
11312
|
+
title: s.title,
|
|
11313
|
+
agent: s.agent,
|
|
11314
|
+
branch: s.branch,
|
|
11315
|
+
costUsd: s.costUsd
|
|
11316
|
+
}));
|
|
11317
|
+
const full = provenance(tasks2, claims, sessions, branches);
|
|
11318
|
+
return c.json({
|
|
11319
|
+
...full,
|
|
11320
|
+
chains: full.chains.slice(offset, offset + limit),
|
|
11321
|
+
page: { limit, offset, total: full.chains.length },
|
|
11322
|
+
stale
|
|
11323
|
+
});
|
|
9795
11324
|
});
|
|
9796
11325
|
app.get("/v1/memory", (c) => {
|
|
9797
11326
|
const q = c.req.query("q") ?? "";
|
|
@@ -9831,8 +11360,8 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9831
11360
|
const p = id ? store.project(id) : null;
|
|
9832
11361
|
if (id && !p)
|
|
9833
11362
|
return c.json({ error: "unknown project" }, 404);
|
|
9834
|
-
const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
9835
|
-
return c.json({ ...policy2, provenance, overridden });
|
|
11363
|
+
const { provenance: provenance3, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
11364
|
+
return c.json({ ...policy2, provenance: provenance3, overridden });
|
|
9836
11365
|
});
|
|
9837
11366
|
app.get("/v1/audit", (c) => {
|
|
9838
11367
|
const since = sinceToIso(c.req.query("since"));
|
|
@@ -10384,8 +11913,14 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
10384
11913
|
const p = join10(WEB_DIR, f);
|
|
10385
11914
|
if (!existsSync7(p))
|
|
10386
11915
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
11916
|
+
const st = statSync2(p);
|
|
11917
|
+
const etag = `W/"${st.size.toString(16)}-${st.mtimeMs.toString(16)}"`;
|
|
11918
|
+
if (c.req.header("if-none-match") === etag)
|
|
11919
|
+
return c.body(null, 304, { etag });
|
|
10387
11920
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
10388
|
-
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
11921
|
+
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain",
|
|
11922
|
+
"cache-control": "no-cache",
|
|
11923
|
+
etag
|
|
10389
11924
|
});
|
|
10390
11925
|
});
|
|
10391
11926
|
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
|