@wrongstack/core 0.148.0 → 0.236.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-bridge-r9y6gdn4.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
- package/dist/{agent-subagent-runner-1GeQE_L0.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
- package/dist/{brain-Cp_3GIS2.d.ts → brain-sCZ3lCjq.d.ts} +28 -2
- package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
- package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
- package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
- package/dist/coordination/index.d.ts +70 -13
- package/dist/coordination/index.js +2126 -151
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +27 -27
- package/dist/defaults/index.js +1328 -354
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +45 -16
- package/dist/execution/index.js +367 -59
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +86 -0
- package/dist/execution/prompt-enhancer.js +125 -0
- package/dist/execution/prompt-enhancer.js.map +1 -0
- package/dist/extension/index.d.ts +6 -6
- package/dist/extension/index.js +3 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/{goal-preamble-CYJLg0wk.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
- package/dist/{index-BZdezm3g.d.ts → index-BlMqh5GO.d.ts} +8 -8
- package/dist/{index-CPweVoFM.d.ts → index-C2eSNPsB.d.ts} +7 -5
- package/dist/index.d.ts +439 -129
- package/dist/index.js +5206 -905
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -7
- package/dist/infrastructure/index.js +72 -15
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +7 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
- package/dist/logger-DmmQhf4P.d.ts +65 -0
- package/dist/{mcp-servers-Bl5LTvQg.d.ts → mcp-servers-DFbirBv6.d.ts} +11 -4
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +89 -9
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-QWEzJDlm.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
- package/dist/{null-fleet-bus-BUyfqh23.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
- package/dist/observability/index.d.ts +2 -2
- package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
- package/dist/{parallel-eternal-engine-C75QuhAI.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
- package/dist/{path-resolver-DRjQBkoO.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
- package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
- package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
- package/dist/pipeline-DsmlwTXu.d.ts +493 -0
- package/dist/{plan-templates-CkKNPU3I.d.ts → plan-templates-DPABrDvy.d.ts} +19 -8
- package/dist/{provider-runner-BNpuIyOL.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
- package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +358 -85
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-DoISxaKO.d.ts → secret-vault-BJDY28ev.d.ts} +7 -1
- package/dist/{secret-vault-BTcC_T5v.d.ts → secret-vault-CeVNiy_f.d.ts} +4 -3
- package/dist/security/index.d.ts +6 -5
- package/dist/security/index.js +214 -35
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
- package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
- package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
- package/dist/skills/index.js +171 -21
- package/dist/skills/index.js.map +1 -1
- package/dist/storage/index.d.ts +151 -13
- package/dist/storage/index.js +1117 -256
- package/dist/storage/index.js.map +1 -1
- package/dist/types/index.d.ts +68 -21
- package/dist/types/index.js +616 -74
- package/dist/types/index.js.map +1 -1
- package/dist/utils/expect-defined.js +3 -1
- package/dist/utils/expect-defined.js.map +1 -1
- package/dist/utils/index.d.ts +80 -4
- package/dist/utils/index.js +100 -15
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
- package/package.json +7 -3
- package/skills/chimera/SKILL.md +105 -0
- package/skills/research-web/SKILL.md +342 -0
- package/dist/logger-B9J5puGM.d.ts +0 -32
- package/dist/pipeline-BG7UgbDc.d.ts +0 -239
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { randomUUID, randomBytes } from 'crypto';
|
|
1
|
+
import { randomUUID, createHash, randomBytes } from 'crypto';
|
|
2
2
|
import * as fsp6 from 'fs/promises';
|
|
3
3
|
import * as path4 from 'path';
|
|
4
4
|
import { isAbsolute, resolve } from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
5
6
|
import { hostname } from 'os';
|
|
6
7
|
import { EventEmitter } from 'events';
|
|
7
8
|
|
|
@@ -54,12 +55,12 @@ var BrainDecisionQueue = class {
|
|
|
54
55
|
options: request.options,
|
|
55
56
|
rationale: "Decision escalated to human authority."
|
|
56
57
|
};
|
|
57
|
-
const pending = new Promise((
|
|
58
|
-
const entry = { request, resolve:
|
|
58
|
+
const pending = new Promise((resolve3) => {
|
|
59
|
+
const entry = { request, resolve: resolve3 };
|
|
59
60
|
if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
|
|
60
61
|
entry.timer = setTimeout(() => {
|
|
61
62
|
this.pending.delete(request.id);
|
|
62
|
-
|
|
63
|
+
resolve3({ type: "deny", reason: "Brain human decision timed out." });
|
|
63
64
|
}, this.opts.timeoutMs);
|
|
64
65
|
}
|
|
65
66
|
this.pending.set(request.id, entry);
|
|
@@ -191,6 +192,49 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
191
192
|
async function ensureDir(dir) {
|
|
192
193
|
await fsp6.mkdir(dir, { recursive: true });
|
|
193
194
|
}
|
|
195
|
+
async function withFileLock(targetPath, fn, opts = {}) {
|
|
196
|
+
const dir = path4.dirname(targetPath);
|
|
197
|
+
await fsp6.mkdir(dir, { recursive: true });
|
|
198
|
+
const lockPath = path4.join(dir, `.${path4.basename(targetPath)}.lock`);
|
|
199
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
200
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
201
|
+
const started = Date.now();
|
|
202
|
+
let handle;
|
|
203
|
+
for (; ; ) {
|
|
204
|
+
try {
|
|
205
|
+
handle = await fsp6.open(lockPath, "wx");
|
|
206
|
+
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
207
|
+
break;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err.code !== "EEXIST") throw err;
|
|
210
|
+
try {
|
|
211
|
+
const stat5 = await fsp6.stat(lockPath);
|
|
212
|
+
if (Date.now() - stat5.mtimeMs > staleMs) {
|
|
213
|
+
await fsp6.unlink(lockPath);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (Date.now() - started >= timeoutMs) {
|
|
220
|
+
throw new Error(`Timed out waiting for file lock: ${targetPath}`);
|
|
221
|
+
}
|
|
222
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
return await fn();
|
|
227
|
+
} finally {
|
|
228
|
+
try {
|
|
229
|
+
await handle?.close();
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await fsp6.unlink(lockPath);
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
194
238
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
195
239
|
async function renameWithRetry(from, to) {
|
|
196
240
|
if (process.platform !== "win32") {
|
|
@@ -209,7 +253,7 @@ async function renameWithRetry(from, to) {
|
|
|
209
253
|
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
210
254
|
throw err;
|
|
211
255
|
}
|
|
212
|
-
await new Promise((
|
|
256
|
+
await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
|
|
213
257
|
}
|
|
214
258
|
}
|
|
215
259
|
throw lastErr;
|
|
@@ -395,6 +439,62 @@ function safeParse(input, maxBytes = 5e6) {
|
|
|
395
439
|
}
|
|
396
440
|
}
|
|
397
441
|
|
|
442
|
+
// src/types/errors.ts
|
|
443
|
+
var ERROR_CODES = {
|
|
444
|
+
// Provider
|
|
445
|
+
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
446
|
+
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
447
|
+
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
448
|
+
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
449
|
+
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
450
|
+
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
|
|
451
|
+
// Agent
|
|
452
|
+
AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
|
|
453
|
+
AGENT_ABORTED: "AGENT_ABORTED",
|
|
454
|
+
AGENT_RUN_FAILED: "AGENT_RUN_FAILED"};
|
|
455
|
+
var WrongStackError = class extends Error {
|
|
456
|
+
code;
|
|
457
|
+
subsystem;
|
|
458
|
+
severity;
|
|
459
|
+
recoverable;
|
|
460
|
+
context;
|
|
461
|
+
constructor(opts) {
|
|
462
|
+
super(opts.message, { cause: opts.cause });
|
|
463
|
+
this.name = "WrongStackError";
|
|
464
|
+
this.code = opts.code;
|
|
465
|
+
this.subsystem = opts.subsystem;
|
|
466
|
+
this.severity = opts.severity ?? "error";
|
|
467
|
+
this.recoverable = opts.recoverable ?? false;
|
|
468
|
+
this.context = opts.context;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Render a one-line user-facing description.
|
|
472
|
+
* Subclasses should override for domain-specific formatting.
|
|
473
|
+
*/
|
|
474
|
+
describe() {
|
|
475
|
+
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
476
|
+
return `${this.code}: ${this.message}${ctx}`;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
function formatContext(ctx) {
|
|
480
|
+
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
481
|
+
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
482
|
+
}
|
|
483
|
+
var AgentError = class extends WrongStackError {
|
|
484
|
+
constructor(opts) {
|
|
485
|
+
super({
|
|
486
|
+
message: opts.message,
|
|
487
|
+
code: opts.code,
|
|
488
|
+
subsystem: "agent",
|
|
489
|
+
severity: opts.code === ERROR_CODES.AGENT_ABORTED ? "warning" : "error",
|
|
490
|
+
recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
491
|
+
context: opts.context,
|
|
492
|
+
cause: opts.cause
|
|
493
|
+
});
|
|
494
|
+
this.name = "AgentError";
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
398
498
|
// src/coordination/in-memory-transport.ts
|
|
399
499
|
var InMemoryBridgeTransport = class {
|
|
400
500
|
subs = /* @__PURE__ */ new Map();
|
|
@@ -483,28 +583,40 @@ var InMemoryAgentBridge = class {
|
|
|
483
583
|
return () => this.subscriptions.delete(handler);
|
|
484
584
|
}
|
|
485
585
|
async request(msg, timeoutMs) {
|
|
486
|
-
if (this.stopped) throw new
|
|
586
|
+
if (this.stopped) throw new AgentError({
|
|
587
|
+
message: "Bridge is stopped",
|
|
588
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
589
|
+
});
|
|
487
590
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
488
591
|
const correlationId = msg.id;
|
|
489
592
|
if (this.inflightGuards.has(correlationId)) {
|
|
490
|
-
throw new
|
|
491
|
-
`Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids
|
|
492
|
-
|
|
593
|
+
throw new AgentError({
|
|
594
|
+
message: `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`,
|
|
595
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
596
|
+
context: { correlationId }
|
|
597
|
+
});
|
|
493
598
|
}
|
|
494
599
|
this.inflightGuards.add(correlationId);
|
|
495
|
-
return new Promise((
|
|
600
|
+
return new Promise((resolve3, reject) => {
|
|
496
601
|
const timer = setTimeout(() => {
|
|
497
602
|
this.inflightGuards.delete(correlationId);
|
|
498
603
|
this.pendingRequests.delete(correlationId);
|
|
499
|
-
reject(new
|
|
604
|
+
reject(new AgentError({
|
|
605
|
+
message: `Request ${correlationId} timed out after ${timeout}ms`,
|
|
606
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
607
|
+
context: { correlationId, timeoutMs: timeout }
|
|
608
|
+
}));
|
|
500
609
|
}, timeout);
|
|
501
610
|
if (!this.inflightGuards.has(correlationId)) {
|
|
502
611
|
clearTimeout(timer);
|
|
503
|
-
reject(new
|
|
612
|
+
reject(new AgentError({
|
|
613
|
+
message: "Bridge stopped",
|
|
614
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
615
|
+
}));
|
|
504
616
|
return;
|
|
505
617
|
}
|
|
506
618
|
this.pendingRequests.set(correlationId, {
|
|
507
|
-
resolve:
|
|
619
|
+
resolve: resolve3,
|
|
508
620
|
reject,
|
|
509
621
|
timer
|
|
510
622
|
});
|
|
@@ -521,7 +633,10 @@ var InMemoryAgentBridge = class {
|
|
|
521
633
|
this.stopped = true;
|
|
522
634
|
for (const [, p] of this.pendingRequests) {
|
|
523
635
|
clearTimeout(p.timer);
|
|
524
|
-
p.reject(new
|
|
636
|
+
p.reject(new AgentError({
|
|
637
|
+
message: "Bridge stopped",
|
|
638
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
639
|
+
}));
|
|
525
640
|
}
|
|
526
641
|
this.pendingRequests.clear();
|
|
527
642
|
this.inflightGuards.clear();
|
|
@@ -546,7 +661,9 @@ function createMessage(type, from, payload, to) {
|
|
|
546
661
|
// src/utils/expect-defined.ts
|
|
547
662
|
function expectDefined(value, label) {
|
|
548
663
|
if (value === null || value === void 0) {
|
|
549
|
-
|
|
664
|
+
const err = new Error("Expected value to be defined");
|
|
665
|
+
err.name = "ExpectDefinedError";
|
|
666
|
+
throw err;
|
|
550
667
|
}
|
|
551
668
|
return value;
|
|
552
669
|
}
|
|
@@ -1306,7 +1423,23 @@ Bridge contract:
|
|
|
1306
1423
|
subagents' context. Those are not yours to read.
|
|
1307
1424
|
- Your final task output is what the Director sees. Be concise,
|
|
1308
1425
|
structured, and self-contained \u2014 assume the Director will paste your
|
|
1309
|
-
output into its own context
|
|
1426
|
+
output into its own context.
|
|
1427
|
+
|
|
1428
|
+
Inter-agent mailbox (if you have the \`mail_send\`/\`mail_inbox\`/\`mailbox\` tools):
|
|
1429
|
+
- You are part of a project-wide fleet that may span other terminals and
|
|
1430
|
+
WebUIs. Your mailbox identity is \`<your-name>@<session-tag>\` (unique
|
|
1431
|
+
per session); mail addressed to you, to your bare name, or broadcast
|
|
1432
|
+
to \`*\` is injected into your conversation automatically before each
|
|
1433
|
+
step \u2014 read it once, it is marked read.
|
|
1434
|
+
- Broadcast milestones: when you complete a significant piece of work,
|
|
1435
|
+
\`mail_send to="*"\` a one-line summary so parallel agents don't collide
|
|
1436
|
+
with or duplicate it.
|
|
1437
|
+
- Hand off matching work: if another online agent's role fits a follow-up
|
|
1438
|
+
better (e.g. a reviewer while you just wrote code), \`mail_send\` it to
|
|
1439
|
+
their exact id instead of doing everything yourself. Discover ids with
|
|
1440
|
+
\`mailbox action=online\`.
|
|
1441
|
+
- Answer your mail: reply to the sender's exact \`from\` id. When done with
|
|
1442
|
+
an assigned task, post a \`result\` back to whoever assigned it.`;
|
|
1310
1443
|
function composeDirectorPrompt(parts = {}) {
|
|
1311
1444
|
const sections = [];
|
|
1312
1445
|
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
@@ -1380,11 +1513,11 @@ var HEAVY_BUDGET = {
|
|
|
1380
1513
|
};
|
|
1381
1514
|
var TOOLS = {
|
|
1382
1515
|
/** Pure read/inspect — safe for analysis and review agents. */
|
|
1383
|
-
read: ["read", "grep", "glob", "search", "tree"],
|
|
1516
|
+
read: ["read", "grep", "glob", "search", "tree", "mailbox"],
|
|
1384
1517
|
/** Read + structured inspection (logs, diffs, json, dependency audit). */
|
|
1385
|
-
inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit"],
|
|
1518
|
+
inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
|
|
1386
1519
|
/** Read + edit (no shell). For agents that write code/docs but don't run it. */
|
|
1387
|
-
write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch"],
|
|
1520
|
+
write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
|
|
1388
1521
|
/** Full build loop: edit + run (lint/format/typecheck/test/bash). */
|
|
1389
1522
|
build: [
|
|
1390
1523
|
"read",
|
|
@@ -1401,16 +1534,17 @@ var TOOLS = {
|
|
|
1401
1534
|
"lint",
|
|
1402
1535
|
"format",
|
|
1403
1536
|
"typecheck",
|
|
1404
|
-
"test"
|
|
1537
|
+
"test",
|
|
1538
|
+
"mailbox"
|
|
1405
1539
|
],
|
|
1406
1540
|
/** Version control. */
|
|
1407
1541
|
vcs: ["read", "grep", "glob", "git", "diff"],
|
|
1408
1542
|
/** Dependency management + CVE audit. */
|
|
1409
|
-
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json"],
|
|
1543
|
+
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
|
|
1410
1544
|
/** Documentation authoring. */
|
|
1411
|
-
docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document"],
|
|
1545
|
+
docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
|
|
1412
1546
|
/** Web research. */
|
|
1413
|
-
research: ["read", "grep", "glob", "search", "fetch"]
|
|
1547
|
+
research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
|
|
1414
1548
|
};
|
|
1415
1549
|
|
|
1416
1550
|
// src/coordination/agents/phase1-discovery.ts
|
|
@@ -2208,15 +2342,44 @@ Working rules:
|
|
|
2208
2342
|
id: "e2e",
|
|
2209
2343
|
name: "E2E",
|
|
2210
2344
|
role: "e2e",
|
|
2211
|
-
tools: [
|
|
2345
|
+
tools: [
|
|
2346
|
+
...TOOLS.build,
|
|
2347
|
+
"fetch",
|
|
2348
|
+
"playwright_navigate",
|
|
2349
|
+
"playwright_screenshot",
|
|
2350
|
+
"playwright_click",
|
|
2351
|
+
"playwright_type",
|
|
2352
|
+
"playwright_evaluate",
|
|
2353
|
+
"playwright_select_option",
|
|
2354
|
+
"playwright_hover",
|
|
2355
|
+
"playwright_fill_form",
|
|
2356
|
+
"playwright_wait_for",
|
|
2357
|
+
"playwright_press_key",
|
|
2358
|
+
"playwright_drag"
|
|
2359
|
+
],
|
|
2212
2360
|
prompt: `You are the E2E agent. Your job is end-to-end testing: drive the whole
|
|
2213
2361
|
system the way a user would and verify the full flow works across boundaries.
|
|
2214
2362
|
|
|
2215
2363
|
Scope:
|
|
2216
2364
|
- Author end-to-end scenarios that exercise real user journeys
|
|
2217
2365
|
- Drive UI/CLI/API across process and network boundaries
|
|
2366
|
+
- Use Playwright browser tools (navigate, click, type, screenshot, evaluate)
|
|
2367
|
+
to automate web UI flows \u2014 open pages, interact with forms, capture evidence
|
|
2218
2368
|
- Set up and tear down realistic test state
|
|
2219
|
-
- Capture failures with enough detail to reproduce (screenshots, logs)
|
|
2369
|
+
- Capture failures with enough detail to reproduce (screenshots, logs, page HTML)
|
|
2370
|
+
|
|
2371
|
+
Playwright tools available (require the "playwright" MCP server to be enabled):
|
|
2372
|
+
playwright_navigate(url) \u2014 open a page at the given URL
|
|
2373
|
+
playwright_screenshot() \u2014 capture a full-page or viewport screenshot
|
|
2374
|
+
playwright_click(selector) \u2014 click on an element matching a CSS selector
|
|
2375
|
+
playwright_type(selector, text) \u2014 type text into a focused input element
|
|
2376
|
+
playwright_evaluate(script) \u2014 run arbitrary JavaScript in the page context
|
|
2377
|
+
playwright_select_option(selector, value) \u2014 pick a <select> dropdown option
|
|
2378
|
+
playwright_hover(selector) \u2014 hover the mouse over an element
|
|
2379
|
+
playwright_fill_form(fields) \u2014 fill multiple form fields in one call
|
|
2380
|
+
playwright_wait_for(selector) \u2014 block until an element appears on the page
|
|
2381
|
+
playwright_press_key(key) \u2014 press a keyboard key (Enter, Tab, Escape, \u2026)
|
|
2382
|
+
playwright_drag(from, to) \u2014 drag an element from one selector to another
|
|
2220
2383
|
|
|
2221
2384
|
Input format you accept:
|
|
2222
2385
|
{ "task": "scenario | smoke | journey", "flow": "<user journey>", "surface": "ui | cli | api" }
|
|
@@ -2230,8 +2393,10 @@ Output: Markdown e2e report:
|
|
|
2230
2393
|
Working rules:
|
|
2231
2394
|
- Test the real flow end to end; don't stub the thing under test
|
|
2232
2395
|
- Make scenarios deterministic \u2014 control time, randomness, and external state
|
|
2233
|
-
- On failure, capture artifacts (logs
|
|
2234
|
-
- Keep scenarios independent so one failure doesn't cascade
|
|
2396
|
+
- On failure, capture artifacts (screenshots, page HTML, logs) for reproduction
|
|
2397
|
+
- Keep scenarios independent so one failure doesn't cascade
|
|
2398
|
+
- For browser tests: playwright_navigate first, then interact, then playwright_screenshot as evidence
|
|
2399
|
+
- If playwright tools are unavailable, report it and fall back to API/CLI testing`
|
|
2235
2400
|
},
|
|
2236
2401
|
budget: HEAVY_BUDGET,
|
|
2237
2402
|
capability: {
|
|
@@ -2244,10 +2409,106 @@ Working rules:
|
|
|
2244
2409
|
"user journey",
|
|
2245
2410
|
"smoke test",
|
|
2246
2411
|
"playwright",
|
|
2412
|
+
"browser",
|
|
2413
|
+
"screenshot",
|
|
2414
|
+
"web ui",
|
|
2415
|
+
"headless",
|
|
2247
2416
|
"cypress",
|
|
2248
2417
|
"full flow",
|
|
2249
2418
|
"browser test",
|
|
2250
|
-
"acceptance test"
|
|
2419
|
+
"acceptance test",
|
|
2420
|
+
"navigate",
|
|
2421
|
+
"click",
|
|
2422
|
+
"form fill",
|
|
2423
|
+
"dom",
|
|
2424
|
+
"page load"
|
|
2425
|
+
]
|
|
2426
|
+
}
|
|
2427
|
+
},
|
|
2428
|
+
{
|
|
2429
|
+
config: {
|
|
2430
|
+
id: "browser",
|
|
2431
|
+
name: "Browser",
|
|
2432
|
+
role: "browser",
|
|
2433
|
+
tools: [
|
|
2434
|
+
...TOOLS.read,
|
|
2435
|
+
"fetch",
|
|
2436
|
+
"playwright_navigate",
|
|
2437
|
+
"playwright_screenshot",
|
|
2438
|
+
"playwright_click",
|
|
2439
|
+
"playwright_type",
|
|
2440
|
+
"playwright_evaluate",
|
|
2441
|
+
"playwright_select_option",
|
|
2442
|
+
"playwright_hover",
|
|
2443
|
+
"playwright_fill_form",
|
|
2444
|
+
"playwright_wait_for",
|
|
2445
|
+
"playwright_press_key",
|
|
2446
|
+
"playwright_drag"
|
|
2447
|
+
],
|
|
2448
|
+
prompt: `You are the Browser agent. Your job is browser automation: open web pages,
|
|
2449
|
+
interact with them, extract data, capture screenshots, and return structured
|
|
2450
|
+
results. You are a read-focused agent \u2014 you drive the browser, not the filesystem.
|
|
2451
|
+
|
|
2452
|
+
Scope:
|
|
2453
|
+
- Navigate to URLs and wait for pages to load
|
|
2454
|
+
- Take full-page or element screenshots as evidence
|
|
2455
|
+
- Click buttons, fill forms, select options, type text \u2014 full user simulation
|
|
2456
|
+
- Extract page content: text, HTML, element attributes, data tables
|
|
2457
|
+
- Evaluate JavaScript in the page context to extract structured data
|
|
2458
|
+
- Verify visual state (element visibility, text content, attribute values)
|
|
2459
|
+
|
|
2460
|
+
Playwright tools available (require the "playwright" MCP server to be enabled):
|
|
2461
|
+
playwright_navigate(url) \u2014 open a page at the given URL
|
|
2462
|
+
playwright_screenshot() \u2014 capture a full-page or viewport screenshot
|
|
2463
|
+
playwright_click(selector) \u2014 click on an element matching a CSS selector
|
|
2464
|
+
playwright_type(selector, text) \u2014 type text into a focused input element
|
|
2465
|
+
playwright_evaluate(script) \u2014 run arbitrary JavaScript in the page context
|
|
2466
|
+
playwright_select_option(selector, value) \u2014 pick a <select> dropdown option
|
|
2467
|
+
playwright_hover(selector) \u2014 hover the mouse over an element
|
|
2468
|
+
playwright_fill_form(fields) \u2014 fill multiple form fields in one call
|
|
2469
|
+
playwright_wait_for(selector) \u2014 block until an element appears on the page
|
|
2470
|
+
playwright_press_key(key) \u2014 press a keyboard key (Enter, Tab, Escape, \u2026)
|
|
2471
|
+
playwright_drag(from, to) \u2014 drag an element from one selector to another
|
|
2472
|
+
|
|
2473
|
+
Input format you accept:
|
|
2474
|
+
{ "task": "navigate | screenshot | extract | interact | verify", "url": "<url>", "steps": ["step1", "step2"] }
|
|
2475
|
+
|
|
2476
|
+
Output: Structured markdown report:
|
|
2477
|
+
- ## Page (URL, title, load status)
|
|
2478
|
+
- ## Actions Taken (step-by-step with timestamps)
|
|
2479
|
+
- ## Results (extracted data, element states, verification results)
|
|
2480
|
+
- ## Screenshots (list attached screenshot references)
|
|
2481
|
+
- ## Errors (any failures with stack traces)
|
|
2482
|
+
|
|
2483
|
+
Working rules:
|
|
2484
|
+
- Always playwright_navigate first before any interaction
|
|
2485
|
+
- Always playwright_wait_for after navigation to ensure the page is ready
|
|
2486
|
+
- playwright_screenshot is your primary evidence \u2014 use it before and after interactions
|
|
2487
|
+
- Use playwright_evaluate for structured data extraction (JSON, text content)
|
|
2488
|
+
- If a selector fails, try alternative selectors before giving up
|
|
2489
|
+
- Report exact CSS selectors used \u2014 they're part of the evidence
|
|
2490
|
+
- If playwright tools are unavailable, report the error immediately \u2014 do not guess`
|
|
2491
|
+
},
|
|
2492
|
+
budget: MEDIUM_BUDGET,
|
|
2493
|
+
capability: {
|
|
2494
|
+
phase: "verify",
|
|
2495
|
+
summary: "Browser automation: opens pages, clicks, types, screenshots, extracts data via Playwright headless Chromium.",
|
|
2496
|
+
keywords: [
|
|
2497
|
+
"browser",
|
|
2498
|
+
"screenshot",
|
|
2499
|
+
"navigate",
|
|
2500
|
+
"web page",
|
|
2501
|
+
"scrape",
|
|
2502
|
+
"crawl",
|
|
2503
|
+
"headless",
|
|
2504
|
+
"chrome",
|
|
2505
|
+
"open url",
|
|
2506
|
+
"capture",
|
|
2507
|
+
"page title",
|
|
2508
|
+
"extract data",
|
|
2509
|
+
"fill form",
|
|
2510
|
+
"click button",
|
|
2511
|
+
"take screenshot"
|
|
2251
2512
|
]
|
|
2252
2513
|
}
|
|
2253
2514
|
},
|
|
@@ -3696,7 +3957,7 @@ Working rules:
|
|
|
3696
3957
|
id: "tech-stack",
|
|
3697
3958
|
name: "Tech Stack Validator",
|
|
3698
3959
|
role: "tech-stack",
|
|
3699
|
-
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json"],
|
|
3960
|
+
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json", "mailbox"],
|
|
3700
3961
|
prompt: `You are the Tech Stack Validator \u2014 a single-shot validation agent that fires
|
|
3701
3962
|
before any package, library, or framework choice is committed.
|
|
3702
3963
|
|
|
@@ -3704,6 +3965,16 @@ Your ONLY job: verify that a technology choice is current, real, and not obsolet
|
|
|
3704
3965
|
You are the "this isn't code, this is 10-year-old technology" agent. Intervene
|
|
3705
3966
|
hard when the LLM hallucinates a version number or suggests dead tech.
|
|
3706
3967
|
|
|
3968
|
+
## Before you begin
|
|
3969
|
+
|
|
3970
|
+
Check the inter-agent mailbox for pending tasks. Other agents or the file-watcher
|
|
3971
|
+
may have left assign messages with dependency files to audit:
|
|
3972
|
+
- mailbox action=check
|
|
3973
|
+
|
|
3974
|
+
If you find an assign message, use the specified file path and packages.
|
|
3975
|
+
When done, post results back:
|
|
3976
|
+
- mailbox action=send to=<sender> type=result subject="Tech stack audit results" body="..."
|
|
3977
|
+
|
|
3707
3978
|
## Critical rules
|
|
3708
3979
|
|
|
3709
3980
|
1. **Verify existence.** Search npm registry (fetch https://registry.npmjs.org/<pkg>/latest)
|
|
@@ -3762,11 +4033,11 @@ When APPROVED:
|
|
|
3762
4033
|
**Install**: pnpm add <name>@^<major>.<minor>.0`
|
|
3763
4034
|
},
|
|
3764
4035
|
budget: {
|
|
3765
|
-
timeoutMs:
|
|
3766
|
-
maxIterations:
|
|
3767
|
-
maxToolCalls:
|
|
3768
|
-
maxTokens:
|
|
3769
|
-
maxCostUsd: 0.
|
|
4036
|
+
timeoutMs: 12e4,
|
|
4037
|
+
maxIterations: 10,
|
|
4038
|
+
maxToolCalls: 40,
|
|
4039
|
+
maxTokens: 6e4,
|
|
4040
|
+
maxCostUsd: 0.25
|
|
3770
4041
|
},
|
|
3771
4042
|
capability: {
|
|
3772
4043
|
phase: "meta",
|
|
@@ -4898,12 +5169,12 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
4898
5169
|
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
4899
5170
|
return Promise.resolve("stop");
|
|
4900
5171
|
}
|
|
4901
|
-
return new Promise((
|
|
5172
|
+
return new Promise((resolve3) => {
|
|
4902
5173
|
let resolved = false;
|
|
4903
5174
|
const respond = (d) => {
|
|
4904
5175
|
if (resolved) return;
|
|
4905
5176
|
resolved = true;
|
|
4906
|
-
|
|
5177
|
+
resolve3(d);
|
|
4907
5178
|
};
|
|
4908
5179
|
const fallback = setTimeout(
|
|
4909
5180
|
() => respond("stop"),
|
|
@@ -5024,44 +5295,6 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5024
5295
|
}
|
|
5025
5296
|
};
|
|
5026
5297
|
|
|
5027
|
-
// src/types/errors.ts
|
|
5028
|
-
var ERROR_CODES = {
|
|
5029
|
-
// Provider
|
|
5030
|
-
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
5031
|
-
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
5032
|
-
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
5033
|
-
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
5034
|
-
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
5035
|
-
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR"};
|
|
5036
|
-
var WrongStackError = class extends Error {
|
|
5037
|
-
code;
|
|
5038
|
-
subsystem;
|
|
5039
|
-
severity;
|
|
5040
|
-
recoverable;
|
|
5041
|
-
context;
|
|
5042
|
-
constructor(opts) {
|
|
5043
|
-
super(opts.message, { cause: opts.cause });
|
|
5044
|
-
this.name = "WrongStackError";
|
|
5045
|
-
this.code = opts.code;
|
|
5046
|
-
this.subsystem = opts.subsystem;
|
|
5047
|
-
this.severity = opts.severity ?? "error";
|
|
5048
|
-
this.recoverable = opts.recoverable ?? false;
|
|
5049
|
-
this.context = opts.context;
|
|
5050
|
-
}
|
|
5051
|
-
/**
|
|
5052
|
-
* Render a one-line user-facing description.
|
|
5053
|
-
* Subclasses should override for domain-specific formatting.
|
|
5054
|
-
*/
|
|
5055
|
-
describe() {
|
|
5056
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
5057
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
5058
|
-
}
|
|
5059
|
-
};
|
|
5060
|
-
function formatContext(ctx) {
|
|
5061
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
5062
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
5063
|
-
}
|
|
5064
|
-
|
|
5065
5298
|
// src/types/provider.ts
|
|
5066
5299
|
var ProviderError = class extends WrongStackError {
|
|
5067
5300
|
status;
|
|
@@ -5136,6 +5369,9 @@ function providerStatusToCode(status, type) {
|
|
|
5136
5369
|
|
|
5137
5370
|
// src/coordination/coordinator/error-classifier.ts
|
|
5138
5371
|
function classifySubagentError(err, hints = {}) {
|
|
5372
|
+
if (err instanceof AgentError && err.cause) {
|
|
5373
|
+
return classifySubagentError(err.cause, hints);
|
|
5374
|
+
}
|
|
5139
5375
|
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
5140
5376
|
if (err instanceof ProviderError) {
|
|
5141
5377
|
const baseMessage2 = err.describe();
|
|
@@ -5168,7 +5404,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
5168
5404
|
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
5169
5405
|
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
5170
5406
|
}
|
|
5171
|
-
if (/empty response
|
|
5407
|
+
if (/empty response/i.test(baseMessage)) {
|
|
5172
5408
|
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
5173
5409
|
}
|
|
5174
5410
|
if (/^tool failed: /i.test(baseMessage)) {
|
|
@@ -5853,7 +6089,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
5853
6089
|
taskIds.map((id) => {
|
|
5854
6090
|
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
5855
6091
|
if (cached) return cached;
|
|
5856
|
-
return new Promise((
|
|
6092
|
+
return new Promise((resolve3, reject) => {
|
|
5857
6093
|
const timeout = setTimeout(() => {
|
|
5858
6094
|
this.off("task.completed", handler);
|
|
5859
6095
|
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
@@ -5862,7 +6098,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
5862
6098
|
if (result.taskId === id) {
|
|
5863
6099
|
clearTimeout(timeout);
|
|
5864
6100
|
this.off("task.completed", handler);
|
|
5865
|
-
|
|
6101
|
+
resolve3(result);
|
|
5866
6102
|
}
|
|
5867
6103
|
};
|
|
5868
6104
|
this.on("task.completed", handler);
|
|
@@ -7235,11 +7471,11 @@ var Director = class _Director {
|
|
|
7235
7471
|
if (cached) return cached;
|
|
7236
7472
|
const existing = this.taskWaiters.get(id);
|
|
7237
7473
|
if (existing) return existing.promise;
|
|
7238
|
-
let
|
|
7474
|
+
let resolve3;
|
|
7239
7475
|
const promise = new Promise((res) => {
|
|
7240
|
-
|
|
7476
|
+
resolve3 = res;
|
|
7241
7477
|
});
|
|
7242
|
-
this.taskWaiters.set(id, { promise, resolve:
|
|
7478
|
+
this.taskWaiters.set(id, { promise, resolve: resolve3 });
|
|
7243
7479
|
return promise;
|
|
7244
7480
|
})
|
|
7245
7481
|
);
|
|
@@ -7635,7 +7871,7 @@ function createDelegateTool(opts) {
|
|
|
7635
7871
|
subagentId
|
|
7636
7872
|
});
|
|
7637
7873
|
const dir = director;
|
|
7638
|
-
const result = await new Promise((
|
|
7874
|
+
const result = await new Promise((resolve3) => {
|
|
7639
7875
|
let settled = false;
|
|
7640
7876
|
let timer;
|
|
7641
7877
|
const finish = (value) => {
|
|
@@ -7645,7 +7881,7 @@ function createDelegateTool(opts) {
|
|
|
7645
7881
|
offTool();
|
|
7646
7882
|
offIter();
|
|
7647
7883
|
offProgress();
|
|
7648
|
-
|
|
7884
|
+
resolve3(value);
|
|
7649
7885
|
};
|
|
7650
7886
|
const arm = () => {
|
|
7651
7887
|
if (timer) clearTimeout(timer);
|
|
@@ -7906,6 +8142,7 @@ async function readSubagentPartial(opts, subagentId) {
|
|
|
7906
8142
|
function makeAgentSubagentRunner(opts) {
|
|
7907
8143
|
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
7908
8144
|
return async (task, ctx) => {
|
|
8145
|
+
const taskStartedAt = Date.now();
|
|
7909
8146
|
const factoryResult = await opts.factory(ctx.config);
|
|
7910
8147
|
const { agent, events } = factoryResult;
|
|
7911
8148
|
const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
|
|
@@ -8002,7 +8239,7 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8002
8239
|
}),
|
|
8003
8240
|
events.on("provider.text_delta", (e) => {
|
|
8004
8241
|
ctx.budget.markActivity();
|
|
8005
|
-
streamingTextAcc = (streamingTextAcc + e.text).slice(-
|
|
8242
|
+
streamingTextAcc = (streamingTextAcc + e.text).slice(-2e3);
|
|
8006
8243
|
})
|
|
8007
8244
|
);
|
|
8008
8245
|
const onParentAbort = () => aborter.abort();
|
|
@@ -8010,6 +8247,15 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8010
8247
|
let result;
|
|
8011
8248
|
try {
|
|
8012
8249
|
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
8250
|
+
events.emit("subagent.task_completed", {
|
|
8251
|
+
subagentId: ctx.subagentId,
|
|
8252
|
+
taskId: task.id,
|
|
8253
|
+
status: result.status === "done" ? "success" : "failed",
|
|
8254
|
+
iterations: result.iterations,
|
|
8255
|
+
toolCalls: ctx.budget.usage().toolCalls,
|
|
8256
|
+
durationMs: Date.now() - taskStartedAt,
|
|
8257
|
+
finalText: result.finalText?.trim() || void 0
|
|
8258
|
+
});
|
|
8013
8259
|
} finally {
|
|
8014
8260
|
detachFleet?.();
|
|
8015
8261
|
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
@@ -8045,21 +8291,40 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8045
8291
|
if (budgetError) throw budgetError;
|
|
8046
8292
|
}
|
|
8047
8293
|
if (result.status === "failed") {
|
|
8048
|
-
throw result.error instanceof
|
|
8294
|
+
throw result.error instanceof AgentError ? result.error : new AgentError({
|
|
8295
|
+
message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
|
|
8296
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
8297
|
+
cause: result.error
|
|
8298
|
+
});
|
|
8049
8299
|
}
|
|
8050
8300
|
if (result.status === "aborted") {
|
|
8051
|
-
throw new
|
|
8301
|
+
throw new AgentError({
|
|
8302
|
+
message: "agent aborted",
|
|
8303
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
8304
|
+
});
|
|
8052
8305
|
}
|
|
8053
8306
|
if (result.status === "max_iterations") {
|
|
8054
|
-
throw new
|
|
8307
|
+
throw new AgentError({
|
|
8308
|
+
message: "agent exhausted iteration limit",
|
|
8309
|
+
code: ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
8310
|
+
recoverable: true
|
|
8311
|
+
});
|
|
8055
8312
|
}
|
|
8056
8313
|
const usage = ctx.budget.usage();
|
|
8057
8314
|
const finalText = (result.finalText ?? "").trim();
|
|
8058
8315
|
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
8059
|
-
throw new
|
|
8316
|
+
throw new AgentError({
|
|
8317
|
+
message: "empty response \u2014 agent produced no text and no tool calls",
|
|
8318
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
8319
|
+
context: { iterations: result.iterations }
|
|
8320
|
+
});
|
|
8060
8321
|
}
|
|
8061
8322
|
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
8062
|
-
throw new
|
|
8323
|
+
throw new AgentError({
|
|
8324
|
+
message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
|
|
8325
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
8326
|
+
context: { tool: lastToolFailed, iterations: result.iterations }
|
|
8327
|
+
});
|
|
8063
8328
|
}
|
|
8064
8329
|
return {
|
|
8065
8330
|
result: result.finalText,
|
|
@@ -8224,7 +8489,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8224
8489
|
onClose: (s) => this.appendToIndex(s)
|
|
8225
8490
|
});
|
|
8226
8491
|
} catch (err) {
|
|
8227
|
-
await handle.close().catch((e) => console.warn(
|
|
8492
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
8493
|
+
level: "warn",
|
|
8494
|
+
event: "session_store.handle_close_failed",
|
|
8495
|
+
message: e instanceof Error ? e.message : String(e),
|
|
8496
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8497
|
+
})));
|
|
8228
8498
|
throw err;
|
|
8229
8499
|
}
|
|
8230
8500
|
}
|
|
@@ -8251,11 +8521,25 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8251
8521
|
provider: data.metadata.provider
|
|
8252
8522
|
},
|
|
8253
8523
|
this.events,
|
|
8254
|
-
{
|
|
8524
|
+
{
|
|
8525
|
+
resumed: true,
|
|
8526
|
+
// Shard directory (sessions/<date>/) — must match create() so the
|
|
8527
|
+
// .summary.json sidecar lands next to the JSONL instead of the
|
|
8528
|
+
// sessions root (where summaryFor() would never find it).
|
|
8529
|
+
dir: path4.dirname(file),
|
|
8530
|
+
filePath: file,
|
|
8531
|
+
secretScrubber: this.secretScrubber,
|
|
8532
|
+
onClose: (s) => this.appendToIndex(s)
|
|
8533
|
+
}
|
|
8255
8534
|
);
|
|
8256
8535
|
return { writer, data };
|
|
8257
8536
|
} catch (err) {
|
|
8258
|
-
await handle.close().catch((e) => console.warn(
|
|
8537
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
8538
|
+
level: "warn",
|
|
8539
|
+
event: "session_store.handle_close_failed",
|
|
8540
|
+
message: e instanceof Error ? e.message : String(e),
|
|
8541
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8542
|
+
})));
|
|
8259
8543
|
throw err;
|
|
8260
8544
|
}
|
|
8261
8545
|
}
|
|
@@ -8275,7 +8559,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8275
8559
|
}
|
|
8276
8560
|
const meta = this.metaFromEvents(id, events);
|
|
8277
8561
|
const { messages, usage } = this.replay(events, id);
|
|
8278
|
-
|
|
8562
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
8563
|
+
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
8279
8564
|
}
|
|
8280
8565
|
async list(limit = 20) {
|
|
8281
8566
|
try {
|
|
@@ -8431,10 +8716,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8431
8716
|
const stat5 = await fsp6.stat(full);
|
|
8432
8717
|
const summary = await this.summarize(id, stat5.mtime.toISOString());
|
|
8433
8718
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
8434
|
-
console.warn(
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8719
|
+
console.warn(JSON.stringify({
|
|
8720
|
+
level: "warn",
|
|
8721
|
+
event: "session_store.manifest_write_failed",
|
|
8722
|
+
sessionId: id,
|
|
8723
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8724
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8725
|
+
}));
|
|
8438
8726
|
});
|
|
8439
8727
|
return summary;
|
|
8440
8728
|
}
|
|
@@ -8442,17 +8730,48 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8442
8730
|
/**
|
|
8443
8731
|
* Delete a session and all associated files: JSONL, summary, plan/todos
|
|
8444
8732
|
* sidecars, and the session directory (fleet.json, shared/, subagents/).
|
|
8733
|
+
*
|
|
8734
|
+
* Individual file deletions are best-effort (logged as structured warnings),
|
|
8735
|
+
* but a tombstone is always written so readIndex() filters this session out.
|
|
8736
|
+
* If the session directory itself can't be removed, the error is surfaced
|
|
8737
|
+
* to the caller so prune() can report it.
|
|
8445
8738
|
*/
|
|
8446
8739
|
async deleteSession(id) {
|
|
8447
|
-
|
|
8448
|
-
|
|
8740
|
+
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
8741
|
+
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
8449
8742
|
const shardDir = path4.dirname(path4.join(this.dir, id));
|
|
8450
8743
|
const base = path4.basename(id);
|
|
8451
|
-
for (const ext of [".plan.json", ".todos.json"]) {
|
|
8452
|
-
await fsp6.unlink(path4.join(shardDir, `${base}${ext}`)).catch((err) => console.warn(`[session-store] delete ${ext} failed: ${err}`));
|
|
8453
|
-
}
|
|
8454
8744
|
const sessDir = path4.join(shardDir, base);
|
|
8455
|
-
|
|
8745
|
+
const deletions = [
|
|
8746
|
+
fsp6.unlink(jsonlPath),
|
|
8747
|
+
fsp6.unlink(summaryPath),
|
|
8748
|
+
fsp6.unlink(path4.join(shardDir, `${base}.plan.json`)),
|
|
8749
|
+
fsp6.unlink(path4.join(shardDir, `${base}.todos.json`))
|
|
8750
|
+
];
|
|
8751
|
+
const results = await Promise.allSettled(deletions);
|
|
8752
|
+
for (const r of results) {
|
|
8753
|
+
if (r.status === "rejected") {
|
|
8754
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
8755
|
+
if (r.reason?.code !== "ENOENT") {
|
|
8756
|
+
console.warn(JSON.stringify({
|
|
8757
|
+
level: "warn",
|
|
8758
|
+
event: "session_store.delete_failed",
|
|
8759
|
+
sessionId: id,
|
|
8760
|
+
message: msg,
|
|
8761
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8762
|
+
}));
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
await fsp6.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
8767
|
+
console.warn(JSON.stringify({
|
|
8768
|
+
level: "warn",
|
|
8769
|
+
event: "session_store.rmdir_failed",
|
|
8770
|
+
sessionId: id,
|
|
8771
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8772
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8773
|
+
}));
|
|
8774
|
+
});
|
|
8456
8775
|
await this.writeTombstone(id);
|
|
8457
8776
|
}
|
|
8458
8777
|
async delete(id) {
|
|
@@ -8468,24 +8787,33 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8468
8787
|
activeSessionId = active.sessionId ?? null;
|
|
8469
8788
|
} catch {
|
|
8470
8789
|
}
|
|
8790
|
+
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
8791
|
+
const pruneFile = async (dir, name, prefix) => {
|
|
8792
|
+
const jsonlPath = path4.join(dir, name);
|
|
8793
|
+
try {
|
|
8794
|
+
const stat5 = await fsp6.stat(jsonlPath);
|
|
8795
|
+
if (stat5.mtimeMs >= cutoff) return;
|
|
8796
|
+
} catch {
|
|
8797
|
+
return;
|
|
8798
|
+
}
|
|
8799
|
+
const base = name.replace(/\.jsonl$/, "");
|
|
8800
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
8801
|
+
if (activeSessionId && id === activeSessionId) return;
|
|
8802
|
+
await this.deleteSession(id);
|
|
8803
|
+
deleted++;
|
|
8804
|
+
};
|
|
8471
8805
|
const entries = await fsp6.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
8472
8806
|
for (const entry of entries) {
|
|
8807
|
+
if (entry.isFile()) {
|
|
8808
|
+
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
8809
|
+
continue;
|
|
8810
|
+
}
|
|
8473
8811
|
if (!entry.isDirectory()) continue;
|
|
8474
8812
|
const dateDir = path4.join(this.dir, entry.name);
|
|
8475
8813
|
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
8476
8814
|
for (const file of files) {
|
|
8477
|
-
if (!file.isFile() || !file.name
|
|
8478
|
-
|
|
8479
|
-
try {
|
|
8480
|
-
const stat5 = await fsp6.stat(jsonlPath);
|
|
8481
|
-
if (stat5.mtimeMs >= cutoff) continue;
|
|
8482
|
-
} catch {
|
|
8483
|
-
continue;
|
|
8484
|
-
}
|
|
8485
|
-
const id = `${entry.name}/${file.name.replace(/\.jsonl$/, "")}`;
|
|
8486
|
-
if (activeSessionId && id === activeSessionId) continue;
|
|
8487
|
-
await this.deleteSession(id);
|
|
8488
|
-
deleted++;
|
|
8815
|
+
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
8816
|
+
await pruneFile(dateDir, file.name, entry.name);
|
|
8489
8817
|
}
|
|
8490
8818
|
}
|
|
8491
8819
|
if (deleted > 0) {
|
|
@@ -8574,7 +8902,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8574
8902
|
}
|
|
8575
8903
|
metaFromEvents(id, events) {
|
|
8576
8904
|
const start = events.find((e) => e.type === "session_start");
|
|
8577
|
-
const end = events.
|
|
8905
|
+
const end = events.findLast((e) => e.type === "session_end");
|
|
8578
8906
|
return {
|
|
8579
8907
|
id,
|
|
8580
8908
|
startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -8591,9 +8919,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8591
8919
|
for (const e of events) {
|
|
8592
8920
|
if (e.type === "user_input") {
|
|
8593
8921
|
openToolUses.clear();
|
|
8594
|
-
messages.push({ role: "user", content: e.content });
|
|
8922
|
+
messages.push({ role: "user", content: e.content, ts: e.ts });
|
|
8595
8923
|
} else if (e.type === "llm_response") {
|
|
8596
|
-
messages.push({ role: "assistant", content: e.content });
|
|
8924
|
+
messages.push({ role: "assistant", content: e.content, ts: e.ts });
|
|
8597
8925
|
for (const b of e.content) {
|
|
8598
8926
|
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
8599
8927
|
}
|
|
@@ -8612,25 +8940,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8612
8940
|
continue;
|
|
8613
8941
|
}
|
|
8614
8942
|
openToolUses.delete(e.id);
|
|
8615
|
-
const
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
}
|
|
8622
|
-
];
|
|
8943
|
+
const resultBlock = {
|
|
8944
|
+
type: "tool_result",
|
|
8945
|
+
tool_use_id: e.id,
|
|
8946
|
+
content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
|
|
8947
|
+
is_error: e.isError
|
|
8948
|
+
};
|
|
8623
8949
|
const last = messages[messages.length - 1];
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
} else if (typeof last.content === "string") {
|
|
8628
|
-
last.content = [{ type: "text", text: last.content }, ...content];
|
|
8629
|
-
} else {
|
|
8630
|
-
messages.push({ role: "user", content });
|
|
8631
|
-
}
|
|
8950
|
+
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
8951
|
+
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
8952
|
+
last.content.push(resultBlock);
|
|
8632
8953
|
} else {
|
|
8633
|
-
messages.push({ role: "user", content });
|
|
8954
|
+
messages.push({ role: "user", content: [resultBlock], ts: e.ts });
|
|
8634
8955
|
}
|
|
8635
8956
|
}
|
|
8636
8957
|
}
|
|
@@ -8650,7 +8971,24 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8650
8971
|
return { messages: repaired.messages, usage };
|
|
8651
8972
|
}
|
|
8652
8973
|
};
|
|
8653
|
-
|
|
8974
|
+
function extractToolCallEnds(events) {
|
|
8975
|
+
const result = [];
|
|
8976
|
+
for (const e of events) {
|
|
8977
|
+
if (e.type === "tool_call_end") {
|
|
8978
|
+
result.push({
|
|
8979
|
+
name: e.name,
|
|
8980
|
+
id: e.id,
|
|
8981
|
+
durationMs: e.durationMs,
|
|
8982
|
+
ok: e.ok ?? false,
|
|
8983
|
+
outputBytes: e.outputBytes,
|
|
8984
|
+
outputTokens: e.outputTokens,
|
|
8985
|
+
outputLines: e.outputLines
|
|
8986
|
+
});
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
return result;
|
|
8990
|
+
}
|
|
8991
|
+
var FileSessionWriter = class _FileSessionWriter {
|
|
8654
8992
|
constructor(id, handle, startedAt, meta, events, opts = {}) {
|
|
8655
8993
|
this.id = id;
|
|
8656
8994
|
this.handle = handle;
|
|
@@ -8677,7 +9015,7 @@ var FileSessionWriter = class {
|
|
|
8677
9015
|
meta;
|
|
8678
9016
|
events;
|
|
8679
9017
|
closed = false;
|
|
8680
|
-
|
|
9018
|
+
closePromise = null;
|
|
8681
9019
|
manifestFile;
|
|
8682
9020
|
summary;
|
|
8683
9021
|
tokenIn = 0;
|
|
@@ -8686,12 +9024,51 @@ var FileSessionWriter = class {
|
|
|
8686
9024
|
get transcriptPath() {
|
|
8687
9025
|
return this.filePath || void 0;
|
|
8688
9026
|
}
|
|
8689
|
-
|
|
9027
|
+
/**
|
|
9028
|
+
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
9029
|
+
* A single promise (not a boolean) so a second append racing the first
|
|
9030
|
+
* can't push its event into the buffer BEFORE the first append's event —
|
|
9031
|
+
* every appender awaits the same init and resumes in FIFO call order.
|
|
9032
|
+
*/
|
|
9033
|
+
initPromise = null;
|
|
9034
|
+
ensureInit() {
|
|
9035
|
+
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
9036
|
+
return this.initPromise;
|
|
9037
|
+
}
|
|
8690
9038
|
resumed;
|
|
8691
9039
|
appendFailCount = 0;
|
|
8692
9040
|
lastAppendWarnAt = 0;
|
|
8693
9041
|
secretScrubber;
|
|
8694
9042
|
onCloseCb;
|
|
9043
|
+
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
9044
|
+
//
|
|
9045
|
+
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
9046
|
+
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
9047
|
+
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
9048
|
+
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
9049
|
+
// format — the JSONL is still one JSON object per line.
|
|
9050
|
+
writeBuffer = [];
|
|
9051
|
+
flushTimer = null;
|
|
9052
|
+
static FLUSH_INTERVAL_MS = 500;
|
|
9053
|
+
static FLUSH_SIZE = 50;
|
|
9054
|
+
// ── Write serialization ─────────────────────────────────────────────────
|
|
9055
|
+
//
|
|
9056
|
+
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
9057
|
+
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
9058
|
+
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
9059
|
+
// may complete them out of order (chronology breaks) or, for large
|
|
9060
|
+
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
9061
|
+
// exactly one write in flight; failures don't break the chain.
|
|
9062
|
+
writeChain = Promise.resolve();
|
|
9063
|
+
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
9064
|
+
enqueueWrite(data) {
|
|
9065
|
+
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
9066
|
+
this.writeChain = write.then(
|
|
9067
|
+
() => void 0,
|
|
9068
|
+
() => void 0
|
|
9069
|
+
);
|
|
9070
|
+
return write;
|
|
9071
|
+
}
|
|
8695
9072
|
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
8696
9073
|
iterationCount = 0;
|
|
8697
9074
|
toolCallCount = 0;
|
|
@@ -8741,31 +9118,91 @@ var FileSessionWriter = class {
|
|
|
8741
9118
|
})}
|
|
8742
9119
|
`;
|
|
8743
9120
|
try {
|
|
8744
|
-
|
|
8745
|
-
await fsp6.writeFile(this.filePath, record, { flag: "a", mode: 384 });
|
|
8746
|
-
}
|
|
9121
|
+
await this.enqueueWrite(record);
|
|
8747
9122
|
} catch {
|
|
8748
9123
|
}
|
|
8749
9124
|
}
|
|
8750
9125
|
async append(event) {
|
|
8751
9126
|
if (this.closed) return;
|
|
8752
|
-
|
|
8753
|
-
this.initDone = true;
|
|
8754
|
-
await this.writeSessionStartLazy();
|
|
8755
|
-
}
|
|
9127
|
+
await this.ensureInit();
|
|
8756
9128
|
const scrubbed = this.scrubEvent(event);
|
|
8757
9129
|
this.observeForSummary(scrubbed);
|
|
9130
|
+
this.writeBuffer.push(scrubbed);
|
|
9131
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9132
|
+
if (this.flushTimer) {
|
|
9133
|
+
clearTimeout(this.flushTimer);
|
|
9134
|
+
this.flushTimer = null;
|
|
9135
|
+
}
|
|
9136
|
+
await this.flushBuffer();
|
|
9137
|
+
} else {
|
|
9138
|
+
this.scheduleFlush();
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
async appendBatch(events) {
|
|
9142
|
+
if (this.closed || events.length === 0) return;
|
|
9143
|
+
await this.ensureInit();
|
|
9144
|
+
for (const event of events) {
|
|
9145
|
+
const scrubbed = this.scrubEvent(event);
|
|
9146
|
+
this.observeForSummary(scrubbed);
|
|
9147
|
+
this.writeBuffer.push(scrubbed);
|
|
9148
|
+
}
|
|
9149
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9150
|
+
if (this.flushTimer) {
|
|
9151
|
+
clearTimeout(this.flushTimer);
|
|
9152
|
+
this.flushTimer = null;
|
|
9153
|
+
}
|
|
9154
|
+
await this.flushBuffer();
|
|
9155
|
+
} else {
|
|
9156
|
+
this.scheduleFlush();
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
/**
|
|
9160
|
+
* Flush buffered events to disk immediately. Critical events
|
|
9161
|
+
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
9162
|
+
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
9163
|
+
*
|
|
9164
|
+
* Idempotent — cancels any pending timer and writes whatever has
|
|
9165
|
+
* accumulated in the buffer. Safe to call even when the buffer
|
|
9166
|
+
* is empty (no-op).
|
|
9167
|
+
*/
|
|
9168
|
+
async flush() {
|
|
9169
|
+
if (this.flushTimer) {
|
|
9170
|
+
clearTimeout(this.flushTimer);
|
|
9171
|
+
this.flushTimer = null;
|
|
9172
|
+
}
|
|
9173
|
+
await this.flushBuffer();
|
|
9174
|
+
}
|
|
9175
|
+
/** Schedule a deferred flush. No-op if a timer is already pending. */
|
|
9176
|
+
scheduleFlush() {
|
|
9177
|
+
if (this.flushTimer) return;
|
|
9178
|
+
this.flushTimer = setTimeout(() => {
|
|
9179
|
+
this.flushTimer = null;
|
|
9180
|
+
this.flushBuffer().catch(() => {
|
|
9181
|
+
});
|
|
9182
|
+
}, _FileSessionWriter.FLUSH_INTERVAL_MS);
|
|
9183
|
+
}
|
|
9184
|
+
/**
|
|
9185
|
+
* Flush all buffered events to disk as a single appendFile call.
|
|
9186
|
+
* Errors use the same throttled-warning pattern the old per-event
|
|
9187
|
+
* append path used — one warning every 5s with a suppressed count.
|
|
9188
|
+
* On failure the buffer is cleared (events are best-effort, same as
|
|
9189
|
+
* the old per-event path where a failed write was silently dropped).
|
|
9190
|
+
*/
|
|
9191
|
+
async flushBuffer() {
|
|
9192
|
+
if (this.writeBuffer.length === 0) return;
|
|
9193
|
+
const eventCount = this.writeBuffer.length;
|
|
9194
|
+
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
9195
|
+
this.writeBuffer = [];
|
|
8758
9196
|
try {
|
|
8759
|
-
await this.
|
|
8760
|
-
`, "utf8");
|
|
9197
|
+
await this.enqueueWrite(batch);
|
|
8761
9198
|
} catch (err) {
|
|
8762
|
-
this.appendFailCount
|
|
9199
|
+
this.appendFailCount += eventCount;
|
|
8763
9200
|
const now = Date.now();
|
|
8764
9201
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
8765
9202
|
const suppressed = this.appendFailCount - 1;
|
|
8766
9203
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
8767
9204
|
console.warn(
|
|
8768
|
-
"[session]
|
|
9205
|
+
"[session] flush failed:",
|
|
8769
9206
|
err instanceof Error ? err.message : String(err),
|
|
8770
9207
|
tail
|
|
8771
9208
|
);
|
|
@@ -8775,6 +9212,11 @@ var FileSessionWriter = class {
|
|
|
8775
9212
|
}
|
|
8776
9213
|
}
|
|
8777
9214
|
observeForSummary(event) {
|
|
9215
|
+
if (event.type === "llm_response") {
|
|
9216
|
+
for (const block of event.content) {
|
|
9217
|
+
if (block.type === "tool_use") this.openToolUses.add(block.id);
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
8778
9220
|
if (event.type === "tool_use") {
|
|
8779
9221
|
this.openToolUses.add(event.id);
|
|
8780
9222
|
} else if (event.type === "tool_call_start") {
|
|
@@ -8808,9 +9250,18 @@ var FileSessionWriter = class {
|
|
|
8808
9250
|
}
|
|
8809
9251
|
}
|
|
8810
9252
|
async close() {
|
|
8811
|
-
if (this.
|
|
8812
|
-
this.
|
|
9253
|
+
if (this.closePromise) return this.closePromise;
|
|
9254
|
+
this.closePromise = this.doClose();
|
|
9255
|
+
return this.closePromise;
|
|
9256
|
+
}
|
|
9257
|
+
async doClose() {
|
|
8813
9258
|
this.closed = true;
|
|
9259
|
+
if (this.flushTimer) {
|
|
9260
|
+
clearTimeout(this.flushTimer);
|
|
9261
|
+
this.flushTimer = null;
|
|
9262
|
+
}
|
|
9263
|
+
await this.flushBuffer();
|
|
9264
|
+
await this.writeChain;
|
|
8814
9265
|
this.summary = {
|
|
8815
9266
|
...this.summary,
|
|
8816
9267
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8866,6 +9317,12 @@ var FileSessionWriter = class {
|
|
|
8866
9317
|
}
|
|
8867
9318
|
async truncateToCheckpoint(targetPromptIndex) {
|
|
8868
9319
|
if (!this.filePath) return 0;
|
|
9320
|
+
if (this.flushTimer) {
|
|
9321
|
+
clearTimeout(this.flushTimer);
|
|
9322
|
+
this.flushTimer = null;
|
|
9323
|
+
}
|
|
9324
|
+
await this.flushBuffer();
|
|
9325
|
+
await this.writeChain;
|
|
8869
9326
|
const raw = await fsp6.readFile(this.filePath, "utf8");
|
|
8870
9327
|
const lines = raw.split("\n");
|
|
8871
9328
|
const kept = [];
|
|
@@ -8928,6 +9385,12 @@ var FileSessionWriter = class {
|
|
|
8928
9385
|
}
|
|
8929
9386
|
async clearSession() {
|
|
8930
9387
|
if (!this.filePath) return;
|
|
9388
|
+
if (this.flushTimer) {
|
|
9389
|
+
clearTimeout(this.flushTimer);
|
|
9390
|
+
this.flushTimer = null;
|
|
9391
|
+
}
|
|
9392
|
+
this.writeBuffer = [];
|
|
9393
|
+
await this.writeChain;
|
|
8931
9394
|
const record = `${JSON.stringify({
|
|
8932
9395
|
type: "session_start",
|
|
8933
9396
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -9426,6 +9889,1518 @@ var FleetManager = class {
|
|
|
9426
9889
|
}
|
|
9427
9890
|
};
|
|
9428
9891
|
|
|
9429
|
-
|
|
9892
|
+
// src/coordination/mailbox-types.ts
|
|
9893
|
+
function normalizeRecipient(to) {
|
|
9894
|
+
return to.trim().toLowerCase() === "all" ? "*" : to.trim();
|
|
9895
|
+
}
|
|
9896
|
+
|
|
9897
|
+
// src/coordination/mailbox.ts
|
|
9898
|
+
var MAILBOX_FILE = "_mailbox.jsonl";
|
|
9899
|
+
var LINE_SEPARATOR = "\n";
|
|
9900
|
+
var DefaultMailbox = class {
|
|
9901
|
+
filePath;
|
|
9902
|
+
constructor(sessionDir) {
|
|
9903
|
+
this.filePath = path4.join(sessionDir, MAILBOX_FILE);
|
|
9904
|
+
}
|
|
9905
|
+
get mailboxPath() {
|
|
9906
|
+
return this.filePath;
|
|
9907
|
+
}
|
|
9908
|
+
// ── Send ──────────────────────────────────────────────────────────────
|
|
9909
|
+
async send(input) {
|
|
9910
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9911
|
+
const msg = {
|
|
9912
|
+
id: randomUUID(),
|
|
9913
|
+
from: input.from,
|
|
9914
|
+
// "all" is an accepted spelling of the broadcast address — canonical
|
|
9915
|
+
// form on disk is '*' so every query/checker matches it.
|
|
9916
|
+
to: normalizeRecipient(input.to),
|
|
9917
|
+
type: input.type,
|
|
9918
|
+
subject: input.subject,
|
|
9919
|
+
body: input.body,
|
|
9920
|
+
priority: input.priority ?? "normal",
|
|
9921
|
+
readBy: {},
|
|
9922
|
+
completed: false,
|
|
9923
|
+
timestamp: now,
|
|
9924
|
+
replyTo: input.replyTo,
|
|
9925
|
+
taskContext: input.taskContext
|
|
9926
|
+
};
|
|
9927
|
+
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
9928
|
+
await fsp6.mkdir(path4.dirname(this.filePath), { recursive: true });
|
|
9929
|
+
await withFileLock(this.filePath, async () => {
|
|
9930
|
+
await fsp6.appendFile(this.filePath, line, "utf8");
|
|
9931
|
+
});
|
|
9932
|
+
return msg;
|
|
9933
|
+
}
|
|
9934
|
+
// ── Query ─────────────────────────────────────────────────────────────
|
|
9935
|
+
async query(q) {
|
|
9936
|
+
const all = await this._readAll();
|
|
9937
|
+
const limit = q.limit ?? 50;
|
|
9938
|
+
let filtered = all;
|
|
9939
|
+
if (q.to !== void 0) {
|
|
9940
|
+
filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
|
|
9941
|
+
}
|
|
9942
|
+
if (q.from !== void 0) {
|
|
9943
|
+
filtered = filtered.filter((m) => m.from === q.from);
|
|
9944
|
+
}
|
|
9945
|
+
if (q.unreadBy !== void 0) {
|
|
9946
|
+
filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
|
|
9947
|
+
}
|
|
9948
|
+
if (q.incompleteOnly) {
|
|
9949
|
+
filtered = filtered.filter((m) => !m.completed);
|
|
9950
|
+
}
|
|
9951
|
+
if (q.type !== void 0) {
|
|
9952
|
+
filtered = filtered.filter((m) => m.type === q.type);
|
|
9953
|
+
}
|
|
9954
|
+
if (q.minPriority !== void 0) {
|
|
9955
|
+
const order = { low: 0, normal: 1, high: 2 };
|
|
9956
|
+
const min = order[q.minPriority];
|
|
9957
|
+
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
9958
|
+
}
|
|
9959
|
+
if (q.since !== void 0) {
|
|
9960
|
+
const since = q.since;
|
|
9961
|
+
filtered = filtered.filter((m) => m.timestamp > since);
|
|
9962
|
+
}
|
|
9963
|
+
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
9964
|
+
return filtered.slice(0, limit);
|
|
9965
|
+
}
|
|
9966
|
+
// ── Ack ───────────────────────────────────────────────────────────────
|
|
9967
|
+
async ack(input) {
|
|
9968
|
+
let result = null;
|
|
9969
|
+
await withFileLock(this.filePath, async () => {
|
|
9970
|
+
const all = await this._readAll();
|
|
9971
|
+
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
9972
|
+
if (idx === -1) return;
|
|
9973
|
+
const msg = all[idx];
|
|
9974
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9975
|
+
if (input.read !== false) {
|
|
9976
|
+
msg.readBy[input.readerId] = now;
|
|
9977
|
+
}
|
|
9978
|
+
if (input.completed) {
|
|
9979
|
+
msg.completed = true;
|
|
9980
|
+
msg.completedBy = input.readerId;
|
|
9981
|
+
msg.completedAt = now;
|
|
9982
|
+
}
|
|
9983
|
+
if (input.outcome !== void 0) {
|
|
9984
|
+
msg.outcome = input.outcome;
|
|
9985
|
+
}
|
|
9986
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
9987
|
+
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
9988
|
+
result = msg;
|
|
9989
|
+
});
|
|
9990
|
+
return result;
|
|
9991
|
+
}
|
|
9992
|
+
// ── Agent statuses ────────────────────────────────────────────────────
|
|
9993
|
+
async getAgentStatuses() {
|
|
9994
|
+
const all = await this._readAll();
|
|
9995
|
+
const latest = /* @__PURE__ */ new Map();
|
|
9996
|
+
for (const m of all) {
|
|
9997
|
+
if (m.type !== "status") continue;
|
|
9998
|
+
const existing = latest.get(m.from);
|
|
9999
|
+
if (existing && m.timestamp <= existing.lastActivityAt) continue;
|
|
10000
|
+
latest.set(m.from, {
|
|
10001
|
+
agentId: m.from,
|
|
10002
|
+
name: m.taskContext?.agentName ?? m.from,
|
|
10003
|
+
role: m.taskContext?.agentRole,
|
|
10004
|
+
sessionId: m.senderSessionId ?? "?",
|
|
10005
|
+
status: m.taskContext?.status ?? "idle",
|
|
10006
|
+
currentTool: void 0,
|
|
10007
|
+
currentTask: m.subject,
|
|
10008
|
+
iterations: 0,
|
|
10009
|
+
toolCalls: 0,
|
|
10010
|
+
lastActivityAt: m.timestamp,
|
|
10011
|
+
lastSeenAt: m.timestamp,
|
|
10012
|
+
online: true,
|
|
10013
|
+
pid: 0,
|
|
10014
|
+
source: void 0
|
|
10015
|
+
});
|
|
10016
|
+
}
|
|
10017
|
+
return Array.from(latest.values()).sort(
|
|
10018
|
+
(a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt)
|
|
10019
|
+
);
|
|
10020
|
+
}
|
|
10021
|
+
// ── Stubs for cross-session features (not applicable per-session) ─────
|
|
10022
|
+
async getOnlineAgents() {
|
|
10023
|
+
return this.getAgentStatuses();
|
|
10024
|
+
}
|
|
10025
|
+
async registerAgent(_input) {
|
|
10026
|
+
}
|
|
10027
|
+
async heartbeat(_input) {
|
|
10028
|
+
}
|
|
10029
|
+
async unreadCount(forAgentId) {
|
|
10030
|
+
const all = await this._readAll();
|
|
10031
|
+
return all.filter(
|
|
10032
|
+
(m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
|
|
10033
|
+
).length;
|
|
10034
|
+
}
|
|
10035
|
+
async close() {
|
|
10036
|
+
}
|
|
10037
|
+
async clearAll() {
|
|
10038
|
+
await withFileLock(this.filePath, async () => {
|
|
10039
|
+
await fsp6.writeFile(this.filePath, "", "utf8");
|
|
10040
|
+
});
|
|
10041
|
+
}
|
|
10042
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
10043
|
+
async _readAll() {
|
|
10044
|
+
try {
|
|
10045
|
+
const raw = await fsp6.readFile(this.filePath, "utf8");
|
|
10046
|
+
const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
10047
|
+
const messages = [];
|
|
10048
|
+
for (const line of lines) {
|
|
10049
|
+
try {
|
|
10050
|
+
const parsed = JSON.parse(line);
|
|
10051
|
+
if (!parsed["readBy"]) {
|
|
10052
|
+
const readBy = {};
|
|
10053
|
+
if (parsed["read"] && parsed["readAt"]) {
|
|
10054
|
+
readBy[parsed["to"] ?? "unknown"] = parsed["readAt"];
|
|
10055
|
+
}
|
|
10056
|
+
parsed["readBy"] = readBy;
|
|
10057
|
+
delete parsed["read"];
|
|
10058
|
+
delete parsed["readAt"];
|
|
10059
|
+
}
|
|
10060
|
+
messages.push(parsed);
|
|
10061
|
+
} catch {
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
return messages;
|
|
10065
|
+
} catch (err) {
|
|
10066
|
+
if (err.code === "ENOENT") return [];
|
|
10067
|
+
throw err;
|
|
10068
|
+
}
|
|
10069
|
+
}
|
|
10070
|
+
};
|
|
10071
|
+
var BrainMonitor = class {
|
|
10072
|
+
constructor(opts) {
|
|
10073
|
+
this.opts = opts;
|
|
10074
|
+
this.toolFailureStreak = opts.toolFailureStreak ?? 3;
|
|
10075
|
+
this.errorStormCount = opts.errorStormCount ?? 4;
|
|
10076
|
+
this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
|
|
10077
|
+
this.cooldownMs = opts.cooldownMs ?? 12e4;
|
|
10078
|
+
}
|
|
10079
|
+
opts;
|
|
10080
|
+
failStreaks = /* @__PURE__ */ new Map();
|
|
10081
|
+
errorTimestamps = [];
|
|
10082
|
+
lastEngagedAt = /* @__PURE__ */ new Map();
|
|
10083
|
+
unsubscribers = [];
|
|
10084
|
+
engaging = false;
|
|
10085
|
+
toolFailureStreak;
|
|
10086
|
+
errorStormCount;
|
|
10087
|
+
errorStormWindowMs;
|
|
10088
|
+
cooldownMs;
|
|
10089
|
+
start() {
|
|
10090
|
+
this.unsubscribers.push(
|
|
10091
|
+
this.opts.events.on("tool.executed", (e) => {
|
|
10092
|
+
if (e.ok) {
|
|
10093
|
+
this.failStreaks.delete(e.name);
|
|
10094
|
+
return;
|
|
10095
|
+
}
|
|
10096
|
+
const streak = (this.failStreaks.get(e.name) ?? 0) + 1;
|
|
10097
|
+
this.failStreaks.set(e.name, streak);
|
|
10098
|
+
if (streak >= this.toolFailureStreak) {
|
|
10099
|
+
this.failStreaks.delete(e.name);
|
|
10100
|
+
void this.engage("tool_failure_streak", {
|
|
10101
|
+
question: `The tool "${e.name}" has failed ${streak} times in a row. Should the agent be steered to a different approach?`,
|
|
10102
|
+
context: [
|
|
10103
|
+
`Tool: ${e.name}`,
|
|
10104
|
+
`Consecutive failures: ${streak}`,
|
|
10105
|
+
e.output ? `Last output (truncated): ${String(e.output).slice(0, 400)}` : ""
|
|
10106
|
+
].filter(Boolean).join("\n")
|
|
10107
|
+
});
|
|
10108
|
+
}
|
|
10109
|
+
})
|
|
10110
|
+
);
|
|
10111
|
+
this.unsubscribers.push(
|
|
10112
|
+
this.opts.events.on("error", (e) => {
|
|
10113
|
+
const now = Date.now();
|
|
10114
|
+
this.errorTimestamps.push(now);
|
|
10115
|
+
this.errorTimestamps = this.errorTimestamps.filter(
|
|
10116
|
+
(t) => now - t <= this.errorStormWindowMs
|
|
10117
|
+
);
|
|
10118
|
+
if (this.errorTimestamps.length >= this.errorStormCount) {
|
|
10119
|
+
const count = this.errorTimestamps.length;
|
|
10120
|
+
this.errorTimestamps = [];
|
|
10121
|
+
const message = e.err instanceof Error ? e.err.message : String(e.err);
|
|
10122
|
+
void this.engage("error_storm", {
|
|
10123
|
+
question: `${count} errors occurred within ${Math.round(this.errorStormWindowMs / 1e3)}s (phase: ${e.phase}). Should the agent be steered before more work is wasted?`,
|
|
10124
|
+
context: `Latest error: ${message.slice(0, 400)}`
|
|
10125
|
+
});
|
|
10126
|
+
}
|
|
10127
|
+
})
|
|
10128
|
+
);
|
|
10129
|
+
}
|
|
10130
|
+
stop() {
|
|
10131
|
+
for (const off of this.unsubscribers) off();
|
|
10132
|
+
this.unsubscribers.length = 0;
|
|
10133
|
+
this.failStreaks.clear();
|
|
10134
|
+
this.errorTimestamps = [];
|
|
10135
|
+
}
|
|
10136
|
+
async engage(kind, input) {
|
|
10137
|
+
const last = this.lastEngagedAt.get(kind) ?? 0;
|
|
10138
|
+
if (this.engaging || Date.now() - last < this.cooldownMs) return;
|
|
10139
|
+
this.engaging = true;
|
|
10140
|
+
this.lastEngagedAt.set(kind, Date.now());
|
|
10141
|
+
try {
|
|
10142
|
+
const request = {
|
|
10143
|
+
id: `brainmon-${randomUUID()}`,
|
|
10144
|
+
source: "system",
|
|
10145
|
+
question: input.question,
|
|
10146
|
+
context: input.context,
|
|
10147
|
+
options: [
|
|
10148
|
+
{
|
|
10149
|
+
id: "steer",
|
|
10150
|
+
label: "Steer the agent with corrective guidance",
|
|
10151
|
+
consequence: "A steer message is injected before its next step.",
|
|
10152
|
+
risk: "low"
|
|
10153
|
+
},
|
|
10154
|
+
{
|
|
10155
|
+
id: "continue",
|
|
10156
|
+
label: "Let the agent continue unaided",
|
|
10157
|
+
risk: "low"
|
|
10158
|
+
}
|
|
10159
|
+
],
|
|
10160
|
+
risk: "medium",
|
|
10161
|
+
// Without an LLM layer the policy brain resolves this fallback to
|
|
10162
|
+
// "continue" — the monitor observes but never interferes.
|
|
10163
|
+
fallback: "continue"
|
|
10164
|
+
};
|
|
10165
|
+
const decision = await this.opts.brain.decide(request);
|
|
10166
|
+
const intervened = await this.maybeIntervene(kind, request, decision);
|
|
10167
|
+
this.opts.events.emit("brain.intervention", {
|
|
10168
|
+
kind,
|
|
10169
|
+
request,
|
|
10170
|
+
decision,
|
|
10171
|
+
intervened,
|
|
10172
|
+
at: Date.now()
|
|
10173
|
+
});
|
|
10174
|
+
} catch {
|
|
10175
|
+
} finally {
|
|
10176
|
+
this.engaging = false;
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
10179
|
+
async maybeIntervene(kind, request, decision) {
|
|
10180
|
+
if (decision.type !== "answer") return false;
|
|
10181
|
+
const choseSteer = decision.optionId === "steer";
|
|
10182
|
+
const freeTextGuidance = !decision.optionId && !/^continue\b/i.test(decision.text.trim()) && decision.text.trim().length > 0;
|
|
10183
|
+
if (!choseSteer && !freeTextGuidance) return false;
|
|
10184
|
+
const guidance = decision.rationale?.trim() || decision.text.trim();
|
|
10185
|
+
try {
|
|
10186
|
+
await this.opts.intervene({
|
|
10187
|
+
subject: `Brain intervention: ${kind.replace(/_/g, " ")}`,
|
|
10188
|
+
body: [
|
|
10189
|
+
`The Brain engaged after detecting: ${request.question}`,
|
|
10190
|
+
"",
|
|
10191
|
+
`Guidance: ${guidance}`,
|
|
10192
|
+
"",
|
|
10193
|
+
"Adjust your approach accordingly \u2014 do not simply retry the same action."
|
|
10194
|
+
].join("\n")
|
|
10195
|
+
});
|
|
10196
|
+
return true;
|
|
10197
|
+
} catch {
|
|
10198
|
+
return false;
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
};
|
|
10202
|
+
function projectSlug(absRoot) {
|
|
10203
|
+
const base = slugify(path4.basename(absRoot));
|
|
10204
|
+
const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
10205
|
+
return `${base}-${hash}`;
|
|
10206
|
+
}
|
|
10207
|
+
function slugify(name) {
|
|
10208
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
10209
|
+
}
|
|
10210
|
+
function wstackGlobalRoot() {
|
|
10211
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
10212
|
+
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
10213
|
+
return path4.join(os.homedir(), ".wrongstack");
|
|
10214
|
+
}
|
|
10215
|
+
|
|
10216
|
+
// src/coordination/global-mailbox.ts
|
|
10217
|
+
var MAILBOX_FILE2 = "_mailbox.jsonl";
|
|
10218
|
+
var AGENT_STALE_MS = 6e4;
|
|
10219
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
10220
|
+
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
10221
|
+
var LINE_SEPARATOR2 = "\n";
|
|
10222
|
+
function resolveProjectDir(projectRoot, globalRoot) {
|
|
10223
|
+
return path4.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
10224
|
+
}
|
|
10225
|
+
var GlobalMailbox = class {
|
|
10226
|
+
/** Path to the JSONL message file. */
|
|
10227
|
+
messagePath;
|
|
10228
|
+
/** Path to the JSON agent registry file. */
|
|
10229
|
+
registryPath;
|
|
10230
|
+
/** Optional event bus for emitting agent registration/heartbeat events. */
|
|
10231
|
+
_events;
|
|
10232
|
+
/**
|
|
10233
|
+
* Local cache of the agent registry to avoid re-reading on every call.
|
|
10234
|
+
* Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
|
|
10235
|
+
* whole point of GlobalMailbox), so a cache served forever would never see
|
|
10236
|
+
* agents registered by other sessions. Writers always bypass it.
|
|
10237
|
+
*/
|
|
10238
|
+
_registryCache = null;
|
|
10239
|
+
/** When the registry cache was last refreshed from disk (epoch ms). */
|
|
10240
|
+
_registryCacheAt = 0;
|
|
10241
|
+
/** Last time each local agent sent a heartbeat (throttle). */
|
|
10242
|
+
_lastHeartbeat = /* @__PURE__ */ new Map();
|
|
10243
|
+
/**
|
|
10244
|
+
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
10245
|
+
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
10246
|
+
*/
|
|
10247
|
+
constructor(projectDir, events) {
|
|
10248
|
+
this.messagePath = path4.join(projectDir, MAILBOX_FILE2);
|
|
10249
|
+
this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
|
|
10250
|
+
this._events = events;
|
|
10251
|
+
}
|
|
10252
|
+
// ── Messages ────────────────────────────────────────────────────────────
|
|
10253
|
+
async send(input) {
|
|
10254
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10255
|
+
const msg = {
|
|
10256
|
+
id: randomUUID(),
|
|
10257
|
+
from: input.from,
|
|
10258
|
+
// "all" is an accepted spelling of the broadcast address — canonical
|
|
10259
|
+
// form on disk is '*' so every query/checker matches it.
|
|
10260
|
+
to: normalizeRecipient(input.to),
|
|
10261
|
+
type: input.type,
|
|
10262
|
+
subject: input.subject,
|
|
10263
|
+
body: input.body,
|
|
10264
|
+
priority: input.priority ?? "normal",
|
|
10265
|
+
readBy: {},
|
|
10266
|
+
completed: false,
|
|
10267
|
+
timestamp: now,
|
|
10268
|
+
replyTo: input.replyTo,
|
|
10269
|
+
taskContext: input.taskContext
|
|
10270
|
+
};
|
|
10271
|
+
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
10272
|
+
await fsp6.mkdir(path4.dirname(this.messagePath), { recursive: true });
|
|
10273
|
+
await withFileLock(this.messagePath, async () => {
|
|
10274
|
+
await fsp6.appendFile(this.messagePath, line, "utf8");
|
|
10275
|
+
});
|
|
10276
|
+
return msg;
|
|
10277
|
+
}
|
|
10278
|
+
async query(q) {
|
|
10279
|
+
const all = await this._readMessages();
|
|
10280
|
+
const limit = q.limit ?? 50;
|
|
10281
|
+
let filtered = all;
|
|
10282
|
+
if (q.to !== void 0) {
|
|
10283
|
+
filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
|
|
10284
|
+
}
|
|
10285
|
+
if (q.from !== void 0) {
|
|
10286
|
+
filtered = filtered.filter((m) => m.from === q.from);
|
|
10287
|
+
}
|
|
10288
|
+
if (q.unreadBy !== void 0) {
|
|
10289
|
+
filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
|
|
10290
|
+
}
|
|
10291
|
+
if (q.incompleteOnly) {
|
|
10292
|
+
filtered = filtered.filter((m) => !m.completed);
|
|
10293
|
+
}
|
|
10294
|
+
if (q.type !== void 0) {
|
|
10295
|
+
filtered = filtered.filter((m) => m.type === q.type);
|
|
10296
|
+
}
|
|
10297
|
+
if (q.minPriority !== void 0) {
|
|
10298
|
+
const order = { low: 0, normal: 1, high: 2 };
|
|
10299
|
+
const min = order[q.minPriority];
|
|
10300
|
+
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
10301
|
+
}
|
|
10302
|
+
if (q.since !== void 0) {
|
|
10303
|
+
const since = q.since;
|
|
10304
|
+
filtered = filtered.filter((m) => m.timestamp > since);
|
|
10305
|
+
}
|
|
10306
|
+
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
10307
|
+
return filtered.slice(0, limit);
|
|
10308
|
+
}
|
|
10309
|
+
async ack(input) {
|
|
10310
|
+
let result = null;
|
|
10311
|
+
await withFileLock(this.messagePath, async () => {
|
|
10312
|
+
const all = await this._readMessages();
|
|
10313
|
+
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10314
|
+
if (idx === -1) return;
|
|
10315
|
+
const msg = all[idx];
|
|
10316
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10317
|
+
if (input.read !== false) {
|
|
10318
|
+
msg.readBy[input.readerId] = now;
|
|
10319
|
+
}
|
|
10320
|
+
if (input.completed) {
|
|
10321
|
+
msg.completed = true;
|
|
10322
|
+
msg.completedBy = input.readerId;
|
|
10323
|
+
msg.completedAt = now;
|
|
10324
|
+
}
|
|
10325
|
+
if (input.outcome !== void 0) {
|
|
10326
|
+
msg.outcome = input.outcome;
|
|
10327
|
+
}
|
|
10328
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
10329
|
+
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10330
|
+
result = msg;
|
|
10331
|
+
});
|
|
10332
|
+
return result;
|
|
10333
|
+
}
|
|
10334
|
+
async unreadCount(forAgentId) {
|
|
10335
|
+
const all = await this._readMessages();
|
|
10336
|
+
return all.filter(
|
|
10337
|
+
(m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
|
|
10338
|
+
).length;
|
|
10339
|
+
}
|
|
10340
|
+
// ── Agent registry ──────────────────────────────────────────────────────
|
|
10341
|
+
async registerAgent(input) {
|
|
10342
|
+
await this._ensureRegistry();
|
|
10343
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10344
|
+
const agent = {
|
|
10345
|
+
agentId: input.agentId,
|
|
10346
|
+
sessionId: input.sessionId,
|
|
10347
|
+
name: input.name,
|
|
10348
|
+
role: input.role,
|
|
10349
|
+
status: "idle",
|
|
10350
|
+
currentTool: void 0,
|
|
10351
|
+
currentTask: void 0,
|
|
10352
|
+
iterations: 0,
|
|
10353
|
+
toolCalls: 0,
|
|
10354
|
+
registeredAt: now,
|
|
10355
|
+
lastSeenAt: now,
|
|
10356
|
+
pid: input.pid,
|
|
10357
|
+
source: input.source
|
|
10358
|
+
};
|
|
10359
|
+
await withFileLock(this.registryPath, async () => {
|
|
10360
|
+
const registry = await this._readRegistry({ fresh: true });
|
|
10361
|
+
this._pruneStaleInPlace(registry);
|
|
10362
|
+
registry.set(input.agentId, agent);
|
|
10363
|
+
this._registryCache = registry;
|
|
10364
|
+
this._registryCacheAt = Date.now();
|
|
10365
|
+
await this._writeRegistry(registry);
|
|
10366
|
+
});
|
|
10367
|
+
this._events?.emitCustom("mailbox.agent_registered", {
|
|
10368
|
+
agentId: input.agentId,
|
|
10369
|
+
sessionId: input.sessionId,
|
|
10370
|
+
name: input.name,
|
|
10371
|
+
role: input.role,
|
|
10372
|
+
source: input.source
|
|
10373
|
+
});
|
|
10374
|
+
}
|
|
10375
|
+
async heartbeat(input) {
|
|
10376
|
+
const last = this._lastHeartbeat.get(input.agentId) ?? 0;
|
|
10377
|
+
const now = Date.now();
|
|
10378
|
+
if (now - last < HEARTBEAT_THROTTLE_MS) return;
|
|
10379
|
+
this._lastHeartbeat.set(input.agentId, now);
|
|
10380
|
+
await this._ensureRegistry();
|
|
10381
|
+
await withFileLock(this.registryPath, async () => {
|
|
10382
|
+
const registry = await this._readRegistry({ fresh: true });
|
|
10383
|
+
this._pruneStaleInPlace(registry);
|
|
10384
|
+
const agent = registry.get(input.agentId);
|
|
10385
|
+
if (agent) {
|
|
10386
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
10387
|
+
agent.lastSeenAt = iso;
|
|
10388
|
+
if (input.status !== void 0) agent.status = input.status;
|
|
10389
|
+
if (input.currentTool !== void 0) agent.currentTool = input.currentTool;
|
|
10390
|
+
if (input.currentTask !== void 0) agent.currentTask = input.currentTask;
|
|
10391
|
+
if (input.iterations !== void 0) agent.iterations = input.iterations;
|
|
10392
|
+
if (input.toolCalls !== void 0) agent.toolCalls = input.toolCalls;
|
|
10393
|
+
}
|
|
10394
|
+
this._registryCache = registry;
|
|
10395
|
+
this._registryCacheAt = Date.now();
|
|
10396
|
+
await this._writeRegistry(registry);
|
|
10397
|
+
});
|
|
10398
|
+
this._events?.emitCustom("mailbox.agent_heartbeat", {
|
|
10399
|
+
agentId: input.agentId,
|
|
10400
|
+
status: input.status,
|
|
10401
|
+
currentTool: input.currentTool,
|
|
10402
|
+
currentTask: input.currentTask
|
|
10403
|
+
});
|
|
10404
|
+
}
|
|
10405
|
+
async getAgentStatuses() {
|
|
10406
|
+
await this._ensureRegistry();
|
|
10407
|
+
const registry = await this._readRegistry();
|
|
10408
|
+
this._pruneStaleInPlace(registry);
|
|
10409
|
+
const now = Date.now();
|
|
10410
|
+
return Array.from(registry.values()).map((a) => ({
|
|
10411
|
+
agentId: a.agentId,
|
|
10412
|
+
name: a.name,
|
|
10413
|
+
role: a.role,
|
|
10414
|
+
sessionId: a.sessionId,
|
|
10415
|
+
status: a.status,
|
|
10416
|
+
currentTool: a.currentTool,
|
|
10417
|
+
currentTask: a.currentTask,
|
|
10418
|
+
iterations: a.iterations,
|
|
10419
|
+
toolCalls: a.toolCalls,
|
|
10420
|
+
lastActivityAt: a.lastSeenAt,
|
|
10421
|
+
lastSeenAt: a.lastSeenAt,
|
|
10422
|
+
online: now - new Date(a.lastSeenAt).getTime() < AGENT_STALE_MS,
|
|
10423
|
+
pid: a.pid,
|
|
10424
|
+
source: a.source
|
|
10425
|
+
})).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
10426
|
+
}
|
|
10427
|
+
async getOnlineAgents() {
|
|
10428
|
+
const all = await this.getAgentStatuses();
|
|
10429
|
+
return all.filter((a) => a.online);
|
|
10430
|
+
}
|
|
10431
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
10432
|
+
async close() {
|
|
10433
|
+
this._registryCache = null;
|
|
10434
|
+
}
|
|
10435
|
+
async clearAll() {
|
|
10436
|
+
await withFileLock(this.messagePath, async () => {
|
|
10437
|
+
await fsp6.writeFile(this.messagePath, "", "utf8");
|
|
10438
|
+
});
|
|
10439
|
+
}
|
|
10440
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
10441
|
+
async _readMessages() {
|
|
10442
|
+
try {
|
|
10443
|
+
const raw = await fsp6.readFile(this.messagePath, "utf8");
|
|
10444
|
+
const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
|
|
10445
|
+
const messages = [];
|
|
10446
|
+
for (const line of lines) {
|
|
10447
|
+
try {
|
|
10448
|
+
const parsed = JSON.parse(line);
|
|
10449
|
+
if (!parsed["readBy"]) {
|
|
10450
|
+
const readBy = {};
|
|
10451
|
+
if (parsed["read"] && parsed["readAt"]) {
|
|
10452
|
+
readBy[parsed["to"]] = parsed["readAt"];
|
|
10453
|
+
}
|
|
10454
|
+
parsed["readBy"] = readBy;
|
|
10455
|
+
delete parsed["read"];
|
|
10456
|
+
delete parsed["readAt"];
|
|
10457
|
+
}
|
|
10458
|
+
messages.push(parsed);
|
|
10459
|
+
} catch {
|
|
10460
|
+
}
|
|
10461
|
+
}
|
|
10462
|
+
return messages;
|
|
10463
|
+
} catch (err) {
|
|
10464
|
+
if (err.code === "ENOENT") return [];
|
|
10465
|
+
throw err;
|
|
10466
|
+
}
|
|
10467
|
+
}
|
|
10468
|
+
async _ensureRegistry() {
|
|
10469
|
+
await fsp6.mkdir(path4.dirname(this.registryPath), { recursive: true });
|
|
10470
|
+
}
|
|
10471
|
+
async _readRegistry(opts) {
|
|
10472
|
+
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
10473
|
+
return new Map(this._registryCache);
|
|
10474
|
+
}
|
|
10475
|
+
try {
|
|
10476
|
+
const raw = await fsp6.readFile(this.registryPath, "utf8");
|
|
10477
|
+
const data = JSON.parse(raw);
|
|
10478
|
+
const map = /* @__PURE__ */ new Map();
|
|
10479
|
+
for (const [id, agent] of Object.entries(data)) {
|
|
10480
|
+
map.set(id, agent);
|
|
10481
|
+
}
|
|
10482
|
+
this._registryCache = map;
|
|
10483
|
+
this._registryCacheAt = Date.now();
|
|
10484
|
+
return new Map(map);
|
|
10485
|
+
} catch (err) {
|
|
10486
|
+
if (err.code === "ENOENT") {
|
|
10487
|
+
const empty = /* @__PURE__ */ new Map();
|
|
10488
|
+
this._registryCache = empty;
|
|
10489
|
+
this._registryCacheAt = Date.now();
|
|
10490
|
+
return empty;
|
|
10491
|
+
}
|
|
10492
|
+
throw err;
|
|
10493
|
+
}
|
|
10494
|
+
}
|
|
10495
|
+
_pruneStaleInPlace(registry) {
|
|
10496
|
+
const cutoff = Date.now() - AGENT_STALE_MS;
|
|
10497
|
+
for (const agent of registry.values()) {
|
|
10498
|
+
if (new Date(agent.lastSeenAt).getTime() < cutoff) {
|
|
10499
|
+
agent.status = "idle";
|
|
10500
|
+
}
|
|
10501
|
+
}
|
|
10502
|
+
}
|
|
10503
|
+
async _writeRegistry(registry) {
|
|
10504
|
+
const obj = {};
|
|
10505
|
+
for (const [id, agent] of registry) {
|
|
10506
|
+
obj[id] = agent;
|
|
10507
|
+
}
|
|
10508
|
+
const tmp = `${this.registryPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
10509
|
+
await fsp6.writeFile(tmp, JSON.stringify(obj, null, 2), "utf8");
|
|
10510
|
+
await fsp6.rename(tmp, this.registryPath);
|
|
10511
|
+
}
|
|
10512
|
+
};
|
|
10513
|
+
function defaultResolveProjectDir(ctx) {
|
|
10514
|
+
return resolveProjectDir(ctx.projectRoot, wstackGlobalRoot());
|
|
10515
|
+
}
|
|
10516
|
+
function mailboxSessionTag(sessionId) {
|
|
10517
|
+
return createHash("sha256").update(sessionId).digest("hex").slice(0, 8);
|
|
10518
|
+
}
|
|
10519
|
+
function resolveMailboxIdentity(ctx, fallbackBase = "leader") {
|
|
10520
|
+
const fieldId = ctx.agentId && ctx.agentId !== "unknown" ? ctx.agentId : void 0;
|
|
10521
|
+
const baseId = ctx.meta["agentId"] ?? fieldId ?? fallbackBase;
|
|
10522
|
+
const sessionId = ctx.meta["sessionId"] ?? ctx.session?.id ?? "default";
|
|
10523
|
+
const callerId = ctx.meta["globalAgentId"] ?? `${baseId}@${mailboxSessionTag(sessionId)}`;
|
|
10524
|
+
const fieldName = ctx.agentName && ctx.agentName !== "Unknown Agent" ? ctx.agentName : void 0;
|
|
10525
|
+
const name = ctx.meta["agentName"] ?? fieldName ?? baseId;
|
|
10526
|
+
const role = ctx.meta["agentRole"];
|
|
10527
|
+
return { baseId, callerId, name, role, sessionId };
|
|
10528
|
+
}
|
|
10529
|
+
function makeMailboxTool(opts = {}) {
|
|
10530
|
+
const resolveMailbox = opts.resolveMailbox ?? ((ctx) => {
|
|
10531
|
+
const dir = opts.projectDir ?? defaultResolveProjectDir(ctx);
|
|
10532
|
+
return new GlobalMailbox(dir, opts.events);
|
|
10533
|
+
});
|
|
10534
|
+
const agentId = opts.agentId ?? "leader";
|
|
10535
|
+
const sessionId = opts.sessionId ?? "default";
|
|
10536
|
+
const shortHint = "Sub-commands: check (unread), send (to/broadcast), ack (read/complete), query (filter), status (all agents), online (active only), unread (count).";
|
|
10537
|
+
return {
|
|
10538
|
+
name: "mailbox",
|
|
10539
|
+
description: "Inter-agent mailbox with cross-session support. Send messages, check for incoming messages, acknowledge with read receipts, query by criteria, see online agents.",
|
|
10540
|
+
usageHint: shortHint,
|
|
10541
|
+
category: "coordination",
|
|
10542
|
+
permission: "auto",
|
|
10543
|
+
mutating: true,
|
|
10544
|
+
inputSchema: {
|
|
10545
|
+
type: "object",
|
|
10546
|
+
properties: {
|
|
10547
|
+
action: {
|
|
10548
|
+
type: "string",
|
|
10549
|
+
enum: ["check", "send", "ack", "query", "status", "online", "unread"],
|
|
10550
|
+
description: "Which mailbox operation to perform."
|
|
10551
|
+
},
|
|
10552
|
+
to: { type: "string", description: "Recipient agent id, or '*' / 'all' for broadcast." },
|
|
10553
|
+
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result"], description: "Message type." },
|
|
10554
|
+
subject: { type: "string", description: "Short subject line." },
|
|
10555
|
+
body: { type: "string", description: "Full message content." },
|
|
10556
|
+
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
10557
|
+
replyTo: { type: "string", description: "Reply to a specific message id." },
|
|
10558
|
+
messageId: { type: "string", description: "Message id to acknowledge. Required for 'ack'." },
|
|
10559
|
+
read: { type: "boolean", description: "Mark as read (adds read receipt)." },
|
|
10560
|
+
completed: { type: "boolean", description: "Mark as completed." },
|
|
10561
|
+
outcome: { type: "string", description: "Outcome summary when marking complete." },
|
|
10562
|
+
unreadBy: { type: "string", description: "Filter messages unread by this agent. Used by 'check'." },
|
|
10563
|
+
incompleteOnly: { type: "boolean", description: "Only incomplete messages." },
|
|
10564
|
+
from: { type: "string", description: "Filter by sender." },
|
|
10565
|
+
minPriority: { type: "string", enum: ["low", "normal", "high"] },
|
|
10566
|
+
since: { type: "string", description: "ISO8601 timestamp \u2014 only messages after this." },
|
|
10567
|
+
limit: { type: "number", description: "Max messages to return." }
|
|
10568
|
+
},
|
|
10569
|
+
required: ["action"]
|
|
10570
|
+
},
|
|
10571
|
+
async execute(input, ctx) {
|
|
10572
|
+
const mb = resolveMailbox(ctx);
|
|
10573
|
+
const i = input ?? {};
|
|
10574
|
+
const action = i.action;
|
|
10575
|
+
const identity = resolveMailboxIdentity(ctx, agentId);
|
|
10576
|
+
const baseCallerId = identity.baseId;
|
|
10577
|
+
const callerId = identity.callerId;
|
|
10578
|
+
const callerSessionId = ctx.meta["sessionId"] ?? (ctx.session?.id ?? sessionId);
|
|
10579
|
+
try {
|
|
10580
|
+
await mb.registerAgent({
|
|
10581
|
+
agentId: callerId,
|
|
10582
|
+
sessionId: callerSessionId,
|
|
10583
|
+
name: identity.name,
|
|
10584
|
+
role: identity.role,
|
|
10585
|
+
pid: process.pid,
|
|
10586
|
+
source: ctx.meta["source"] ?? "cli"
|
|
10587
|
+
});
|
|
10588
|
+
} catch {
|
|
10589
|
+
}
|
|
10590
|
+
try {
|
|
10591
|
+
await mb.heartbeat({ agentId: callerId });
|
|
10592
|
+
} catch {
|
|
10593
|
+
}
|
|
10594
|
+
switch (action) {
|
|
10595
|
+
case "check":
|
|
10596
|
+
return executeCheck(mb, callerId, [baseCallerId], i);
|
|
10597
|
+
case "send":
|
|
10598
|
+
return executeSend(mb, callerId, callerSessionId, i);
|
|
10599
|
+
case "ack":
|
|
10600
|
+
return executeAck(mb, callerId, i);
|
|
10601
|
+
case "query":
|
|
10602
|
+
return executeQuery(mb, i);
|
|
10603
|
+
case "status":
|
|
10604
|
+
return executeStatus(mb);
|
|
10605
|
+
case "online":
|
|
10606
|
+
return executeOnline(mb);
|
|
10607
|
+
case "unread":
|
|
10608
|
+
return executeUnread(mb, callerId, [baseCallerId]);
|
|
10609
|
+
default:
|
|
10610
|
+
return { ok: false, error: `Unknown action: "${action}". Use check, send, ack, query, status, online, or unread.` };
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10613
|
+
};
|
|
10614
|
+
}
|
|
10615
|
+
async function executeCheck(mb, agentId, aliases, i) {
|
|
10616
|
+
const limit = i.limit ?? 20;
|
|
10617
|
+
const targets = [agentId, ...aliases.filter((al) => al && al !== agentId)];
|
|
10618
|
+
const batches = await Promise.all(
|
|
10619
|
+
targets.map(
|
|
10620
|
+
(to) => mb.query({ to, unreadBy: agentId, limit, minPriority: "low" }).catch(() => [])
|
|
10621
|
+
)
|
|
10622
|
+
);
|
|
10623
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10624
|
+
const messages = batches.flat().filter((m) => {
|
|
10625
|
+
if (seen.has(m.id)) return false;
|
|
10626
|
+
seen.add(m.id);
|
|
10627
|
+
return true;
|
|
10628
|
+
});
|
|
10629
|
+
const acked = await Promise.all(
|
|
10630
|
+
messages.map(async (m) => {
|
|
10631
|
+
const updated = await mb.ack({ messageId: m.id, readerId: agentId, read: true }).catch(() => null);
|
|
10632
|
+
return updated ?? m;
|
|
10633
|
+
})
|
|
10634
|
+
);
|
|
10635
|
+
return {
|
|
10636
|
+
ok: true,
|
|
10637
|
+
count: acked.length,
|
|
10638
|
+
messages: acked.map((m) => formatMessage(m, agentId)),
|
|
10639
|
+
summary: acked.length === 0 ? "No unread messages." : `${acked.length} unread message(s).`
|
|
10640
|
+
};
|
|
10641
|
+
}
|
|
10642
|
+
async function executeSend(mb, agentId, _sessionId, i) {
|
|
10643
|
+
const to = i.to;
|
|
10644
|
+
const tp = i.type;
|
|
10645
|
+
const subject = i.subject;
|
|
10646
|
+
const body = i.body;
|
|
10647
|
+
if (!to) return { ok: false, error: '"to" is required.' };
|
|
10648
|
+
if (!tp) return { ok: false, error: '"type" is required.' };
|
|
10649
|
+
if (!subject) return { ok: false, error: '"subject" is required.' };
|
|
10650
|
+
if (body === void 0 || body === null) return { ok: false, error: '"body" is required.' };
|
|
10651
|
+
const msg = await mb.send({
|
|
10652
|
+
from: agentId,
|
|
10653
|
+
to,
|
|
10654
|
+
type: tp,
|
|
10655
|
+
subject,
|
|
10656
|
+
body,
|
|
10657
|
+
priority: i.priority ?? "normal",
|
|
10658
|
+
replyTo: i.replyTo
|
|
10659
|
+
});
|
|
10660
|
+
return {
|
|
10661
|
+
ok: true,
|
|
10662
|
+
messageId: msg.id,
|
|
10663
|
+
to: msg.to,
|
|
10664
|
+
type: msg.type,
|
|
10665
|
+
timestamp: msg.timestamp,
|
|
10666
|
+
summary: `Message sent to ${msg.to === "*" ? "all agents" : msg.to}. Id: ${msg.id}`
|
|
10667
|
+
};
|
|
10668
|
+
}
|
|
10669
|
+
async function executeAck(mb, agentId, i) {
|
|
10670
|
+
const messageId = i.messageId;
|
|
10671
|
+
if (!messageId) return { ok: false, error: '"messageId" is required.' };
|
|
10672
|
+
const updated = await mb.ack({
|
|
10673
|
+
messageId,
|
|
10674
|
+
readerId: agentId,
|
|
10675
|
+
read: i.read,
|
|
10676
|
+
completed: i.completed,
|
|
10677
|
+
outcome: i.outcome
|
|
10678
|
+
});
|
|
10679
|
+
if (!updated) return { ok: false, error: `Message "${messageId}" not found.` };
|
|
10680
|
+
return {
|
|
10681
|
+
ok: true,
|
|
10682
|
+
messageId: updated.id,
|
|
10683
|
+
readBy: Object.keys(updated.readBy),
|
|
10684
|
+
readByCount: Object.keys(updated.readBy).length,
|
|
10685
|
+
completed: updated.completed,
|
|
10686
|
+
completedBy: updated.completedBy,
|
|
10687
|
+
outcome: updated.outcome,
|
|
10688
|
+
summary: `Message ${messageId} acknowledged. Read by ${Object.keys(updated.readBy).length} agent(s), Completed: ${updated.completed}.`
|
|
10689
|
+
};
|
|
10690
|
+
}
|
|
10691
|
+
async function executeQuery(mb, i) {
|
|
10692
|
+
const limit = i.limit ?? 50;
|
|
10693
|
+
const messages = await mb.query({
|
|
10694
|
+
to: i.to,
|
|
10695
|
+
from: i.from,
|
|
10696
|
+
unreadBy: i.unreadBy,
|
|
10697
|
+
incompleteOnly: i.incompleteOnly,
|
|
10698
|
+
type: i.type,
|
|
10699
|
+
minPriority: i.minPriority,
|
|
10700
|
+
since: i.since,
|
|
10701
|
+
limit
|
|
10702
|
+
});
|
|
10703
|
+
return { ok: true, count: messages.length, messages, summary: `${messages.length} message(s).` };
|
|
10704
|
+
}
|
|
10705
|
+
async function executeStatus(mb) {
|
|
10706
|
+
const agents = await mb.getAgentStatuses();
|
|
10707
|
+
return {
|
|
10708
|
+
ok: true,
|
|
10709
|
+
count: agents.length,
|
|
10710
|
+
agents: agents.map((a) => ({
|
|
10711
|
+
agentId: a.agentId,
|
|
10712
|
+
name: a.name,
|
|
10713
|
+
role: a.role,
|
|
10714
|
+
sessionId: a.sessionId,
|
|
10715
|
+
status: a.status,
|
|
10716
|
+
currentTool: a.currentTool,
|
|
10717
|
+
currentTask: a.currentTask,
|
|
10718
|
+
iterations: a.iterations,
|
|
10719
|
+
toolCalls: a.toolCalls,
|
|
10720
|
+
lastSeenAt: a.lastSeenAt,
|
|
10721
|
+
online: a.online,
|
|
10722
|
+
pid: a.pid,
|
|
10723
|
+
source: a.source
|
|
10724
|
+
})),
|
|
10725
|
+
summary: `${agents.filter((a) => a.online).length} online, ${agents.length} total.`
|
|
10726
|
+
};
|
|
10727
|
+
}
|
|
10728
|
+
async function executeOnline(mb) {
|
|
10729
|
+
const agents = await mb.getOnlineAgents();
|
|
10730
|
+
return {
|
|
10731
|
+
ok: true,
|
|
10732
|
+
count: agents.length,
|
|
10733
|
+
agents: agents.map((a) => ({
|
|
10734
|
+
agentId: a.agentId,
|
|
10735
|
+
name: a.name,
|
|
10736
|
+
role: a.role,
|
|
10737
|
+
sessionId: a.sessionId,
|
|
10738
|
+
status: a.status,
|
|
10739
|
+
currentTool: a.currentTool,
|
|
10740
|
+
currentTask: a.currentTask,
|
|
10741
|
+
lastSeenAt: a.lastSeenAt,
|
|
10742
|
+
source: a.source
|
|
10743
|
+
})),
|
|
10744
|
+
summary: `${agents.length} online agent(s).`
|
|
10745
|
+
};
|
|
10746
|
+
}
|
|
10747
|
+
async function executeUnread(mb, agentId, aliases = []) {
|
|
10748
|
+
const targets = [agentId, ...aliases.filter((al) => al && al !== agentId)];
|
|
10749
|
+
const batches = await Promise.all(
|
|
10750
|
+
targets.map((to) => mb.query({ to, unreadBy: agentId, limit: 200 }).catch(() => []))
|
|
10751
|
+
);
|
|
10752
|
+
const ids = new Set(batches.flat().map((m) => m.id));
|
|
10753
|
+
return { ok: true, count: ids.size, summary: `${ids.size} unread message(s) for you.` };
|
|
10754
|
+
}
|
|
10755
|
+
function formatMessage(m, readerId) {
|
|
10756
|
+
const maxBody = 2e3;
|
|
10757
|
+
const truncated = m.body.length > maxBody ? `${m.body.slice(0, maxBody)}\u2026 [truncated]` : m.body;
|
|
10758
|
+
return {
|
|
10759
|
+
id: m.id,
|
|
10760
|
+
from: m.from,
|
|
10761
|
+
to: m.to,
|
|
10762
|
+
type: m.type,
|
|
10763
|
+
subject: m.subject,
|
|
10764
|
+
body: truncated,
|
|
10765
|
+
priority: m.priority,
|
|
10766
|
+
readByMe: readerId in m.readBy,
|
|
10767
|
+
readByCount: Object.keys(m.readBy).length,
|
|
10768
|
+
readBy: m.readBy,
|
|
10769
|
+
completed: m.completed,
|
|
10770
|
+
completedBy: m.completedBy,
|
|
10771
|
+
outcome: m.outcome,
|
|
10772
|
+
timestamp: m.timestamp,
|
|
10773
|
+
replyTo: m.replyTo,
|
|
10774
|
+
senderSessionId: m.senderSessionId
|
|
10775
|
+
};
|
|
10776
|
+
}
|
|
10777
|
+
|
|
10778
|
+
// src/coordination/mail-tools.ts
|
|
10779
|
+
function makeResolver(opts) {
|
|
10780
|
+
return opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
10781
|
+
}
|
|
10782
|
+
async function register(mb, ctx) {
|
|
10783
|
+
const identity = resolveMailboxIdentity(ctx);
|
|
10784
|
+
try {
|
|
10785
|
+
await mb.registerAgent({
|
|
10786
|
+
agentId: identity.callerId,
|
|
10787
|
+
sessionId: identity.sessionId,
|
|
10788
|
+
name: identity.name,
|
|
10789
|
+
role: identity.role,
|
|
10790
|
+
pid: process.pid,
|
|
10791
|
+
source: ctx.meta["source"] ?? "cli"
|
|
10792
|
+
});
|
|
10793
|
+
await mb.heartbeat({ agentId: identity.callerId });
|
|
10794
|
+
} catch {
|
|
10795
|
+
}
|
|
10796
|
+
return identity;
|
|
10797
|
+
}
|
|
10798
|
+
function makeMailSendTool(opts = {}) {
|
|
10799
|
+
const resolveMailbox = makeResolver(opts);
|
|
10800
|
+
return {
|
|
10801
|
+
name: "mail_send",
|
|
10802
|
+
description: 'Send a mail to other agents working on this project (other terminals, TUIs, WebUIs). Use it to hand off work ("can you review src/auth.ts?"), ask questions, or announce what you just did. to="*" broadcasts to everyone; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Recipients see your mail automatically before their next step.',
|
|
10803
|
+
usageHint: 'mail_send to="*" subject="auth refactor done" body="touched src/auth/*, please review"',
|
|
10804
|
+
category: "coordination",
|
|
10805
|
+
permission: "auto",
|
|
10806
|
+
mutating: true,
|
|
10807
|
+
inputSchema: {
|
|
10808
|
+
type: "object",
|
|
10809
|
+
properties: {
|
|
10810
|
+
to: {
|
|
10811
|
+
type: "string",
|
|
10812
|
+
description: 'Recipient: exact agent id ("leader@a1b2c3d4"), base alias ("leader"), or "*" / "all" for everyone.'
|
|
10813
|
+
},
|
|
10814
|
+
subject: { type: "string", description: "Short subject line." },
|
|
10815
|
+
body: { type: "string", description: "The message." },
|
|
10816
|
+
type: {
|
|
10817
|
+
type: "string",
|
|
10818
|
+
enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result"],
|
|
10819
|
+
description: 'Message intent. Default: "broadcast" when to="*", otherwise "note".'
|
|
10820
|
+
},
|
|
10821
|
+
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
10822
|
+
replyTo: { type: "string", description: "Message id this replies to." }
|
|
10823
|
+
},
|
|
10824
|
+
required: ["to", "subject", "body"]
|
|
10825
|
+
},
|
|
10826
|
+
async execute(input, ctx) {
|
|
10827
|
+
const i = input ?? {};
|
|
10828
|
+
const rawTo = i.to;
|
|
10829
|
+
const subject = i.subject;
|
|
10830
|
+
const body = i.body;
|
|
10831
|
+
if (!rawTo || !subject || body === void 0 || body === null) {
|
|
10832
|
+
return { ok: false, error: '"to", "subject" and "body" are required.' };
|
|
10833
|
+
}
|
|
10834
|
+
const to = normalizeRecipient(rawTo);
|
|
10835
|
+
const mb = resolveMailbox(ctx);
|
|
10836
|
+
const identity = await register(mb, ctx);
|
|
10837
|
+
const type = i.type ?? (to === "*" ? "broadcast" : "note");
|
|
10838
|
+
const msg = await mb.send({
|
|
10839
|
+
from: identity.callerId,
|
|
10840
|
+
to,
|
|
10841
|
+
type,
|
|
10842
|
+
subject,
|
|
10843
|
+
body,
|
|
10844
|
+
priority: i.priority ?? "normal",
|
|
10845
|
+
replyTo: i.replyTo
|
|
10846
|
+
});
|
|
10847
|
+
return {
|
|
10848
|
+
ok: true,
|
|
10849
|
+
messageId: msg.id,
|
|
10850
|
+
from: identity.callerId,
|
|
10851
|
+
to: msg.to,
|
|
10852
|
+
summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}.`
|
|
10853
|
+
};
|
|
10854
|
+
}
|
|
10855
|
+
};
|
|
10856
|
+
}
|
|
10857
|
+
function makeMailInboxTool(opts = {}) {
|
|
10858
|
+
const resolveMailbox = makeResolver(opts);
|
|
10859
|
+
return {
|
|
10860
|
+
name: "mail_inbox",
|
|
10861
|
+
description: 'Read your unread mail from other agents on this project and mark it read. Covers mail addressed to you directly, to your base name (e.g. "leader"), and broadcasts ("*"). Urgent steer/btw mail is already injected automatically \u2014 use this to catch up on notes, questions, handoffs and results, or after a long stretch of tool work.',
|
|
10862
|
+
usageHint: "mail_inbox (optionally: limit=10, markRead=false to peek)",
|
|
10863
|
+
category: "coordination",
|
|
10864
|
+
permission: "auto",
|
|
10865
|
+
mutating: false,
|
|
10866
|
+
inputSchema: {
|
|
10867
|
+
type: "object",
|
|
10868
|
+
properties: {
|
|
10869
|
+
limit: { type: "number", description: "Max messages to return (default 20)." },
|
|
10870
|
+
markRead: {
|
|
10871
|
+
type: "boolean",
|
|
10872
|
+
description: "Add a read receipt for each returned message (default true)."
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
},
|
|
10876
|
+
async execute(input, ctx) {
|
|
10877
|
+
const i = input ?? {};
|
|
10878
|
+
const limit = i.limit ?? 20;
|
|
10879
|
+
const markRead = i.markRead ?? true;
|
|
10880
|
+
const mb = resolveMailbox(ctx);
|
|
10881
|
+
const identity = await register(mb, ctx);
|
|
10882
|
+
const targets = [identity.callerId];
|
|
10883
|
+
if (identity.baseId !== identity.callerId) targets.push(identity.baseId);
|
|
10884
|
+
const batches = await Promise.all(
|
|
10885
|
+
targets.map(
|
|
10886
|
+
(to) => mb.query({ to, unreadBy: identity.callerId, limit }).catch(() => [])
|
|
10887
|
+
)
|
|
10888
|
+
);
|
|
10889
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10890
|
+
const messages = batches.flat().filter((m) => {
|
|
10891
|
+
if (seen.has(m.id) || m.from === identity.callerId) return false;
|
|
10892
|
+
seen.add(m.id);
|
|
10893
|
+
return true;
|
|
10894
|
+
}).slice(0, limit);
|
|
10895
|
+
if (markRead) {
|
|
10896
|
+
await Promise.all(
|
|
10897
|
+
messages.map(
|
|
10898
|
+
(m) => mb.ack({ messageId: m.id, readerId: identity.callerId, read: true }).catch(() => null)
|
|
10899
|
+
)
|
|
10900
|
+
);
|
|
10901
|
+
}
|
|
10902
|
+
return {
|
|
10903
|
+
ok: true,
|
|
10904
|
+
you: identity.callerId,
|
|
10905
|
+
count: messages.length,
|
|
10906
|
+
messages: messages.map((m) => ({
|
|
10907
|
+
id: m.id,
|
|
10908
|
+
from: m.from,
|
|
10909
|
+
to: m.to,
|
|
10910
|
+
type: m.type,
|
|
10911
|
+
subject: m.subject,
|
|
10912
|
+
body: m.body.length > 2e3 ? `${m.body.slice(0, 2e3)}\u2026 [truncated]` : m.body,
|
|
10913
|
+
timestamp: m.timestamp,
|
|
10914
|
+
replyTo: m.replyTo
|
|
10915
|
+
})),
|
|
10916
|
+
summary: messages.length === 0 ? "Inbox empty." : `${messages.length} unread message(s)${markRead ? " (marked read)" : ""}. Reply with mail_send using the sender id.`
|
|
10917
|
+
};
|
|
10918
|
+
}
|
|
10919
|
+
};
|
|
10920
|
+
}
|
|
10921
|
+
|
|
10922
|
+
// src/coordination/dep-watcher.ts
|
|
10923
|
+
var DEPENDENCY_FILE_PATTERNS = [
|
|
10924
|
+
"package.json",
|
|
10925
|
+
"tsconfig.json",
|
|
10926
|
+
"pnpm-lock.yaml",
|
|
10927
|
+
"yarn.lock",
|
|
10928
|
+
"package-lock.json",
|
|
10929
|
+
"go.mod",
|
|
10930
|
+
"go.sum",
|
|
10931
|
+
"Cargo.toml",
|
|
10932
|
+
"Cargo.lock",
|
|
10933
|
+
"pyproject.toml",
|
|
10934
|
+
"setup.py",
|
|
10935
|
+
"setup.cfg",
|
|
10936
|
+
"requirements.txt",
|
|
10937
|
+
"Pipfile",
|
|
10938
|
+
"Pipfile.lock",
|
|
10939
|
+
"Gemfile",
|
|
10940
|
+
"Gemfile.lock",
|
|
10941
|
+
"composer.json",
|
|
10942
|
+
"composer.lock",
|
|
10943
|
+
"mix.exs",
|
|
10944
|
+
"mix.lock",
|
|
10945
|
+
"pom.xml",
|
|
10946
|
+
"build.gradle",
|
|
10947
|
+
"build.gradle.kts",
|
|
10948
|
+
"settings.gradle",
|
|
10949
|
+
"settings.gradle.kts",
|
|
10950
|
+
"*.csproj",
|
|
10951
|
+
"packages.config",
|
|
10952
|
+
"pubspec.yaml",
|
|
10953
|
+
"pubspec.lock",
|
|
10954
|
+
"CMakeLists.txt",
|
|
10955
|
+
"conanfile.txt",
|
|
10956
|
+
"conanfile.py",
|
|
10957
|
+
"vcpkg.json"
|
|
10958
|
+
];
|
|
10959
|
+
function makeDependencyWatcherConfig(opts) {
|
|
10960
|
+
const {
|
|
10961
|
+
projectRoot,
|
|
10962
|
+
mailbox,
|
|
10963
|
+
targetAgent = "*",
|
|
10964
|
+
watcherAgentId = "dep-watcher",
|
|
10965
|
+
debounceMs = 3e3,
|
|
10966
|
+
patterns = DEPENDENCY_FILE_PATTERNS
|
|
10967
|
+
} = opts;
|
|
10968
|
+
const watchPaths = [];
|
|
10969
|
+
for (const p of patterns) {
|
|
10970
|
+
if (p.includes("*")) {
|
|
10971
|
+
continue;
|
|
10972
|
+
}
|
|
10973
|
+
watchPaths.push(`${projectRoot}/${p}`);
|
|
10974
|
+
}
|
|
10975
|
+
watchPaths.push(projectRoot);
|
|
10976
|
+
const unique = [...new Set(watchPaths)];
|
|
10977
|
+
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
10978
|
+
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
10979
|
+
function matchesPattern(filePath) {
|
|
10980
|
+
const basename5 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
10981
|
+
if (plainPatterns.includes(basename5)) return true;
|
|
10982
|
+
for (const gp of globPatterns) {
|
|
10983
|
+
const regex = new RegExp(
|
|
10984
|
+
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
10985
|
+
);
|
|
10986
|
+
if (regex.test(basename5)) return true;
|
|
10987
|
+
}
|
|
10988
|
+
return false;
|
|
10989
|
+
}
|
|
10990
|
+
const pending = /* @__PURE__ */ new Map();
|
|
10991
|
+
return {
|
|
10992
|
+
watchPaths: unique,
|
|
10993
|
+
debounceMs,
|
|
10994
|
+
async onChange(entry) {
|
|
10995
|
+
if (entry.event === "delete") return;
|
|
10996
|
+
if (!matchesPattern(entry.path)) return;
|
|
10997
|
+
const key = entry.path;
|
|
10998
|
+
const existing = pending.get(key);
|
|
10999
|
+
if (existing) clearTimeout(existing);
|
|
11000
|
+
pending.set(
|
|
11001
|
+
key,
|
|
11002
|
+
setTimeout(async () => {
|
|
11003
|
+
pending.delete(key);
|
|
11004
|
+
try {
|
|
11005
|
+
const fileName = entry.path.split("/").pop()?.split("\\").pop() ?? entry.path;
|
|
11006
|
+
await mailbox.send({
|
|
11007
|
+
from: watcherAgentId,
|
|
11008
|
+
to: targetAgent,
|
|
11009
|
+
type: "assign",
|
|
11010
|
+
subject: `Dependency file changed: ${fileName}`,
|
|
11011
|
+
body: [
|
|
11012
|
+
`File: ${entry.path}`,
|
|
11013
|
+
`Event: ${entry.event}`,
|
|
11014
|
+
`Timestamp: ${entry.timestamp}`,
|
|
11015
|
+
"",
|
|
11016
|
+
`Action: Run a tech-stack audit on the changed dependency file.`,
|
|
11017
|
+
`Validate any new packages, check versions, flag deprecated or prehistoric packages.`,
|
|
11018
|
+
`Report findings back via mailbox (type: result).`
|
|
11019
|
+
].join("\n"),
|
|
11020
|
+
priority: "high",
|
|
11021
|
+
taskContext: {
|
|
11022
|
+
agentRole: "tech-stack",
|
|
11023
|
+
status: "pending"
|
|
11024
|
+
}
|
|
11025
|
+
});
|
|
11026
|
+
} catch {
|
|
11027
|
+
}
|
|
11028
|
+
}, debounceMs)
|
|
11029
|
+
);
|
|
11030
|
+
}
|
|
11031
|
+
};
|
|
11032
|
+
}
|
|
11033
|
+
|
|
11034
|
+
// src/coordination/dep-watcher-bridge.ts
|
|
11035
|
+
function attachDepWatcherBridge(opts) {
|
|
11036
|
+
const {
|
|
11037
|
+
events,
|
|
11038
|
+
mailbox,
|
|
11039
|
+
projectRoot,
|
|
11040
|
+
targetAgent = "tech-stack",
|
|
11041
|
+
watcherAgentId = "dep-watcher",
|
|
11042
|
+
debounceMs = 3e3
|
|
11043
|
+
} = opts;
|
|
11044
|
+
const cfg = makeDependencyWatcherConfig({
|
|
11045
|
+
projectRoot,
|
|
11046
|
+
mailbox,
|
|
11047
|
+
targetAgent,
|
|
11048
|
+
watcherAgentId,
|
|
11049
|
+
debounceMs
|
|
11050
|
+
});
|
|
11051
|
+
const unsub = events.onPattern("file-watcher:changed", (_eventName, rawPayload) => {
|
|
11052
|
+
const payload = rawPayload;
|
|
11053
|
+
if (!payload?.path) return;
|
|
11054
|
+
void cfg.onChange({
|
|
11055
|
+
path: payload.path,
|
|
11056
|
+
event: payload.event ?? "change",
|
|
11057
|
+
timestamp: payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
11058
|
+
}).catch(() => {
|
|
11059
|
+
});
|
|
11060
|
+
});
|
|
11061
|
+
return () => {
|
|
11062
|
+
unsub();
|
|
11063
|
+
};
|
|
11064
|
+
}
|
|
11065
|
+
|
|
11066
|
+
// src/coordination/mailbox-hooks.ts
|
|
11067
|
+
function createMailboxHooks(opts) {
|
|
11068
|
+
const { mailbox, agentId, notifyNewMail = true, heartbeat = true } = opts;
|
|
11069
|
+
let lastUnreadCount = -1;
|
|
11070
|
+
return {
|
|
11071
|
+
/**
|
|
11072
|
+
* Call before each tool execution. Checks mailbox and emits events.
|
|
11073
|
+
* @param events — EventBus-like object with emit method.
|
|
11074
|
+
*/
|
|
11075
|
+
async beforeTool(events) {
|
|
11076
|
+
try {
|
|
11077
|
+
const count = await mailbox.unreadCount(agentId);
|
|
11078
|
+
if (notifyNewMail && count !== lastUnreadCount) {
|
|
11079
|
+
lastUnreadCount = count;
|
|
11080
|
+
events.emit("mailbox.unread_count", { agentId, count });
|
|
11081
|
+
}
|
|
11082
|
+
} catch {
|
|
11083
|
+
}
|
|
11084
|
+
},
|
|
11085
|
+
/**
|
|
11086
|
+
* Call after each tool execution. Updates heartbeat and optionally
|
|
11087
|
+
* current tool status.
|
|
11088
|
+
*/
|
|
11089
|
+
async afterTool(toolName) {
|
|
11090
|
+
if (!heartbeat) return;
|
|
11091
|
+
try {
|
|
11092
|
+
await mailbox.heartbeat({
|
|
11093
|
+
agentId,
|
|
11094
|
+
status: "running",
|
|
11095
|
+
currentTool: toolName
|
|
11096
|
+
});
|
|
11097
|
+
} catch {
|
|
11098
|
+
}
|
|
11099
|
+
},
|
|
11100
|
+
/** Reset the cached unread count (e.g., after the agent checks manually). */
|
|
11101
|
+
reset() {
|
|
11102
|
+
lastUnreadCount = -1;
|
|
11103
|
+
}
|
|
11104
|
+
};
|
|
11105
|
+
}
|
|
11106
|
+
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
11107
|
+
var LOG_FILENAME = "package-authors.json";
|
|
11108
|
+
function logPath(storageDir) {
|
|
11109
|
+
return path4.join(storageDir, LOG_FILENAME);
|
|
11110
|
+
}
|
|
11111
|
+
async function loadLog(storageDir, projectRoot) {
|
|
11112
|
+
try {
|
|
11113
|
+
const raw = await fsp6.readFile(logPath(storageDir), "utf-8");
|
|
11114
|
+
const parsed = JSON.parse(raw);
|
|
11115
|
+
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
11116
|
+
return { projectRoot, entries: [] };
|
|
11117
|
+
}
|
|
11118
|
+
return parsed;
|
|
11119
|
+
} catch (err) {
|
|
11120
|
+
if (err.code === "ENOENT") {
|
|
11121
|
+
return { projectRoot, entries: [] };
|
|
11122
|
+
}
|
|
11123
|
+
throw err;
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
async function saveLog(storageDir, log) {
|
|
11127
|
+
await fsp6.mkdir(storageDir, { recursive: true });
|
|
11128
|
+
const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
|
|
11129
|
+
await fsp6.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
11130
|
+
await fsp6.rename(tmp, logPath(storageDir));
|
|
11131
|
+
}
|
|
11132
|
+
function detectEcosystem(manifestPath) {
|
|
11133
|
+
const name = path4.basename(manifestPath).toLowerCase();
|
|
11134
|
+
if (name === "package.json") return "npm";
|
|
11135
|
+
if (name === "go.mod") return "go";
|
|
11136
|
+
if (name === "cargo.toml") return "cargo";
|
|
11137
|
+
if (name === "pyproject.toml" || name === "requirements.txt" || name === "pipfile" || name === "pipfile.lock") return "pip";
|
|
11138
|
+
if (name === "gemfile" || name === "gemfile.lock") return "gem";
|
|
11139
|
+
if (name === "composer.json" || name === "composer.lock") return "composer";
|
|
11140
|
+
if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
|
|
11141
|
+
if (name === "mix.exs" || name === "mix.lock") return "elixir";
|
|
11142
|
+
if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
|
|
11143
|
+
if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
|
|
11144
|
+
if (name === "vcpkg.json") return "vcpkg";
|
|
11145
|
+
if (name === "conanfile.txt" || name === "conanfile.py") return "conan";
|
|
11146
|
+
if (name === "cmakeLists.txt") return "cmake";
|
|
11147
|
+
return "unknown";
|
|
11148
|
+
}
|
|
11149
|
+
async function recordPackageAction(opts, entry) {
|
|
11150
|
+
const { storageDir, projectRoot, maxEntries = DEFAULT_MAX_ENTRIES } = opts;
|
|
11151
|
+
const log = await loadLog(storageDir, projectRoot);
|
|
11152
|
+
log.entries.push({
|
|
11153
|
+
...entry,
|
|
11154
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11155
|
+
});
|
|
11156
|
+
if (log.entries.length > maxEntries) {
|
|
11157
|
+
const keep = Math.floor(maxEntries * 0.8);
|
|
11158
|
+
log.entries = log.entries.slice(-keep);
|
|
11159
|
+
log.lastCompactedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11160
|
+
}
|
|
11161
|
+
await saveLog(storageDir, log);
|
|
11162
|
+
}
|
|
11163
|
+
async function getPackageAuthor(opts, manifestPath, packageName) {
|
|
11164
|
+
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
11165
|
+
const normalizedManifest = manifestPath.replace(/\\/g, "/");
|
|
11166
|
+
for (let i = log.entries.length - 1; i >= 0; i--) {
|
|
11167
|
+
const e = log.entries[i];
|
|
11168
|
+
if (e && e.manifestPath.replace(/\\/g, "/") === normalizedManifest && e.packageName === packageName) {
|
|
11169
|
+
return e;
|
|
11170
|
+
}
|
|
11171
|
+
}
|
|
11172
|
+
return void 0;
|
|
11173
|
+
}
|
|
11174
|
+
async function getManifestPackages(opts, manifestPath) {
|
|
11175
|
+
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
11176
|
+
const normalizedManifest = manifestPath.replace(/\\/g, "/");
|
|
11177
|
+
return log.entries.filter(
|
|
11178
|
+
(e) => e.manifestPath.replace(/\\/g, "/") === normalizedManifest
|
|
11179
|
+
);
|
|
11180
|
+
}
|
|
11181
|
+
async function getPackagesByAgent(opts, agentId) {
|
|
11182
|
+
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
11183
|
+
const map = /* @__PURE__ */ new Map();
|
|
11184
|
+
for (const e of log.entries) {
|
|
11185
|
+
if (e.agentId === agentId) {
|
|
11186
|
+
const key = `${e.manifestPath}|${e.packageName}`;
|
|
11187
|
+
map.set(key, e);
|
|
11188
|
+
}
|
|
11189
|
+
}
|
|
11190
|
+
return map;
|
|
11191
|
+
}
|
|
11192
|
+
async function updatePackageOutdatedStatus(opts, manifestPath, packageName, outdated, latestVersion) {
|
|
11193
|
+
const { storageDir, projectRoot } = opts;
|
|
11194
|
+
const log = await loadLog(storageDir, projectRoot);
|
|
11195
|
+
log.entries.push({
|
|
11196
|
+
manifestPath,
|
|
11197
|
+
packageName,
|
|
11198
|
+
versionSpec: "",
|
|
11199
|
+
ecosystem: detectEcosystem(manifestPath),
|
|
11200
|
+
agentId: "outdated-checker",
|
|
11201
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11202
|
+
outdated,
|
|
11203
|
+
latestVersion
|
|
11204
|
+
});
|
|
11205
|
+
await saveLog(storageDir, log);
|
|
11206
|
+
}
|
|
11207
|
+
async function getFullPackageLog(opts) {
|
|
11208
|
+
return loadLog(opts.storageDir, opts.projectRoot);
|
|
11209
|
+
}
|
|
11210
|
+
|
|
11211
|
+
// src/coordination/package-outdated-watcher.ts
|
|
11212
|
+
function parseOutdatedPackages(body) {
|
|
11213
|
+
const results = [];
|
|
11214
|
+
const tableRows = body.matchAll(
|
|
11215
|
+
/^\|\s*([^-][^|]*?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm
|
|
11216
|
+
);
|
|
11217
|
+
for (const rowMatch of tableRows) {
|
|
11218
|
+
const cols = rowMatch[0].split("|").map((c) => c.trim()).filter(Boolean);
|
|
11219
|
+
if (cols.length >= 5 && cols[0] && cols[0] !== "Package") {
|
|
11220
|
+
results.push({
|
|
11221
|
+
name: cols[0] ?? "",
|
|
11222
|
+
currentVersion: cols[1] ?? "",
|
|
11223
|
+
latestVersion: cols[2] ?? "",
|
|
11224
|
+
wantedVersion: cols[3] ?? "",
|
|
11225
|
+
manifestPath: cols[4] ?? "",
|
|
11226
|
+
ecosystem: detectEcosystem2(cols[4] ?? "")
|
|
11227
|
+
});
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
if (results.length === 0) {
|
|
11231
|
+
const kvMatches = body.matchAll(
|
|
11232
|
+
/(?:package|name)[\s:=]+([\w@/-]+).*?(?:current|version)[\s:=]+([\d.]+).*?latest[\s:=]+([\d.]+)/gi
|
|
11233
|
+
);
|
|
11234
|
+
for (const m of kvMatches) {
|
|
11235
|
+
results.push({
|
|
11236
|
+
name: m[1] ?? "",
|
|
11237
|
+
currentVersion: m[2] ?? "",
|
|
11238
|
+
latestVersion: m[3] ?? "",
|
|
11239
|
+
wantedVersion: m[2] ?? "",
|
|
11240
|
+
manifestPath: "",
|
|
11241
|
+
ecosystem: "unknown"
|
|
11242
|
+
});
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
return results;
|
|
11246
|
+
}
|
|
11247
|
+
function detectEcosystem2(manifestPath) {
|
|
11248
|
+
const name = manifestPath.split("/").pop()?.split("\\").pop() ?? manifestPath;
|
|
11249
|
+
if (name === "package.json") return "npm";
|
|
11250
|
+
if (name === "go.mod") return "go";
|
|
11251
|
+
if (name === "cargo.toml") return "cargo";
|
|
11252
|
+
if (name === "pyproject.toml" || name === "requirements.txt") return "pip";
|
|
11253
|
+
if (name === "gemfile" || name === "gemfile.lock") return "gem";
|
|
11254
|
+
if (name === "composer.json" || name === "composer.lock") return "composer";
|
|
11255
|
+
if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
|
|
11256
|
+
if (name === "mix.exs" || name === "mix.lock") return "elixir";
|
|
11257
|
+
if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
|
|
11258
|
+
if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
|
|
11259
|
+
return "unknown";
|
|
11260
|
+
}
|
|
11261
|
+
function startPackageOutdatedWatcher(opts) {
|
|
11262
|
+
const {
|
|
11263
|
+
mailbox,
|
|
11264
|
+
packageTrackerOpts,
|
|
11265
|
+
pollIntervalMs = 60 * 60 * 1e3,
|
|
11266
|
+
watcherAgentId = "pkg-outdated-watcher",
|
|
11267
|
+
onNotify,
|
|
11268
|
+
onLog,
|
|
11269
|
+
onError
|
|
11270
|
+
} = opts;
|
|
11271
|
+
const log = (msg) => onLog?.(msg);
|
|
11272
|
+
const handleError = (err) => onError?.(err);
|
|
11273
|
+
const state = {
|
|
11274
|
+
running: true,
|
|
11275
|
+
timer: null,
|
|
11276
|
+
processedIds: /* @__PURE__ */ new Set()
|
|
11277
|
+
};
|
|
11278
|
+
async function pollOnce() {
|
|
11279
|
+
if (!state.running) return;
|
|
11280
|
+
try {
|
|
11281
|
+
const messages = await mailbox.query({
|
|
11282
|
+
to: watcherAgentId,
|
|
11283
|
+
type: "result",
|
|
11284
|
+
unreadBy: watcherAgentId,
|
|
11285
|
+
limit: 10
|
|
11286
|
+
});
|
|
11287
|
+
for (const msg of messages) {
|
|
11288
|
+
if (state.processedIds.has(msg.id)) continue;
|
|
11289
|
+
state.processedIds.add(msg.id);
|
|
11290
|
+
await mailbox.ack({
|
|
11291
|
+
messageId: msg.id,
|
|
11292
|
+
readerId: watcherAgentId,
|
|
11293
|
+
read: true
|
|
11294
|
+
});
|
|
11295
|
+
await processResultMessage(msg);
|
|
11296
|
+
}
|
|
11297
|
+
} catch (err) {
|
|
11298
|
+
handleError(err);
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
async function processResultMessage(msg) {
|
|
11302
|
+
const entries = parseOutdatedPackages(msg.body ?? "");
|
|
11303
|
+
if (entries.length === 0) {
|
|
11304
|
+
log(`[pkg-outdated-watcher] No outdated packages found in message ${msg.id}`);
|
|
11305
|
+
return;
|
|
11306
|
+
}
|
|
11307
|
+
log(`[pkg-outdated-watcher] Processing ${entries.length} outdated package(s) from ${msg.from}`);
|
|
11308
|
+
for (const entry of entries) {
|
|
11309
|
+
try {
|
|
11310
|
+
const author = await getPackageAuthor(
|
|
11311
|
+
packageTrackerOpts,
|
|
11312
|
+
entry.manifestPath,
|
|
11313
|
+
entry.name
|
|
11314
|
+
);
|
|
11315
|
+
const notifyTarget = author?.agentId ?? "*";
|
|
11316
|
+
const notifyBody = buildNotifyBody(entry, author?.agentName);
|
|
11317
|
+
const notifyMsg = {
|
|
11318
|
+
from: watcherAgentId,
|
|
11319
|
+
to: notifyTarget,
|
|
11320
|
+
subject: `Outdated package: ${entry.name}@${entry.currentVersion} \u2192 ${entry.latestVersion}`,
|
|
11321
|
+
body: notifyBody,
|
|
11322
|
+
priority: "high"
|
|
11323
|
+
};
|
|
11324
|
+
await onNotify(notifyMsg);
|
|
11325
|
+
log(
|
|
11326
|
+
`[pkg-outdated-watcher] Notified ${notifyTarget} about outdated ${entry.name} (${entry.currentVersion} \u2192 ${entry.latestVersion}) in ${entry.manifestPath}`
|
|
11327
|
+
);
|
|
11328
|
+
} catch (err) {
|
|
11329
|
+
handleError(err);
|
|
11330
|
+
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11333
|
+
}
|
|
11334
|
+
state.timer = setInterval(() => {
|
|
11335
|
+
void pollOnce();
|
|
11336
|
+
}, pollIntervalMs);
|
|
11337
|
+
void pollOnce();
|
|
11338
|
+
return () => {
|
|
11339
|
+
state.running = false;
|
|
11340
|
+
if (state.timer) {
|
|
11341
|
+
clearInterval(state.timer);
|
|
11342
|
+
state.timer = null;
|
|
11343
|
+
}
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
function buildNotifyBody(entry, authorName) {
|
|
11347
|
+
const lines = [
|
|
11348
|
+
`The package **${entry.name}** is outdated.`,
|
|
11349
|
+
"",
|
|
11350
|
+
`| Field | Value |`,
|
|
11351
|
+
`|-------|-------|`,
|
|
11352
|
+
`| Package | ${entry.name} |`,
|
|
11353
|
+
`| Installed | ${entry.currentVersion} |`,
|
|
11354
|
+
`| Latest | ${entry.latestVersion} |`,
|
|
11355
|
+
`| Wanted | ${entry.wantedVersion} |`,
|
|
11356
|
+
`| Manifest | ${entry.manifestPath} |`,
|
|
11357
|
+
`| Ecosystem | ${entry.ecosystem} |`,
|
|
11358
|
+
""
|
|
11359
|
+
];
|
|
11360
|
+
if (authorName) {
|
|
11361
|
+
lines.push(
|
|
11362
|
+
`You added this package${authorName !== "unknown" ? ` (as ${authorName})` : ""}. Consider updating it with the install tool.`
|
|
11363
|
+
);
|
|
11364
|
+
} else {
|
|
11365
|
+
lines.push(
|
|
11366
|
+
`This package appears to have been added by an agent no longer on record. Consider reviewing and updating it.`
|
|
11367
|
+
);
|
|
11368
|
+
}
|
|
11369
|
+
lines.push(
|
|
11370
|
+
"",
|
|
11371
|
+
`Update with:`,
|
|
11372
|
+
`\`\`\``,
|
|
11373
|
+
`${getUpdateCommand(entry)}`,
|
|
11374
|
+
`\`\`\``
|
|
11375
|
+
);
|
|
11376
|
+
return lines.join("\n");
|
|
11377
|
+
}
|
|
11378
|
+
function getUpdateCommand(entry) {
|
|
11379
|
+
switch (entry.ecosystem) {
|
|
11380
|
+
case "npm":
|
|
11381
|
+
return `pnpm add ${entry.name}@latest # or: pnpm update ${entry.name}`;
|
|
11382
|
+
case "cargo":
|
|
11383
|
+
return `cargo update ${entry.name}`;
|
|
11384
|
+
case "go":
|
|
11385
|
+
return `go get ${entry.name}@latest`;
|
|
11386
|
+
case "pip":
|
|
11387
|
+
return `pip install --upgrade ${entry.name}`;
|
|
11388
|
+
case "gem":
|
|
11389
|
+
return `gem install ${entry.name}`;
|
|
11390
|
+
case "composer":
|
|
11391
|
+
return `composer require ${entry.name}:^${entry.latestVersion} --update-with-dependencies`;
|
|
11392
|
+
case "nuget":
|
|
11393
|
+
return `dotnet add package ${entry.name}`;
|
|
11394
|
+
case "maven":
|
|
11395
|
+
return `# Update the <version> in pom.xml or run:
|
|
11396
|
+
mvn versions:use-latest-versions`;
|
|
11397
|
+
case "dart":
|
|
11398
|
+
return `dart pub upgrade ${entry.name}`;
|
|
11399
|
+
default:
|
|
11400
|
+
return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
|
|
11401
|
+
}
|
|
11402
|
+
}
|
|
11403
|
+
|
|
11404
|
+
export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus };
|
|
9430
11405
|
//# sourceMappingURL=index.js.map
|
|
9431
11406
|
//# sourceMappingURL=index.js.map
|