@ra3orblade/swarm 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/swarm-hook.js +3 -1
- package/dist/swarm-mcp.js +35 -6
- package/dist/swarm.js +184 -9
- package/dist/swarmd.js +954 -75
- package/package.json +1 -1
- package/web/app.js +150 -11
- package/web/index.html +43 -0
- package/web/release-notes.js +1 -1
- package/web/viz.js +20 -3
package/dist/swarmd.js
CHANGED
|
@@ -3,9 +3,33 @@
|
|
|
3
3
|
|
|
4
4
|
// packages/client/src/daemon.ts
|
|
5
5
|
import { randomBytes } from "crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
// packages/client/src/bins.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { dirname, resolve } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var SRC = {
|
|
15
|
+
swarm: "cli",
|
|
16
|
+
swarmd: "daemon",
|
|
17
|
+
"swarm-hook": "hook",
|
|
18
|
+
"swarm-mcp": "mcp"
|
|
19
|
+
};
|
|
20
|
+
function resolveBin(name, from = import.meta.url) {
|
|
21
|
+
const here = dirname(fileURLToPath(from));
|
|
22
|
+
const candidates = [
|
|
23
|
+
resolve(here, `../../${SRC[name]}/src/bin.ts`),
|
|
24
|
+
resolve(here, `${name}.js`)
|
|
25
|
+
];
|
|
26
|
+
for (const c of candidates)
|
|
27
|
+
if (existsSync(c))
|
|
28
|
+
return ["bun", c];
|
|
29
|
+
return [name];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// packages/client/src/daemon.ts
|
|
9
33
|
function swarmHome() {
|
|
10
34
|
return process.env.SWARM_HOME ?? join(homedir(), ".swarm");
|
|
11
35
|
}
|
|
@@ -41,6 +65,9 @@ function ensureToken(home = swarmHome()) {
|
|
|
41
65
|
function clearDaemonInfo() {
|
|
42
66
|
rmSync(infoFile(), { force: true });
|
|
43
67
|
}
|
|
68
|
+
function daemonCommand() {
|
|
69
|
+
return resolveBin("swarmd");
|
|
70
|
+
}
|
|
44
71
|
// packages/core/src/actor.ts
|
|
45
72
|
var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
46
73
|
var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
@@ -218,6 +245,79 @@ function parseCodexRollout(chunk) {
|
|
|
218
245
|
return out;
|
|
219
246
|
}
|
|
220
247
|
|
|
248
|
+
// packages/core/src/adapters/gemini/chats.ts
|
|
249
|
+
function partText(content) {
|
|
250
|
+
if (typeof content === "string")
|
|
251
|
+
return content.slice(0, 400);
|
|
252
|
+
const parts = Array.isArray(content) ? content : [content];
|
|
253
|
+
let out = "";
|
|
254
|
+
for (const p of parts) {
|
|
255
|
+
if (typeof p === "string")
|
|
256
|
+
out += p;
|
|
257
|
+
else if (p && typeof p === "object" && typeof p.text === "string")
|
|
258
|
+
out += p.text;
|
|
259
|
+
if (out.length >= 400)
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
return out.slice(0, 400);
|
|
263
|
+
}
|
|
264
|
+
function parseGeminiChat(chunk) {
|
|
265
|
+
const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
|
|
266
|
+
let subagent = false;
|
|
267
|
+
for (const raw of chunk.split(`
|
|
268
|
+
`)) {
|
|
269
|
+
if (!raw.trim())
|
|
270
|
+
continue;
|
|
271
|
+
let d = null;
|
|
272
|
+
try {
|
|
273
|
+
d = JSON.parse(raw);
|
|
274
|
+
} catch {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (d.$set) {
|
|
278
|
+
const set = d.$set;
|
|
279
|
+
if (typeof set.summary === "string")
|
|
280
|
+
out.title = set.summary;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (d.sessionId && d.projectHash !== undefined) {
|
|
284
|
+
out.sessionId = d.sessionId;
|
|
285
|
+
if (Array.isArray(d.directories) && typeof d.directories[0] === "string")
|
|
286
|
+
out.cwd = d.directories[0];
|
|
287
|
+
if (typeof d.summary === "string")
|
|
288
|
+
out.title = d.summary;
|
|
289
|
+
if (d.kind === "subagent")
|
|
290
|
+
subagent = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (d.type !== "gemini" || !d.id)
|
|
294
|
+
continue;
|
|
295
|
+
const t = d.tokens ?? {};
|
|
296
|
+
const cacheRead = t.cached ?? 0;
|
|
297
|
+
const turn = {
|
|
298
|
+
id: `${out.sessionId ?? "gemini"}-${d.id}`,
|
|
299
|
+
ts: d.timestamp ?? new Date(0).toISOString(),
|
|
300
|
+
model: d.model ?? out.model ?? "gemini-2.5-pro",
|
|
301
|
+
usage: {
|
|
302
|
+
input: Math.max(0, (t.input ?? 0) - cacheRead),
|
|
303
|
+
output: (t.output ?? 0) + (t.tool ?? 0),
|
|
304
|
+
cacheWrite: 0,
|
|
305
|
+
cacheWrite1h: 0,
|
|
306
|
+
cacheRead,
|
|
307
|
+
thinking: t.thoughts ?? 0
|
|
308
|
+
},
|
|
309
|
+
text: partText(d.content),
|
|
310
|
+
tools: (d.toolCalls ?? []).map((c) => c.name ?? c.displayName ?? c.tool ?? "").filter(Boolean),
|
|
311
|
+
effort: null,
|
|
312
|
+
sidechain: subagent
|
|
313
|
+
};
|
|
314
|
+
if (d.model)
|
|
315
|
+
out.model = d.model;
|
|
316
|
+
out.turns.push(turn);
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
221
321
|
// packages/core/src/adapters/grok/updates.ts
|
|
222
322
|
function parseGrokUpdates(chunk) {
|
|
223
323
|
const out = { turns: [], sessionId: null, model: null, cwd: null, title: null };
|
|
@@ -421,7 +521,9 @@ var AUDIT_TYPES = new Set([
|
|
|
421
521
|
"permission.resolved",
|
|
422
522
|
"incident.opened",
|
|
423
523
|
"incident.acked",
|
|
424
|
-
"run.result"
|
|
524
|
+
"run.result",
|
|
525
|
+
"workflow.started",
|
|
526
|
+
"workflow.finished"
|
|
425
527
|
]);
|
|
426
528
|
var isAuditType = (t) => AUDIT_TYPES.has(t);
|
|
427
529
|
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
@@ -594,16 +696,82 @@ function runProfile(name) {
|
|
|
594
696
|
return RUN_PROFILES[name] ?? null;
|
|
595
697
|
}
|
|
596
698
|
// packages/core/src/config.ts
|
|
597
|
-
import { existsSync as
|
|
699
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
598
700
|
import { join as join2 } from "path";
|
|
701
|
+
|
|
702
|
+
// packages/core/src/workflows.ts
|
|
703
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
704
|
+
function isRecord(v) {
|
|
705
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
706
|
+
}
|
|
707
|
+
function parseWorkflows(raw) {
|
|
708
|
+
const out = {};
|
|
709
|
+
if (!Array.isArray(raw))
|
|
710
|
+
return out;
|
|
711
|
+
for (const w of raw) {
|
|
712
|
+
if (!isRecord(w) || typeof w.name !== "string" || !NAME_RE.test(w.name))
|
|
713
|
+
continue;
|
|
714
|
+
if (!Array.isArray(w.steps) || !w.steps.length)
|
|
715
|
+
continue;
|
|
716
|
+
const prompts = isRecord(w.prompts) ? w.prompts : {};
|
|
717
|
+
const steps = [];
|
|
718
|
+
for (const s of w.steps) {
|
|
719
|
+
if (typeof s !== "string" || !s.trim()) {
|
|
720
|
+
steps.length = 0;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
const t = s.trim();
|
|
724
|
+
if (t === "pr")
|
|
725
|
+
steps.push({ kind: "pr" });
|
|
726
|
+
else if (t.startsWith("gate:")) {
|
|
727
|
+
const gate = t.slice(5);
|
|
728
|
+
if (!NAME_RE.test(gate)) {
|
|
729
|
+
steps.length = 0;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
steps.push({ kind: "gate", gate });
|
|
733
|
+
} else if (NAME_RE.test(t)) {
|
|
734
|
+
const p = prompts[t];
|
|
735
|
+
steps.push({
|
|
736
|
+
kind: "run",
|
|
737
|
+
name: t,
|
|
738
|
+
prompt: typeof p === "string" && p.trim() ? p.trim() : null
|
|
739
|
+
});
|
|
740
|
+
} else {
|
|
741
|
+
steps.length = 0;
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (steps.length)
|
|
746
|
+
out[w.name] = { name: w.name, steps };
|
|
747
|
+
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
function workflowStepPrompt(step, task, ctx) {
|
|
751
|
+
if (step.prompt)
|
|
752
|
+
return step.prompt.replaceAll("{task}", task.id).replaceAll("{title}", task.title ?? "");
|
|
753
|
+
return [
|
|
754
|
+
`Task ${task.id}: ${task.title}`,
|
|
755
|
+
"",
|
|
756
|
+
`You are the "${step.name}" step of the "${ctx.workflow}" workflow. Work only inside this worktree; commit and push as you go.`,
|
|
757
|
+
ctx.remaining.length ? `After you finish, the workflow itself runs: ${ctx.remaining.join(" \u2192 ")}. Do not do those yourself.` : "You are the last step.",
|
|
758
|
+
"When done, call swarm_handoff with what was done and what remains."
|
|
759
|
+
].join(`
|
|
760
|
+
`);
|
|
761
|
+
}
|
|
762
|
+
function stepLabel(s) {
|
|
763
|
+
return s.kind === "run" ? s.name : s.kind === "gate" ? `gate:${s.gate}` : "pr";
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// packages/core/src/config.ts
|
|
599
767
|
var DEFAULT_GATE_TIMEOUT_S = 900;
|
|
600
768
|
var AUTO_MODES = ["session-end", "stop", "off"];
|
|
601
769
|
function parseGateDefs(gates) {
|
|
602
770
|
const out = {};
|
|
603
|
-
if (!
|
|
771
|
+
if (!isRecord2(gates))
|
|
604
772
|
return out;
|
|
605
773
|
for (const [name, v] of Object.entries(gates)) {
|
|
606
|
-
if (!
|
|
774
|
+
if (!isRecord2(v))
|
|
607
775
|
continue;
|
|
608
776
|
const builtin = v.builtin === "review" ? "review" : null;
|
|
609
777
|
const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
|
|
@@ -626,6 +794,7 @@ var DEFAULT_CONFIG = {
|
|
|
626
794
|
daemon: { port: 7777, auth: "loopback-optional" },
|
|
627
795
|
tasks: { source: null, labels: [], team: null },
|
|
628
796
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
797
|
+
workflows: {},
|
|
629
798
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
630
799
|
events: { retain_days: 30 },
|
|
631
800
|
audit: { retain_days: 0 },
|
|
@@ -650,11 +819,11 @@ var DEFAULT_CONFIG = {
|
|
|
650
819
|
}
|
|
651
820
|
};
|
|
652
821
|
var MODES = ["ask", "deny", "off"];
|
|
653
|
-
function
|
|
822
|
+
function isRecord2(v) {
|
|
654
823
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
655
824
|
}
|
|
656
825
|
function merge(a, b) {
|
|
657
|
-
if (!
|
|
826
|
+
if (!isRecord2(a) || !isRecord2(b))
|
|
658
827
|
return b === undefined ? a : b;
|
|
659
828
|
const out = { ...a };
|
|
660
829
|
for (const [k, v] of Object.entries(b))
|
|
@@ -721,6 +890,7 @@ function validate(c) {
|
|
|
721
890
|
warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
|
|
722
891
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
723
892
|
},
|
|
893
|
+
workflows: parseWorkflows(c.workflows),
|
|
724
894
|
events: {
|
|
725
895
|
retain_days: days(c.events?.retain_days, 30)
|
|
726
896
|
},
|
|
@@ -760,7 +930,7 @@ function validate(c) {
|
|
|
760
930
|
};
|
|
761
931
|
}
|
|
762
932
|
function leafPaths(v, prefix = "") {
|
|
763
|
-
if (!
|
|
933
|
+
if (!isRecord2(v))
|
|
764
934
|
return prefix ? [prefix] : [];
|
|
765
935
|
const keys = Object.keys(v);
|
|
766
936
|
if (keys.length === 0)
|
|
@@ -770,7 +940,7 @@ function leafPaths(v, prefix = "") {
|
|
|
770
940
|
function getPath(v, path) {
|
|
771
941
|
let cur = v;
|
|
772
942
|
for (const seg of path.split(".")) {
|
|
773
|
-
if (!
|
|
943
|
+
if (!isRecord2(cur))
|
|
774
944
|
return;
|
|
775
945
|
cur = cur[seg];
|
|
776
946
|
}
|
|
@@ -780,7 +950,7 @@ function setPath(obj, path, value) {
|
|
|
780
950
|
const segs = path.split(".");
|
|
781
951
|
let cur = obj;
|
|
782
952
|
for (const seg of segs.slice(0, -1)) {
|
|
783
|
-
if (!
|
|
953
|
+
if (!isRecord2(cur[seg]))
|
|
784
954
|
cur[seg] = {};
|
|
785
955
|
cur = cur[seg];
|
|
786
956
|
}
|
|
@@ -788,7 +958,7 @@ function setPath(obj, path, value) {
|
|
|
788
958
|
}
|
|
789
959
|
var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
|
|
790
960
|
function readLayer(path) {
|
|
791
|
-
return
|
|
961
|
+
return existsSync3(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
|
|
792
962
|
}
|
|
793
963
|
function loadConfigDetailed(opts = {}) {
|
|
794
964
|
const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
|
|
@@ -1274,11 +1444,11 @@ ${shown.map((f) => `- \`${f.path}\`${f.added >= 0 ? ` +${f.added} \u2212${f.dele
|
|
|
1274
1444
|
`) };
|
|
1275
1445
|
}
|
|
1276
1446
|
// packages/core/src/gates.ts
|
|
1277
|
-
var
|
|
1447
|
+
var NAME_RE2 = /^[a-z0-9][a-z0-9_.-]{0,39}$/i;
|
|
1278
1448
|
function validateGateRun(input) {
|
|
1279
1449
|
if (!input.task?.trim())
|
|
1280
1450
|
return { ok: false, reason: "task is required" };
|
|
1281
|
-
if (!
|
|
1451
|
+
if (!NAME_RE2.test(input.gate ?? ""))
|
|
1282
1452
|
return { ok: false, reason: "gate must be a short name (letters, digits, _ . -)" };
|
|
1283
1453
|
if (input.verdict !== "pass" && input.verdict !== "fail")
|
|
1284
1454
|
return { ok: false, reason: 'verdict must be "pass" or "fail"' };
|
|
@@ -1639,6 +1809,35 @@ function parseMemoryQuery(q) {
|
|
|
1639
1809
|
}
|
|
1640
1810
|
return { match: terms.join(" "), kind, task };
|
|
1641
1811
|
}
|
|
1812
|
+
// packages/core/src/messages.ts
|
|
1813
|
+
var MESSAGE_MAX = 4000;
|
|
1814
|
+
function validateMessage(text) {
|
|
1815
|
+
if (typeof text !== "string" || !text.trim())
|
|
1816
|
+
return { ok: false, reason: "message text is required" };
|
|
1817
|
+
const t = text.trim();
|
|
1818
|
+
if (t.length > MESSAGE_MAX)
|
|
1819
|
+
return { ok: false, reason: `message is over ${MESSAGE_MAX} chars` };
|
|
1820
|
+
return { ok: true, text: t };
|
|
1821
|
+
}
|
|
1822
|
+
function parseTo(to) {
|
|
1823
|
+
if (typeof to !== "string" || !to.trim())
|
|
1824
|
+
return null;
|
|
1825
|
+
const t = to.trim();
|
|
1826
|
+
if (t === "lead")
|
|
1827
|
+
return { kind: "lead" };
|
|
1828
|
+
if (/^[0-9a-f]{8}(-[0-9a-f-]{4,28})?$/i.test(t))
|
|
1829
|
+
return { kind: "session", id: t };
|
|
1830
|
+
return { kind: "task", task: t };
|
|
1831
|
+
}
|
|
1832
|
+
function formatMessages(ms) {
|
|
1833
|
+
if (!ms.length)
|
|
1834
|
+
return null;
|
|
1835
|
+
const lines = ms.map((m) => `- from ${m.from ?? "unknown"}${m.task ? ` (re ${m.task})` : ""}: ${m.text}`);
|
|
1836
|
+
return `[swarm] While you were working, message${ms.length === 1 ? "" : "s"} arrived:
|
|
1837
|
+
${lines.join(`
|
|
1838
|
+
`)}
|
|
1839
|
+
Reply with swarm_send if a reply is expected.`;
|
|
1840
|
+
}
|
|
1642
1841
|
// packages/core/src/policy.ts
|
|
1643
1842
|
import { createHash } from "crypto";
|
|
1644
1843
|
var HOOK_MARK = "swarm-hook";
|
|
@@ -2212,10 +2411,10 @@ function planGc(worktrees, claims) {
|
|
|
2212
2411
|
return out;
|
|
2213
2412
|
}
|
|
2214
2413
|
// packages/daemon/src/app.ts
|
|
2215
|
-
import { existsSync as
|
|
2414
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
2216
2415
|
import { homedir as homedir4 } from "os";
|
|
2217
|
-
import { dirname as
|
|
2218
|
-
import { fileURLToPath } from "url";
|
|
2416
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
2417
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2219
2418
|
|
|
2220
2419
|
// node_modules/.bun/hono@4.13.3/node_modules/hono/dist/compose.js
|
|
2221
2420
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -4088,7 +4287,7 @@ class Dispatcher {
|
|
|
4088
4287
|
}
|
|
4089
4288
|
|
|
4090
4289
|
// packages/daemon/src/forge.ts
|
|
4091
|
-
import { existsSync as
|
|
4290
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4092
4291
|
import { homedir as homedir2 } from "os";
|
|
4093
4292
|
import { join as join4 } from "path";
|
|
4094
4293
|
var EXTRA_BIN_DIRS = [
|
|
@@ -4106,7 +4305,7 @@ function findBin(name) {
|
|
|
4106
4305
|
return onPath;
|
|
4107
4306
|
for (const d of EXTRA_BIN_DIRS) {
|
|
4108
4307
|
const p = join4(d, name);
|
|
4109
|
-
if (
|
|
4308
|
+
if (existsSync4(p))
|
|
4110
4309
|
return p;
|
|
4111
4310
|
}
|
|
4112
4311
|
return null;
|
|
@@ -4805,7 +5004,7 @@ class Runner {
|
|
|
4805
5004
|
import { Database } from "bun:sqlite";
|
|
4806
5005
|
import {
|
|
4807
5006
|
closeSync,
|
|
4808
|
-
existsSync as
|
|
5007
|
+
existsSync as existsSync6,
|
|
4809
5008
|
mkdirSync as mkdirSync4,
|
|
4810
5009
|
openSync as openSync3,
|
|
4811
5010
|
readdirSync,
|
|
@@ -4818,11 +5017,11 @@ import {
|
|
|
4818
5017
|
writeFileSync as writeFileSync2
|
|
4819
5018
|
} from "fs";
|
|
4820
5019
|
import { homedir as homedir3, tmpdir, userInfo } from "os";
|
|
4821
|
-
import { basename, dirname as
|
|
5020
|
+
import { basename, dirname as dirname3, join as join8 } from "path";
|
|
4822
5021
|
|
|
4823
5022
|
// packages/daemon/src/bootstrap.ts
|
|
4824
|
-
import { cpSync, existsSync as
|
|
4825
|
-
import { dirname, join as join7 } from "path";
|
|
5023
|
+
import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
|
|
5024
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
4826
5025
|
function runBootstrap(plan, opts) {
|
|
4827
5026
|
const logDir = join7(opts.home, "logs", opts.projectId);
|
|
4828
5027
|
mkdirSync3(logDir, { recursive: true });
|
|
@@ -4830,12 +5029,12 @@ function runBootstrap(plan, opts) {
|
|
|
4830
5029
|
const copied = [];
|
|
4831
5030
|
const skipped = [];
|
|
4832
5031
|
for (const c of plan.copies) {
|
|
4833
|
-
if (!
|
|
5032
|
+
if (!existsSync5(c.from)) {
|
|
4834
5033
|
skipped.push(c.rel);
|
|
4835
5034
|
continue;
|
|
4836
5035
|
}
|
|
4837
5036
|
try {
|
|
4838
|
-
mkdirSync3(
|
|
5037
|
+
mkdirSync3(dirname2(c.to), { recursive: true });
|
|
4839
5038
|
cpSync(c.from, c.to, { recursive: true, force: true });
|
|
4840
5039
|
copied.push(c.rel);
|
|
4841
5040
|
} catch (e) {
|
|
@@ -4997,6 +5196,12 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
4997
5196
|
answer TEXT, answered_by TEXT, answered_at TEXT, delivered_at TEXT
|
|
4998
5197
|
);
|
|
4999
5198
|
CREATE INDEX IF NOT EXISTS messages_open ON messages(project_id, answered_at, delivered_at);
|
|
5199
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
5200
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, task TEXT, workflow TEXT,
|
|
5201
|
+
step INTEGER, step_label TEXT, steps TEXT, state TEXT, detail TEXT, run_id TEXT,
|
|
5202
|
+
started_at TEXT, updated_at TEXT, ended_at TEXT, actor_kind TEXT, actor_id TEXT
|
|
5203
|
+
);
|
|
5204
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_proj ON workflow_runs(project_id, id);
|
|
5000
5205
|
CREATE TABLE IF NOT EXISTS claims (
|
|
5001
5206
|
project_id TEXT, task TEXT, owner TEXT, worktree TEXT, branch TEXT,
|
|
5002
5207
|
acquired_at TEXT, expires_at TEXT, released_at TEXT, state TEXT,
|
|
@@ -5027,6 +5232,8 @@ class Store {
|
|
|
5027
5232
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
5028
5233
|
this.ensureColumn("projects", "icon", "TEXT");
|
|
5029
5234
|
this.ensureColumn("projects", "color", "TEXT");
|
|
5235
|
+
this.ensureColumn("messages", "to_kind", "TEXT");
|
|
5236
|
+
this.ensureColumn("messages", "from_session", "TEXT");
|
|
5030
5237
|
this.migrate();
|
|
5031
5238
|
this.migrateProjectsJson(join8(home, "projects.json"));
|
|
5032
5239
|
this.reconcileMovedProjects();
|
|
@@ -5076,9 +5283,9 @@ class Store {
|
|
|
5076
5283
|
reconcileMovedProjects() {
|
|
5077
5284
|
const all = this.projects();
|
|
5078
5285
|
for (const stale of all) {
|
|
5079
|
-
if (
|
|
5286
|
+
if (existsSync6(stale.root))
|
|
5080
5287
|
continue;
|
|
5081
|
-
const live = all.filter((p) => p.id !== stale.id && p.name === stale.name &&
|
|
5288
|
+
const live = all.filter((p) => p.id !== stale.id && p.name === stale.name && existsSync6(p.root));
|
|
5082
5289
|
if (live.length !== 1)
|
|
5083
5290
|
continue;
|
|
5084
5291
|
this.mergeProject(stale.id, live[0].id);
|
|
@@ -5162,7 +5369,7 @@ class Store {
|
|
|
5162
5369
|
return actorFrom(owner, sessionId, { user: osUser(), runId });
|
|
5163
5370
|
}
|
|
5164
5371
|
migrateProjectsJson(file) {
|
|
5165
|
-
if (!
|
|
5372
|
+
if (!existsSync6(file))
|
|
5166
5373
|
return;
|
|
5167
5374
|
try {
|
|
5168
5375
|
const list = JSON.parse(readFileSync3(file, "utf8"));
|
|
@@ -5177,7 +5384,7 @@ class Store {
|
|
|
5177
5384
|
const hit = this.topCache.get(cwd);
|
|
5178
5385
|
if (hit && Date.now() - hit.t < 1e4)
|
|
5179
5386
|
return hit.v;
|
|
5180
|
-
const v = cwd &&
|
|
5387
|
+
const v = cwd && existsSync6(cwd) ? gitToplevel(cwd) : null;
|
|
5181
5388
|
this.topCache.set(cwd, { v, t: Date.now() });
|
|
5182
5389
|
return v;
|
|
5183
5390
|
}
|
|
@@ -5383,7 +5590,7 @@ class Store {
|
|
|
5383
5590
|
}));
|
|
5384
5591
|
}
|
|
5385
5592
|
sessionContext(cwd) {
|
|
5386
|
-
if (!cwd || !
|
|
5593
|
+
if (!cwd || !existsSync6(cwd))
|
|
5387
5594
|
return null;
|
|
5388
5595
|
const toplevel = this.toplevel(cwd);
|
|
5389
5596
|
const project = this.resolveProject(cwd);
|
|
@@ -5523,7 +5730,160 @@ class Store {
|
|
|
5523
5730
|
return qs;
|
|
5524
5731
|
}
|
|
5525
5732
|
answerContext(sessionId) {
|
|
5526
|
-
|
|
5733
|
+
const parts = [
|
|
5734
|
+
formatAnswers(this.inbox(sessionId)),
|
|
5735
|
+
formatMessages(this.messageInbox(sessionId))
|
|
5736
|
+
];
|
|
5737
|
+
const out = parts.filter(Boolean);
|
|
5738
|
+
return out.length ? out.join(`
|
|
5739
|
+
`) : null;
|
|
5740
|
+
}
|
|
5741
|
+
wfInsert(projectId, task, workflow, steps, actor2) {
|
|
5742
|
+
const now = new Date().toISOString();
|
|
5743
|
+
const r = this.db.query(`INSERT INTO workflow_runs (project_id, task, workflow, step, step_label, steps, state, started_at, updated_at, actor_kind, actor_id)
|
|
5744
|
+
VALUES (?, ?, ?, 0, ?, ?, 'running', ?, ?, ?, ?)`).run(projectId, task, workflow, steps[0] ?? "", JSON.stringify(steps), now, now, actor2.kind, actor2.id);
|
|
5745
|
+
this.touch();
|
|
5746
|
+
return Number(r.lastInsertRowid);
|
|
5747
|
+
}
|
|
5748
|
+
wfUpdate(id, patch) {
|
|
5749
|
+
const sets = ["updated_at = ?"];
|
|
5750
|
+
const args = [new Date().toISOString()];
|
|
5751
|
+
if (patch.step !== undefined) {
|
|
5752
|
+
sets.push("step = ?");
|
|
5753
|
+
args.push(patch.step);
|
|
5754
|
+
}
|
|
5755
|
+
if (patch.stepLabel !== undefined) {
|
|
5756
|
+
sets.push("step_label = ?");
|
|
5757
|
+
args.push(patch.stepLabel);
|
|
5758
|
+
}
|
|
5759
|
+
if (patch.state !== undefined) {
|
|
5760
|
+
sets.push("state = ?");
|
|
5761
|
+
args.push(patch.state);
|
|
5762
|
+
}
|
|
5763
|
+
if (patch.detail !== undefined) {
|
|
5764
|
+
sets.push("detail = ?");
|
|
5765
|
+
args.push(patch.detail);
|
|
5766
|
+
}
|
|
5767
|
+
if (patch.runId !== undefined) {
|
|
5768
|
+
sets.push("run_id = ?");
|
|
5769
|
+
args.push(patch.runId);
|
|
5770
|
+
}
|
|
5771
|
+
if (patch.ended) {
|
|
5772
|
+
sets.push("ended_at = ?");
|
|
5773
|
+
args.push(new Date().toISOString());
|
|
5774
|
+
}
|
|
5775
|
+
this.db.query(`UPDATE workflow_runs SET ${sets.join(", ")} WHERE id = ?`).run(...args, id);
|
|
5776
|
+
this.touch();
|
|
5777
|
+
}
|
|
5778
|
+
wfRuns(projectId, limit = 50) {
|
|
5779
|
+
return this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? ORDER BY id DESC LIMIT ?").all(projectId, limit).map(rowToWorkflowRun);
|
|
5780
|
+
}
|
|
5781
|
+
wfActive(projectId, task) {
|
|
5782
|
+
const r = this.db.query("SELECT * FROM workflow_runs WHERE project_id = ? AND task = ? AND state = 'running' ORDER BY id DESC LIMIT 1").get(projectId, task);
|
|
5783
|
+
return r ? rowToWorkflowRun(r) : null;
|
|
5784
|
+
}
|
|
5785
|
+
wfSweepOrphans() {
|
|
5786
|
+
this.db.query("UPDATE workflow_runs SET state = 'stopped', detail = COALESCE(detail, 'daemon restarted mid-workflow'), ended_at = ? WHERE state = 'running'").run(new Date().toISOString());
|
|
5787
|
+
}
|
|
5788
|
+
send(projectId, input) {
|
|
5789
|
+
if (!this.project(projectId))
|
|
5790
|
+
return { ok: false, error: "unknown project" };
|
|
5791
|
+
const v = validateMessage(input.text);
|
|
5792
|
+
if (!v.ok)
|
|
5793
|
+
return { ok: false, error: v.reason };
|
|
5794
|
+
const to = parseTo(input.to);
|
|
5795
|
+
if (!to)
|
|
5796
|
+
return { ok: false, error: 'to must be a session id, a task, or "lead"' };
|
|
5797
|
+
let sessionId = null;
|
|
5798
|
+
let task = null;
|
|
5799
|
+
if (to.kind === "session") {
|
|
5800
|
+
sessionId = this.knownSession(to.id) ?? this.sessionByPrefix(to.id);
|
|
5801
|
+
if (!sessionId)
|
|
5802
|
+
return { ok: false, error: `unknown session ${to.id}` };
|
|
5803
|
+
} else if (to.kind === "task") {
|
|
5804
|
+
task = to.task;
|
|
5805
|
+
sessionId = this.sessionForTask(projectId, to.task);
|
|
5806
|
+
} else {
|
|
5807
|
+
sessionId = this.leadSession(projectId);
|
|
5808
|
+
}
|
|
5809
|
+
const createdAt = new Date().toISOString();
|
|
5810
|
+
const from = input.from ?? (input.fromSession ? `agent ${input.fromSession.slice(0, 8)}` : null);
|
|
5811
|
+
const r = this.db.query(`INSERT INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
|
|
5812
|
+
VALUES (?, ?, ?, 'message', ?, ?, ?, ?, ?)`).run(projectId, sessionId, task, v.text, from, createdAt, to.kind, input.fromSession ?? null);
|
|
5813
|
+
const message = this.message(Number(r.lastInsertRowid));
|
|
5814
|
+
this.append({
|
|
5815
|
+
ts: createdAt,
|
|
5816
|
+
type: "message.sent",
|
|
5817
|
+
projectId,
|
|
5818
|
+
sessionId: input.fromSession ?? null,
|
|
5819
|
+
actor: this.actorFor(input.from ?? null, input.fromSession ?? null),
|
|
5820
|
+
payload: {
|
|
5821
|
+
id: message.id,
|
|
5822
|
+
to: input.to,
|
|
5823
|
+
task,
|
|
5824
|
+
recipient: sessionId,
|
|
5825
|
+
text: v.text.slice(0, 400),
|
|
5826
|
+
summary: `message to ${String(input.to)}: ${v.text.slice(0, 120)}`
|
|
5827
|
+
}
|
|
5828
|
+
});
|
|
5829
|
+
return { ok: true, message };
|
|
5830
|
+
}
|
|
5831
|
+
message(id) {
|
|
5832
|
+
const r = this.db.query("SELECT * FROM messages WHERE id = ? AND kind = 'message'").get(id);
|
|
5833
|
+
return r ? rowToMessage(r) : null;
|
|
5834
|
+
}
|
|
5835
|
+
messages(opts = {}) {
|
|
5836
|
+
const where = ["kind = 'message'"];
|
|
5837
|
+
const args = [];
|
|
5838
|
+
if (opts.projectId) {
|
|
5839
|
+
where.push("project_id = ?");
|
|
5840
|
+
args.push(opts.projectId);
|
|
5841
|
+
}
|
|
5842
|
+
if (opts.sessionId) {
|
|
5843
|
+
where.push("(session_id = ? OR from_session = ?)");
|
|
5844
|
+
args.push(opts.sessionId, opts.sessionId);
|
|
5845
|
+
}
|
|
5846
|
+
if (opts.task) {
|
|
5847
|
+
where.push("task = ?");
|
|
5848
|
+
args.push(opts.task);
|
|
5849
|
+
}
|
|
5850
|
+
args.push(opts.limit ?? 100);
|
|
5851
|
+
return this.db.query(`SELECT * FROM messages WHERE ${where.join(" AND ")} ORDER BY id DESC LIMIT ?`).all(...args).map(rowToMessage);
|
|
5852
|
+
}
|
|
5853
|
+
messageInbox(sessionId, opts = {}) {
|
|
5854
|
+
if (!sessionId)
|
|
5855
|
+
return [];
|
|
5856
|
+
const s = this.db.query("SELECT project_id, kind, cwd FROM sessions WHERE id = ?").get(sessionId);
|
|
5857
|
+
if (!s)
|
|
5858
|
+
return [];
|
|
5859
|
+
const task = this.heldClaimsWithWorktree().find((c) => isInside(s.cwd, c.worktree))?.task ?? null;
|
|
5860
|
+
const rows = this.db.query(`SELECT * FROM messages WHERE kind = 'message' AND delivered_at IS NULL AND from_session IS NOT ?
|
|
5861
|
+
AND (session_id = ?
|
|
5862
|
+
OR (to_kind = 'task' AND project_id = ? AND task IS ?)
|
|
5863
|
+
OR (to_kind = 'lead' AND project_id = ? AND ? = 'interactive'))
|
|
5864
|
+
ORDER BY id`).all(sessionId, sessionId, s.project_id, task, s.project_id, s.kind);
|
|
5865
|
+
const ms = rows.map(rowToMessage);
|
|
5866
|
+
if (ms.length && !opts.peek)
|
|
5867
|
+
this.db.query(`UPDATE messages SET delivered_at = ?, session_id = ? WHERE id IN (${ms.map(() => "?").join(",")})`).run(new Date().toISOString(), sessionId, ...ms.map((m) => m.id));
|
|
5868
|
+
return ms;
|
|
5869
|
+
}
|
|
5870
|
+
markMessageDelivered(id, sessionId) {
|
|
5871
|
+
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);
|
|
5872
|
+
}
|
|
5873
|
+
leadSession(projectId) {
|
|
5874
|
+
const r = this.db.query("SELECT id FROM sessions WHERE project_id = ? AND kind = 'interactive' AND state != 'ended' ORDER BY last_seen_at DESC LIMIT 1").get(projectId);
|
|
5875
|
+
return r?.id ?? null;
|
|
5876
|
+
}
|
|
5877
|
+
sessionByPrefix(prefix) {
|
|
5878
|
+
const rows = this.db.query("SELECT id FROM sessions WHERE id LIKE ? ORDER BY last_seen_at DESC LIMIT 2").all(`${prefix}%`);
|
|
5879
|
+
return rows.length === 1 ? rows[0]?.id ?? null : null;
|
|
5880
|
+
}
|
|
5881
|
+
sessionForTask(projectId, task) {
|
|
5882
|
+
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
5883
|
+
if (!claim?.worktree)
|
|
5884
|
+
return null;
|
|
5885
|
+
const rows = this.db.query("SELECT id, cwd FROM sessions WHERE project_id = ? AND state != 'ended' ORDER BY last_seen_at DESC").all(projectId);
|
|
5886
|
+
return rows.find((r) => isInside(r.cwd, claim.worktree))?.id ?? null;
|
|
5527
5887
|
}
|
|
5528
5888
|
questionContext(task, projectId) {
|
|
5529
5889
|
if (!task)
|
|
@@ -5600,13 +5960,13 @@ class Store {
|
|
|
5600
5960
|
};
|
|
5601
5961
|
const claim = this.claims(projectId).find((c) => c.task === task && c.state === "held");
|
|
5602
5962
|
const worktree2 = claim?.worktree;
|
|
5603
|
-
if (!worktree2 || !
|
|
5963
|
+
if (!worktree2 || !existsSync6(worktree2))
|
|
5604
5964
|
return {
|
|
5605
5965
|
ok: false,
|
|
5606
5966
|
reason: `${task} has no held worktree to run ${gate} in \u2014 claim it first`
|
|
5607
5967
|
};
|
|
5608
5968
|
const cwd = def.cwd ? join8(worktree2, def.cwd) : worktree2;
|
|
5609
|
-
if (!
|
|
5969
|
+
if (!existsSync6(cwd))
|
|
5610
5970
|
return { ok: false, reason: `gate cwd ${cwd} does not exist` };
|
|
5611
5971
|
const key = `${projectId}:${task}:${gate}`;
|
|
5612
5972
|
if (this.gateJobs.has(key))
|
|
@@ -5947,7 +6307,7 @@ ${err}
|
|
|
5947
6307
|
error = e.error;
|
|
5948
6308
|
} else {
|
|
5949
6309
|
const path = join8(p.root, source);
|
|
5950
|
-
if (!
|
|
6310
|
+
if (!existsSync6(path))
|
|
5951
6311
|
return { source, required: this.requiredGates(projectId), tasks: [] };
|
|
5952
6312
|
const mtime = statSync(path).mtimeMs;
|
|
5953
6313
|
let md = this.taskCache.get(projectId);
|
|
@@ -5996,7 +6356,7 @@ ${err}
|
|
|
5996
6356
|
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
5997
6357
|
try {
|
|
5998
6358
|
if (!hasLockedRules(loaded)) {
|
|
5999
|
-
if (
|
|
6359
|
+
if (existsSync6(file))
|
|
6000
6360
|
unlinkSync(file);
|
|
6001
6361
|
return;
|
|
6002
6362
|
}
|
|
@@ -6012,13 +6372,13 @@ ${err}
|
|
|
6012
6372
|
claudeSettings() {
|
|
6013
6373
|
const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
|
|
6014
6374
|
try {
|
|
6015
|
-
return
|
|
6375
|
+
return existsSync6(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
|
|
6016
6376
|
} catch {
|
|
6017
6377
|
return null;
|
|
6018
6378
|
}
|
|
6019
6379
|
}
|
|
6020
6380
|
checkPolicy(cwd, sessionId) {
|
|
6021
|
-
const project =
|
|
6381
|
+
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6022
6382
|
const repoRoot = project?.root ?? null;
|
|
6023
6383
|
const loaded = this.policyFor(repoRoot);
|
|
6024
6384
|
const settings = this.claudeSettings();
|
|
@@ -6043,7 +6403,7 @@ ${err}
|
|
|
6043
6403
|
return findings;
|
|
6044
6404
|
}
|
|
6045
6405
|
evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
|
|
6046
|
-
if (BUDGET_ASK_TOOLS.has(tool) && cwd &&
|
|
6406
|
+
if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync6(cwd)) {
|
|
6047
6407
|
const project = this.resolveProject(cwd);
|
|
6048
6408
|
const b = this.budgetFor(project.id);
|
|
6049
6409
|
if (b && b.status.level === "exceeded" && b.config.on_exceed === "ask") {
|
|
@@ -6134,7 +6494,7 @@ ${err}
|
|
|
6134
6494
|
return this.openIncident(d, cwd, id, cmd);
|
|
6135
6495
|
}
|
|
6136
6496
|
openIncident(d, cwd, sessionId, command) {
|
|
6137
|
-
const project = cwd &&
|
|
6497
|
+
const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6138
6498
|
this.append({
|
|
6139
6499
|
ts: new Date().toISOString(),
|
|
6140
6500
|
type: "incident.opened",
|
|
@@ -6185,7 +6545,7 @@ ${err}
|
|
|
6185
6545
|
}
|
|
6186
6546
|
}
|
|
6187
6547
|
const report = dryRunRules(calls, modes, {
|
|
6188
|
-
toplevel: (cwd) => cwd &&
|
|
6548
|
+
toplevel: (cwd) => cwd && existsSync6(cwd) ? this.toplevel(cwd) : null,
|
|
6189
6549
|
claims: this.heldWorktrees()
|
|
6190
6550
|
});
|
|
6191
6551
|
return { ...report, modes };
|
|
@@ -6201,7 +6561,7 @@ ${err}
|
|
|
6201
6561
|
this.prices = { ...PRICES };
|
|
6202
6562
|
for (const f of ["pricing.litellm.json", "pricing.json"]) {
|
|
6203
6563
|
const p = join8(this.home, f);
|
|
6204
|
-
if (!
|
|
6564
|
+
if (!existsSync6(p))
|
|
6205
6565
|
continue;
|
|
6206
6566
|
try {
|
|
6207
6567
|
const j = JSON.parse(readFileSync3(p, "utf8"));
|
|
@@ -6426,10 +6786,10 @@ ${err}
|
|
|
6426
6786
|
if (typeof raw2.cwd === "string")
|
|
6427
6787
|
this.autoRenewFor(typeof raw2.session_id === "string" ? raw2.session_id : null, raw2.cwd);
|
|
6428
6788
|
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : process.cwd();
|
|
6429
|
-
const project =
|
|
6789
|
+
const project = existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6430
6790
|
const e = this.append(normalizeHook(event, raw2, project?.id ?? "p_unknown"));
|
|
6431
6791
|
if ((event === "Stop" || event === "SessionEnd") && e.sessionId) {
|
|
6432
|
-
if (
|
|
6792
|
+
if (existsSync6(cwd)) {
|
|
6433
6793
|
this.autoHandoff(e.sessionId, cwd);
|
|
6434
6794
|
this.autoGate(event, e.sessionId, cwd);
|
|
6435
6795
|
}
|
|
@@ -6475,7 +6835,7 @@ ${err}
|
|
|
6475
6835
|
return;
|
|
6476
6836
|
const p = e.payload;
|
|
6477
6837
|
const row = this.db.query("SELECT id, tool_counts FROM sessions WHERE id = ?").get(e.sessionId);
|
|
6478
|
-
const branch = p.cwd &&
|
|
6838
|
+
const branch = p.cwd && existsSync6(p.cwd) ? currentBranch(p.cwd) : null;
|
|
6479
6839
|
if (!row) {
|
|
6480
6840
|
this.db.query("INSERT INTO sessions (id, project_id, kind, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, ?, 'active')").run(e.sessionId, e.projectId, p.cwd ?? "", branch, e.ts, e.ts, p.summary ?? e.type, e.type);
|
|
6481
6841
|
}
|
|
@@ -6547,10 +6907,10 @@ ${err}
|
|
|
6547
6907
|
}
|
|
6548
6908
|
tailSession(sessionId) {
|
|
6549
6909
|
const s = this.db.query("SELECT transcript_path FROM sessions WHERE id = ?").get(sessionId);
|
|
6550
|
-
if (!s?.transcript_path || !
|
|
6910
|
+
if (!s?.transcript_path || !existsSync6(s.transcript_path))
|
|
6551
6911
|
return 0;
|
|
6552
6912
|
let n = this.tailFile(s.transcript_path, sessionId, null);
|
|
6553
|
-
const subDir = join8(
|
|
6913
|
+
const subDir = join8(dirname3(s.transcript_path), basename(s.transcript_path, ".jsonl"), "subagents");
|
|
6554
6914
|
for (const f of this.subagentFiles(subDir)) {
|
|
6555
6915
|
n += this.tailFile(join8(subDir, f), sessionId, f.replace(/^agent-|\.jsonl$/g, ""));
|
|
6556
6916
|
}
|
|
@@ -6618,7 +6978,7 @@ ${err}
|
|
|
6618
6978
|
return out;
|
|
6619
6979
|
}
|
|
6620
6980
|
tailCodex(windowMs = 3 * 24 * 60 * 60000) {
|
|
6621
|
-
if (!
|
|
6981
|
+
if (!existsSync6(this.codexRoot()))
|
|
6622
6982
|
return 0;
|
|
6623
6983
|
let n = 0;
|
|
6624
6984
|
for (const path of this.codexRolloutFiles(Date.now() - windowMs)) {
|
|
@@ -6630,9 +6990,54 @@ ${err}
|
|
|
6630
6990
|
return process.env.SWARM_GROK_DIR ?? join8(homedir3(), ".grok", "sessions");
|
|
6631
6991
|
}
|
|
6632
6992
|
grokSummary = new Map;
|
|
6993
|
+
tailGemini(windowMs = 3 * 24 * 60 * 60000) {
|
|
6994
|
+
const root = process.env.SWARM_GEMINI_ROOT ?? join8(homedir3(), ".gemini", "tmp");
|
|
6995
|
+
if (!existsSync6(root))
|
|
6996
|
+
return 0;
|
|
6997
|
+
const since = Date.now() - windowMs;
|
|
6998
|
+
const ls = (p) => {
|
|
6999
|
+
try {
|
|
7000
|
+
return readdirSync(p);
|
|
7001
|
+
} catch {
|
|
7002
|
+
return [];
|
|
7003
|
+
}
|
|
7004
|
+
};
|
|
7005
|
+
let n = 0;
|
|
7006
|
+
const ingestDir = (dir) => {
|
|
7007
|
+
for (const f of ls(dir)) {
|
|
7008
|
+
const path = join8(dir, f);
|
|
7009
|
+
if (!f.endsWith(".jsonl")) {
|
|
7010
|
+
try {
|
|
7011
|
+
if (statSync(path).isDirectory()) {
|
|
7012
|
+
for (const g of ls(path))
|
|
7013
|
+
if (g.endsWith(".jsonl"))
|
|
7014
|
+
ingestFile(join8(path, g));
|
|
7015
|
+
}
|
|
7016
|
+
} catch {}
|
|
7017
|
+
continue;
|
|
7018
|
+
}
|
|
7019
|
+
ingestFile(path);
|
|
7020
|
+
}
|
|
7021
|
+
};
|
|
7022
|
+
const ingestFile = (path) => {
|
|
7023
|
+
try {
|
|
7024
|
+
if (statSync(path).mtimeMs < since)
|
|
7025
|
+
return;
|
|
7026
|
+
} catch {
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
7029
|
+
n += this.ingestLog(path, "gemini", parseGeminiChat);
|
|
7030
|
+
};
|
|
7031
|
+
for (const hash of ls(root)) {
|
|
7032
|
+
const chats = join8(root, hash, "chats");
|
|
7033
|
+
if (existsSync6(chats))
|
|
7034
|
+
ingestDir(chats);
|
|
7035
|
+
}
|
|
7036
|
+
return n;
|
|
7037
|
+
}
|
|
6633
7038
|
tailGrok(windowMs = 3 * 24 * 60 * 60000) {
|
|
6634
7039
|
const root = this.grokRoot();
|
|
6635
|
-
if (!
|
|
7040
|
+
if (!existsSync6(root))
|
|
6636
7041
|
return 0;
|
|
6637
7042
|
const since = Date.now() - windowMs;
|
|
6638
7043
|
const ls = (p) => {
|
|
@@ -6655,7 +7060,7 @@ ${err}
|
|
|
6655
7060
|
const cwdDir = join8(root, enc);
|
|
6656
7061
|
for (const sid of ls(cwdDir)) {
|
|
6657
7062
|
const path = join8(cwdDir, sid, "updates.jsonl");
|
|
6658
|
-
if (!
|
|
7063
|
+
if (!existsSync6(path))
|
|
6659
7064
|
continue;
|
|
6660
7065
|
try {
|
|
6661
7066
|
if (statSync(path).mtimeMs < since)
|
|
@@ -6714,9 +7119,9 @@ ${err}
|
|
|
6714
7119
|
ensureAgentSession(sid, agent, cwd, mtime) {
|
|
6715
7120
|
if (this.db.query("SELECT 1 FROM sessions WHERE id = ?").get(sid))
|
|
6716
7121
|
return;
|
|
6717
|
-
const project = cwd &&
|
|
7122
|
+
const project = cwd && existsSync6(cwd) ? this.resolveProject(cwd) : null;
|
|
6718
7123
|
const ts = new Date(mtime).toISOString();
|
|
6719
|
-
this.db.query("INSERT INTO sessions (id, project_id, kind, agent, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, '', '', 'active')").run(sid, project?.id ?? "p_unknown", agent, cwd, cwd &&
|
|
7124
|
+
this.db.query("INSERT INTO sessions (id, project_id, kind, agent, cwd, branch, started_at, last_seen_at, last, last_type, state) VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, '', '', 'active')").run(sid, project?.id ?? "p_unknown", agent, cwd, cwd && existsSync6(cwd) ? currentBranch(cwd) : null, ts, ts);
|
|
6720
7125
|
}
|
|
6721
7126
|
claimRows(projectId) {
|
|
6722
7127
|
return this.db.query("SELECT * FROM claims WHERE project_id = ?").all(projectId).map((r) => ({
|
|
@@ -6762,9 +7167,9 @@ ${err}
|
|
|
6762
7167
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
6763
7168
|
const branch = `task/${task}`;
|
|
6764
7169
|
const worktree2 = this.worktreePath(projectId, task);
|
|
6765
|
-
if (
|
|
7170
|
+
if (existsSync6(worktree2))
|
|
6766
7171
|
return { ok: false, error: `${worktree2} already exists; release ${task} first` };
|
|
6767
|
-
mkdirSync4(
|
|
7172
|
+
mkdirSync4(dirname3(worktree2), { recursive: true });
|
|
6768
7173
|
const created = worktreeAdd(p.root, worktree2, branch, baseRef);
|
|
6769
7174
|
if (!created)
|
|
6770
7175
|
return { ok: false, error: `git worktree add failed for ${task}` };
|
|
@@ -6874,7 +7279,7 @@ ${err}
|
|
|
6874
7279
|
for (const c of this.claimRows(p.id)) {
|
|
6875
7280
|
if (c.state !== "held" || isActive(c, now))
|
|
6876
7281
|
continue;
|
|
6877
|
-
const exists = c.worktree ?
|
|
7282
|
+
const exists = c.worktree ? existsSync6(c.worktree) : false;
|
|
6878
7283
|
const work = exists ? heldWork(c.worktree) : null;
|
|
6879
7284
|
if (reapAction(c, now, exists, work) !== "keep-orphaned")
|
|
6880
7285
|
continue;
|
|
@@ -6930,7 +7335,7 @@ ${err}
|
|
|
6930
7335
|
if (!row)
|
|
6931
7336
|
return { ok: false, error: `no claim on ${task}` };
|
|
6932
7337
|
const worktree2 = row.worktree ?? "";
|
|
6933
|
-
if (worktree2 &&
|
|
7338
|
+
if (worktree2 && existsSync6(worktree2)) {
|
|
6934
7339
|
const work = heldWork(worktree2);
|
|
6935
7340
|
const can = canRelease(work, force);
|
|
6936
7341
|
if (!can.ok)
|
|
@@ -6965,7 +7370,7 @@ ${err}
|
|
|
6965
7370
|
continue;
|
|
6966
7371
|
if (isActive({ ...c, state: "held" }, now))
|
|
6967
7372
|
continue;
|
|
6968
|
-
const exists = c.worktree ?
|
|
7373
|
+
const exists = c.worktree ? existsSync6(c.worktree) : false;
|
|
6969
7374
|
const work = exists ? heldWork(c.worktree) : null;
|
|
6970
7375
|
const action = reapAction({ ...c, state: "held" }, now, exists, work);
|
|
6971
7376
|
if (action === "not-expired")
|
|
@@ -7064,9 +7469,9 @@ ${err}
|
|
|
7064
7469
|
if (!slug || slug === "." || slug === "..")
|
|
7065
7470
|
return { ok: false, error: "bad worktree name" };
|
|
7066
7471
|
const path = this.worktreePath(projectId, slug);
|
|
7067
|
-
if (
|
|
7472
|
+
if (existsSync6(path))
|
|
7068
7473
|
return { ok: false, error: `${path} already exists` };
|
|
7069
|
-
mkdirSync4(
|
|
7474
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
7070
7475
|
const br = branch?.trim() || `wt/${slug}`;
|
|
7071
7476
|
const created = worktreeAdd(p.root, path, br, baseRef);
|
|
7072
7477
|
if (!created)
|
|
@@ -7751,6 +8156,48 @@ ${err}
|
|
|
7751
8156
|
seq() {
|
|
7752
8157
|
return this.db.query("SELECT COALESCE(MAX(seq),0) AS seq FROM events").get().seq;
|
|
7753
8158
|
}
|
|
8159
|
+
timelineDetail(hours, projectId) {
|
|
8160
|
+
const from = new Date(Date.now() - Math.min(Math.max(hours, 1), 168) * 3600000).toISOString();
|
|
8161
|
+
const args = [from];
|
|
8162
|
+
let filter = "";
|
|
8163
|
+
if (projectId) {
|
|
8164
|
+
filter = " AND s.project_id = ?";
|
|
8165
|
+
args.push(projectId);
|
|
8166
|
+
}
|
|
8167
|
+
const rows = this.db.query(`SELECT t.session_id AS sid, t.ts FROM turns t JOIN sessions s ON s.id = t.session_id
|
|
8168
|
+
WHERE t.ts >= ? AND t.sidechain = 0${filter} ORDER BY t.ts LIMIT 20000`).all(...args);
|
|
8169
|
+
const turns = {};
|
|
8170
|
+
for (const r of rows) {
|
|
8171
|
+
turns[r.sid] ??= [];
|
|
8172
|
+
turns[r.sid]?.push(new Date(r.ts).getTime());
|
|
8173
|
+
}
|
|
8174
|
+
const claims = this.claims().filter((c) => (!projectId || c.projectId === projectId) && c.state !== "released").map((c) => ({
|
|
8175
|
+
projectId: c.projectId,
|
|
8176
|
+
task: c.task,
|
|
8177
|
+
owner: c.owner,
|
|
8178
|
+
state: c.state,
|
|
8179
|
+
acquiredAt: c.acquiredAt,
|
|
8180
|
+
expiresAt: c.expiresAt
|
|
8181
|
+
}));
|
|
8182
|
+
return { turns, claims };
|
|
8183
|
+
}
|
|
8184
|
+
spendSparks() {
|
|
8185
|
+
const from = localDayIso(-13);
|
|
8186
|
+
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
|
+
FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.ts >= ? GROUP BY pid, day`).all(from);
|
|
8188
|
+
const days2 = [];
|
|
8189
|
+
for (let i = 13;i >= 0; i--)
|
|
8190
|
+
days2.push(localDayIso(-i).slice(0, 10));
|
|
8191
|
+
const out = {};
|
|
8192
|
+
for (const r of rows) {
|
|
8193
|
+
out[r.pid] ??= new Array(14).fill(0);
|
|
8194
|
+
const arr = out[r.pid];
|
|
8195
|
+
const i = days2.indexOf(r.day);
|
|
8196
|
+
if (i >= 0)
|
|
8197
|
+
arr[i] = (arr[i] ?? 0) + (r.usd ?? 0);
|
|
8198
|
+
}
|
|
8199
|
+
return out;
|
|
8200
|
+
}
|
|
7754
8201
|
snapshot() {
|
|
7755
8202
|
const worktrees = {};
|
|
7756
8203
|
const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
|
|
@@ -7761,6 +8208,7 @@ ${err}
|
|
|
7761
8208
|
worktrees,
|
|
7762
8209
|
sessions: this.memoised("sessions", 2000, () => this.sessions()),
|
|
7763
8210
|
spend: this.memoised("spend", 30000, () => this.spend()),
|
|
8211
|
+
spendSparks: this.memoised("spendSparks", 60000, () => this.spendSparks()),
|
|
7764
8212
|
claims: this.claims(),
|
|
7765
8213
|
processes: this.memoised("processes", 5000, () => this.processes()),
|
|
7766
8214
|
incidents: this.memoised("incidents", 30000, () => this.incidents(20, { open: true })),
|
|
@@ -7858,15 +8306,242 @@ function isScratchRoot(root) {
|
|
|
7858
8306
|
const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
|
|
7859
8307
|
return tmp.some((t) => root === t || root.startsWith(`${t}/`));
|
|
7860
8308
|
}
|
|
8309
|
+
function rowToMessage(r) {
|
|
8310
|
+
return {
|
|
8311
|
+
id: r.id,
|
|
8312
|
+
projectId: r.project_id,
|
|
8313
|
+
task: r.task ?? null,
|
|
8314
|
+
sessionId: r.session_id ?? null,
|
|
8315
|
+
toKind: r.to_kind ?? "session",
|
|
8316
|
+
from: r.asked_by ?? null,
|
|
8317
|
+
fromSession: r.from_session ?? null,
|
|
8318
|
+
text: r.text,
|
|
8319
|
+
createdAt: r.created_at,
|
|
8320
|
+
deliveredAt: r.delivered_at ?? null
|
|
8321
|
+
};
|
|
8322
|
+
}
|
|
8323
|
+
function rowToWorkflowRun(r) {
|
|
8324
|
+
return {
|
|
8325
|
+
id: r.id,
|
|
8326
|
+
projectId: r.project_id,
|
|
8327
|
+
task: r.task,
|
|
8328
|
+
workflow: r.workflow,
|
|
8329
|
+
step: r.step,
|
|
8330
|
+
stepLabel: r.step_label ?? "",
|
|
8331
|
+
steps: JSON.parse(r.steps ?? "[]"),
|
|
8332
|
+
state: r.state,
|
|
8333
|
+
detail: r.detail ?? null,
|
|
8334
|
+
runId: r.run_id ?? null,
|
|
8335
|
+
startedAt: r.started_at,
|
|
8336
|
+
updatedAt: r.updated_at,
|
|
8337
|
+
endedAt: r.ended_at ?? null
|
|
8338
|
+
};
|
|
8339
|
+
}
|
|
8340
|
+
function localDayIso(offsetDays) {
|
|
8341
|
+
const d = new Date;
|
|
8342
|
+
d.setHours(0, 0, 0, 0);
|
|
8343
|
+
d.setDate(d.getDate() + offsetDays);
|
|
8344
|
+
return d.toISOString();
|
|
8345
|
+
}
|
|
8346
|
+
|
|
8347
|
+
// packages/daemon/src/workflow.ts
|
|
8348
|
+
class WorkflowEngine {
|
|
8349
|
+
store;
|
|
8350
|
+
runner;
|
|
8351
|
+
forge;
|
|
8352
|
+
active = new Map;
|
|
8353
|
+
constructor(store, runner, forge2) {
|
|
8354
|
+
this.store = store;
|
|
8355
|
+
this.runner = runner;
|
|
8356
|
+
this.forge = forge2;
|
|
8357
|
+
store.wfSweepOrphans();
|
|
8358
|
+
runner.onEnd((run2) => void this.onRunEnd(run2));
|
|
8359
|
+
}
|
|
8360
|
+
start(projectId, task, workflow, opts = {}) {
|
|
8361
|
+
const def = this.store.config(projectId).workflows[workflow];
|
|
8362
|
+
if (!def) {
|
|
8363
|
+
const known = Object.keys(this.store.config(projectId).workflows);
|
|
8364
|
+
return {
|
|
8365
|
+
ok: false,
|
|
8366
|
+
error: `unknown workflow ${workflow}${known.length ? ` \u2014 this repo declares: ${known.join(", ")}` : " \u2014 declare [[workflows]] in .swarm.toml"}`
|
|
8367
|
+
};
|
|
8368
|
+
}
|
|
8369
|
+
const key = `${projectId}:${task}`;
|
|
8370
|
+
if (this.active.has(key) || this.store.wfActive(projectId, task))
|
|
8371
|
+
return { ok: false, error: `a workflow is already running on ${task}` };
|
|
8372
|
+
const title = this.store.tasks(projectId)?.tasks.find((t) => t.id === task)?.title ?? task;
|
|
8373
|
+
const owner = opts.owner ?? "workflow";
|
|
8374
|
+
const id = this.store.wfInsert(projectId, task, workflow, def.steps.map(stepLabel), this.store.actorFor(owner, opts.sessionId ?? null));
|
|
8375
|
+
const w = { id, projectId, task, title, def, step: 0, runId: null, owner };
|
|
8376
|
+
this.active.set(key, w);
|
|
8377
|
+
this.store.append({
|
|
8378
|
+
ts: new Date().toISOString(),
|
|
8379
|
+
type: "workflow.started",
|
|
8380
|
+
projectId,
|
|
8381
|
+
sessionId: opts.sessionId ?? null,
|
|
8382
|
+
payload: {
|
|
8383
|
+
id,
|
|
8384
|
+
task,
|
|
8385
|
+
workflow,
|
|
8386
|
+
steps: def.steps.map(stepLabel),
|
|
8387
|
+
summary: `workflow ${workflow} on ${task}: ${def.steps.map(stepLabel).join(" \u2192 ")}`
|
|
8388
|
+
}
|
|
8389
|
+
});
|
|
8390
|
+
this.advance(w);
|
|
8391
|
+
return { ok: true, id };
|
|
8392
|
+
}
|
|
8393
|
+
status(projectId) {
|
|
8394
|
+
return this.store.wfRuns(projectId);
|
|
8395
|
+
}
|
|
8396
|
+
stop(projectId, task) {
|
|
8397
|
+
const key = `${projectId}:${task}`;
|
|
8398
|
+
const w = this.active.get(key);
|
|
8399
|
+
if (!w)
|
|
8400
|
+
return { ok: false, error: `no running workflow on ${task}` };
|
|
8401
|
+
if (w.runId)
|
|
8402
|
+
this.runner.stop(w.runId);
|
|
8403
|
+
this.finish(w, "stopped", `stopped at ${this.label(w)}`);
|
|
8404
|
+
return { ok: true };
|
|
8405
|
+
}
|
|
8406
|
+
label(w) {
|
|
8407
|
+
const s = w.def.steps[w.step];
|
|
8408
|
+
return s ? stepLabel(s) : "done";
|
|
8409
|
+
}
|
|
8410
|
+
async advance(w) {
|
|
8411
|
+
while (w.step < w.def.steps.length) {
|
|
8412
|
+
const s = w.def.steps[w.step];
|
|
8413
|
+
this.store.wfUpdate(w.id, { step: w.step, stepLabel: stepLabel(s), runId: null });
|
|
8414
|
+
this.step(w, `step ${w.step + 1}/${w.def.steps.length}: ${stepLabel(s)}`);
|
|
8415
|
+
if (s.kind === "run") {
|
|
8416
|
+
const cfg = this.store.config(w.projectId).dispatch;
|
|
8417
|
+
const remaining = w.def.steps.slice(w.step + 1).map(stepLabel);
|
|
8418
|
+
const r = await this.runner.start({
|
|
8419
|
+
projectId: w.projectId,
|
|
8420
|
+
task: w.task,
|
|
8421
|
+
prompt: workflowStepPrompt(s, { id: w.task, title: w.title }, { workflow: w.def.name, remaining }),
|
|
8422
|
+
owner: w.owner,
|
|
8423
|
+
permissionMode: cfg.permission_mode ?? "acceptEdits",
|
|
8424
|
+
model: cfg.model ?? undefined,
|
|
8425
|
+
maxTurns: cfg.max_turns ?? undefined,
|
|
8426
|
+
profile: cfg.profile ?? undefined
|
|
8427
|
+
});
|
|
8428
|
+
if (!r.ok)
|
|
8429
|
+
return this.fail(w, `could not start ${stepLabel(s)}: ${r.reason}`);
|
|
8430
|
+
w.runId = r.run.id;
|
|
8431
|
+
this.store.wfUpdate(w.id, { runId: r.run.id });
|
|
8432
|
+
return;
|
|
8433
|
+
}
|
|
8434
|
+
if (s.kind === "gate") {
|
|
8435
|
+
const r = await this.store.runGates(w.projectId, w.task, [s.gate], { owner: w.owner });
|
|
8436
|
+
const run2 = r.runs.find((x) => x.gate === s.gate);
|
|
8437
|
+
if (!run2)
|
|
8438
|
+
return this.fail(w, `gate ${s.gate} did not run: ${r.skipped[0]?.reason ?? "unknown"}`);
|
|
8439
|
+
if (run2.verdict !== "pass")
|
|
8440
|
+
return this.fail(w, `gate ${s.gate} failed \u2014 ${run2.rubric}`);
|
|
8441
|
+
w.step++;
|
|
8442
|
+
continue;
|
|
8443
|
+
}
|
|
8444
|
+
const d = await this.store.prDraftFor(w.projectId, w.task);
|
|
8445
|
+
if (!d.ok)
|
|
8446
|
+
return this.fail(w, `pr: ${d.error}`);
|
|
8447
|
+
const pr = await this.forge.openPR(w.projectId, d.worktree, {
|
|
8448
|
+
title: d.title,
|
|
8449
|
+
body: d.body,
|
|
8450
|
+
isDraft: false
|
|
8451
|
+
});
|
|
8452
|
+
if (!pr.ok)
|
|
8453
|
+
return this.fail(w, `pr: ${pr.error}`);
|
|
8454
|
+
this.store.recordPrOpened(w.projectId, d.task, d.worktree.path, pr.url);
|
|
8455
|
+
this.store.wfUpdate(w.id, { detail: `PR ${pr.url}` });
|
|
8456
|
+
w.step++;
|
|
8457
|
+
}
|
|
8458
|
+
this.finish(w, "done", null);
|
|
8459
|
+
}
|
|
8460
|
+
async onRunEnd(run2) {
|
|
8461
|
+
const w = this.active.get(`${run2.projectId}:${run2.task}`);
|
|
8462
|
+
if (!w || w.runId !== run2.id)
|
|
8463
|
+
return;
|
|
8464
|
+
w.runId = null;
|
|
8465
|
+
if (run2.stopped)
|
|
8466
|
+
return this.finish(w, "stopped", `stopped during ${this.label(w)}`);
|
|
8467
|
+
if (run2.exitCode !== 0 || run2.result?.isError)
|
|
8468
|
+
return this.fail(w, `${this.label(w)} exited ${run2.exitCode}${run2.result?.isError ? " (error)" : ""} \u2014 log: ${run2.log}`);
|
|
8469
|
+
w.step++;
|
|
8470
|
+
this.advance(w);
|
|
8471
|
+
}
|
|
8472
|
+
step(w, summary) {
|
|
8473
|
+
this.store.append({
|
|
8474
|
+
ts: new Date().toISOString(),
|
|
8475
|
+
type: "workflow.step",
|
|
8476
|
+
projectId: w.projectId,
|
|
8477
|
+
sessionId: null,
|
|
8478
|
+
payload: {
|
|
8479
|
+
id: w.id,
|
|
8480
|
+
task: w.task,
|
|
8481
|
+
workflow: w.def.name,
|
|
8482
|
+
step: w.step,
|
|
8483
|
+
label: this.label(w),
|
|
8484
|
+
summary: `workflow ${w.def.name} on ${w.task} \u2014 ${summary}`
|
|
8485
|
+
}
|
|
8486
|
+
});
|
|
8487
|
+
}
|
|
8488
|
+
fail(w, detail) {
|
|
8489
|
+
this.store.wfUpdate(w.id, { state: "failed", detail, ended: true });
|
|
8490
|
+
this.active.delete(`${w.projectId}:${w.task}`);
|
|
8491
|
+
this.store.append({
|
|
8492
|
+
ts: new Date().toISOString(),
|
|
8493
|
+
type: "workflow.finished",
|
|
8494
|
+
projectId: w.projectId,
|
|
8495
|
+
sessionId: null,
|
|
8496
|
+
payload: {
|
|
8497
|
+
id: w.id,
|
|
8498
|
+
task: w.task,
|
|
8499
|
+
workflow: w.def.name,
|
|
8500
|
+
outcome: "failed",
|
|
8501
|
+
detail,
|
|
8502
|
+
summary: `workflow ${w.def.name} on ${w.task} failed at ${this.label(w)}: ${detail.slice(0, 160)}`
|
|
8503
|
+
}
|
|
8504
|
+
});
|
|
8505
|
+
this.store.append({
|
|
8506
|
+
ts: new Date().toISOString(),
|
|
8507
|
+
type: "incident.opened",
|
|
8508
|
+
projectId: w.projectId,
|
|
8509
|
+
sessionId: null,
|
|
8510
|
+
payload: {
|
|
8511
|
+
rule: "workflow_failed",
|
|
8512
|
+
action: "failed",
|
|
8513
|
+
command: `${w.task} \xB7 ${w.def.name} \xB7 ${this.label(w)}`,
|
|
8514
|
+
reason: detail.slice(0, 400)
|
|
8515
|
+
}
|
|
8516
|
+
});
|
|
8517
|
+
}
|
|
8518
|
+
finish(w, state, detail) {
|
|
8519
|
+
this.store.wfUpdate(w.id, { state, ...detail !== null ? { detail } : {}, ended: true });
|
|
8520
|
+
this.active.delete(`${w.projectId}:${w.task}`);
|
|
8521
|
+
this.store.append({
|
|
8522
|
+
ts: new Date().toISOString(),
|
|
8523
|
+
type: "workflow.finished",
|
|
8524
|
+
projectId: w.projectId,
|
|
8525
|
+
sessionId: null,
|
|
8526
|
+
payload: {
|
|
8527
|
+
id: w.id,
|
|
8528
|
+
task: w.task,
|
|
8529
|
+
workflow: w.def.name,
|
|
8530
|
+
outcome: state,
|
|
8531
|
+
summary: `workflow ${w.def.name} on ${w.task}: ${state}${detail ? ` \u2014 ${detail}` : ""}`
|
|
8532
|
+
}
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8535
|
+
}
|
|
7861
8536
|
|
|
7862
8537
|
// packages/daemon/src/app.ts
|
|
7863
|
-
var VERSION = "0.
|
|
8538
|
+
var VERSION = "0.9.0";
|
|
7864
8539
|
var WEB_DIR = (() => {
|
|
7865
8540
|
if (process.env.SWARM_WEB_DIR)
|
|
7866
8541
|
return process.env.SWARM_WEB_DIR;
|
|
7867
|
-
const here =
|
|
8542
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
7868
8543
|
const dev = join9(here, "../../web/public");
|
|
7869
|
-
return
|
|
8544
|
+
return existsSync7(join9(dev, "index.html")) ? dev : join9(here, "../web");
|
|
7870
8545
|
})();
|
|
7871
8546
|
var REPLAY_TAIL = 200;
|
|
7872
8547
|
var wireCache = new WeakMap;
|
|
@@ -7880,13 +8555,39 @@ function wireJson(e) {
|
|
|
7880
8555
|
}
|
|
7881
8556
|
function hookRepoRoot(store, raw2) {
|
|
7882
8557
|
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
|
|
7883
|
-
return cwd &&
|
|
8558
|
+
return cwd && existsSync7(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
|
|
7884
8559
|
}
|
|
7885
|
-
function
|
|
8560
|
+
function claudeSettings() {
|
|
8561
|
+
try {
|
|
8562
|
+
const p = process.env.CLAUDE_SETTINGS ?? join9(homedir4(), ".claude", "settings.json");
|
|
8563
|
+
return existsSync7(p) ? JSON.parse(readFileSync4(p, "utf8")) : null;
|
|
8564
|
+
} catch {
|
|
8565
|
+
return null;
|
|
8566
|
+
}
|
|
8567
|
+
}
|
|
8568
|
+
function diskVersion() {
|
|
8569
|
+
try {
|
|
8570
|
+
const entry = daemonCommand().at(-1);
|
|
8571
|
+
if (!entry || !existsSync7(entry))
|
|
8572
|
+
return null;
|
|
8573
|
+
for (const f of [entry, join9(dirname4(entry), "app.ts")]) {
|
|
8574
|
+
if (!existsSync7(f))
|
|
8575
|
+
continue;
|
|
8576
|
+
const m = /SWARM_VERSION\s*\?\?\s*"(\d+\.\d+\.\d+)"/.exec(readFileSync4(f, "utf8"));
|
|
8577
|
+
if (m?.[1])
|
|
8578
|
+
return m[1];
|
|
8579
|
+
}
|
|
8580
|
+
return null;
|
|
8581
|
+
} catch {
|
|
8582
|
+
return null;
|
|
8583
|
+
}
|
|
8584
|
+
}
|
|
8585
|
+
function createApp(store = new Store, hooks2 = {}) {
|
|
7886
8586
|
const app = new Hono2;
|
|
7887
8587
|
const forge2 = new ForgeService(store);
|
|
7888
8588
|
const runner = new Runner(store, store.home);
|
|
7889
8589
|
const dispatcher = new Dispatcher(store, runner, forge2);
|
|
8590
|
+
const workflows2 = new WorkflowEngine(store, runner, forge2);
|
|
7890
8591
|
store.onBudgetStop((projectId) => {
|
|
7891
8592
|
dispatcher.clear(projectId);
|
|
7892
8593
|
for (const run2 of runner.list(projectId))
|
|
@@ -7908,6 +8609,8 @@ function createApp(store = new Store) {
|
|
|
7908
8609
|
return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
|
|
7909
8610
|
});
|
|
7910
8611
|
app.get("/v1/health", (c) => c.json({
|
|
8612
|
+
disk: diskVersion(),
|
|
8613
|
+
hooksInstalled: hookCoverage(claudeSettings()).complete,
|
|
7911
8614
|
ok: true,
|
|
7912
8615
|
version: VERSION,
|
|
7913
8616
|
schema: store.schemaVersion(),
|
|
@@ -7942,13 +8645,13 @@ function createApp(store = new Store) {
|
|
|
7942
8645
|
const q = c.req.query("path");
|
|
7943
8646
|
let dir;
|
|
7944
8647
|
try {
|
|
7945
|
-
dir = realpathSync3(q &&
|
|
8648
|
+
dir = realpathSync3(q && existsSync7(q) ? q : homedir4());
|
|
7946
8649
|
} catch {
|
|
7947
8650
|
dir = homedir4();
|
|
7948
8651
|
}
|
|
7949
8652
|
try {
|
|
7950
|
-
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo:
|
|
7951
|
-
const parent =
|
|
8653
|
+
const entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => ({ name: e.name, repo: existsSync7(join9(dir, e.name, ".git")) })).sort((a, b) => a.name.localeCompare(b.name));
|
|
8654
|
+
const parent = dirname4(dir);
|
|
7952
8655
|
return c.json({ path: dir, parent: parent === dir ? null : parent, entries });
|
|
7953
8656
|
} catch (e) {
|
|
7954
8657
|
return c.json({ error: e.message, path: dir }, 400);
|
|
@@ -7998,6 +8701,12 @@ function createApp(store = new Store) {
|
|
|
7998
8701
|
const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
|
|
7999
8702
|
return c.body(formatAudit(rows, format), 200, { "content-type": ct });
|
|
8000
8703
|
});
|
|
8704
|
+
app.post("/v1/daemon/restart", (c) => {
|
|
8705
|
+
if (!hooks2.restart)
|
|
8706
|
+
return c.json({ error: "not restartable in this environment" }, 501);
|
|
8707
|
+
setTimeout(() => hooks2.restart?.(), 50);
|
|
8708
|
+
return c.json({ ok: true, restarting: true });
|
|
8709
|
+
});
|
|
8001
8710
|
app.get("/v1/rules/dryrun", (c) => {
|
|
8002
8711
|
const projectId = c.req.query("project");
|
|
8003
8712
|
if (!projectId)
|
|
@@ -8132,6 +8841,58 @@ function createApp(store = new Store) {
|
|
|
8132
8841
|
return c.json(r, r.ok ? 200 : 409);
|
|
8133
8842
|
});
|
|
8134
8843
|
app.get("/v1/inbox", (c) => c.json(store.inbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
|
|
8844
|
+
app.get("/v1/workflows", (c) => {
|
|
8845
|
+
const project = c.req.query("project");
|
|
8846
|
+
if (!project)
|
|
8847
|
+
return c.json({ error: "project required" }, 400);
|
|
8848
|
+
return c.json({ defs: store.config(project).workflows, runs: workflows2.status(project) });
|
|
8849
|
+
});
|
|
8850
|
+
app.post("/v1/workflows", async (c) => {
|
|
8851
|
+
const b = await c.req.json().catch(() => ({}));
|
|
8852
|
+
if (!b.projectId || !b.task || !b.workflow)
|
|
8853
|
+
return c.json({ ok: false, error: "projectId, task, workflow required" }, 400);
|
|
8854
|
+
const r = workflows2.start(b.projectId, b.task, b.workflow, {
|
|
8855
|
+
...b.owner ? { owner: b.owner } : {},
|
|
8856
|
+
sessionId: b.sessionId ?? null
|
|
8857
|
+
});
|
|
8858
|
+
return c.json(r, r.ok ? 201 : 409);
|
|
8859
|
+
});
|
|
8860
|
+
app.post("/v1/workflows/stop", async (c) => {
|
|
8861
|
+
const b = await c.req.json().catch(() => ({}));
|
|
8862
|
+
if (!b.projectId || !b.task)
|
|
8863
|
+
return c.json({ ok: false, error: "projectId and task required" }, 400);
|
|
8864
|
+
const r = workflows2.stop(b.projectId, b.task);
|
|
8865
|
+
return c.json(r, r.ok ? 200 : 404);
|
|
8866
|
+
});
|
|
8867
|
+
app.get("/v1/timeline", (c) => c.json(store.timelineDetail(Number(c.req.query("hours")) || 12, c.req.query("project") || null)));
|
|
8868
|
+
app.get("/v1/messages", (c) => c.json(store.messages({
|
|
8869
|
+
...c.req.query("project") ? { projectId: c.req.query("project") } : {},
|
|
8870
|
+
...c.req.query("session") ? { sessionId: c.req.query("session") } : {},
|
|
8871
|
+
...c.req.query("task") ? { task: c.req.query("task") } : {},
|
|
8872
|
+
limit: Number(c.req.query("limit")) || 100
|
|
8873
|
+
})));
|
|
8874
|
+
app.get("/v1/messages/inbox", (c) => c.json(store.messageInbox(c.req.query("session") || null, { peek: c.req.query("peek") === "1" })));
|
|
8875
|
+
app.post("/v1/messages", async (c) => {
|
|
8876
|
+
const b = await c.req.json().catch(() => ({}));
|
|
8877
|
+
if (!b.projectId)
|
|
8878
|
+
return c.json({ ok: false, error: "projectId required" }, 400);
|
|
8879
|
+
const r = store.send(b.projectId, {
|
|
8880
|
+
to: b.to,
|
|
8881
|
+
text: b.text,
|
|
8882
|
+
from: b.from ?? null,
|
|
8883
|
+
fromSession: b.sessionId ?? null
|
|
8884
|
+
});
|
|
8885
|
+
if (!r.ok)
|
|
8886
|
+
return c.json(r, 400);
|
|
8887
|
+
const m = r.message;
|
|
8888
|
+
const run2 = m.task ? runner.get(m.task) : m.sessionId ? runner.get(m.sessionId) : null;
|
|
8889
|
+
if (run2 && !run2.endedAt) {
|
|
8890
|
+
const sent = runner.send(run2.id, `[swarm] message from ${m.from ?? "unknown"}: ${m.text}`);
|
|
8891
|
+
if (sent.ok)
|
|
8892
|
+
store.markMessageDelivered(m.id, run2.sessionId);
|
|
8893
|
+
}
|
|
8894
|
+
return c.json({ ok: true, message: store.message(m.id) }, 201);
|
|
8895
|
+
});
|
|
8135
8896
|
app.get("/v1/dispatch", (c) => {
|
|
8136
8897
|
const project = c.req.query("project");
|
|
8137
8898
|
if (!project)
|
|
@@ -8453,7 +9214,7 @@ function createApp(store = new Store) {
|
|
|
8453
9214
|
await stream2.writeSSE({ id: String(e.seq), event: e.type, data: JSON.stringify(e) });
|
|
8454
9215
|
}
|
|
8455
9216
|
await stream2.writeSSE({ event: "ping", data: "" });
|
|
8456
|
-
await new Promise((
|
|
9217
|
+
await new Promise((resolve2) => {
|
|
8457
9218
|
const off = store.subscribe((e) => {
|
|
8458
9219
|
stream2.writeSSE({ id: String(e.seq), event: e.type, data: wireJson(e) });
|
|
8459
9220
|
});
|
|
@@ -8461,7 +9222,7 @@ function createApp(store = new Store) {
|
|
|
8461
9222
|
stream2.onAbort(() => {
|
|
8462
9223
|
clearInterval(beat);
|
|
8463
9224
|
off();
|
|
8464
|
-
|
|
9225
|
+
resolve2();
|
|
8465
9226
|
});
|
|
8466
9227
|
});
|
|
8467
9228
|
});
|
|
@@ -8471,18 +9232,107 @@ function createApp(store = new Store) {
|
|
|
8471
9232
|
app.get("/:file{[a-z0-9-]+\\.(js|css)}", (c) => {
|
|
8472
9233
|
const f = c.req.param("file");
|
|
8473
9234
|
const p = join9(WEB_DIR, f);
|
|
8474
|
-
if (!
|
|
9235
|
+
if (!existsSync7(p))
|
|
8475
9236
|
return c.text(`${f} not built \u2014 run: bun run build:web`, 404);
|
|
8476
9237
|
return c.body(readFileSync4(p, "utf8"), 200, {
|
|
8477
9238
|
"content-type": MIME[f.split(".").pop() ?? ""] ?? "text/plain"
|
|
8478
9239
|
});
|
|
8479
9240
|
});
|
|
8480
|
-
return { app, store, forge: forge2, runner, dispatcher };
|
|
9241
|
+
return { app, store, forge: forge2, runner, dispatcher, workflows: workflows2 };
|
|
9242
|
+
}
|
|
9243
|
+
|
|
9244
|
+
// packages/daemon/src/demo.ts
|
|
9245
|
+
var H = 3600000;
|
|
9246
|
+
var iso = (msAgo) => new Date(Date.now() - msAgo).toISOString();
|
|
9247
|
+
function isEmpty(store) {
|
|
9248
|
+
return !store.db.query("SELECT 1 FROM sessions LIMIT 1").get();
|
|
9249
|
+
}
|
|
9250
|
+
function seedDemo(store) {
|
|
9251
|
+
const db = store.db;
|
|
9252
|
+
const project = (id, name, root, icon, color) => db.query("INSERT OR IGNORE INTO projects (id, root, common_dir, name, discovered, created_at, icon, color) VALUES (?, ?, ?, ?, 0, ?, ?, ?)").run(id, root, `${root}/.git`, name, iso(90 * 24 * H), icon, color);
|
|
9253
|
+
project("p_demo1", "acme-app", "/work/acme-app", "\uD83D\uDED2", "c3");
|
|
9254
|
+
project("p_demo2", "acme-site", "/work/acme-site", "\uD83C\uDF10", "c5");
|
|
9255
|
+
const session = (id, pid, agent, title, cwd, branch, startedAgo, lastAgo, state, model) => {
|
|
9256
|
+
db.query(`INSERT OR IGNORE INTO sessions (id, project_id, kind, agent, cwd, branch, title, model, started_at, last_seen_at, last, last_type, last_text, state, tool_calls)
|
|
9257
|
+
VALUES (?, ?, 'interactive', ?, ?, ?, ?, ?, ?, ?, ?, 'tool.completed', ?, ?, ?)`).run(id, pid, agent, cwd, branch, title, model, iso(startedAgo), iso(lastAgo), state === "active" ? "Bash bun test" : "session ended", state === "active" ? "Running the suite before handing off." : "Done \u2014 PR opened, gates green.", state, 40 + Math.floor(Math.random() * 200));
|
|
9258
|
+
let t = startedAgo;
|
|
9259
|
+
let i = 0;
|
|
9260
|
+
while (t > lastAgo) {
|
|
9261
|
+
const out = 300 + Math.floor(Math.random() * 4000);
|
|
9262
|
+
const read = 50000 + Math.floor(Math.random() * 900000);
|
|
9263
|
+
db.query(`INSERT OR IGNORE 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)
|
|
9264
|
+
VALUES (?, ?, NULL, ?, ?, NULL, 0, ?, ?, ?, 0, ?, ?, ?, ?, '["Bash","Edit"]')`).run(`${id}-t${i}`, id, iso(t), model, 800 + i * 97 % 2000, out, 12000, read, i % 3 === 0 ? 900 : 0, 0.02 + out / 1e6 * 15 + read / 1e6 * 0.3, i % 4 === 0 ? "Tests are green; tightening the error path next." : "");
|
|
9265
|
+
t -= (8 + i * 13 % 30) * 60000;
|
|
9266
|
+
i++;
|
|
9267
|
+
}
|
|
9268
|
+
};
|
|
9269
|
+
session("demo-s1", "p_demo1", "claude-code", "Checkout flow refactor", "/work/acme-app-wt/checkout", "task/checkout", 5 * H, 2 * 60000, "active", "claude-fable-5");
|
|
9270
|
+
session("demo-s2", "p_demo1", "codex", "Fix flaky cart tests", "/work/acme-app", "main", 7 * H, 3 * H, "ended", "gpt-5.2-codex");
|
|
9271
|
+
session("demo-s3", "p_demo1", "gemini", "Payment webhook audit", "/work/acme-app", "main", 26 * H, 22 * H, "ended", "gemini-2.5-pro");
|
|
9272
|
+
session("demo-s4", "p_demo2", "grok", "Landing page rewrite", "/work/acme-site", "task/landing", 30 * H, 25 * H, "ended", "grok-4");
|
|
9273
|
+
session("demo-s5", "p_demo2", "claude-code", "SEO metadata sweep", "/work/acme-site", "main", 50 * H, 47 * H, "ended", "claude-sonnet-5");
|
|
9274
|
+
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
9275
|
+
VALUES ('p_demo1', 'checkout', 'demo-s1', '/work/acme-app-wt/checkout', 'task/checkout', ?, ?, NULL, 'held', 'agent', 'demo-s1')`).run(iso(5 * H), iso(-30 * 60000));
|
|
9276
|
+
db.query(`INSERT OR IGNORE INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
9277
|
+
VALUES ('p_demo1', 'webhooks', 'alice', '/work/acme-app-wt/webhooks', 'task/webhooks', ?, ?, NULL, 'orphaned', 'human', 'alice')`).run(iso(26 * H), iso(20 * H));
|
|
9278
|
+
const gate = (task, gate2, verdict, rubric, ago, sid) => db.query(`INSERT OR IGNORE INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
|
|
9279
|
+
VALUES ('p_demo1', ?, ?, ?, ?, NULL, ?, ?, 'daemon', 'daemon')`).run(task, gate2, verdict, rubric, sid, iso(ago));
|
|
9280
|
+
gate("checkout", "tests", "fail", "ran `bun test` \u2014 exit 1 in 41s", 3 * H, "demo-s1");
|
|
9281
|
+
gate("checkout", "tests", "pass", "ran `bun test` \u2014 exit 0 in 39s", 1 * H, "demo-s1");
|
|
9282
|
+
gate("checkout", "review", "pass", "review: no blocker/major findings", 40 * 60000, null);
|
|
9283
|
+
gate("webhooks", "tests", "pass", "ran `bun test` \u2014 exit 0 in 22s", 22 * H, "demo-s3");
|
|
9284
|
+
const ev = (type, ago, sid, payload) => store.append({
|
|
9285
|
+
ts: iso(ago),
|
|
9286
|
+
type,
|
|
9287
|
+
projectId: "p_demo1",
|
|
9288
|
+
sessionId: sid,
|
|
9289
|
+
payload
|
|
9290
|
+
});
|
|
9291
|
+
ev("incident.opened", 4 * H, "demo-s1", {
|
|
9292
|
+
rule: "pattern_kill",
|
|
9293
|
+
action: "ask",
|
|
9294
|
+
command: "pkill -f vite",
|
|
9295
|
+
reason: "This kills processes by command pattern \u2014 other agents' dev servers match too."
|
|
9296
|
+
});
|
|
9297
|
+
ev("incident.opened", 26 * H, "demo-s3", {
|
|
9298
|
+
rule: "shared_tree",
|
|
9299
|
+
action: "deny",
|
|
9300
|
+
command: "git reset --hard",
|
|
9301
|
+
reason: "Another session (demo-s2) is active in this same checkout."
|
|
9302
|
+
});
|
|
9303
|
+
ev("claim.acquired", 5 * H, "demo-s1", {
|
|
9304
|
+
task: "checkout",
|
|
9305
|
+
owner: "demo-s1",
|
|
9306
|
+
summary: "claim checkout"
|
|
9307
|
+
});
|
|
9308
|
+
ev("pr.opened", 30 * 60000, "demo-s1", {
|
|
9309
|
+
task: "checkout",
|
|
9310
|
+
url: "https://github.com/acme/app/pull/128",
|
|
9311
|
+
summary: "PR #128 opened for checkout"
|
|
9312
|
+
});
|
|
9313
|
+
ev("question.asked", 20 * 60000, "demo-s1", {
|
|
9314
|
+
id: 1,
|
|
9315
|
+
task: "checkout",
|
|
9316
|
+
text: "Coupon codes: keep the legacy endpoint alive for one release, or cut over now?",
|
|
9317
|
+
options: ["Keep one release", "Cut over"],
|
|
9318
|
+
summary: "question #1"
|
|
9319
|
+
});
|
|
9320
|
+
db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, options, asked_by, created_at)
|
|
9321
|
+
VALUES ('p_demo1', 'demo-s1', 'checkout', 'question', 'Coupon codes: keep the legacy endpoint alive for one release, or cut over now?', '["Keep one release","Cut over"]', 'demo-s1', ?)`).run(iso(20 * 60000));
|
|
9322
|
+
db.query(`INSERT OR IGNORE INTO messages (project_id, session_id, task, kind, text, asked_by, created_at, to_kind, from_session)
|
|
9323
|
+
VALUES ('p_demo1', 'demo-s1', 'checkout', 'message', 'Cart tests are green again \u2014 rebasing on main is safe now.', 'agent demo-s2', ?, 'task', 'demo-s2')`).run(iso(50 * 60000));
|
|
9324
|
+
db.query(`INSERT OR IGNORE INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
9325
|
+
VALUES ('p_demo1', 'webhooks', 'Signature validation + retries done', 'Dead-letter queue wiring', '["src/webhooks.ts","src/queue.ts"]', 'bun test \u2014 118 pass', 'auto:demo-s3', 'demo-s3', ?, 'daemon', 'daemon')`).run(iso(22 * H));
|
|
9326
|
+
db.query(`INSERT OR IGNORE INTO workflow_runs (project_id, task, workflow, step, step_label, steps, state, detail, started_at, updated_at, ended_at, actor_kind, actor_id)
|
|
9327
|
+
VALUES ('p_demo1', 'checkout', 'ship', 2, 'gate:review', '["implement","gate:tests","gate:review","pr"]', 'running', NULL, ?, ?, NULL, 'human', 'demo')`).run(iso(2 * H), iso(10 * 60000));
|
|
8481
9328
|
}
|
|
8482
9329
|
|
|
8483
9330
|
// packages/daemon/src/bin.ts
|
|
8484
9331
|
var DEFAULT_PORT2 = process.env.SWARM_PORT ? DEFAULT_PORT : loadConfig().daemon.port;
|
|
8485
|
-
var
|
|
9332
|
+
var appHooks = {};
|
|
9333
|
+
var { app, store, runner } = createApp(new Store, appHooks);
|
|
9334
|
+
if (process.env.SWARM_DEMO === "1" && isEmpty(store))
|
|
9335
|
+
seedDemo(store);
|
|
8486
9336
|
function serve() {
|
|
8487
9337
|
const bind = (p) => Bun.serve({ port: p, hostname: "127.0.0.1", idleTimeout: 0, fetch: app.fetch });
|
|
8488
9338
|
try {
|
|
@@ -8494,21 +9344,50 @@ function serve() {
|
|
|
8494
9344
|
return bind(0);
|
|
8495
9345
|
}
|
|
8496
9346
|
}
|
|
8497
|
-
var server
|
|
9347
|
+
var server;
|
|
9348
|
+
var restart = () => {
|
|
9349
|
+
console.error("swarmd: restarting into the version on disk\u2026");
|
|
9350
|
+
try {
|
|
9351
|
+
clearInterval(tailer);
|
|
9352
|
+
clearInterval(wtRefresh);
|
|
9353
|
+
clearInterval(pruner);
|
|
9354
|
+
} catch {}
|
|
9355
|
+
try {
|
|
9356
|
+
server.stop(true);
|
|
9357
|
+
} catch {}
|
|
9358
|
+
clearDaemonInfo();
|
|
9359
|
+
const [cmd, ...args] = daemonCommand();
|
|
9360
|
+
if (cmd)
|
|
9361
|
+
Bun.spawn([cmd, ...args], {
|
|
9362
|
+
stdin: "ignore",
|
|
9363
|
+
stdout: "ignore",
|
|
9364
|
+
stderr: "ignore",
|
|
9365
|
+
env: { ...process.env }
|
|
9366
|
+
}).unref();
|
|
9367
|
+
setTimeout(() => process.exit(0), 100);
|
|
9368
|
+
};
|
|
9369
|
+
appHooks.restart = restart;
|
|
9370
|
+
server = serve();
|
|
8498
9371
|
var port = server.port ?? DEFAULT_PORT2;
|
|
8499
9372
|
ensureToken();
|
|
8500
9373
|
writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
|
|
8501
9374
|
var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
|
|
8502
9375
|
var backfillMs = backfillDays * 24 * 60 * 60000;
|
|
8503
|
-
|
|
8504
|
-
|
|
9376
|
+
var DEMO = process.env.SWARM_DEMO === "1";
|
|
9377
|
+
if (!DEMO) {
|
|
9378
|
+
store.tailCodex(backfillMs);
|
|
9379
|
+
store.tailGrok(backfillMs);
|
|
9380
|
+
store.tailGemini(backfillMs);
|
|
9381
|
+
}
|
|
8505
9382
|
var tick = 0;
|
|
8506
9383
|
var tailer = setInterval(() => {
|
|
8507
9384
|
tick++;
|
|
8508
|
-
|
|
8509
|
-
|
|
9385
|
+
if (!DEMO)
|
|
9386
|
+
store.tailActive();
|
|
9387
|
+
if (!DEMO && (tick % 3 === 0 || store.hasActiveSessions())) {
|
|
8510
9388
|
store.tailCodex();
|
|
8511
9389
|
store.tailGrok();
|
|
9390
|
+
store.tailGemini();
|
|
8512
9391
|
}
|
|
8513
9392
|
store.reapResources();
|
|
8514
9393
|
store.reapProcesses();
|