@ra3orblade/swarm 0.9.0 → 0.10.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 +8 -1
- package/dist/swarm-hook.js +2 -0
- package/dist/swarm.js +171 -2
- package/dist/swarmd.js +1179 -20
- package/package.json +1 -1
- package/web/app.js +184 -18
- package/web/index.html +21 -12
- package/web/release-notes.js +1 -1
- package/web/viz.js +47 -1
package/dist/swarmd.js
CHANGED
|
@@ -105,6 +105,150 @@ function actorFromColumns(kind, id, session) {
|
|
|
105
105
|
a.session = session;
|
|
106
106
|
return a;
|
|
107
107
|
}
|
|
108
|
+
// packages/core/src/adapters/aider/history.ts
|
|
109
|
+
var djb2 = (s) => {
|
|
110
|
+
let h = 5381;
|
|
111
|
+
for (let i = 0;i < s.length; i++)
|
|
112
|
+
h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
|
|
113
|
+
return h.toString(36);
|
|
114
|
+
};
|
|
115
|
+
var toks = (s) => {
|
|
116
|
+
if (!s)
|
|
117
|
+
return 0;
|
|
118
|
+
const n = Number.parseFloat(s.replaceAll(",", ""));
|
|
119
|
+
return Math.round(s.trim().endsWith("k") ? n * 1000 : n);
|
|
120
|
+
};
|
|
121
|
+
var HEADER = /^# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
|
|
122
|
+
var MODEL = /^> Model: (\S+) with /;
|
|
123
|
+
var TOKENS = /^(?:> )?Tokens: ([\d.,]+k?) sent(?:, ([\d.,]+k?) cache write)?(?:, ([\d.,]+k?) cache hit)?, ([\d.,]+k?) received\./;
|
|
124
|
+
var COST = /Cost: \$([\d.]+(?:e-?\d+)?) message/;
|
|
125
|
+
var EDIT = /^> Applied edit to (.+)/;
|
|
126
|
+
var COMMIT = /^> Commit [0-9a-f]{6,}/;
|
|
127
|
+
function parseAiderHistory(chunk, seed, carry) {
|
|
128
|
+
const segments = [];
|
|
129
|
+
let cur = carry ? {
|
|
130
|
+
sessionId: carry.sessionId,
|
|
131
|
+
startMs: carry.startMs,
|
|
132
|
+
model: carry.model,
|
|
133
|
+
title: carry.title,
|
|
134
|
+
turns: [],
|
|
135
|
+
c: { ...carry }
|
|
136
|
+
} : null;
|
|
137
|
+
const closeTurn = (t, cost) => {
|
|
138
|
+
if (!cur)
|
|
139
|
+
return;
|
|
140
|
+
t.cost = cost;
|
|
141
|
+
cur.turns.push(t);
|
|
142
|
+
cur.c.turns++;
|
|
143
|
+
cur.c.text = "";
|
|
144
|
+
cur.c.tools = [];
|
|
145
|
+
cur.c.pending = null;
|
|
146
|
+
};
|
|
147
|
+
const flushPending = () => {
|
|
148
|
+
if (cur?.c.pending)
|
|
149
|
+
closeTurn(cur.c.pending, null);
|
|
150
|
+
};
|
|
151
|
+
for (const line of chunk.split(`
|
|
152
|
+
`)) {
|
|
153
|
+
const h = line.match(HEADER);
|
|
154
|
+
if (h) {
|
|
155
|
+
flushPending();
|
|
156
|
+
if (cur)
|
|
157
|
+
segments.push(cur);
|
|
158
|
+
const stamp = h[1] ?? "";
|
|
159
|
+
const startMs = Date.parse(stamp.replace(" ", "T"));
|
|
160
|
+
const sessionId = `aider-${djb2(`${seed}|${stamp}`)}`;
|
|
161
|
+
cur = {
|
|
162
|
+
sessionId,
|
|
163
|
+
startMs,
|
|
164
|
+
model: null,
|
|
165
|
+
title: null,
|
|
166
|
+
turns: [],
|
|
167
|
+
c: {
|
|
168
|
+
sessionId,
|
|
169
|
+
startMs,
|
|
170
|
+
model: null,
|
|
171
|
+
title: null,
|
|
172
|
+
turns: 0,
|
|
173
|
+
text: "",
|
|
174
|
+
tools: [],
|
|
175
|
+
pending: null
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!cur)
|
|
181
|
+
continue;
|
|
182
|
+
const c = cur.c;
|
|
183
|
+
if (c.pending) {
|
|
184
|
+
const cost = line.match(COST);
|
|
185
|
+
if (cost) {
|
|
186
|
+
closeTurn(c.pending, Number.parseFloat(cost[1] ?? "0"));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
flushPending();
|
|
190
|
+
}
|
|
191
|
+
const m = line.match(MODEL);
|
|
192
|
+
if (m) {
|
|
193
|
+
cur.model = (m[1] ?? "").split("/").pop() || null;
|
|
194
|
+
c.model = cur.model;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const tk = line.match(TOKENS);
|
|
198
|
+
if (tk) {
|
|
199
|
+
const cacheRead = toks(tk[3]);
|
|
200
|
+
const turn = {
|
|
201
|
+
id: `${c.sessionId}-t${c.turns}`,
|
|
202
|
+
ts: new Date(c.startMs + c.turns * 1000).toISOString(),
|
|
203
|
+
model: c.model ?? "aider",
|
|
204
|
+
usage: {
|
|
205
|
+
input: Math.max(0, toks(tk[1]) - cacheRead),
|
|
206
|
+
output: toks(tk[4]),
|
|
207
|
+
cacheWrite: toks(tk[2]),
|
|
208
|
+
cacheWrite1h: 0,
|
|
209
|
+
cacheRead,
|
|
210
|
+
thinking: 0
|
|
211
|
+
},
|
|
212
|
+
text: c.text,
|
|
213
|
+
tools: c.tools,
|
|
214
|
+
effort: null,
|
|
215
|
+
sidechain: false
|
|
216
|
+
};
|
|
217
|
+
const cost = line.match(COST);
|
|
218
|
+
if (cost)
|
|
219
|
+
closeTurn(turn, Number.parseFloat(cost[1] ?? "0"));
|
|
220
|
+
else
|
|
221
|
+
c.pending = turn;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (line.startsWith("#### ")) {
|
|
225
|
+
const t = line.slice(5).trim();
|
|
226
|
+
if (t && !cur.title) {
|
|
227
|
+
cur.title = t.slice(0, 80);
|
|
228
|
+
c.title = cur.title;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (EDIT.test(line)) {
|
|
233
|
+
c.tools.push("edit");
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (COMMIT.test(line)) {
|
|
237
|
+
c.tools.push("commit");
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (line.startsWith(">"))
|
|
241
|
+
continue;
|
|
242
|
+
if (line.trim() && c.text.length < 400)
|
|
243
|
+
c.text = `${c.text}${c.text ? `
|
|
244
|
+
` : ""}${line}`.slice(0, 400);
|
|
245
|
+
}
|
|
246
|
+
if (cur)
|
|
247
|
+
segments.push(cur);
|
|
248
|
+
const last = cur ? { ...cur.c } : null;
|
|
249
|
+
return { segments, carry: last };
|
|
250
|
+
}
|
|
251
|
+
|
|
108
252
|
// packages/core/src/adapters/claude-code/transcript.ts
|
|
109
253
|
function parseTranscriptChunk(chunk) {
|
|
110
254
|
const out = {
|
|
@@ -385,6 +529,62 @@ function parseGrokUpdates(chunk) {
|
|
|
385
529
|
}
|
|
386
530
|
return out;
|
|
387
531
|
}
|
|
532
|
+
|
|
533
|
+
// packages/core/src/adapters/opencode/db.ts
|
|
534
|
+
var ocModel = (d) => {
|
|
535
|
+
if (typeof d.model === "string")
|
|
536
|
+
return d.model;
|
|
537
|
+
if (d.model && typeof d.model === "object" && typeof d.model.id === "string")
|
|
538
|
+
return d.model.id;
|
|
539
|
+
return typeof d.modelID === "string" ? d.modelID : null;
|
|
540
|
+
};
|
|
541
|
+
var ocTs = (t, fallbackMs) => {
|
|
542
|
+
if (typeof t === "number")
|
|
543
|
+
return new Date(t).toISOString();
|
|
544
|
+
if (typeof t === "string" && !Number.isNaN(Date.parse(t)))
|
|
545
|
+
return new Date(t).toISOString();
|
|
546
|
+
return new Date(fallbackMs).toISOString();
|
|
547
|
+
};
|
|
548
|
+
function opencodeTurn(sessionId, msgId, data, fallbackMs = 0, sidechain = false) {
|
|
549
|
+
let d;
|
|
550
|
+
try {
|
|
551
|
+
d = JSON.parse(data);
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if ((d.type ?? d.role) !== "assistant")
|
|
556
|
+
return null;
|
|
557
|
+
const t = d.tokens ?? {};
|
|
558
|
+
let text = "";
|
|
559
|
+
const tools = [];
|
|
560
|
+
for (const p of Array.isArray(d.content) ? d.content : []) {
|
|
561
|
+
if (p?.type === "text" && typeof p.text === "string" && text.length < 400)
|
|
562
|
+
text = `${text}${p.text}`.slice(0, 400);
|
|
563
|
+
else if (p?.type === "tool") {
|
|
564
|
+
const name = p.tool ?? p.name;
|
|
565
|
+
if (name)
|
|
566
|
+
tools.push(name);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
id: `${sessionId}-${msgId}`,
|
|
571
|
+
ts: ocTs(d.time?.created, fallbackMs),
|
|
572
|
+
model: ocModel(d) ?? "opencode",
|
|
573
|
+
usage: {
|
|
574
|
+
input: t.input ?? 0,
|
|
575
|
+
output: t.output ?? 0,
|
|
576
|
+
cacheWrite: t.cache?.write ?? 0,
|
|
577
|
+
cacheWrite1h: 0,
|
|
578
|
+
cacheRead: t.cache?.read ?? 0,
|
|
579
|
+
thinking: t.reasoning ?? 0
|
|
580
|
+
},
|
|
581
|
+
text,
|
|
582
|
+
tools,
|
|
583
|
+
effort: null,
|
|
584
|
+
sidechain,
|
|
585
|
+
cost: typeof d.cost === "number" && d.cost > 0 ? d.cost : null
|
|
586
|
+
};
|
|
587
|
+
}
|
|
388
588
|
// packages/core/src/adapters/claude-code/hooks.ts
|
|
389
589
|
var HOOK_EVENTS = [
|
|
390
590
|
"SessionStart",
|
|
@@ -796,6 +996,9 @@ var DEFAULT_CONFIG = {
|
|
|
796
996
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
797
997
|
workflows: {},
|
|
798
998
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
999
|
+
models: { allow: [] },
|
|
1000
|
+
notify: { webhook: null },
|
|
1001
|
+
team: { url: null, forward: ["ledger", "cost"], interval: 5 },
|
|
799
1002
|
events: { retain_days: 30 },
|
|
800
1003
|
audit: { retain_days: 0 },
|
|
801
1004
|
privacy: DEFAULT_PRIVACY,
|
|
@@ -891,6 +1094,26 @@ function validate(c) {
|
|
|
891
1094
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
892
1095
|
},
|
|
893
1096
|
workflows: parseWorkflows(c.workflows),
|
|
1097
|
+
notify: {
|
|
1098
|
+
webhook: (() => {
|
|
1099
|
+
const w = c.notify?.webhook;
|
|
1100
|
+
return typeof w === "string" && /^https?:\/\//.test(w.trim()) ? w.trim() : null;
|
|
1101
|
+
})()
|
|
1102
|
+
},
|
|
1103
|
+
models: {
|
|
1104
|
+
allow: Array.isArray(c.models?.allow) ? c.models.allow.filter((m) => typeof m === "string" && m.trim() !== "") : []
|
|
1105
|
+
},
|
|
1106
|
+
team: (() => {
|
|
1107
|
+
const t = c.team ?? {};
|
|
1108
|
+
const url = typeof t.url === "string" && /^https?:\/\//.test(t.url.trim()) ? t.url.trim().replace(/\/+$/, "") : null;
|
|
1109
|
+
const iv = Number(t.interval);
|
|
1110
|
+
const KINDS = ["ledger", "cost", "transcripts"];
|
|
1111
|
+
return {
|
|
1112
|
+
url,
|
|
1113
|
+
forward: Array.isArray(t.forward) ? t.forward.filter((k) => typeof k === "string" && KINDS.includes(k)) : ["ledger", "cost"],
|
|
1114
|
+
interval: Number.isFinite(iv) && iv >= 1 && iv <= 300 ? Math.round(iv) : 5
|
|
1115
|
+
};
|
|
1116
|
+
})(),
|
|
894
1117
|
events: {
|
|
895
1118
|
retain_days: days(c.events?.retain_days, 30)
|
|
896
1119
|
},
|
|
@@ -1505,6 +1728,41 @@ function executedGateInput(task, gate, cmd, outcome) {
|
|
|
1505
1728
|
evidence: evidenceTail(outcome.output) || null
|
|
1506
1729
|
};
|
|
1507
1730
|
}
|
|
1731
|
+
// packages/core/src/graphs.ts
|
|
1732
|
+
function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
1733
|
+
const files = new Map;
|
|
1734
|
+
const sessions = new Map;
|
|
1735
|
+
for (const r of rows) {
|
|
1736
|
+
if (!r.path || !r.sessionId)
|
|
1737
|
+
continue;
|
|
1738
|
+
const f = files.get(r.path) ?? { readers: new Set, writers: new Set };
|
|
1739
|
+
const s = sessions.get(r.sessionId) ?? { files: new Set, writes: 0 };
|
|
1740
|
+
if (writeTools.has(r.tool)) {
|
|
1741
|
+
f.writers.add(r.sessionId);
|
|
1742
|
+
s.writes++;
|
|
1743
|
+
} else
|
|
1744
|
+
f.readers.add(r.sessionId);
|
|
1745
|
+
s.files.add(r.path);
|
|
1746
|
+
files.set(r.path, f);
|
|
1747
|
+
sessions.set(r.sessionId, s);
|
|
1748
|
+
}
|
|
1749
|
+
const out = [...files.entries()].map(([path, f]) => {
|
|
1750
|
+
const writers = [...f.writers].sort();
|
|
1751
|
+
const readers = [...f.readers].filter((id) => !f.writers.has(id)).sort();
|
|
1752
|
+
const touchers = writers.length + readers.length;
|
|
1753
|
+
return { path, readers, writers, contested: touchers >= 2 && writers.length >= 1 };
|
|
1754
|
+
});
|
|
1755
|
+
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));
|
|
1756
|
+
return {
|
|
1757
|
+
sessions: [...sessions.entries()].map(([id, s]) => ({
|
|
1758
|
+
id,
|
|
1759
|
+
files: s.files.size,
|
|
1760
|
+
writes: s.writes
|
|
1761
|
+
})),
|
|
1762
|
+
files: out,
|
|
1763
|
+
contested: out.filter((f) => f.contested).length
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1508
1766
|
// packages/core/src/ledger.ts
|
|
1509
1767
|
var DEFAULT_LEASE_MINUTES = 45;
|
|
1510
1768
|
function isExpired(claim, now) {
|
|
@@ -1838,6 +2096,92 @@ ${lines.join(`
|
|
|
1838
2096
|
`)}
|
|
1839
2097
|
Reply with swarm_send if a reply is expected.`;
|
|
1840
2098
|
}
|
|
2099
|
+
// packages/core/src/outcomes.ts
|
|
2100
|
+
var DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
|
|
2101
|
+
var median = (xs) => {
|
|
2102
|
+
if (!xs.length)
|
|
2103
|
+
return null;
|
|
2104
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
2105
|
+
const mid = Math.floor(s.length / 2);
|
|
2106
|
+
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
2107
|
+
};
|
|
2108
|
+
function scorecard(key, rows) {
|
|
2109
|
+
const merged = rows.filter((r) => r.outcome === "merged");
|
|
2110
|
+
const reverted = rows.filter((r) => r.outcome === "reverted");
|
|
2111
|
+
const open = rows.filter((r) => r.outcome === "open");
|
|
2112
|
+
const noPr = rows.filter((r) => r.outcome === "no-pr");
|
|
2113
|
+
const finished = merged.length + reverted.length + noPr.length;
|
|
2114
|
+
const mergedCost = merged.reduce((a, r) => a + r.costUsd, 0);
|
|
2115
|
+
return {
|
|
2116
|
+
key,
|
|
2117
|
+
branches: rows.length,
|
|
2118
|
+
merged: merged.length,
|
|
2119
|
+
reverted: reverted.length,
|
|
2120
|
+
open: open.length,
|
|
2121
|
+
noPr: noPr.length,
|
|
2122
|
+
mergeRate: finished ? merged.length / finished : null,
|
|
2123
|
+
medianLeadHours: median(merged.map((r) => r.leadHours).filter((x) => x != null)),
|
|
2124
|
+
costPerMerge: merged.length ? mergedCost / merged.length : null
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function outcomeReport(sessions, prs, revertedShas) {
|
|
2128
|
+
const byBranch = new Map;
|
|
2129
|
+
for (const s of sessions) {
|
|
2130
|
+
if (!s.branch || DEFAULT_BRANCHES.has(s.branch))
|
|
2131
|
+
continue;
|
|
2132
|
+
const a = byBranch.get(s.branch) ?? [];
|
|
2133
|
+
a.push(s);
|
|
2134
|
+
byBranch.set(s.branch, a);
|
|
2135
|
+
}
|
|
2136
|
+
const prByBranch = new Map;
|
|
2137
|
+
for (const pr of prs) {
|
|
2138
|
+
const prev = prByBranch.get(pr.branch);
|
|
2139
|
+
if (!prev || pr.state === "merged" && prev.state !== "merged" || pr.state === prev.state && pr.number > prev.number)
|
|
2140
|
+
prByBranch.set(pr.branch, pr);
|
|
2141
|
+
}
|
|
2142
|
+
const rows = [...byBranch.entries()].map(([branch, ss]) => {
|
|
2143
|
+
const dominant = [...ss].sort((a, b) => (b.costUsd ?? 0) - (a.costUsd ?? 0) || a.startedAt.localeCompare(b.startedAt))[0];
|
|
2144
|
+
const pr = prByBranch.get(branch) ?? null;
|
|
2145
|
+
const wasReverted = (sha) => {
|
|
2146
|
+
if (!sha)
|
|
2147
|
+
return false;
|
|
2148
|
+
const s = sha.toLowerCase();
|
|
2149
|
+
for (const r of revertedShas)
|
|
2150
|
+
if (s.startsWith(r) || r.startsWith(s))
|
|
2151
|
+
return true;
|
|
2152
|
+
return false;
|
|
2153
|
+
};
|
|
2154
|
+
const outcome = !pr ? "no-pr" : pr.state === "open" ? "open" : wasReverted(pr.mergeSha) ? "reverted" : "merged";
|
|
2155
|
+
const firstStart = ss.map((s) => s.startedAt).sort()[0];
|
|
2156
|
+
const leadHours = outcome === "merged" && pr?.mergedAt ? Math.max(0, (new Date(pr.mergedAt).getTime() - new Date(firstStart).getTime()) / 3600000) : null;
|
|
2157
|
+
return {
|
|
2158
|
+
branch,
|
|
2159
|
+
outcome,
|
|
2160
|
+
prNumber: pr?.number ?? null,
|
|
2161
|
+
title: pr?.title ?? null,
|
|
2162
|
+
url: pr?.url ?? null,
|
|
2163
|
+
mergedAt: pr?.mergedAt ?? null,
|
|
2164
|
+
leadHours,
|
|
2165
|
+
sessions: ss.map((s) => s.id),
|
|
2166
|
+
model: dominant.model,
|
|
2167
|
+
agent: dominant.agent,
|
|
2168
|
+
costUsd: ss.reduce((a, s) => a + (s.costUsd ?? 0), 0)
|
|
2169
|
+
};
|
|
2170
|
+
});
|
|
2171
|
+
rows.sort((a, b) => (b.mergedAt ?? "").localeCompare(a.mergedAt ?? "") || a.branch.localeCompare(b.branch));
|
|
2172
|
+
const group = (key) => {
|
|
2173
|
+
const m = new Map;
|
|
2174
|
+
for (const r of rows) {
|
|
2175
|
+
const k = key(r) ?? "unknown";
|
|
2176
|
+
m.set(k, [...m.get(k) ?? [], r]);
|
|
2177
|
+
}
|
|
2178
|
+
return [...m.entries()].map(([k, rs]) => scorecard(k, rs)).sort((a, b) => b.branches - a.branches);
|
|
2179
|
+
};
|
|
2180
|
+
return { branches: rows, byModel: group((r) => r.model), byAgent: group((r) => r.agent) };
|
|
2181
|
+
}
|
|
2182
|
+
function parseReverts(gitLog) {
|
|
2183
|
+
return new Set([...gitLog.matchAll(/This reverts commit ([0-9a-f]{7,40})/gi)].map((m) => m[1].toLowerCase()));
|
|
2184
|
+
}
|
|
1841
2185
|
// packages/core/src/policy.ts
|
|
1842
2186
|
import { createHash } from "crypto";
|
|
1843
2187
|
var HOOK_MARK = "swarm-hook";
|
|
@@ -2178,6 +2522,49 @@ ${lines.join(`
|
|
|
2178
2522
|
`)}` : ""}`
|
|
2179
2523
|
};
|
|
2180
2524
|
}
|
|
2525
|
+
// packages/core/src/stall.ts
|
|
2526
|
+
var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
|
|
2527
|
+
function toolResponseErrored(resp) {
|
|
2528
|
+
if (typeof resp === "string")
|
|
2529
|
+
return /^\s*error[:\s]/i.test(resp);
|
|
2530
|
+
if (!resp || typeof resp !== "object")
|
|
2531
|
+
return false;
|
|
2532
|
+
const r = resp;
|
|
2533
|
+
if (r.is_error === true || r.isError === true)
|
|
2534
|
+
return true;
|
|
2535
|
+
if (r.success === false)
|
|
2536
|
+
return true;
|
|
2537
|
+
if (r.interrupted === true)
|
|
2538
|
+
return true;
|
|
2539
|
+
if (typeof r.error === "string" && r.error.length > 0)
|
|
2540
|
+
return true;
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
function detectStall(calls, opts = {}) {
|
|
2544
|
+
const o = { ...STALL_DEFAULTS, ...opts };
|
|
2545
|
+
const tail = calls.slice(-o.window);
|
|
2546
|
+
const last = tail.at(-1);
|
|
2547
|
+
if (!last)
|
|
2548
|
+
return null;
|
|
2549
|
+
let run = 0;
|
|
2550
|
+
let runErrors = 0;
|
|
2551
|
+
for (let i = tail.length - 1;i >= 0; i--) {
|
|
2552
|
+
const c = tail[i];
|
|
2553
|
+
if (!c || c.tool !== last.tool || c.input !== last.input)
|
|
2554
|
+
break;
|
|
2555
|
+
run++;
|
|
2556
|
+
if (c.errored)
|
|
2557
|
+
runErrors++;
|
|
2558
|
+
}
|
|
2559
|
+
if (run >= o.repeat && runErrors >= o.repeatErrors)
|
|
2560
|
+
return { kind: "repeat", reason: `repeating a failing ${last.tool} call \xD7${run}` };
|
|
2561
|
+
let streak = 0;
|
|
2562
|
+
for (let i = tail.length - 1;i >= 0 && tail[i]?.errored; i--)
|
|
2563
|
+
streak++;
|
|
2564
|
+
if (streak >= o.errors)
|
|
2565
|
+
return { kind: "errors", reason: `${streak} tool calls failing in a row` };
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2181
2568
|
// packages/core/src/tasks.ts
|
|
2182
2569
|
var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
|
|
2183
2570
|
var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
|
|
@@ -2337,6 +2724,35 @@ function linearIssuesQuery(teamKey, first = 200) {
|
|
|
2337
2724
|
inverseRelations { nodes { type issue { identifier } } }
|
|
2338
2725
|
} } }`;
|
|
2339
2726
|
}
|
|
2727
|
+
// packages/core/src/team.ts
|
|
2728
|
+
import { createPublicKey, verify as nodeVerify } from "crypto";
|
|
2729
|
+
function modelAllowed(model, allow) {
|
|
2730
|
+
if (!allow.length)
|
|
2731
|
+
return true;
|
|
2732
|
+
return allow.some((g) => {
|
|
2733
|
+
const re = new RegExp(`^${g.trim().split("*").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`, "i");
|
|
2734
|
+
return re.test(model);
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
function verifyPolicySignature(toml, signatureB64, publicKeyB64) {
|
|
2738
|
+
try {
|
|
2739
|
+
return nodeVerify(null, Buffer.from(toml), createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" }), Buffer.from(signatureB64, "base64"));
|
|
2740
|
+
} catch {
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function clusterProjectKey(remoteUrl) {
|
|
2745
|
+
if (!remoteUrl)
|
|
2746
|
+
return null;
|
|
2747
|
+
const url = remoteUrl.trim();
|
|
2748
|
+
const m = url.match(/^https?:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/) ?? url.match(/^(?:ssh:\/\/)?(?:[^@/]+@)?([^:/]+)[:/](.+?)(?:\.git)?\/?$/);
|
|
2749
|
+
if (!m?.[1] || !m[2])
|
|
2750
|
+
return null;
|
|
2751
|
+
const host = m[1].toLowerCase();
|
|
2752
|
+
if (host.includes(" ") || !host.includes("."))
|
|
2753
|
+
return null;
|
|
2754
|
+
return `${host}/${m[2]}`;
|
|
2755
|
+
}
|
|
2340
2756
|
// packages/core/src/worktree.ts
|
|
2341
2757
|
import { join as join3 } from "path";
|
|
2342
2758
|
function planBootstrap(cfg, repoRoot, worktree) {
|
|
@@ -2413,7 +2829,7 @@ function planGc(worktrees, claims) {
|
|
|
2413
2829
|
// packages/daemon/src/app.ts
|
|
2414
2830
|
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
2415
2831
|
import { homedir as homedir4 } from "os";
|
|
2416
|
-
import { dirname as dirname4, join as
|
|
2832
|
+
import { dirname as dirname4, join as join10 } from "path";
|
|
2417
2833
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2418
2834
|
|
|
2419
2835
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
@@ -4343,6 +4759,64 @@ class ForgeService {
|
|
|
4343
4759
|
}
|
|
4344
4760
|
}));
|
|
4345
4761
|
}
|
|
4762
|
+
outcomeCache = new Map;
|
|
4763
|
+
async merged(projectId, root) {
|
|
4764
|
+
const hit = this.outcomeCache.get(projectId);
|
|
4765
|
+
if (hit && Date.now() - hit.at < 600000)
|
|
4766
|
+
return hit;
|
|
4767
|
+
let merged = [];
|
|
4768
|
+
const remote = this.remote(root);
|
|
4769
|
+
if (remote?.forge === "github") {
|
|
4770
|
+
const out = await this.run([
|
|
4771
|
+
"gh",
|
|
4772
|
+
"pr",
|
|
4773
|
+
"list",
|
|
4774
|
+
"--state",
|
|
4775
|
+
"merged",
|
|
4776
|
+
"--limit",
|
|
4777
|
+
"200",
|
|
4778
|
+
"--json",
|
|
4779
|
+
"number,title,headRefName,url,createdAt,mergedAt,mergeCommit"
|
|
4780
|
+
], root);
|
|
4781
|
+
if (out)
|
|
4782
|
+
merged = JSON.parse(out).map((r) => ({
|
|
4783
|
+
branch: String(r.headRefName ?? ""),
|
|
4784
|
+
number: Number(r.number ?? 0),
|
|
4785
|
+
title: String(r.title ?? ""),
|
|
4786
|
+
url: String(r.url ?? ""),
|
|
4787
|
+
createdAt: r.createdAt ?? null,
|
|
4788
|
+
mergedAt: r.mergedAt ?? null,
|
|
4789
|
+
mergeSha: (r.mergeCommit?.oid ?? null)?.toLowerCase() ?? null
|
|
4790
|
+
}));
|
|
4791
|
+
} else if (remote?.forge === "gitlab") {
|
|
4792
|
+
const out = await this.run(["glab", "mr", "list", "--merged", "--output", "json"], root);
|
|
4793
|
+
if (out)
|
|
4794
|
+
merged = JSON.parse(out).map((r) => ({
|
|
4795
|
+
branch: String(r.source_branch ?? ""),
|
|
4796
|
+
number: Number(r.iid ?? 0),
|
|
4797
|
+
title: String(r.title ?? ""),
|
|
4798
|
+
url: String(r.web_url ?? ""),
|
|
4799
|
+
createdAt: r.created_at ?? null,
|
|
4800
|
+
mergedAt: r.merged_at ?? null,
|
|
4801
|
+
mergeSha: (r.merge_commit_sha ?? null)?.toLowerCase() ?? null
|
|
4802
|
+
}));
|
|
4803
|
+
}
|
|
4804
|
+
const log = Bun.spawnSync([
|
|
4805
|
+
"git",
|
|
4806
|
+
"-C",
|
|
4807
|
+
root,
|
|
4808
|
+
"log",
|
|
4809
|
+
"--grep",
|
|
4810
|
+
"This reverts commit",
|
|
4811
|
+
"--format=%B",
|
|
4812
|
+
"-n",
|
|
4813
|
+
"300"
|
|
4814
|
+
]);
|
|
4815
|
+
const reverted = log.exitCode === 0 ? [...parseReverts(new TextDecoder().decode(log.stdout))] : [];
|
|
4816
|
+
const entry = { at: Date.now(), merged, reverted };
|
|
4817
|
+
this.outcomeCache.set(projectId, entry);
|
|
4818
|
+
return entry;
|
|
4819
|
+
}
|
|
4346
4820
|
remote(root) {
|
|
4347
4821
|
const r = Bun.spawnSync(["git", "-C", root, "remote", "get-url", "origin"]);
|
|
4348
4822
|
if (r.exitCode !== 0)
|
|
@@ -4575,6 +5049,16 @@ function currentBranch(cwd) {
|
|
|
4575
5049
|
branchCache.set(cwd, { v: v === "HEAD" ? "(detached)" : v, t: now });
|
|
4576
5050
|
return branchCache.get(cwd)?.v ?? null;
|
|
4577
5051
|
}
|
|
5052
|
+
var originCache = new Map;
|
|
5053
|
+
function originUrl(root) {
|
|
5054
|
+
const hit = originCache.get(root);
|
|
5055
|
+
const now = Date.now();
|
|
5056
|
+
if (hit && now - hit.t < 300000)
|
|
5057
|
+
return hit.v;
|
|
5058
|
+
const v = git(root, ["config", "--get", "remote.origin.url"])?.trim() || null;
|
|
5059
|
+
originCache.set(root, { v, t: now });
|
|
5060
|
+
return v;
|
|
5061
|
+
}
|
|
4578
5062
|
function worktreeAdd(repoRoot, path, branch, baseRef = "HEAD") {
|
|
4579
5063
|
const branchExists = git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]) !== null;
|
|
4580
5064
|
const args = branchExists ? ["worktree", "add", path, branch] : ["worktree", "add", "-b", branch, path, baseRef];
|
|
@@ -4706,6 +5190,14 @@ class Runner {
|
|
|
4706
5190
|
return { ok: false, reason: "unknown project" };
|
|
4707
5191
|
if (!input.prompt.trim())
|
|
4708
5192
|
return { ok: false, reason: "prompt is required" };
|
|
5193
|
+
if (input.model) {
|
|
5194
|
+
const allow = this.store.config(input.projectId).models.allow;
|
|
5195
|
+
if (!modelAllowed(input.model, allow))
|
|
5196
|
+
return {
|
|
5197
|
+
ok: false,
|
|
5198
|
+
reason: `model "${input.model}" is not in [models] allow (${allow.join(", ")})`
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
4709
5201
|
if (input.permissionMode && !PERMISSION_MODES.includes(input.permissionMode))
|
|
4710
5202
|
return { ok: false, reason: `permission mode must be one of ${PERMISSION_MODES.join(", ")}` };
|
|
4711
5203
|
if (this.get(input.task)?.projectId === input.projectId)
|
|
@@ -5004,6 +5496,7 @@ class Runner {
|
|
|
5004
5496
|
import { Database } from "bun:sqlite";
|
|
5005
5497
|
import {
|
|
5006
5498
|
closeSync,
|
|
5499
|
+
copyFileSync,
|
|
5007
5500
|
existsSync as existsSync6,
|
|
5008
5501
|
mkdirSync as mkdirSync4,
|
|
5009
5502
|
openSync as openSync3,
|
|
@@ -5016,7 +5509,7 @@ import {
|
|
|
5016
5509
|
unlinkSync,
|
|
5017
5510
|
writeFileSync as writeFileSync2
|
|
5018
5511
|
} from "fs";
|
|
5019
|
-
import { homedir as homedir3, tmpdir, userInfo } from "os";
|
|
5512
|
+
import { homedir as homedir3, hostname, tmpdir, userInfo } from "os";
|
|
5020
5513
|
import { basename, dirname as dirname3, join as join8 } from "path";
|
|
5021
5514
|
|
|
5022
5515
|
// packages/daemon/src/bootstrap.ts
|
|
@@ -5126,14 +5619,14 @@ class TaskSources {
|
|
|
5126
5619
|
`)[0] ?? code}`);
|
|
5127
5620
|
return normalizeGithubIssues(JSON.parse(out));
|
|
5128
5621
|
}
|
|
5129
|
-
async linear(
|
|
5622
|
+
async linear(team2) {
|
|
5130
5623
|
const key = this.env.LINEAR_API_KEY;
|
|
5131
5624
|
if (!key)
|
|
5132
5625
|
throw new Error("LINEAR_API_KEY not set \u2014 export it in the environment swarmd starts from (never stored)");
|
|
5133
5626
|
const r = await fetch("https://api.linear.app/graphql", {
|
|
5134
5627
|
method: "POST",
|
|
5135
5628
|
headers: { "content-type": "application/json", authorization: key },
|
|
5136
|
-
body: JSON.stringify({ query: linearIssuesQuery(
|
|
5629
|
+
body: JSON.stringify({ query: linearIssuesQuery(team2) })
|
|
5137
5630
|
});
|
|
5138
5631
|
if (!r.ok)
|
|
5139
5632
|
throw new Error(`Linear API ${r.status}`);
|
|
@@ -5159,11 +5652,12 @@ CREATE INDEX IF NOT EXISTS events_type_seq ON events(type, seq);
|
|
|
5159
5652
|
CREATE TABLE IF NOT EXISTS turns (
|
|
5160
5653
|
id TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, ts TEXT, model TEXT, effort TEXT, sidechain INTEGER,
|
|
5161
5654
|
input INTEGER, output INTEGER, cache_write INTEGER, cache_write_1h INTEGER, cache_read INTEGER, thinking INTEGER,
|
|
5162
|
-
cost_usd REAL, text TEXT, tools TEXT
|
|
5655
|
+
cost_usd REAL, cost_fixed INTEGER DEFAULT 0, text TEXT, tools TEXT
|
|
5163
5656
|
);
|
|
5164
5657
|
CREATE INDEX IF NOT EXISTS turns_session ON turns(session_id, ts);
|
|
5165
5658
|
CREATE INDEX IF NOT EXISTS turns_ts ON turns(ts);
|
|
5166
5659
|
CREATE TABLE IF NOT EXISTS tails (path TEXT PRIMARY KEY, session_id TEXT, agent_id TEXT, offset INTEGER);
|
|
5660
|
+
CREATE TABLE IF NOT EXISTS outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, payload TEXT, created_at TEXT);
|
|
5167
5661
|
CREATE TABLE IF NOT EXISTS resources (
|
|
5168
5662
|
name TEXT, project_id TEXT, kind TEXT, owner TEXT, session_id TEXT,
|
|
5169
5663
|
pid INTEGER, port INTEGER, acquired_at TEXT, expires_at TEXT, released INTEGER DEFAULT 0,
|
|
@@ -5229,6 +5723,8 @@ class Store {
|
|
|
5229
5723
|
this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA mmap_size=268435456; PRAGMA cache_size=-32000;");
|
|
5230
5724
|
this.db.exec(SCHEMA);
|
|
5231
5725
|
this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
|
|
5726
|
+
this.ensureColumn("claims", "team_state", "TEXT");
|
|
5727
|
+
this.ensureColumn("turns", "cost_fixed", "INTEGER DEFAULT 0");
|
|
5232
5728
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
5233
5729
|
this.ensureColumn("projects", "icon", "TEXT");
|
|
5234
5730
|
this.ensureColumn("projects", "color", "TEXT");
|
|
@@ -5254,6 +5750,12 @@ class Store {
|
|
|
5254
5750
|
setMeta(key, value) {
|
|
5255
5751
|
this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
5256
5752
|
}
|
|
5753
|
+
metaValue(key) {
|
|
5754
|
+
return this.meta(key) || null;
|
|
5755
|
+
}
|
|
5756
|
+
setMetaValue(key, value) {
|
|
5757
|
+
this.setMeta(key, value);
|
|
5758
|
+
}
|
|
5257
5759
|
slimExistingEvents() {
|
|
5258
5760
|
if (this.meta("events_slim") === "1")
|
|
5259
5761
|
return;
|
|
@@ -6414,6 +6916,18 @@ ${err}
|
|
|
6414
6916
|
};
|
|
6415
6917
|
return { decision: d, display: input.command ?? input.file_path ?? tool };
|
|
6416
6918
|
}
|
|
6919
|
+
const tb = this.teamBudgets().find((x) => x.level === "exceeded" && x.on_exceed === "ask" && (x.scope !== "project" || x.key === this.clusterKeyFor(project.id)));
|
|
6920
|
+
if (tb) {
|
|
6921
|
+
const label = tb.scope === "org" ? "the org" : `${tb.scope} ${tb.key}`;
|
|
6922
|
+
return {
|
|
6923
|
+
decision: {
|
|
6924
|
+
action: "ask",
|
|
6925
|
+
rule: "budget",
|
|
6926
|
+
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)`
|
|
6927
|
+
},
|
|
6928
|
+
display: input.command ?? input.file_path ?? tool
|
|
6929
|
+
};
|
|
6930
|
+
}
|
|
6417
6931
|
}
|
|
6418
6932
|
const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
|
|
6419
6933
|
const cmd = tool === "Bash" ? input.command : undefined;
|
|
@@ -6581,7 +7095,7 @@ ${err}
|
|
|
6581
7095
|
this.reprice();
|
|
6582
7096
|
}
|
|
6583
7097
|
reprice() {
|
|
6584
|
-
const rows = this.db.query("SELECT id, model, input, output, cache_write, cache_write_1h, cache_read FROM turns").all();
|
|
7098
|
+
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
7099
|
const up = this.db.query("UPDATE turns SET cost_usd = ? WHERE id = ?");
|
|
6586
7100
|
const tx = this.db.transaction(() => {
|
|
6587
7101
|
for (const r of rows)
|
|
@@ -6742,12 +7256,69 @@ ${err}
|
|
|
6742
7256
|
if (stored.type === "incident.opened")
|
|
6743
7257
|
this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
|
|
6744
7258
|
this.projectSession(stored);
|
|
7259
|
+
if (stored.type === "incident.opened") {
|
|
7260
|
+
const webhook = this.policyFor(null).config.notify.webhook;
|
|
7261
|
+
if (webhook) {
|
|
7262
|
+
const p2 = stored.payload ?? {};
|
|
7263
|
+
const project = this.project(stored.projectId)?.name ?? stored.projectId;
|
|
7264
|
+
fetch(webhook, {
|
|
7265
|
+
method: "POST",
|
|
7266
|
+
headers: { "content-type": "application/json" },
|
|
7267
|
+
body: JSON.stringify({
|
|
7268
|
+
text: `Swarm incident \xB7 ${p2.rule ?? "?"} \xB7 ${project}
|
|
7269
|
+
${p2.command ?? ""}
|
|
7270
|
+
${p2.reason ?? ""}`.trim(),
|
|
7271
|
+
rule: p2.rule,
|
|
7272
|
+
project,
|
|
7273
|
+
sessionId: stored.sessionId,
|
|
7274
|
+
ts: stored.ts
|
|
7275
|
+
}),
|
|
7276
|
+
signal: AbortSignal.timeout(5000)
|
|
7277
|
+
}).catch(() => {});
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
const team2 = this.policyFor(null).config.team;
|
|
7281
|
+
if (team2.url && team2.forward.includes("ledger") && isAuditType(stored.type)) {
|
|
7282
|
+
this.db.query("INSERT INTO outbox (kind, payload, created_at) VALUES ('event', ?, ?)").run(JSON.stringify({
|
|
7283
|
+
seq: stored.seq,
|
|
7284
|
+
ts: stored.ts,
|
|
7285
|
+
type: stored.type,
|
|
7286
|
+
projectId: stored.projectId,
|
|
7287
|
+
sessionId: stored.sessionId,
|
|
7288
|
+
actor: actor2,
|
|
7289
|
+
payload: slim.payload ?? null
|
|
7290
|
+
}), stored.ts);
|
|
7291
|
+
}
|
|
6745
7292
|
this.touch();
|
|
6746
7293
|
const wire = toWire(stored);
|
|
6747
7294
|
for (const l of this.listeners)
|
|
6748
7295
|
l(wire);
|
|
6749
7296
|
return stored;
|
|
6750
7297
|
}
|
|
7298
|
+
outboxPending(limit = 200) {
|
|
7299
|
+
return this.db.query("SELECT seq, kind, payload FROM outbox ORDER BY seq LIMIT ?").all(limit);
|
|
7300
|
+
}
|
|
7301
|
+
outboxAck(upTo) {
|
|
7302
|
+
this.db.query("DELETE FROM outbox WHERE seq <= ?").run(upTo);
|
|
7303
|
+
}
|
|
7304
|
+
outboxStatus() {
|
|
7305
|
+
const r = this.db.query("SELECT COUNT(*) AS n, MIN(created_at) AS oldest FROM outbox").get();
|
|
7306
|
+
return { pending: r.n, oldest: r.oldest };
|
|
7307
|
+
}
|
|
7308
|
+
machineIdentity() {
|
|
7309
|
+
let id = this.meta("machine_id");
|
|
7310
|
+
if (!id) {
|
|
7311
|
+
id = crypto.randomUUID();
|
|
7312
|
+
this.setMeta("machine_id", id);
|
|
7313
|
+
}
|
|
7314
|
+
return { id, name: hostname() };
|
|
7315
|
+
}
|
|
7316
|
+
spendRollup(day = new Date().toISOString().slice(0, 10)) {
|
|
7317
|
+
return this.db.query(`SELECT s.project_id AS projectId, COALESCE(s.agent, 'claude-code') AS agent, t.model AS model,
|
|
7318
|
+
SUM(t.cost_usd) AS cost, SUM(t.input + t.cache_write + t.cache_read) AS tokensIn, SUM(t.output) AS tokensOut
|
|
7319
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
7320
|
+
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`);
|
|
7321
|
+
}
|
|
6751
7322
|
audit(opts = {}) {
|
|
6752
7323
|
const where = [`type IN (${AUDIT_TYPES_SQL})`];
|
|
6753
7324
|
const args = [];
|
|
@@ -6875,13 +7446,13 @@ ${err}
|
|
|
6875
7446
|
persistTurns(sessionId, agentId, turns) {
|
|
6876
7447
|
const privacy = this.policyFor(null).config.privacy;
|
|
6877
7448
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7449
|
+
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)
|
|
7450
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6880
7451
|
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`);
|
|
7452
|
+
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
7453
|
const tx = this.db.transaction((ts) => {
|
|
6883
7454
|
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));
|
|
7455
|
+
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
7456
|
}
|
|
6886
7457
|
});
|
|
6887
7458
|
if (turns.length)
|
|
@@ -7091,6 +7662,160 @@ ${err}
|
|
|
7091
7662
|
}
|
|
7092
7663
|
return n;
|
|
7093
7664
|
}
|
|
7665
|
+
heldClaimsForSync() {
|
|
7666
|
+
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) => ({
|
|
7667
|
+
projectId: r.project_id,
|
|
7668
|
+
task: r.task,
|
|
7669
|
+
acquiredAt: r.acquired_at,
|
|
7670
|
+
expiresAt: r.expires_at,
|
|
7671
|
+
actorKind: r.actor_kind ?? null,
|
|
7672
|
+
actorId: r.actor_id ?? null,
|
|
7673
|
+
teamState: r.team_state ?? null
|
|
7674
|
+
}));
|
|
7675
|
+
}
|
|
7676
|
+
markClaimTeamState(projectId, task, state) {
|
|
7677
|
+
this.db.query("UPDATE claims SET team_state = ? WHERE project_id = ? AND task = ?").run(state, projectId, task);
|
|
7678
|
+
}
|
|
7679
|
+
revokeClaimConflict(projectId, task, holder) {
|
|
7680
|
+
const row = this.db.query("SELECT state FROM claims WHERE project_id = ? AND task = ?").get(projectId, task);
|
|
7681
|
+
if (row?.state !== "held")
|
|
7682
|
+
return;
|
|
7683
|
+
const now = new Date().toISOString();
|
|
7684
|
+
this.db.query("UPDATE claims SET state = 'released', released_at = ?, team_state = 'conflict' WHERE project_id = ? AND task = ?").run(now, projectId, task);
|
|
7685
|
+
this.append({
|
|
7686
|
+
ts: now,
|
|
7687
|
+
type: "claim.released",
|
|
7688
|
+
projectId,
|
|
7689
|
+
sessionId: null,
|
|
7690
|
+
payload: { task, summary: `revoked \u2014 the team ledger holds ${task} on ${holder}` }
|
|
7691
|
+
});
|
|
7692
|
+
this.append({
|
|
7693
|
+
ts: now,
|
|
7694
|
+
type: "incident.opened",
|
|
7695
|
+
projectId,
|
|
7696
|
+
sessionId: null,
|
|
7697
|
+
payload: {
|
|
7698
|
+
rule: "claim_conflict",
|
|
7699
|
+
action: "revoked",
|
|
7700
|
+
command: task,
|
|
7701
|
+
reason: `the team daemon holds ${task} for ${holder}; the local claim was revoked \u2014 the worktree is untouched`
|
|
7702
|
+
}
|
|
7703
|
+
});
|
|
7704
|
+
}
|
|
7705
|
+
aiderCarries = new Map;
|
|
7706
|
+
recoverAiderCarry(sessionId) {
|
|
7707
|
+
const s = this.db.query("SELECT started_at, model, title FROM sessions WHERE id = ?").get(sessionId);
|
|
7708
|
+
if (!s)
|
|
7709
|
+
return null;
|
|
7710
|
+
const t = this.db.query("SELECT COUNT(*) AS n FROM turns WHERE session_id = ?").get(sessionId);
|
|
7711
|
+
return {
|
|
7712
|
+
sessionId,
|
|
7713
|
+
startMs: Date.parse(s.started_at) || 0,
|
|
7714
|
+
model: s.model,
|
|
7715
|
+
title: s.title,
|
|
7716
|
+
turns: t.n,
|
|
7717
|
+
text: "",
|
|
7718
|
+
tools: [],
|
|
7719
|
+
pending: null
|
|
7720
|
+
};
|
|
7721
|
+
}
|
|
7722
|
+
tailAider(windowMs = 3 * 24 * 60 * 60000) {
|
|
7723
|
+
const roots = this.db.query("SELECT DISTINCT root FROM projects WHERE root IS NOT NULL AND root != ''").all();
|
|
7724
|
+
let n = 0;
|
|
7725
|
+
for (const { root } of roots) {
|
|
7726
|
+
const path = join8(root, ".aider.chat.history.md");
|
|
7727
|
+
let mtime;
|
|
7728
|
+
try {
|
|
7729
|
+
mtime = statSync(path).mtimeMs;
|
|
7730
|
+
} catch {
|
|
7731
|
+
continue;
|
|
7732
|
+
}
|
|
7733
|
+
if (mtime < Date.now() - windowMs)
|
|
7734
|
+
continue;
|
|
7735
|
+
const row = this.db.query("SELECT offset, session_id FROM tails WHERE path = ?").get(path);
|
|
7736
|
+
const r = this.readFrom(path, row?.offset ?? 0);
|
|
7737
|
+
if (!r)
|
|
7738
|
+
continue;
|
|
7739
|
+
let carry = this.aiderCarries.get(path) ?? null;
|
|
7740
|
+
if (!carry && row?.session_id)
|
|
7741
|
+
carry = this.recoverAiderCarry(row.session_id);
|
|
7742
|
+
const { segments, carry: next } = parseAiderHistory(r.chunk, path, carry);
|
|
7743
|
+
this.aiderCarries.set(path, next);
|
|
7744
|
+
const lastSeg = segments.at(-1);
|
|
7745
|
+
for (const seg of segments) {
|
|
7746
|
+
this.ensureAgentSession(seg.sessionId, "aider", root, seg.startMs || mtime);
|
|
7747
|
+
this.persistTurns(seg.sessionId, null, seg.turns);
|
|
7748
|
+
const live = seg === lastSeg && Date.now() - mtime < 90000;
|
|
7749
|
+
const lastSeen = new Date(seg === lastSeg ? mtime : seg.startMs + seg.turns.length * 1000).toISOString();
|
|
7750
|
+
const lastText = [...seg.turns].reverse().find((t) => t.text)?.text ?? null;
|
|
7751
|
+
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);
|
|
7752
|
+
n += seg.turns.length;
|
|
7753
|
+
}
|
|
7754
|
+
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);
|
|
7755
|
+
}
|
|
7756
|
+
return n;
|
|
7757
|
+
}
|
|
7758
|
+
ocDbs = new Map;
|
|
7759
|
+
tailOpencode(windowMs = 3 * 24 * 60 * 60000) {
|
|
7760
|
+
const dir = process.env.SWARM_OPENCODE_DIR ?? join8(process.env.XDG_DATA_HOME ?? join8(homedir3(), ".local", "share"), "opencode");
|
|
7761
|
+
let files;
|
|
7762
|
+
try {
|
|
7763
|
+
files = readdirSync(dir).filter((f) => /^opencode[^/]*\.db$/.test(f));
|
|
7764
|
+
} catch {
|
|
7765
|
+
return 0;
|
|
7766
|
+
}
|
|
7767
|
+
let n = 0;
|
|
7768
|
+
for (const f of files) {
|
|
7769
|
+
const path = join8(dir, f);
|
|
7770
|
+
let db = this.ocDbs.get(path);
|
|
7771
|
+
if (!db) {
|
|
7772
|
+
try {
|
|
7773
|
+
db = new Database(path, { readonly: true });
|
|
7774
|
+
} catch {
|
|
7775
|
+
continue;
|
|
7776
|
+
}
|
|
7777
|
+
this.ocDbs.set(path, db);
|
|
7778
|
+
}
|
|
7779
|
+
const row = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path);
|
|
7780
|
+
const lower = Math.max(row?.offset ?? 0, Date.now() - windowMs);
|
|
7781
|
+
let rows;
|
|
7782
|
+
try {
|
|
7783
|
+
rows = db.query(`SELECT m.id, m.session_id, m.time_created, m.time_updated, m.data,
|
|
7784
|
+
s.directory, s.title, s.parent_id
|
|
7785
|
+
FROM message m JOIN session s ON s.id = m.session_id
|
|
7786
|
+
WHERE m.time_updated > ? ORDER BY m.time_updated ASC LIMIT 2000`).all(lower);
|
|
7787
|
+
} catch {
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
if (!rows.length)
|
|
7791
|
+
continue;
|
|
7792
|
+
let cursor = lower;
|
|
7793
|
+
const bySession = new Map;
|
|
7794
|
+
for (const m of rows) {
|
|
7795
|
+
cursor = Math.max(cursor, m.time_updated ?? 0);
|
|
7796
|
+
const g = bySession.get(m.session_id) ?? { rows: [], last: 0 };
|
|
7797
|
+
g.rows.push(m);
|
|
7798
|
+
g.last = Math.max(g.last, m.time_updated ?? m.time_created ?? 0);
|
|
7799
|
+
bySession.set(m.session_id, g);
|
|
7800
|
+
}
|
|
7801
|
+
for (const [sid, g] of bySession) {
|
|
7802
|
+
const first = g.rows[0];
|
|
7803
|
+
if (!first)
|
|
7804
|
+
continue;
|
|
7805
|
+
this.ensureAgentSession(sid, "opencode", first.directory ?? "", first.time_created ?? g.last);
|
|
7806
|
+
const turns = g.rows.map((m) => opencodeTurn(sid, m.id, m.data, m.time_created ?? 0, first.parent_id != null)).filter((t) => t != null);
|
|
7807
|
+
this.persistTurns(sid, null, turns);
|
|
7808
|
+
const live = Date.now() - g.last < 90000;
|
|
7809
|
+
const lastSeen = new Date(g.last).toISOString();
|
|
7810
|
+
const lastText = [...turns].reverse().find((t) => t.text)?.text ?? null;
|
|
7811
|
+
const model = [...turns].reverse().find((t) => t.model !== "opencode")?.model ?? null;
|
|
7812
|
+
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);
|
|
7813
|
+
n += turns.length;
|
|
7814
|
+
}
|
|
7815
|
+
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);
|
|
7816
|
+
}
|
|
7817
|
+
return n;
|
|
7818
|
+
}
|
|
7094
7819
|
ingestLog(path, agent, parse, cwdHint, titleHint) {
|
|
7095
7820
|
const off = this.db.query("SELECT offset FROM tails WHERE path = ?").get(path) ?? { offset: 0 };
|
|
7096
7821
|
const r = this.readFrom(path, off.offset);
|
|
@@ -7272,6 +7997,77 @@ ${err}
|
|
|
7272
7997
|
expiresAt: r.expires_at
|
|
7273
7998
|
}));
|
|
7274
7999
|
}
|
|
8000
|
+
collisions(projectId) {
|
|
8001
|
+
const cutoff = new Date(Date.now() - IDLE_MS).toISOString();
|
|
8002
|
+
const live = this.db.query(`SELECT id, project_id, title, agent, kind FROM sessions
|
|
8003
|
+
WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [cutoff, projectId] : [cutoff]);
|
|
8004
|
+
if (!live.length)
|
|
8005
|
+
return { sessions: [], files: [], contested: 0 };
|
|
8006
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool, json_extract(payload,'$.toolInput.file_path') AS path
|
|
8007
|
+
FROM events WHERE type = 'tool.requested' AND session_id IN (${live.map(() => "?").join(",")})
|
|
8008
|
+
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL`).all(...live.map((s) => s.id));
|
|
8009
|
+
const g = collisionGraph(rows.map((r) => ({ sessionId: r.session_id, tool: r.tool ?? "", path: r.path ?? "" })));
|
|
8010
|
+
const meta = new Map(live.map((s) => [s.id, s]));
|
|
8011
|
+
return {
|
|
8012
|
+
...g,
|
|
8013
|
+
sessions: g.sessions.map((s) => {
|
|
8014
|
+
const m = meta.get(s.id);
|
|
8015
|
+
return {
|
|
8016
|
+
...s,
|
|
8017
|
+
title: m?.title ?? null,
|
|
8018
|
+
agent: m?.agent ?? "claude-code",
|
|
8019
|
+
projectId: m?.project_id ?? null
|
|
8020
|
+
};
|
|
8021
|
+
})
|
|
8022
|
+
};
|
|
8023
|
+
}
|
|
8024
|
+
stalls = new Map;
|
|
8025
|
+
checkStalls() {
|
|
8026
|
+
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());
|
|
8027
|
+
const liveIds = new Set(live.map((s) => s.id));
|
|
8028
|
+
for (const id of [...this.stalls.keys()])
|
|
8029
|
+
if (!liveIds.has(id))
|
|
8030
|
+
this.stalls.delete(id);
|
|
8031
|
+
let flagged = 0;
|
|
8032
|
+
for (const s of live) {
|
|
8033
|
+
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);
|
|
8034
|
+
const calls = rows.reverse().map((r) => {
|
|
8035
|
+
let p = {};
|
|
8036
|
+
try {
|
|
8037
|
+
p = JSON.parse(r.payload || "{}");
|
|
8038
|
+
} catch {}
|
|
8039
|
+
return {
|
|
8040
|
+
tool: typeof p.tool === "string" ? p.tool : "?",
|
|
8041
|
+
input: JSON.stringify(p.toolInput ?? null),
|
|
8042
|
+
errored: toolResponseErrored(p.toolResponse),
|
|
8043
|
+
ts: ""
|
|
8044
|
+
};
|
|
8045
|
+
});
|
|
8046
|
+
const stall2 = detectStall(calls);
|
|
8047
|
+
if (!stall2) {
|
|
8048
|
+
this.stalls.delete(s.id);
|
|
8049
|
+
continue;
|
|
8050
|
+
}
|
|
8051
|
+
flagged++;
|
|
8052
|
+
const prev = this.stalls.get(s.id);
|
|
8053
|
+
this.stalls.set(s.id, stall2);
|
|
8054
|
+
if (prev?.kind === stall2.kind)
|
|
8055
|
+
continue;
|
|
8056
|
+
this.append({
|
|
8057
|
+
ts: new Date().toISOString(),
|
|
8058
|
+
type: "session.stuck",
|
|
8059
|
+
projectId: s.project_id,
|
|
8060
|
+
sessionId: s.id,
|
|
8061
|
+
payload: {
|
|
8062
|
+
kind: stall2.kind,
|
|
8063
|
+
reason: stall2.reason,
|
|
8064
|
+
summary: `session looks stuck \u2014 ${stall2.reason}`
|
|
8065
|
+
}
|
|
8066
|
+
});
|
|
8067
|
+
this.touch();
|
|
8068
|
+
}
|
|
8069
|
+
return flagged;
|
|
8070
|
+
}
|
|
7275
8071
|
sweepOrphans() {
|
|
7276
8072
|
const now = Date.now();
|
|
7277
8073
|
let n = 0;
|
|
@@ -7649,6 +8445,7 @@ ${err}
|
|
|
7649
8445
|
lastType: r.last_type,
|
|
7650
8446
|
lastText: r.last_text ?? null,
|
|
7651
8447
|
state,
|
|
8448
|
+
stuck: state === "active" || state === "waiting" ? this.stalls.get(r.id)?.reason ?? null : null,
|
|
7652
8449
|
toolCalls: r.tool_calls,
|
|
7653
8450
|
subagents: r.subagents,
|
|
7654
8451
|
turns: r.turns,
|
|
@@ -7713,8 +8510,120 @@ ${err}
|
|
|
7713
8510
|
fn(p.id, b.status);
|
|
7714
8511
|
this.touch();
|
|
7715
8512
|
}
|
|
8513
|
+
for (const b of this.teamBudgets()) {
|
|
8514
|
+
if (b.level === "ok")
|
|
8515
|
+
continue;
|
|
8516
|
+
const mapKey = `team:${b.scope}:${b.key}`;
|
|
8517
|
+
const seen = `${day}:${b.level}`;
|
|
8518
|
+
const affected = b.scope === "project" ? this.projects().filter((p) => this.clusterKeyFor(p.id) === b.key) : this.projects();
|
|
8519
|
+
if (this.budgetNotified.get(mapKey) !== seen) {
|
|
8520
|
+
this.budgetNotified.set(mapKey, seen);
|
|
8521
|
+
const label = b.scope === "org" ? "the org" : `${b.scope} ${b.key}`;
|
|
8522
|
+
this.append({
|
|
8523
|
+
ts: new Date().toISOString(),
|
|
8524
|
+
type: "incident.opened",
|
|
8525
|
+
projectId: affected[0]?.id ?? "p_unknown",
|
|
8526
|
+
sessionId: null,
|
|
8527
|
+
payload: {
|
|
8528
|
+
rule: "budget",
|
|
8529
|
+
action: b.level === "exceeded" ? b.on_exceed : "warn",
|
|
8530
|
+
command: `team ${b.kind ?? ""} budget \xB7 ${label}`,
|
|
8531
|
+
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`
|
|
8532
|
+
}
|
|
8533
|
+
});
|
|
8534
|
+
this.touch();
|
|
8535
|
+
}
|
|
8536
|
+
if (b.level === "exceeded" && b.on_exceed === "stop")
|
|
8537
|
+
for (const p of affected)
|
|
8538
|
+
for (const fn of this.budgetListeners)
|
|
8539
|
+
fn(p.id, {
|
|
8540
|
+
level: "exceeded",
|
|
8541
|
+
kind: b.kind === "daily" ? "daily" : "weekly",
|
|
8542
|
+
spent: b.spent,
|
|
8543
|
+
limit: b.limit,
|
|
8544
|
+
pct: b.limit ? b.spent / b.limit : 1,
|
|
8545
|
+
daily: { spent: b.spent, limit: b.limit, pct: 1 },
|
|
8546
|
+
weekly: { spent: 0, limit: null, pct: 0 }
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
7716
8549
|
return out;
|
|
7717
8550
|
}
|
|
8551
|
+
teamBudgets() {
|
|
8552
|
+
try {
|
|
8553
|
+
return JSON.parse(this.metaValue("team_budget") ?? "[]");
|
|
8554
|
+
} catch {
|
|
8555
|
+
return [];
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
backupTo(destDir) {
|
|
8559
|
+
mkdirSync4(destDir, { recursive: true });
|
|
8560
|
+
const files = [];
|
|
8561
|
+
const dbDest = join8(destDir, "swarm.db");
|
|
8562
|
+
if (existsSync6(dbDest))
|
|
8563
|
+
unlinkSync(dbDest);
|
|
8564
|
+
this.db.exec(`VACUUM INTO '${dbDest.replaceAll("'", "''")}'`);
|
|
8565
|
+
files.push("swarm.db");
|
|
8566
|
+
for (const f of [
|
|
8567
|
+
"config.toml",
|
|
8568
|
+
"policy.toml",
|
|
8569
|
+
"policy.sig.json",
|
|
8570
|
+
"token",
|
|
8571
|
+
"pricing.json",
|
|
8572
|
+
"pricing.litellm.json",
|
|
8573
|
+
"team-token"
|
|
8574
|
+
]) {
|
|
8575
|
+
const src = join8(this.home, f);
|
|
8576
|
+
if (!existsSync6(src))
|
|
8577
|
+
continue;
|
|
8578
|
+
copyFileSync(src, join8(destDir, f));
|
|
8579
|
+
files.push(f);
|
|
8580
|
+
}
|
|
8581
|
+
return { dest: destDir, files };
|
|
8582
|
+
}
|
|
8583
|
+
clusterKeyCache = new Map;
|
|
8584
|
+
clusterKeyFor(projectId) {
|
|
8585
|
+
const hit = this.clusterKeyCache.get(projectId);
|
|
8586
|
+
if (hit)
|
|
8587
|
+
return hit;
|
|
8588
|
+
const root = this.db.query("SELECT root FROM projects WHERE id = ?").get(projectId)?.root;
|
|
8589
|
+
const key = root && clusterProjectKey(originUrl(root)) || `local:${projectId}`;
|
|
8590
|
+
this.clusterKeyCache.set(projectId, key);
|
|
8591
|
+
return key;
|
|
8592
|
+
}
|
|
8593
|
+
taskSpendRollup(day = new Date().toISOString().slice(0, 10)) {
|
|
8594
|
+
return this.db.query(`SELECT s.project_id AS projectId, c.task AS task, SUM(t.cost_usd) AS cost
|
|
8595
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
8596
|
+
JOIN claims c ON c.project_id = s.project_id AND c.worktree != '' AND (s.cwd = c.worktree OR s.cwd LIKE c.worktree || '/%')
|
|
8597
|
+
WHERE t.ts >= ? AND t.ts < ? GROUP BY s.project_id, c.task`).all(`${day}T00:00:00.000Z`, `${day}T23:59:59.999Z`);
|
|
8598
|
+
}
|
|
8599
|
+
modelFlagged = new Set;
|
|
8600
|
+
checkModels() {
|
|
8601
|
+
const since = new Date(Date.now() - IDLE_MS).toISOString();
|
|
8602
|
+
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);
|
|
8603
|
+
let n = 0;
|
|
8604
|
+
for (const s of rows) {
|
|
8605
|
+
if (this.modelFlagged.has(s.id))
|
|
8606
|
+
continue;
|
|
8607
|
+
const allow = this.config(s.project_id).models.allow;
|
|
8608
|
+
if (!allow.length || modelAllowed(s.model, allow))
|
|
8609
|
+
continue;
|
|
8610
|
+
this.modelFlagged.add(s.id);
|
|
8611
|
+
n++;
|
|
8612
|
+
this.append({
|
|
8613
|
+
ts: new Date().toISOString(),
|
|
8614
|
+
type: "incident.opened",
|
|
8615
|
+
projectId: s.project_id,
|
|
8616
|
+
sessionId: s.id,
|
|
8617
|
+
payload: {
|
|
8618
|
+
rule: "model_allowlist",
|
|
8619
|
+
action: "observed",
|
|
8620
|
+
command: s.model,
|
|
8621
|
+
reason: `session runs on "${s.model}", outside [models] allow (${allow.join(", ")}) \u2014 nothing was interrupted; spawned runs on this model are refused`
|
|
8622
|
+
}
|
|
8623
|
+
});
|
|
8624
|
+
}
|
|
8625
|
+
return n;
|
|
8626
|
+
}
|
|
7718
8627
|
spend() {
|
|
7719
8628
|
const dayStart = new Date;
|
|
7720
8629
|
dayStart.setHours(0, 0, 0, 0);
|
|
@@ -8344,6 +9253,192 @@ function localDayIso(offsetDays) {
|
|
|
8344
9253
|
return d.toISOString();
|
|
8345
9254
|
}
|
|
8346
9255
|
|
|
9256
|
+
// packages/daemon/src/team.ts
|
|
9257
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
9258
|
+
import { join as join9 } from "path";
|
|
9259
|
+
var SPEND_EVERY_MS = 60000;
|
|
9260
|
+
var MAX_BACKOFF_MS = 300000;
|
|
9261
|
+
var POLICY_EVERY_MS = 300000;
|
|
9262
|
+
|
|
9263
|
+
class TeamForwarder {
|
|
9264
|
+
store;
|
|
9265
|
+
version;
|
|
9266
|
+
lastTry = 0;
|
|
9267
|
+
backoffMs = 0;
|
|
9268
|
+
lastSpend = 0;
|
|
9269
|
+
constructor(store, version) {
|
|
9270
|
+
this.store = store;
|
|
9271
|
+
this.version = version;
|
|
9272
|
+
}
|
|
9273
|
+
projectKey(projectId) {
|
|
9274
|
+
return this.store.clusterKeyFor(projectId);
|
|
9275
|
+
}
|
|
9276
|
+
status() {
|
|
9277
|
+
const team2 = this.store.policyFor(null).config.team;
|
|
9278
|
+
const box = this.store.outboxStatus();
|
|
9279
|
+
return {
|
|
9280
|
+
configured: team2.url != null,
|
|
9281
|
+
url: team2.url,
|
|
9282
|
+
forward: team2.forward,
|
|
9283
|
+
pending: box.pending,
|
|
9284
|
+
oldest: box.oldest,
|
|
9285
|
+
lastAckAt: this.store.metaValue("team_last_ack") ?? null,
|
|
9286
|
+
lastError: this.store.metaValue("team_last_error") ?? null,
|
|
9287
|
+
machine: this.store.machineIdentity(),
|
|
9288
|
+
authed: this.store.metaValue("team_machine_token") != null
|
|
9289
|
+
};
|
|
9290
|
+
}
|
|
9291
|
+
async tick(now = Date.now()) {
|
|
9292
|
+
const team2 = this.store.policyFor(null).config.team;
|
|
9293
|
+
if (!team2.url)
|
|
9294
|
+
return 0;
|
|
9295
|
+
if (now - this.lastTry < team2.interval * 1000 + this.backoffMs)
|
|
9296
|
+
return 0;
|
|
9297
|
+
this.lastTry = now;
|
|
9298
|
+
const records = [];
|
|
9299
|
+
const events = team2.forward.includes("ledger") ? this.store.outboxPending() : [];
|
|
9300
|
+
for (const e of events) {
|
|
9301
|
+
const body = JSON.parse(e.payload);
|
|
9302
|
+
if (typeof body.projectId === "string")
|
|
9303
|
+
body.projectKey = this.projectKey(body.projectId);
|
|
9304
|
+
records.push({ seq: e.seq, kind: e.kind, body });
|
|
9305
|
+
}
|
|
9306
|
+
let spendRows = 0;
|
|
9307
|
+
if (team2.forward.includes("cost") && now - this.lastSpend > SPEND_EVERY_MS) {
|
|
9308
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
9309
|
+
for (const r of this.store.spendRollup(day)) {
|
|
9310
|
+
records.push({
|
|
9311
|
+
seq: 0,
|
|
9312
|
+
kind: "spend",
|
|
9313
|
+
body: { day, ...r, projectKey: this.projectKey(r.projectId) }
|
|
9314
|
+
});
|
|
9315
|
+
spendRows++;
|
|
9316
|
+
}
|
|
9317
|
+
for (const r of this.store.taskSpendRollup(day)) {
|
|
9318
|
+
records.push({
|
|
9319
|
+
seq: 0,
|
|
9320
|
+
kind: "spend_task",
|
|
9321
|
+
body: { day, ...r, projectKey: this.projectKey(r.projectId) }
|
|
9322
|
+
});
|
|
9323
|
+
spendRows++;
|
|
9324
|
+
}
|
|
9325
|
+
}
|
|
9326
|
+
const held = this.store.heldClaimsForSync();
|
|
9327
|
+
if (!records.length && !held.length)
|
|
9328
|
+
return 0;
|
|
9329
|
+
const machine = { ...this.store.machineIdentity(), version: this.version };
|
|
9330
|
+
const token = this.store.metaValue("team_machine_token");
|
|
9331
|
+
const headers = {
|
|
9332
|
+
"content-type": "application/json",
|
|
9333
|
+
...token ? { authorization: `Bearer ${token}` } : {}
|
|
9334
|
+
};
|
|
9335
|
+
const base = this.store.policyFor(null).config.team.url;
|
|
9336
|
+
try {
|
|
9337
|
+
if (records.length) {
|
|
9338
|
+
const req = { machine, records };
|
|
9339
|
+
const res = await fetch(`${base}/t1/ingest`, {
|
|
9340
|
+
method: "POST",
|
|
9341
|
+
headers,
|
|
9342
|
+
body: JSON.stringify(req),
|
|
9343
|
+
signal: AbortSignal.timeout(1e4)
|
|
9344
|
+
});
|
|
9345
|
+
if (!res.ok)
|
|
9346
|
+
throw new Error(`ingest ${res.status}`);
|
|
9347
|
+
const reply = await res.json();
|
|
9348
|
+
if (reply.ack > 0)
|
|
9349
|
+
this.store.outboxAck(reply.ack);
|
|
9350
|
+
if (spendRows)
|
|
9351
|
+
this.lastSpend = now;
|
|
9352
|
+
}
|
|
9353
|
+
if (held.length) {
|
|
9354
|
+
const claims = held.map((c) => ({
|
|
9355
|
+
projectKey: this.projectKey(c.projectId),
|
|
9356
|
+
task: c.task,
|
|
9357
|
+
acquiredAt: c.acquiredAt,
|
|
9358
|
+
expiresAt: c.expiresAt,
|
|
9359
|
+
actor: c.actorKind && c.actorId ? { kind: c.actorKind, id: c.actorId } : undefined
|
|
9360
|
+
}));
|
|
9361
|
+
const res = await fetch(`${base}/t1/claims`, {
|
|
9362
|
+
method: "POST",
|
|
9363
|
+
headers,
|
|
9364
|
+
body: JSON.stringify({ machine, claims }),
|
|
9365
|
+
signal: AbortSignal.timeout(1e4)
|
|
9366
|
+
});
|
|
9367
|
+
if (!res.ok)
|
|
9368
|
+
throw new Error(`claims ${res.status}`);
|
|
9369
|
+
const reply = await res.json();
|
|
9370
|
+
for (const r of reply.results) {
|
|
9371
|
+
const local = held.find((c) => c.task === r.task && this.projectKey(c.projectId) === r.projectKey);
|
|
9372
|
+
if (!local)
|
|
9373
|
+
continue;
|
|
9374
|
+
if (r.status === "ok") {
|
|
9375
|
+
if (local.teamState !== "registered")
|
|
9376
|
+
this.store.markClaimTeamState(local.projectId, local.task, "registered");
|
|
9377
|
+
} else
|
|
9378
|
+
this.store.revokeClaimConflict(local.projectId, local.task, r.holder);
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
try {
|
|
9382
|
+
const res = await fetch(`${base}/t1/budget`, {
|
|
9383
|
+
headers,
|
|
9384
|
+
signal: AbortSignal.timeout(1e4)
|
|
9385
|
+
});
|
|
9386
|
+
if (res.ok) {
|
|
9387
|
+
const { budgets } = await res.json();
|
|
9388
|
+
this.store.setMetaValue("team_budget", JSON.stringify(budgets ?? []));
|
|
9389
|
+
}
|
|
9390
|
+
} catch {}
|
|
9391
|
+
this.backoffMs = 0;
|
|
9392
|
+
this.store.setMetaValue("team_last_ack", new Date(now).toISOString());
|
|
9393
|
+
this.store.setMetaValue("team_last_error", "");
|
|
9394
|
+
await this.syncPolicy(base, headers, now);
|
|
9395
|
+
return records.length + held.length;
|
|
9396
|
+
} catch (e) {
|
|
9397
|
+
this.backoffMs = Math.min(this.backoffMs ? this.backoffMs * 2 : 5000, MAX_BACKOFF_MS);
|
|
9398
|
+
this.store.setMetaValue("team_last_error", e.message);
|
|
9399
|
+
return 0;
|
|
9400
|
+
}
|
|
9401
|
+
}
|
|
9402
|
+
lastPolicy = 0;
|
|
9403
|
+
async syncPolicy(base, headers, now) {
|
|
9404
|
+
if (now - this.lastPolicy < POLICY_EVERY_MS)
|
|
9405
|
+
return;
|
|
9406
|
+
this.lastPolicy = now;
|
|
9407
|
+
try {
|
|
9408
|
+
const res = await fetch(`${base}/t1/policy`, {
|
|
9409
|
+
headers,
|
|
9410
|
+
signal: AbortSignal.timeout(1e4)
|
|
9411
|
+
});
|
|
9412
|
+
if (!res.ok)
|
|
9413
|
+
return;
|
|
9414
|
+
const { policy: policy2 } = await res.json();
|
|
9415
|
+
if (!policy2)
|
|
9416
|
+
return;
|
|
9417
|
+
let pinned = this.store.metaValue("team_policy_pubkey");
|
|
9418
|
+
if (!pinned) {
|
|
9419
|
+
pinned = policy2.publicKey;
|
|
9420
|
+
this.store.setMetaValue("team_policy_pubkey", pinned);
|
|
9421
|
+
}
|
|
9422
|
+
if (!verifyPolicySignature(policy2.toml, policy2.signature, pinned)) {
|
|
9423
|
+
this.store.setMetaValue("team_last_error", "org policy signature invalid \u2014 not installed");
|
|
9424
|
+
return;
|
|
9425
|
+
}
|
|
9426
|
+
const file = join9(this.store.home, "policy.toml");
|
|
9427
|
+
const prev = this.store.metaValue("team_policy_sig");
|
|
9428
|
+
if (prev === policy2.signature)
|
|
9429
|
+
return;
|
|
9430
|
+
writeFileSync3(file, policy2.toml, { mode: 384 });
|
|
9431
|
+
writeFileSync3(join9(this.store.home, "policy.sig.json"), JSON.stringify({
|
|
9432
|
+
signature: policy2.signature,
|
|
9433
|
+
publicKey: pinned,
|
|
9434
|
+
fetchedAt: new Date(now).toISOString(),
|
|
9435
|
+
url: base
|
|
9436
|
+
}), { mode: 384 });
|
|
9437
|
+
this.store.setMetaValue("team_policy_sig", policy2.signature);
|
|
9438
|
+
} catch {}
|
|
9439
|
+
}
|
|
9440
|
+
}
|
|
9441
|
+
|
|
8347
9442
|
// packages/daemon/src/workflow.ts
|
|
8348
9443
|
class WorkflowEngine {
|
|
8349
9444
|
store;
|
|
@@ -8535,13 +9630,13 @@ class WorkflowEngine {
|
|
|
8535
9630
|
}
|
|
8536
9631
|
|
|
8537
9632
|
// packages/daemon/src/app.ts
|
|
8538
|
-
var VERSION = "0.
|
|
9633
|
+
var VERSION = "0.10.0";
|
|
8539
9634
|
var WEB_DIR = (() => {
|
|
8540
9635
|
if (process.env.SWARM_WEB_DIR)
|
|
8541
9636
|
return process.env.SWARM_WEB_DIR;
|
|
8542
9637
|
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
8543
|
-
const dev =
|
|
8544
|
-
return existsSync7(
|
|
9638
|
+
const dev = join10(here, "../../web/public");
|
|
9639
|
+
return existsSync7(join10(dev, "index.html")) ? dev : join10(here, "../web");
|
|
8545
9640
|
})();
|
|
8546
9641
|
var REPLAY_TAIL = 200;
|
|
8547
9642
|
var wireCache = new WeakMap;
|
|
@@ -8559,7 +9654,7 @@ function hookRepoRoot(store, raw2) {
|
|
|
8559
9654
|
}
|
|
8560
9655
|
function claudeSettings() {
|
|
8561
9656
|
try {
|
|
8562
|
-
const p = process.env.CLAUDE_SETTINGS ??
|
|
9657
|
+
const p = process.env.CLAUDE_SETTINGS ?? join10(homedir4(), ".claude", "settings.json");
|
|
8563
9658
|
return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
|
|
8564
9659
|
} catch {
|
|
8565
9660
|
return null;
|
|
@@ -8570,7 +9665,7 @@ function diskVersion() {
|
|
|
8570
9665
|
const entry = daemonCommand().at(-1);
|
|
8571
9666
|
if (!entry || !existsSync7(entry))
|
|
8572
9667
|
return null;
|
|
8573
|
-
for (const f of [entry,
|
|
9668
|
+
for (const f of [entry, join10(dirname4(entry), "app.ts")]) {
|
|
8574
9669
|
if (!existsSync7(f))
|
|
8575
9670
|
continue;
|
|
8576
9671
|
const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
|
|
@@ -8588,6 +9683,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8588
9683
|
const runner = new Runner(store, store.home);
|
|
8589
9684
|
const dispatcher = new Dispatcher(store, runner, forge2);
|
|
8590
9685
|
const workflows2 = new WorkflowEngine(store, runner, forge2);
|
|
9686
|
+
const team2 = new TeamForwarder(store, VERSION);
|
|
8591
9687
|
store.onBudgetStop((projectId) => {
|
|
8592
9688
|
dispatcher.clear(projectId);
|
|
8593
9689
|
for (const run2 of runner.list(projectId))
|
|
@@ -8650,7 +9746,7 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8650
9746
|
dir = homedir4();
|
|
8651
9747
|
}
|
|
8652
9748
|
try {
|
|
8653
|
-
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(
|
|
9749
|
+
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
9750
|
const parent = dirname4(dir);
|
|
8655
9751
|
return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
|
|
8656
9752
|
} catch (e) {
|
|
@@ -8659,10 +9755,44 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8659
9755
|
});
|
|
8660
9756
|
app.get("/v1/state", (c) => c.json(store.snapshot()));
|
|
8661
9757
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
9758
|
+
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
8662
9759
|
app.get("/v1/incidents", (c) => c.json(store.incidents(Number(c.req.query("limit") ?? 50), {
|
|
8663
9760
|
open: c.req.query("open") === "1",
|
|
8664
9761
|
projectId: c.req.query("project") || undefined
|
|
8665
9762
|
})));
|
|
9763
|
+
app.get("/v1/outcomes", async (c) => {
|
|
9764
|
+
const project = c.req.query("project") || undefined;
|
|
9765
|
+
const sessions = store.snapshot().sessions.filter((s) => !project || s.projectId === project).map((s) => ({
|
|
9766
|
+
id: s.id,
|
|
9767
|
+
branch: s.branch,
|
|
9768
|
+
model: s.model,
|
|
9769
|
+
agent: s.agent,
|
|
9770
|
+
costUsd: s.costUsd,
|
|
9771
|
+
startedAt: s.startedAt
|
|
9772
|
+
}));
|
|
9773
|
+
const prs = [];
|
|
9774
|
+
const reverted = new Set;
|
|
9775
|
+
for (const p of store.projects().filter((x) => !project || x.id === project)) {
|
|
9776
|
+
const o = await forge2.merged(p.id, p.root);
|
|
9777
|
+
for (const m of o.merged)
|
|
9778
|
+
prs.push({ ...m, state: "merged" });
|
|
9779
|
+
for (const sha of o.reverted)
|
|
9780
|
+
reverted.add(sha);
|
|
9781
|
+
}
|
|
9782
|
+
for (const pr of forge2.prs())
|
|
9783
|
+
if (!project || pr.projectId === project)
|
|
9784
|
+
prs.push({
|
|
9785
|
+
branch: pr.branch,
|
|
9786
|
+
number: pr.number,
|
|
9787
|
+
state: "open",
|
|
9788
|
+
title: pr.title,
|
|
9789
|
+
url: pr.url,
|
|
9790
|
+
createdAt: pr.createdAt,
|
|
9791
|
+
mergedAt: null,
|
|
9792
|
+
mergeSha: null
|
|
9793
|
+
});
|
|
9794
|
+
return c.json(outcomeReport(sessions, prs, reverted));
|
|
9795
|
+
});
|
|
8666
9796
|
app.get("/v1/memory", (c) => {
|
|
8667
9797
|
const q = c.req.query("q") ?? "";
|
|
8668
9798
|
const kind = c.req.query("kind");
|
|
@@ -8676,6 +9806,26 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
8676
9806
|
})
|
|
8677
9807
|
});
|
|
8678
9808
|
});
|
|
9809
|
+
app.post("/v1/backup", async (c) => {
|
|
9810
|
+
const b = await c.req.json().catch(() => ({}));
|
|
9811
|
+
if (typeof b.dest !== "string" || !b.dest.startsWith("/"))
|
|
9812
|
+
return c.json({ error: "dest must be an absolute path" }, 400);
|
|
9813
|
+
try {
|
|
9814
|
+
return c.json(store.backupTo(b.dest));
|
|
9815
|
+
} catch (e) {
|
|
9816
|
+
return c.json({ error: e.message }, 500);
|
|
9817
|
+
}
|
|
9818
|
+
});
|
|
9819
|
+
app.get("/v1/team", (c) => c.json(team2.status()));
|
|
9820
|
+
app.post("/v1/team/credentials", async (c) => {
|
|
9821
|
+
const b = await c.req.json().catch(() => ({}));
|
|
9822
|
+
if (typeof b.token !== "string" || !b.token)
|
|
9823
|
+
return c.json({ error: "token required" }, 400);
|
|
9824
|
+
store.setMetaValue("team_machine_token", b.token);
|
|
9825
|
+
if (typeof b.policyPublicKey === "string" && b.policyPublicKey)
|
|
9826
|
+
store.setMetaValue("team_policy_pubkey", b.policyPublicKey);
|
|
9827
|
+
return c.json({ ok: true, machine: store.machineIdentity() });
|
|
9828
|
+
});
|
|
8679
9829
|
app.get("/v1/policy", (c) => {
|
|
8680
9830
|
const id = c.req.query("project");
|
|
8681
9831
|
const p = id ? store.project(id) : null;
|
|
@@ -9227,18 +10377,18 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
9227
10377
|
});
|
|
9228
10378
|
});
|
|
9229
10379
|
});
|
|
9230
|
-
app.get("/", (c) => c.html(readFileSync4(
|
|
10380
|
+
app.get("/", (c) => c.html(readFileSync4(join10(WEB_DIR, "index.html"), "utf8")));
|
|
9231
10381
|
const MIME = { js: "text/javascript", css: "text/css" };
|
|
9232
10382
|
app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
|
|
9233
10383
|
const f = c.req.param("file");
|
|
9234
|
-
const p =
|
|
10384
|
+
const p = join10(WEB_DIR, f);
|
|
9235
10385
|
if (!existsSync7(p))
|
|
9236
10386
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
9237
10387
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
9238
10388
|
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
9239
10389
|
});
|
|
9240
10390
|
});
|
|
9241
|
-
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2 };
|
|
10391
|
+
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2, team: team2 };
|
|
9242
10392
|
}
|
|
9243
10393
|
|
|
9244
10394
|
// packages/daemon/src/demo.ts
|
|
@@ -9330,7 +10480,7 @@ function seedDemo(store) {
|
|
|
9330
10480
|
// packages/daemon/src/bin.ts
|
|
9331
10481
|
var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
|
|
9332
10482
|
var appHooks = {};
|
|
9333
|
-
var { app, store, runner } = createApp(new Store, appHooks);
|
|
10483
|
+
var { app, store, runner, team: team2 } = createApp(new Store, appHooks);
|
|
9334
10484
|
if (process.env.SWARM_DEMO === "1" && isEmpty(store))
|
|
9335
10485
|
seedDemo(store);
|
|
9336
10486
|
function serve() {
|
|
@@ -9378,6 +10528,8 @@ if (!DEMO) {
|
|
|
9378
10528
|
store.tailCodex(backfillMs);
|
|
9379
10529
|
store.tailGrok(backfillMs);
|
|
9380
10530
|
store.tailGemini(backfillMs);
|
|
10531
|
+
store.tailAider(backfillMs);
|
|
10532
|
+
store.tailOpencode(backfillMs);
|
|
9381
10533
|
}
|
|
9382
10534
|
var tick = 0;
|
|
9383
10535
|
var tailer = setInterval(() => {
|
|
@@ -9388,6 +10540,8 @@ var tailer = setInterval(() => {
|
|
|
9388
10540
|
store.tailCodex();
|
|
9389
10541
|
store.tailGrok();
|
|
9390
10542
|
store.tailGemini();
|
|
10543
|
+
store.tailAider();
|
|
10544
|
+
store.tailOpencode();
|
|
9391
10545
|
}
|
|
9392
10546
|
store.reapResources();
|
|
9393
10547
|
store.reapProcesses();
|
|
@@ -9395,6 +10549,11 @@ var tailer = setInterval(() => {
|
|
|
9395
10549
|
store.sweepOrphans();
|
|
9396
10550
|
if (tick % 6 === 0)
|
|
9397
10551
|
store.checkBudgets();
|
|
10552
|
+
if (tick % 12 === 0)
|
|
10553
|
+
store.checkModels();
|
|
10554
|
+
if (tick % 2 === 0)
|
|
10555
|
+
store.checkStalls();
|
|
10556
|
+
team2.tick();
|
|
9398
10557
|
}, 5000);
|
|
9399
10558
|
store.refreshAllWorktrees();
|
|
9400
10559
|
var wtRefresh = setInterval(() => void store.refreshAllWorktrees(), 15000);
|