@robzilla1738/agentswarm 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +4 -14
- package/dist/cli.js +19 -9
- package/dist/config.js +17 -0
- package/dist/crawltools.js +3 -22
- package/dist/executor.js +37 -21
- package/dist/hub.js +1 -3
- package/dist/journal.js +39 -5
- package/dist/memory.js +12 -7
- package/dist/report.js +2 -1
- package/dist/state.js +41 -19
- package/dist/tools.js +14 -2
- package/dist/util.js +32 -3
- package/dist/webtools.js +41 -18
- package/package.json +1 -1
- package/ui/out/404/index.html +1 -1
- package/ui/out/404.html +1 -1
- package/ui/out/_next/static/chunks/677-a62d486d6734bcf3.js +1 -0
- package/ui/out/_next/static/chunks/app/run/page-c29f95c51af08c60.js +1 -0
- package/ui/out/index.html +1 -1
- package/ui/out/index.txt +2 -2
- package/ui/out/run/index.html +1 -1
- package/ui/out/run/index.txt +2 -2
- package/ui/out/settings/index.html +1 -1
- package/ui/out/settings/index.txt +2 -2
- package/ui/out/_next/static/chunks/677-721ce1c8b7a6a317.js +0 -1
- package/ui/out/_next/static/chunks/app/run/page-3674e103981703a2.js +0 -1
- /package/ui/out/_next/static/{7_pihFubDGD40BCy2ynlr → JFkx5KtNi0DYyqm_THzbY}/_buildManifest.js +0 -0
- /package/ui/out/_next/static/{7_pihFubDGD40BCy2ynlr → JFkx5KtNi0DYyqm_THzbY}/_ssgManifest.js +0 -0
package/dist/agent.js
CHANGED
|
@@ -59,20 +59,10 @@ async function runAgent(p) {
|
|
|
59
59
|
if (stopReason)
|
|
60
60
|
break;
|
|
61
61
|
steps++;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
catch (e) {
|
|
67
|
-
// The chat client already retries 429/5xx; this catches the rest of the
|
|
68
|
-
// transient class (connection resets, DNS blips) once per step so a
|
|
69
|
-
// single network hiccup doesn't burn a whole task attempt.
|
|
70
|
-
if (p.signal.aborted)
|
|
71
|
-
throw e;
|
|
72
|
-
hooks.onLog?.("warn", `${p.agentId}: model call failed (${(0, util_1.errMsg)(e)}); retrying once`);
|
|
73
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
74
|
-
res = await callModel();
|
|
75
|
-
}
|
|
62
|
+
// No retry here: the chat client already retries the whole transient
|
|
63
|
+
// class (429/5xx/network) with backoff; anything that escapes it is
|
|
64
|
+
// permanent (4xx) and re-sending only doubles the doomed spend.
|
|
65
|
+
const res = await callModel();
|
|
76
66
|
hooks.onUsage?.(p.model, res.usage);
|
|
77
67
|
usage = (0, types_1.addUsage)(usage, res.usage);
|
|
78
68
|
if (res.toolCalls.length === 0) {
|
package/dist/cli.js
CHANGED
|
@@ -524,7 +524,16 @@ async function cmdConfig(rest, flags) {
|
|
|
524
524
|
const cfg = (0, config_1.loadConfig)();
|
|
525
525
|
if (sub === "get" && rest[1]) {
|
|
526
526
|
const key = rest[1];
|
|
527
|
-
|
|
527
|
+
if (key === "providers") {
|
|
528
|
+
// Nested per-provider creds — mask every apiKey, never dump raw.
|
|
529
|
+
const masked = Object.fromEntries(Object.entries(cfg.providers ?? {}).map(([id, c]) => [
|
|
530
|
+
id,
|
|
531
|
+
{ ...c, ...(c?.apiKey ? { apiKey: (0, config_1.maskKey)(c.apiKey) } : {}) },
|
|
532
|
+
]));
|
|
533
|
+
console.log(JSON.stringify(masked, null, 2));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const v = (0, config_1.isSecretConfigKey)(key) ? (0, config_1.maskKey)(String(cfg[key] ?? "")) : cfg[key];
|
|
528
537
|
console.log(typeof v === "object" ? JSON.stringify(v, null, 2) : String(v));
|
|
529
538
|
return;
|
|
530
539
|
}
|
|
@@ -533,7 +542,7 @@ async function cmdConfig(rest, flags) {
|
|
|
533
542
|
let v = cfg[k];
|
|
534
543
|
// Every secret-bearing key prints masked — `config list` output ends up
|
|
535
544
|
// in terminal scrollback and pasted bug reports.
|
|
536
|
-
if (
|
|
545
|
+
if ((0, config_1.isSecretConfigKey)(k)) {
|
|
537
546
|
v = v ? (0, config_1.maskKey)(String(v)) : k === "apiKey" ? util_1.ansi.red("(not set)") : "(not set)";
|
|
538
547
|
}
|
|
539
548
|
console.log(` ${k.padEnd(18)} ${util_1.ansi.gray(String(v))}`);
|
|
@@ -566,14 +575,15 @@ async function cmdConfig(rest, flags) {
|
|
|
566
575
|
const key = rest[1];
|
|
567
576
|
if (!key)
|
|
568
577
|
throw new Error("usage: swarm config unset <key>");
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const clearable = config_1.SETTABLE_KEYS.filter((k) => /apikey|token|secret|url|model/i.test(k));
|
|
572
|
-
if (!clearable.includes(key)) {
|
|
573
|
-
throw new Error(`not clearable. Clearable keys: ${clearable.join(", ")}`);
|
|
578
|
+
if (!config_1.SETTABLE_KEYS.includes(key)) {
|
|
579
|
+
throw new Error(`unknown key. Keys: ${config_1.SETTABLE_KEYS.join(", ")}`);
|
|
574
580
|
}
|
|
575
|
-
|
|
576
|
-
|
|
581
|
+
// apiKey/baseUrl route into the active provider's creds, so clearing
|
|
582
|
+
// means writing "". Everything else is deleted from the file outright —
|
|
583
|
+
// the default applies again (unset model:"" would brick every run).
|
|
584
|
+
const cred = key === "apiKey" || key === "baseUrl";
|
|
585
|
+
(0, config_1.saveConfig)({ [key]: cred ? "" : undefined });
|
|
586
|
+
console.log(util_1.ansi.green("✓ ") + `cleared ${key}` + (cred ? "" : util_1.ansi.gray(" — default applies")));
|
|
577
587
|
return;
|
|
578
588
|
}
|
|
579
589
|
if (sub === "path") {
|
package/dist/config.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.runDir = runDir;
|
|
|
41
41
|
exports.configPath = configPath;
|
|
42
42
|
exports.loadConfig = loadConfig;
|
|
43
43
|
exports.saveConfig = saveConfig;
|
|
44
|
+
exports.isSecretConfigKey = isSecretConfigKey;
|
|
44
45
|
exports.maskKey = maskKey;
|
|
45
46
|
exports.coerceConfigValue = coerceConfigValue;
|
|
46
47
|
const fs = __importStar(require("fs"));
|
|
@@ -192,6 +193,13 @@ function loadConfig() {
|
|
|
192
193
|
apiKey: cred.apiKey || "",
|
|
193
194
|
baseUrl: cred.baseUrl || info.baseUrl,
|
|
194
195
|
};
|
|
196
|
+
// A cleared/hand-edited model must fall back, not brick every run with
|
|
197
|
+
// model:"" requests. (cheapModel/strongModel legitimately clear to "" —
|
|
198
|
+
// they mean "use `model`".)
|
|
199
|
+
if (!cfg.model)
|
|
200
|
+
cfg.model = info.defaultModel;
|
|
201
|
+
if (!cfg.conductorModel)
|
|
202
|
+
cfg.conductorModel = cfg.model;
|
|
195
203
|
// Env overrides: provider-specific key env, plus legacy DEEPSEEK_API_KEY.
|
|
196
204
|
if (info.keyEnv && process.env[info.keyEnv])
|
|
197
205
|
cfg.apiKey = process.env[info.keyEnv];
|
|
@@ -249,6 +257,15 @@ function saveConfig(patch) {
|
|
|
249
257
|
}
|
|
250
258
|
return loadConfig();
|
|
251
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Config keys whose values must never print in cleartext — CLI output ends up
|
|
262
|
+
* in terminal scrollback and pasted bug reports. `providers` holds nested
|
|
263
|
+
* per-provider apiKeys, so it counts too. Single source of truth for the CLI
|
|
264
|
+
* masking sites (the hub's publicConfig is a strict allowlist already).
|
|
265
|
+
*/
|
|
266
|
+
function isSecretConfigKey(key) {
|
|
267
|
+
return /apikey|token|secret/i.test(key) || key === "providers";
|
|
268
|
+
}
|
|
252
269
|
function maskKey(key) {
|
|
253
270
|
if (!key)
|
|
254
271
|
return "";
|
package/dist/crawltools.js
CHANGED
|
@@ -138,7 +138,7 @@ async function firecrawlCrawl(cfg, opts, warnings) {
|
|
|
138
138
|
warnings.push(`crawl still running after 120s; returning ${partial.length} partial pages`);
|
|
139
139
|
return partial;
|
|
140
140
|
}
|
|
141
|
-
await sleep(pollMs, opts.signal);
|
|
141
|
+
await (0, util_1.sleep)(pollMs, opts.signal);
|
|
142
142
|
}
|
|
143
143
|
// Completed: collect pages, following `next` pagination until maxPages.
|
|
144
144
|
const pages = mapFirecrawlPages(last);
|
|
@@ -206,18 +206,12 @@ function friendlyHttpError(service, status, body) {
|
|
|
206
206
|
return new Error(`${service}: rate limited (HTTP 429) — retry later`);
|
|
207
207
|
return new Error(`${service}: HTTP ${status} ${(0, util_1.truncateMiddle)(body, 300, "chars")}`);
|
|
208
208
|
}
|
|
209
|
-
function mergeSignal(timeoutMs, signal) {
|
|
210
|
-
const t = AbortSignal.timeout(timeoutMs);
|
|
211
|
-
if (!signal)
|
|
212
|
-
return t;
|
|
213
|
-
return typeof AbortSignal.any === "function" ? AbortSignal.any([t, signal]) : signal;
|
|
214
|
-
}
|
|
215
209
|
async function callJson(service, url, key, body, timeoutMs, signal) {
|
|
216
210
|
const res = await fetch(url, {
|
|
217
211
|
method: "POST",
|
|
218
212
|
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
219
213
|
body: JSON.stringify(body),
|
|
220
|
-
signal: mergeSignal(timeoutMs, signal),
|
|
214
|
+
signal: (0, util_1.mergeSignal)(timeoutMs, signal),
|
|
221
215
|
});
|
|
222
216
|
if (!res.ok)
|
|
223
217
|
throw friendlyHttpError(service, res.status, await res.text().catch(() => ""));
|
|
@@ -226,22 +220,9 @@ async function callJson(service, url, key, body, timeoutMs, signal) {
|
|
|
226
220
|
async function getJson(service, url, key, signal) {
|
|
227
221
|
const res = await fetch(url, {
|
|
228
222
|
headers: { authorization: `Bearer ${key}` },
|
|
229
|
-
signal: mergeSignal(30_000, signal),
|
|
223
|
+
signal: (0, util_1.mergeSignal)(30_000, signal),
|
|
230
224
|
});
|
|
231
225
|
if (!res.ok)
|
|
232
226
|
throw friendlyHttpError(service, res.status, await res.text().catch(() => ""));
|
|
233
227
|
return res.json();
|
|
234
228
|
}
|
|
235
|
-
function sleep(ms, signal) {
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
const t = setTimeout(() => {
|
|
238
|
-
signal?.removeEventListener("abort", onAbort);
|
|
239
|
-
resolve();
|
|
240
|
-
}, ms);
|
|
241
|
-
const onAbort = () => {
|
|
242
|
-
clearTimeout(t);
|
|
243
|
-
reject(new Error("aborted"));
|
|
244
|
-
};
|
|
245
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
246
|
-
});
|
|
247
|
-
}
|
package/dist/executor.js
CHANGED
|
@@ -69,10 +69,9 @@ class Executor {
|
|
|
69
69
|
finishNotes = "";
|
|
70
70
|
finishReason = "";
|
|
71
71
|
fatal = null;
|
|
72
|
+
/** "error" = the turn ended in a call failure, not a decision. */
|
|
72
73
|
lastConductorAction = "none";
|
|
73
74
|
conductorFailures = 0;
|
|
74
|
-
/** True when the last conductor turn ended in a call error, not a decision. */
|
|
75
|
-
lastConductorErrored = false;
|
|
76
75
|
resumed = false;
|
|
77
76
|
sandbox;
|
|
78
77
|
mode;
|
|
@@ -138,8 +137,10 @@ class Executor {
|
|
|
138
137
|
// must not resurrect across a restart.
|
|
139
138
|
const settled = new Set(state.taskList().filter((t) => ["done", "failed", "blocked"].includes(t.status) && !reset.has(t.id)).map((t) => t.id));
|
|
140
139
|
this.notes = state.notes
|
|
141
|
-
.map((n) => ({ taskId: n.taskId, key: n.key, kind: n.kind, text: n.text, url: n.url }))
|
|
142
|
-
|
|
140
|
+
.map((n) => ({ taskId: n.taskId, teamId: n.teamId, key: n.key, kind: n.kind, text: n.text, url: n.url }))
|
|
141
|
+
// Team claims always drop: the owning child executor died with the
|
|
142
|
+
// crash, and a re-run team task re-claims from scratch.
|
|
143
|
+
.filter((n) => !(n.kind === "claim" && (n.teamId || (n.taskId && settled.has(n.taskId)))));
|
|
143
144
|
const lastPhase = state.phases[state.phases.length - 1];
|
|
144
145
|
if (lastPhase)
|
|
145
146
|
this.phase = { name: lastPhase.name, goal: lastPhase.goal, exitCriteria: lastPhase.exitCriteria };
|
|
@@ -398,6 +399,13 @@ class Executor {
|
|
|
398
399
|
sharedNotes: this.notes,
|
|
399
400
|
});
|
|
400
401
|
await child.run();
|
|
402
|
+
// The sub-swarm is over: claims its tasks left behind (e.g. after a child
|
|
403
|
+
// cancellation) are no longer live and must not haunt the shared board.
|
|
404
|
+
for (let i = this.notes.length - 1; i >= 0; i--) {
|
|
405
|
+
const n = this.notes[i];
|
|
406
|
+
if (n.kind === "claim" && n.teamId === task.id)
|
|
407
|
+
this.notes.splice(i, 1);
|
|
408
|
+
}
|
|
401
409
|
if (this.ac.signal.aborted) {
|
|
402
410
|
this.finalizeTask(task, "failed", "run cancelled");
|
|
403
411
|
return;
|
|
@@ -406,11 +414,13 @@ class Executor {
|
|
|
406
414
|
for (const a of child.teamArtifacts())
|
|
407
415
|
if (!task.artifacts.includes(a))
|
|
408
416
|
task.artifacts.push(a);
|
|
417
|
+
const ok = child.anyTaskDone();
|
|
418
|
+
const reportStatus = ok ? "done" : "blocked";
|
|
409
419
|
task.report = report;
|
|
410
|
-
task.reportStatus =
|
|
420
|
+
task.reportStatus = reportStatus;
|
|
411
421
|
this.journal.append("team.report", { taskId: task.id, report, artifacts: task.artifacts });
|
|
412
|
-
this.journal.append("task.report", { taskId: task.id, status:
|
|
413
|
-
this.finalizeTask(task,
|
|
422
|
+
this.journal.append("task.report", { taskId: task.id, status: reportStatus, report, artifacts: task.artifacts });
|
|
423
|
+
this.finalizeTask(task, ok ? "done" : "failed", report);
|
|
414
424
|
}
|
|
415
425
|
async mainLoop() {
|
|
416
426
|
while (!this.finishing) {
|
|
@@ -446,7 +456,7 @@ class Executor {
|
|
|
446
456
|
// An errored turn is not a decision — keep looping so the breaker
|
|
447
457
|
// can retry (and eventually trip) instead of misreading the error
|
|
448
458
|
// as "the conductor chose to stop".
|
|
449
|
-
if (this.lastConductorAction !== "spawn" &&
|
|
459
|
+
if (this.lastConductorAction !== "spawn" && this.lastConductorAction !== "error") {
|
|
450
460
|
this.finishing = true;
|
|
451
461
|
this.finishReason = this.finishReason || "all tasks settled";
|
|
452
462
|
}
|
|
@@ -455,7 +465,7 @@ class Executor {
|
|
|
455
465
|
// Stuck: pending tasks exist but can't run (failed/blocked deps).
|
|
456
466
|
this.appendConductorUpdate("Some tasks cannot run because their dependencies failed or were blocked. Re-plan around them or finish.", reports);
|
|
457
467
|
await this.conductorTurn();
|
|
458
|
-
if (this.lastConductorAction === "wait"
|
|
468
|
+
if (this.lastConductorAction === "wait") {
|
|
459
469
|
this.finishing = true;
|
|
460
470
|
this.finishReason = "stalled: dependencies unmet and conductor chose to wait";
|
|
461
471
|
}
|
|
@@ -596,12 +606,10 @@ class Executor {
|
|
|
596
606
|
const scale = Number(process.env.SWARM_BACKOFF_SCALE || "1") || 1;
|
|
597
607
|
const backoff = [2_000, 5_000, 15_000, 30_000][Math.min(this.conductorFailures - 1, 3)] * scale;
|
|
598
608
|
await new Promise((r) => setTimeout(r, backoff));
|
|
599
|
-
this.lastConductorAction = "
|
|
600
|
-
this.lastConductorErrored = true;
|
|
609
|
+
this.lastConductorAction = "error";
|
|
601
610
|
return;
|
|
602
611
|
}
|
|
603
612
|
this.conductorFailures = 0;
|
|
604
|
-
this.lastConductorErrored = false;
|
|
605
613
|
this.onUsage(this.meta.options.conductorModel, res.usage);
|
|
606
614
|
if (res.content.trim())
|
|
607
615
|
this.journal.append("conductor.say", { text: (0, util_1.clip)(res.content, 4000) });
|
|
@@ -783,7 +791,8 @@ class Executor {
|
|
|
783
791
|
return reports.map(prompts_1.reportBlock);
|
|
784
792
|
const important = reports.filter((t) => t.status !== "done");
|
|
785
793
|
const done = reports.filter((t) => t.status === "done");
|
|
786
|
-
const
|
|
794
|
+
const room = Math.max(0, CAP - important.length);
|
|
795
|
+
const fullDone = room > 0 ? done.slice(-room) : []; // slice(-0) would return everything
|
|
787
796
|
const briefDone = done.slice(0, done.length - fullDone.length);
|
|
788
797
|
return [
|
|
789
798
|
...important.map(prompts_1.reportBlock),
|
|
@@ -1012,7 +1021,7 @@ class Executor {
|
|
|
1012
1021
|
signal: this.ac.signal,
|
|
1013
1022
|
addCheckpoint: task ? (summary) => this.recordCheckpoint(task, agentId, summary) : undefined,
|
|
1014
1023
|
addNote: (text, key, kind, url) => {
|
|
1015
|
-
this.notes.push({ taskId: task?.id, key, kind, text, url });
|
|
1024
|
+
this.notes.push({ taskId: task?.id, teamId: this.teamId, key, kind, text, url });
|
|
1016
1025
|
// Only the recent tail ever feeds digests; without a cap a multi-day
|
|
1017
1026
|
// run accumulates every note in memory. Decisions and conflicts are
|
|
1018
1027
|
// kept regardless. In-place splice: teams share this array by reference.
|
|
@@ -1035,11 +1044,16 @@ class Executor {
|
|
|
1035
1044
|
readReport: (taskId) => this.readReportText(taskId),
|
|
1036
1045
|
checkClaim: (rel) => {
|
|
1037
1046
|
const norm = rel.replace(/^\.\//, "");
|
|
1038
|
-
const claim = this.notes.find((n) =>
|
|
1039
|
-
n.key
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1047
|
+
const claim = this.notes.find((n) => {
|
|
1048
|
+
if (n.kind !== "claim" || n.key !== norm || !n.taskId)
|
|
1049
|
+
return false;
|
|
1050
|
+
// Another executor's claim: its tasks aren't in this.tasks, but
|
|
1051
|
+
// claims are spliced out when their task settles (and when a team
|
|
1052
|
+
// ends), so presence alone means the holder is still live.
|
|
1053
|
+
if (n.teamId !== this.teamId)
|
|
1054
|
+
return true;
|
|
1055
|
+
return n.taskId !== task?.id && ["running", "verifying"].includes(this.tasks.get(n.taskId)?.status ?? "");
|
|
1056
|
+
});
|
|
1043
1057
|
return claim
|
|
1044
1058
|
? `⚠ ${claim.taskId} holds a claim on ${norm} ("${(0, util_1.oneLine)(claim.text, 80)}") — coordinate via the blackboard before further edits.`
|
|
1045
1059
|
: null;
|
|
@@ -1401,7 +1415,7 @@ class Executor {
|
|
|
1401
1415
|
// teams share this array by reference.
|
|
1402
1416
|
for (let i = this.notes.length - 1; i >= 0; i--) {
|
|
1403
1417
|
const n = this.notes[i];
|
|
1404
|
-
if (n.kind === "claim" && n.taskId === task.id)
|
|
1418
|
+
if (n.kind === "claim" && n.taskId === task.id && n.teamId === this.teamId)
|
|
1405
1419
|
this.notes.splice(i, 1);
|
|
1406
1420
|
}
|
|
1407
1421
|
this.journal.append("task.status", { taskId: task.id, status, attempt: task.attempt, reason });
|
|
@@ -1508,7 +1522,9 @@ class Executor {
|
|
|
1508
1522
|
queueDelta(agentId, taskId, channel, text) {
|
|
1509
1523
|
// Deltas are UI sugar, never state — thin them under load so a 100-agent
|
|
1510
1524
|
// swarm doesn't write gigabytes of streaming chatter into the journal.
|
|
1511
|
-
|
|
1525
|
+
// inflight.size over-counts verifying tasks slightly, but these are fuzzy
|
|
1526
|
+
// thresholds and this runs per streaming token — O(1) matters here.
|
|
1527
|
+
const load = this.inflight.size;
|
|
1512
1528
|
if (channel === "think" && load > 48) {
|
|
1513
1529
|
if (!this.thinkDropLogged) {
|
|
1514
1530
|
this.thinkDropLogged = true;
|
package/dist/hub.js
CHANGED
|
@@ -180,9 +180,7 @@ async function api(req, res, url, opts) {
|
|
|
180
180
|
return { engine, ok: false, detail: (0, util_1.errMsg)(e) };
|
|
181
181
|
}
|
|
182
182
|
};
|
|
183
|
-
const checks =
|
|
184
|
-
if (cfg.tinyfishApiKey)
|
|
185
|
-
checks.push(probe("tinyfish", () => (0, webtools_1.tinyfishSearch)(cfg, q, 3)));
|
|
183
|
+
const checks = (0, webtools_1.searchEngines)(cfg).map((e) => probe(e.name, () => e.search(q, 3)));
|
|
186
184
|
const engines = await Promise.all(checks);
|
|
187
185
|
return sendJson(res, 200, { ok: engines.some((e) => e.ok), engines });
|
|
188
186
|
}
|
package/dist/journal.js
CHANGED
|
@@ -88,11 +88,14 @@ class Journal {
|
|
|
88
88
|
}
|
|
89
89
|
return ev;
|
|
90
90
|
}
|
|
91
|
+
/** The chunk an async drain is writing right now — flushSync must see it. */
|
|
92
|
+
inFlight = "";
|
|
91
93
|
async drain() {
|
|
92
94
|
if (!this.buf)
|
|
93
95
|
return;
|
|
94
96
|
const chunk = this.buf;
|
|
95
97
|
this.buf = "";
|
|
98
|
+
this.inFlight = chunk;
|
|
96
99
|
try {
|
|
97
100
|
await fs.promises.appendFile(this.file, chunk, "utf8");
|
|
98
101
|
this.failures = 0;
|
|
@@ -107,16 +110,26 @@ class Journal {
|
|
|
107
110
|
process.stderr.write(`agentswarm: journal writes are failing (${String(e)}); run state is no longer durable\n`);
|
|
108
111
|
}
|
|
109
112
|
}
|
|
113
|
+
finally {
|
|
114
|
+
this.inFlight = "";
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
flush() {
|
|
112
118
|
return this.chain.then(() => this.drain());
|
|
113
119
|
}
|
|
114
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Last-gasp synchronous flush for signal handlers and exit paths. Includes
|
|
122
|
+
* any chunk a pending async drain holds: process.exit would abandon that
|
|
123
|
+
* write, silently losing just-settled events. If the abandoned write did
|
|
124
|
+
* land first, the chunk appears twice — readers dedupe by seq.
|
|
125
|
+
*/
|
|
115
126
|
flushSync() {
|
|
116
|
-
|
|
127
|
+
const pending = this.inFlight + this.buf;
|
|
128
|
+
if (!pending)
|
|
117
129
|
return;
|
|
118
130
|
try {
|
|
119
|
-
fs.appendFileSync(this.file,
|
|
131
|
+
fs.appendFileSync(this.file, pending, "utf8");
|
|
132
|
+
this.inFlight = "";
|
|
120
133
|
this.buf = "";
|
|
121
134
|
}
|
|
122
135
|
catch {
|
|
@@ -136,7 +149,24 @@ function readEvents(runDirPath) {
|
|
|
136
149
|
catch {
|
|
137
150
|
return [];
|
|
138
151
|
}
|
|
139
|
-
return parseLines(raw).events;
|
|
152
|
+
return dedupeBySeq(parseLines(raw).events);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Seq is strictly increasing in a healthy journal; a chunk can appear twice
|
|
156
|
+
* when a signal-handler flushSync raced an in-flight async append. Replays of
|
|
157
|
+
* already-seen seqs are dropped.
|
|
158
|
+
*/
|
|
159
|
+
function dedupeBySeq(events, lastSeq = 0) {
|
|
160
|
+
let max = lastSeq;
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const ev of events) {
|
|
163
|
+
if (typeof ev.seq === "number" && ev.seq <= max)
|
|
164
|
+
continue;
|
|
165
|
+
if (typeof ev.seq === "number")
|
|
166
|
+
max = ev.seq;
|
|
167
|
+
out.push(ev);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
140
170
|
}
|
|
141
171
|
function lastSeq(runDirPath) {
|
|
142
172
|
const evs = readEvents(runDirPath);
|
|
@@ -155,6 +185,7 @@ function readNewEvents(file, state) {
|
|
|
155
185
|
// Truncated/rewritten (should not happen) — start over.
|
|
156
186
|
state.offset = 0;
|
|
157
187
|
state.carry = "";
|
|
188
|
+
state.lastSeq = 0;
|
|
158
189
|
}
|
|
159
190
|
if (stat.size === state.offset)
|
|
160
191
|
return [];
|
|
@@ -172,7 +203,10 @@ function readNewEvents(file, state) {
|
|
|
172
203
|
const text = state.carry + buf.toString("utf8", 0, n);
|
|
173
204
|
const parsed = parseLines(text, true);
|
|
174
205
|
state.carry = parsed.carry;
|
|
175
|
-
|
|
206
|
+
const fresh = dedupeBySeq(parsed.events, state.lastSeq ?? 0);
|
|
207
|
+
if (fresh.length)
|
|
208
|
+
state.lastSeq = fresh[fresh.length - 1].seq;
|
|
209
|
+
out.push(...fresh);
|
|
176
210
|
}
|
|
177
211
|
state.offset += read;
|
|
178
212
|
return out;
|
package/dist/memory.js
CHANGED
|
@@ -38,7 +38,6 @@ exports.loadMemory = loadMemory;
|
|
|
38
38
|
exports.appendMemory = appendMemory;
|
|
39
39
|
exports.memoryBlock = memoryBlock;
|
|
40
40
|
const crypto = __importStar(require("crypto"));
|
|
41
|
-
const fs = __importStar(require("fs"));
|
|
42
41
|
const path = __importStar(require("path"));
|
|
43
42
|
const config_1 = require("./config");
|
|
44
43
|
const util_1 = require("./util");
|
|
@@ -48,13 +47,19 @@ function memoryFile(cwd) {
|
|
|
48
47
|
return path.join((0, config_1.home)(), "memory", `${hash}.json`);
|
|
49
48
|
}
|
|
50
49
|
function loadMemory(cwd) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return Array.isArray(raw?.entries) ? raw.entries : [];
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
50
|
+
const raw = (0, util_1.readJson)(memoryFile(cwd), {});
|
|
51
|
+
if (!Array.isArray(raw.entries))
|
|
56
52
|
return [];
|
|
57
|
-
|
|
53
|
+
// Memory is best-effort and the file is user-editable: one malformed entry
|
|
54
|
+
// must degrade to "forgotten", never crash a run at startup.
|
|
55
|
+
return raw.entries.filter((e) => !!e &&
|
|
56
|
+
typeof e === "object" &&
|
|
57
|
+
typeof e.mission === "string" &&
|
|
58
|
+
typeof e.summary === "string" &&
|
|
59
|
+
typeof e.status === "string" &&
|
|
60
|
+
Number.isFinite(e.finishedAt) &&
|
|
61
|
+
Array.isArray(e.keyDecisions) &&
|
|
62
|
+
e.keyDecisions.every((d) => typeof d === "string"));
|
|
58
63
|
}
|
|
59
64
|
function appendMemory(cwd, entry) {
|
|
60
65
|
try {
|
package/dist/report.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.sourcesBlock = sourcesBlock;
|
|
|
16
16
|
exports.mdToHtml = mdToHtml;
|
|
17
17
|
exports.renderFinalHtml = renderFinalHtml;
|
|
18
18
|
const searchcore_1 = require("./searchcore");
|
|
19
|
+
const util_1 = require("./util");
|
|
19
20
|
/**
|
|
20
21
|
* Dedupe every task's reported sources (by canonical URL) into one numbered
|
|
21
22
|
* bibliography for the synthesizer. First occurrence wins the number; later
|
|
@@ -278,7 +279,7 @@ function renderFinalHtml(o) {
|
|
|
278
279
|
<span class="badge ${o.status}">${o.status}</span>
|
|
279
280
|
<span>run ${esc(o.runId)}</span>
|
|
280
281
|
<span>${esc(date)}</span>
|
|
281
|
-
<span title="${esc(o.mission.slice(0, 600))}">mission: ${esc(
|
|
282
|
+
<span title="${esc(o.mission.slice(0, 600))}">mission: ${esc((0, util_1.clip)(o.mission, 90))}</span>
|
|
282
283
|
</header>
|
|
283
284
|
<main>
|
|
284
285
|
${mdToHtml(o.markdown)}
|
package/dist/state.js
CHANGED
|
@@ -59,6 +59,12 @@ class RunState {
|
|
|
59
59
|
this.cost += (0, types_1.usageCost)(u, this.pricing[model]);
|
|
60
60
|
this.pushBudgetPoint(ev.t);
|
|
61
61
|
}
|
|
62
|
+
else if (ev.type === "note.added") {
|
|
63
|
+
// The blackboard is shared swarm-wide at runtime, so team notes are
|
|
64
|
+
// root facts too — without this, a resume would forget every note a
|
|
65
|
+
// team agent posted (decisions included).
|
|
66
|
+
this.pushNote(ev, teamId);
|
|
67
|
+
}
|
|
62
68
|
return;
|
|
63
69
|
}
|
|
64
70
|
switch (ev.type) {
|
|
@@ -200,25 +206,7 @@ class RunState {
|
|
|
200
206
|
});
|
|
201
207
|
break;
|
|
202
208
|
case "note.added":
|
|
203
|
-
this.
|
|
204
|
-
t: ev.t,
|
|
205
|
-
taskId: ev.taskId,
|
|
206
|
-
agentId: ev.agentId,
|
|
207
|
-
key: ev.key,
|
|
208
|
-
kind: ev.kind,
|
|
209
|
-
text: ev.text,
|
|
210
|
-
url: typeof ev.url === "string" ? ev.url : undefined,
|
|
211
|
-
});
|
|
212
|
-
// Reduced state is held live by the hub and the resume seed — keep
|
|
213
|
-
// only the tail that digests/views actually use. Decisions and
|
|
214
|
-
// conflicts are never dropped: they anchor long-horizon coherence.
|
|
215
|
-
if (this.notes.length > 1000) {
|
|
216
|
-
const keep = (n) => n.kind === "decision" || n.kind === "conflict";
|
|
217
|
-
const pinned = this.notes.filter(keep);
|
|
218
|
-
const rest = this.notes.filter((n) => !keep(n));
|
|
219
|
-
rest.splice(0, rest.length - Math.max(0, 1000 - pinned.length));
|
|
220
|
-
this.notes = [...pinned, ...rest].sort((a, b) => a.t - b.t);
|
|
221
|
-
}
|
|
209
|
+
this.pushNote(ev);
|
|
222
210
|
break;
|
|
223
211
|
case "conductor.say":
|
|
224
212
|
this.conductorLog.push({ t: ev.t, text: ev.text });
|
|
@@ -269,6 +257,40 @@ class RunState {
|
|
|
269
257
|
this.budgetSeries = this.budgetSeries.filter((_, i) => i % 2 === 0 || i === this.budgetSeries.length - 1);
|
|
270
258
|
}
|
|
271
259
|
}
|
|
260
|
+
pushNote(ev, teamId) {
|
|
261
|
+
this.notes.push({
|
|
262
|
+
t: ev.t,
|
|
263
|
+
taskId: ev.taskId,
|
|
264
|
+
teamId,
|
|
265
|
+
agentId: ev.agentId,
|
|
266
|
+
key: ev.key,
|
|
267
|
+
kind: ev.kind,
|
|
268
|
+
text: ev.text,
|
|
269
|
+
url: typeof ev.url === "string" ? ev.url : undefined,
|
|
270
|
+
});
|
|
271
|
+
// Reduced state is held live by the hub and the resume seed — keep only
|
|
272
|
+
// the tail that digests/views actually use. Decisions and conflicts are
|
|
273
|
+
// never dropped: they anchor long-horizon coherence. Forward-pass splice
|
|
274
|
+
// (mirroring the executor's addNote): the array is permanently at the cap
|
|
275
|
+
// once a long run passes it, so this runs on every note event — no
|
|
276
|
+
// filter/sort allocations on the reducer hot path.
|
|
277
|
+
if (this.notes.length > 1000) {
|
|
278
|
+
const keep = (n) => n.kind === "decision" || n.kind === "conflict";
|
|
279
|
+
let pinnedCount = 0;
|
|
280
|
+
for (const n of this.notes)
|
|
281
|
+
if (keep(n))
|
|
282
|
+
pinnedCount++;
|
|
283
|
+
let toDrop = this.notes.length - Math.max(pinnedCount, 1000);
|
|
284
|
+
for (let i = 0; i < this.notes.length && toDrop > 0;) {
|
|
285
|
+
if (!keep(this.notes[i])) {
|
|
286
|
+
this.notes.splice(i, 1);
|
|
287
|
+
toDrop--;
|
|
288
|
+
}
|
|
289
|
+
else
|
|
290
|
+
i++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
272
294
|
taskList() {
|
|
273
295
|
return this.taskOrder.map((id) => this.tasks.get(id)).filter(Boolean);
|
|
274
296
|
}
|
package/dist/tools.js
CHANGED
|
@@ -301,9 +301,21 @@ function workerToolset(cfg) {
|
|
|
301
301
|
const excludes = ["node_modules", ".git", "dist", ".next", "out", "build", "target", "__pycache__", ".venv"]
|
|
302
302
|
.map((d) => ` --exclude-dir=${d}`)
|
|
303
303
|
.join("");
|
|
304
|
-
|
|
304
|
+
// No `| head`: a pipe would mask grep's exit code, and an invalid regex
|
|
305
|
+
// or unreadable path must fail loudly, not read as "no matches".
|
|
306
|
+
// (Output volume is already bounded by the sandbox's collect cap.)
|
|
307
|
+
const cmd = `grep ${flags}${include}${excludes} -e ${shq(pattern)} ${shq(root)}`;
|
|
305
308
|
const r = await ctx.sandbox.exec(cmd, { cwd: ctx.workdir, timeoutSec: 60, signal: ctx.signal });
|
|
306
|
-
|
|
309
|
+
// Sandbox exec merges stderr into out — separate grep's diagnostics.
|
|
310
|
+
const all = r.out.split("\n").filter(Boolean);
|
|
311
|
+
const diags = all.filter((l) => l.startsWith("grep:"));
|
|
312
|
+
const lines = all.filter((l) => !l.startsWith("grep:"));
|
|
313
|
+
// Exit 1 = clean no-match. Anything past 1 with zero matches is a real
|
|
314
|
+
// failure (bad pattern, missing path); with matches it's partial
|
|
315
|
+
// (some files unreadable) and the matches still count.
|
|
316
|
+
if (r.code !== 0 && r.code !== 1 && !lines.length) {
|
|
317
|
+
throw new Error(`grep failed (exit ${r.code}): ${diags.join("; ").slice(0, 300) || "no error detail"}`);
|
|
318
|
+
}
|
|
307
319
|
if (!lines.length)
|
|
308
320
|
return "no matches";
|
|
309
321
|
const shown = lines.slice(0, max);
|
package/dist/util.js
CHANGED
|
@@ -33,8 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ansi =
|
|
36
|
+
exports.ansi = void 0;
|
|
37
37
|
exports.rid = rid;
|
|
38
|
+
exports.sleep = sleep;
|
|
39
|
+
exports.mergeSignal = mergeSignal;
|
|
38
40
|
exports.truncateMiddle = truncateMiddle;
|
|
39
41
|
exports.clip = clip;
|
|
40
42
|
exports.oneLine = oneLine;
|
|
@@ -62,8 +64,35 @@ function rid(prefix) {
|
|
|
62
64
|
const c = ridCounter.toString(36).padStart(2, "0");
|
|
63
65
|
return `${prefix}_${t}${r}${c}`;
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
/** Resolves after ms; rejects early if the signal aborts first. */
|
|
68
|
+
function sleep(ms, signal) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
if (!signal) {
|
|
71
|
+
setTimeout(resolve, ms);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (signal.aborted) {
|
|
75
|
+
reject(new Error("aborted"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const t = setTimeout(() => {
|
|
79
|
+
signal.removeEventListener("abort", onAbort);
|
|
80
|
+
resolve();
|
|
81
|
+
}, ms);
|
|
82
|
+
const onAbort = () => {
|
|
83
|
+
clearTimeout(t);
|
|
84
|
+
reject(new Error("aborted"));
|
|
85
|
+
};
|
|
86
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** A timeout signal, combined with the caller's signal when one is given. */
|
|
90
|
+
function mergeSignal(timeoutMs, signal) {
|
|
91
|
+
const t = AbortSignal.timeout(timeoutMs);
|
|
92
|
+
if (!signal)
|
|
93
|
+
return t;
|
|
94
|
+
return typeof AbortSignal.any === "function" ? AbortSignal.any([t, signal]) : signal;
|
|
95
|
+
}
|
|
67
96
|
// ---------- strings ----------
|
|
68
97
|
function truncateMiddle(s, max, label = "bytes") {
|
|
69
98
|
if (s.length <= max)
|