@ra3orblade/swarm 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -17
- package/dist/swarm-hook.js +41 -0
- package/dist/swarm.js +210 -3
- package/dist/swarmd.js +2816 -144
- package/package.json +1 -1
- package/web/app.js +847 -66
- package/web/icons.js +2 -2
- package/web/index.html +110 -30
- package/web/menus.js +7 -7
- package/web/release-notes.js +1 -1
- package/web/table.js +1 -1
- package/web/viz.js +122 -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"]);
|
|
@@ -105,6 +143,150 @@ function actorFromColumns(kind, id, session) {
|
|
|
105
143
|
a.session = session;
|
|
106
144
|
return a;
|
|
107
145
|
}
|
|
146
|
+
// packages/core/src/adapters/aider/history.ts
|
|
147
|
+
var djb2 = (s) => {
|
|
148
|
+
let h = 5381;
|
|
149
|
+
for (let i = 0;i < s.length; i++)
|
|
150
|
+
h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
|
|
151
|
+
return h.toString(36);
|
|
152
|
+
};
|
|
153
|
+
var toks = (s) => {
|
|
154
|
+
if (!s)
|
|
155
|
+
return 0;
|
|
156
|
+
const n = Number.parseFloat(s.replaceAll(",", ""));
|
|
157
|
+
return Math.round(s.trim().endsWith("k") ? n * 1000 : n);
|
|
158
|
+
};
|
|
159
|
+
var HEADER = /^# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
|
|
160
|
+
var MODEL = /^> Model: (\S+) with /;
|
|
161
|
+
var TOKENS = /^(?:> )?Tokens: ([\d.,]+k?) sent(?:, ([\d.,]+k?) cache write)?(?:, ([\d.,]+k?) cache hit)?, ([\d.,]+k?) received\./;
|
|
162
|
+
var COST = /Cost: \$([\d.]+(?:e-?\d+)?) message/;
|
|
163
|
+
var EDIT = /^> Applied edit to (.+)/;
|
|
164
|
+
var COMMIT = /^> Commit [0-9a-f]{6,}/;
|
|
165
|
+
function parseAiderHistory(chunk, seed, carry) {
|
|
166
|
+
const segments = [];
|
|
167
|
+
let cur = carry ? {
|
|
168
|
+
sessionId: carry.sessionId,
|
|
169
|
+
startMs: carry.startMs,
|
|
170
|
+
model: carry.model,
|
|
171
|
+
title: carry.title,
|
|
172
|
+
turns: [],
|
|
173
|
+
c: { ...carry }
|
|
174
|
+
} : null;
|
|
175
|
+
const closeTurn = (t, cost) => {
|
|
176
|
+
if (!cur)
|
|
177
|
+
return;
|
|
178
|
+
t.cost = cost;
|
|
179
|
+
cur.turns.push(t);
|
|
180
|
+
cur.c.turns++;
|
|
181
|
+
cur.c.text = "";
|
|
182
|
+
cur.c.tools = [];
|
|
183
|
+
cur.c.pending = null;
|
|
184
|
+
};
|
|
185
|
+
const flushPending = () => {
|
|
186
|
+
if (cur?.c.pending)
|
|
187
|
+
closeTurn(cur.c.pending, null);
|
|
188
|
+
};
|
|
189
|
+
for (const line of chunk.split(`
|
|
190
|
+
`)) {
|
|
191
|
+
const h = line.match(HEADER);
|
|
192
|
+
if (h) {
|
|
193
|
+
flushPending();
|
|
194
|
+
if (cur)
|
|
195
|
+
segments.push(cur);
|
|
196
|
+
const stamp = h[1] ?? "";
|
|
197
|
+
const startMs = Date.parse(stamp.replace(" ", "T"));
|
|
198
|
+
const sessionId = `aider-${djb2(`${seed}|${stamp}`)}`;
|
|
199
|
+
cur = {
|
|
200
|
+
sessionId,
|
|
201
|
+
startMs,
|
|
202
|
+
model: null,
|
|
203
|
+
title: null,
|
|
204
|
+
turns: [],
|
|
205
|
+
c: {
|
|
206
|
+
sessionId,
|
|
207
|
+
startMs,
|
|
208
|
+
model: null,
|
|
209
|
+
title: null,
|
|
210
|
+
turns: 0,
|
|
211
|
+
text: "",
|
|
212
|
+
tools: [],
|
|
213
|
+
pending: null
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!cur)
|
|
219
|
+
continue;
|
|
220
|
+
const c = cur.c;
|
|
221
|
+
if (c.pending) {
|
|
222
|
+
const cost = line.match(COST);
|
|
223
|
+
if (cost) {
|
|
224
|
+
closeTurn(c.pending, Number.parseFloat(cost[1] ?? "0"));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
flushPending();
|
|
228
|
+
}
|
|
229
|
+
const m = line.match(MODEL);
|
|
230
|
+
if (m) {
|
|
231
|
+
cur.model = (m[1] ?? "").split("/").pop() || null;
|
|
232
|
+
c.model = cur.model;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const tk = line.match(TOKENS);
|
|
236
|
+
if (tk) {
|
|
237
|
+
const cacheRead = toks(tk[3]);
|
|
238
|
+
const turn = {
|
|
239
|
+
id: `${c.sessionId}-t${c.turns}`,
|
|
240
|
+
ts: new Date(c.startMs + c.turns * 1000).toISOString(),
|
|
241
|
+
model: c.model ?? "aider",
|
|
242
|
+
usage: {
|
|
243
|
+
input: Math.max(0, toks(tk[1]) - cacheRead),
|
|
244
|
+
output: toks(tk[4]),
|
|
245
|
+
cacheWrite: toks(tk[2]),
|
|
246
|
+
cacheWrite1h: 0,
|
|
247
|
+
cacheRead,
|
|
248
|
+
thinking: 0
|
|
249
|
+
},
|
|
250
|
+
text: c.text,
|
|
251
|
+
tools: c.tools,
|
|
252
|
+
effort: null,
|
|
253
|
+
sidechain: false
|
|
254
|
+
};
|
|
255
|
+
const cost = line.match(COST);
|
|
256
|
+
if (cost)
|
|
257
|
+
closeTurn(turn, Number.parseFloat(cost[1] ?? "0"));
|
|
258
|
+
else
|
|
259
|
+
c.pending = turn;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (line.startsWith("#### ")) {
|
|
263
|
+
const t = line.slice(5).trim();
|
|
264
|
+
if (t && !cur.title) {
|
|
265
|
+
cur.title = t.slice(0, 80);
|
|
266
|
+
c.title = cur.title;
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (EDIT.test(line)) {
|
|
271
|
+
c.tools.push("edit");
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (COMMIT.test(line)) {
|
|
275
|
+
c.tools.push("commit");
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (line.startsWith(">"))
|
|
279
|
+
continue;
|
|
280
|
+
if (line.trim() && c.text.length < 400)
|
|
281
|
+
c.text = `${c.text}${c.text ? `
|
|
282
|
+
` : ""}${line}`.slice(0, 400);
|
|
283
|
+
}
|
|
284
|
+
if (cur)
|
|
285
|
+
segments.push(cur);
|
|
286
|
+
const last = cur ? { ...cur.c } : null;
|
|
287
|
+
return { segments, carry: last };
|
|
288
|
+
}
|
|
289
|
+
|
|
108
290
|
// packages/core/src/adapters/claude-code/transcript.ts
|
|
109
291
|
function parseTranscriptChunk(chunk) {
|
|
110
292
|
const out = {
|
|
@@ -385,6 +567,62 @@ function parseGrokUpdates(chunk) {
|
|
|
385
567
|
}
|
|
386
568
|
return out;
|
|
387
569
|
}
|
|
570
|
+
|
|
571
|
+
// packages/core/src/adapters/opencode/db.ts
|
|
572
|
+
var ocModel = (d) => {
|
|
573
|
+
if (typeof d.model === "string")
|
|
574
|
+
return d.model;
|
|
575
|
+
if (d.model && typeof d.model === "object" && typeof d.model.id === "string")
|
|
576
|
+
return d.model.id;
|
|
577
|
+
return typeof d.modelID === "string" ? d.modelID : null;
|
|
578
|
+
};
|
|
579
|
+
var ocTs = (t, fallbackMs) => {
|
|
580
|
+
if (typeof t === "number")
|
|
581
|
+
return new Date(t).toISOString();
|
|
582
|
+
if (typeof t === "string" && !Number.isNaN(Date.parse(t)))
|
|
583
|
+
return new Date(t).toISOString();
|
|
584
|
+
return new Date(fallbackMs).toISOString();
|
|
585
|
+
};
|
|
586
|
+
function opencodeTurn(sessionId, msgId, data, fallbackMs = 0, sidechain = false) {
|
|
587
|
+
let d;
|
|
588
|
+
try {
|
|
589
|
+
d = JSON.parse(data);
|
|
590
|
+
} catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if ((d.type ?? d.role) !== "assistant")
|
|
594
|
+
return null;
|
|
595
|
+
const t = d.tokens ?? {};
|
|
596
|
+
let text = "";
|
|
597
|
+
const tools = [];
|
|
598
|
+
for (const p of Array.isArray(d.content) ? d.content : []) {
|
|
599
|
+
if (p?.type === "text" && typeof p.text === "string" && text.length < 400)
|
|
600
|
+
text = `${text}${p.text}`.slice(0, 400);
|
|
601
|
+
else if (p?.type === "tool") {
|
|
602
|
+
const name = p.tool ?? p.name;
|
|
603
|
+
if (name)
|
|
604
|
+
tools.push(name);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
id: `${sessionId}-${msgId}`,
|
|
609
|
+
ts: ocTs(d.time?.created, fallbackMs),
|
|
610
|
+
model: ocModel(d) ?? "opencode",
|
|
611
|
+
usage: {
|
|
612
|
+
input: t.input ?? 0,
|
|
613
|
+
output: t.output ?? 0,
|
|
614
|
+
cacheWrite: t.cache?.write ?? 0,
|
|
615
|
+
cacheWrite1h: 0,
|
|
616
|
+
cacheRead: t.cache?.read ?? 0,
|
|
617
|
+
thinking: t.reasoning ?? 0
|
|
618
|
+
},
|
|
619
|
+
text,
|
|
620
|
+
tools,
|
|
621
|
+
effort: null,
|
|
622
|
+
sidechain,
|
|
623
|
+
cost: typeof d.cost === "number" && d.cost > 0 ? d.cost : null
|
|
624
|
+
};
|
|
625
|
+
}
|
|
388
626
|
// packages/core/src/adapters/claude-code/hooks.ts
|
|
389
627
|
var HOOK_EVENTS = [
|
|
390
628
|
"SessionStart",
|
|
@@ -491,6 +729,38 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
|
|
|
491
729
|
payload.prompt = raw.prompt;
|
|
492
730
|
return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
|
|
493
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);
|
|
494
764
|
// packages/core/src/audit.ts
|
|
495
765
|
var AUDIT_TYPES = new Set([
|
|
496
766
|
"session.started",
|
|
@@ -636,8 +906,8 @@ function sinceToIso(since, now = Date.now()) {
|
|
|
636
906
|
const m = /^(\d+)([dhm])$/.exec(since.trim());
|
|
637
907
|
if (m) {
|
|
638
908
|
const n = Number(m[1]);
|
|
639
|
-
const
|
|
640
|
-
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();
|
|
641
911
|
}
|
|
642
912
|
const t = Date.parse(since);
|
|
643
913
|
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
@@ -796,6 +1066,9 @@ var DEFAULT_CONFIG = {
|
|
|
796
1066
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
797
1067
|
workflows: {},
|
|
798
1068
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
1069
|
+
models: { allow: [] },
|
|
1070
|
+
notify: { webhook: null },
|
|
1071
|
+
team: { url: null, forward: ["ledger", "cost"], interval: 5 },
|
|
799
1072
|
events: { retain_days: 30 },
|
|
800
1073
|
audit: { retain_days: 0 },
|
|
801
1074
|
privacy: DEFAULT_PRIVACY,
|
|
@@ -891,6 +1164,26 @@ function validate(c) {
|
|
|
891
1164
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
892
1165
|
},
|
|
893
1166
|
workflows: parseWorkflows(c.workflows),
|
|
1167
|
+
notify: {
|
|
1168
|
+
webhook: (() => {
|
|
1169
|
+
const w = c.notify?.webhook;
|
|
1170
|
+
return typeof w === "string" && /^https?:\/\//.test(w.trim()) ? w.trim() : null;
|
|
1171
|
+
})()
|
|
1172
|
+
},
|
|
1173
|
+
models: {
|
|
1174
|
+
allow: Array.isArray(c.models?.allow) ? c.models.allow.filter((m) => typeof m === "string" && m.trim() !== "") : []
|
|
1175
|
+
},
|
|
1176
|
+
team: (() => {
|
|
1177
|
+
const t = c.team ?? {};
|
|
1178
|
+
const url = typeof t.url === "string" && /^https?:\/\//.test(t.url.trim()) ? t.url.trim().replace(/\/+$/, "") : null;
|
|
1179
|
+
const iv = Number(t.interval);
|
|
1180
|
+
const KINDS = ["ledger", "cost", "transcripts"];
|
|
1181
|
+
return {
|
|
1182
|
+
url,
|
|
1183
|
+
forward: Array.isArray(t.forward) ? t.forward.filter((k) => typeof k === "string" && KINDS.includes(k)) : ["ledger", "cost"],
|
|
1184
|
+
interval: Number.isFinite(iv) && iv >= 1 && iv <= 300 ? Math.round(iv) : 5
|
|
1185
|
+
};
|
|
1186
|
+
})(),
|
|
894
1187
|
events: {
|
|
895
1188
|
retain_days: days(c.events?.retain_days, 30)
|
|
896
1189
|
},
|
|
@@ -1008,6 +1301,238 @@ function loadConfigDetailed(opts = {}) {
|
|
|
1008
1301
|
function loadConfig(opts = {}) {
|
|
1009
1302
|
return loadConfigDetailed(opts).config;
|
|
1010
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
|
+
}
|
|
1011
1536
|
// packages/core/src/dispatch.ts
|
|
1012
1537
|
function planDispatch(tasks, wanted, opts) {
|
|
1013
1538
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
@@ -1443,6 +1968,83 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
|
|
|
1443
1968
|
return { title, body: b.join(`
|
|
1444
1969
|
`) };
|
|
1445
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
|
+
}
|
|
1446
2048
|
// packages/core/src/gates.ts
|
|
1447
2049
|
var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
1448
2050
|
function validateGateRun(input) {
|
|
@@ -1502,7 +2104,196 @@ function executedGateInput(task, gate, cmd, outcome) {
|
|
|
1502
2104
|
gate,
|
|
1503
2105
|
verdict: outcome.exitCode === 0 && !outcome.timedOut ? "pass" : "fail",
|
|
1504
2106
|
rubric: `ran \`${cmd}\` \u2014 ${how} in ${(outcome.durationMs / 1000).toFixed(1)}s`,
|
|
1505
|
-
evidence: evidenceTail(outcome.output) || null
|
|
2107
|
+
evidence: evidenceTail(outcome.output) || null,
|
|
2108
|
+
durationMs: outcome.durationMs
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
// packages/core/src/graphs.ts
|
|
2112
|
+
function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
2113
|
+
const files = new Map;
|
|
2114
|
+
const sessions = new Map;
|
|
2115
|
+
for (const r of rows) {
|
|
2116
|
+
if (!r.path || !r.sessionId)
|
|
2117
|
+
continue;
|
|
2118
|
+
const f = files.get(r.path) ?? { readers: new Set, writers: new Set };
|
|
2119
|
+
const s = sessions.get(r.sessionId) ?? { files: new Set, writes: 0 };
|
|
2120
|
+
if (writeTools.has(r.tool)) {
|
|
2121
|
+
f.writers.add(r.sessionId);
|
|
2122
|
+
s.writes++;
|
|
2123
|
+
} else
|
|
2124
|
+
f.readers.add(r.sessionId);
|
|
2125
|
+
s.files.add(r.path);
|
|
2126
|
+
files.set(r.path, f);
|
|
2127
|
+
sessions.set(r.sessionId, s);
|
|
2128
|
+
}
|
|
2129
|
+
const out = [...files.entries()].map(([path, f]) => {
|
|
2130
|
+
const writers = [...f.writers].sort();
|
|
2131
|
+
const readers = [...f.readers].filter((id) => !f.writers.has(id)).sort();
|
|
2132
|
+
const touchers = writers.length + readers.length;
|
|
2133
|
+
return { path, readers, writers, contested: touchers >= 2 && writers.length >= 1 };
|
|
2134
|
+
});
|
|
2135
|
+
out.sort((a, b) => Number(b.contested) - Number(a.contested) || b.readers.length + b.writers.length - (a.readers.length + a.writers.length) || a.path.localeCompare(b.path));
|
|
2136
|
+
return {
|
|
2137
|
+
sessions: [...sessions.entries()].map(([id, s]) => ({
|
|
2138
|
+
id,
|
|
2139
|
+
files: s.files.size,
|
|
2140
|
+
writes: s.writes
|
|
2141
|
+
})),
|
|
2142
|
+
files: out,
|
|
2143
|
+
contested: out.filter((f) => f.contested).length
|
|
2144
|
+
};
|
|
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
|
+
}
|
|
1506
2297
|
};
|
|
1507
2298
|
}
|
|
1508
2299
|
// packages/core/src/ledger.ts
|
|
@@ -1712,6 +2503,250 @@ function incidentKey(inc) {
|
|
|
1712
2503
|
return `protected_ports:${portsIn(inc.command).join(",")}`;
|
|
1713
2504
|
return inc.rule;
|
|
1714
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
|
+
}
|
|
1715
2750
|
// packages/core/src/memory.ts
|
|
1716
2751
|
var MEMORY_KINDS = ["handoff", "incident", "gate", "session"];
|
|
1717
2752
|
function handoffDoc(projectId, id, h, sessionId) {
|
|
@@ -1829,15 +2864,101 @@ function parseTo(to) {
|
|
|
1829
2864
|
return { kind: "session", id: t };
|
|
1830
2865
|
return { kind: "task", task: t };
|
|
1831
2866
|
}
|
|
1832
|
-
function formatMessages(
|
|
1833
|
-
if (!
|
|
2867
|
+
function formatMessages(ms3) {
|
|
2868
|
+
if (!ms3.length)
|
|
1834
2869
|
return null;
|
|
1835
|
-
const lines =
|
|
1836
|
-
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:
|
|
1837
2872
|
${lines.join(`
|
|
1838
2873
|
`)}
|
|
1839
2874
|
Reply with swarm_send if a reply is expected.`;
|
|
1840
2875
|
}
|
|
2876
|
+
// packages/core/src/outcomes.ts
|
|
2877
|
+
var DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
|
|
2878
|
+
var median = (xs) => {
|
|
2879
|
+
if (!xs.length)
|
|
2880
|
+
return null;
|
|
2881
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
2882
|
+
const mid = Math.floor(s.length / 2);
|
|
2883
|
+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
2884
|
+
};
|
|
2885
|
+
function scorecard(key, rows) {
|
|
2886
|
+
const merged = rows.filter((r) => r.outcome === "merged");
|
|
2887
|
+
const reverted = rows.filter((r) => r.outcome === "reverted");
|
|
2888
|
+
const open = rows.filter((r) => r.outcome === "open");
|
|
2889
|
+
const noPr = rows.filter((r) => r.outcome === "no-pr");
|
|
2890
|
+
const finished = merged.length + reverted.length + noPr.length;
|
|
2891
|
+
const mergedCost = merged.reduce((a, r) => a + r.costUsd, 0);
|
|
2892
|
+
return {
|
|
2893
|
+
key,
|
|
2894
|
+
branches: rows.length,
|
|
2895
|
+
merged: merged.length,
|
|
2896
|
+
reverted: reverted.length,
|
|
2897
|
+
open: open.length,
|
|
2898
|
+
noPr: noPr.length,
|
|
2899
|
+
mergeRate: finished ? merged.length / finished : null,
|
|
2900
|
+
medianLeadHours: median(merged.map((r) => r.leadHours).filter((x) => x != null)),
|
|
2901
|
+
costPerMerge: merged.length ? mergedCost / merged.length : null
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function outcomeReport(sessions, prs, revertedShas) {
|
|
2905
|
+
const byBranch = new Map;
|
|
2906
|
+
for (const s of sessions) {
|
|
2907
|
+
if (!s.branch || DEFAULT_BRANCHES.has(s.branch))
|
|
2908
|
+
continue;
|
|
2909
|
+
const a = byBranch.get(s.branch) ?? [];
|
|
2910
|
+
a.push(s);
|
|
2911
|
+
byBranch.set(s.branch, a);
|
|
2912
|
+
}
|
|
2913
|
+
const prByBranch = new Map;
|
|
2914
|
+
for (const pr of prs) {
|
|
2915
|
+
const prev = prByBranch.get(pr.branch);
|
|
2916
|
+
if (!prev || pr.state === "merged" && prev.state !== "merged" || pr.state === prev.state && pr.number > prev.number)
|
|
2917
|
+
prByBranch.set(pr.branch, pr);
|
|
2918
|
+
}
|
|
2919
|
+
const rows = [...byBranch.entries()].map(([branch, ss]) => {
|
|
2920
|
+
const dominant = [...ss].sort((a, b) => (b.costUsd ?? 0) - (a.costUsd ?? 0) || a.startedAt.localeCompare(b.startedAt))[0];
|
|
2921
|
+
const pr = prByBranch.get(branch) ?? null;
|
|
2922
|
+
const wasReverted = (sha) => {
|
|
2923
|
+
if (!sha)
|
|
2924
|
+
return false;
|
|
2925
|
+
const s = sha.toLowerCase();
|
|
2926
|
+
for (const r of revertedShas)
|
|
2927
|
+
if (s.startsWith(r) || r.startsWith(s))
|
|
2928
|
+
return true;
|
|
2929
|
+
return false;
|
|
2930
|
+
};
|
|
2931
|
+
const outcome = !pr ? "no-pr" : pr.state === "open" ? "open" : wasReverted(pr.mergeSha) ? "reverted" : "merged";
|
|
2932
|
+
const firstStart = ss.map((s) => s.startedAt).sort()[0];
|
|
2933
|
+
const leadHours = outcome === "merged" && pr?.mergedAt ? Math.max(0, (new Date(pr.mergedAt).getTime() - new Date(firstStart).getTime()) / 3600000) : null;
|
|
2934
|
+
return {
|
|
2935
|
+
branch,
|
|
2936
|
+
outcome,
|
|
2937
|
+
prNumber: pr?.number ?? null,
|
|
2938
|
+
title: pr?.title ?? null,
|
|
2939
|
+
url: pr?.url ?? null,
|
|
2940
|
+
mergedAt: pr?.mergedAt ?? null,
|
|
2941
|
+
leadHours,
|
|
2942
|
+
sessions: ss.map((s) => s.id),
|
|
2943
|
+
model: dominant.model,
|
|
2944
|
+
agent: dominant.agent,
|
|
2945
|
+
costUsd: ss.reduce((a, s) => a + (s.costUsd ?? 0), 0)
|
|
2946
|
+
};
|
|
2947
|
+
});
|
|
2948
|
+
rows.sort((a, b) => (b.mergedAt ?? "").localeCompare(a.mergedAt ?? "") || a.branch.localeCompare(b.branch));
|
|
2949
|
+
const group = (key) => {
|
|
2950
|
+
const m = new Map;
|
|
2951
|
+
for (const r of rows) {
|
|
2952
|
+
const k = key(r) ?? "unknown";
|
|
2953
|
+
m.set(k, [...m.get(k) ?? [], r]);
|
|
2954
|
+
}
|
|
2955
|
+
return [...m.entries()].map(([k, rs]) => scorecard(k, rs)).sort((a, b) => b.branches - a.branches);
|
|
2956
|
+
};
|
|
2957
|
+
return { branches: rows, byModel: group((r) => r.model), byAgent: group((r) => r.agent) };
|
|
2958
|
+
}
|
|
2959
|
+
function parseReverts(gitLog) {
|
|
2960
|
+
return new Set([...gitLog.matchAll(/This reverts commit ([0-9a-f]{7,40})/gi)].map((m) => m[1].toLowerCase()));
|
|
2961
|
+
}
|
|
1841
2962
|
// packages/core/src/policy.ts
|
|
1842
2963
|
import { createHash } from "crypto";
|
|
1843
2964
|
var HOOK_MARK = "swarm-hook";
|
|
@@ -2022,6 +3143,147 @@ function projectIdentity(opts) {
|
|
|
2022
3143
|
const name = parts[parts.length - 1] ?? opts.root;
|
|
2023
3144
|
return { id: `p_${fnv1a(key)}`, root: opts.root, commonDir: opts.commonDir, name };
|
|
2024
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
|
+
}
|
|
2025
3287
|
// packages/core/src/questions.ts
|
|
2026
3288
|
function validateQuestion(text, options) {
|
|
2027
3289
|
const t = typeof text === "string" ? text.trim() : "";
|
|
@@ -2178,6 +3440,49 @@ ${lines.join(`
|
|
|
2178
3440
|
`)}` : ""}`
|
|
2179
3441
|
};
|
|
2180
3442
|
}
|
|
3443
|
+
// packages/core/src/stall.ts
|
|
3444
|
+
var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
|
|
3445
|
+
function toolResponseErrored(resp) {
|
|
3446
|
+
if (typeof resp === "string")
|
|
3447
|
+
return /^\s*error[:\s]/i.test(resp);
|
|
3448
|
+
if (!resp || typeof resp !== "object")
|
|
3449
|
+
return false;
|
|
3450
|
+
const r = resp;
|
|
3451
|
+
if (r.is_error === true || r.isError === true)
|
|
3452
|
+
return true;
|
|
3453
|
+
if (r.success === false)
|
|
3454
|
+
return true;
|
|
3455
|
+
if (r.interrupted === true)
|
|
3456
|
+
return true;
|
|
3457
|
+
if (typeof r.error === "string" && r.error.length > 0)
|
|
3458
|
+
return true;
|
|
3459
|
+
return false;
|
|
3460
|
+
}
|
|
3461
|
+
function detectStall(calls, opts = {}) {
|
|
3462
|
+
const o = { ...STALL_DEFAULTS, ...opts };
|
|
3463
|
+
const tail = calls.slice(-o.window);
|
|
3464
|
+
const last = tail.at(-1);
|
|
3465
|
+
if (!last)
|
|
3466
|
+
return null;
|
|
3467
|
+
let run = 0;
|
|
3468
|
+
let runErrors = 0;
|
|
3469
|
+
for (let i = tail.length - 1;i >= 0; i--) {
|
|
3470
|
+
const c = tail[i];
|
|
3471
|
+
if (!c || c.tool !== last.tool || c.input !== last.input)
|
|
3472
|
+
break;
|
|
3473
|
+
run++;
|
|
3474
|
+
if (c.errored)
|
|
3475
|
+
runErrors++;
|
|
3476
|
+
}
|
|
3477
|
+
if (run >= o.repeat && runErrors >= o.repeatErrors)
|
|
3478
|
+
return { kind: "repeat", reason: `repeating a failing ${last.tool} call \xD7${run}` };
|
|
3479
|
+
let streak = 0;
|
|
3480
|
+
for (let i = tail.length - 1;i >= 0 && tail[i]?.errored; i--)
|
|
3481
|
+
streak++;
|
|
3482
|
+
if (streak >= o.errors)
|
|
3483
|
+
return { kind: "errors", reason: `${streak} tool calls failing in a row` };
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
2181
3486
|
// packages/core/src/tasks.ts
|
|
2182
3487
|
var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
|
|
2183
3488
|
var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
|
|
@@ -2337,88 +3642,157 @@ function linearIssuesQuery(teamKey, first = 200) {
|
|
|
2337
3642
|
inverseRelations { nodes { type issue { identifier } } }
|
|
2338
3643
|
} } }`;
|
|
2339
3644
|
}
|
|
2340
|
-
// packages/core/src/
|
|
2341
|
-
import {
|
|
2342
|
-
function
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
if (seen.has(rel))
|
|
2350
|
-
continue;
|
|
2351
|
-
seen.add(rel);
|
|
2352
|
-
copies.push({ rel, from: join3(repoRoot, rel), to: join3(worktree, rel) });
|
|
2353
|
-
}
|
|
2354
|
-
return { copies, setup: cfg.worktree.setup };
|
|
2355
|
-
}
|
|
2356
|
-
var needsBootstrap = (plan) => plan.copies.length > 0 || plan.setup !== null;
|
|
2357
|
-
function summarizeBootstrap(o) {
|
|
2358
|
-
const parts = [];
|
|
2359
|
-
if (o.copied.length)
|
|
2360
|
-
parts.push(`copied ${o.copied.join(", ")}`);
|
|
2361
|
-
if (o.skipped.length)
|
|
2362
|
-
parts.push(`skipped ${o.skipped.join(", ")} (missing)`);
|
|
2363
|
-
if (o.setup)
|
|
2364
|
-
parts.push(`${o.setup.command} \u2192 ${o.setup.exitCode === 0 ? "ok" : `exit ${o.setup.exitCode}`} in ${(o.setup.durationMs / 1000).toFixed(1)}s`);
|
|
2365
|
-
return parts.join("; ") || "nothing to do";
|
|
2366
|
-
}
|
|
2367
|
-
function canRemoveWorktree(w, heldByClaim, force) {
|
|
2368
|
-
if (w.main)
|
|
2369
|
-
return { ok: false, reason: "main" };
|
|
2370
|
-
if (heldByClaim)
|
|
2371
|
-
return { ok: false, reason: "held" };
|
|
2372
|
-
if (force)
|
|
2373
|
-
return { ok: true };
|
|
2374
|
-
if (w.dirty > 0)
|
|
2375
|
-
return { ok: false, reason: "dirty" };
|
|
2376
|
-
if (w.ahead > 0)
|
|
2377
|
-
return { ok: false, reason: "unpushed" };
|
|
2378
|
-
return { ok: true };
|
|
3645
|
+
// packages/core/src/team.ts
|
|
3646
|
+
import { createPublicKey, verify as nodeVerify } from "crypto";
|
|
3647
|
+
function modelAllowed(model, allow) {
|
|
3648
|
+
if (!allow.length)
|
|
3649
|
+
return true;
|
|
3650
|
+
return allow.some((g) => {
|
|
3651
|
+
const re = new RegExp(`^${g.trim().split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`, "i");
|
|
3652
|
+
return re.test(model);
|
|
3653
|
+
});
|
|
2379
3654
|
}
|
|
2380
|
-
function
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
return `${path} is held by claim ${task ?? "?"} \u2014 release the claim instead`;
|
|
2386
|
-
case "dirty":
|
|
2387
|
-
return `${path} has uncommitted changes \u2014 commit or stash them, or --force to discard`;
|
|
2388
|
-
case "unpushed":
|
|
2389
|
-
return `${path} has unpushed commits \u2014 push them, or --force to discard`;
|
|
3655
|
+
function verifyPolicySignature(toml, signatureB64, publicKeyB64) {
|
|
3656
|
+
try {
|
|
3657
|
+
return nodeVerify(null, Buffer.from(toml), createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" }), Buffer.from(signatureB64, "base64"));
|
|
3658
|
+
} catch {
|
|
3659
|
+
return false;
|
|
2390
3660
|
}
|
|
2391
3661
|
}
|
|
2392
|
-
function
|
|
2393
|
-
|
|
2394
|
-
|
|
3662
|
+
function clusterProjectKey(remoteUrl) {
|
|
3663
|
+
if (!remoteUrl)
|
|
3664
|
+
return null;
|
|
3665
|
+
const url = remoteUrl.trim();
|
|
3666
|
+
const m = url.match(/^https?:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/) ?? url.match(/^(?:ssh:\/\/)?(?:[^@/]+@)?([^:/]+)[:/](.+?)(?:\.git)?\/?$/);
|
|
3667
|
+
if (!m?.[1] || !m[2])
|
|
3668
|
+
return null;
|
|
3669
|
+
const host = m[1].toLowerCase();
|
|
3670
|
+
if (host.includes(" ") || !host.includes("."))
|
|
3671
|
+
return null;
|
|
3672
|
+
return `${host}/${m[2]}`;
|
|
3673
|
+
}
|
|
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);
|
|
3687
|
+
}
|
|
3688
|
+
function pairWaits(samples, now, sessionEnds = {}) {
|
|
3689
|
+
const nowMs = ms3(now);
|
|
3690
|
+
const open = new Map;
|
|
2395
3691
|
const out = [];
|
|
2396
|
-
|
|
2397
|
-
|
|
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);
|
|
2398
3700
|
continue;
|
|
2399
|
-
|
|
2400
|
-
|
|
3701
|
+
}
|
|
3702
|
+
const start = open.get(k);
|
|
3703
|
+
if (!start)
|
|
2401
3704
|
continue;
|
|
2402
|
-
|
|
3705
|
+
open.delete(k);
|
|
2403
3706
|
out.push({
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
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
|
|
2409
3716
|
});
|
|
2410
3717
|
}
|
|
2411
|
-
|
|
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
|
+
};
|
|
2412
3786
|
}
|
|
2413
3787
|
// packages/daemon/src/app.ts
|
|
2414
|
-
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";
|
|
2415
3789
|
import { homedir as homedir4 } from "os";
|
|
2416
|
-
import { dirname as dirname4, join as
|
|
3790
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
2417
3791
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2418
3792
|
|
|
2419
3793
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
2420
3794
|
var compose = (middleware, onError, onNotFound) => {
|
|
2421
|
-
return (
|
|
3795
|
+
return (context2, next) => {
|
|
2422
3796
|
let index = -1;
|
|
2423
3797
|
return dispatch2(0);
|
|
2424
3798
|
async function dispatch2(i) {
|
|
@@ -2431,31 +3805,31 @@ var compose = (middleware, onError, onNotFound) => {
|
|
|
2431
3805
|
let handler;
|
|
2432
3806
|
if (middleware[i]) {
|
|
2433
3807
|
handler = middleware[i][0][0];
|
|
2434
|
-
|
|
3808
|
+
context2.req.routeIndex = i;
|
|
2435
3809
|
} else {
|
|
2436
3810
|
handler = i === middleware.length && next || undefined;
|
|
2437
3811
|
}
|
|
2438
3812
|
if (handler) {
|
|
2439
3813
|
try {
|
|
2440
|
-
res = await handler(
|
|
3814
|
+
res = await handler(context2, () => dispatch2(i + 1));
|
|
2441
3815
|
} catch (err) {
|
|
2442
3816
|
if (err instanceof Error && onError) {
|
|
2443
|
-
|
|
2444
|
-
res = await onError(err,
|
|
3817
|
+
context2.error = err;
|
|
3818
|
+
res = await onError(err, context2);
|
|
2445
3819
|
isError = true;
|
|
2446
3820
|
} else {
|
|
2447
3821
|
throw err;
|
|
2448
3822
|
}
|
|
2449
3823
|
}
|
|
2450
3824
|
} else {
|
|
2451
|
-
if (
|
|
2452
|
-
res = await onNotFound(
|
|
3825
|
+
if (context2.finalized === false && onNotFound) {
|
|
3826
|
+
res = await onNotFound(context2);
|
|
2453
3827
|
}
|
|
2454
3828
|
}
|
|
2455
|
-
if (res && (
|
|
2456
|
-
|
|
3829
|
+
if (res && (context2.finalized === false || isError)) {
|
|
3830
|
+
context2.res = res;
|
|
2457
3831
|
}
|
|
2458
|
-
return
|
|
3832
|
+
return context2;
|
|
2459
3833
|
}
|
|
2460
3834
|
};
|
|
2461
3835
|
};
|
|
@@ -2876,7 +4250,7 @@ var raw = (value, callbacks) => {
|
|
|
2876
4250
|
escapedString.callbacks = callbacks;
|
|
2877
4251
|
return escapedString;
|
|
2878
4252
|
};
|
|
2879
|
-
var resolveCallback = async (str, phase, preserveCallbacks,
|
|
4253
|
+
var resolveCallback = async (str, phase, preserveCallbacks, context2, buffer) => {
|
|
2880
4254
|
if (typeof str === "object" && !(str instanceof String)) {
|
|
2881
4255
|
if (!(str instanceof Promise)) {
|
|
2882
4256
|
str = str.toString();
|
|
@@ -2894,7 +4268,7 @@ var resolveCallback = async (str, phase, preserveCallbacks, context, buffer) =>
|
|
|
2894
4268
|
} else {
|
|
2895
4269
|
buffer = [str];
|
|
2896
4270
|
}
|
|
2897
|
-
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]));
|
|
2898
4272
|
if (preserveCallbacks) {
|
|
2899
4273
|
return raw(await resStr, callbacks);
|
|
2900
4274
|
} else {
|
|
@@ -3293,11 +4667,11 @@ var Hono = class _Hono {
|
|
|
3293
4667
|
const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler);
|
|
3294
4668
|
return (async () => {
|
|
3295
4669
|
try {
|
|
3296
|
-
const
|
|
3297
|
-
if (!
|
|
4670
|
+
const context2 = await composed(c);
|
|
4671
|
+
if (!context2.finalized) {
|
|
3298
4672
|
throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");
|
|
3299
4673
|
}
|
|
3300
|
-
return
|
|
4674
|
+
return context2.res;
|
|
3301
4675
|
} catch (err) {
|
|
3302
4676
|
return this.#handleError(err, c);
|
|
3303
4677
|
}
|
|
@@ -3370,7 +4744,7 @@ var Node = class _Node {
|
|
|
3370
4744
|
#index;
|
|
3371
4745
|
#varIndex;
|
|
3372
4746
|
#children = /* @__PURE__ */ Object.create(null);
|
|
3373
|
-
insert(tokens, index, paramMap,
|
|
4747
|
+
insert(tokens, index, paramMap, context2, isStatic) {
|
|
3374
4748
|
let node = this;
|
|
3375
4749
|
for (let i = 0, len = tokens.length;i < len; i++) {
|
|
3376
4750
|
const token = tokens[i];
|
|
@@ -3403,7 +4777,7 @@ var Node = class _Node {
|
|
|
3403
4777
|
nextNode = node.#children[regexpStr] = new _Node;
|
|
3404
4778
|
}
|
|
3405
4779
|
if (name !== "") {
|
|
3406
|
-
nextNode.#varIndex ??=
|
|
4780
|
+
nextNode.#varIndex ??= context2.varIndex++;
|
|
3407
4781
|
paramMap.push([name, nextNode.#varIndex]);
|
|
3408
4782
|
}
|
|
3409
4783
|
} else {
|
|
@@ -3928,8 +5302,8 @@ var StreamingApi = class {
|
|
|
3928
5302
|
`);
|
|
3929
5303
|
return this;
|
|
3930
5304
|
}
|
|
3931
|
-
sleep(
|
|
3932
|
-
return new Promise((res) => setTimeout(res,
|
|
5305
|
+
sleep(ms4) {
|
|
5306
|
+
return new Promise((res) => setTimeout(res, ms4));
|
|
3933
5307
|
}
|
|
3934
5308
|
async close() {
|
|
3935
5309
|
this.closed = true;
|
|
@@ -4183,6 +5557,7 @@ class Dispatcher {
|
|
|
4183
5557
|
task: t.id,
|
|
4184
5558
|
runId: r.run.id,
|
|
4185
5559
|
worktree: r.run.worktree,
|
|
5560
|
+
by: this.opts.get(projectId)?.owner ?? null,
|
|
4186
5561
|
summary: `dispatch ${t.id} \u2192 run ${r.run.id}`
|
|
4187
5562
|
}
|
|
4188
5563
|
});
|
|
@@ -4343,6 +5718,64 @@ class ForgeService {
|
|
|
4343
5718
|
}
|
|
4344
5719
|
}));
|
|
4345
5720
|
}
|
|
5721
|
+
outcomeCache = new Map;
|
|
5722
|
+
async merged(projectId, root) {
|
|
5723
|
+
const hit = this.outcomeCache.get(projectId);
|
|
5724
|
+
if (hit && Date.now() - hit.at < 600000)
|
|
5725
|
+
return hit;
|
|
5726
|
+
let merged = [];
|
|
5727
|
+
const remote = this.remote(root);
|
|
5728
|
+
if (remote?.forge === "github") {
|
|
5729
|
+
const out = await this.run([
|
|
5730
|
+
"gh",
|
|
5731
|
+
"pr",
|
|
5732
|
+
"list",
|
|
5733
|
+
"--state",
|
|
5734
|
+
"merged",
|
|
5735
|
+
"--limit",
|
|
5736
|
+
"200",
|
|
5737
|
+
"--json",
|
|
5738
|
+
"number,title,headRefName,url,createdAt,mergedAt,mergeCommit"
|
|
5739
|
+
], root);
|
|
5740
|
+
if (out)
|
|
5741
|
+
merged = JSON.parse(out).map((r) => ({
|
|
5742
|
+
branch: String(r.headRefName ?? ""),
|
|
5743
|
+
number: Number(r.number ?? 0),
|
|
5744
|
+
title: String(r.title ?? ""),
|
|
5745
|
+
url: String(r.url ?? ""),
|
|
5746
|
+
createdAt: r.createdAt ?? null,
|
|
5747
|
+
mergedAt: r.mergedAt ?? null,
|
|
5748
|
+
mergeSha: (r.mergeCommit?.oid ?? null)?.toLowerCase() ?? null
|
|
5749
|
+
}));
|
|
5750
|
+
} else if (remote?.forge === "gitlab") {
|
|
5751
|
+
const out = await this.run(["glab", "mr", "list", "--merged", "--output", "json"], root);
|
|
5752
|
+
if (out)
|
|
5753
|
+
merged = JSON.parse(out).map((r) => ({
|
|
5754
|
+
branch: String(r.source_branch ?? ""),
|
|
5755
|
+
number: Number(r.iid ?? 0),
|
|
5756
|
+
title: String(r.title ?? ""),
|
|
5757
|
+
url: String(r.web_url ?? ""),
|
|
5758
|
+
createdAt: r.created_at ?? null,
|
|
5759
|
+
mergedAt: r.merged_at ?? null,
|
|
5760
|
+
mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
|
|
5761
|
+
}));
|
|
5762
|
+
}
|
|
5763
|
+
const log = Bun.spawnSync([
|
|
5764
|
+
"git",
|
|
5765
|
+
"-C",
|
|
5766
|
+
root,
|
|
5767
|
+
"log",
|
|
5768
|
+
"--grep",
|
|
5769
|
+
"This reverts commit",
|
|
5770
|
+
"--format=%B",
|
|
5771
|
+
"-n",
|
|
5772
|
+
"300"
|
|
5773
|
+
]);
|
|
5774
|
+
const reverted = log.exitCode === 0 ? [...parseReverts(new TextDecoder().decode(log.stdout))] : [];
|
|
5775
|
+
const entry = { at: Date.now(), merged, reverted };
|
|
5776
|
+
this.outcomeCache.set(projectId, entry);
|
|
5777
|
+
return entry;
|
|
5778
|
+
}
|
|
4346
5779
|
remote(root) {
|
|
4347
5780
|
const r = Bun.spawnSync(["git", "-C", root, "remote", "get-url", "origin"]);
|
|
4348
5781
|
if (r.exitCode !== 0)
|
|
@@ -4575,6 +6008,16 @@ function currentBranch(cwd) {
|
|
|
4575
6008
|
branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
|
|
4576
6009
|
return branchCache.get(cwd)?.v ?? null;
|
|
4577
6010
|
}
|
|
6011
|
+
var originCache = new Map;
|
|
6012
|
+
function originUrl(root) {
|
|
6013
|
+
const hit = originCache.get(root);
|
|
6014
|
+
const now = Date.now();
|
|
6015
|
+
if (hit && now - hit.t < 300000)
|
|
6016
|
+
return hit.v;
|
|
6017
|
+
const v = git(root, ["config", "--get", "remote.origin.url"])?.trim() || null;
|
|
6018
|
+
originCache.set(root, { v, t: now });
|
|
6019
|
+
return v;
|
|
6020
|
+
}
|
|
4578
6021
|
function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
|
|
4579
6022
|
const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
|
|
4580
6023
|
const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
|
|
@@ -4706,6 +6149,14 @@ class Runner {
|
|
|
4706
6149
|
return { ok: false, reason: "unknown project" };
|
|
4707
6150
|
if (!input.prompt.trim())
|
|
4708
6151
|
return { ok: false, reason: "prompt is required" };
|
|
6152
|
+
if (input.model) {
|
|
6153
|
+
const allow = this.store.config(input.projectId).models.allow;
|
|
6154
|
+
if (!modelAllowed(input.model, allow))
|
|
6155
|
+
return {
|
|
6156
|
+
ok: false,
|
|
6157
|
+
reason: `model "${input.model}" is not in [models] allow (${allow.join(", ")})`
|
|
6158
|
+
};
|
|
6159
|
+
}
|
|
4709
6160
|
if (input.permissionMode && !PERMISSION_MODES.includes(input.permissionMode))
|
|
4710
6161
|
return { ok: false, reason: `permission mode must be one of ${PERMISSION_MODES.join(", ")}` };
|
|
4711
6162
|
if (this.get(input.task)?.projectId === input.projectId)
|
|
@@ -5004,6 +6455,7 @@ class Runner {
|
|
|
5004
6455
|
import { Database } from "bun:sqlite";
|
|
5005
6456
|
import {
|
|
5006
6457
|
closeSync,
|
|
6458
|
+
copyFileSync,
|
|
5007
6459
|
existsSync as existsSync6,
|
|
5008
6460
|
mkdirSync as mkdirSync4,
|
|
5009
6461
|
openSync as openSync3,
|
|
@@ -5016,7 +6468,7 @@ import {
|
|
|
5016
6468
|
unlinkSync,
|
|
5017
6469
|
writeFileSync as writeFileSync2
|
|
5018
6470
|
} from "fs";
|
|
5019
|
-
import { homedir as homedir3, tmpdir, userInfo } from "os";
|
|
6471
|
+
import { homedir as homedir3, hostname, tmpdir, userInfo } from "os";
|
|
5020
6472
|
import { basename, dirname as dirname3, join as join8 } from "path";
|
|
5021
6473
|
|
|
5022
6474
|
// packages/daemon/src/bootstrap.ts
|
|
@@ -5126,14 +6578,14 @@ class TaskSources {
|
|
|
5126
6578
|
`)[0] ?? code}`);
|
|
5127
6579
|
return normalizeGithubIssues(JSON.parse(out));
|
|
5128
6580
|
}
|
|
5129
|
-
async linear(
|
|
6581
|
+
async linear(team2) {
|
|
5130
6582
|
const key = this.env.LINEAR_API_KEY;
|
|
5131
6583
|
if (!key)
|
|
5132
6584
|
throw new Error("LINEAR_API_KEY not set \u2014 export it in the environment swarmd starts from (never stored)");
|
|
5133
6585
|
const r = await fetch("https://api.linear.app/graphql", {
|
|
5134
6586
|
method: "POST",
|
|
5135
6587
|
headers: { "content-type": "application/json", authorization: key },
|
|
5136
|
-
body: JSON.stringify({ query: linearIssuesQuery(
|
|
6588
|
+
body: JSON.stringify({ query: linearIssuesQuery(team2) })
|
|
5137
6589
|
});
|
|
5138
6590
|
if (!r.ok)
|
|
5139
6591
|
throw new Error(`Linear API ${r.status}`);
|
|
@@ -5159,11 +6611,12 @@ CREATE INDEX IF NOT EXISTS events_type_seq ON events(type, seq);
|
|
|
5159
6611
|
CREATE TABLE IF NOT EXISTS turns (
|
|
5160
6612
|
id TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, ts TEXT, model TEXT, effort TEXT, sidechain INTEGER,
|
|
5161
6613
|
input INTEGER, output INTEGER, cache_write INTEGER, cache_write_1h INTEGER, cache_read INTEGER, thinking INTEGER,
|
|
5162
|
-
cost_usd REAL, text TEXT, tools TEXT
|
|
6614
|
+
cost_usd REAL, cost_fixed INTEGER DEFAULT 0, text TEXT, tools TEXT
|
|
5163
6615
|
);
|
|
5164
6616
|
CREATE INDEX IF NOT EXISTS turns_session ON turns(session_id, ts);
|
|
5165
6617
|
CREATE INDEX IF NOT EXISTS turns_ts ON turns(ts);
|
|
5166
6618
|
CREATE TABLE IF NOT EXISTS tails (path TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, offset INTEGER);
|
|
6619
|
+
CREATE TABLE IF NOT EXISTS outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, payload TEXT, created_at TEXT);
|
|
5167
6620
|
CREATE TABLE IF NOT EXISTS resources (
|
|
5168
6621
|
name TEXT, project_id TEXT, kind TEXT, owner TEXT, session_id TEXT,
|
|
5169
6622
|
pid INTEGER, port INTEGER, acquired_at TEXT, expires_at TEXT, released INTEGER DEFAULT 0,
|
|
@@ -5182,7 +6635,7 @@ CREATE TABLE IF NOT EXISTS processes (
|
|
|
5182
6635
|
CREATE INDEX IF NOT EXISTS processes_live ON processes(ended_at, project_id);
|
|
5183
6636
|
CREATE TABLE IF NOT EXISTS gates (
|
|
5184
6637
|
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, gate TEXT, verdict TEXT,
|
|
5185
|
-
rubric TEXT, evidence TEXT, session_id TEXT, created_at TEXT
|
|
6638
|
+
rubric TEXT, evidence TEXT, session_id TEXT, duration_ms INTEGER, created_at TEXT
|
|
5186
6639
|
);
|
|
5187
6640
|
CREATE INDEX IF NOT EXISTS gates_task ON gates(project_id, task, created_at);
|
|
5188
6641
|
CREATE TABLE IF NOT EXISTS handoffs (
|
|
@@ -5229,6 +6682,8 @@ class Store {
|
|
|
5229
6682
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
|
|
5230
6683
|
this.db.exec(SCHEMA);
|
|
5231
6684
|
this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
|
|
6685
|
+
this.ensureColumn("claims", "team_state", "TEXT");
|
|
6686
|
+
this.ensureColumn("turns", "cost_fixed", "INTEGER DEFAULT 0");
|
|
5232
6687
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
5233
6688
|
this.ensureColumn("projects", "icon", "TEXT");
|
|
5234
6689
|
this.ensureColumn("projects", "color", "TEXT");
|
|
@@ -5254,6 +6709,12 @@ class Store {
|
|
|
5254
6709
|
setMeta(key, value) {
|
|
5255
6710
|
this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
5256
6711
|
}
|
|
6712
|
+
metaValue(key) {
|
|
6713
|
+
return this.meta(key) || null;
|
|
6714
|
+
}
|
|
6715
|
+
setMetaValue(key, value) {
|
|
6716
|
+
this.setMeta(key, value);
|
|
6717
|
+
}
|
|
5257
6718
|
slimExistingEvents() {
|
|
5258
6719
|
if (this.meta("events_slim") === "1")
|
|
5259
6720
|
return;
|
|
@@ -5317,7 +6778,7 @@ class Store {
|
|
|
5317
6778
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
|
|
5318
6779
|
}
|
|
5319
6780
|
}
|
|
5320
|
-
static SCHEMA_VERSION =
|
|
6781
|
+
static SCHEMA_VERSION = 2;
|
|
5321
6782
|
schemaVersion() {
|
|
5322
6783
|
return Number(this.meta("schema_version") ?? 0);
|
|
5323
6784
|
}
|
|
@@ -5355,6 +6816,16 @@ class Store {
|
|
|
5355
6816
|
fill("incident_acks", "'dashboard'", null, "acks");
|
|
5356
6817
|
fill("sessions", "NULL", "id", "sessions");
|
|
5357
6818
|
fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
|
|
6819
|
+
},
|
|
6820
|
+
(db) => {
|
|
6821
|
+
this.ensureColumn("gates", "duration_ms", "INTEGER");
|
|
6822
|
+
const rows = db.query("SELECT id, rubric FROM gates WHERE duration_ms IS NULL AND rubric LIKE '%s'").all();
|
|
6823
|
+
const upd = db.query("UPDATE gates SET duration_ms = ? WHERE id = ?");
|
|
6824
|
+
for (const r of rows) {
|
|
6825
|
+
const m = /\bin ([0-9]+(?:\.[0-9]+)?)s$/.exec(r.rubric ?? "");
|
|
6826
|
+
if (m)
|
|
6827
|
+
upd.run(Math.round(Number(m[1]) * 1000), r.id);
|
|
6828
|
+
}
|
|
5358
6829
|
}
|
|
5359
6830
|
];
|
|
5360
6831
|
for (let v = this.schemaVersion();v < steps.length; v++) {
|
|
@@ -5862,10 +7333,10 @@ class Store {
|
|
|
5862
7333
|
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
5863
7334
|
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
5864
7335
|
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
5865
|
-
const
|
|
5866
|
-
if (
|
|
5867
|
-
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${
|
|
5868
|
-
return
|
|
7336
|
+
const ms4 = rows.map(rowToMessage);
|
|
7337
|
+
if (ms4.length && !opts.peek)
|
|
7338
|
+
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));
|
|
7339
|
+
return ms4;
|
|
5869
7340
|
}
|
|
5870
7341
|
markMessageDelivered(id, sessionId) {
|
|
5871
7342
|
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);
|
|
@@ -5920,6 +7391,7 @@ class Store {
|
|
|
5920
7391
|
rubric: r.rubric,
|
|
5921
7392
|
evidence: r.evidence ?? null,
|
|
5922
7393
|
sessionId: r.session_id ?? null,
|
|
7394
|
+
durationMs: r.duration_ms ?? null,
|
|
5923
7395
|
createdAt: r.created_at
|
|
5924
7396
|
};
|
|
5925
7397
|
}
|
|
@@ -6070,10 +7542,10 @@ class Store {
|
|
|
6070
7542
|
};
|
|
6071
7543
|
const done = (async () => {
|
|
6072
7544
|
let diffText = "";
|
|
6073
|
-
let
|
|
7545
|
+
let stat2 = "";
|
|
6074
7546
|
try {
|
|
6075
7547
|
const diff = await worktreeDiff(p.root, where.worktree);
|
|
6076
|
-
|
|
7548
|
+
stat2 = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
|
|
6077
7549
|
`);
|
|
6078
7550
|
diffText = await worktreePatch(where.worktree, diff.base);
|
|
6079
7551
|
} catch (e) {
|
|
@@ -6095,7 +7567,7 @@ class Store {
|
|
|
6095
7567
|
task,
|
|
6096
7568
|
title: taskRow?.title ?? null,
|
|
6097
7569
|
branch: w?.branch ?? null,
|
|
6098
|
-
stat,
|
|
7570
|
+
stat: stat2,
|
|
6099
7571
|
patch: diffText
|
|
6100
7572
|
});
|
|
6101
7573
|
writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
|
|
@@ -6253,8 +7725,8 @@ ${err}
|
|
|
6253
7725
|
return v;
|
|
6254
7726
|
const createdAt = new Date().toISOString();
|
|
6255
7727
|
const sessionId = this.knownSession(input.sessionId);
|
|
6256
|
-
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
|
|
6257
|
-
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)));
|
|
7728
|
+
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)
|
|
7729
|
+
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)));
|
|
6258
7730
|
const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
6259
7731
|
this.remember(gateDoc(projectId, run2.id, run2, sessionId));
|
|
6260
7732
|
this.append({
|
|
@@ -6414,6 +7886,18 @@ ${err}
|
|
|
6414
7886
|
};
|
|
6415
7887
|
return { decision: d, display: input.command ?? input.file_path ?? tool };
|
|
6416
7888
|
}
|
|
7889
|
+
const tb = this.teamBudgets().find((x) => x.level === "exceeded" && x.on_exceed === "ask" && (x.scope !== "project" || x.key === this.clusterKeyFor(project.id)));
|
|
7890
|
+
if (tb) {
|
|
7891
|
+
const label = tb.scope === "org" ? "the org" : `${tb.scope} ${tb.key}`;
|
|
7892
|
+
return {
|
|
7893
|
+
decision: {
|
|
7894
|
+
action: "ask",
|
|
7895
|
+
rule: "budget",
|
|
7896
|
+
reason: `team ${tb.kind} budget for ${label} is exceeded ($${tb.spent.toFixed(2)} of $${tb.limit}) \u2014 the team's on_exceed = "ask": confirm each change, or have an admin raise it (POST /t1/budgets)`
|
|
7897
|
+
},
|
|
7898
|
+
display: input.command ?? input.file_path ?? tool
|
|
7899
|
+
};
|
|
7900
|
+
}
|
|
6417
7901
|
}
|
|
6418
7902
|
const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
|
|
6419
7903
|
const cmd = tool === "Bash" ? input.command : undefined;
|
|
@@ -6581,7 +8065,7 @@ ${err}
|
|
|
6581
8065
|
this.reprice();
|
|
6582
8066
|
}
|
|
6583
8067
|
reprice() {
|
|
6584
|
-
const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns").all();
|
|
8068
|
+
const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns WHERE cost_fixed IS NOT 1").all();
|
|
6585
8069
|
const up = this.db.query("UPDATE turns SET cost_usd = ? WHERE id = ?");
|
|
6586
8070
|
const tx = this.db.transaction(() => {
|
|
6587
8071
|
for (const r of rows)
|
|
@@ -6742,12 +8226,69 @@ ${err}
|
|
|
6742
8226
|
if (stored.type === "incident.opened")
|
|
6743
8227
|
this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
|
|
6744
8228
|
this.projectSession(stored);
|
|
8229
|
+
if (stored.type === "incident.opened") {
|
|
8230
|
+
const webhook = this.policyFor(null).config.notify.webhook;
|
|
8231
|
+
if (webhook) {
|
|
8232
|
+
const p2 = stored.payload ?? {};
|
|
8233
|
+
const project = this.project(stored.projectId)?.name ?? stored.projectId;
|
|
8234
|
+
fetch(webhook, {
|
|
8235
|
+
method: "POST",
|
|
8236
|
+
headers: { "content-type": "application/json" },
|
|
8237
|
+
body: JSON.stringify({
|
|
8238
|
+
text: `Swarm incident \xB7 ${p2.rule ?? "?"} \xB7 ${project}
|
|
8239
|
+
${p2.command ?? ""}
|
|
8240
|
+
${p2.reason ?? ""}`.trim(),
|
|
8241
|
+
rule: p2.rule,
|
|
8242
|
+
project,
|
|
8243
|
+
sessionId: stored.sessionId,
|
|
8244
|
+
ts: stored.ts
|
|
8245
|
+
}),
|
|
8246
|
+
signal: AbortSignal.timeout(5000)
|
|
8247
|
+
}).catch(() => {});
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
const team2 = this.policyFor(null).config.team;
|
|
8251
|
+
if (team2.url && team2.forward.includes("ledger") && isAuditType(stored.type)) {
|
|
8252
|
+
this.db.query("INSERT INTO outbox (kind, payload, created_at) VALUES ('event', ?, ?)").run(JSON.stringify({
|
|
8253
|
+
seq: stored.seq,
|
|
8254
|
+
ts: stored.ts,
|
|
8255
|
+
type: stored.type,
|
|
8256
|
+
projectId: stored.projectId,
|
|
8257
|
+
sessionId: stored.sessionId,
|
|
8258
|
+
actor: actor2,
|
|
8259
|
+
payload: slim.payload ?? null
|
|
8260
|
+
}), stored.ts);
|
|
8261
|
+
}
|
|
6745
8262
|
this.touch();
|
|
6746
8263
|
const wire = toWire(stored);
|
|
6747
8264
|
for (const l of this.listeners)
|
|
6748
8265
|
l(wire);
|
|
6749
8266
|
return stored;
|
|
6750
8267
|
}
|
|
8268
|
+
outboxPending(limit = 200) {
|
|
8269
|
+
return this.db.query("SELECT seq, kind, payload FROM outbox ORDER BY seq LIMIT ?").all(limit);
|
|
8270
|
+
}
|
|
8271
|
+
outboxAck(upTo) {
|
|
8272
|
+
this.db.query("DELETE FROM outbox WHERE seq <= ?").run(upTo);
|
|
8273
|
+
}
|
|
8274
|
+
outboxStatus() {
|
|
8275
|
+
const r = this.db.query("SELECT COUNT(*) AS n, MIN(created_at) AS oldest FROM outbox").get();
|
|
8276
|
+
return { pending: r.n, oldest: r.oldest };
|
|
8277
|
+
}
|
|
8278
|
+
machineIdentity() {
|
|
8279
|
+
let id = this.meta("machine_id");
|
|
8280
|
+
if (!id) {
|
|
8281
|
+
id = crypto.randomUUID();
|
|
8282
|
+
this.setMeta("machine_id", id);
|
|
8283
|
+
}
|
|
8284
|
+
return { id, name: hostname() };
|
|
8285
|
+
}
|
|
8286
|
+
spendRollup(day = new Date().toISOString().slice(0, 10)) {
|
|
8287
|
+
return this.db.query(`SELECT s.project_id AS projectId, COALESCE(s.agent, 'claude-code') AS agent, t.model AS model,
|
|
8288
|
+
SUM(t.cost_usd) AS cost, SUM(t.input + t.cache_write + t.cache_read) AS tokensIn, SUM(t.output) AS tokensOut
|
|
8289
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
8290
|
+
WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, agent, t.model`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
|
|
8291
|
+
}
|
|
6751
8292
|
audit(opts = {}) {
|
|
6752
8293
|
const where = [`type IN (${AUDIT_TYPES_SQL})`];
|
|
6753
8294
|
const args = [];
|
|
@@ -6767,9 +8308,9 @@ ${err}
|
|
|
6767
8308
|
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);
|
|
6768
8309
|
return rows.map((r) => auditRow(wireRowToEvent(r)));
|
|
6769
8310
|
}
|
|
6770
|
-
prune(
|
|
8311
|
+
prune(days3) {
|
|
6771
8312
|
const cfg = this.policyFor(null).config;
|
|
6772
|
-
const chatter =
|
|
8313
|
+
const chatter = days3 ?? cfg.events.retain_days;
|
|
6773
8314
|
const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
|
|
6774
8315
|
let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
|
|
6775
8316
|
if (cfg.audit.retain_days > 0) {
|
|
@@ -6875,13 +8416,13 @@ ${err}
|
|
|
6875
8416
|
persistTurns(sessionId, agentId, turns) {
|
|
6876
8417
|
const privacy = this.policyFor(null).config.privacy;
|
|
6877
8418
|
const res = this.redactions();
|
|
6878
|
-
const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
|
|
6879
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8419
|
+
const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, cost_fixed, text, tools)
|
|
8420
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6880
8421
|
ON CONFLICT(id) DO UPDATE SET input=excluded.input, output=excluded.output, cache_write=excluded.cache_write, cache_write_1h=excluded.cache_write_1h,
|
|
6881
|
-
cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
|
|
8422
|
+
cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, cost_fixed=excluded.cost_fixed, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
|
|
6882
8423
|
const tx = this.db.transaction((ts) => {
|
|
6883
8424
|
for (const t of ts) {
|
|
6884
|
-
up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
|
|
8425
|
+
up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, t.cost ?? costUsd(t.model, t.usage, this.prices), t.cost != null ? 1 : 0, privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
|
|
6885
8426
|
}
|
|
6886
8427
|
});
|
|
6887
8428
|
if (turns.length)
|
|
@@ -7072,22 +8613,176 @@ ${err}
|
|
|
7072
8613
|
let title;
|
|
7073
8614
|
let fresh = false;
|
|
7074
8615
|
try {
|
|
7075
|
-
const m = statSync(sumPath).mtimeMs;
|
|
7076
|
-
const hit = this.grokSummary.get(sumPath);
|
|
7077
|
-
if (hit && hit.mtime === m)
|
|
7078
|
-
title = hit.title;
|
|
7079
|
-
else {
|
|
7080
|
-
const sum = JSON.parse(readFileSync3(sumPath, "utf8"));
|
|
7081
|
-
title = sum.session_summary;
|
|
7082
|
-
this.grokSummary.set(sumPath, { mtime: m, title });
|
|
7083
|
-
fresh = true;
|
|
7084
|
-
}
|
|
7085
|
-
} catch {}
|
|
7086
|
-
n += this.ingestLog(path, "grok", parseGrokUpdates, cwd, title);
|
|
7087
|
-
if (title && fresh) {
|
|
7088
|
-
this.db.query("UPDATE sessions SET title = ? WHERE id = ? AND (title IS NULL OR title = '')").run(title, sid);
|
|
8616
|
+
const m = statSync(sumPath).mtimeMs;
|
|
8617
|
+
const hit = this.grokSummary.get(sumPath);
|
|
8618
|
+
if (hit && hit.mtime === m)
|
|
8619
|
+
title = hit.title;
|
|
8620
|
+
else {
|
|
8621
|
+
const sum = JSON.parse(readFileSync3(sumPath, "utf8"));
|
|
8622
|
+
title = sum.session_summary;
|
|
8623
|
+
this.grokSummary.set(sumPath, { mtime: m, title });
|
|
8624
|
+
fresh = true;
|
|
8625
|
+
}
|
|
8626
|
+
} catch {}
|
|
8627
|
+
n += this.ingestLog(path, "grok", parseGrokUpdates, cwd, title);
|
|
8628
|
+
if (title && fresh) {
|
|
8629
|
+
this.db.query("UPDATE sessions SET title = ? WHERE id = ? AND (title IS NULL OR title = '')").run(title, sid);
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
}
|
|
8633
|
+
return n;
|
|
8634
|
+
}
|
|
8635
|
+
heldClaimsForSync() {
|
|
8636
|
+
return this.db.query("SELECT project_id, task, acquired_at, expires_at, actor_kind, actor_id, team_state FROM claims WHERE state = 'held'").all().map((r) => ({
|
|
8637
|
+
projectId: r.project_id,
|
|
8638
|
+
task: r.task,
|
|
8639
|
+
acquiredAt: r.acquired_at,
|
|
8640
|
+
expiresAt: r.expires_at,
|
|
8641
|
+
actorKind: r.actor_kind ?? null,
|
|
8642
|
+
actorId: r.actor_id ?? null,
|
|
8643
|
+
teamState: r.team_state ?? null
|
|
8644
|
+
}));
|
|
8645
|
+
}
|
|
8646
|
+
markClaimTeamState(projectId, task, state) {
|
|
8647
|
+
this.db.query("UPDATE claims SET team_state = ? WHERE project_id = ? AND task = ?").run(state, projectId, task);
|
|
8648
|
+
}
|
|
8649
|
+
revokeClaimConflict(projectId, task, holder) {
|
|
8650
|
+
const row = this.db.query("SELECT state FROM claims WHERE project_id = ? AND task = ?").get(projectId, task);
|
|
8651
|
+
if (row?.state !== "held")
|
|
8652
|
+
return;
|
|
8653
|
+
const now = new Date().toISOString();
|
|
8654
|
+
this.db.query("UPDATE claims SET state = 'released', released_at = ?, team_state = 'conflict' WHERE project_id = ? AND task = ?").run(now, projectId, task);
|
|
8655
|
+
this.append({
|
|
8656
|
+
ts: now,
|
|
8657
|
+
type: "claim.released",
|
|
8658
|
+
projectId,
|
|
8659
|
+
sessionId: null,
|
|
8660
|
+
payload: { task, summary: `revoked \u2014 the team ledger holds ${task} on ${holder}` }
|
|
8661
|
+
});
|
|
8662
|
+
this.append({
|
|
8663
|
+
ts: now,
|
|
8664
|
+
type: "incident.opened",
|
|
8665
|
+
projectId,
|
|
8666
|
+
sessionId: null,
|
|
8667
|
+
payload: {
|
|
8668
|
+
rule: "claim_conflict",
|
|
8669
|
+
action: "revoked",
|
|
8670
|
+
command: task,
|
|
8671
|
+
reason: `the team daemon holds ${task} for ${holder}; the local claim was revoked \u2014 the worktree is untouched`
|
|
8672
|
+
}
|
|
8673
|
+
});
|
|
8674
|
+
}
|
|
8675
|
+
aiderCarries = new Map;
|
|
8676
|
+
recoverAiderCarry(sessionId) {
|
|
8677
|
+
const s = this.db.query("SELECT started_at, model, title FROM sessions WHERE id = ?").get(sessionId);
|
|
8678
|
+
if (!s)
|
|
8679
|
+
return null;
|
|
8680
|
+
const t = this.db.query("SELECT COUNT(*) AS n FROM turns WHERE session_id = ?").get(sessionId);
|
|
8681
|
+
return {
|
|
8682
|
+
sessionId,
|
|
8683
|
+
startMs: Date.parse(s.started_at) || 0,
|
|
8684
|
+
model: s.model,
|
|
8685
|
+
title: s.title,
|
|
8686
|
+
turns: t.n,
|
|
8687
|
+
text: "",
|
|
8688
|
+
tools: [],
|
|
8689
|
+
pending: null
|
|
8690
|
+
};
|
|
8691
|
+
}
|
|
8692
|
+
tailAider(windowMs = 3 * 24 * 60 * 60000) {
|
|
8693
|
+
const roots = this.db.query("SELECT DISTINCT root FROM projects WHERE root IS NOT NULL AND root != ''").all();
|
|
8694
|
+
let n = 0;
|
|
8695
|
+
for (const { root } of roots) {
|
|
8696
|
+
const path = join8(root, ".aider.chat.history.md");
|
|
8697
|
+
let mtime;
|
|
8698
|
+
try {
|
|
8699
|
+
mtime = statSync(path).mtimeMs;
|
|
8700
|
+
} catch {
|
|
8701
|
+
continue;
|
|
8702
|
+
}
|
|
8703
|
+
if (mtime < Date.now() - windowMs)
|
|
8704
|
+
continue;
|
|
8705
|
+
const row = this.db.query("SELECT offset, session_id FROM tails WHERE path = ?").get(path);
|
|
8706
|
+
const r = this.readFrom(path, row?.offset ?? 0);
|
|
8707
|
+
if (!r)
|
|
8708
|
+
continue;
|
|
8709
|
+
let carry = this.aiderCarries.get(path) ?? null;
|
|
8710
|
+
if (!carry && row?.session_id)
|
|
8711
|
+
carry = this.recoverAiderCarry(row.session_id);
|
|
8712
|
+
const { segments, carry: next } = parseAiderHistory(r.chunk, path, carry);
|
|
8713
|
+
this.aiderCarries.set(path, next);
|
|
8714
|
+
const lastSeg = segments.at(-1);
|
|
8715
|
+
for (const seg of segments) {
|
|
8716
|
+
this.ensureAgentSession(seg.sessionId, "aider", root, seg.startMs || mtime);
|
|
8717
|
+
this.persistTurns(seg.sessionId, null, seg.turns);
|
|
8718
|
+
const live = seg === lastSeg && Date.now() - mtime < 90000;
|
|
8719
|
+
const lastSeen = new Date(seg === lastSeg ? mtime : seg.startMs + seg.turns.length * 1000).toISOString();
|
|
8720
|
+
const lastText = [...seg.turns].reverse().find((t) => t.text)?.text ?? null;
|
|
8721
|
+
this.db.query("UPDATE sessions SET title = COALESCE(title, ?), model = COALESCE(?, model), last_text = COALESCE(?, last_text), last_seen_at = ?, state = ?, ended_at = CASE WHEN ? = 'ended' AND ended_at IS NULL THEN ? ELSE ended_at END WHERE id = ?").run(seg.title, seg.model, lastText, lastSeen, live ? "active" : "ended", live ? "active" : "ended", lastSeen, seg.sessionId);
|
|
8722
|
+
n += seg.turns.length;
|
|
8723
|
+
}
|
|
8724
|
+
this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, ?, NULL, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset, session_id = excluded.session_id").run(path, lastSeg?.sessionId ?? row?.session_id ?? null, r.next);
|
|
8725
|
+
}
|
|
8726
|
+
return n;
|
|
8727
|
+
}
|
|
8728
|
+
ocDbs = new Map;
|
|
8729
|
+
tailOpencode(windowMs = 3 * 24 * 60 * 60000) {
|
|
8730
|
+
const dir = process.env.SWARM_OPENCODE_DIR ?? join8(process.env.XDG_DATA_HOME ?? join8(homedir3(), ".local", "share"), "opencode");
|
|
8731
|
+
let files;
|
|
8732
|
+
try {
|
|
8733
|
+
files = readdirSync(dir).filter((f) => /^opencode[^/]*\.db$/.test(f));
|
|
8734
|
+
} catch {
|
|
8735
|
+
return 0;
|
|
8736
|
+
}
|
|
8737
|
+
let n = 0;
|
|
8738
|
+
for (const f of files) {
|
|
8739
|
+
const path = join8(dir, f);
|
|
8740
|
+
let db = this.ocDbs.get(path);
|
|
8741
|
+
if (!db) {
|
|
8742
|
+
try {
|
|
8743
|
+
db = new Database(path, { readonly: true });
|
|
8744
|
+
} catch {
|
|
8745
|
+
continue;
|
|
7089
8746
|
}
|
|
8747
|
+
this.ocDbs.set(path, db);
|
|
8748
|
+
}
|
|
8749
|
+
const row = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path);
|
|
8750
|
+
const lower = Math.max(row?.offset ?? 0, Date.now() - windowMs);
|
|
8751
|
+
let rows;
|
|
8752
|
+
try {
|
|
8753
|
+
rows = db.query(`SELECT m.id, m.session_id, m.time_created, m.time_updated, m.data,
|
|
8754
|
+
s.directory, s.title, s.parent_id
|
|
8755
|
+
FROM message m JOIN session s ON s.id = m.session_id
|
|
8756
|
+
WHERE m.time_updated > ? ORDER BY m.time_updated ASC LIMIT 2000`).all(lower);
|
|
8757
|
+
} catch {
|
|
8758
|
+
continue;
|
|
8759
|
+
}
|
|
8760
|
+
if (!rows.length)
|
|
8761
|
+
continue;
|
|
8762
|
+
let cursor = lower;
|
|
8763
|
+
const bySession = new Map;
|
|
8764
|
+
for (const m of rows) {
|
|
8765
|
+
cursor = Math.max(cursor, m.time_updated ?? 0);
|
|
8766
|
+
const g = bySession.get(m.session_id) ?? { rows: [], last: 0 };
|
|
8767
|
+
g.rows.push(m);
|
|
8768
|
+
g.last = Math.max(g.last, m.time_updated ?? m.time_created ?? 0);
|
|
8769
|
+
bySession.set(m.session_id, g);
|
|
7090
8770
|
}
|
|
8771
|
+
for (const [sid, g] of bySession) {
|
|
8772
|
+
const first = g.rows[0];
|
|
8773
|
+
if (!first)
|
|
8774
|
+
continue;
|
|
8775
|
+
this.ensureAgentSession(sid, "opencode", first.directory ?? "", first.time_created ?? g.last);
|
|
8776
|
+
const turns = g.rows.map((m) => opencodeTurn(sid, m.id, m.data, m.time_created ?? 0, first.parent_id != null)).filter((t) => t != null);
|
|
8777
|
+
this.persistTurns(sid, null, turns);
|
|
8778
|
+
const live = Date.now() - g.last < 90000;
|
|
8779
|
+
const lastSeen = new Date(g.last).toISOString();
|
|
8780
|
+
const lastText = [...turns].reverse().find((t) => t.text)?.text ?? null;
|
|
8781
|
+
const model = [...turns].reverse().find((t) => t.model !== "opencode")?.model ?? null;
|
|
8782
|
+
this.db.query("UPDATE sessions SET title = COALESCE(?, title), model = COALESCE(?, model), last_text = COALESCE(?, last_text), last_seen_at = ?, state = ?, ended_at = CASE WHEN ? = 'ended' AND ended_at IS NULL THEN ? ELSE ended_at END WHERE id = ?").run(first.title, model, lastText, lastSeen, live ? "active" : "ended", live ? "active" : "ended", lastSeen, sid);
|
|
8783
|
+
n += turns.length;
|
|
8784
|
+
}
|
|
8785
|
+
this.db.query("INSERT INTO tails (path, session_id, agent_id, offset) VALUES (?, NULL, NULL, ?) ON CONFLICT(path) DO UPDATE SET offset = excluded.offset").run(path, cursor);
|
|
7091
8786
|
}
|
|
7092
8787
|
return n;
|
|
7093
8788
|
}
|
|
@@ -7144,7 +8839,8 @@ ${err}
|
|
|
7144
8839
|
acquiredAt: r.acquired_at,
|
|
7145
8840
|
expiresAt: r.expires_at,
|
|
7146
8841
|
releasedAt: r.released_at ?? null,
|
|
7147
|
-
state: r.state
|
|
8842
|
+
state: r.state,
|
|
8843
|
+
sessionId: r.actor_kind === "agent" ? r.actor_id ?? null : null
|
|
7148
8844
|
}));
|
|
7149
8845
|
const now = Date.now();
|
|
7150
8846
|
for (const c of rows)
|
|
@@ -7272,6 +8968,507 @@ ${err}
|
|
|
7272
8968
|
expiresAt: r.expires_at
|
|
7273
8969
|
}));
|
|
7274
8970
|
}
|
|
8971
|
+
waiting(projectId, days3 = 7) {
|
|
8972
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
8973
|
+
const pArgs = projectId ? [projectId] : [];
|
|
8974
|
+
const paired = this.db.query(`SELECT type, session_id, project_id, ts,
|
|
8975
|
+
COALESCE(json_extract(payload,'$.requestId'), json_extract(payload,'$.id')) AS key,
|
|
8976
|
+
COALESCE(json_extract(payload,'$.tool'), json_extract(payload,'$.text')) AS label
|
|
8977
|
+
FROM events
|
|
8978
|
+
WHERE type IN ('permission.requested','permission.resolved','question.asked','question.answered')
|
|
8979
|
+
AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
8980
|
+
const notes = this.db.query(`SELECT n.seq, n.session_id, n.project_id, n.ts,
|
|
8981
|
+
json_extract(n.payload,'$.summary') AS label,
|
|
8982
|
+
(SELECT MIN(a.ts) FROM events a
|
|
8983
|
+
WHERE a.session_id = n.session_id AND a.seq > n.seq
|
|
8984
|
+
AND a.type IN ('prompt.submitted','tool.requested')) AS resumed
|
|
8985
|
+
FROM events n
|
|
8986
|
+
WHERE n.type = 'session.notification' AND n.ts >= ?${projectId ? " AND n.project_id = ?" : ""}`).all(since, ...pArgs);
|
|
8987
|
+
const samples = [];
|
|
8988
|
+
for (const r of paired) {
|
|
8989
|
+
if (!r.session_id || r.key === null)
|
|
8990
|
+
continue;
|
|
8991
|
+
const kind = r.type.startsWith("permission") ? "permission" : "question";
|
|
8992
|
+
samples.push({
|
|
8993
|
+
sessionId: r.session_id,
|
|
8994
|
+
projectId: r.project_id,
|
|
8995
|
+
kind,
|
|
8996
|
+
key: String(r.key),
|
|
8997
|
+
phase: r.type.endsWith(".requested") || r.type.endsWith(".asked") ? "start" : "end",
|
|
8998
|
+
ts: r.ts,
|
|
8999
|
+
...r.label ? { label: r.label.slice(0, 120) } : {}
|
|
9000
|
+
});
|
|
9001
|
+
}
|
|
9002
|
+
for (const n of notes) {
|
|
9003
|
+
if (!n.session_id)
|
|
9004
|
+
continue;
|
|
9005
|
+
const key = String(n.seq);
|
|
9006
|
+
samples.push({
|
|
9007
|
+
sessionId: n.session_id,
|
|
9008
|
+
projectId: n.project_id,
|
|
9009
|
+
kind: "notification",
|
|
9010
|
+
key,
|
|
9011
|
+
phase: "start",
|
|
9012
|
+
ts: n.ts,
|
|
9013
|
+
...n.label ? { label: n.label.slice(0, 120) } : {}
|
|
9014
|
+
});
|
|
9015
|
+
if (n.resumed)
|
|
9016
|
+
samples.push({
|
|
9017
|
+
sessionId: n.session_id,
|
|
9018
|
+
projectId: n.project_id,
|
|
9019
|
+
kind: "notification",
|
|
9020
|
+
key,
|
|
9021
|
+
phase: "end",
|
|
9022
|
+
ts: n.resumed
|
|
9023
|
+
});
|
|
9024
|
+
}
|
|
9025
|
+
const ends = {};
|
|
9026
|
+
for (const r of this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all())
|
|
9027
|
+
ends[r.id] = r.ended_at;
|
|
9028
|
+
const report = waitingReport(pairWaits(samples, new Date().toISOString(), ends));
|
|
9029
|
+
const meta = new Map(this.db.query("SELECT id, title, agent, project_id FROM sessions").all().map((r) => [r.id, r]));
|
|
9030
|
+
return {
|
|
9031
|
+
...report,
|
|
9032
|
+
sessions: report.sessions.map((s) => ({
|
|
9033
|
+
...s,
|
|
9034
|
+
title: meta.get(s.sessionId)?.title ?? null,
|
|
9035
|
+
agent: meta.get(s.sessionId)?.agent ?? "claude-code",
|
|
9036
|
+
projectId: s.projectId ?? meta.get(s.sessionId)?.project_id ?? null
|
|
9037
|
+
}))
|
|
9038
|
+
};
|
|
9039
|
+
}
|
|
9040
|
+
gateHealth(projectId, days3 = 30) {
|
|
9041
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9042
|
+
const rows = this.db.query(`SELECT project_id, task, gate, verdict, duration_ms, created_at FROM gates
|
|
9043
|
+
WHERE created_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
9044
|
+
return gateHealth(rows.filter((r) => r.verdict === "pass" || r.verdict === "fail").map((r) => ({
|
|
9045
|
+
projectId: r.project_id,
|
|
9046
|
+
task: r.task,
|
|
9047
|
+
gate: r.gate,
|
|
9048
|
+
verdict: r.verdict,
|
|
9049
|
+
durationMs: r.duration_ms,
|
|
9050
|
+
at: r.created_at
|
|
9051
|
+
})));
|
|
9052
|
+
}
|
|
9053
|
+
static worktreeIdleMs(path) {
|
|
9054
|
+
let newest = 0;
|
|
9055
|
+
for (const f of [path, join8(path, ".git")]) {
|
|
9056
|
+
try {
|
|
9057
|
+
newest = Math.max(newest, statSync(f).mtimeMs);
|
|
9058
|
+
} catch {}
|
|
9059
|
+
}
|
|
9060
|
+
return newest ? Math.max(0, Date.now() - newest) : null;
|
|
9061
|
+
}
|
|
9062
|
+
duCache = new Map;
|
|
9063
|
+
duInflight = null;
|
|
9064
|
+
refreshDisk(paths, ttlMs) {
|
|
9065
|
+
if (this.duInflight)
|
|
9066
|
+
return;
|
|
9067
|
+
const stale = paths.filter((p) => {
|
|
9068
|
+
const hit = this.duCache.get(p);
|
|
9069
|
+
return !hit || Date.now() - hit.t >= ttlMs;
|
|
9070
|
+
});
|
|
9071
|
+
if (!stale.length)
|
|
9072
|
+
return;
|
|
9073
|
+
this.duInflight = (async () => {
|
|
9074
|
+
for (const path of stale) {
|
|
9075
|
+
let v = null;
|
|
9076
|
+
try {
|
|
9077
|
+
const proc = Bun.spawn(["du", "-sk", "-x", path], { stdout: "pipe", stderr: "ignore" });
|
|
9078
|
+
const out = await new Response(proc.stdout).text();
|
|
9079
|
+
if (await proc.exited === 0) {
|
|
9080
|
+
const n = Number.parseInt(out.trim().split(/\s+/)[0] ?? "", 10);
|
|
9081
|
+
if (Number.isFinite(n))
|
|
9082
|
+
v = n;
|
|
9083
|
+
}
|
|
9084
|
+
} catch {}
|
|
9085
|
+
this.duCache.set(path, { v, t: Date.now() });
|
|
9086
|
+
}
|
|
9087
|
+
})().finally(() => {
|
|
9088
|
+
this.duInflight = null;
|
|
9089
|
+
});
|
|
9090
|
+
}
|
|
9091
|
+
hygiene(projectId, diskTtlMs = 600000) {
|
|
9092
|
+
const rows = this.db.query(`SELECT * FROM processes WHERE ended_at IS NULL${projectId ? " AND project_id = ?" : ""}
|
|
9093
|
+
ORDER BY started_at DESC`).all(...projectId ? [projectId] : []).map((r) => this.rowToProcess(r));
|
|
9094
|
+
const usage = new Map;
|
|
9095
|
+
if (rows.length) {
|
|
9096
|
+
try {
|
|
9097
|
+
const out = Bun.spawnSync([
|
|
9098
|
+
"ps",
|
|
9099
|
+
"-o",
|
|
9100
|
+
"pid=,pcpu=,rss=",
|
|
9101
|
+
"-p",
|
|
9102
|
+
rows.map((r) => r.pid).join(",")
|
|
9103
|
+
]);
|
|
9104
|
+
for (const line of new TextDecoder().decode(out.stdout).split(`
|
|
9105
|
+
`)) {
|
|
9106
|
+
const [pid, cpu, rss] = line.trim().split(/\s+/);
|
|
9107
|
+
if (pid)
|
|
9108
|
+
usage.set(Number(pid), { cpu: Number(cpu) || 0, rss: Number(rss) || 0 });
|
|
9109
|
+
}
|
|
9110
|
+
} catch {}
|
|
9111
|
+
}
|
|
9112
|
+
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));
|
|
9113
|
+
const procs = rows.map((p) => {
|
|
9114
|
+
const u = usage.get(p.pid);
|
|
9115
|
+
return {
|
|
9116
|
+
pid: p.pid,
|
|
9117
|
+
name: p.name,
|
|
9118
|
+
kind: p.kind,
|
|
9119
|
+
projectId: p.projectId,
|
|
9120
|
+
sessionId: p.sessionId,
|
|
9121
|
+
port: p.port,
|
|
9122
|
+
startedAt: p.startedAt,
|
|
9123
|
+
alive: this.processIsOurs(p),
|
|
9124
|
+
sessionLive: !p.sessionId || liveSessionIds.has(p.sessionId),
|
|
9125
|
+
cpuPct: u ? u.cpu : null,
|
|
9126
|
+
rssKb: u ? u.rss : null
|
|
9127
|
+
};
|
|
9128
|
+
});
|
|
9129
|
+
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9130
|
+
const claims = this.claims().filter((c) => c.state !== "released");
|
|
9131
|
+
const trees = [];
|
|
9132
|
+
for (const pid of projects)
|
|
9133
|
+
for (const w of this.worktrees(pid)) {
|
|
9134
|
+
const held = claims.find((c) => c.worktree === w.path);
|
|
9135
|
+
trees.push({
|
|
9136
|
+
projectId: pid,
|
|
9137
|
+
path: w.path,
|
|
9138
|
+
branch: w.branch,
|
|
9139
|
+
main: w.main,
|
|
9140
|
+
dirty: w.dirty,
|
|
9141
|
+
ahead: w.ahead,
|
|
9142
|
+
merged: w.merged,
|
|
9143
|
+
idleMs: Store.worktreeIdleMs(w.path),
|
|
9144
|
+
diskKb: this.duCache.get(w.path)?.v ?? null,
|
|
9145
|
+
heldByClaim: held?.task ?? null,
|
|
9146
|
+
liveSessions: this.sessions().filter((s) => s.cwd?.startsWith(w.path) && !s.endedAt && s.state !== "ended").length
|
|
9147
|
+
});
|
|
9148
|
+
}
|
|
9149
|
+
this.refreshDisk(trees.map((t) => t.path), diskTtlMs);
|
|
9150
|
+
return hygieneReport(procs, trees);
|
|
9151
|
+
}
|
|
9152
|
+
lineage(projectId, days3 = 14, expanded = []) {
|
|
9153
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9154
|
+
const pArgs = projectId ? [projectId] : [];
|
|
9155
|
+
const rows = this.db.query(`SELECT id, project_id, title, agent, kind, state, parent_id, started_at, ended_at
|
|
9156
|
+
FROM sessions WHERE last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs);
|
|
9157
|
+
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]));
|
|
9158
|
+
const sessions = rows.map((r) => ({
|
|
9159
|
+
id: r.id,
|
|
9160
|
+
projectId: r.project_id,
|
|
9161
|
+
title: r.title,
|
|
9162
|
+
agent: r.agent ?? "claude-code",
|
|
9163
|
+
kind: r.kind,
|
|
9164
|
+
state: r.state,
|
|
9165
|
+
startedAt: r.started_at,
|
|
9166
|
+
endedAt: r.ended_at,
|
|
9167
|
+
costUsd: cost.get(r.id) ?? null,
|
|
9168
|
+
outcome: null
|
|
9169
|
+
}));
|
|
9170
|
+
const known = new Set(sessions.map((s) => s.id));
|
|
9171
|
+
const edges = [];
|
|
9172
|
+
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,
|
|
9173
|
+
SUM(t.cost_usd) AS cost, COUNT(*) AS turns
|
|
9174
|
+
FROM turns t WHERE t.agent_id IS NOT NULL AND t.ts >= ?
|
|
9175
|
+
GROUP BY t.session_id, t.agent_id`).all(since)) {
|
|
9176
|
+
const parent = sessions.find((x) => x.id === a.session_id);
|
|
9177
|
+
if (!parent)
|
|
9178
|
+
continue;
|
|
9179
|
+
const id = `sub:${a.session_id}:${a.agent_id}`;
|
|
9180
|
+
sessions.push({
|
|
9181
|
+
id,
|
|
9182
|
+
projectId: parent.projectId,
|
|
9183
|
+
title: `subagent ${a.agent_id.slice(0, 8)} \xB7 ${a.turns} turn${a.turns === 1 ? "" : "s"}`,
|
|
9184
|
+
agent: parent.agent,
|
|
9185
|
+
kind: "subagent",
|
|
9186
|
+
state: "ended",
|
|
9187
|
+
startedAt: a.first_ts,
|
|
9188
|
+
endedAt: a.last_ts,
|
|
9189
|
+
costUsd: a.cost,
|
|
9190
|
+
outcome: null
|
|
9191
|
+
});
|
|
9192
|
+
known.add(id);
|
|
9193
|
+
edges.push({ from: a.session_id, to: id, kind: "subagent", at: a.first_ts });
|
|
9194
|
+
}
|
|
9195
|
+
for (const d of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.by') AS by, json_extract(payload,'$.task') AS task
|
|
9196
|
+
FROM events WHERE type = 'dispatch.started' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs)) {
|
|
9197
|
+
if (!d.session_id || !d.by)
|
|
9198
|
+
continue;
|
|
9199
|
+
const a = actorFrom(d.by, null);
|
|
9200
|
+
if (a.kind === "agent" && known.has(a.id) && a.id !== d.session_id)
|
|
9201
|
+
edges.push({ from: a.id, to: d.session_id, kind: "dispatch", at: d.ts, label: d.task });
|
|
9202
|
+
}
|
|
9203
|
+
for (const m of this.db.query(`SELECT session_id, ts, json_extract(payload,'$.recipient') AS to_session,
|
|
9204
|
+
json_extract(payload,'$.text') AS text
|
|
9205
|
+
FROM events WHERE type = 'message.sent' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs))
|
|
9206
|
+
if (m.session_id && m.to_session)
|
|
9207
|
+
edges.push({
|
|
9208
|
+
from: m.session_id,
|
|
9209
|
+
to: m.to_session,
|
|
9210
|
+
kind: "message",
|
|
9211
|
+
at: m.ts,
|
|
9212
|
+
label: m.text?.slice(0, 80) ?? null
|
|
9213
|
+
});
|
|
9214
|
+
const holds = this.db.query(`SELECT task, project_id, actor_kind, actor_id, acquired_at FROM claims
|
|
9215
|
+
WHERE acquired_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...pArgs).map((c) => ({
|
|
9216
|
+
task: c.task,
|
|
9217
|
+
projectId: c.project_id,
|
|
9218
|
+
sessionId: c.actor_kind === "agent" ? c.actor_id : null,
|
|
9219
|
+
at: c.acquired_at
|
|
9220
|
+
}));
|
|
9221
|
+
edges.push(...handoffEdges(holds));
|
|
9222
|
+
return lineageGraph(sessions, edges, { expanded });
|
|
9223
|
+
}
|
|
9224
|
+
mcpHealth(projectId, days3 = 7) {
|
|
9225
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9226
|
+
const rows = this.db.query(`SELECT session_id, seq, ts, type,
|
|
9227
|
+
json_extract(payload,'$.tool') AS tool,
|
|
9228
|
+
json_extract(payload,'$.toolResponse') AS response
|
|
9229
|
+
FROM events
|
|
9230
|
+
WHERE type IN ('tool.requested','tool.completed') AND ts >= ?
|
|
9231
|
+
AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
|
|
9232
|
+
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
9233
|
+
const calls = [];
|
|
9234
|
+
const pending = new Map;
|
|
9235
|
+
const abandon = (sessionId, tool, at) => {
|
|
9236
|
+
calls.push({ sessionId, tool, ms: null, errored: false, at });
|
|
9237
|
+
};
|
|
9238
|
+
for (const r of rows) {
|
|
9239
|
+
if (!r.session_id)
|
|
9240
|
+
continue;
|
|
9241
|
+
const perSession = pending.get(r.session_id) ?? new Map;
|
|
9242
|
+
pending.set(r.session_id, perSession);
|
|
9243
|
+
if (r.type === "tool.requested") {
|
|
9244
|
+
const open = perSession.get(r.tool);
|
|
9245
|
+
if (open)
|
|
9246
|
+
abandon(r.session_id, r.tool, open);
|
|
9247
|
+
perSession.set(r.tool, r.ts);
|
|
9248
|
+
continue;
|
|
9249
|
+
}
|
|
9250
|
+
const startedAt = perSession.get(r.tool);
|
|
9251
|
+
perSession.delete(r.tool);
|
|
9252
|
+
calls.push({
|
|
9253
|
+
sessionId: r.session_id,
|
|
9254
|
+
tool: r.tool,
|
|
9255
|
+
ms: startedAt ? Math.max(0, new Date(r.ts).getTime() - new Date(startedAt).getTime()) : null,
|
|
9256
|
+
errored: toolResponseErrored(safeJson(r.response)),
|
|
9257
|
+
at: r.ts
|
|
9258
|
+
});
|
|
9259
|
+
}
|
|
9260
|
+
for (const [sessionId, perSession] of pending)
|
|
9261
|
+
for (const [tool, at] of perSession)
|
|
9262
|
+
abandon(sessionId, tool, at);
|
|
9263
|
+
return mcpHealth(calls);
|
|
9264
|
+
}
|
|
9265
|
+
context(projectId, days3 = 7) {
|
|
9266
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9267
|
+
const pArgs = projectId ? [projectId] : [];
|
|
9268
|
+
const where = projectId ? " AND project_id = ?" : "";
|
|
9269
|
+
const results = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
9270
|
+
LENGTH(json_extract(payload,'$.toolResponse')) AS chars
|
|
9271
|
+
FROM events
|
|
9272
|
+
WHERE type = 'tool.completed' AND ts >= ?${where}
|
|
9273
|
+
AND json_extract(payload,'$.tool') IS NOT NULL
|
|
9274
|
+
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 }));
|
|
9275
|
+
const reads = this.db.query(`SELECT req.session_id,
|
|
9276
|
+
json_extract(req.payload,'$.toolInput.file_path') AS path,
|
|
9277
|
+
(SELECT LENGTH(json_extract(done.payload,'$.toolResponse')) FROM events done
|
|
9278
|
+
WHERE done.session_id = req.session_id AND done.seq > req.seq
|
|
9279
|
+
AND done.type = 'tool.completed'
|
|
9280
|
+
AND json_extract(done.payload,'$.tool') = 'Read'
|
|
9281
|
+
ORDER BY done.seq LIMIT 1) AS chars
|
|
9282
|
+
FROM events req
|
|
9283
|
+
WHERE req.type = 'tool.requested' AND json_extract(req.payload,'$.tool') = 'Read'
|
|
9284
|
+
AND req.ts >= ?${where.replace("project_id", "req.project_id")}
|
|
9285
|
+
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 }));
|
|
9286
|
+
const turns = this.db.query(`SELECT t.session_id, SUM(t.input) AS input, SUM(t.cache_read) AS cache_read,
|
|
9287
|
+
SUM(t.cache_write) AS cache_write, SUM(t.thinking) AS thinking, SUM(t.output) AS output
|
|
9288
|
+
FROM turns t${projectId ? " JOIN sessions s ON s.id = t.session_id" : ""}
|
|
9289
|
+
WHERE t.ts >= ?${projectId ? " AND s.project_id = ?" : ""}
|
|
9290
|
+
GROUP BY t.session_id`).all(since, ...pArgs).map((r) => ({
|
|
9291
|
+
sessionId: r.session_id,
|
|
9292
|
+
input: r.input ?? 0,
|
|
9293
|
+
cacheRead: r.cache_read ?? 0,
|
|
9294
|
+
cacheWrite: r.cache_write ?? 0,
|
|
9295
|
+
thinking: r.thinking ?? 0,
|
|
9296
|
+
output: r.output ?? 0
|
|
9297
|
+
}));
|
|
9298
|
+
const report = contextReport(results, reads, turns);
|
|
9299
|
+
const meta = new Map(this.db.query("SELECT id, title, agent FROM sessions").all().map((r) => [r.id, r]));
|
|
9300
|
+
return {
|
|
9301
|
+
...report,
|
|
9302
|
+
sessions: report.sessions.map((s) => ({
|
|
9303
|
+
...s,
|
|
9304
|
+
title: meta.get(s.sessionId)?.title ?? null,
|
|
9305
|
+
agent: meta.get(s.sessionId)?.agent ?? "claude-code"
|
|
9306
|
+
}))
|
|
9307
|
+
};
|
|
9308
|
+
}
|
|
9309
|
+
abTrials(projectId) {
|
|
9310
|
+
const projects = projectId ? [projectId] : this.projects().map((p) => p.id);
|
|
9311
|
+
const out = [];
|
|
9312
|
+
for (const pid of projects) {
|
|
9313
|
+
const tasks2 = [
|
|
9314
|
+
...new Set(this.claims(pid).map((c) => splitArmTask(c.task)).filter((x) => x.arm).map((x) => x.task))
|
|
9315
|
+
];
|
|
9316
|
+
for (const t of tasks2.sort())
|
|
9317
|
+
out.push({ ...this.abTrial(pid, t), projectId: pid });
|
|
9318
|
+
}
|
|
9319
|
+
const rank = { undecided: 0, "all-failed": 1, winner: 2 };
|
|
9320
|
+
return out.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.totals.costUsd - a.totals.costUsd);
|
|
9321
|
+
}
|
|
9322
|
+
diffCache = new Map;
|
|
9323
|
+
diffInflight = null;
|
|
9324
|
+
refreshDiffs(worktrees, base, ttlMs = 30000) {
|
|
9325
|
+
if (this.diffInflight)
|
|
9326
|
+
return;
|
|
9327
|
+
const stale = worktrees.filter((w) => {
|
|
9328
|
+
const hit = this.diffCache.get(w);
|
|
9329
|
+
return !hit || Date.now() - hit.t >= ttlMs;
|
|
9330
|
+
});
|
|
9331
|
+
if (!stale.length)
|
|
9332
|
+
return;
|
|
9333
|
+
this.diffInflight = (async () => {
|
|
9334
|
+
for (const wt of stale) {
|
|
9335
|
+
let v = null;
|
|
9336
|
+
try {
|
|
9337
|
+
const proc = Bun.spawn(["git", "diff", "--shortstat", `${base}...HEAD`], {
|
|
9338
|
+
cwd: wt,
|
|
9339
|
+
stdout: "pipe",
|
|
9340
|
+
stderr: "ignore"
|
|
9341
|
+
});
|
|
9342
|
+
const out = await new Response(proc.stdout).text();
|
|
9343
|
+
if (await proc.exited === 0) {
|
|
9344
|
+
const f = /(\d+) files? changed/.exec(out)?.[1];
|
|
9345
|
+
const i = /(\d+) insertions?/.exec(out)?.[1];
|
|
9346
|
+
const d = /(\d+) deletions?/.exec(out)?.[1];
|
|
9347
|
+
v = [Number(f ?? 0), Number(i ?? 0), Number(d ?? 0)];
|
|
9348
|
+
}
|
|
9349
|
+
} catch {}
|
|
9350
|
+
this.diffCache.set(wt, { v, t: Date.now() });
|
|
9351
|
+
}
|
|
9352
|
+
})().finally(() => {
|
|
9353
|
+
this.diffInflight = null;
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
abTrial(projectId, task) {
|
|
9357
|
+
const claims = this.claims(projectId).filter((c) => splitArmTask(c.task).task === task);
|
|
9358
|
+
const all = this.sessions();
|
|
9359
|
+
const sessions = new Map(all.map((s) => [s.id, s]));
|
|
9360
|
+
const byCwd = new Map;
|
|
9361
|
+
for (const s of all)
|
|
9362
|
+
if (s.cwd && !byCwd.has(s.cwd))
|
|
9363
|
+
byCwd.set(s.cwd, s);
|
|
9364
|
+
const gateRuns = this.gateRuns(projectId, undefined, 2000);
|
|
9365
|
+
const arms = [];
|
|
9366
|
+
for (const c of claims) {
|
|
9367
|
+
const label = splitArmTask(c.task).arm;
|
|
9368
|
+
if (!label)
|
|
9369
|
+
continue;
|
|
9370
|
+
const sess = (c.worktree ? byCwd.get(c.worktree) : undefined) ?? (c.sessionId ? sessions.get(c.sessionId) : undefined);
|
|
9371
|
+
const sid = sess?.id ?? c.sessionId;
|
|
9372
|
+
const g = gateRuns.filter((x) => x.task === c.task);
|
|
9373
|
+
const latest = new Map;
|
|
9374
|
+
for (const run2 of [...g].sort((a, b) => a.createdAt < b.createdAt ? -1 : 1))
|
|
9375
|
+
latest.set(run2.gate, run2);
|
|
9376
|
+
const verdicts = [...latest.values()];
|
|
9377
|
+
const diff = c.worktree ? this.diffCache.get(c.worktree)?.v : null;
|
|
9378
|
+
arms.push({
|
|
9379
|
+
label,
|
|
9380
|
+
task: c.task,
|
|
9381
|
+
model: sess?.model ?? null,
|
|
9382
|
+
agent: sess?.agent ?? "claude-code",
|
|
9383
|
+
sessionId: sid,
|
|
9384
|
+
worktree: c.worktree || null,
|
|
9385
|
+
startedAt: c.acquiredAt,
|
|
9386
|
+
endedAt: sess?.endedAt ?? null,
|
|
9387
|
+
state: sess && sess.state !== "ended" ? "running" : sess ? "done" : c.state === "held" ? "running" : "failed",
|
|
9388
|
+
costUsd: sess?.costUsd ?? 0,
|
|
9389
|
+
turns: sess?.turns ?? 0,
|
|
9390
|
+
gatesPassed: verdicts.filter((v) => v.verdict === "pass").length,
|
|
9391
|
+
gatesFailed: verdicts.filter((v) => v.verdict === "fail").length,
|
|
9392
|
+
filesChanged: diff ? diff[0] : null,
|
|
9393
|
+
insertions: diff ? diff[1] : null,
|
|
9394
|
+
deletions: diff ? diff[2] : null
|
|
9395
|
+
});
|
|
9396
|
+
}
|
|
9397
|
+
const base = this.worktrees(projectId).find((w) => w.main)?.branch ?? "main";
|
|
9398
|
+
this.refreshDiffs(arms.map((a) => a.worktree).filter((w) => !!w), base);
|
|
9399
|
+
return scoreTrial(task, arms);
|
|
9400
|
+
}
|
|
9401
|
+
collisions(projectId) {
|
|
9402
|
+
const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
|
|
9403
|
+
const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
|
|
9404
|
+
WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [cutoff, projectId] : [cutoff]);
|
|
9405
|
+
if (!live.length)
|
|
9406
|
+
return { sessions: [], files: [], contested: 0 };
|
|
9407
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool, json_extract(payload,'$.toolInput.file_path') AS path
|
|
9408
|
+
FROM events WHERE type = 'tool.requested' AND session_id IN (${live.map(() => "?").join(",")})
|
|
9409
|
+
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL`).all(...live.map((s) => s.id));
|
|
9410
|
+
const g = collisionGraph(rows.map((r) => ({ sessionId: r.session_id, tool: r.tool ?? "", path: r.path ?? "" })));
|
|
9411
|
+
const meta = new Map(live.map((s) => [s.id, s]));
|
|
9412
|
+
return {
|
|
9413
|
+
...g,
|
|
9414
|
+
sessions: g.sessions.map((s) => {
|
|
9415
|
+
const m = meta.get(s.id);
|
|
9416
|
+
return {
|
|
9417
|
+
...s,
|
|
9418
|
+
title: m?.title ?? null,
|
|
9419
|
+
agent: m?.agent ?? "claude-code",
|
|
9420
|
+
projectId: m?.project_id ?? null
|
|
9421
|
+
};
|
|
9422
|
+
})
|
|
9423
|
+
};
|
|
9424
|
+
}
|
|
9425
|
+
stalls = new Map;
|
|
9426
|
+
checkStalls() {
|
|
9427
|
+
const live = this.db.query("SELECT id, project_id FROM sessions WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?").all(new Date(Date.now() - IDLE_MS).toISOString());
|
|
9428
|
+
const liveIds = new Set(live.map((s) => s.id));
|
|
9429
|
+
for (const id of [...this.stalls.keys()])
|
|
9430
|
+
if (!liveIds.has(id))
|
|
9431
|
+
this.stalls.delete(id);
|
|
9432
|
+
let flagged = 0;
|
|
9433
|
+
for (const s of live) {
|
|
9434
|
+
const rows = this.db.query("SELECT payload FROM events WHERE session_id = ? AND type = 'tool.completed' ORDER BY seq DESC LIMIT 12").all(s.id);
|
|
9435
|
+
const calls = rows.reverse().map((r) => {
|
|
9436
|
+
let p = {};
|
|
9437
|
+
try {
|
|
9438
|
+
p = JSON.parse(r.payload || "{}");
|
|
9439
|
+
} catch {}
|
|
9440
|
+
return {
|
|
9441
|
+
tool: typeof p.tool === "string" ? p.tool : "?",
|
|
9442
|
+
input: JSON.stringify(p.toolInput ?? null),
|
|
9443
|
+
errored: toolResponseErrored(p.toolResponse),
|
|
9444
|
+
ts: ""
|
|
9445
|
+
};
|
|
9446
|
+
});
|
|
9447
|
+
const stall2 = detectStall(calls);
|
|
9448
|
+
if (!stall2) {
|
|
9449
|
+
this.stalls.delete(s.id);
|
|
9450
|
+
continue;
|
|
9451
|
+
}
|
|
9452
|
+
flagged++;
|
|
9453
|
+
const prev = this.stalls.get(s.id);
|
|
9454
|
+
this.stalls.set(s.id, stall2);
|
|
9455
|
+
if (prev?.kind === stall2.kind)
|
|
9456
|
+
continue;
|
|
9457
|
+
this.append({
|
|
9458
|
+
ts: new Date().toISOString(),
|
|
9459
|
+
type: "session.stuck",
|
|
9460
|
+
projectId: s.project_id,
|
|
9461
|
+
sessionId: s.id,
|
|
9462
|
+
payload: {
|
|
9463
|
+
kind: stall2.kind,
|
|
9464
|
+
reason: stall2.reason,
|
|
9465
|
+
summary: `session looks stuck \u2014 ${stall2.reason}`
|
|
9466
|
+
}
|
|
9467
|
+
});
|
|
9468
|
+
this.touch();
|
|
9469
|
+
}
|
|
9470
|
+
return flagged;
|
|
9471
|
+
}
|
|
7275
9472
|
sweepOrphans() {
|
|
7276
9473
|
const now = Date.now();
|
|
7277
9474
|
let n = 0;
|
|
@@ -7649,6 +9846,7 @@ ${err}
|
|
|
7649
9846
|
lastType: r.last_type,
|
|
7650
9847
|
lastText: r.last_text ?? null,
|
|
7651
9848
|
state,
|
|
9849
|
+
stuck: state === "active" || state === "waiting" ? this.stalls.get(r.id)?.reason ?? null : null,
|
|
7652
9850
|
toolCalls: r.tool_calls,
|
|
7653
9851
|
subagents: r.subagents,
|
|
7654
9852
|
turns: r.turns,
|
|
@@ -7713,8 +9911,120 @@ ${err}
|
|
|
7713
9911
|
fn(p.id, b.status);
|
|
7714
9912
|
this.touch();
|
|
7715
9913
|
}
|
|
9914
|
+
for (const b of this.teamBudgets()) {
|
|
9915
|
+
if (b.level === "ok")
|
|
9916
|
+
continue;
|
|
9917
|
+
const mapKey = `team:${b.scope}:${b.key}`;
|
|
9918
|
+
const seen = `${day}:${b.level}`;
|
|
9919
|
+
const affected = b.scope === "project" ? this.projects().filter((p) => this.clusterKeyFor(p.id) === b.key) : this.projects();
|
|
9920
|
+
if (this.budgetNotified.get(mapKey) !== seen) {
|
|
9921
|
+
this.budgetNotified.set(mapKey, seen);
|
|
9922
|
+
const label = b.scope === "org" ? "the org" : `${b.scope} ${b.key}`;
|
|
9923
|
+
this.append({
|
|
9924
|
+
ts: new Date().toISOString(),
|
|
9925
|
+
type: "incident.opened",
|
|
9926
|
+
projectId: affected[0]?.id ?? "p_unknown",
|
|
9927
|
+
sessionId: null,
|
|
9928
|
+
payload: {
|
|
9929
|
+
rule: "budget",
|
|
9930
|
+
action: b.level === "exceeded" ? b.on_exceed : "warn",
|
|
9931
|
+
command: `team ${b.kind ?? ""} budget \xB7 ${label}`,
|
|
9932
|
+
reason: b.level === "exceeded" ? `${label} spent $${b.spent.toFixed(2)} of the $${b.limit} ${b.kind} ceiling set on the team daemon. ${b.on_exceed === "stop" ? "Spawned runs were stopped." : b.on_exceed === "ask" ? "Every Bash/Edit/Write now asks first." : "An admin can raise it via POST /t1/budgets."}` : `${label} is at $${b.spent.toFixed(2)} of the $${b.limit} ${b.kind} ceiling \u2014 approaching the team's limit`
|
|
9933
|
+
}
|
|
9934
|
+
});
|
|
9935
|
+
this.touch();
|
|
9936
|
+
}
|
|
9937
|
+
if (b.level === "exceeded" && b.on_exceed === "stop")
|
|
9938
|
+
for (const p of affected)
|
|
9939
|
+
for (const fn of this.budgetListeners)
|
|
9940
|
+
fn(p.id, {
|
|
9941
|
+
level: "exceeded",
|
|
9942
|
+
kind: b.kind === "daily" ? "daily" : "weekly",
|
|
9943
|
+
spent: b.spent,
|
|
9944
|
+
limit: b.limit,
|
|
9945
|
+
pct: b.limit ? b.spent / b.limit : 1,
|
|
9946
|
+
daily: { spent: b.spent, limit: b.limit, pct: 1 },
|
|
9947
|
+
weekly: { spent: 0, limit: null, pct: 0 }
|
|
9948
|
+
});
|
|
9949
|
+
}
|
|
7716
9950
|
return out;
|
|
7717
9951
|
}
|
|
9952
|
+
teamBudgets() {
|
|
9953
|
+
try {
|
|
9954
|
+
return JSON.parse(this.metaValue("team_budget") ?? "[]");
|
|
9955
|
+
} catch {
|
|
9956
|
+
return [];
|
|
9957
|
+
}
|
|
9958
|
+
}
|
|
9959
|
+
backupTo(destDir) {
|
|
9960
|
+
mkdirSync4(destDir, { recursive: true });
|
|
9961
|
+
const files = [];
|
|
9962
|
+
const dbDest = join8(destDir, "swarm.db");
|
|
9963
|
+
if (existsSync6(dbDest))
|
|
9964
|
+
unlinkSync(dbDest);
|
|
9965
|
+
this.db.exec(`VACUUM INTO '${dbDest.replaceAll("'", "''")}'`);
|
|
9966
|
+
files.push("swarm.db");
|
|
9967
|
+
for (const f of [
|
|
9968
|
+
"config.toml",
|
|
9969
|
+
"policy.toml",
|
|
9970
|
+
"policy.sig.json",
|
|
9971
|
+
"token",
|
|
9972
|
+
"pricing.json",
|
|
9973
|
+
"pricing.litellm.json",
|
|
9974
|
+
"team-token"
|
|
9975
|
+
]) {
|
|
9976
|
+
const src = join8(this.home, f);
|
|
9977
|
+
if (!existsSync6(src))
|
|
9978
|
+
continue;
|
|
9979
|
+
copyFileSync(src, join8(destDir, f));
|
|
9980
|
+
files.push(f);
|
|
9981
|
+
}
|
|
9982
|
+
return { dest: destDir, files };
|
|
9983
|
+
}
|
|
9984
|
+
clusterKeyCache = new Map;
|
|
9985
|
+
clusterKeyFor(projectId) {
|
|
9986
|
+
const hit = this.clusterKeyCache.get(projectId);
|
|
9987
|
+
if (hit)
|
|
9988
|
+
return hit;
|
|
9989
|
+
const root = this.db.query("SELECT root FROM projects WHERE id = ?").get(projectId)?.root;
|
|
9990
|
+
const key = root && clusterProjectKey(originUrl(root)) || `local:${projectId}`;
|
|
9991
|
+
this.clusterKeyCache.set(projectId, key);
|
|
9992
|
+
return key;
|
|
9993
|
+
}
|
|
9994
|
+
taskSpendRollup(day = new Date().toISOString().slice(0, 10)) {
|
|
9995
|
+
return this.db.query(`SELECT s.project_id AS projectId, c.task AS task, SUM(t.cost_usd) AS cost
|
|
9996
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
9997
|
+
JOIN claims c ON c.project_id = s.project_id AND c.worktree != '' AND (s.cwd = c.worktree OR s.cwd LIKE c.worktree || '/%')
|
|
9998
|
+
WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, c.task`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
|
|
9999
|
+
}
|
|
10000
|
+
modelFlagged = new Set;
|
|
10001
|
+
checkModels() {
|
|
10002
|
+
const since = new Date(Date.now() - IDLE_MS).toISOString();
|
|
10003
|
+
const rows = this.db.query("SELECT id, project_id, model FROM sessions WHERE model IS NOT NULL AND model != '' AND state != 'ended' AND last_seen_at > ?").all(since);
|
|
10004
|
+
let n = 0;
|
|
10005
|
+
for (const s of rows) {
|
|
10006
|
+
if (this.modelFlagged.has(s.id))
|
|
10007
|
+
continue;
|
|
10008
|
+
const allow = this.config(s.project_id).models.allow;
|
|
10009
|
+
if (!allow.length || modelAllowed(s.model, allow))
|
|
10010
|
+
continue;
|
|
10011
|
+
this.modelFlagged.add(s.id);
|
|
10012
|
+
n++;
|
|
10013
|
+
this.append({
|
|
10014
|
+
ts: new Date().toISOString(),
|
|
10015
|
+
type: "incident.opened",
|
|
10016
|
+
projectId: s.project_id,
|
|
10017
|
+
sessionId: s.id,
|
|
10018
|
+
payload: {
|
|
10019
|
+
rule: "model_allowlist",
|
|
10020
|
+
action: "observed",
|
|
10021
|
+
command: s.model,
|
|
10022
|
+
reason: `session runs on "${s.model}", outside [models] allow (${allow.join(", ")}) \u2014 nothing was interrupted; spawned runs on this model are refused`
|
|
10023
|
+
}
|
|
10024
|
+
});
|
|
10025
|
+
}
|
|
10026
|
+
return n;
|
|
10027
|
+
}
|
|
7718
10028
|
spend() {
|
|
7719
10029
|
const dayStart = new Date;
|
|
7720
10030
|
dayStart.setHours(0, 0, 0, 0);
|
|
@@ -7862,6 +10172,16 @@ ${err}
|
|
|
7862
10172
|
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
|
|
7863
10173
|
return r.n;
|
|
7864
10174
|
}
|
|
10175
|
+
openIncidentsByProject() {
|
|
10176
|
+
const rows = this.db.query(`SELECT e.project_id AS pid, COUNT(*) AS n FROM events e
|
|
10177
|
+
LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
10178
|
+
WHERE e.type = 'incident.opened' AND a.seq IS NULL AND e.project_id IS NOT NULL
|
|
10179
|
+
GROUP BY e.project_id`).all();
|
|
10180
|
+
const out = {};
|
|
10181
|
+
for (const r of rows)
|
|
10182
|
+
out[r.pid] = r.n;
|
|
10183
|
+
return out;
|
|
10184
|
+
}
|
|
7865
10185
|
ackIncident(seq, by) {
|
|
7866
10186
|
const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
|
|
7867
10187
|
if (!row)
|
|
@@ -8185,14 +10505,14 @@ ${err}
|
|
|
8185
10505
|
const from = localDayIso(-13);
|
|
8186
10506
|
const rows = this.db.query(`SELECT s.project_id AS pid, substr(t.ts, 1, 10) AS day, SUM(t.cost_usd) AS usd
|
|
8187
10507
|
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
8188
|
-
const
|
|
10508
|
+
const days3 = [];
|
|
8189
10509
|
for (let i = 13;i >= 0; i--)
|
|
8190
|
-
|
|
10510
|
+
days3.push(localDayIso(-i).slice(0, 10));
|
|
8191
10511
|
const out = {};
|
|
8192
10512
|
for (const r of rows) {
|
|
8193
10513
|
out[r.pid] ??= new Array(14).fill(0);
|
|
8194
10514
|
const arr = out[r.pid];
|
|
8195
|
-
const i =
|
|
10515
|
+
const i = days3.indexOf(r.day);
|
|
8196
10516
|
if (i >= 0)
|
|
8197
10517
|
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
8198
10518
|
}
|
|
@@ -8213,12 +10533,22 @@ ${err}
|
|
|
8213
10533
|
processes: this.memoised("processes", 5000, () => this.processes()),
|
|
8214
10534
|
incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
|
|
8215
10535
|
openIncidents: this.memoised("openIncidents", 30000, () => this.openIncidents()),
|
|
10536
|
+
openIncidentsByProject: this.memoised("openIncidentsByProject", 30000, () => this.openIncidentsByProject()),
|
|
8216
10537
|
questions: this.questions({ open: true, limit: 50 }),
|
|
8217
10538
|
resources: this.resources(),
|
|
8218
10539
|
seq: this.seq()
|
|
8219
10540
|
};
|
|
8220
10541
|
}
|
|
8221
10542
|
}
|
|
10543
|
+
function safeJson(v) {
|
|
10544
|
+
if (v === null)
|
|
10545
|
+
return null;
|
|
10546
|
+
try {
|
|
10547
|
+
return JSON.parse(v);
|
|
10548
|
+
} catch {
|
|
10549
|
+
return v;
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
8222
10552
|
var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
8223
10553
|
var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
|
|
8224
10554
|
var TOOL_INPUT_MAX = 2048;
|
|
@@ -8344,6 +10674,192 @@ function localDayIso(offsetDays) {
|
|
|
8344
10674
|
return d.toISOString();
|
|
8345
10675
|
}
|
|
8346
10676
|
|
|
10677
|
+
// packages/daemon/src/team.ts
|
|
10678
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
10679
|
+
import { join as join9 } from "path";
|
|
10680
|
+
var SPEND_EVERY_MS = 60000;
|
|
10681
|
+
var MAX_BACKOFF_MS = 300000;
|
|
10682
|
+
var POLICY_EVERY_MS = 300000;
|
|
10683
|
+
|
|
10684
|
+
class TeamForwarder {
|
|
10685
|
+
store;
|
|
10686
|
+
version;
|
|
10687
|
+
lastTry = 0;
|
|
10688
|
+
backoffMs = 0;
|
|
10689
|
+
lastSpend = 0;
|
|
10690
|
+
constructor(store, version) {
|
|
10691
|
+
this.store = store;
|
|
10692
|
+
this.version = version;
|
|
10693
|
+
}
|
|
10694
|
+
projectKey(projectId) {
|
|
10695
|
+
return this.store.clusterKeyFor(projectId);
|
|
10696
|
+
}
|
|
10697
|
+
status() {
|
|
10698
|
+
const team2 = this.store.policyFor(null).config.team;
|
|
10699
|
+
const box = this.store.outboxStatus();
|
|
10700
|
+
return {
|
|
10701
|
+
configured: team2.url != null,
|
|
10702
|
+
url: team2.url,
|
|
10703
|
+
forward: team2.forward,
|
|
10704
|
+
pending: box.pending,
|
|
10705
|
+
oldest: box.oldest,
|
|
10706
|
+
lastAckAt: this.store.metaValue("team_last_ack") ?? null,
|
|
10707
|
+
lastError: this.store.metaValue("team_last_error") ?? null,
|
|
10708
|
+
machine: this.store.machineIdentity(),
|
|
10709
|
+
authed: this.store.metaValue("team_machine_token") != null
|
|
10710
|
+
};
|
|
10711
|
+
}
|
|
10712
|
+
async tick(now = Date.now()) {
|
|
10713
|
+
const team2 = this.store.policyFor(null).config.team;
|
|
10714
|
+
if (!team2.url)
|
|
10715
|
+
return 0;
|
|
10716
|
+
if (now - this.lastTry < team2.interval * 1000 + this.backoffMs)
|
|
10717
|
+
return 0;
|
|
10718
|
+
this.lastTry = now;
|
|
10719
|
+
const records = [];
|
|
10720
|
+
const events = team2.forward.includes("ledger") ? this.store.outboxPending() : [];
|
|
10721
|
+
for (const e of events) {
|
|
10722
|
+
const body = JSON.parse(e.payload);
|
|
10723
|
+
if (typeof body.projectId === "string")
|
|
10724
|
+
body.projectKey = this.projectKey(body.projectId);
|
|
10725
|
+
records.push({ seq: e.seq, kind: e.kind, body });
|
|
10726
|
+
}
|
|
10727
|
+
let spendRows = 0;
|
|
10728
|
+
if (team2.forward.includes("cost") && now - this.lastSpend > SPEND_EVERY_MS) {
|
|
10729
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
10730
|
+
for (const r of this.store.spendRollup(day)) {
|
|
10731
|
+
records.push({
|
|
10732
|
+
seq: 0,
|
|
10733
|
+
kind: "spend",
|
|
10734
|
+
body: { day, ...r, projectKey: this.projectKey(r.projectId) }
|
|
10735
|
+
});
|
|
10736
|
+
spendRows++;
|
|
10737
|
+
}
|
|
10738
|
+
for (const r of this.store.taskSpendRollup(day)) {
|
|
10739
|
+
records.push({
|
|
10740
|
+
seq: 0,
|
|
10741
|
+
kind: "spend_task",
|
|
10742
|
+
body: { day, ...r, projectKey: this.projectKey(r.projectId) }
|
|
10743
|
+
});
|
|
10744
|
+
spendRows++;
|
|
10745
|
+
}
|
|
10746
|
+
}
|
|
10747
|
+
const held = this.store.heldClaimsForSync();
|
|
10748
|
+
if (!records.length && !held.length)
|
|
10749
|
+
return 0;
|
|
10750
|
+
const machine = { ...this.store.machineIdentity(), version: this.version };
|
|
10751
|
+
const token = this.store.metaValue("team_machine_token");
|
|
10752
|
+
const headers = {
|
|
10753
|
+
"content-type": "application/json",
|
|
10754
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
10755
|
+
};
|
|
10756
|
+
const base = this.store.policyFor(null).config.team.url;
|
|
10757
|
+
try {
|
|
10758
|
+
if (records.length) {
|
|
10759
|
+
const req = { machine, records };
|
|
10760
|
+
const res = await fetch(`${base}/t1/ingest`, {
|
|
10761
|
+
method: "POST",
|
|
10762
|
+
headers,
|
|
10763
|
+
body: JSON.stringify(req),
|
|
10764
|
+
signal: AbortSignal.timeout(1e4)
|
|
10765
|
+
});
|
|
10766
|
+
if (!res.ok)
|
|
10767
|
+
throw new Error(`ingest ${res.status}`);
|
|
10768
|
+
const reply = await res.json();
|
|
10769
|
+
if (reply.ack > 0)
|
|
10770
|
+
this.store.outboxAck(reply.ack);
|
|
10771
|
+
if (spendRows)
|
|
10772
|
+
this.lastSpend = now;
|
|
10773
|
+
}
|
|
10774
|
+
if (held.length) {
|
|
10775
|
+
const claims = held.map((c) => ({
|
|
10776
|
+
projectKey: this.projectKey(c.projectId),
|
|
10777
|
+
task: c.task,
|
|
10778
|
+
acquiredAt: c.acquiredAt,
|
|
10779
|
+
expiresAt: c.expiresAt,
|
|
10780
|
+
actor: c.actorKind && c.actorId ? { kind: c.actorKind, id: c.actorId } : undefined
|
|
10781
|
+
}));
|
|
10782
|
+
const res = await fetch(`${base}/t1/claims`, {
|
|
10783
|
+
method: "POST",
|
|
10784
|
+
headers,
|
|
10785
|
+
body: JSON.stringify({ machine, claims }),
|
|
10786
|
+
signal: AbortSignal.timeout(1e4)
|
|
10787
|
+
});
|
|
10788
|
+
if (!res.ok)
|
|
10789
|
+
throw new Error(`claims ${res.status}`);
|
|
10790
|
+
const reply = await res.json();
|
|
10791
|
+
for (const r of reply.results) {
|
|
10792
|
+
const local = held.find((c) => c.task === r.task && this.projectKey(c.projectId) === r.projectKey);
|
|
10793
|
+
if (!local)
|
|
10794
|
+
continue;
|
|
10795
|
+
if (r.status === "ok") {
|
|
10796
|
+
if (local.teamState !== "registered")
|
|
10797
|
+
this.store.markClaimTeamState(local.projectId, local.task, "registered");
|
|
10798
|
+
} else
|
|
10799
|
+
this.store.revokeClaimConflict(local.projectId, local.task, r.holder);
|
|
10800
|
+
}
|
|
10801
|
+
}
|
|
10802
|
+
try {
|
|
10803
|
+
const res = await fetch(`${base}/t1/budget`, {
|
|
10804
|
+
headers,
|
|
10805
|
+
signal: AbortSignal.timeout(1e4)
|
|
10806
|
+
});
|
|
10807
|
+
if (res.ok) {
|
|
10808
|
+
const { budgets } = await res.json();
|
|
10809
|
+
this.store.setMetaValue("team_budget", JSON.stringify(budgets ?? []));
|
|
10810
|
+
}
|
|
10811
|
+
} catch {}
|
|
10812
|
+
this.backoffMs = 0;
|
|
10813
|
+
this.store.setMetaValue("team_last_ack", new Date(now).toISOString());
|
|
10814
|
+
this.store.setMetaValue("team_last_error", "");
|
|
10815
|
+
await this.syncPolicy(base, headers, now);
|
|
10816
|
+
return records.length + held.length;
|
|
10817
|
+
} catch (e) {
|
|
10818
|
+
this.backoffMs = Math.min(this.backoffMs ? this.backoffMs * 2 : 5000, MAX_BACKOFF_MS);
|
|
10819
|
+
this.store.setMetaValue("team_last_error", e.message);
|
|
10820
|
+
return 0;
|
|
10821
|
+
}
|
|
10822
|
+
}
|
|
10823
|
+
lastPolicy = 0;
|
|
10824
|
+
async syncPolicy(base, headers, now) {
|
|
10825
|
+
if (now - this.lastPolicy < POLICY_EVERY_MS)
|
|
10826
|
+
return;
|
|
10827
|
+
this.lastPolicy = now;
|
|
10828
|
+
try {
|
|
10829
|
+
const res = await fetch(`${base}/t1/policy`, {
|
|
10830
|
+
headers,
|
|
10831
|
+
signal: AbortSignal.timeout(1e4)
|
|
10832
|
+
});
|
|
10833
|
+
if (!res.ok)
|
|
10834
|
+
return;
|
|
10835
|
+
const { policy: policy2 } = await res.json();
|
|
10836
|
+
if (!policy2)
|
|
10837
|
+
return;
|
|
10838
|
+
let pinned = this.store.metaValue("team_policy_pubkey");
|
|
10839
|
+
if (!pinned) {
|
|
10840
|
+
pinned = policy2.publicKey;
|
|
10841
|
+
this.store.setMetaValue("team_policy_pubkey", pinned);
|
|
10842
|
+
}
|
|
10843
|
+
if (!verifyPolicySignature(policy2.toml, policy2.signature, pinned)) {
|
|
10844
|
+
this.store.setMetaValue("team_last_error", "org policy signature invalid \u2014 not installed");
|
|
10845
|
+
return;
|
|
10846
|
+
}
|
|
10847
|
+
const file = join9(this.store.home, "policy.toml");
|
|
10848
|
+
const prev = this.store.metaValue("team_policy_sig");
|
|
10849
|
+
if (prev === policy2.signature)
|
|
10850
|
+
return;
|
|
10851
|
+
writeFileSync3(file, policy2.toml, { mode: 384 });
|
|
10852
|
+
writeFileSync3(join9(this.store.home, "policy.sig.json"), JSON.stringify({
|
|
10853
|
+
signature: policy2.signature,
|
|
10854
|
+
publicKey: pinned,
|
|
10855
|
+
fetchedAt: new Date(now).toISOString(),
|
|
10856
|
+
url: base
|
|
10857
|
+
}), { mode: 384 });
|
|
10858
|
+
this.store.setMetaValue("team_policy_sig", policy2.signature);
|
|
10859
|
+
} catch {}
|
|
10860
|
+
}
|
|
10861
|
+
}
|
|
10862
|
+
|
|
8347
10863
|
// packages/daemon/src/workflow.ts
|
|
8348
10864
|
class WorkflowEngine {
|
|
8349
10865
|
store;
|
|
@@ -8535,13 +11051,13 @@ class WorkflowEngine {
|
|
|
8535
11051
|
}
|
|
8536
11052
|
|
|
8537
11053
|
// packages/daemon/src/app.ts
|
|
8538
|
-
var VERSION = "0.
|
|
11054
|
+
var VERSION = "0.11.0";
|
|
8539
11055
|
var WEB_DIR = (() => {
|
|
8540
11056
|
if (process.env.SWARM_WEB_DIR)
|
|
8541
11057
|
return process.env.SWARM_WEB_DIR;
|
|
8542
11058
|
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
8543
|
-
const dev =
|
|
8544
|
-
return existsSync7(
|
|
11059
|
+
const dev = join10(here, "../../web/public");
|
|
11060
|
+
return existsSync7(join10(dev, "index.html")) ? dev : join10(here, "../web");
|
|
8545
11061
|
})();
|
|
8546
11062
|
var REPLAY_TAIL = 200;
|
|
8547
11063
|
var wireCache = new WeakMap;
|
|
@@ -8559,7 +11075,7 @@ function hookRepoRoot(store, raw2) {
|
|
|
8559
11075
|
}
|
|
8560
11076
|
function claudeSettings() {
|
|
8561
11077
|
try {
|
|
8562
|
-
const p = process.env.CLAUDE_SETTINGS ??
|
|
11078
|
+
const p = process.env.CLAUDE_SETTINGS ?? join10(homedir4(), ".claude", "settings.json");
|
|
8563
11079
|
return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
|
|
8564
11080
|
} catch {
|
|
8565
11081
|
return null;
|
|
@@ -8570,7 +11086,7 @@ function diskVersion() {
|
|
|
8570
11086
|
const entry = daemonCommand().at(-1);
|
|
8571
11087
|
if (!entry || !existsSync7(entry))
|
|
8572
11088
|
return null;
|
|
8573
|
-
for (const f of [entry,
|
|
11089
|
+
for (const f of [entry, join10(dirname4(entry), "app.ts")]) {
|
|
8574
11090
|
if (!existsSync7(f))
|
|
8575
11091
|
continue;
|
|
8576
11092
|
const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
|
|
@@ -8588,6 +11104,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8588
11104
|
const runner = new Runner(store, store.home);
|
|
8589
11105
|
const dispatcher = new Dispatcher(store, runner, forge2);
|
|
8590
11106
|
const workflows2 = new WorkflowEngine(store, runner, forge2);
|
|
11107
|
+
const team2 = new TeamForwarder(store, VERSION);
|
|
8591
11108
|
store.onBudgetStop((projectId) => {
|
|
8592
11109
|
dispatcher.clear(projectId);
|
|
8593
11110
|
for (const run2 of runner.list(projectId))
|
|
@@ -8650,7 +11167,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8650
11167
|
dir = homedir4();
|
|
8651
11168
|
}
|
|
8652
11169
|
try {
|
|
8653
|
-
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(
|
|
11170
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join10(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
8654
11171
|
const parent = dirname4(dir);
|
|
8655
11172
|
return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
|
|
8656
11173
|
} catch (e) {
|
|
@@ -8659,10 +11176,130 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8659
11176
|
});
|
|
8660
11177
|
app.get("/v1/state", (c) => c.json(store.snapshot()));
|
|
8661
11178
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
11179
|
+
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11180
|
+
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") ?? [])));
|
|
11181
|
+
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)))));
|
|
11182
|
+
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)))));
|
|
11183
|
+
app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
|
|
11184
|
+
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)))));
|
|
11185
|
+
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)))));
|
|
8662
11186
|
app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
|
|
8663
11187
|
open: c.req.query("open") === "1",
|
|
8664
11188
|
projectId: c.req.query("project") || undefined
|
|
8665
11189
|
})));
|
|
11190
|
+
const outcomesFor = async (project) => {
|
|
11191
|
+
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
11192
|
+
id: s.id,
|
|
11193
|
+
branch: s.branch,
|
|
11194
|
+
model: s.model,
|
|
11195
|
+
agent: s.agent,
|
|
11196
|
+
costUsd: s.costUsd,
|
|
11197
|
+
startedAt: s.startedAt
|
|
11198
|
+
}));
|
|
11199
|
+
const prs = [];
|
|
11200
|
+
const reverted = new Set;
|
|
11201
|
+
for (const p of store.projects().filter((x) => !project || x.id === project)) {
|
|
11202
|
+
const o = await forge2.merged(p.id, p.root);
|
|
11203
|
+
for (const m of o.merged)
|
|
11204
|
+
prs.push({ ...m, state: "merged" });
|
|
11205
|
+
for (const sha of o.reverted)
|
|
11206
|
+
reverted.add(sha);
|
|
11207
|
+
}
|
|
11208
|
+
for (const pr of forge2.prs())
|
|
11209
|
+
if (!project || pr.projectId === project)
|
|
11210
|
+
prs.push({
|
|
11211
|
+
branch: pr.branch,
|
|
11212
|
+
number: pr.number,
|
|
11213
|
+
state: "open",
|
|
11214
|
+
title: pr.title,
|
|
11215
|
+
url: pr.url,
|
|
11216
|
+
createdAt: pr.createdAt,
|
|
11217
|
+
mergedAt: null,
|
|
11218
|
+
mergeSha: null
|
|
11219
|
+
});
|
|
11220
|
+
return outcomeReport(sessions, prs, reverted);
|
|
11221
|
+
};
|
|
11222
|
+
app.get("/v1/outcomes", async (c) => c.json(await outcomesFor(c.req.query("project") || undefined)));
|
|
11223
|
+
app.get("/v1/ab", (c) => {
|
|
11224
|
+
const project = c.req.query("project") || undefined;
|
|
11225
|
+
const task = c.req.query("task");
|
|
11226
|
+
if (task) {
|
|
11227
|
+
if (!project)
|
|
11228
|
+
return c.json({ error: "project is required with task" }, 400);
|
|
11229
|
+
return c.json(store.abTrial(project, task));
|
|
11230
|
+
}
|
|
11231
|
+
return c.json({ trials: store.abTrials(project) });
|
|
11232
|
+
});
|
|
11233
|
+
app.post("/v1/ab", async (c) => {
|
|
11234
|
+
const b = await c.req.json().catch(() => ({}));
|
|
11235
|
+
if (!b.projectId || !b.task)
|
|
11236
|
+
return c.json({ error: "projectId and task are required" }, 400);
|
|
11237
|
+
const arms = (b.arms ?? []).filter((a) => a.model || a.label);
|
|
11238
|
+
if (arms.length < 2)
|
|
11239
|
+
return c.json({ error: "a trial needs at least two arms" }, 400);
|
|
11240
|
+
const cfg = store.config(b.projectId);
|
|
11241
|
+
const started = [];
|
|
11242
|
+
const failed = [];
|
|
11243
|
+
for (const a of arms) {
|
|
11244
|
+
const label = (a.label ?? a.model ?? "").replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
11245
|
+
const id = armTask(b.task, label);
|
|
11246
|
+
const r = await runner.start({
|
|
11247
|
+
projectId: b.projectId,
|
|
11248
|
+
task: id,
|
|
11249
|
+
prompt: taskPrompt({ id: b.task, title: b.title ?? b.task }, {
|
|
11250
|
+
requiredGates: cfg.gates.required,
|
|
11251
|
+
executableGates: cfg.gates.required.filter((g) => cfg.gates.defs[g]),
|
|
11252
|
+
openPr: false
|
|
11253
|
+
}),
|
|
11254
|
+
owner: `ab:${label}`,
|
|
11255
|
+
permissionMode: cfg.dispatch.permission_mode ?? "acceptEdits",
|
|
11256
|
+
...a.model ? { model: a.model } : {},
|
|
11257
|
+
...cfg.dispatch.max_turns ? { maxTurns: cfg.dispatch.max_turns } : {}
|
|
11258
|
+
});
|
|
11259
|
+
if (r.ok)
|
|
11260
|
+
started.push(label);
|
|
11261
|
+
else
|
|
11262
|
+
failed.push({ arm: label, reason: r.reason });
|
|
11263
|
+
}
|
|
11264
|
+
store.append({
|
|
11265
|
+
ts: new Date().toISOString(),
|
|
11266
|
+
type: "dispatch.queued",
|
|
11267
|
+
projectId: b.projectId,
|
|
11268
|
+
sessionId: null,
|
|
11269
|
+
payload: {
|
|
11270
|
+
task: b.task,
|
|
11271
|
+
arms: arms.map((a) => a.label ?? a.model),
|
|
11272
|
+
summary: `A/B ${b.task}: ${started.length} arm${started.length === 1 ? "" : "s"} started`
|
|
11273
|
+
}
|
|
11274
|
+
});
|
|
11275
|
+
return c.json({ ok: failed.length === 0, started, failed }, failed.length ? 207 : 200);
|
|
11276
|
+
});
|
|
11277
|
+
app.get("/v1/provenance", async (c) => {
|
|
11278
|
+
const project = c.req.query("project") || undefined;
|
|
11279
|
+
const { branches } = await outcomesFor(project);
|
|
11280
|
+
const projects = store.projects().filter((p) => !project || p.id === project);
|
|
11281
|
+
const tasks2 = [];
|
|
11282
|
+
for (const p of projects)
|
|
11283
|
+
for (const t of store.tasks(p.id)?.tasks ?? [])
|
|
11284
|
+
tasks2.push({ id: t.id, title: t.title, status: t.statusText, url: null });
|
|
11285
|
+
const claims = store.claims(project).map((cl) => ({
|
|
11286
|
+
task: cl.task,
|
|
11287
|
+
sessionId: cl.sessionId,
|
|
11288
|
+
owner: cl.owner || null,
|
|
11289
|
+
worktree: cl.worktree || null,
|
|
11290
|
+
branch: cl.branch || null,
|
|
11291
|
+
acquiredAt: cl.acquiredAt,
|
|
11292
|
+
state: cl.state
|
|
11293
|
+
}));
|
|
11294
|
+
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
11295
|
+
id: s.id,
|
|
11296
|
+
title: s.title,
|
|
11297
|
+
agent: s.agent,
|
|
11298
|
+
branch: s.branch,
|
|
11299
|
+
costUsd: s.costUsd
|
|
11300
|
+
}));
|
|
11301
|
+
return c.json(provenance(tasks2, claims, sessions, branches));
|
|
11302
|
+
});
|
|
8666
11303
|
app.get("/v1/memory", (c) => {
|
|
8667
11304
|
const q = c.req.query("q") ?? "";
|
|
8668
11305
|
const kind = c.req.query("kind");
|
|
@@ -8676,13 +11313,33 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8676
11313
|
})
|
|
8677
11314
|
});
|
|
8678
11315
|
});
|
|
11316
|
+
app.post("/v1/backup", async (c) => {
|
|
11317
|
+
const b = await c.req.json().catch(() => ({}));
|
|
11318
|
+
if (typeof b.dest !== "string" || !b.dest.startsWith("/"))
|
|
11319
|
+
return c.json({ error: "dest must be an absolute path" }, 400);
|
|
11320
|
+
try {
|
|
11321
|
+
return c.json(store.backupTo(b.dest));
|
|
11322
|
+
} catch (e) {
|
|
11323
|
+
return c.json({ error: e.message }, 500);
|
|
11324
|
+
}
|
|
11325
|
+
});
|
|
11326
|
+
app.get("/v1/team", (c) => c.json(team2.status()));
|
|
11327
|
+
app.post("/v1/team/credentials", async (c) => {
|
|
11328
|
+
const b = await c.req.json().catch(() => ({}));
|
|
11329
|
+
if (typeof b.token !== "string" || !b.token)
|
|
11330
|
+
return c.json({ error: "token required" }, 400);
|
|
11331
|
+
store.setMetaValue("team_machine_token", b.token);
|
|
11332
|
+
if (typeof b.policyPublicKey === "string" && b.policyPublicKey)
|
|
11333
|
+
store.setMetaValue("team_policy_pubkey", b.policyPublicKey);
|
|
11334
|
+
return c.json({ ok: true, machine: store.machineIdentity() });
|
|
11335
|
+
});
|
|
8679
11336
|
app.get("/v1/policy", (c) => {
|
|
8680
11337
|
const id = c.req.query("project");
|
|
8681
11338
|
const p = id ? store.project(id) : null;
|
|
8682
11339
|
if (id && !p)
|
|
8683
11340
|
return c.json({ error: "unknown project" }, 404);
|
|
8684
|
-
const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
8685
|
-
return c.json({ ...policy2, provenance, overridden });
|
|
11341
|
+
const { provenance: provenance3, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
11342
|
+
return c.json({ ...policy2, provenance: provenance3, overridden });
|
|
8686
11343
|
});
|
|
8687
11344
|
app.get("/v1/audit", (c) => {
|
|
8688
11345
|
const since = sinceToIso(c.req.query("since"));
|
|
@@ -9227,18 +11884,24 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9227
11884
|
});
|
|
9228
11885
|
});
|
|
9229
11886
|
});
|
|
9230
|
-
app.get("/", (c) => c.html(readFileSync4(
|
|
11887
|
+
app.get("/", (c) => c.html(readFileSync4(join10(WEB_DIR, "index.html"), "utf8")));
|
|
9231
11888
|
const MIME = { js: "text/javascript", css: "text/css" };
|
|
9232
11889
|
app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
|
|
9233
11890
|
const f = c.req.param("file");
|
|
9234
|
-
const p =
|
|
11891
|
+
const p = join10(WEB_DIR, f);
|
|
9235
11892
|
if (!existsSync7(p))
|
|
9236
11893
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
11894
|
+
const st = statSync2(p);
|
|
11895
|
+
const etag = `W/"${st.size.toString(16)}-${st.mtimeMs.toString(16)}"`;
|
|
11896
|
+
if (c.req.header("if-none-match") === etag)
|
|
11897
|
+
return c.body(null, 304, { etag });
|
|
9237
11898
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
9238
|
-
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
11899
|
+
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain",
|
|
11900
|
+
"cache-control": "no-cache",
|
|
11901
|
+
etag
|
|
9239
11902
|
});
|
|
9240
11903
|
});
|
|
9241
|
-
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2 };
|
|
11904
|
+
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
|
|
9242
11905
|
}
|
|
9243
11906
|
|
|
9244
11907
|
// packages/daemon/src/demo.ts
|
|
@@ -9330,7 +11993,7 @@ function seedDemo(store) {
|
|
|
9330
11993
|
// packages/daemon/src/bin.ts
|
|
9331
11994
|
var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
|
|
9332
11995
|
var appHooks = {};
|
|
9333
|
-
var { app, store, runner } = createApp(new Store, appHooks);
|
|
11996
|
+
var { app, store, runner, team: team2 } = createApp(new Store, appHooks);
|
|
9334
11997
|
if (process.env.SWARM_DEMO === "1" && isEmpty(store))
|
|
9335
11998
|
seedDemo(store);
|
|
9336
11999
|
function serve() {
|
|
@@ -9378,6 +12041,8 @@ if (!DEMO) {
|
|
|
9378
12041
|
store.tailCodex(backfillMs);
|
|
9379
12042
|
store.tailGrok(backfillMs);
|
|
9380
12043
|
store.tailGemini(backfillMs);
|
|
12044
|
+
store.tailAider(backfillMs);
|
|
12045
|
+
store.tailOpencode(backfillMs);
|
|
9381
12046
|
}
|
|
9382
12047
|
var tick = 0;
|
|
9383
12048
|
var tailer = setInterval(() => {
|
|
@@ -9388,6 +12053,8 @@ var tailer = setInterval(() => {
|
|
|
9388
12053
|
store.tailCodex();
|
|
9389
12054
|
store.tailGrok();
|
|
9390
12055
|
store.tailGemini();
|
|
12056
|
+
store.tailAider();
|
|
12057
|
+
store.tailOpencode();
|
|
9391
12058
|
}
|
|
9392
12059
|
store.reapResources();
|
|
9393
12060
|
store.reapProcesses();
|
|
@@ -9395,6 +12062,11 @@ var tailer = setInterval(() => {
|
|
|
9395
12062
|
store.sweepOrphans();
|
|
9396
12063
|
if (tick % 6 === 0)
|
|
9397
12064
|
store.checkBudgets();
|
|
12065
|
+
if (tick % 12 === 0)
|
|
12066
|
+
store.checkModels();
|
|
12067
|
+
if (tick % 2 === 0)
|
|
12068
|
+
store.checkStalls();
|
|
12069
|
+
team2.tick();
|
|
9398
12070
|
}, 5000);
|
|
9399
12071
|
store.refreshAllWorktrees();
|
|
9400
12072
|
var wtRefresh = setInterval(() => void store.refreshAllWorktrees(), 15000);
|