@wrongstack/core 0.155.0 → 0.250.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-BbZU5TPN.d.ts → agent-bridge-4gc0vfW2.d.ts} +1 -1
- package/dist/{agent-subagent-runner-Bsueu0J2.d.ts → agent-subagent-runner-Dz-9kiE6.d.ts} +9 -8
- package/dist/{brain-CS_B0vIE.d.ts → brain-sCZ3lCjq.d.ts} +26 -2
- package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
- package/dist/{config-BaVThgnT.d.ts → config-eSsrto5d.d.ts} +8 -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 +1986 -146
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +26 -26
- package/dist/defaults/index.js +1110 -296
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +45 -16
- package/dist/execution/index.js +229 -56
- 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-CbV8pXLD.d.ts → goal-preamble-BjJpnLW4.d.ts} +19 -10
- package/dist/{index-CI1hRfPt.d.ts → index-Dy8OwfBD.d.ts} +8 -8
- package/dist/{index-B5wz-GXm.d.ts → index-IehiNryU.d.ts} +7 -5
- package/dist/index.d.ts +438 -128
- package/dist/index.js +4989 -849
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +7 -7
- package/dist/infrastructure/index.js +61 -13
- 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-CPERR2De.d.ts → mcp-servers-DfXxCASH.d.ts} +3 -3
- 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-DpanBg8D.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-BSKSFNhv.d.ts → multi-agent-coordinator-CnbEqpv0.d.ts} +8 -8
- package/dist/{null-fleet-bus-CGOez8Le.d.ts → null-fleet-bus-Do1OLYpj.d.ts} +7 -7
- package/dist/observability/index.d.ts +2 -2
- package/dist/package-outdated-watcher-CA5GGB4C.d.ts +560 -0
- package/dist/{parallel-eternal-engine-CYoTKjsz.d.ts → parallel-eternal-engine-UZg1xOzE.d.ts} +13 -9
- package/dist/{path-resolver-DuhlmPil.d.ts → path-resolver-BaP06Owy.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-D1n-gQI-.d.ts +493 -0
- package/dist/{plan-templates-DbH7lg-t.d.ts → plan-templates-BUVRY0pU.d.ts} +18 -7
- package/dist/{provider-runner-Cocq0O9E.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 +221 -87
- package/dist/sdd/index.js.map +1 -1
- package/dist/{secret-vault-w8MbUe2Q.d.ts → secret-vault-CeVNiy_f.d.ts} +3 -2
- package/dist/security/index.d.ts +5 -4
- package/dist/security/index.js +155 -13
- 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 +150 -12
- package/dist/storage/index.js +1041 -214
- package/dist/storage/index.js.map +1 -1
- package/dist/types/index.d.ts +67 -20
- package/dist/types/index.js +562 -55
- 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 +25 -4
- package/dist/utils/index.js +45 -14
- 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
|
|
@@ -3823,7 +3957,7 @@ Working rules:
|
|
|
3823
3957
|
id: "tech-stack",
|
|
3824
3958
|
name: "Tech Stack Validator",
|
|
3825
3959
|
role: "tech-stack",
|
|
3826
|
-
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json"],
|
|
3960
|
+
tools: ["search", "fetch", "read", "grep", "glob", "outdated", "audit", "json", "mailbox"],
|
|
3827
3961
|
prompt: `You are the Tech Stack Validator \u2014 a single-shot validation agent that fires
|
|
3828
3962
|
before any package, library, or framework choice is committed.
|
|
3829
3963
|
|
|
@@ -3831,6 +3965,16 @@ Your ONLY job: verify that a technology choice is current, real, and not obsolet
|
|
|
3831
3965
|
You are the "this isn't code, this is 10-year-old technology" agent. Intervene
|
|
3832
3966
|
hard when the LLM hallucinates a version number or suggests dead tech.
|
|
3833
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
|
+
|
|
3834
3978
|
## Critical rules
|
|
3835
3979
|
|
|
3836
3980
|
1. **Verify existence.** Search npm registry (fetch https://registry.npmjs.org/<pkg>/latest)
|
|
@@ -3889,11 +4033,11 @@ When APPROVED:
|
|
|
3889
4033
|
**Install**: pnpm add <name>@^<major>.<minor>.0`
|
|
3890
4034
|
},
|
|
3891
4035
|
budget: {
|
|
3892
|
-
timeoutMs:
|
|
3893
|
-
maxIterations:
|
|
3894
|
-
maxToolCalls:
|
|
3895
|
-
maxTokens:
|
|
3896
|
-
maxCostUsd: 0.
|
|
4036
|
+
timeoutMs: 12e4,
|
|
4037
|
+
maxIterations: 10,
|
|
4038
|
+
maxToolCalls: 40,
|
|
4039
|
+
maxTokens: 6e4,
|
|
4040
|
+
maxCostUsd: 0.25
|
|
3897
4041
|
},
|
|
3898
4042
|
capability: {
|
|
3899
4043
|
phase: "meta",
|
|
@@ -5025,12 +5169,12 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5025
5169
|
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
5026
5170
|
return Promise.resolve("stop");
|
|
5027
5171
|
}
|
|
5028
|
-
return new Promise((
|
|
5172
|
+
return new Promise((resolve3) => {
|
|
5029
5173
|
let resolved = false;
|
|
5030
5174
|
const respond = (d) => {
|
|
5031
5175
|
if (resolved) return;
|
|
5032
5176
|
resolved = true;
|
|
5033
|
-
|
|
5177
|
+
resolve3(d);
|
|
5034
5178
|
};
|
|
5035
5179
|
const fallback = setTimeout(
|
|
5036
5180
|
() => respond("stop"),
|
|
@@ -5151,42 +5295,9 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
5151
5295
|
}
|
|
5152
5296
|
};
|
|
5153
5297
|
|
|
5154
|
-
// src/
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
|
|
5158
|
-
PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
|
|
5159
|
-
PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
|
|
5160
|
-
PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
|
|
5161
|
-
PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
|
|
5162
|
-
PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR"};
|
|
5163
|
-
var WrongStackError = class extends Error {
|
|
5164
|
-
code;
|
|
5165
|
-
subsystem;
|
|
5166
|
-
severity;
|
|
5167
|
-
recoverable;
|
|
5168
|
-
context;
|
|
5169
|
-
constructor(opts) {
|
|
5170
|
-
super(opts.message, { cause: opts.cause });
|
|
5171
|
-
this.name = "WrongStackError";
|
|
5172
|
-
this.code = opts.code;
|
|
5173
|
-
this.subsystem = opts.subsystem;
|
|
5174
|
-
this.severity = opts.severity ?? "error";
|
|
5175
|
-
this.recoverable = opts.recoverable ?? false;
|
|
5176
|
-
this.context = opts.context;
|
|
5177
|
-
}
|
|
5178
|
-
/**
|
|
5179
|
-
* Render a one-line user-facing description.
|
|
5180
|
-
* Subclasses should override for domain-specific formatting.
|
|
5181
|
-
*/
|
|
5182
|
-
describe() {
|
|
5183
|
-
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
5184
|
-
return `${this.code}: ${this.message}${ctx}`;
|
|
5185
|
-
}
|
|
5186
|
-
};
|
|
5187
|
-
function formatContext(ctx) {
|
|
5188
|
-
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
5189
|
-
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
5298
|
+
// src/utils/string.ts
|
|
5299
|
+
function truncate(s, max) {
|
|
5300
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
5190
5301
|
}
|
|
5191
5302
|
|
|
5192
5303
|
// src/types/provider.ts
|
|
@@ -5247,9 +5358,6 @@ function describeStatus(status, type) {
|
|
|
5247
5358
|
if (type) return `${type} (${status})`;
|
|
5248
5359
|
return `HTTP ${status}`;
|
|
5249
5360
|
}
|
|
5250
|
-
function truncate(s, n) {
|
|
5251
|
-
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
5252
|
-
}
|
|
5253
5361
|
function providerStatusToCode(status, type) {
|
|
5254
5362
|
if (status === 0) return ERROR_CODES.PROVIDER_NETWORK_ERROR;
|
|
5255
5363
|
if (type === "rate_limit_error" || status === 429) return ERROR_CODES.PROVIDER_RATE_LIMITED;
|
|
@@ -5263,6 +5371,9 @@ function providerStatusToCode(status, type) {
|
|
|
5263
5371
|
|
|
5264
5372
|
// src/coordination/coordinator/error-classifier.ts
|
|
5265
5373
|
function classifySubagentError(err, hints = {}) {
|
|
5374
|
+
if (err instanceof AgentError && err.cause) {
|
|
5375
|
+
return classifySubagentError(err.cause, hints);
|
|
5376
|
+
}
|
|
5266
5377
|
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
5267
5378
|
if (err instanceof ProviderError) {
|
|
5268
5379
|
const baseMessage2 = err.describe();
|
|
@@ -5295,7 +5406,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
5295
5406
|
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
5296
5407
|
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
5297
5408
|
}
|
|
5298
|
-
if (/empty response
|
|
5409
|
+
if (/empty response/i.test(baseMessage)) {
|
|
5299
5410
|
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
5300
5411
|
}
|
|
5301
5412
|
if (/^tool failed: /i.test(baseMessage)) {
|
|
@@ -5980,7 +6091,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
5980
6091
|
taskIds.map((id) => {
|
|
5981
6092
|
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
5982
6093
|
if (cached) return cached;
|
|
5983
|
-
return new Promise((
|
|
6094
|
+
return new Promise((resolve3, reject) => {
|
|
5984
6095
|
const timeout = setTimeout(() => {
|
|
5985
6096
|
this.off("task.completed", handler);
|
|
5986
6097
|
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
@@ -5989,7 +6100,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
5989
6100
|
if (result.taskId === id) {
|
|
5990
6101
|
clearTimeout(timeout);
|
|
5991
6102
|
this.off("task.completed", handler);
|
|
5992
|
-
|
|
6103
|
+
resolve3(result);
|
|
5993
6104
|
}
|
|
5994
6105
|
};
|
|
5995
6106
|
this.on("task.completed", handler);
|
|
@@ -7362,11 +7473,11 @@ var Director = class _Director {
|
|
|
7362
7473
|
if (cached) return cached;
|
|
7363
7474
|
const existing = this.taskWaiters.get(id);
|
|
7364
7475
|
if (existing) return existing.promise;
|
|
7365
|
-
let
|
|
7476
|
+
let resolve3;
|
|
7366
7477
|
const promise = new Promise((res) => {
|
|
7367
|
-
|
|
7478
|
+
resolve3 = res;
|
|
7368
7479
|
});
|
|
7369
|
-
this.taskWaiters.set(id, { promise, resolve:
|
|
7480
|
+
this.taskWaiters.set(id, { promise, resolve: resolve3 });
|
|
7370
7481
|
return promise;
|
|
7371
7482
|
})
|
|
7372
7483
|
);
|
|
@@ -7762,7 +7873,7 @@ function createDelegateTool(opts) {
|
|
|
7762
7873
|
subagentId
|
|
7763
7874
|
});
|
|
7764
7875
|
const dir = director;
|
|
7765
|
-
const result = await new Promise((
|
|
7876
|
+
const result = await new Promise((resolve3) => {
|
|
7766
7877
|
let settled = false;
|
|
7767
7878
|
let timer;
|
|
7768
7879
|
const finish = (value) => {
|
|
@@ -7772,7 +7883,7 @@ function createDelegateTool(opts) {
|
|
|
7772
7883
|
offTool();
|
|
7773
7884
|
offIter();
|
|
7774
7885
|
offProgress();
|
|
7775
|
-
|
|
7886
|
+
resolve3(value);
|
|
7776
7887
|
};
|
|
7777
7888
|
const arm = () => {
|
|
7778
7889
|
if (timer) clearTimeout(timer);
|
|
@@ -8182,21 +8293,40 @@ function makeAgentSubagentRunner(opts) {
|
|
|
8182
8293
|
if (budgetError) throw budgetError;
|
|
8183
8294
|
}
|
|
8184
8295
|
if (result.status === "failed") {
|
|
8185
|
-
throw result.error instanceof
|
|
8296
|
+
throw result.error instanceof AgentError ? result.error : new AgentError({
|
|
8297
|
+
message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
|
|
8298
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
8299
|
+
cause: result.error
|
|
8300
|
+
});
|
|
8186
8301
|
}
|
|
8187
8302
|
if (result.status === "aborted") {
|
|
8188
|
-
throw new
|
|
8303
|
+
throw new AgentError({
|
|
8304
|
+
message: "agent aborted",
|
|
8305
|
+
code: ERROR_CODES.AGENT_ABORTED
|
|
8306
|
+
});
|
|
8189
8307
|
}
|
|
8190
8308
|
if (result.status === "max_iterations") {
|
|
8191
|
-
throw new
|
|
8309
|
+
throw new AgentError({
|
|
8310
|
+
message: "agent exhausted iteration limit",
|
|
8311
|
+
code: ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
8312
|
+
recoverable: true
|
|
8313
|
+
});
|
|
8192
8314
|
}
|
|
8193
8315
|
const usage = ctx.budget.usage();
|
|
8194
8316
|
const finalText = (result.finalText ?? "").trim();
|
|
8195
8317
|
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
8196
|
-
throw new
|
|
8318
|
+
throw new AgentError({
|
|
8319
|
+
message: "empty response \u2014 agent produced no text and no tool calls",
|
|
8320
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
8321
|
+
context: { iterations: result.iterations }
|
|
8322
|
+
});
|
|
8197
8323
|
}
|
|
8198
8324
|
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
8199
|
-
throw new
|
|
8325
|
+
throw new AgentError({
|
|
8326
|
+
message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
|
|
8327
|
+
code: ERROR_CODES.AGENT_RUN_FAILED,
|
|
8328
|
+
context: { tool: lastToolFailed, iterations: result.iterations }
|
|
8329
|
+
});
|
|
8200
8330
|
}
|
|
8201
8331
|
return {
|
|
8202
8332
|
result: result.finalText,
|
|
@@ -8361,7 +8491,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8361
8491
|
onClose: (s) => this.appendToIndex(s)
|
|
8362
8492
|
});
|
|
8363
8493
|
} catch (err) {
|
|
8364
|
-
await handle.close().catch((e) => console.warn(
|
|
8494
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
8495
|
+
level: "warn",
|
|
8496
|
+
event: "session_store.handle_close_failed",
|
|
8497
|
+
message: e instanceof Error ? e.message : String(e),
|
|
8498
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8499
|
+
})));
|
|
8365
8500
|
throw err;
|
|
8366
8501
|
}
|
|
8367
8502
|
}
|
|
@@ -8388,11 +8523,25 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8388
8523
|
provider: data.metadata.provider
|
|
8389
8524
|
},
|
|
8390
8525
|
this.events,
|
|
8391
|
-
{
|
|
8526
|
+
{
|
|
8527
|
+
resumed: true,
|
|
8528
|
+
// Shard directory (sessions/<date>/) — must match create() so the
|
|
8529
|
+
// .summary.json sidecar lands next to the JSONL instead of the
|
|
8530
|
+
// sessions root (where summaryFor() would never find it).
|
|
8531
|
+
dir: path4.dirname(file),
|
|
8532
|
+
filePath: file,
|
|
8533
|
+
secretScrubber: this.secretScrubber,
|
|
8534
|
+
onClose: (s) => this.appendToIndex(s)
|
|
8535
|
+
}
|
|
8392
8536
|
);
|
|
8393
8537
|
return { writer, data };
|
|
8394
8538
|
} catch (err) {
|
|
8395
|
-
await handle.close().catch((e) => console.warn(
|
|
8539
|
+
await handle.close().catch((e) => console.warn(JSON.stringify({
|
|
8540
|
+
level: "warn",
|
|
8541
|
+
event: "session_store.handle_close_failed",
|
|
8542
|
+
message: e instanceof Error ? e.message : String(e),
|
|
8543
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8544
|
+
})));
|
|
8396
8545
|
throw err;
|
|
8397
8546
|
}
|
|
8398
8547
|
}
|
|
@@ -8412,7 +8561,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8412
8561
|
}
|
|
8413
8562
|
const meta = this.metaFromEvents(id, events);
|
|
8414
8563
|
const { messages, usage } = this.replay(events, id);
|
|
8415
|
-
|
|
8564
|
+
const toolCallEnds = extractToolCallEnds(events);
|
|
8565
|
+
return { metadata: meta, events, messages, usage, toolCallEnds };
|
|
8416
8566
|
}
|
|
8417
8567
|
async list(limit = 20) {
|
|
8418
8568
|
try {
|
|
@@ -8568,10 +8718,13 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8568
8718
|
const stat5 = await fsp6.stat(full);
|
|
8569
8719
|
const summary = await this.summarize(id, stat5.mtime.toISOString());
|
|
8570
8720
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
8571
|
-
console.warn(
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8721
|
+
console.warn(JSON.stringify({
|
|
8722
|
+
level: "warn",
|
|
8723
|
+
event: "session_store.manifest_write_failed",
|
|
8724
|
+
sessionId: id,
|
|
8725
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8726
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8727
|
+
}));
|
|
8575
8728
|
});
|
|
8576
8729
|
return summary;
|
|
8577
8730
|
}
|
|
@@ -8579,17 +8732,48 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8579
8732
|
/**
|
|
8580
8733
|
* Delete a session and all associated files: JSONL, summary, plan/todos
|
|
8581
8734
|
* sidecars, and the session directory (fleet.json, shared/, subagents/).
|
|
8735
|
+
*
|
|
8736
|
+
* Individual file deletions are best-effort (logged as structured warnings),
|
|
8737
|
+
* but a tombstone is always written so readIndex() filters this session out.
|
|
8738
|
+
* If the session directory itself can't be removed, the error is surfaced
|
|
8739
|
+
* to the caller so prune() can report it.
|
|
8582
8740
|
*/
|
|
8583
8741
|
async deleteSession(id) {
|
|
8584
|
-
|
|
8585
|
-
|
|
8742
|
+
const jsonlPath = this.sessionPath(id, ".jsonl");
|
|
8743
|
+
const summaryPath = this.sessionPath(id, ".summary.json");
|
|
8586
8744
|
const shardDir = path4.dirname(path4.join(this.dir, id));
|
|
8587
8745
|
const base = path4.basename(id);
|
|
8588
|
-
for (const ext of [".plan.json", ".todos.json"]) {
|
|
8589
|
-
await fsp6.unlink(path4.join(shardDir, `${base}${ext}`)).catch((err) => console.warn(`[session-store] delete ${ext} failed: ${err}`));
|
|
8590
|
-
}
|
|
8591
8746
|
const sessDir = path4.join(shardDir, base);
|
|
8592
|
-
|
|
8747
|
+
const deletions = [
|
|
8748
|
+
fsp6.unlink(jsonlPath),
|
|
8749
|
+
fsp6.unlink(summaryPath),
|
|
8750
|
+
fsp6.unlink(path4.join(shardDir, `${base}.plan.json`)),
|
|
8751
|
+
fsp6.unlink(path4.join(shardDir, `${base}.todos.json`))
|
|
8752
|
+
];
|
|
8753
|
+
const results = await Promise.allSettled(deletions);
|
|
8754
|
+
for (const r of results) {
|
|
8755
|
+
if (r.status === "rejected") {
|
|
8756
|
+
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
8757
|
+
if (r.reason?.code !== "ENOENT") {
|
|
8758
|
+
console.warn(JSON.stringify({
|
|
8759
|
+
level: "warn",
|
|
8760
|
+
event: "session_store.delete_failed",
|
|
8761
|
+
sessionId: id,
|
|
8762
|
+
message: msg,
|
|
8763
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8764
|
+
}));
|
|
8765
|
+
}
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
await fsp6.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
8769
|
+
console.warn(JSON.stringify({
|
|
8770
|
+
level: "warn",
|
|
8771
|
+
event: "session_store.rmdir_failed",
|
|
8772
|
+
sessionId: id,
|
|
8773
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8774
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8775
|
+
}));
|
|
8776
|
+
});
|
|
8593
8777
|
await this.writeTombstone(id);
|
|
8594
8778
|
}
|
|
8595
8779
|
async delete(id) {
|
|
@@ -8605,24 +8789,33 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8605
8789
|
activeSessionId = active.sessionId ?? null;
|
|
8606
8790
|
} catch {
|
|
8607
8791
|
}
|
|
8792
|
+
const isPrunableJsonl = (name) => name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
|
|
8793
|
+
const pruneFile = async (dir, name, prefix) => {
|
|
8794
|
+
const jsonlPath = path4.join(dir, name);
|
|
8795
|
+
try {
|
|
8796
|
+
const stat5 = await fsp6.stat(jsonlPath);
|
|
8797
|
+
if (stat5.mtimeMs >= cutoff) return;
|
|
8798
|
+
} catch {
|
|
8799
|
+
return;
|
|
8800
|
+
}
|
|
8801
|
+
const base = name.replace(/\.jsonl$/, "");
|
|
8802
|
+
const id = prefix ? `${prefix}/${base}` : base;
|
|
8803
|
+
if (activeSessionId && id === activeSessionId) return;
|
|
8804
|
+
await this.deleteSession(id);
|
|
8805
|
+
deleted++;
|
|
8806
|
+
};
|
|
8608
8807
|
const entries = await fsp6.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
8609
8808
|
for (const entry of entries) {
|
|
8809
|
+
if (entry.isFile()) {
|
|
8810
|
+
if (isPrunableJsonl(entry.name)) await pruneFile(this.dir, entry.name, "");
|
|
8811
|
+
continue;
|
|
8812
|
+
}
|
|
8610
8813
|
if (!entry.isDirectory()) continue;
|
|
8611
8814
|
const dateDir = path4.join(this.dir, entry.name);
|
|
8612
8815
|
const files = await fsp6.readdir(dateDir, { withFileTypes: true }).catch(() => []);
|
|
8613
8816
|
for (const file of files) {
|
|
8614
|
-
if (!file.isFile() || !file.name
|
|
8615
|
-
|
|
8616
|
-
try {
|
|
8617
|
-
const stat5 = await fsp6.stat(jsonlPath);
|
|
8618
|
-
if (stat5.mtimeMs >= cutoff) continue;
|
|
8619
|
-
} catch {
|
|
8620
|
-
continue;
|
|
8621
|
-
}
|
|
8622
|
-
const id = `${entry.name}/${file.name.replace(/\.jsonl$/, "")}`;
|
|
8623
|
-
if (activeSessionId && id === activeSessionId) continue;
|
|
8624
|
-
await this.deleteSession(id);
|
|
8625
|
-
deleted++;
|
|
8817
|
+
if (!file.isFile() || !isPrunableJsonl(file.name)) continue;
|
|
8818
|
+
await pruneFile(dateDir, file.name, entry.name);
|
|
8626
8819
|
}
|
|
8627
8820
|
}
|
|
8628
8821
|
if (deleted > 0) {
|
|
@@ -8711,7 +8904,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8711
8904
|
}
|
|
8712
8905
|
metaFromEvents(id, events) {
|
|
8713
8906
|
const start = events.find((e) => e.type === "session_start");
|
|
8714
|
-
const end = events.
|
|
8907
|
+
const end = events.findLast((e) => e.type === "session_end");
|
|
8715
8908
|
return {
|
|
8716
8909
|
id,
|
|
8717
8910
|
startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -8728,9 +8921,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8728
8921
|
for (const e of events) {
|
|
8729
8922
|
if (e.type === "user_input") {
|
|
8730
8923
|
openToolUses.clear();
|
|
8731
|
-
messages.push({ role: "user", content: e.content });
|
|
8924
|
+
messages.push({ role: "user", content: e.content, ts: e.ts });
|
|
8732
8925
|
} else if (e.type === "llm_response") {
|
|
8733
|
-
messages.push({ role: "assistant", content: e.content });
|
|
8926
|
+
messages.push({ role: "assistant", content: e.content, ts: e.ts });
|
|
8734
8927
|
for (const b of e.content) {
|
|
8735
8928
|
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
8736
8929
|
}
|
|
@@ -8749,25 +8942,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8749
8942
|
continue;
|
|
8750
8943
|
}
|
|
8751
8944
|
openToolUses.delete(e.id);
|
|
8752
|
-
const
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
}
|
|
8759
|
-
];
|
|
8945
|
+
const resultBlock = {
|
|
8946
|
+
type: "tool_result",
|
|
8947
|
+
tool_use_id: e.id,
|
|
8948
|
+
content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
|
|
8949
|
+
is_error: e.isError
|
|
8950
|
+
};
|
|
8760
8951
|
const last = messages[messages.length - 1];
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
} else if (typeof last.content === "string") {
|
|
8765
|
-
last.content = [{ type: "text", text: last.content }, ...content];
|
|
8766
|
-
} else {
|
|
8767
|
-
messages.push({ role: "user", content });
|
|
8768
|
-
}
|
|
8952
|
+
const lastIsToolResultUser = last?.role === "user" && Array.isArray(last.content) && last.content.every((b) => b.type === "tool_result");
|
|
8953
|
+
if (lastIsToolResultUser && Array.isArray(last.content)) {
|
|
8954
|
+
last.content.push(resultBlock);
|
|
8769
8955
|
} else {
|
|
8770
|
-
messages.push({ role: "user", content });
|
|
8956
|
+
messages.push({ role: "user", content: [resultBlock], ts: e.ts });
|
|
8771
8957
|
}
|
|
8772
8958
|
}
|
|
8773
8959
|
}
|
|
@@ -8787,7 +8973,24 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
8787
8973
|
return { messages: repaired.messages, usage };
|
|
8788
8974
|
}
|
|
8789
8975
|
};
|
|
8790
|
-
|
|
8976
|
+
function extractToolCallEnds(events) {
|
|
8977
|
+
const result = [];
|
|
8978
|
+
for (const e of events) {
|
|
8979
|
+
if (e.type === "tool_call_end") {
|
|
8980
|
+
result.push({
|
|
8981
|
+
name: e.name,
|
|
8982
|
+
id: e.id,
|
|
8983
|
+
durationMs: e.durationMs,
|
|
8984
|
+
ok: e.ok ?? false,
|
|
8985
|
+
outputBytes: e.outputBytes,
|
|
8986
|
+
outputTokens: e.outputTokens,
|
|
8987
|
+
outputLines: e.outputLines
|
|
8988
|
+
});
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
return result;
|
|
8992
|
+
}
|
|
8993
|
+
var FileSessionWriter = class _FileSessionWriter {
|
|
8791
8994
|
constructor(id, handle, startedAt, meta, events, opts = {}) {
|
|
8792
8995
|
this.id = id;
|
|
8793
8996
|
this.handle = handle;
|
|
@@ -8814,7 +9017,7 @@ var FileSessionWriter = class {
|
|
|
8814
9017
|
meta;
|
|
8815
9018
|
events;
|
|
8816
9019
|
closed = false;
|
|
8817
|
-
|
|
9020
|
+
closePromise = null;
|
|
8818
9021
|
manifestFile;
|
|
8819
9022
|
summary;
|
|
8820
9023
|
tokenIn = 0;
|
|
@@ -8823,12 +9026,51 @@ var FileSessionWriter = class {
|
|
|
8823
9026
|
get transcriptPath() {
|
|
8824
9027
|
return this.filePath || void 0;
|
|
8825
9028
|
}
|
|
8826
|
-
|
|
9029
|
+
/**
|
|
9030
|
+
* Lazy session_start/session_resumed init, shared by all appenders.
|
|
9031
|
+
* A single promise (not a boolean) so a second append racing the first
|
|
9032
|
+
* can't push its event into the buffer BEFORE the first append's event —
|
|
9033
|
+
* every appender awaits the same init and resumes in FIFO call order.
|
|
9034
|
+
*/
|
|
9035
|
+
initPromise = null;
|
|
9036
|
+
ensureInit() {
|
|
9037
|
+
if (!this.initPromise) this.initPromise = this.writeSessionStartLazy();
|
|
9038
|
+
return this.initPromise;
|
|
9039
|
+
}
|
|
8827
9040
|
resumed;
|
|
8828
9041
|
appendFailCount = 0;
|
|
8829
9042
|
lastAppendWarnAt = 0;
|
|
8830
9043
|
secretScrubber;
|
|
8831
9044
|
onCloseCb;
|
|
9045
|
+
// ── Write buffer — batches events to reduce per-event disk I/O ─────────
|
|
9046
|
+
//
|
|
9047
|
+
// Every append() pushes the scrubbed event into an in-memory buffer instead
|
|
9048
|
+
// of calling handle.appendFile() synchronously. The buffer flushes to disk
|
|
9049
|
+
// when it reaches FLUSH_SIZE events OR after FLUSH_INTERVAL_MS of inactivity.
|
|
9050
|
+
// This cuts the number of disk writes by ~95% without changing the on-disk
|
|
9051
|
+
// format — the JSONL is still one JSON object per line.
|
|
9052
|
+
writeBuffer = [];
|
|
9053
|
+
flushTimer = null;
|
|
9054
|
+
static FLUSH_INTERVAL_MS = 500;
|
|
9055
|
+
static FLUSH_SIZE = 50;
|
|
9056
|
+
// ── Write serialization ─────────────────────────────────────────────────
|
|
9057
|
+
//
|
|
9058
|
+
// All disk writes are funneled through a FIFO promise chain. Without it,
|
|
9059
|
+
// a timer-driven flush racing an explicit flush()/close() issues two
|
|
9060
|
+
// concurrent appendFile() calls on the shared O_APPEND handle — the kernel
|
|
9061
|
+
// may complete them out of order (chronology breaks) or, for large
|
|
9062
|
+
// batches, interleave partial writes (torn JSONL lines). The chain keeps
|
|
9063
|
+
// exactly one write in flight; failures don't break the chain.
|
|
9064
|
+
writeChain = Promise.resolve();
|
|
9065
|
+
/** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */
|
|
9066
|
+
enqueueWrite(data) {
|
|
9067
|
+
const write = this.writeChain.then(() => this.handle.appendFile(data, "utf8"));
|
|
9068
|
+
this.writeChain = write.then(
|
|
9069
|
+
() => void 0,
|
|
9070
|
+
() => void 0
|
|
9071
|
+
);
|
|
9072
|
+
return write;
|
|
9073
|
+
}
|
|
8832
9074
|
// ── Enriched summary tracking ──────────────────────────────────────────
|
|
8833
9075
|
iterationCount = 0;
|
|
8834
9076
|
toolCallCount = 0;
|
|
@@ -8878,31 +9120,91 @@ var FileSessionWriter = class {
|
|
|
8878
9120
|
})}
|
|
8879
9121
|
`;
|
|
8880
9122
|
try {
|
|
8881
|
-
|
|
8882
|
-
await fsp6.writeFile(this.filePath, record, { flag: "a", mode: 384 });
|
|
8883
|
-
}
|
|
9123
|
+
await this.enqueueWrite(record);
|
|
8884
9124
|
} catch {
|
|
8885
9125
|
}
|
|
8886
9126
|
}
|
|
8887
9127
|
async append(event) {
|
|
8888
9128
|
if (this.closed) return;
|
|
8889
|
-
|
|
8890
|
-
this.initDone = true;
|
|
8891
|
-
await this.writeSessionStartLazy();
|
|
8892
|
-
}
|
|
9129
|
+
await this.ensureInit();
|
|
8893
9130
|
const scrubbed = this.scrubEvent(event);
|
|
8894
9131
|
this.observeForSummary(scrubbed);
|
|
9132
|
+
this.writeBuffer.push(scrubbed);
|
|
9133
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9134
|
+
if (this.flushTimer) {
|
|
9135
|
+
clearTimeout(this.flushTimer);
|
|
9136
|
+
this.flushTimer = null;
|
|
9137
|
+
}
|
|
9138
|
+
await this.flushBuffer();
|
|
9139
|
+
} else {
|
|
9140
|
+
this.scheduleFlush();
|
|
9141
|
+
}
|
|
9142
|
+
}
|
|
9143
|
+
async appendBatch(events) {
|
|
9144
|
+
if (this.closed || events.length === 0) return;
|
|
9145
|
+
await this.ensureInit();
|
|
9146
|
+
for (const event of events) {
|
|
9147
|
+
const scrubbed = this.scrubEvent(event);
|
|
9148
|
+
this.observeForSummary(scrubbed);
|
|
9149
|
+
this.writeBuffer.push(scrubbed);
|
|
9150
|
+
}
|
|
9151
|
+
if (this.writeBuffer.length >= _FileSessionWriter.FLUSH_SIZE) {
|
|
9152
|
+
if (this.flushTimer) {
|
|
9153
|
+
clearTimeout(this.flushTimer);
|
|
9154
|
+
this.flushTimer = null;
|
|
9155
|
+
}
|
|
9156
|
+
await this.flushBuffer();
|
|
9157
|
+
} else {
|
|
9158
|
+
this.scheduleFlush();
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
9161
|
+
/**
|
|
9162
|
+
* Flush buffered events to disk immediately. Critical events
|
|
9163
|
+
* (user_input, llm_response) call this so they survive SIGKILL/crash
|
|
9164
|
+
* instead of sitting in the in-memory buffer for up to 500ms.
|
|
9165
|
+
*
|
|
9166
|
+
* Idempotent — cancels any pending timer and writes whatever has
|
|
9167
|
+
* accumulated in the buffer. Safe to call even when the buffer
|
|
9168
|
+
* is empty (no-op).
|
|
9169
|
+
*/
|
|
9170
|
+
async flush() {
|
|
9171
|
+
if (this.flushTimer) {
|
|
9172
|
+
clearTimeout(this.flushTimer);
|
|
9173
|
+
this.flushTimer = null;
|
|
9174
|
+
}
|
|
9175
|
+
await this.flushBuffer();
|
|
9176
|
+
}
|
|
9177
|
+
/** Schedule a deferred flush. No-op if a timer is already pending. */
|
|
9178
|
+
scheduleFlush() {
|
|
9179
|
+
if (this.flushTimer) return;
|
|
9180
|
+
this.flushTimer = setTimeout(() => {
|
|
9181
|
+
this.flushTimer = null;
|
|
9182
|
+
this.flushBuffer().catch(() => {
|
|
9183
|
+
});
|
|
9184
|
+
}, _FileSessionWriter.FLUSH_INTERVAL_MS);
|
|
9185
|
+
}
|
|
9186
|
+
/**
|
|
9187
|
+
* Flush all buffered events to disk as a single appendFile call.
|
|
9188
|
+
* Errors use the same throttled-warning pattern the old per-event
|
|
9189
|
+
* append path used — one warning every 5s with a suppressed count.
|
|
9190
|
+
* On failure the buffer is cleared (events are best-effort, same as
|
|
9191
|
+
* the old per-event path where a failed write was silently dropped).
|
|
9192
|
+
*/
|
|
9193
|
+
async flushBuffer() {
|
|
9194
|
+
if (this.writeBuffer.length === 0) return;
|
|
9195
|
+
const eventCount = this.writeBuffer.length;
|
|
9196
|
+
const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
9197
|
+
this.writeBuffer = [];
|
|
8895
9198
|
try {
|
|
8896
|
-
await this.
|
|
8897
|
-
`, "utf8");
|
|
9199
|
+
await this.enqueueWrite(batch);
|
|
8898
9200
|
} catch (err) {
|
|
8899
|
-
this.appendFailCount
|
|
9201
|
+
this.appendFailCount += eventCount;
|
|
8900
9202
|
const now = Date.now();
|
|
8901
9203
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
8902
9204
|
const suppressed = this.appendFailCount - 1;
|
|
8903
9205
|
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
8904
9206
|
console.warn(
|
|
8905
|
-
"[session]
|
|
9207
|
+
"[session] flush failed:",
|
|
8906
9208
|
err instanceof Error ? err.message : String(err),
|
|
8907
9209
|
tail
|
|
8908
9210
|
);
|
|
@@ -8912,6 +9214,11 @@ var FileSessionWriter = class {
|
|
|
8912
9214
|
}
|
|
8913
9215
|
}
|
|
8914
9216
|
observeForSummary(event) {
|
|
9217
|
+
if (event.type === "llm_response") {
|
|
9218
|
+
for (const block of event.content) {
|
|
9219
|
+
if (block.type === "tool_use") this.openToolUses.add(block.id);
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
8915
9222
|
if (event.type === "tool_use") {
|
|
8916
9223
|
this.openToolUses.add(event.id);
|
|
8917
9224
|
} else if (event.type === "tool_call_start") {
|
|
@@ -8945,9 +9252,18 @@ var FileSessionWriter = class {
|
|
|
8945
9252
|
}
|
|
8946
9253
|
}
|
|
8947
9254
|
async close() {
|
|
8948
|
-
if (this.
|
|
8949
|
-
this.
|
|
9255
|
+
if (this.closePromise) return this.closePromise;
|
|
9256
|
+
this.closePromise = this.doClose();
|
|
9257
|
+
return this.closePromise;
|
|
9258
|
+
}
|
|
9259
|
+
async doClose() {
|
|
8950
9260
|
this.closed = true;
|
|
9261
|
+
if (this.flushTimer) {
|
|
9262
|
+
clearTimeout(this.flushTimer);
|
|
9263
|
+
this.flushTimer = null;
|
|
9264
|
+
}
|
|
9265
|
+
await this.flushBuffer();
|
|
9266
|
+
await this.writeChain;
|
|
8951
9267
|
this.summary = {
|
|
8952
9268
|
...this.summary,
|
|
8953
9269
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -9003,6 +9319,12 @@ var FileSessionWriter = class {
|
|
|
9003
9319
|
}
|
|
9004
9320
|
async truncateToCheckpoint(targetPromptIndex) {
|
|
9005
9321
|
if (!this.filePath) return 0;
|
|
9322
|
+
if (this.flushTimer) {
|
|
9323
|
+
clearTimeout(this.flushTimer);
|
|
9324
|
+
this.flushTimer = null;
|
|
9325
|
+
}
|
|
9326
|
+
await this.flushBuffer();
|
|
9327
|
+
await this.writeChain;
|
|
9006
9328
|
const raw = await fsp6.readFile(this.filePath, "utf8");
|
|
9007
9329
|
const lines = raw.split("\n");
|
|
9008
9330
|
const kept = [];
|
|
@@ -9065,6 +9387,12 @@ var FileSessionWriter = class {
|
|
|
9065
9387
|
}
|
|
9066
9388
|
async clearSession() {
|
|
9067
9389
|
if (!this.filePath) return;
|
|
9390
|
+
if (this.flushTimer) {
|
|
9391
|
+
clearTimeout(this.flushTimer);
|
|
9392
|
+
this.flushTimer = null;
|
|
9393
|
+
}
|
|
9394
|
+
this.writeBuffer = [];
|
|
9395
|
+
await this.writeChain;
|
|
9068
9396
|
const record = `${JSON.stringify({
|
|
9069
9397
|
type: "session_start",
|
|
9070
9398
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -9563,6 +9891,1518 @@ var FleetManager = class {
|
|
|
9563
9891
|
}
|
|
9564
9892
|
};
|
|
9565
9893
|
|
|
9566
|
-
|
|
9894
|
+
// src/coordination/mailbox-types.ts
|
|
9895
|
+
function normalizeRecipient(to) {
|
|
9896
|
+
return to.trim().toLowerCase() === "all" ? "*" : to.trim();
|
|
9897
|
+
}
|
|
9898
|
+
|
|
9899
|
+
// src/coordination/mailbox.ts
|
|
9900
|
+
var MAILBOX_FILE = "_mailbox.jsonl";
|
|
9901
|
+
var LINE_SEPARATOR = "\n";
|
|
9902
|
+
var DefaultMailbox = class {
|
|
9903
|
+
filePath;
|
|
9904
|
+
constructor(sessionDir) {
|
|
9905
|
+
this.filePath = path4.join(sessionDir, MAILBOX_FILE);
|
|
9906
|
+
}
|
|
9907
|
+
get mailboxPath() {
|
|
9908
|
+
return this.filePath;
|
|
9909
|
+
}
|
|
9910
|
+
// ── Send ──────────────────────────────────────────────────────────────
|
|
9911
|
+
async send(input) {
|
|
9912
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9913
|
+
const msg = {
|
|
9914
|
+
id: randomUUID(),
|
|
9915
|
+
from: input.from,
|
|
9916
|
+
// "all" is an accepted spelling of the broadcast address — canonical
|
|
9917
|
+
// form on disk is '*' so every query/checker matches it.
|
|
9918
|
+
to: normalizeRecipient(input.to),
|
|
9919
|
+
type: input.type,
|
|
9920
|
+
subject: input.subject,
|
|
9921
|
+
body: input.body,
|
|
9922
|
+
priority: input.priority ?? "normal",
|
|
9923
|
+
readBy: {},
|
|
9924
|
+
completed: false,
|
|
9925
|
+
timestamp: now,
|
|
9926
|
+
replyTo: input.replyTo,
|
|
9927
|
+
taskContext: input.taskContext
|
|
9928
|
+
};
|
|
9929
|
+
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
9930
|
+
await fsp6.mkdir(path4.dirname(this.filePath), { recursive: true });
|
|
9931
|
+
await withFileLock(this.filePath, async () => {
|
|
9932
|
+
await fsp6.appendFile(this.filePath, line, "utf8");
|
|
9933
|
+
});
|
|
9934
|
+
return msg;
|
|
9935
|
+
}
|
|
9936
|
+
// ── Query ─────────────────────────────────────────────────────────────
|
|
9937
|
+
async query(q) {
|
|
9938
|
+
const all = await this._readAll();
|
|
9939
|
+
const limit = q.limit ?? 50;
|
|
9940
|
+
let filtered = all;
|
|
9941
|
+
if (q.to !== void 0) {
|
|
9942
|
+
filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
|
|
9943
|
+
}
|
|
9944
|
+
if (q.from !== void 0) {
|
|
9945
|
+
filtered = filtered.filter((m) => m.from === q.from);
|
|
9946
|
+
}
|
|
9947
|
+
if (q.unreadBy !== void 0) {
|
|
9948
|
+
filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
|
|
9949
|
+
}
|
|
9950
|
+
if (q.incompleteOnly) {
|
|
9951
|
+
filtered = filtered.filter((m) => !m.completed);
|
|
9952
|
+
}
|
|
9953
|
+
if (q.type !== void 0) {
|
|
9954
|
+
filtered = filtered.filter((m) => m.type === q.type);
|
|
9955
|
+
}
|
|
9956
|
+
if (q.minPriority !== void 0) {
|
|
9957
|
+
const order = { low: 0, normal: 1, high: 2 };
|
|
9958
|
+
const min = order[q.minPriority];
|
|
9959
|
+
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
9960
|
+
}
|
|
9961
|
+
if (q.since !== void 0) {
|
|
9962
|
+
const since = q.since;
|
|
9963
|
+
filtered = filtered.filter((m) => m.timestamp > since);
|
|
9964
|
+
}
|
|
9965
|
+
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
9966
|
+
return filtered.slice(0, limit);
|
|
9967
|
+
}
|
|
9968
|
+
// ── Ack ───────────────────────────────────────────────────────────────
|
|
9969
|
+
async ack(input) {
|
|
9970
|
+
let result = null;
|
|
9971
|
+
await withFileLock(this.filePath, async () => {
|
|
9972
|
+
const all = await this._readAll();
|
|
9973
|
+
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
9974
|
+
if (idx === -1) return;
|
|
9975
|
+
const msg = all[idx];
|
|
9976
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9977
|
+
if (input.read !== false) {
|
|
9978
|
+
msg.readBy[input.readerId] = now;
|
|
9979
|
+
}
|
|
9980
|
+
if (input.completed) {
|
|
9981
|
+
msg.completed = true;
|
|
9982
|
+
msg.completedBy = input.readerId;
|
|
9983
|
+
msg.completedAt = now;
|
|
9984
|
+
}
|
|
9985
|
+
if (input.outcome !== void 0) {
|
|
9986
|
+
msg.outcome = input.outcome;
|
|
9987
|
+
}
|
|
9988
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
9989
|
+
await fsp6.writeFile(this.filePath, serialized, "utf8");
|
|
9990
|
+
result = msg;
|
|
9991
|
+
});
|
|
9992
|
+
return result;
|
|
9993
|
+
}
|
|
9994
|
+
// ── Agent statuses ────────────────────────────────────────────────────
|
|
9995
|
+
async getAgentStatuses() {
|
|
9996
|
+
const all = await this._readAll();
|
|
9997
|
+
const latest = /* @__PURE__ */ new Map();
|
|
9998
|
+
for (const m of all) {
|
|
9999
|
+
if (m.type !== "status") continue;
|
|
10000
|
+
const existing = latest.get(m.from);
|
|
10001
|
+
if (existing && m.timestamp <= existing.lastActivityAt) continue;
|
|
10002
|
+
latest.set(m.from, {
|
|
10003
|
+
agentId: m.from,
|
|
10004
|
+
name: m.taskContext?.agentName ?? m.from,
|
|
10005
|
+
role: m.taskContext?.agentRole,
|
|
10006
|
+
sessionId: m.senderSessionId ?? "?",
|
|
10007
|
+
status: m.taskContext?.status ?? "idle",
|
|
10008
|
+
currentTool: void 0,
|
|
10009
|
+
currentTask: m.subject,
|
|
10010
|
+
iterations: 0,
|
|
10011
|
+
toolCalls: 0,
|
|
10012
|
+
lastActivityAt: m.timestamp,
|
|
10013
|
+
lastSeenAt: m.timestamp,
|
|
10014
|
+
online: true,
|
|
10015
|
+
pid: 0,
|
|
10016
|
+
source: void 0
|
|
10017
|
+
});
|
|
10018
|
+
}
|
|
10019
|
+
return Array.from(latest.values()).sort(
|
|
10020
|
+
(a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt)
|
|
10021
|
+
);
|
|
10022
|
+
}
|
|
10023
|
+
// ── Stubs for cross-session features (not applicable per-session) ─────
|
|
10024
|
+
async getOnlineAgents() {
|
|
10025
|
+
return this.getAgentStatuses();
|
|
10026
|
+
}
|
|
10027
|
+
async registerAgent(_input) {
|
|
10028
|
+
}
|
|
10029
|
+
async heartbeat(_input) {
|
|
10030
|
+
}
|
|
10031
|
+
async unreadCount(forAgentId) {
|
|
10032
|
+
const all = await this._readAll();
|
|
10033
|
+
return all.filter(
|
|
10034
|
+
(m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
|
|
10035
|
+
).length;
|
|
10036
|
+
}
|
|
10037
|
+
async close() {
|
|
10038
|
+
}
|
|
10039
|
+
async clearAll() {
|
|
10040
|
+
await withFileLock(this.filePath, async () => {
|
|
10041
|
+
await fsp6.writeFile(this.filePath, "", "utf8");
|
|
10042
|
+
});
|
|
10043
|
+
}
|
|
10044
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
10045
|
+
async _readAll() {
|
|
10046
|
+
try {
|
|
10047
|
+
const raw = await fsp6.readFile(this.filePath, "utf8");
|
|
10048
|
+
const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
10049
|
+
const messages = [];
|
|
10050
|
+
for (const line of lines) {
|
|
10051
|
+
try {
|
|
10052
|
+
const parsed = JSON.parse(line);
|
|
10053
|
+
if (!parsed["readBy"]) {
|
|
10054
|
+
const readBy = {};
|
|
10055
|
+
if (parsed["read"] && parsed["readAt"]) {
|
|
10056
|
+
readBy[parsed["to"] ?? "unknown"] = parsed["readAt"];
|
|
10057
|
+
}
|
|
10058
|
+
parsed["readBy"] = readBy;
|
|
10059
|
+
delete parsed["read"];
|
|
10060
|
+
delete parsed["readAt"];
|
|
10061
|
+
}
|
|
10062
|
+
messages.push(parsed);
|
|
10063
|
+
} catch {
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
return messages;
|
|
10067
|
+
} catch (err) {
|
|
10068
|
+
if (err.code === "ENOENT") return [];
|
|
10069
|
+
throw err;
|
|
10070
|
+
}
|
|
10071
|
+
}
|
|
10072
|
+
};
|
|
10073
|
+
var BrainMonitor = class {
|
|
10074
|
+
constructor(opts) {
|
|
10075
|
+
this.opts = opts;
|
|
10076
|
+
this.toolFailureStreak = opts.toolFailureStreak ?? 3;
|
|
10077
|
+
this.errorStormCount = opts.errorStormCount ?? 4;
|
|
10078
|
+
this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
|
|
10079
|
+
this.cooldownMs = opts.cooldownMs ?? 12e4;
|
|
10080
|
+
}
|
|
10081
|
+
opts;
|
|
10082
|
+
failStreaks = /* @__PURE__ */ new Map();
|
|
10083
|
+
errorTimestamps = [];
|
|
10084
|
+
lastEngagedAt = /* @__PURE__ */ new Map();
|
|
10085
|
+
unsubscribers = [];
|
|
10086
|
+
engaging = false;
|
|
10087
|
+
toolFailureStreak;
|
|
10088
|
+
errorStormCount;
|
|
10089
|
+
errorStormWindowMs;
|
|
10090
|
+
cooldownMs;
|
|
10091
|
+
start() {
|
|
10092
|
+
this.unsubscribers.push(
|
|
10093
|
+
this.opts.events.on("tool.executed", (e) => {
|
|
10094
|
+
if (e.ok) {
|
|
10095
|
+
this.failStreaks.delete(e.name);
|
|
10096
|
+
return;
|
|
10097
|
+
}
|
|
10098
|
+
const streak = (this.failStreaks.get(e.name) ?? 0) + 1;
|
|
10099
|
+
this.failStreaks.set(e.name, streak);
|
|
10100
|
+
if (streak >= this.toolFailureStreak) {
|
|
10101
|
+
this.failStreaks.delete(e.name);
|
|
10102
|
+
void this.engage("tool_failure_streak", {
|
|
10103
|
+
question: `The tool "${e.name}" has failed ${streak} times in a row. Should the agent be steered to a different approach?`,
|
|
10104
|
+
context: [
|
|
10105
|
+
`Tool: ${e.name}`,
|
|
10106
|
+
`Consecutive failures: ${streak}`,
|
|
10107
|
+
e.output ? `Last output (truncated): ${String(e.output).slice(0, 400)}` : ""
|
|
10108
|
+
].filter(Boolean).join("\n")
|
|
10109
|
+
});
|
|
10110
|
+
}
|
|
10111
|
+
})
|
|
10112
|
+
);
|
|
10113
|
+
this.unsubscribers.push(
|
|
10114
|
+
this.opts.events.on("error", (e) => {
|
|
10115
|
+
const now = Date.now();
|
|
10116
|
+
this.errorTimestamps.push(now);
|
|
10117
|
+
this.errorTimestamps = this.errorTimestamps.filter(
|
|
10118
|
+
(t) => now - t <= this.errorStormWindowMs
|
|
10119
|
+
);
|
|
10120
|
+
if (this.errorTimestamps.length >= this.errorStormCount) {
|
|
10121
|
+
const count = this.errorTimestamps.length;
|
|
10122
|
+
this.errorTimestamps = [];
|
|
10123
|
+
const message = e.err instanceof Error ? e.err.message : String(e.err);
|
|
10124
|
+
void this.engage("error_storm", {
|
|
10125
|
+
question: `${count} errors occurred within ${Math.round(this.errorStormWindowMs / 1e3)}s (phase: ${e.phase}). Should the agent be steered before more work is wasted?`,
|
|
10126
|
+
context: `Latest error: ${message.slice(0, 400)}`
|
|
10127
|
+
});
|
|
10128
|
+
}
|
|
10129
|
+
})
|
|
10130
|
+
);
|
|
10131
|
+
}
|
|
10132
|
+
stop() {
|
|
10133
|
+
for (const off of this.unsubscribers) off();
|
|
10134
|
+
this.unsubscribers.length = 0;
|
|
10135
|
+
this.failStreaks.clear();
|
|
10136
|
+
this.errorTimestamps = [];
|
|
10137
|
+
}
|
|
10138
|
+
async engage(kind, input) {
|
|
10139
|
+
const last = this.lastEngagedAt.get(kind) ?? 0;
|
|
10140
|
+
if (this.engaging || Date.now() - last < this.cooldownMs) return;
|
|
10141
|
+
this.engaging = true;
|
|
10142
|
+
this.lastEngagedAt.set(kind, Date.now());
|
|
10143
|
+
try {
|
|
10144
|
+
const request = {
|
|
10145
|
+
id: `brainmon-${randomUUID()}`,
|
|
10146
|
+
source: "system",
|
|
10147
|
+
question: input.question,
|
|
10148
|
+
context: input.context,
|
|
10149
|
+
options: [
|
|
10150
|
+
{
|
|
10151
|
+
id: "steer",
|
|
10152
|
+
label: "Steer the agent with corrective guidance",
|
|
10153
|
+
consequence: "A steer message is injected before its next step.",
|
|
10154
|
+
risk: "low"
|
|
10155
|
+
},
|
|
10156
|
+
{
|
|
10157
|
+
id: "continue",
|
|
10158
|
+
label: "Let the agent continue unaided",
|
|
10159
|
+
risk: "low"
|
|
10160
|
+
}
|
|
10161
|
+
],
|
|
10162
|
+
risk: "medium",
|
|
10163
|
+
// Without an LLM layer the policy brain resolves this fallback to
|
|
10164
|
+
// "continue" — the monitor observes but never interferes.
|
|
10165
|
+
fallback: "continue"
|
|
10166
|
+
};
|
|
10167
|
+
const decision = await this.opts.brain.decide(request);
|
|
10168
|
+
const intervened = await this.maybeIntervene(kind, request, decision);
|
|
10169
|
+
this.opts.events.emit("brain.intervention", {
|
|
10170
|
+
kind,
|
|
10171
|
+
request,
|
|
10172
|
+
decision,
|
|
10173
|
+
intervened,
|
|
10174
|
+
at: Date.now()
|
|
10175
|
+
});
|
|
10176
|
+
} catch {
|
|
10177
|
+
} finally {
|
|
10178
|
+
this.engaging = false;
|
|
10179
|
+
}
|
|
10180
|
+
}
|
|
10181
|
+
async maybeIntervene(kind, request, decision) {
|
|
10182
|
+
if (decision.type !== "answer") return false;
|
|
10183
|
+
const choseSteer = decision.optionId === "steer";
|
|
10184
|
+
const freeTextGuidance = !decision.optionId && !/^continue\b/i.test(decision.text.trim()) && decision.text.trim().length > 0;
|
|
10185
|
+
if (!choseSteer && !freeTextGuidance) return false;
|
|
10186
|
+
const guidance = decision.rationale?.trim() || decision.text.trim();
|
|
10187
|
+
try {
|
|
10188
|
+
await this.opts.intervene({
|
|
10189
|
+
subject: `Brain intervention: ${kind.replace(/_/g, " ")}`,
|
|
10190
|
+
body: [
|
|
10191
|
+
`The Brain engaged after detecting: ${request.question}`,
|
|
10192
|
+
"",
|
|
10193
|
+
`Guidance: ${guidance}`,
|
|
10194
|
+
"",
|
|
10195
|
+
"Adjust your approach accordingly \u2014 do not simply retry the same action."
|
|
10196
|
+
].join("\n")
|
|
10197
|
+
});
|
|
10198
|
+
return true;
|
|
10199
|
+
} catch {
|
|
10200
|
+
return false;
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
};
|
|
10204
|
+
function projectSlug(absRoot) {
|
|
10205
|
+
const base = slugify(path4.basename(absRoot));
|
|
10206
|
+
const hash = createHash("sha256").update(path4.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
10207
|
+
return `${base}-${hash}`;
|
|
10208
|
+
}
|
|
10209
|
+
function slugify(name) {
|
|
10210
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
10211
|
+
}
|
|
10212
|
+
function wstackGlobalRoot() {
|
|
10213
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
10214
|
+
if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
|
|
10215
|
+
return path4.join(os.homedir(), ".wrongstack");
|
|
10216
|
+
}
|
|
10217
|
+
|
|
10218
|
+
// src/coordination/global-mailbox.ts
|
|
10219
|
+
var MAILBOX_FILE2 = "_mailbox.jsonl";
|
|
10220
|
+
var AGENT_STALE_MS = 6e4;
|
|
10221
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
10222
|
+
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
10223
|
+
var LINE_SEPARATOR2 = "\n";
|
|
10224
|
+
function resolveProjectDir(projectRoot, globalRoot) {
|
|
10225
|
+
return path4.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
10226
|
+
}
|
|
10227
|
+
var GlobalMailbox = class {
|
|
10228
|
+
/** Path to the JSONL message file. */
|
|
10229
|
+
messagePath;
|
|
10230
|
+
/** Path to the JSON agent registry file. */
|
|
10231
|
+
registryPath;
|
|
10232
|
+
/** Optional event bus for emitting agent registration/heartbeat events. */
|
|
10233
|
+
_events;
|
|
10234
|
+
/**
|
|
10235
|
+
* Local cache of the agent registry to avoid re-reading on every call.
|
|
10236
|
+
* Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
|
|
10237
|
+
* whole point of GlobalMailbox), so a cache served forever would never see
|
|
10238
|
+
* agents registered by other sessions. Writers always bypass it.
|
|
10239
|
+
*/
|
|
10240
|
+
_registryCache = null;
|
|
10241
|
+
/** When the registry cache was last refreshed from disk (epoch ms). */
|
|
10242
|
+
_registryCacheAt = 0;
|
|
10243
|
+
/** Last time each local agent sent a heartbeat (throttle). */
|
|
10244
|
+
_lastHeartbeat = /* @__PURE__ */ new Map();
|
|
10245
|
+
/**
|
|
10246
|
+
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
10247
|
+
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
10248
|
+
*/
|
|
10249
|
+
constructor(projectDir, events) {
|
|
10250
|
+
this.messagePath = path4.join(projectDir, MAILBOX_FILE2);
|
|
10251
|
+
this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
|
|
10252
|
+
this._events = events;
|
|
10253
|
+
}
|
|
10254
|
+
// ── Messages ────────────────────────────────────────────────────────────
|
|
10255
|
+
async send(input) {
|
|
10256
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10257
|
+
const msg = {
|
|
10258
|
+
id: randomUUID(),
|
|
10259
|
+
from: input.from,
|
|
10260
|
+
// "all" is an accepted spelling of the broadcast address — canonical
|
|
10261
|
+
// form on disk is '*' so every query/checker matches it.
|
|
10262
|
+
to: normalizeRecipient(input.to),
|
|
10263
|
+
type: input.type,
|
|
10264
|
+
subject: input.subject,
|
|
10265
|
+
body: input.body,
|
|
10266
|
+
priority: input.priority ?? "normal",
|
|
10267
|
+
readBy: {},
|
|
10268
|
+
completed: false,
|
|
10269
|
+
timestamp: now,
|
|
10270
|
+
replyTo: input.replyTo,
|
|
10271
|
+
taskContext: input.taskContext
|
|
10272
|
+
};
|
|
10273
|
+
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
10274
|
+
await fsp6.mkdir(path4.dirname(this.messagePath), { recursive: true });
|
|
10275
|
+
await withFileLock(this.messagePath, async () => {
|
|
10276
|
+
await fsp6.appendFile(this.messagePath, line, "utf8");
|
|
10277
|
+
});
|
|
10278
|
+
return msg;
|
|
10279
|
+
}
|
|
10280
|
+
async query(q) {
|
|
10281
|
+
const all = await this._readMessages();
|
|
10282
|
+
const limit = q.limit ?? 50;
|
|
10283
|
+
let filtered = all;
|
|
10284
|
+
if (q.to !== void 0) {
|
|
10285
|
+
filtered = filtered.filter((m) => m.to === q.to || m.to === "*");
|
|
10286
|
+
}
|
|
10287
|
+
if (q.from !== void 0) {
|
|
10288
|
+
filtered = filtered.filter((m) => m.from === q.from);
|
|
10289
|
+
}
|
|
10290
|
+
if (q.unreadBy !== void 0) {
|
|
10291
|
+
filtered = filtered.filter((m) => !(q.unreadBy in m.readBy));
|
|
10292
|
+
}
|
|
10293
|
+
if (q.incompleteOnly) {
|
|
10294
|
+
filtered = filtered.filter((m) => !m.completed);
|
|
10295
|
+
}
|
|
10296
|
+
if (q.type !== void 0) {
|
|
10297
|
+
filtered = filtered.filter((m) => m.type === q.type);
|
|
10298
|
+
}
|
|
10299
|
+
if (q.minPriority !== void 0) {
|
|
10300
|
+
const order = { low: 0, normal: 1, high: 2 };
|
|
10301
|
+
const min = order[q.minPriority];
|
|
10302
|
+
filtered = filtered.filter((m) => (order[m.priority] ?? 1) >= min);
|
|
10303
|
+
}
|
|
10304
|
+
if (q.since !== void 0) {
|
|
10305
|
+
const since = q.since;
|
|
10306
|
+
filtered = filtered.filter((m) => m.timestamp > since);
|
|
10307
|
+
}
|
|
10308
|
+
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
10309
|
+
return filtered.slice(0, limit);
|
|
10310
|
+
}
|
|
10311
|
+
async ack(input) {
|
|
10312
|
+
let result = null;
|
|
10313
|
+
await withFileLock(this.messagePath, async () => {
|
|
10314
|
+
const all = await this._readMessages();
|
|
10315
|
+
const idx = all.findIndex((m) => m.id === input.messageId);
|
|
10316
|
+
if (idx === -1) return;
|
|
10317
|
+
const msg = all[idx];
|
|
10318
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10319
|
+
if (input.read !== false) {
|
|
10320
|
+
msg.readBy[input.readerId] = now;
|
|
10321
|
+
}
|
|
10322
|
+
if (input.completed) {
|
|
10323
|
+
msg.completed = true;
|
|
10324
|
+
msg.completedBy = input.readerId;
|
|
10325
|
+
msg.completedAt = now;
|
|
10326
|
+
}
|
|
10327
|
+
if (input.outcome !== void 0) {
|
|
10328
|
+
msg.outcome = input.outcome;
|
|
10329
|
+
}
|
|
10330
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR2) + LINE_SEPARATOR2;
|
|
10331
|
+
await fsp6.writeFile(this.messagePath, serialized, "utf8");
|
|
10332
|
+
result = msg;
|
|
10333
|
+
});
|
|
10334
|
+
return result;
|
|
10335
|
+
}
|
|
10336
|
+
async unreadCount(forAgentId) {
|
|
10337
|
+
const all = await this._readMessages();
|
|
10338
|
+
return all.filter(
|
|
10339
|
+
(m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
|
|
10340
|
+
).length;
|
|
10341
|
+
}
|
|
10342
|
+
// ── Agent registry ──────────────────────────────────────────────────────
|
|
10343
|
+
async registerAgent(input) {
|
|
10344
|
+
await this._ensureRegistry();
|
|
10345
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10346
|
+
const agent = {
|
|
10347
|
+
agentId: input.agentId,
|
|
10348
|
+
sessionId: input.sessionId,
|
|
10349
|
+
name: input.name,
|
|
10350
|
+
role: input.role,
|
|
10351
|
+
status: "idle",
|
|
10352
|
+
currentTool: void 0,
|
|
10353
|
+
currentTask: void 0,
|
|
10354
|
+
iterations: 0,
|
|
10355
|
+
toolCalls: 0,
|
|
10356
|
+
registeredAt: now,
|
|
10357
|
+
lastSeenAt: now,
|
|
10358
|
+
pid: input.pid,
|
|
10359
|
+
source: input.source
|
|
10360
|
+
};
|
|
10361
|
+
await withFileLock(this.registryPath, async () => {
|
|
10362
|
+
const registry = await this._readRegistry({ fresh: true });
|
|
10363
|
+
this._pruneStaleInPlace(registry);
|
|
10364
|
+
registry.set(input.agentId, agent);
|
|
10365
|
+
this._registryCache = registry;
|
|
10366
|
+
this._registryCacheAt = Date.now();
|
|
10367
|
+
await this._writeRegistry(registry);
|
|
10368
|
+
});
|
|
10369
|
+
this._events?.emitCustom("mailbox.agent_registered", {
|
|
10370
|
+
agentId: input.agentId,
|
|
10371
|
+
sessionId: input.sessionId,
|
|
10372
|
+
name: input.name,
|
|
10373
|
+
role: input.role,
|
|
10374
|
+
source: input.source
|
|
10375
|
+
});
|
|
10376
|
+
}
|
|
10377
|
+
async heartbeat(input) {
|
|
10378
|
+
const last = this._lastHeartbeat.get(input.agentId) ?? 0;
|
|
10379
|
+
const now = Date.now();
|
|
10380
|
+
if (now - last < HEARTBEAT_THROTTLE_MS) return;
|
|
10381
|
+
this._lastHeartbeat.set(input.agentId, now);
|
|
10382
|
+
await this._ensureRegistry();
|
|
10383
|
+
await withFileLock(this.registryPath, async () => {
|
|
10384
|
+
const registry = await this._readRegistry({ fresh: true });
|
|
10385
|
+
this._pruneStaleInPlace(registry);
|
|
10386
|
+
const agent = registry.get(input.agentId);
|
|
10387
|
+
if (agent) {
|
|
10388
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
10389
|
+
agent.lastSeenAt = iso;
|
|
10390
|
+
if (input.status !== void 0) agent.status = input.status;
|
|
10391
|
+
if (input.currentTool !== void 0) agent.currentTool = input.currentTool;
|
|
10392
|
+
if (input.currentTask !== void 0) agent.currentTask = input.currentTask;
|
|
10393
|
+
if (input.iterations !== void 0) agent.iterations = input.iterations;
|
|
10394
|
+
if (input.toolCalls !== void 0) agent.toolCalls = input.toolCalls;
|
|
10395
|
+
}
|
|
10396
|
+
this._registryCache = registry;
|
|
10397
|
+
this._registryCacheAt = Date.now();
|
|
10398
|
+
await this._writeRegistry(registry);
|
|
10399
|
+
});
|
|
10400
|
+
this._events?.emitCustom("mailbox.agent_heartbeat", {
|
|
10401
|
+
agentId: input.agentId,
|
|
10402
|
+
status: input.status,
|
|
10403
|
+
currentTool: input.currentTool,
|
|
10404
|
+
currentTask: input.currentTask
|
|
10405
|
+
});
|
|
10406
|
+
}
|
|
10407
|
+
async getAgentStatuses() {
|
|
10408
|
+
await this._ensureRegistry();
|
|
10409
|
+
const registry = await this._readRegistry();
|
|
10410
|
+
this._pruneStaleInPlace(registry);
|
|
10411
|
+
const now = Date.now();
|
|
10412
|
+
return Array.from(registry.values()).map((a) => ({
|
|
10413
|
+
agentId: a.agentId,
|
|
10414
|
+
name: a.name,
|
|
10415
|
+
role: a.role,
|
|
10416
|
+
sessionId: a.sessionId,
|
|
10417
|
+
status: a.status,
|
|
10418
|
+
currentTool: a.currentTool,
|
|
10419
|
+
currentTask: a.currentTask,
|
|
10420
|
+
iterations: a.iterations,
|
|
10421
|
+
toolCalls: a.toolCalls,
|
|
10422
|
+
lastActivityAt: a.lastSeenAt,
|
|
10423
|
+
lastSeenAt: a.lastSeenAt,
|
|
10424
|
+
online: now - new Date(a.lastSeenAt).getTime() < AGENT_STALE_MS,
|
|
10425
|
+
pid: a.pid,
|
|
10426
|
+
source: a.source
|
|
10427
|
+
})).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
10428
|
+
}
|
|
10429
|
+
async getOnlineAgents() {
|
|
10430
|
+
const all = await this.getAgentStatuses();
|
|
10431
|
+
return all.filter((a) => a.online);
|
|
10432
|
+
}
|
|
10433
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
10434
|
+
async close() {
|
|
10435
|
+
this._registryCache = null;
|
|
10436
|
+
}
|
|
10437
|
+
async clearAll() {
|
|
10438
|
+
await withFileLock(this.messagePath, async () => {
|
|
10439
|
+
await fsp6.writeFile(this.messagePath, "", "utf8");
|
|
10440
|
+
});
|
|
10441
|
+
}
|
|
10442
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
10443
|
+
async _readMessages() {
|
|
10444
|
+
try {
|
|
10445
|
+
const raw = await fsp6.readFile(this.messagePath, "utf8");
|
|
10446
|
+
const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
|
|
10447
|
+
const messages = [];
|
|
10448
|
+
for (const line of lines) {
|
|
10449
|
+
try {
|
|
10450
|
+
const parsed = JSON.parse(line);
|
|
10451
|
+
if (!parsed["readBy"]) {
|
|
10452
|
+
const readBy = {};
|
|
10453
|
+
if (parsed["read"] && parsed["readAt"]) {
|
|
10454
|
+
readBy[parsed["to"]] = parsed["readAt"];
|
|
10455
|
+
}
|
|
10456
|
+
parsed["readBy"] = readBy;
|
|
10457
|
+
delete parsed["read"];
|
|
10458
|
+
delete parsed["readAt"];
|
|
10459
|
+
}
|
|
10460
|
+
messages.push(parsed);
|
|
10461
|
+
} catch {
|
|
10462
|
+
}
|
|
10463
|
+
}
|
|
10464
|
+
return messages;
|
|
10465
|
+
} catch (err) {
|
|
10466
|
+
if (err.code === "ENOENT") return [];
|
|
10467
|
+
throw err;
|
|
10468
|
+
}
|
|
10469
|
+
}
|
|
10470
|
+
async _ensureRegistry() {
|
|
10471
|
+
await fsp6.mkdir(path4.dirname(this.registryPath), { recursive: true });
|
|
10472
|
+
}
|
|
10473
|
+
async _readRegistry(opts) {
|
|
10474
|
+
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
10475
|
+
return new Map(this._registryCache);
|
|
10476
|
+
}
|
|
10477
|
+
try {
|
|
10478
|
+
const raw = await fsp6.readFile(this.registryPath, "utf8");
|
|
10479
|
+
const data = JSON.parse(raw);
|
|
10480
|
+
const map = /* @__PURE__ */ new Map();
|
|
10481
|
+
for (const [id, agent] of Object.entries(data)) {
|
|
10482
|
+
map.set(id, agent);
|
|
10483
|
+
}
|
|
10484
|
+
this._registryCache = map;
|
|
10485
|
+
this._registryCacheAt = Date.now();
|
|
10486
|
+
return new Map(map);
|
|
10487
|
+
} catch (err) {
|
|
10488
|
+
if (err.code === "ENOENT") {
|
|
10489
|
+
const empty = /* @__PURE__ */ new Map();
|
|
10490
|
+
this._registryCache = empty;
|
|
10491
|
+
this._registryCacheAt = Date.now();
|
|
10492
|
+
return empty;
|
|
10493
|
+
}
|
|
10494
|
+
throw err;
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10497
|
+
_pruneStaleInPlace(registry) {
|
|
10498
|
+
const cutoff = Date.now() - AGENT_STALE_MS;
|
|
10499
|
+
for (const agent of registry.values()) {
|
|
10500
|
+
if (new Date(agent.lastSeenAt).getTime() < cutoff) {
|
|
10501
|
+
agent.status = "idle";
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
}
|
|
10505
|
+
async _writeRegistry(registry) {
|
|
10506
|
+
const obj = {};
|
|
10507
|
+
for (const [id, agent] of registry) {
|
|
10508
|
+
obj[id] = agent;
|
|
10509
|
+
}
|
|
10510
|
+
const tmp = `${this.registryPath}.${randomUUID().slice(0, 8)}.tmp`;
|
|
10511
|
+
await fsp6.writeFile(tmp, JSON.stringify(obj, null, 2), "utf8");
|
|
10512
|
+
await fsp6.rename(tmp, this.registryPath);
|
|
10513
|
+
}
|
|
10514
|
+
};
|
|
10515
|
+
function defaultResolveProjectDir(ctx) {
|
|
10516
|
+
return resolveProjectDir(ctx.projectRoot, wstackGlobalRoot());
|
|
10517
|
+
}
|
|
10518
|
+
function mailboxSessionTag(sessionId) {
|
|
10519
|
+
return createHash("sha256").update(sessionId).digest("hex").slice(0, 8);
|
|
10520
|
+
}
|
|
10521
|
+
function resolveMailboxIdentity(ctx, fallbackBase = "leader") {
|
|
10522
|
+
const fieldId = ctx.agentId && ctx.agentId !== "unknown" ? ctx.agentId : void 0;
|
|
10523
|
+
const baseId = ctx.meta["agentId"] ?? fieldId ?? fallbackBase;
|
|
10524
|
+
const sessionId = ctx.meta["sessionId"] ?? ctx.session?.id ?? "default";
|
|
10525
|
+
const callerId = ctx.meta["globalAgentId"] ?? `${baseId}@${mailboxSessionTag(sessionId)}`;
|
|
10526
|
+
const fieldName = ctx.agentName && ctx.agentName !== "Unknown Agent" ? ctx.agentName : void 0;
|
|
10527
|
+
const name = ctx.meta["agentName"] ?? fieldName ?? baseId;
|
|
10528
|
+
const role = ctx.meta["agentRole"];
|
|
10529
|
+
return { baseId, callerId, name, role, sessionId };
|
|
10530
|
+
}
|
|
10531
|
+
function makeMailboxTool(opts = {}) {
|
|
10532
|
+
const resolveMailbox = opts.resolveMailbox ?? ((ctx) => {
|
|
10533
|
+
const dir = opts.projectDir ?? defaultResolveProjectDir(ctx);
|
|
10534
|
+
return new GlobalMailbox(dir, opts.events);
|
|
10535
|
+
});
|
|
10536
|
+
const agentId = opts.agentId ?? "leader";
|
|
10537
|
+
const sessionId = opts.sessionId ?? "default";
|
|
10538
|
+
const shortHint = "Sub-commands: check (unread), send (to/broadcast), ack (read/complete), query (filter), status (all agents), online (active only), unread (count).";
|
|
10539
|
+
return {
|
|
10540
|
+
name: "mailbox",
|
|
10541
|
+
description: "Inter-agent mailbox with cross-session support. Send messages, check for incoming messages, acknowledge with read receipts, query by criteria, see online agents.",
|
|
10542
|
+
usageHint: shortHint,
|
|
10543
|
+
category: "coordination",
|
|
10544
|
+
permission: "auto",
|
|
10545
|
+
mutating: true,
|
|
10546
|
+
inputSchema: {
|
|
10547
|
+
type: "object",
|
|
10548
|
+
properties: {
|
|
10549
|
+
action: {
|
|
10550
|
+
type: "string",
|
|
10551
|
+
enum: ["check", "send", "ack", "query", "status", "online", "unread"],
|
|
10552
|
+
description: "Which mailbox operation to perform."
|
|
10553
|
+
},
|
|
10554
|
+
to: { type: "string", description: "Recipient agent id, or '*' / 'all' for broadcast." },
|
|
10555
|
+
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result"], description: "Message type." },
|
|
10556
|
+
subject: { type: "string", description: "Short subject line." },
|
|
10557
|
+
body: { type: "string", description: "Full message content." },
|
|
10558
|
+
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
10559
|
+
replyTo: { type: "string", description: "Reply to a specific message id." },
|
|
10560
|
+
messageId: { type: "string", description: "Message id to acknowledge. Required for 'ack'." },
|
|
10561
|
+
read: { type: "boolean", description: "Mark as read (adds read receipt)." },
|
|
10562
|
+
completed: { type: "boolean", description: "Mark as completed." },
|
|
10563
|
+
outcome: { type: "string", description: "Outcome summary when marking complete." },
|
|
10564
|
+
unreadBy: { type: "string", description: "Filter messages unread by this agent. Used by 'check'." },
|
|
10565
|
+
incompleteOnly: { type: "boolean", description: "Only incomplete messages." },
|
|
10566
|
+
from: { type: "string", description: "Filter by sender." },
|
|
10567
|
+
minPriority: { type: "string", enum: ["low", "normal", "high"] },
|
|
10568
|
+
since: { type: "string", description: "ISO8601 timestamp \u2014 only messages after this." },
|
|
10569
|
+
limit: { type: "number", description: "Max messages to return." }
|
|
10570
|
+
},
|
|
10571
|
+
required: ["action"]
|
|
10572
|
+
},
|
|
10573
|
+
async execute(input, ctx) {
|
|
10574
|
+
const mb = resolveMailbox(ctx);
|
|
10575
|
+
const i = input ?? {};
|
|
10576
|
+
const action = i.action;
|
|
10577
|
+
const identity = resolveMailboxIdentity(ctx, agentId);
|
|
10578
|
+
const baseCallerId = identity.baseId;
|
|
10579
|
+
const callerId = identity.callerId;
|
|
10580
|
+
const callerSessionId = ctx.meta["sessionId"] ?? (ctx.session?.id ?? sessionId);
|
|
10581
|
+
try {
|
|
10582
|
+
await mb.registerAgent({
|
|
10583
|
+
agentId: callerId,
|
|
10584
|
+
sessionId: callerSessionId,
|
|
10585
|
+
name: identity.name,
|
|
10586
|
+
role: identity.role,
|
|
10587
|
+
pid: process.pid,
|
|
10588
|
+
source: ctx.meta["source"] ?? "cli"
|
|
10589
|
+
});
|
|
10590
|
+
} catch {
|
|
10591
|
+
}
|
|
10592
|
+
try {
|
|
10593
|
+
await mb.heartbeat({ agentId: callerId });
|
|
10594
|
+
} catch {
|
|
10595
|
+
}
|
|
10596
|
+
switch (action) {
|
|
10597
|
+
case "check":
|
|
10598
|
+
return executeCheck(mb, callerId, [baseCallerId], i);
|
|
10599
|
+
case "send":
|
|
10600
|
+
return executeSend(mb, callerId, callerSessionId, i);
|
|
10601
|
+
case "ack":
|
|
10602
|
+
return executeAck(mb, callerId, i);
|
|
10603
|
+
case "query":
|
|
10604
|
+
return executeQuery(mb, i);
|
|
10605
|
+
case "status":
|
|
10606
|
+
return executeStatus(mb);
|
|
10607
|
+
case "online":
|
|
10608
|
+
return executeOnline(mb);
|
|
10609
|
+
case "unread":
|
|
10610
|
+
return executeUnread(mb, callerId, [baseCallerId]);
|
|
10611
|
+
default:
|
|
10612
|
+
return { ok: false, error: `Unknown action: "${action}". Use check, send, ack, query, status, online, or unread.` };
|
|
10613
|
+
}
|
|
10614
|
+
}
|
|
10615
|
+
};
|
|
10616
|
+
}
|
|
10617
|
+
async function executeCheck(mb, agentId, aliases, i) {
|
|
10618
|
+
const limit = i.limit ?? 20;
|
|
10619
|
+
const targets = [agentId, ...aliases.filter((al) => al && al !== agentId)];
|
|
10620
|
+
const batches = await Promise.all(
|
|
10621
|
+
targets.map(
|
|
10622
|
+
(to) => mb.query({ to, unreadBy: agentId, limit, minPriority: "low" }).catch(() => [])
|
|
10623
|
+
)
|
|
10624
|
+
);
|
|
10625
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10626
|
+
const messages = batches.flat().filter((m) => {
|
|
10627
|
+
if (seen.has(m.id)) return false;
|
|
10628
|
+
seen.add(m.id);
|
|
10629
|
+
return true;
|
|
10630
|
+
});
|
|
10631
|
+
const acked = await Promise.all(
|
|
10632
|
+
messages.map(async (m) => {
|
|
10633
|
+
const updated = await mb.ack({ messageId: m.id, readerId: agentId, read: true }).catch(() => null);
|
|
10634
|
+
return updated ?? m;
|
|
10635
|
+
})
|
|
10636
|
+
);
|
|
10637
|
+
return {
|
|
10638
|
+
ok: true,
|
|
10639
|
+
count: acked.length,
|
|
10640
|
+
messages: acked.map((m) => formatMessage(m, agentId)),
|
|
10641
|
+
summary: acked.length === 0 ? "No unread messages." : `${acked.length} unread message(s).`
|
|
10642
|
+
};
|
|
10643
|
+
}
|
|
10644
|
+
async function executeSend(mb, agentId, _sessionId, i) {
|
|
10645
|
+
const to = i.to;
|
|
10646
|
+
const tp = i.type;
|
|
10647
|
+
const subject = i.subject;
|
|
10648
|
+
const body = i.body;
|
|
10649
|
+
if (!to) return { ok: false, error: '"to" is required.' };
|
|
10650
|
+
if (!tp) return { ok: false, error: '"type" is required.' };
|
|
10651
|
+
if (!subject) return { ok: false, error: '"subject" is required.' };
|
|
10652
|
+
if (body === void 0 || body === null) return { ok: false, error: '"body" is required.' };
|
|
10653
|
+
const msg = await mb.send({
|
|
10654
|
+
from: agentId,
|
|
10655
|
+
to,
|
|
10656
|
+
type: tp,
|
|
10657
|
+
subject,
|
|
10658
|
+
body,
|
|
10659
|
+
priority: i.priority ?? "normal",
|
|
10660
|
+
replyTo: i.replyTo
|
|
10661
|
+
});
|
|
10662
|
+
return {
|
|
10663
|
+
ok: true,
|
|
10664
|
+
messageId: msg.id,
|
|
10665
|
+
to: msg.to,
|
|
10666
|
+
type: msg.type,
|
|
10667
|
+
timestamp: msg.timestamp,
|
|
10668
|
+
summary: `Message sent to ${msg.to === "*" ? "all agents" : msg.to}. Id: ${msg.id}`
|
|
10669
|
+
};
|
|
10670
|
+
}
|
|
10671
|
+
async function executeAck(mb, agentId, i) {
|
|
10672
|
+
const messageId = i.messageId;
|
|
10673
|
+
if (!messageId) return { ok: false, error: '"messageId" is required.' };
|
|
10674
|
+
const updated = await mb.ack({
|
|
10675
|
+
messageId,
|
|
10676
|
+
readerId: agentId,
|
|
10677
|
+
read: i.read,
|
|
10678
|
+
completed: i.completed,
|
|
10679
|
+
outcome: i.outcome
|
|
10680
|
+
});
|
|
10681
|
+
if (!updated) return { ok: false, error: `Message "${messageId}" not found.` };
|
|
10682
|
+
return {
|
|
10683
|
+
ok: true,
|
|
10684
|
+
messageId: updated.id,
|
|
10685
|
+
readBy: Object.keys(updated.readBy),
|
|
10686
|
+
readByCount: Object.keys(updated.readBy).length,
|
|
10687
|
+
completed: updated.completed,
|
|
10688
|
+
completedBy: updated.completedBy,
|
|
10689
|
+
outcome: updated.outcome,
|
|
10690
|
+
summary: `Message ${messageId} acknowledged. Read by ${Object.keys(updated.readBy).length} agent(s), Completed: ${updated.completed}.`
|
|
10691
|
+
};
|
|
10692
|
+
}
|
|
10693
|
+
async function executeQuery(mb, i) {
|
|
10694
|
+
const limit = i.limit ?? 50;
|
|
10695
|
+
const messages = await mb.query({
|
|
10696
|
+
to: i.to,
|
|
10697
|
+
from: i.from,
|
|
10698
|
+
unreadBy: i.unreadBy,
|
|
10699
|
+
incompleteOnly: i.incompleteOnly,
|
|
10700
|
+
type: i.type,
|
|
10701
|
+
minPriority: i.minPriority,
|
|
10702
|
+
since: i.since,
|
|
10703
|
+
limit
|
|
10704
|
+
});
|
|
10705
|
+
return { ok: true, count: messages.length, messages, summary: `${messages.length} message(s).` };
|
|
10706
|
+
}
|
|
10707
|
+
async function executeStatus(mb) {
|
|
10708
|
+
const agents = await mb.getAgentStatuses();
|
|
10709
|
+
return {
|
|
10710
|
+
ok: true,
|
|
10711
|
+
count: agents.length,
|
|
10712
|
+
agents: agents.map((a) => ({
|
|
10713
|
+
agentId: a.agentId,
|
|
10714
|
+
name: a.name,
|
|
10715
|
+
role: a.role,
|
|
10716
|
+
sessionId: a.sessionId,
|
|
10717
|
+
status: a.status,
|
|
10718
|
+
currentTool: a.currentTool,
|
|
10719
|
+
currentTask: a.currentTask,
|
|
10720
|
+
iterations: a.iterations,
|
|
10721
|
+
toolCalls: a.toolCalls,
|
|
10722
|
+
lastSeenAt: a.lastSeenAt,
|
|
10723
|
+
online: a.online,
|
|
10724
|
+
pid: a.pid,
|
|
10725
|
+
source: a.source
|
|
10726
|
+
})),
|
|
10727
|
+
summary: `${agents.filter((a) => a.online).length} online, ${agents.length} total.`
|
|
10728
|
+
};
|
|
10729
|
+
}
|
|
10730
|
+
async function executeOnline(mb) {
|
|
10731
|
+
const agents = await mb.getOnlineAgents();
|
|
10732
|
+
return {
|
|
10733
|
+
ok: true,
|
|
10734
|
+
count: agents.length,
|
|
10735
|
+
agents: agents.map((a) => ({
|
|
10736
|
+
agentId: a.agentId,
|
|
10737
|
+
name: a.name,
|
|
10738
|
+
role: a.role,
|
|
10739
|
+
sessionId: a.sessionId,
|
|
10740
|
+
status: a.status,
|
|
10741
|
+
currentTool: a.currentTool,
|
|
10742
|
+
currentTask: a.currentTask,
|
|
10743
|
+
lastSeenAt: a.lastSeenAt,
|
|
10744
|
+
source: a.source
|
|
10745
|
+
})),
|
|
10746
|
+
summary: `${agents.length} online agent(s).`
|
|
10747
|
+
};
|
|
10748
|
+
}
|
|
10749
|
+
async function executeUnread(mb, agentId, aliases = []) {
|
|
10750
|
+
const targets = [agentId, ...aliases.filter((al) => al && al !== agentId)];
|
|
10751
|
+
const batches = await Promise.all(
|
|
10752
|
+
targets.map((to) => mb.query({ to, unreadBy: agentId, limit: 200 }).catch(() => []))
|
|
10753
|
+
);
|
|
10754
|
+
const ids = new Set(batches.flat().map((m) => m.id));
|
|
10755
|
+
return { ok: true, count: ids.size, summary: `${ids.size} unread message(s) for you.` };
|
|
10756
|
+
}
|
|
10757
|
+
function formatMessage(m, readerId) {
|
|
10758
|
+
const maxBody = 2e3;
|
|
10759
|
+
const truncated = m.body.length > maxBody ? `${m.body.slice(0, maxBody)}\u2026 [truncated]` : m.body;
|
|
10760
|
+
return {
|
|
10761
|
+
id: m.id,
|
|
10762
|
+
from: m.from,
|
|
10763
|
+
to: m.to,
|
|
10764
|
+
type: m.type,
|
|
10765
|
+
subject: m.subject,
|
|
10766
|
+
body: truncated,
|
|
10767
|
+
priority: m.priority,
|
|
10768
|
+
readByMe: readerId in m.readBy,
|
|
10769
|
+
readByCount: Object.keys(m.readBy).length,
|
|
10770
|
+
readBy: m.readBy,
|
|
10771
|
+
completed: m.completed,
|
|
10772
|
+
completedBy: m.completedBy,
|
|
10773
|
+
outcome: m.outcome,
|
|
10774
|
+
timestamp: m.timestamp,
|
|
10775
|
+
replyTo: m.replyTo,
|
|
10776
|
+
senderSessionId: m.senderSessionId
|
|
10777
|
+
};
|
|
10778
|
+
}
|
|
10779
|
+
|
|
10780
|
+
// src/coordination/mail-tools.ts
|
|
10781
|
+
function makeResolver(opts) {
|
|
10782
|
+
return opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
10783
|
+
}
|
|
10784
|
+
async function register(mb, ctx) {
|
|
10785
|
+
const identity = resolveMailboxIdentity(ctx);
|
|
10786
|
+
try {
|
|
10787
|
+
await mb.registerAgent({
|
|
10788
|
+
agentId: identity.callerId,
|
|
10789
|
+
sessionId: identity.sessionId,
|
|
10790
|
+
name: identity.name,
|
|
10791
|
+
role: identity.role,
|
|
10792
|
+
pid: process.pid,
|
|
10793
|
+
source: ctx.meta["source"] ?? "cli"
|
|
10794
|
+
});
|
|
10795
|
+
await mb.heartbeat({ agentId: identity.callerId });
|
|
10796
|
+
} catch {
|
|
10797
|
+
}
|
|
10798
|
+
return identity;
|
|
10799
|
+
}
|
|
10800
|
+
function makeMailSendTool(opts = {}) {
|
|
10801
|
+
const resolveMailbox = makeResolver(opts);
|
|
10802
|
+
return {
|
|
10803
|
+
name: "mail_send",
|
|
10804
|
+
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.',
|
|
10805
|
+
usageHint: 'mail_send to="*" subject="auth refactor done" body="touched src/auth/*, please review"',
|
|
10806
|
+
category: "coordination",
|
|
10807
|
+
permission: "auto",
|
|
10808
|
+
mutating: true,
|
|
10809
|
+
inputSchema: {
|
|
10810
|
+
type: "object",
|
|
10811
|
+
properties: {
|
|
10812
|
+
to: {
|
|
10813
|
+
type: "string",
|
|
10814
|
+
description: 'Recipient: exact agent id ("leader@a1b2c3d4"), base alias ("leader"), or "*" / "all" for everyone.'
|
|
10815
|
+
},
|
|
10816
|
+
subject: { type: "string", description: "Short subject line." },
|
|
10817
|
+
body: { type: "string", description: "The message." },
|
|
10818
|
+
type: {
|
|
10819
|
+
type: "string",
|
|
10820
|
+
enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result"],
|
|
10821
|
+
description: 'Message intent. Default: "broadcast" when to="*", otherwise "note".'
|
|
10822
|
+
},
|
|
10823
|
+
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
10824
|
+
replyTo: { type: "string", description: "Message id this replies to." }
|
|
10825
|
+
},
|
|
10826
|
+
required: ["to", "subject", "body"]
|
|
10827
|
+
},
|
|
10828
|
+
async execute(input, ctx) {
|
|
10829
|
+
const i = input ?? {};
|
|
10830
|
+
const rawTo = i.to;
|
|
10831
|
+
const subject = i.subject;
|
|
10832
|
+
const body = i.body;
|
|
10833
|
+
if (!rawTo || !subject || body === void 0 || body === null) {
|
|
10834
|
+
return { ok: false, error: '"to", "subject" and "body" are required.' };
|
|
10835
|
+
}
|
|
10836
|
+
const to = normalizeRecipient(rawTo);
|
|
10837
|
+
const mb = resolveMailbox(ctx);
|
|
10838
|
+
const identity = await register(mb, ctx);
|
|
10839
|
+
const type = i.type ?? (to === "*" ? "broadcast" : "note");
|
|
10840
|
+
const msg = await mb.send({
|
|
10841
|
+
from: identity.callerId,
|
|
10842
|
+
to,
|
|
10843
|
+
type,
|
|
10844
|
+
subject,
|
|
10845
|
+
body,
|
|
10846
|
+
priority: i.priority ?? "normal",
|
|
10847
|
+
replyTo: i.replyTo
|
|
10848
|
+
});
|
|
10849
|
+
return {
|
|
10850
|
+
ok: true,
|
|
10851
|
+
messageId: msg.id,
|
|
10852
|
+
from: identity.callerId,
|
|
10853
|
+
to: msg.to,
|
|
10854
|
+
summary: `Mail sent to ${msg.to === "*" ? "all agents" : msg.to} as ${identity.callerId}.`
|
|
10855
|
+
};
|
|
10856
|
+
}
|
|
10857
|
+
};
|
|
10858
|
+
}
|
|
10859
|
+
function makeMailInboxTool(opts = {}) {
|
|
10860
|
+
const resolveMailbox = makeResolver(opts);
|
|
10861
|
+
return {
|
|
10862
|
+
name: "mail_inbox",
|
|
10863
|
+
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.',
|
|
10864
|
+
usageHint: "mail_inbox (optionally: limit=10, markRead=false to peek)",
|
|
10865
|
+
category: "coordination",
|
|
10866
|
+
permission: "auto",
|
|
10867
|
+
mutating: false,
|
|
10868
|
+
inputSchema: {
|
|
10869
|
+
type: "object",
|
|
10870
|
+
properties: {
|
|
10871
|
+
limit: { type: "number", description: "Max messages to return (default 20)." },
|
|
10872
|
+
markRead: {
|
|
10873
|
+
type: "boolean",
|
|
10874
|
+
description: "Add a read receipt for each returned message (default true)."
|
|
10875
|
+
}
|
|
10876
|
+
}
|
|
10877
|
+
},
|
|
10878
|
+
async execute(input, ctx) {
|
|
10879
|
+
const i = input ?? {};
|
|
10880
|
+
const limit = i.limit ?? 20;
|
|
10881
|
+
const markRead = i.markRead ?? true;
|
|
10882
|
+
const mb = resolveMailbox(ctx);
|
|
10883
|
+
const identity = await register(mb, ctx);
|
|
10884
|
+
const targets = [identity.callerId];
|
|
10885
|
+
if (identity.baseId !== identity.callerId) targets.push(identity.baseId);
|
|
10886
|
+
const batches = await Promise.all(
|
|
10887
|
+
targets.map(
|
|
10888
|
+
(to) => mb.query({ to, unreadBy: identity.callerId, limit }).catch(() => [])
|
|
10889
|
+
)
|
|
10890
|
+
);
|
|
10891
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10892
|
+
const messages = batches.flat().filter((m) => {
|
|
10893
|
+
if (seen.has(m.id) || m.from === identity.callerId) return false;
|
|
10894
|
+
seen.add(m.id);
|
|
10895
|
+
return true;
|
|
10896
|
+
}).slice(0, limit);
|
|
10897
|
+
if (markRead) {
|
|
10898
|
+
await Promise.all(
|
|
10899
|
+
messages.map(
|
|
10900
|
+
(m) => mb.ack({ messageId: m.id, readerId: identity.callerId, read: true }).catch(() => null)
|
|
10901
|
+
)
|
|
10902
|
+
);
|
|
10903
|
+
}
|
|
10904
|
+
return {
|
|
10905
|
+
ok: true,
|
|
10906
|
+
you: identity.callerId,
|
|
10907
|
+
count: messages.length,
|
|
10908
|
+
messages: messages.map((m) => ({
|
|
10909
|
+
id: m.id,
|
|
10910
|
+
from: m.from,
|
|
10911
|
+
to: m.to,
|
|
10912
|
+
type: m.type,
|
|
10913
|
+
subject: m.subject,
|
|
10914
|
+
body: m.body.length > 2e3 ? `${m.body.slice(0, 2e3)}\u2026 [truncated]` : m.body,
|
|
10915
|
+
timestamp: m.timestamp,
|
|
10916
|
+
replyTo: m.replyTo
|
|
10917
|
+
})),
|
|
10918
|
+
summary: messages.length === 0 ? "Inbox empty." : `${messages.length} unread message(s)${markRead ? " (marked read)" : ""}. Reply with mail_send using the sender id.`
|
|
10919
|
+
};
|
|
10920
|
+
}
|
|
10921
|
+
};
|
|
10922
|
+
}
|
|
10923
|
+
|
|
10924
|
+
// src/coordination/dep-watcher.ts
|
|
10925
|
+
var DEPENDENCY_FILE_PATTERNS = [
|
|
10926
|
+
"package.json",
|
|
10927
|
+
"tsconfig.json",
|
|
10928
|
+
"pnpm-lock.yaml",
|
|
10929
|
+
"yarn.lock",
|
|
10930
|
+
"package-lock.json",
|
|
10931
|
+
"go.mod",
|
|
10932
|
+
"go.sum",
|
|
10933
|
+
"Cargo.toml",
|
|
10934
|
+
"Cargo.lock",
|
|
10935
|
+
"pyproject.toml",
|
|
10936
|
+
"setup.py",
|
|
10937
|
+
"setup.cfg",
|
|
10938
|
+
"requirements.txt",
|
|
10939
|
+
"Pipfile",
|
|
10940
|
+
"Pipfile.lock",
|
|
10941
|
+
"Gemfile",
|
|
10942
|
+
"Gemfile.lock",
|
|
10943
|
+
"composer.json",
|
|
10944
|
+
"composer.lock",
|
|
10945
|
+
"mix.exs",
|
|
10946
|
+
"mix.lock",
|
|
10947
|
+
"pom.xml",
|
|
10948
|
+
"build.gradle",
|
|
10949
|
+
"build.gradle.kts",
|
|
10950
|
+
"settings.gradle",
|
|
10951
|
+
"settings.gradle.kts",
|
|
10952
|
+
"*.csproj",
|
|
10953
|
+
"packages.config",
|
|
10954
|
+
"pubspec.yaml",
|
|
10955
|
+
"pubspec.lock",
|
|
10956
|
+
"CMakeLists.txt",
|
|
10957
|
+
"conanfile.txt",
|
|
10958
|
+
"conanfile.py",
|
|
10959
|
+
"vcpkg.json"
|
|
10960
|
+
];
|
|
10961
|
+
function makeDependencyWatcherConfig(opts) {
|
|
10962
|
+
const {
|
|
10963
|
+
projectRoot,
|
|
10964
|
+
mailbox,
|
|
10965
|
+
targetAgent = "*",
|
|
10966
|
+
watcherAgentId = "dep-watcher",
|
|
10967
|
+
debounceMs = 3e3,
|
|
10968
|
+
patterns = DEPENDENCY_FILE_PATTERNS
|
|
10969
|
+
} = opts;
|
|
10970
|
+
const watchPaths = [];
|
|
10971
|
+
for (const p of patterns) {
|
|
10972
|
+
if (p.includes("*")) {
|
|
10973
|
+
continue;
|
|
10974
|
+
}
|
|
10975
|
+
watchPaths.push(`${projectRoot}/${p}`);
|
|
10976
|
+
}
|
|
10977
|
+
watchPaths.push(projectRoot);
|
|
10978
|
+
const unique = [...new Set(watchPaths)];
|
|
10979
|
+
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
10980
|
+
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
10981
|
+
function matchesPattern(filePath) {
|
|
10982
|
+
const basename5 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
10983
|
+
if (plainPatterns.includes(basename5)) return true;
|
|
10984
|
+
for (const gp of globPatterns) {
|
|
10985
|
+
const regex = new RegExp(
|
|
10986
|
+
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
10987
|
+
);
|
|
10988
|
+
if (regex.test(basename5)) return true;
|
|
10989
|
+
}
|
|
10990
|
+
return false;
|
|
10991
|
+
}
|
|
10992
|
+
const pending = /* @__PURE__ */ new Map();
|
|
10993
|
+
return {
|
|
10994
|
+
watchPaths: unique,
|
|
10995
|
+
debounceMs,
|
|
10996
|
+
async onChange(entry) {
|
|
10997
|
+
if (entry.event === "delete") return;
|
|
10998
|
+
if (!matchesPattern(entry.path)) return;
|
|
10999
|
+
const key = entry.path;
|
|
11000
|
+
const existing = pending.get(key);
|
|
11001
|
+
if (existing) clearTimeout(existing);
|
|
11002
|
+
pending.set(
|
|
11003
|
+
key,
|
|
11004
|
+
setTimeout(async () => {
|
|
11005
|
+
pending.delete(key);
|
|
11006
|
+
try {
|
|
11007
|
+
const fileName = entry.path.split("/").pop()?.split("\\").pop() ?? entry.path;
|
|
11008
|
+
await mailbox.send({
|
|
11009
|
+
from: watcherAgentId,
|
|
11010
|
+
to: targetAgent,
|
|
11011
|
+
type: "assign",
|
|
11012
|
+
subject: `Dependency file changed: ${fileName}`,
|
|
11013
|
+
body: [
|
|
11014
|
+
`File: ${entry.path}`,
|
|
11015
|
+
`Event: ${entry.event}`,
|
|
11016
|
+
`Timestamp: ${entry.timestamp}`,
|
|
11017
|
+
"",
|
|
11018
|
+
`Action: Run a tech-stack audit on the changed dependency file.`,
|
|
11019
|
+
`Validate any new packages, check versions, flag deprecated or prehistoric packages.`,
|
|
11020
|
+
`Report findings back via mailbox (type: result).`
|
|
11021
|
+
].join("\n"),
|
|
11022
|
+
priority: "high",
|
|
11023
|
+
taskContext: {
|
|
11024
|
+
agentRole: "tech-stack",
|
|
11025
|
+
status: "pending"
|
|
11026
|
+
}
|
|
11027
|
+
});
|
|
11028
|
+
} catch {
|
|
11029
|
+
}
|
|
11030
|
+
}, debounceMs)
|
|
11031
|
+
);
|
|
11032
|
+
}
|
|
11033
|
+
};
|
|
11034
|
+
}
|
|
11035
|
+
|
|
11036
|
+
// src/coordination/dep-watcher-bridge.ts
|
|
11037
|
+
function attachDepWatcherBridge(opts) {
|
|
11038
|
+
const {
|
|
11039
|
+
events,
|
|
11040
|
+
mailbox,
|
|
11041
|
+
projectRoot,
|
|
11042
|
+
targetAgent = "tech-stack",
|
|
11043
|
+
watcherAgentId = "dep-watcher",
|
|
11044
|
+
debounceMs = 3e3
|
|
11045
|
+
} = opts;
|
|
11046
|
+
const cfg = makeDependencyWatcherConfig({
|
|
11047
|
+
projectRoot,
|
|
11048
|
+
mailbox,
|
|
11049
|
+
targetAgent,
|
|
11050
|
+
watcherAgentId,
|
|
11051
|
+
debounceMs
|
|
11052
|
+
});
|
|
11053
|
+
const unsub = events.onPattern("file-watcher:changed", (_eventName, rawPayload) => {
|
|
11054
|
+
const payload = rawPayload;
|
|
11055
|
+
if (!payload?.path) return;
|
|
11056
|
+
void cfg.onChange({
|
|
11057
|
+
path: payload.path,
|
|
11058
|
+
event: payload.event ?? "change",
|
|
11059
|
+
timestamp: payload.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
11060
|
+
}).catch(() => {
|
|
11061
|
+
});
|
|
11062
|
+
});
|
|
11063
|
+
return () => {
|
|
11064
|
+
unsub();
|
|
11065
|
+
};
|
|
11066
|
+
}
|
|
11067
|
+
|
|
11068
|
+
// src/coordination/mailbox-hooks.ts
|
|
11069
|
+
function createMailboxHooks(opts) {
|
|
11070
|
+
const { mailbox, agentId, notifyNewMail = true, heartbeat = true } = opts;
|
|
11071
|
+
let lastUnreadCount = -1;
|
|
11072
|
+
return {
|
|
11073
|
+
/**
|
|
11074
|
+
* Call before each tool execution. Checks mailbox and emits events.
|
|
11075
|
+
* @param events — EventBus-like object with emit method.
|
|
11076
|
+
*/
|
|
11077
|
+
async beforeTool(events) {
|
|
11078
|
+
try {
|
|
11079
|
+
const count = await mailbox.unreadCount(agentId);
|
|
11080
|
+
if (notifyNewMail && count !== lastUnreadCount) {
|
|
11081
|
+
lastUnreadCount = count;
|
|
11082
|
+
events.emit("mailbox.unread_count", { agentId, count });
|
|
11083
|
+
}
|
|
11084
|
+
} catch {
|
|
11085
|
+
}
|
|
11086
|
+
},
|
|
11087
|
+
/**
|
|
11088
|
+
* Call after each tool execution. Updates heartbeat and optionally
|
|
11089
|
+
* current tool status.
|
|
11090
|
+
*/
|
|
11091
|
+
async afterTool(toolName) {
|
|
11092
|
+
if (!heartbeat) return;
|
|
11093
|
+
try {
|
|
11094
|
+
await mailbox.heartbeat({
|
|
11095
|
+
agentId,
|
|
11096
|
+
status: "running",
|
|
11097
|
+
currentTool: toolName
|
|
11098
|
+
});
|
|
11099
|
+
} catch {
|
|
11100
|
+
}
|
|
11101
|
+
},
|
|
11102
|
+
/** Reset the cached unread count (e.g., after the agent checks manually). */
|
|
11103
|
+
reset() {
|
|
11104
|
+
lastUnreadCount = -1;
|
|
11105
|
+
}
|
|
11106
|
+
};
|
|
11107
|
+
}
|
|
11108
|
+
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
11109
|
+
var LOG_FILENAME = "package-authors.json";
|
|
11110
|
+
function logPath(storageDir) {
|
|
11111
|
+
return path4.join(storageDir, LOG_FILENAME);
|
|
11112
|
+
}
|
|
11113
|
+
async function loadLog(storageDir, projectRoot) {
|
|
11114
|
+
try {
|
|
11115
|
+
const raw = await fsp6.readFile(logPath(storageDir), "utf-8");
|
|
11116
|
+
const parsed = JSON.parse(raw);
|
|
11117
|
+
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
11118
|
+
return { projectRoot, entries: [] };
|
|
11119
|
+
}
|
|
11120
|
+
return parsed;
|
|
11121
|
+
} catch (err) {
|
|
11122
|
+
if (err.code === "ENOENT") {
|
|
11123
|
+
return { projectRoot, entries: [] };
|
|
11124
|
+
}
|
|
11125
|
+
throw err;
|
|
11126
|
+
}
|
|
11127
|
+
}
|
|
11128
|
+
async function saveLog(storageDir, log) {
|
|
11129
|
+
await fsp6.mkdir(storageDir, { recursive: true });
|
|
11130
|
+
const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
|
|
11131
|
+
await fsp6.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
11132
|
+
await fsp6.rename(tmp, logPath(storageDir));
|
|
11133
|
+
}
|
|
11134
|
+
function detectEcosystem(manifestPath) {
|
|
11135
|
+
const name = path4.basename(manifestPath).toLowerCase();
|
|
11136
|
+
if (name === "package.json") return "npm";
|
|
11137
|
+
if (name === "go.mod") return "go";
|
|
11138
|
+
if (name === "cargo.toml") return "cargo";
|
|
11139
|
+
if (name === "pyproject.toml" || name === "requirements.txt" || name === "pipfile" || name === "pipfile.lock") return "pip";
|
|
11140
|
+
if (name === "gemfile" || name === "gemfile.lock") return "gem";
|
|
11141
|
+
if (name === "composer.json" || name === "composer.lock") return "composer";
|
|
11142
|
+
if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
|
|
11143
|
+
if (name === "mix.exs" || name === "mix.lock") return "elixir";
|
|
11144
|
+
if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
|
|
11145
|
+
if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
|
|
11146
|
+
if (name === "vcpkg.json") return "vcpkg";
|
|
11147
|
+
if (name === "conanfile.txt" || name === "conanfile.py") return "conan";
|
|
11148
|
+
if (name === "cmakeLists.txt") return "cmake";
|
|
11149
|
+
return "unknown";
|
|
11150
|
+
}
|
|
11151
|
+
async function recordPackageAction(opts, entry) {
|
|
11152
|
+
const { storageDir, projectRoot, maxEntries = DEFAULT_MAX_ENTRIES } = opts;
|
|
11153
|
+
const log = await loadLog(storageDir, projectRoot);
|
|
11154
|
+
log.entries.push({
|
|
11155
|
+
...entry,
|
|
11156
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11157
|
+
});
|
|
11158
|
+
if (log.entries.length > maxEntries) {
|
|
11159
|
+
const keep = Math.floor(maxEntries * 0.8);
|
|
11160
|
+
log.entries = log.entries.slice(-keep);
|
|
11161
|
+
log.lastCompactedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11162
|
+
}
|
|
11163
|
+
await saveLog(storageDir, log);
|
|
11164
|
+
}
|
|
11165
|
+
async function getPackageAuthor(opts, manifestPath, packageName) {
|
|
11166
|
+
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
11167
|
+
const normalizedManifest = manifestPath.replace(/\\/g, "/");
|
|
11168
|
+
for (let i = log.entries.length - 1; i >= 0; i--) {
|
|
11169
|
+
const e = log.entries[i];
|
|
11170
|
+
if (e && e.manifestPath.replace(/\\/g, "/") === normalizedManifest && e.packageName === packageName) {
|
|
11171
|
+
return e;
|
|
11172
|
+
}
|
|
11173
|
+
}
|
|
11174
|
+
return void 0;
|
|
11175
|
+
}
|
|
11176
|
+
async function getManifestPackages(opts, manifestPath) {
|
|
11177
|
+
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
11178
|
+
const normalizedManifest = manifestPath.replace(/\\/g, "/");
|
|
11179
|
+
return log.entries.filter(
|
|
11180
|
+
(e) => e.manifestPath.replace(/\\/g, "/") === normalizedManifest
|
|
11181
|
+
);
|
|
11182
|
+
}
|
|
11183
|
+
async function getPackagesByAgent(opts, agentId) {
|
|
11184
|
+
const log = await loadLog(opts.storageDir, opts.projectRoot);
|
|
11185
|
+
const map = /* @__PURE__ */ new Map();
|
|
11186
|
+
for (const e of log.entries) {
|
|
11187
|
+
if (e.agentId === agentId) {
|
|
11188
|
+
const key = `${e.manifestPath}|${e.packageName}`;
|
|
11189
|
+
map.set(key, e);
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
11192
|
+
return map;
|
|
11193
|
+
}
|
|
11194
|
+
async function updatePackageOutdatedStatus(opts, manifestPath, packageName, outdated, latestVersion) {
|
|
11195
|
+
const { storageDir, projectRoot } = opts;
|
|
11196
|
+
const log = await loadLog(storageDir, projectRoot);
|
|
11197
|
+
log.entries.push({
|
|
11198
|
+
manifestPath,
|
|
11199
|
+
packageName,
|
|
11200
|
+
versionSpec: "",
|
|
11201
|
+
ecosystem: detectEcosystem(manifestPath),
|
|
11202
|
+
agentId: "outdated-checker",
|
|
11203
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11204
|
+
outdated,
|
|
11205
|
+
latestVersion
|
|
11206
|
+
});
|
|
11207
|
+
await saveLog(storageDir, log);
|
|
11208
|
+
}
|
|
11209
|
+
async function getFullPackageLog(opts) {
|
|
11210
|
+
return loadLog(opts.storageDir, opts.projectRoot);
|
|
11211
|
+
}
|
|
11212
|
+
|
|
11213
|
+
// src/coordination/package-outdated-watcher.ts
|
|
11214
|
+
function parseOutdatedPackages(body) {
|
|
11215
|
+
const results = [];
|
|
11216
|
+
const tableRows = body.matchAll(
|
|
11217
|
+
/^\|\s*([^-][^|]*?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm
|
|
11218
|
+
);
|
|
11219
|
+
for (const rowMatch of tableRows) {
|
|
11220
|
+
const cols = rowMatch[0].split("|").map((c) => c.trim()).filter(Boolean);
|
|
11221
|
+
if (cols.length >= 5 && cols[0] && cols[0] !== "Package") {
|
|
11222
|
+
results.push({
|
|
11223
|
+
name: cols[0] ?? "",
|
|
11224
|
+
currentVersion: cols[1] ?? "",
|
|
11225
|
+
latestVersion: cols[2] ?? "",
|
|
11226
|
+
wantedVersion: cols[3] ?? "",
|
|
11227
|
+
manifestPath: cols[4] ?? "",
|
|
11228
|
+
ecosystem: detectEcosystem2(cols[4] ?? "")
|
|
11229
|
+
});
|
|
11230
|
+
}
|
|
11231
|
+
}
|
|
11232
|
+
if (results.length === 0) {
|
|
11233
|
+
const kvMatches = body.matchAll(
|
|
11234
|
+
/(?:package|name)[\s:=]+([\w@/-]+).*?(?:current|version)[\s:=]+([\d.]+).*?latest[\s:=]+([\d.]+)/gi
|
|
11235
|
+
);
|
|
11236
|
+
for (const m of kvMatches) {
|
|
11237
|
+
results.push({
|
|
11238
|
+
name: m[1] ?? "",
|
|
11239
|
+
currentVersion: m[2] ?? "",
|
|
11240
|
+
latestVersion: m[3] ?? "",
|
|
11241
|
+
wantedVersion: m[2] ?? "",
|
|
11242
|
+
manifestPath: "",
|
|
11243
|
+
ecosystem: "unknown"
|
|
11244
|
+
});
|
|
11245
|
+
}
|
|
11246
|
+
}
|
|
11247
|
+
return results;
|
|
11248
|
+
}
|
|
11249
|
+
function detectEcosystem2(manifestPath) {
|
|
11250
|
+
const name = manifestPath.split("/").pop()?.split("\\").pop() ?? manifestPath;
|
|
11251
|
+
if (name === "package.json") return "npm";
|
|
11252
|
+
if (name === "go.mod") return "go";
|
|
11253
|
+
if (name === "cargo.toml") return "cargo";
|
|
11254
|
+
if (name === "pyproject.toml" || name === "requirements.txt") return "pip";
|
|
11255
|
+
if (name === "gemfile" || name === "gemfile.lock") return "gem";
|
|
11256
|
+
if (name === "composer.json" || name === "composer.lock") return "composer";
|
|
11257
|
+
if (name.endsWith(".csproj") || name === "packages.config") return "nuget";
|
|
11258
|
+
if (name === "mix.exs" || name === "mix.lock") return "elixir";
|
|
11259
|
+
if (name === "pom.xml" || name.startsWith("build.gradle")) return "maven";
|
|
11260
|
+
if (name === "pubspec.yaml" || name === "pubspec.lock") return "dart";
|
|
11261
|
+
return "unknown";
|
|
11262
|
+
}
|
|
11263
|
+
function startPackageOutdatedWatcher(opts) {
|
|
11264
|
+
const {
|
|
11265
|
+
mailbox,
|
|
11266
|
+
packageTrackerOpts,
|
|
11267
|
+
pollIntervalMs = 60 * 60 * 1e3,
|
|
11268
|
+
watcherAgentId = "pkg-outdated-watcher",
|
|
11269
|
+
onNotify,
|
|
11270
|
+
onLog,
|
|
11271
|
+
onError
|
|
11272
|
+
} = opts;
|
|
11273
|
+
const log = (msg) => onLog?.(msg);
|
|
11274
|
+
const handleError = (err) => onError?.(err);
|
|
11275
|
+
const state = {
|
|
11276
|
+
running: true,
|
|
11277
|
+
timer: null,
|
|
11278
|
+
processedIds: /* @__PURE__ */ new Set()
|
|
11279
|
+
};
|
|
11280
|
+
async function pollOnce() {
|
|
11281
|
+
if (!state.running) return;
|
|
11282
|
+
try {
|
|
11283
|
+
const messages = await mailbox.query({
|
|
11284
|
+
to: watcherAgentId,
|
|
11285
|
+
type: "result",
|
|
11286
|
+
unreadBy: watcherAgentId,
|
|
11287
|
+
limit: 10
|
|
11288
|
+
});
|
|
11289
|
+
for (const msg of messages) {
|
|
11290
|
+
if (state.processedIds.has(msg.id)) continue;
|
|
11291
|
+
state.processedIds.add(msg.id);
|
|
11292
|
+
await mailbox.ack({
|
|
11293
|
+
messageId: msg.id,
|
|
11294
|
+
readerId: watcherAgentId,
|
|
11295
|
+
read: true
|
|
11296
|
+
});
|
|
11297
|
+
await processResultMessage(msg);
|
|
11298
|
+
}
|
|
11299
|
+
} catch (err) {
|
|
11300
|
+
handleError(err);
|
|
11301
|
+
}
|
|
11302
|
+
}
|
|
11303
|
+
async function processResultMessage(msg) {
|
|
11304
|
+
const entries = parseOutdatedPackages(msg.body ?? "");
|
|
11305
|
+
if (entries.length === 0) {
|
|
11306
|
+
log(`[pkg-outdated-watcher] No outdated packages found in message ${msg.id}`);
|
|
11307
|
+
return;
|
|
11308
|
+
}
|
|
11309
|
+
log(`[pkg-outdated-watcher] Processing ${entries.length} outdated package(s) from ${msg.from}`);
|
|
11310
|
+
for (const entry of entries) {
|
|
11311
|
+
try {
|
|
11312
|
+
const author = await getPackageAuthor(
|
|
11313
|
+
packageTrackerOpts,
|
|
11314
|
+
entry.manifestPath,
|
|
11315
|
+
entry.name
|
|
11316
|
+
);
|
|
11317
|
+
const notifyTarget = author?.agentId ?? "*";
|
|
11318
|
+
const notifyBody = buildNotifyBody(entry, author?.agentName);
|
|
11319
|
+
const notifyMsg = {
|
|
11320
|
+
from: watcherAgentId,
|
|
11321
|
+
to: notifyTarget,
|
|
11322
|
+
subject: `Outdated package: ${entry.name}@${entry.currentVersion} \u2192 ${entry.latestVersion}`,
|
|
11323
|
+
body: notifyBody,
|
|
11324
|
+
priority: "high"
|
|
11325
|
+
};
|
|
11326
|
+
await onNotify(notifyMsg);
|
|
11327
|
+
log(
|
|
11328
|
+
`[pkg-outdated-watcher] Notified ${notifyTarget} about outdated ${entry.name} (${entry.currentVersion} \u2192 ${entry.latestVersion}) in ${entry.manifestPath}`
|
|
11329
|
+
);
|
|
11330
|
+
} catch (err) {
|
|
11331
|
+
handleError(err);
|
|
11332
|
+
log(`[pkg-outdated-watcher] Failed to notify for ${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11333
|
+
}
|
|
11334
|
+
}
|
|
11335
|
+
}
|
|
11336
|
+
state.timer = setInterval(() => {
|
|
11337
|
+
void pollOnce();
|
|
11338
|
+
}, pollIntervalMs);
|
|
11339
|
+
void pollOnce();
|
|
11340
|
+
return () => {
|
|
11341
|
+
state.running = false;
|
|
11342
|
+
if (state.timer) {
|
|
11343
|
+
clearInterval(state.timer);
|
|
11344
|
+
state.timer = null;
|
|
11345
|
+
}
|
|
11346
|
+
};
|
|
11347
|
+
}
|
|
11348
|
+
function buildNotifyBody(entry, authorName) {
|
|
11349
|
+
const lines = [
|
|
11350
|
+
`The package **${entry.name}** is outdated.`,
|
|
11351
|
+
"",
|
|
11352
|
+
`| Field | Value |`,
|
|
11353
|
+
`|-------|-------|`,
|
|
11354
|
+
`| Package | ${entry.name} |`,
|
|
11355
|
+
`| Installed | ${entry.currentVersion} |`,
|
|
11356
|
+
`| Latest | ${entry.latestVersion} |`,
|
|
11357
|
+
`| Wanted | ${entry.wantedVersion} |`,
|
|
11358
|
+
`| Manifest | ${entry.manifestPath} |`,
|
|
11359
|
+
`| Ecosystem | ${entry.ecosystem} |`,
|
|
11360
|
+
""
|
|
11361
|
+
];
|
|
11362
|
+
if (authorName) {
|
|
11363
|
+
lines.push(
|
|
11364
|
+
`You added this package${authorName !== "unknown" ? ` (as ${authorName})` : ""}. Consider updating it with the install tool.`
|
|
11365
|
+
);
|
|
11366
|
+
} else {
|
|
11367
|
+
lines.push(
|
|
11368
|
+
`This package appears to have been added by an agent no longer on record. Consider reviewing and updating it.`
|
|
11369
|
+
);
|
|
11370
|
+
}
|
|
11371
|
+
lines.push(
|
|
11372
|
+
"",
|
|
11373
|
+
`Update with:`,
|
|
11374
|
+
`\`\`\``,
|
|
11375
|
+
`${getUpdateCommand(entry)}`,
|
|
11376
|
+
`\`\`\``
|
|
11377
|
+
);
|
|
11378
|
+
return lines.join("\n");
|
|
11379
|
+
}
|
|
11380
|
+
function getUpdateCommand(entry) {
|
|
11381
|
+
switch (entry.ecosystem) {
|
|
11382
|
+
case "npm":
|
|
11383
|
+
return `pnpm add ${entry.name}@latest # or: pnpm update ${entry.name}`;
|
|
11384
|
+
case "cargo":
|
|
11385
|
+
return `cargo update ${entry.name}`;
|
|
11386
|
+
case "go":
|
|
11387
|
+
return `go get ${entry.name}@latest`;
|
|
11388
|
+
case "pip":
|
|
11389
|
+
return `pip install --upgrade ${entry.name}`;
|
|
11390
|
+
case "gem":
|
|
11391
|
+
return `gem install ${entry.name}`;
|
|
11392
|
+
case "composer":
|
|
11393
|
+
return `composer require ${entry.name}:^${entry.latestVersion} --update-with-dependencies`;
|
|
11394
|
+
case "nuget":
|
|
11395
|
+
return `dotnet add package ${entry.name}`;
|
|
11396
|
+
case "maven":
|
|
11397
|
+
return `# Update the <version> in pom.xml or run:
|
|
11398
|
+
mvn versions:use-latest-versions`;
|
|
11399
|
+
case "dart":
|
|
11400
|
+
return `dart pub upgrade ${entry.name}`;
|
|
11401
|
+
default:
|
|
11402
|
+
return `# Update ${entry.name} to ${entry.latestVersion} using your package manager`;
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
|
|
11406
|
+
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 };
|
|
9567
11407
|
//# sourceMappingURL=index.js.map
|
|
9568
11408
|
//# sourceMappingURL=index.js.map
|