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