@wrongstack/core 0.285.0 → 0.286.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/autophase/phase-orchestrator.d.ts.map +1 -1
- package/dist/boot.d.ts.map +1 -1
- package/dist/coordination/agent-monitor.d.ts +29 -0
- package/dist/coordination/agent-monitor.d.ts.map +1 -1
- package/dist/coordination/autonomous-brain.d.ts +0 -2
- package/dist/coordination/autonomous-brain.d.ts.map +1 -1
- package/dist/coordination/autonomous-coordinator.d.ts +4 -0
- package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
- package/dist/coordination/brain-ledger.d.ts +142 -0
- package/dist/coordination/brain-ledger.d.ts.map +1 -0
- package/dist/coordination/brain-monitor.d.ts +31 -2
- package/dist/coordination/brain-monitor.d.ts.map +1 -1
- package/dist/coordination/brain.d.ts +43 -0
- package/dist/coordination/brain.d.ts.map +1 -1
- package/dist/coordination/delegate-tool.d.ts.map +1 -1
- package/dist/coordination/director-construction.d.ts.map +1 -1
- package/dist/coordination/director.d.ts +1 -1
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +4 -4
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +63 -2
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +2 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +1566 -462
- package/dist/coordination/index.js.map +4 -4
- package/dist/coordination/knowledge-graph.d.ts +13 -1
- package/dist/coordination/knowledge-graph.d.ts.map +1 -1
- package/dist/coordination/mailbox-actions.d.ts +12 -13
- package/dist/coordination/mailbox-actions.d.ts.map +1 -1
- package/dist/coordination/mailbox-constants.d.ts +64 -0
- package/dist/coordination/mailbox-constants.d.ts.map +1 -0
- package/dist/coordination/mailbox-events.d.ts +32 -0
- package/dist/coordination/mailbox-events.d.ts.map +1 -0
- package/dist/coordination/mailbox-message-codec.d.ts +8 -0
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
- package/dist/coordination/mailbox-types.d.ts +88 -4
- package/dist/coordination/mailbox-types.d.ts.map +1 -1
- package/dist/coordination/mailbox.d.ts +21 -2
- package/dist/coordination/mailbox.d.ts.map +1 -1
- package/dist/coordination/package-author-tracker.d.ts.map +1 -1
- package/dist/coordination/subagent-result-tool.d.ts.map +1 -1
- package/dist/defaults/index.js +846 -523
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/autonomous-runner.d.ts +7 -1
- package/dist/execution/autonomous-runner.d.ts.map +1 -1
- package/dist/execution/autonomy-brain.d.ts +72 -11
- package/dist/execution/autonomy-brain.d.ts.map +1 -1
- package/dist/execution/brain-chain.d.ts +53 -0
- package/dist/execution/brain-chain.d.ts.map +1 -0
- package/dist/execution/brain-evaluation.d.ts +66 -0
- package/dist/execution/brain-evaluation.d.ts.map +1 -0
- package/dist/execution/brain-runtime.d.ts +171 -0
- package/dist/execution/brain-runtime.d.ts.map +1 -0
- package/dist/execution/council-brain.d.ts +79 -0
- package/dist/execution/council-brain.d.ts.map +1 -0
- package/dist/execution/eternal-autonomy.d.ts +12 -0
- package/dist/execution/eternal-autonomy.d.ts.map +1 -1
- package/dist/execution/index.d.ts +5 -1
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +1350 -377
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/parallel-eternal-engine.d.ts +12 -0
- package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/hq/auth-store.d.ts +21 -3
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/factory.d.ts +3 -0
- package/dist/hq/factory.d.ts.map +1 -1
- package/dist/hq/index.js +628 -72
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/protocol.d.ts +54 -2
- package/dist/hq/protocol.d.ts.map +1 -1
- package/dist/hq/publisher.d.ts +4 -0
- package/dist/hq/publisher.d.ts.map +1 -1
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3482 -1495
- package/dist/index.js.map +4 -4
- package/dist/kernel/events.d.ts +22 -4
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js.map +2 -2
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/observability/index.js +16 -15
- package/dist/observability/index.js.map +2 -2
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +253 -21
- package/dist/security/index.js.map +4 -4
- package/dist/security/permission-policy-schema.d.ts +77 -0
- package/dist/security/permission-policy-schema.d.ts.map +1 -0
- package/dist/security/permission-policy.d.ts +5 -0
- package/dist/security/permission-policy.d.ts.map +1 -1
- package/dist/security/secret-vault.d.ts +9 -0
- package/dist/security/secret-vault.d.ts.map +1 -1
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/storage/config-loader.d.ts +10 -0
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/goal-store.d.ts +1 -1
- package/dist/storage/goal-store.d.ts.map +1 -1
- package/dist/storage/index.js +147 -162
- package/dist/storage/index.js.map +2 -2
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/plan-store.d.ts.map +1 -1
- package/dist/storage/queue-store.d.ts +4 -0
- package/dist/storage/queue-store.d.ts.map +1 -1
- package/dist/storage/session-store.d.ts +9 -0
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/storage/task-store.d.ts +1 -1
- package/dist/storage/task-store.d.ts.map +1 -1
- package/dist/storage/todos-checkpoint.d.ts +2 -2
- package/dist/storage/todos-checkpoint.d.ts.map +1 -1
- package/dist/types/config.d.ts +209 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +2 -2
- package/dist/types/multi-agent.d.ts +8 -0
- package/dist/types/multi-agent.d.ts.map +1 -1
- package/dist/types/observability.d.ts +5 -0
- package/dist/types/observability.d.ts.map +1 -1
- package/dist/types/tool.d.ts +7 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/utils/glob-expand.d.ts.map +1 -1
- package/dist/utils/incoming-images.d.ts +39 -0
- package/dist/utils/incoming-images.d.ts.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +81 -3
- package/dist/utils/index.js.map +3 -3
- package/dist/worktree/worktree-manager.d.ts +13 -4
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/instructions/agents/browser.md +25 -16
- package/instructions/coordination/subagent-baseline.md +16 -2
- package/instructions/leader-after-task.md +3 -1
- package/instructions/sections/tool/common-patterns.md +7 -1
- package/instructions/system.md +47 -0
- package/package.json +2 -2
- package/skills/docker-deploy/SKILL.md +1 -1
- package/skills/wrongstack-mailbox/SKILL.md +51 -12
- package/dist/plugins/git-plugin.d.ts +0 -15
- package/dist/plugins/git-plugin.d.ts.map +0 -1
- package/dist/plugins/observability-plugin.d.ts +0 -19
- package/dist/plugins/observability-plugin.d.ts.map +0 -1
- package/dist/plugins/plan-plugin.d.ts +0 -16
- package/dist/plugins/plan-plugin.d.ts.map +0 -1
|
@@ -329,9 +329,17 @@ function globToRegex(pat) {
|
|
|
329
329
|
return new RegExp(re + "$");
|
|
330
330
|
}
|
|
331
331
|
function baseDir(pat) {
|
|
332
|
-
let
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
let firstGlob = pat.length;
|
|
333
|
+
for (let i = 0; i < pat.length; i++) {
|
|
334
|
+
if (GLOB_CHARS.has(expectDefined(pat[i]))) {
|
|
335
|
+
firstGlob = i;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const cut = Math.max(
|
|
340
|
+
pat.lastIndexOf(SEP, firstGlob - 1),
|
|
341
|
+
pat.lastIndexOf("/", firstGlob - 1)
|
|
342
|
+
);
|
|
335
343
|
return cut < 0 ? "." : pat.slice(0, cut);
|
|
336
344
|
}
|
|
337
345
|
async function expandGlob(pattern) {
|
|
@@ -1417,9 +1425,11 @@ function normalizeSubagentStructuredReport(value) {
|
|
|
1417
1425
|
const filesExamined = stringArray(value["files_examined"]);
|
|
1418
1426
|
const nextSteps = stringArray(value["suggested_next_steps"]);
|
|
1419
1427
|
const confidence = value["confidence"];
|
|
1428
|
+
const completion = value["completion"];
|
|
1429
|
+
const remainingWork = optionalString(value["remaining_work"]);
|
|
1420
1430
|
if (!summary || summary.length > MAX_SUMMARY_CHARS || !findings || findings.length > MAX_FINDINGS || !filesExamined || filesExamined.length > MAX_FILES || !nextSteps || nextSteps.length > MAX_NEXT_STEPS || [...findings, ...filesExamined, ...nextSteps].some(
|
|
1421
1431
|
(item) => item.trim().length > MAX_ITEM_CHARS
|
|
1422
|
-
) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
1432
|
+
) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1 || completion !== void 0 && completion !== "complete" && completion !== "partial" || completion === "partial" && !remainingWork || remainingWork !== void 0 && remainingWork.length > MAX_SUMMARY_CHARS) {
|
|
1423
1433
|
return void 0;
|
|
1424
1434
|
}
|
|
1425
1435
|
const report = {
|
|
@@ -1427,7 +1437,9 @@ function normalizeSubagentStructuredReport(value) {
|
|
|
1427
1437
|
findings: findings.map((item) => item.trim()).filter(Boolean),
|
|
1428
1438
|
files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
|
|
1429
1439
|
confidence,
|
|
1430
|
-
suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean)
|
|
1440
|
+
suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
|
|
1441
|
+
...completion === "complete" || completion === "partial" ? { completion } : {},
|
|
1442
|
+
...remainingWork ? { remaining_work: remainingWork } : {}
|
|
1431
1443
|
};
|
|
1432
1444
|
return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
|
|
1433
1445
|
}
|
|
@@ -1440,6 +1452,8 @@ function formatSubagentStructuredReport(report) {
|
|
|
1440
1452
|
const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
|
|
1441
1453
|
return [
|
|
1442
1454
|
report.summary,
|
|
1455
|
+
report.completion ? `Completion: ${report.completion}` : void 0,
|
|
1456
|
+
report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
|
|
1443
1457
|
`
|
|
1444
1458
|
Findings:
|
|
1445
1459
|
${findings}`,
|
|
@@ -1449,7 +1463,7 @@ Files examined: ${files}`,
|
|
|
1449
1463
|
`
|
|
1450
1464
|
Suggested next steps:
|
|
1451
1465
|
${next}`
|
|
1452
|
-
].join("\n");
|
|
1466
|
+
].filter((line) => typeof line === "string").join("\n");
|
|
1453
1467
|
}
|
|
1454
1468
|
function makeSubagentResultTool() {
|
|
1455
1469
|
return {
|
|
@@ -1489,6 +1503,16 @@ function makeSubagentResultTool() {
|
|
|
1489
1503
|
maxItems: MAX_NEXT_STEPS,
|
|
1490
1504
|
items: { type: "string" },
|
|
1491
1505
|
description: "Concrete follow-ups; use an empty array when none remain."
|
|
1506
|
+
},
|
|
1507
|
+
completion: {
|
|
1508
|
+
type: "string",
|
|
1509
|
+
enum: ["complete", "partial"],
|
|
1510
|
+
description: "Use `partial` only when the task is intentionally stopping at a clean checkpoint and another worker should continue."
|
|
1511
|
+
},
|
|
1512
|
+
remaining_work: {
|
|
1513
|
+
type: "string",
|
|
1514
|
+
maxLength: MAX_SUMMARY_CHARS,
|
|
1515
|
+
description: "Concrete work left for a successor. Required by runtime validation when completion is `partial`."
|
|
1492
1516
|
}
|
|
1493
1517
|
},
|
|
1494
1518
|
required: ["summary", "findings", "files_examined", "confidence", "suggested_next_steps"],
|
|
@@ -1518,6 +1542,9 @@ function isRecord(value) {
|
|
|
1518
1542
|
function nonEmptyString(value) {
|
|
1519
1543
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1520
1544
|
}
|
|
1545
|
+
function optionalString(value) {
|
|
1546
|
+
return value === void 0 ? void 0 : nonEmptyString(value);
|
|
1547
|
+
}
|
|
1521
1548
|
function stringArray(value) {
|
|
1522
1549
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
1523
1550
|
}
|
|
@@ -3458,7 +3485,12 @@ var BrainDecisionQueue = class {
|
|
|
3458
3485
|
if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
|
|
3459
3486
|
entry.timer = setTimeout(() => {
|
|
3460
3487
|
this.pending.delete(request.id);
|
|
3461
|
-
resolve9(
|
|
3488
|
+
resolve9(
|
|
3489
|
+
this.opts.onTimeout?.(request) ?? {
|
|
3490
|
+
type: "deny",
|
|
3491
|
+
reason: "Brain human decision timed out."
|
|
3492
|
+
}
|
|
3493
|
+
);
|
|
3462
3494
|
}, this.opts.timeoutMs);
|
|
3463
3495
|
}
|
|
3464
3496
|
this.pending.set(request.id, entry);
|
|
@@ -3475,6 +3507,46 @@ var BrainDecisionQueue = class {
|
|
|
3475
3507
|
}
|
|
3476
3508
|
}
|
|
3477
3509
|
};
|
|
3510
|
+
function terminalPolicyDecision(request) {
|
|
3511
|
+
const recommended = request.options?.find((option) => option.recommended);
|
|
3512
|
+
if (recommended && (request.risk === "low" || request.risk === "medium")) {
|
|
3513
|
+
return {
|
|
3514
|
+
type: "answer",
|
|
3515
|
+
optionId: recommended.id,
|
|
3516
|
+
text: recommended.label,
|
|
3517
|
+
rationale: `Headless terminal policy: caller-recommended option accepted at ${request.risk} risk (no human available by configuration).`
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
if (request.fallback === "continue") {
|
|
3521
|
+
return {
|
|
3522
|
+
type: "answer",
|
|
3523
|
+
text: "Continue with the caller default.",
|
|
3524
|
+
rationale: "Headless terminal policy: request declares continue as its safe fallback."
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
return {
|
|
3528
|
+
type: "deny",
|
|
3529
|
+
reason: `Headless terminal policy: no safe automatic option for "${request.question}" (risk: ${request.risk}). The proposed action was not taken.`
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
var EscalationRoutingBrainArbiter = class {
|
|
3533
|
+
constructor(inner, queue, getMode) {
|
|
3534
|
+
this.inner = inner;
|
|
3535
|
+
this.queue = queue;
|
|
3536
|
+
this.getMode = getMode;
|
|
3537
|
+
}
|
|
3538
|
+
inner;
|
|
3539
|
+
queue;
|
|
3540
|
+
getMode;
|
|
3541
|
+
async decide(request) {
|
|
3542
|
+
const decision = await this.inner.decide(request);
|
|
3543
|
+
if (decision.type !== "ask_human") return decision;
|
|
3544
|
+
if (this.getMode() === "interactive" && this.queue) {
|
|
3545
|
+
return this.queue.requestHumanDecision(request);
|
|
3546
|
+
}
|
|
3547
|
+
return terminalPolicyDecision(request);
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3478
3550
|
var HumanEscalatingBrainArbiter = class {
|
|
3479
3551
|
constructor(inner, queue) {
|
|
3480
3552
|
this.inner = inner;
|
|
@@ -3550,29 +3622,341 @@ function formatHumanPrompt(request) {
|
|
|
3550
3622
|
return lines.join("\n");
|
|
3551
3623
|
}
|
|
3552
3624
|
|
|
3625
|
+
// src/coordination/brain-ledger.ts
|
|
3626
|
+
import { appendFile, mkdir as mkdir2, readFile } from "node:fs/promises";
|
|
3627
|
+
import { dirname as dirname3 } from "node:path";
|
|
3628
|
+
var QUESTION_MAX = 200;
|
|
3629
|
+
var DETAIL_MAX = 240;
|
|
3630
|
+
var truncate2 = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
3631
|
+
function brainDecisionKey(source, question) {
|
|
3632
|
+
const normalized = question.toLowerCase().replace(/[^\s]*\d[^\s]*/g, "#").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3633
|
+
return `${source ?? "?"}:${normalized}`;
|
|
3634
|
+
}
|
|
3635
|
+
function fmtAge(ms) {
|
|
3636
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
3637
|
+
if (s < 60) return `${s}s ago`;
|
|
3638
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
3639
|
+
return `${Math.round(s / 3600)}h ago`;
|
|
3640
|
+
}
|
|
3641
|
+
function createLedgerGuardBrainArbiter(opts) {
|
|
3642
|
+
const denyAfter = opts.denyAfter ?? 3;
|
|
3643
|
+
return {
|
|
3644
|
+
async decide(request) {
|
|
3645
|
+
if (denyAfter > 0) {
|
|
3646
|
+
const streak = opts.failureStreakFor(request);
|
|
3647
|
+
if (streak >= denyAfter) {
|
|
3648
|
+
return {
|
|
3649
|
+
type: "deny",
|
|
3650
|
+
reason: `Ledger guard: the last ${streak} approved decisions of this kind all ended in observed failures \u2014 denying deterministically until a success is recorded. (${request.question.slice(0, 120)})`
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
return opts.inner.decide(request);
|
|
3655
|
+
}
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
var BrainDecisionLedger = class {
|
|
3659
|
+
constructor(opts) {
|
|
3660
|
+
this.opts = opts;
|
|
3661
|
+
this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
|
|
3662
|
+
this.retryWindowMs = opts.interventionRetryWindowMs ?? 10 * 6e4;
|
|
3663
|
+
this.now = opts.now ?? Date.now;
|
|
3664
|
+
}
|
|
3665
|
+
opts;
|
|
3666
|
+
entries = [];
|
|
3667
|
+
outcomeByRequest = /* @__PURE__ */ new Map();
|
|
3668
|
+
/** subagentId → decision request ids awaiting that subagent's task result. */
|
|
3669
|
+
pendingBudgetOutcomes = /* @__PURE__ */ new Map();
|
|
3670
|
+
lastInterventionByKind = /* @__PURE__ */ new Map();
|
|
3671
|
+
unsubscribers = [];
|
|
3672
|
+
/** Serialized fire-and-forget writes — awaited by `stop()` (drain). */
|
|
3673
|
+
writeChain = Promise.resolve();
|
|
3674
|
+
dirReady = false;
|
|
3675
|
+
maxMemoryEntries;
|
|
3676
|
+
retryWindowMs;
|
|
3677
|
+
now;
|
|
3678
|
+
/**
|
|
3679
|
+
* Subscribe to the EventBus and seed the in-memory ring from the JSONL
|
|
3680
|
+
* tail. The returned promise resolves once the seed load finished
|
|
3681
|
+
* (best-effort — a missing/corrupt file is an empty history, not an error).
|
|
3682
|
+
*/
|
|
3683
|
+
async start() {
|
|
3684
|
+
const { events } = this.opts;
|
|
3685
|
+
this.unsubscribers.push(
|
|
3686
|
+
events.on("brain.decision_answered", (e) => {
|
|
3687
|
+
this.record({
|
|
3688
|
+
at: e.at,
|
|
3689
|
+
sessionId: e.sessionId,
|
|
3690
|
+
requestId: e.request.id,
|
|
3691
|
+
kind: "answered",
|
|
3692
|
+
source: e.request.source,
|
|
3693
|
+
risk: e.request.risk,
|
|
3694
|
+
question: truncate2(e.request.question, QUESTION_MAX),
|
|
3695
|
+
optionId: e.decision.type === "answer" ? e.decision.optionId : void 0,
|
|
3696
|
+
detail: e.decision.type === "answer" ? truncate2(e.decision.rationale ?? e.decision.text, DETAIL_MAX) : void 0
|
|
3697
|
+
});
|
|
3698
|
+
}),
|
|
3699
|
+
events.on("brain.decision_denied", (e) => {
|
|
3700
|
+
this.record({
|
|
3701
|
+
at: e.at,
|
|
3702
|
+
sessionId: e.sessionId,
|
|
3703
|
+
requestId: e.request.id,
|
|
3704
|
+
kind: "denied",
|
|
3705
|
+
source: e.request.source,
|
|
3706
|
+
risk: e.request.risk,
|
|
3707
|
+
question: truncate2(e.request.question, QUESTION_MAX),
|
|
3708
|
+
detail: e.decision.type === "deny" ? truncate2(e.decision.reason, DETAIL_MAX) : void 0
|
|
3709
|
+
});
|
|
3710
|
+
}),
|
|
3711
|
+
events.on("brain.decision_ask_human", (e) => {
|
|
3712
|
+
this.record({
|
|
3713
|
+
at: e.at,
|
|
3714
|
+
sessionId: e.sessionId,
|
|
3715
|
+
requestId: e.request.id,
|
|
3716
|
+
kind: "ask_human",
|
|
3717
|
+
source: e.request.source,
|
|
3718
|
+
risk: e.request.risk,
|
|
3719
|
+
question: truncate2(e.request.question, QUESTION_MAX)
|
|
3720
|
+
});
|
|
3721
|
+
}),
|
|
3722
|
+
events.on("brain.intervention", (e) => {
|
|
3723
|
+
this.record({
|
|
3724
|
+
at: e.at,
|
|
3725
|
+
sessionId: e.sessionId,
|
|
3726
|
+
requestId: e.request.id,
|
|
3727
|
+
kind: "intervention",
|
|
3728
|
+
source: e.request.source,
|
|
3729
|
+
risk: e.request.risk,
|
|
3730
|
+
question: truncate2(`[${e.kind}] ${e.request.question}`, QUESTION_MAX),
|
|
3731
|
+
detail: e.intervened ? "steered" : "observed only"
|
|
3732
|
+
});
|
|
3733
|
+
if (!e.intervened) return;
|
|
3734
|
+
const prev = this.lastInterventionByKind.get(e.kind);
|
|
3735
|
+
if (prev && e.at - prev.at <= this.retryWindowMs) {
|
|
3736
|
+
this.recordOutcome(
|
|
3737
|
+
prev.requestId,
|
|
3738
|
+
"failure",
|
|
3739
|
+
`same signal (${e.kind}) re-triggered ${fmtAge(e.at - prev.at).replace(" ago", " later")}`,
|
|
3740
|
+
e.sessionId
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
this.lastInterventionByKind.set(e.kind, { requestId: e.request.id, at: e.at });
|
|
3744
|
+
}),
|
|
3745
|
+
// ── Outcome correlation: budget extensions ────────────────────────
|
|
3746
|
+
events.on("subagent.budget_extended", (e) => {
|
|
3747
|
+
const requestId = `director-budget-${e.subagentId}-${e.kind}`;
|
|
3748
|
+
if (!this.entries.some((entry) => entry.requestId === requestId)) return;
|
|
3749
|
+
let set = this.pendingBudgetOutcomes.get(e.subagentId);
|
|
3750
|
+
if (!set) {
|
|
3751
|
+
set = /* @__PURE__ */ new Set();
|
|
3752
|
+
this.pendingBudgetOutcomes.set(e.subagentId, set);
|
|
3753
|
+
}
|
|
3754
|
+
set.add(requestId);
|
|
3755
|
+
}),
|
|
3756
|
+
events.on("subagent.task_completed", (e) => {
|
|
3757
|
+
const pending = this.pendingBudgetOutcomes.get(e.subagentId);
|
|
3758
|
+
if (!pending) return;
|
|
3759
|
+
this.pendingBudgetOutcomes.delete(e.subagentId);
|
|
3760
|
+
const outcome = e.status === "success" ? "success" : "failure";
|
|
3761
|
+
for (const requestId of pending) {
|
|
3762
|
+
this.recordOutcome(
|
|
3763
|
+
requestId,
|
|
3764
|
+
outcome,
|
|
3765
|
+
`budget-extended subagent's task ${e.status}`,
|
|
3766
|
+
e.sessionId
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
})
|
|
3770
|
+
);
|
|
3771
|
+
await this.load();
|
|
3772
|
+
}
|
|
3773
|
+
/** Unsubscribe and drain pending writes. Safe to call twice. */
|
|
3774
|
+
async stop() {
|
|
3775
|
+
for (const off of this.unsubscribers) off();
|
|
3776
|
+
this.unsubscribers.length = 0;
|
|
3777
|
+
await this.writeChain.catch(() => {
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* Attach a real-world outcome to an earlier decision. Appended to the
|
|
3782
|
+
* JSONL, folded into future digests, and re-emitted as `brain.outcome`.
|
|
3783
|
+
*/
|
|
3784
|
+
recordOutcome(requestId, outcome, detail, sessionId) {
|
|
3785
|
+
this.outcomeByRequest.set(requestId, outcome);
|
|
3786
|
+
this.record({
|
|
3787
|
+
at: this.now(),
|
|
3788
|
+
sessionId,
|
|
3789
|
+
requestId,
|
|
3790
|
+
kind: "outcome",
|
|
3791
|
+
outcome,
|
|
3792
|
+
detail: truncate2(detail, DETAIL_MAX)
|
|
3793
|
+
});
|
|
3794
|
+
this.opts.events.emit("brain.outcome", {
|
|
3795
|
+
sessionId,
|
|
3796
|
+
requestId,
|
|
3797
|
+
outcome,
|
|
3798
|
+
detail,
|
|
3799
|
+
at: this.now()
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
/** Last `n` ledger rows, oldest first. */
|
|
3803
|
+
tail(n) {
|
|
3804
|
+
return this.entries.slice(-n);
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Consecutive most-recent FAILURE outcomes among answered decisions of
|
|
3808
|
+
* this request's group. Decisions whose outcome is still unknown are
|
|
3809
|
+
* skipped (they neither extend nor break the streak); the first known
|
|
3810
|
+
* SUCCESS ends it. Powers the deterministic ledger guard: "the last K
|
|
3811
|
+
* times we approved this, it went wrong — stop approving".
|
|
3812
|
+
*/
|
|
3813
|
+
failureStreakFor(request) {
|
|
3814
|
+
const key = brainDecisionKey(request.source, request.question);
|
|
3815
|
+
let streak = 0;
|
|
3816
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
3817
|
+
const e = this.entries[i];
|
|
3818
|
+
if (e?.kind !== "answered" || e.requestId === request.id || e.question === void 0 || brainDecisionKey(e.source, e.question) !== key) {
|
|
3819
|
+
continue;
|
|
3820
|
+
}
|
|
3821
|
+
const outcome = this.outcomeByRequest.get(e.requestId);
|
|
3822
|
+
if (outcome === void 0) continue;
|
|
3823
|
+
if (outcome === "success") break;
|
|
3824
|
+
streak += 1;
|
|
3825
|
+
}
|
|
3826
|
+
return streak;
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Short decision-history block for the LLM/council prompt: how similar
|
|
3830
|
+
* past decisions went and — when known — how they turned OUT. Returns
|
|
3831
|
+
* undefined when there is no relevant history (no prompt noise).
|
|
3832
|
+
*/
|
|
3833
|
+
digestFor(request) {
|
|
3834
|
+
const key = brainDecisionKey(request.source, request.question);
|
|
3835
|
+
const related = this.entries.filter(
|
|
3836
|
+
(e) => (e.kind === "answered" || e.kind === "denied") && e.requestId !== request.id && e.question !== void 0 && brainDecisionKey(e.source, e.question) === key
|
|
3837
|
+
);
|
|
3838
|
+
if (related.length === 0) return void 0;
|
|
3839
|
+
const recent = related.slice(-5).reverse();
|
|
3840
|
+
const now = this.now();
|
|
3841
|
+
const lines = recent.map((e) => {
|
|
3842
|
+
const what = e.kind === "denied" ? "denied" : e.optionId ? `chose [${e.optionId}]` : "answered";
|
|
3843
|
+
const outcome = this.outcomeByRequest.get(e.requestId);
|
|
3844
|
+
const suffix = outcome === "failure" ? " \u2192 later FAILED" : outcome === "success" ? " \u2192 later succeeded" : "";
|
|
3845
|
+
return `- ${fmtAge(now - e.at)}: ${what}${suffix}`;
|
|
3846
|
+
});
|
|
3847
|
+
const approved = related.filter((e) => e.kind === "answered");
|
|
3848
|
+
const failedAfter = approved.filter(
|
|
3849
|
+
(e) => this.outcomeByRequest.get(e.requestId) === "failure"
|
|
3850
|
+
).length;
|
|
3851
|
+
const summary = `Summary: ${related.length} similar decision(s); ${approved.length} answered${failedAfter > 0 ? ` (${failedAfter} later failed)` : ""}, ${related.length - approved.length} denied.`;
|
|
3852
|
+
return [...lines, summary].join("\n");
|
|
3853
|
+
}
|
|
3854
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
3855
|
+
record(entry) {
|
|
3856
|
+
this.entries.push(entry);
|
|
3857
|
+
if (this.entries.length > this.maxMemoryEntries) {
|
|
3858
|
+
const evictedRequestIds = /* @__PURE__ */ new Set();
|
|
3859
|
+
const evictCount = this.entries.length - this.maxMemoryEntries;
|
|
3860
|
+
const evicted = this.entries.splice(0, evictCount);
|
|
3861
|
+
for (const e of evicted) {
|
|
3862
|
+
if (e.requestId) evictedRequestIds.add(e.requestId);
|
|
3863
|
+
}
|
|
3864
|
+
if (evictedRequestIds.size > 0) {
|
|
3865
|
+
const remainingRequestIds = /* @__PURE__ */ new Set();
|
|
3866
|
+
for (const e of this.entries) {
|
|
3867
|
+
if (e.requestId) remainingRequestIds.add(e.requestId);
|
|
3868
|
+
}
|
|
3869
|
+
for (const id of evictedRequestIds) {
|
|
3870
|
+
if (!remainingRequestIds.has(id)) this.outcomeByRequest.delete(id);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
this.append(entry);
|
|
3875
|
+
}
|
|
3876
|
+
append(entry) {
|
|
3877
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
3878
|
+
if (!this.dirReady) {
|
|
3879
|
+
await mkdir2(dirname3(this.opts.filePath), { recursive: true });
|
|
3880
|
+
this.dirReady = true;
|
|
3881
|
+
}
|
|
3882
|
+
await appendFile(this.opts.filePath, `${JSON.stringify(entry)}
|
|
3883
|
+
`, "utf8");
|
|
3884
|
+
}).catch(() => {
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
/** Seed the in-memory ring from the JSONL tail (cross-session learning). */
|
|
3888
|
+
async load() {
|
|
3889
|
+
let raw;
|
|
3890
|
+
try {
|
|
3891
|
+
raw = await readFile(this.opts.filePath, "utf8");
|
|
3892
|
+
} catch {
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
3896
|
+
const seed = [];
|
|
3897
|
+
for (const line of lines.slice(-this.maxMemoryEntries)) {
|
|
3898
|
+
try {
|
|
3899
|
+
const parsed = JSON.parse(line);
|
|
3900
|
+
if (typeof parsed?.requestId === "string" && typeof parsed.at === "number") {
|
|
3901
|
+
seed.push(parsed);
|
|
3902
|
+
}
|
|
3903
|
+
} catch {
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
this.entries.unshift(...seed);
|
|
3907
|
+
if (this.entries.length > this.maxMemoryEntries) {
|
|
3908
|
+
this.entries.splice(0, this.entries.length - this.maxMemoryEntries);
|
|
3909
|
+
}
|
|
3910
|
+
for (const e of this.entries) {
|
|
3911
|
+
if (e.kind === "outcome" && e.outcome) this.outcomeByRequest.set(e.requestId, e.outcome);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
|
|
3553
3916
|
// src/coordination/brain-monitor.ts
|
|
3554
3917
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3918
|
+
var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "multi_edit", "multiedit", "str_replace"]);
|
|
3919
|
+
function editedPath(input) {
|
|
3920
|
+
if (!input || typeof input !== "object") return void 0;
|
|
3921
|
+
const r = input;
|
|
3922
|
+
const candidate = r["file_path"] ?? r["path"] ?? r["filePath"] ?? r["file"];
|
|
3923
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
|
|
3924
|
+
}
|
|
3555
3925
|
var BrainMonitor = class {
|
|
3556
3926
|
constructor(opts) {
|
|
3557
3927
|
this.opts = opts;
|
|
3558
3928
|
this.toolFailureStreak = opts.toolFailureStreak ?? 3;
|
|
3559
3929
|
this.errorStormCount = opts.errorStormCount ?? 4;
|
|
3560
3930
|
this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
|
|
3931
|
+
this.stallMs = opts.stallMs ?? 3e5;
|
|
3932
|
+
this.stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 3e4;
|
|
3933
|
+
this.fileChurnThreshold = opts.fileChurnThreshold ?? 5;
|
|
3934
|
+
this.fileChurnWindowMs = opts.fileChurnWindowMs ?? 6e5;
|
|
3561
3935
|
this.cooldownMs = opts.cooldownMs ?? 12e4;
|
|
3562
3936
|
}
|
|
3563
3937
|
opts;
|
|
3564
3938
|
failStreaks = /* @__PURE__ */ new Map();
|
|
3565
3939
|
errorTimestamps = [];
|
|
3940
|
+
editTimestamps = /* @__PURE__ */ new Map();
|
|
3566
3941
|
lastEngagedAt = /* @__PURE__ */ new Map();
|
|
3567
3942
|
unsubscribers = [];
|
|
3568
3943
|
engaging = false;
|
|
3944
|
+
activeRuns = 0;
|
|
3945
|
+
lastProgressAt = 0;
|
|
3946
|
+
stallTimer;
|
|
3569
3947
|
toolFailureStreak;
|
|
3570
3948
|
errorStormCount;
|
|
3571
3949
|
errorStormWindowMs;
|
|
3950
|
+
stallMs;
|
|
3951
|
+
stallCheckIntervalMs;
|
|
3952
|
+
fileChurnThreshold;
|
|
3953
|
+
fileChurnWindowMs;
|
|
3572
3954
|
cooldownMs;
|
|
3573
3955
|
start() {
|
|
3574
3956
|
this.unsubscribers.push(
|
|
3575
3957
|
this.opts.events.on("tool.executed", (e) => {
|
|
3958
|
+
this.lastProgressAt = Date.now();
|
|
3959
|
+
this.trackFileChurn(e.name, e.ok, e.input);
|
|
3576
3960
|
if (e.ok) {
|
|
3577
3961
|
this.failStreaks.delete(e.name);
|
|
3578
3962
|
return;
|
|
@@ -3592,6 +3976,38 @@ var BrainMonitor = class {
|
|
|
3592
3976
|
}
|
|
3593
3977
|
})
|
|
3594
3978
|
);
|
|
3979
|
+
if (this.stallMs > 0) {
|
|
3980
|
+
this.unsubscribers.push(
|
|
3981
|
+
this.opts.events.on("agent.run.started", () => {
|
|
3982
|
+
this.activeRuns += 1;
|
|
3983
|
+
this.lastProgressAt = Date.now();
|
|
3984
|
+
}),
|
|
3985
|
+
this.opts.events.on("agent.run.completed", () => {
|
|
3986
|
+
this.activeRuns = Math.max(0, this.activeRuns - 1);
|
|
3987
|
+
}),
|
|
3988
|
+
this.opts.events.on("agent.run.error", () => {
|
|
3989
|
+
this.activeRuns = Math.max(0, this.activeRuns - 1);
|
|
3990
|
+
}),
|
|
3991
|
+
this.opts.events.on("iteration.started", () => {
|
|
3992
|
+
this.lastProgressAt = Date.now();
|
|
3993
|
+
})
|
|
3994
|
+
);
|
|
3995
|
+
this.stallTimer = setInterval(() => {
|
|
3996
|
+
if (this.activeRuns === 0 || this.lastProgressAt === 0) return;
|
|
3997
|
+
const idleMs = Date.now() - this.lastProgressAt;
|
|
3998
|
+
if (idleMs < this.stallMs) return;
|
|
3999
|
+
this.lastProgressAt = Date.now();
|
|
4000
|
+
void this.engage("agent_stall", {
|
|
4001
|
+
question: `An active run has made no observable progress (no tool call or iteration) for ${Math.round(idleMs / 6e4)} minute(s). Should the agent be steered before more time is wasted?`,
|
|
4002
|
+
context: [
|
|
4003
|
+
`Active runs: ${this.activeRuns}`,
|
|
4004
|
+
`Idle for: ${Math.round(idleMs / 1e3)}s`,
|
|
4005
|
+
`Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
|
|
4006
|
+
].join("\n")
|
|
4007
|
+
});
|
|
4008
|
+
}, this.stallCheckIntervalMs);
|
|
4009
|
+
this.stallTimer.unref?.();
|
|
4010
|
+
}
|
|
3595
4011
|
this.unsubscribers.push(
|
|
3596
4012
|
this.opts.events.on("error", (e) => {
|
|
3597
4013
|
const now = Date.now();
|
|
@@ -3616,6 +4032,37 @@ var BrainMonitor = class {
|
|
|
3616
4032
|
this.unsubscribers.length = 0;
|
|
3617
4033
|
this.failStreaks.clear();
|
|
3618
4034
|
this.errorTimestamps = [];
|
|
4035
|
+
this.editTimestamps.clear();
|
|
4036
|
+
if (this.stallTimer) {
|
|
4037
|
+
clearInterval(this.stallTimer);
|
|
4038
|
+
this.stallTimer = void 0;
|
|
4039
|
+
}
|
|
4040
|
+
this.activeRuns = 0;
|
|
4041
|
+
this.lastProgressAt = 0;
|
|
4042
|
+
}
|
|
4043
|
+
/** Sliding-window count of successful edits per file → churn signal. */
|
|
4044
|
+
trackFileChurn(toolName, ok, input) {
|
|
4045
|
+
if (!ok || !FILE_EDIT_TOOLS.has(toolName.toLowerCase())) return;
|
|
4046
|
+
const path20 = editedPath(input);
|
|
4047
|
+
if (!path20) return;
|
|
4048
|
+
const now = Date.now();
|
|
4049
|
+
const stamps = (this.editTimestamps.get(path20) ?? []).filter(
|
|
4050
|
+
(t) => now - t <= this.fileChurnWindowMs
|
|
4051
|
+
);
|
|
4052
|
+
stamps.push(now);
|
|
4053
|
+
if (stamps.length >= this.fileChurnThreshold) {
|
|
4054
|
+
this.editTimestamps.delete(path20);
|
|
4055
|
+
void this.engage("file_churn", {
|
|
4056
|
+
question: `The file "${path20}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
|
|
4057
|
+
context: [
|
|
4058
|
+
`File: ${path20}`,
|
|
4059
|
+
`Edits in window: ${stamps.length}`,
|
|
4060
|
+
`Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
|
|
4061
|
+
].join("\n")
|
|
4062
|
+
});
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
this.editTimestamps.set(path20, stamps);
|
|
3619
4066
|
}
|
|
3620
4067
|
async engage(kind, input) {
|
|
3621
4068
|
const last = this.lastEngagedAt.get(kind) ?? 0;
|
|
@@ -4422,58 +4869,27 @@ import * as path5 from "node:path";
|
|
|
4422
4869
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4423
4870
|
|
|
4424
4871
|
// src/coordination/fleet.ts
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
var
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
prompt: agentPrompt("bug-hunter")
|
|
4441
|
-
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
4442
|
-
};
|
|
4443
|
-
var REFACTOR_PLANNER_AGENT = {
|
|
4444
|
-
id: "refactor-planner",
|
|
4445
|
-
name: "Refactor Planner",
|
|
4446
|
-
role: "refactor-planner",
|
|
4447
|
-
prompt: agentPrompt("refactor-planner")
|
|
4448
|
-
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
4449
|
-
};
|
|
4450
|
-
var SECURITY_SCANNER_AGENT = {
|
|
4451
|
-
id: "security-scanner",
|
|
4452
|
-
name: "Security Scanner",
|
|
4453
|
-
role: "security-scanner",
|
|
4454
|
-
prompt: agentPrompt("security-scanner")
|
|
4455
|
-
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
4456
|
-
};
|
|
4457
|
-
var SHADOW_AGENT = {
|
|
4458
|
-
id: "shadow-agent",
|
|
4459
|
-
name: "Shadow",
|
|
4460
|
-
role: "shadow-agent",
|
|
4461
|
-
prompt: agentPrompt("shadow-agent")
|
|
4462
|
-
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
4463
|
-
};
|
|
4464
|
-
var CRITIC_AGENT = {
|
|
4465
|
-
id: "critic",
|
|
4466
|
-
name: "Critic",
|
|
4467
|
-
role: "critic",
|
|
4468
|
-
prompt: agentPrompt("critic")
|
|
4469
|
-
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
4470
|
-
};
|
|
4872
|
+
function defineAgent(id, name, promptRole = id, provider) {
|
|
4873
|
+
return {
|
|
4874
|
+
id,
|
|
4875
|
+
name,
|
|
4876
|
+
role: id,
|
|
4877
|
+
prompt: agentPrompt(promptRole),
|
|
4878
|
+
...provider === void 0 ? {} : { provider }
|
|
4879
|
+
};
|
|
4880
|
+
}
|
|
4881
|
+
var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
|
|
4882
|
+
var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
|
|
4883
|
+
var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
|
|
4884
|
+
var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
|
|
4885
|
+
var SHADOW_AGENT = defineAgent("shadow-agent", "Shadow");
|
|
4886
|
+
var CRITIC_AGENT = defineAgent("critic", "Critic");
|
|
4471
4887
|
var FLEET_ROSTER = {
|
|
4472
4888
|
"audit-log": AUDIT_LOG_AGENT,
|
|
4473
4889
|
"bug-hunter": BUG_HUNTER_AGENT,
|
|
4474
4890
|
"refactor-planner": REFACTOR_PLANNER_AGENT,
|
|
4475
4891
|
"security-scanner": SECURITY_SCANNER_AGENT,
|
|
4476
|
-
|
|
4892
|
+
critic: CRITIC_AGENT,
|
|
4477
4893
|
"shadow-agent": SHADOW_AGENT,
|
|
4478
4894
|
...Object.fromEntries(
|
|
4479
4895
|
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
|
|
@@ -4485,7 +4901,7 @@ var FLEET_ROSTER_BUDGETS = {
|
|
|
4485
4901
|
"bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
|
|
4486
4902
|
"refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
|
|
4487
4903
|
"security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
|
|
4488
|
-
|
|
4904
|
+
critic: { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
|
|
4489
4905
|
"shadow-agent": {
|
|
4490
4906
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
4491
4907
|
maxIterations: 2e3,
|
|
@@ -4522,41 +4938,11 @@ function applyRosterBudget(cfg) {
|
|
|
4522
4938
|
};
|
|
4523
4939
|
}
|
|
4524
4940
|
var ALL_FLEET_AGENTS = Object.values(FLEET_ROSTER);
|
|
4525
|
-
var CLINE_AGENT =
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
provider: "acp"
|
|
4531
|
-
};
|
|
4532
|
-
var GEMINI_CLI_AGENT = {
|
|
4533
|
-
id: "gemini-cli",
|
|
4534
|
-
name: "Gemini CLI",
|
|
4535
|
-
role: "gemini-cli",
|
|
4536
|
-
prompt: agentPrompt("acp-gemini-cli"),
|
|
4537
|
-
provider: "acp"
|
|
4538
|
-
};
|
|
4539
|
-
var COPILOT_AGENT = {
|
|
4540
|
-
id: "copilot",
|
|
4541
|
-
name: "GitHub Copilot",
|
|
4542
|
-
role: "copilot",
|
|
4543
|
-
prompt: agentPrompt("acp-copilot"),
|
|
4544
|
-
provider: "acp"
|
|
4545
|
-
};
|
|
4546
|
-
var OPENHANDS_AGENT = {
|
|
4547
|
-
id: "openhands",
|
|
4548
|
-
name: "OpenHands",
|
|
4549
|
-
role: "openhands",
|
|
4550
|
-
prompt: agentPrompt("acp-openhands"),
|
|
4551
|
-
provider: "acp"
|
|
4552
|
-
};
|
|
4553
|
-
var GOOSE_AGENT = {
|
|
4554
|
-
id: "goose",
|
|
4555
|
-
name: "Goose",
|
|
4556
|
-
role: "goose",
|
|
4557
|
-
prompt: agentPrompt("acp-goose"),
|
|
4558
|
-
provider: "acp"
|
|
4559
|
-
};
|
|
4941
|
+
var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
|
|
4942
|
+
var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
|
|
4943
|
+
var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
|
|
4944
|
+
var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
|
|
4945
|
+
var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
|
|
4560
4946
|
var ACP_AGENTS = [
|
|
4561
4947
|
CLINE_AGENT,
|
|
4562
4948
|
GEMINI_CLI_AGENT,
|
|
@@ -4564,11 +4950,14 @@ var ACP_AGENTS = [
|
|
|
4564
4950
|
OPENHANDS_AGENT,
|
|
4565
4951
|
GOOSE_AGENT
|
|
4566
4952
|
];
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4953
|
+
var ACP_AGENT_BUDGET = {
|
|
4954
|
+
timeoutMs: 10 * 60 * 60 * 1e3,
|
|
4955
|
+
maxIterations: 8e3,
|
|
4956
|
+
maxToolCalls: 2e4
|
|
4957
|
+
};
|
|
4958
|
+
for (const agent of ACP_AGENTS) {
|
|
4959
|
+
FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
|
|
4960
|
+
}
|
|
4572
4961
|
var FLEET_ROSTER_WITHACP = {
|
|
4573
4962
|
...FLEET_ROSTER,
|
|
4574
4963
|
...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
|
|
@@ -4576,7 +4965,7 @@ var FLEET_ROSTER_WITHACP = {
|
|
|
4576
4965
|
|
|
4577
4966
|
// src/coordination/delegate-tool.ts
|
|
4578
4967
|
function createDelegateTool(opts) {
|
|
4579
|
-
const defaultTimeoutMs = opts.defaultTimeoutMs ??
|
|
4968
|
+
const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
|
|
4580
4969
|
const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
|
|
4581
4970
|
const inputSchema = {
|
|
4582
4971
|
type: "object",
|
|
@@ -4635,16 +5024,23 @@ function createDelegateTool(opts) {
|
|
|
4635
5024
|
type: "number",
|
|
4636
5025
|
minimum: 0,
|
|
4637
5026
|
description: "Maximum estimated USD cost the subagent may incur. Unset = use the role/coordinator default."
|
|
5027
|
+
},
|
|
5028
|
+
maxHandoffs: {
|
|
5029
|
+
type: "number",
|
|
5030
|
+
minimum: 0,
|
|
5031
|
+
maximum: 8,
|
|
5032
|
+
description: "Maximum fresh-worker continuations after partial completion or budget exhaustion. Default 1; set 0 to disable. Each successor receives the prior structured/partial report and is told to continue only the remaining work."
|
|
4638
5033
|
}
|
|
4639
5034
|
},
|
|
4640
5035
|
required: ["task"]
|
|
4641
5036
|
};
|
|
4642
5037
|
return {
|
|
4643
5038
|
name: "delegate",
|
|
4644
|
-
description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and
|
|
4645
|
-
usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster
|
|
5039
|
+
description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and a generous, auto-extending budget. If it reports explicit partial completion or still exhausts a recoverable budget, delegate can continue the remaining work with a fresh subagent (bounded by `maxHandoffs`, default 1). Workers may ask the leader to spawn a parallel helper through the mailbox, but cannot recursively spawn agents themselves. Use specialized roster roles when possible; otherwise pass `name` + `task`. Call multiple delegates in parallel through the provider's parallel-tool-call surface for independent work.",
|
|
5040
|
+
usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster or pass `name` for a free-form coding agent. Defaults are intentionally generous; override `timeoutMs`, `maxIterations`, `maxToolCalls`, or `maxHandoffs` only when the task warrants it. Returns the final TaskResult plus a `handoffs` history when fresh workers continued partial work. Requires Director mode in normal CLI sessions.",
|
|
4646
5041
|
permission: "auto",
|
|
4647
5042
|
mutating: false,
|
|
5043
|
+
managesOwnTimeout: true,
|
|
4648
5044
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
4649
5045
|
inputSchema,
|
|
4650
5046
|
async execute(input, _ctx, execOpts) {
|
|
@@ -4684,7 +5080,7 @@ function createDelegateTool(opts) {
|
|
|
4684
5080
|
error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
|
|
4685
5081
|
};
|
|
4686
5082
|
}
|
|
4687
|
-
cfg = instantiateRosterConfig(i.role, base);
|
|
5083
|
+
cfg = instantiateRosterConfig(i.role, base, i.timeoutMs, defaultTimeoutMs);
|
|
4688
5084
|
if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
|
|
4689
5085
|
if (i.provider) cfg.provider = i.provider;
|
|
4690
5086
|
if (i.model) cfg.model = i.model;
|
|
@@ -4723,172 +5119,175 @@ function createDelegateTool(opts) {
|
|
|
4723
5119
|
cfg.timeoutMs = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
|
|
4724
5120
|
}
|
|
4725
5121
|
opts.events?.emit("delegate.started", { sessionId, target, task: i.task });
|
|
4726
|
-
const subagentId = await director.spawn(cfg);
|
|
4727
|
-
const taskId = await director.assign({
|
|
4728
|
-
id: `${randomUUID4()}`,
|
|
4729
|
-
description: i.task,
|
|
4730
|
-
subagentId
|
|
4731
|
-
});
|
|
4732
5122
|
const dir = director;
|
|
4733
|
-
const
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
const
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
offIter();
|
|
4744
|
-
offProgress();
|
|
4745
|
-
offAbort();
|
|
4746
|
-
resolve9(value);
|
|
4747
|
-
};
|
|
4748
|
-
const arm = () => {
|
|
4749
|
-
if (timer) clearTimeout(timer);
|
|
4750
|
-
timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
|
|
4751
|
-
};
|
|
4752
|
-
const bump = (e) => {
|
|
4753
|
-
if (e.subagentId === subagentId) arm();
|
|
4754
|
-
};
|
|
4755
|
-
const offTool = dir.fleet.filter("tool.executed", bump);
|
|
4756
|
-
const offIter = dir.fleet.filter("iteration.started", bump);
|
|
4757
|
-
const offProgress = dir.fleet.filter("tool.progress", bump);
|
|
4758
|
-
if (abortSignal) {
|
|
4759
|
-
const onAbortSignal = () => finish({ __aborted: true });
|
|
4760
|
-
abortSignal.addEventListener("abort", onAbortSignal, { once: true });
|
|
4761
|
-
offAbort = () => abortSignal.removeEventListener("abort", onAbortSignal);
|
|
4762
|
-
if (abortSignal.aborted) onAbortSignal();
|
|
4763
|
-
}
|
|
4764
|
-
arm();
|
|
4765
|
-
dir.awaitTasks([taskId]).then((r) => finish(r[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
|
|
4766
|
-
});
|
|
4767
|
-
if ("__aborted" in result) {
|
|
4768
|
-
try {
|
|
4769
|
-
await dir.terminate(subagentId);
|
|
4770
|
-
} catch {
|
|
4771
|
-
}
|
|
4772
|
-
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
4773
|
-
opts.events?.emit("delegate.completed", {
|
|
4774
|
-
sessionId,
|
|
4775
|
-
target,
|
|
4776
|
-
task: i.task,
|
|
4777
|
-
ok: false,
|
|
4778
|
-
status: "aborted",
|
|
4779
|
-
summary: `[${target}] aborted \u2014 the run was interrupted`,
|
|
4780
|
-
durationMs: 0,
|
|
4781
|
-
iterations: partial2?.events ?? 0,
|
|
4782
|
-
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5123
|
+
const maxHandoffs = Math.min(8, Math.max(0, Math.floor(i.maxHandoffs ?? 1)));
|
|
5124
|
+
const handoffs = [];
|
|
5125
|
+
let delegatedTask = i.task;
|
|
5126
|
+
let handoffCount = 0;
|
|
5127
|
+
for (; ; ) {
|
|
5128
|
+
const attemptConfig = handoffCount === 0 ? cfg : freshHandoffConfig(cfg, i.role, handoffCount);
|
|
5129
|
+
const subagentId = await dir.spawn(attemptConfig);
|
|
5130
|
+
const taskId = await dir.assign({
|
|
5131
|
+
id: randomUUID4(),
|
|
5132
|
+
description: delegatedTask,
|
|
4783
5133
|
subagentId
|
|
4784
5134
|
});
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
stopReason: "aborted",
|
|
4788
|
-
error: "Delegated task aborted \u2014 the run was interrupted.",
|
|
5135
|
+
const result = await awaitDelegateAttempt(
|
|
5136
|
+
dir,
|
|
4789
5137
|
subagentId,
|
|
4790
5138
|
taskId,
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
5139
|
+
timeoutMs,
|
|
5140
|
+
abortSignal
|
|
5141
|
+
);
|
|
5142
|
+
if ("__aborted" in result) {
|
|
5143
|
+
try {
|
|
5144
|
+
await dir.terminate(subagentId);
|
|
5145
|
+
} catch {
|
|
5146
|
+
}
|
|
5147
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
5148
|
+
opts.events?.emit("delegate.completed", {
|
|
5149
|
+
sessionId,
|
|
5150
|
+
target,
|
|
5151
|
+
task: i.task,
|
|
5152
|
+
ok: false,
|
|
5153
|
+
status: "aborted",
|
|
5154
|
+
summary: `[${target}] aborted \u2014 the run was interrupted`,
|
|
5155
|
+
durationMs: 0,
|
|
5156
|
+
iterations: partial2?.events ?? 0,
|
|
5157
|
+
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5158
|
+
subagentId
|
|
5159
|
+
});
|
|
5160
|
+
return {
|
|
5161
|
+
ok: false,
|
|
5162
|
+
stopReason: "aborted",
|
|
5163
|
+
error: "Delegated task aborted \u2014 the run was interrupted.",
|
|
5164
|
+
subagentId,
|
|
5165
|
+
taskId,
|
|
5166
|
+
partial: partial2,
|
|
5167
|
+
...handoffs.length > 0 ? { handoffs } : {}
|
|
5168
|
+
};
|
|
5169
|
+
}
|
|
5170
|
+
if ("__timeout" in result) {
|
|
5171
|
+
try {
|
|
5172
|
+
await dir.terminate(subagentId);
|
|
5173
|
+
} catch {
|
|
5174
|
+
}
|
|
5175
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
5176
|
+
opts.events?.emit("delegate.completed", {
|
|
5177
|
+
sessionId,
|
|
5178
|
+
target,
|
|
5179
|
+
task: i.task,
|
|
5180
|
+
ok: false,
|
|
5181
|
+
status: "host_timeout",
|
|
5182
|
+
summary: `[${target}] timed out \u2014 no progress within ${Math.round(timeoutMs / 1e3)}s`,
|
|
5183
|
+
durationMs: timeoutMs,
|
|
5184
|
+
iterations: partial2?.events ?? 0,
|
|
5185
|
+
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5186
|
+
subagentId
|
|
5187
|
+
});
|
|
5188
|
+
return {
|
|
5189
|
+
ok: false,
|
|
5190
|
+
stopReason: "host_timeout",
|
|
5191
|
+
error: `Subagent timed out: it did not finish or report progress within ${timeoutMs}ms.`,
|
|
5192
|
+
hint: "Raise timeoutMs for unusually long single operations, or split the remaining work.",
|
|
5193
|
+
subagentId,
|
|
5194
|
+
taskId,
|
|
5195
|
+
partial: partial2,
|
|
5196
|
+
...handoffs.length > 0 ? { handoffs } : {}
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
if ("__emptyResult" in result) {
|
|
5200
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
5201
|
+
opts.events?.emit("delegate.completed", {
|
|
5202
|
+
sessionId,
|
|
5203
|
+
target,
|
|
5204
|
+
task: i.task,
|
|
5205
|
+
ok: false,
|
|
5206
|
+
status: "empty_result",
|
|
5207
|
+
summary: `[${target}] completed without a task result`,
|
|
5208
|
+
durationMs: 0,
|
|
5209
|
+
iterations: partial2?.events ?? 0,
|
|
5210
|
+
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5211
|
+
subagentId
|
|
5212
|
+
});
|
|
5213
|
+
return {
|
|
5214
|
+
ok: false,
|
|
5215
|
+
stopReason: "error",
|
|
5216
|
+
error: "Director returned no task result for the delegated task.",
|
|
5217
|
+
hint: "Check fleet state, then retry or reassign the task.",
|
|
5218
|
+
subagentId,
|
|
5219
|
+
taskId,
|
|
5220
|
+
partial: partial2,
|
|
5221
|
+
...handoffs.length > 0 ? { handoffs } : {}
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
const partial = result.status === "success" ? void 0 : result.partial ? {
|
|
5225
|
+
lastAssistantText: result.partial.text,
|
|
5226
|
+
toolUsesObserved: result.toolCalls,
|
|
5227
|
+
events: result.iterations
|
|
5228
|
+
} : await readSubagentPartial(opts, subagentId);
|
|
5229
|
+
const continuation = continuationFor(result, partial, attemptConfig);
|
|
5230
|
+
if (continuation && handoffCount < maxHandoffs) {
|
|
5231
|
+
handoffs.push({
|
|
5232
|
+
fromSubagentId: result.subagentId,
|
|
5233
|
+
fromTaskId: result.taskId,
|
|
5234
|
+
status: result.status,
|
|
5235
|
+
errorKind: result.error?.kind,
|
|
5236
|
+
summary: continuation.summary,
|
|
5237
|
+
remainingWork: continuation.remainingWork
|
|
5238
|
+
});
|
|
5239
|
+
handoffCount += 1;
|
|
5240
|
+
delegatedTask = buildHandoffTask(i.task, continuation, handoffCount, maxHandoffs);
|
|
5241
|
+
continue;
|
|
5242
|
+
}
|
|
5243
|
+
const incomplete = result.report?.completion === "partial";
|
|
5244
|
+
const baseStopReason = incomplete ? "handoff_limit" : result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
|
|
5245
|
+
const errorKind = result.error?.kind;
|
|
5246
|
+
const retryable = result.error?.retryable;
|
|
5247
|
+
const backoffMs = result.error?.backoffMs;
|
|
5248
|
+
const summary = incomplete ? `[${target}] partial checkpoint \u2014 handoff limit reached after ${handoffCount} continuation(s)` : buildDelegateSummary(i.role, result);
|
|
5249
|
+
let costUsd;
|
|
5250
|
+
try {
|
|
5251
|
+
costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
|
|
5252
|
+
} catch {
|
|
5253
|
+
costUsd = void 0;
|
|
5254
|
+
}
|
|
4820
5255
|
opts.events?.emit("delegate.completed", {
|
|
4821
5256
|
sessionId,
|
|
4822
5257
|
target,
|
|
4823
5258
|
task: i.task,
|
|
4824
|
-
ok:
|
|
4825
|
-
status: "
|
|
4826
|
-
summary
|
|
4827
|
-
durationMs:
|
|
4828
|
-
iterations:
|
|
4829
|
-
toolCalls:
|
|
4830
|
-
|
|
5259
|
+
ok: result.status === "success" && !incomplete,
|
|
5260
|
+
status: incomplete ? "partial" : result.status,
|
|
5261
|
+
summary,
|
|
5262
|
+
durationMs: result.durationMs,
|
|
5263
|
+
iterations: result.iterations,
|
|
5264
|
+
toolCalls: result.toolCalls,
|
|
5265
|
+
costUsd,
|
|
5266
|
+
subagentId: result.subagentId
|
|
4831
5267
|
});
|
|
4832
5268
|
return {
|
|
4833
|
-
ok:
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
5269
|
+
ok: result.status === "success" && !incomplete,
|
|
5270
|
+
status: incomplete ? "partial" : result.status,
|
|
5271
|
+
stopReason: baseStopReason,
|
|
5272
|
+
errorKind,
|
|
5273
|
+
retryable,
|
|
5274
|
+
backoffMs,
|
|
5275
|
+
subagentId: result.subagentId,
|
|
5276
|
+
taskId: result.taskId,
|
|
5277
|
+
result: result.result,
|
|
5278
|
+
report: result.report,
|
|
5279
|
+
error: result.error,
|
|
5280
|
+
iterations: result.iterations,
|
|
5281
|
+
toolCalls: result.toolCalls,
|
|
5282
|
+
durationMs: result.durationMs,
|
|
5283
|
+
...partial ? { partial } : {},
|
|
5284
|
+
...handoffs.length > 0 ? { handoffs } : {},
|
|
5285
|
+
...incomplete ? {
|
|
5286
|
+
hint: "A clean partial checkpoint remains. Reinvoke delegate with a larger maxHandoffs or assign report.remaining_work explicitly."
|
|
5287
|
+
} : hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
|
|
5288
|
+
summary
|
|
4840
5289
|
};
|
|
4841
5290
|
}
|
|
4842
|
-
const baseStopReason = result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
|
|
4843
|
-
const partial = result.status === "success" ? void 0 : result.partial ? {
|
|
4844
|
-
lastAssistantText: result.partial.text,
|
|
4845
|
-
toolUsesObserved: result.toolCalls,
|
|
4846
|
-
events: result.iterations
|
|
4847
|
-
} : await readSubagentPartial(opts, subagentId);
|
|
4848
|
-
const errorKind = result.error?.kind;
|
|
4849
|
-
const retryable = result.error?.retryable;
|
|
4850
|
-
const backoffMs = result.error?.backoffMs;
|
|
4851
|
-
const summary = buildDelegateSummary(i.role, result);
|
|
4852
|
-
let costUsd;
|
|
4853
|
-
try {
|
|
4854
|
-
costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
|
|
4855
|
-
} catch {
|
|
4856
|
-
costUsd = void 0;
|
|
4857
|
-
}
|
|
4858
|
-
opts.events?.emit("delegate.completed", {
|
|
4859
|
-
sessionId,
|
|
4860
|
-
target,
|
|
4861
|
-
task: i.task,
|
|
4862
|
-
ok: result.status === "success",
|
|
4863
|
-
status: result.status,
|
|
4864
|
-
summary,
|
|
4865
|
-
durationMs: result.durationMs,
|
|
4866
|
-
iterations: result.iterations,
|
|
4867
|
-
toolCalls: result.toolCalls,
|
|
4868
|
-
costUsd,
|
|
4869
|
-
subagentId: result.subagentId
|
|
4870
|
-
});
|
|
4871
|
-
return {
|
|
4872
|
-
ok: result.status === "success",
|
|
4873
|
-
status: result.status,
|
|
4874
|
-
stopReason: baseStopReason,
|
|
4875
|
-
errorKind,
|
|
4876
|
-
retryable,
|
|
4877
|
-
backoffMs,
|
|
4878
|
-
subagentId: result.subagentId,
|
|
4879
|
-
taskId: result.taskId,
|
|
4880
|
-
result: result.result,
|
|
4881
|
-
report: result.report,
|
|
4882
|
-
error: result.error,
|
|
4883
|
-
iterations: result.iterations,
|
|
4884
|
-
toolCalls: result.toolCalls,
|
|
4885
|
-
durationMs: result.durationMs,
|
|
4886
|
-
...partial ? { partial } : {},
|
|
4887
|
-
...hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
|
|
4888
|
-
// Summary is included so callers (TUI, CLI renderer) can surface
|
|
4889
|
-
// it as a chat history line — the LLM also sees it for continuity.
|
|
4890
|
-
summary
|
|
4891
|
-
};
|
|
4892
5291
|
} catch (err) {
|
|
4893
5292
|
const message = toErrorMessage(err);
|
|
4894
5293
|
opts.events?.emit("delegate.completed", {
|
|
@@ -4911,10 +5310,103 @@ function createDelegateTool(opts) {
|
|
|
4911
5310
|
}
|
|
4912
5311
|
};
|
|
4913
5312
|
}
|
|
4914
|
-
function
|
|
5313
|
+
async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
|
|
5314
|
+
return new Promise((resolve9) => {
|
|
5315
|
+
let settled = false;
|
|
5316
|
+
let timer;
|
|
5317
|
+
let offAbort = () => {
|
|
5318
|
+
};
|
|
5319
|
+
const finish = (value) => {
|
|
5320
|
+
if (settled) return;
|
|
5321
|
+
settled = true;
|
|
5322
|
+
if (timer) clearTimeout(timer);
|
|
5323
|
+
offTool();
|
|
5324
|
+
offIter();
|
|
5325
|
+
offProgress();
|
|
5326
|
+
offAbort();
|
|
5327
|
+
resolve9(value);
|
|
5328
|
+
};
|
|
5329
|
+
const arm = () => {
|
|
5330
|
+
if (timer) clearTimeout(timer);
|
|
5331
|
+
timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
|
|
5332
|
+
};
|
|
5333
|
+
const bump = (event) => {
|
|
5334
|
+
if (event.subagentId === subagentId) arm();
|
|
5335
|
+
};
|
|
5336
|
+
const offTool = director.fleet.filter("tool.executed", bump);
|
|
5337
|
+
const offIter = director.fleet.filter("iteration.started", bump);
|
|
5338
|
+
const offProgress = director.fleet.filter("tool.progress", bump);
|
|
5339
|
+
if (abortSignal) {
|
|
5340
|
+
const onAbort = () => finish({ __aborted: true });
|
|
5341
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
5342
|
+
offAbort = () => abortSignal.removeEventListener("abort", onAbort);
|
|
5343
|
+
if (abortSignal.aborted) onAbort();
|
|
5344
|
+
}
|
|
5345
|
+
arm();
|
|
5346
|
+
director.awaitTasks([taskId]).then((results) => finish(results[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
function freshHandoffConfig(cfg, role, handoffCount) {
|
|
5350
|
+
return {
|
|
5351
|
+
...cfg,
|
|
5352
|
+
id: role ? `${role}-${randomUUID4().slice(0, 8)}` : `${cfg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "subagent"}-handoff-${handoffCount}-${randomUUID4().slice(0, 6)}`
|
|
5353
|
+
};
|
|
5354
|
+
}
|
|
5355
|
+
function continuationFor(result, partial, config) {
|
|
5356
|
+
if (result.report?.completion === "partial" && result.report.remaining_work) {
|
|
5357
|
+
return {
|
|
5358
|
+
summary: result.report.summary,
|
|
5359
|
+
remainingWork: result.report.remaining_work,
|
|
5360
|
+
partialText: typeof result.result === "string" ? result.result : partial?.lastAssistantText
|
|
5361
|
+
};
|
|
5362
|
+
}
|
|
5363
|
+
const budgetKinds = /* @__PURE__ */ new Set([
|
|
5364
|
+
"budget_iterations",
|
|
5365
|
+
"budget_tool_calls",
|
|
5366
|
+
"budget_tokens",
|
|
5367
|
+
"budget_cost",
|
|
5368
|
+
"budget_timeout"
|
|
5369
|
+
]);
|
|
5370
|
+
const partialText = result.partial?.text ?? partial?.lastAssistantText;
|
|
5371
|
+
const mayHaveIsolatedWrites = config.worktree !== false && config.worktree !== "off" && (!config.tools || config.tools.some(
|
|
5372
|
+
(tool) => ["write", "edit", "replace", "patch", "bash", "exec", "install", "format"].includes(tool)
|
|
5373
|
+
));
|
|
5374
|
+
if (!mayHaveIsolatedWrites && result.status !== "success" && result.error?.kind && budgetKinds.has(result.error.kind) && partialText) {
|
|
5375
|
+
return {
|
|
5376
|
+
summary: `Prior worker stopped at ${result.error.kind} after ${result.iterations} iterations and ${result.toolCalls} tool calls.`,
|
|
5377
|
+
remainingWork: "Inspect the existing workspace and finish only the work that remains from the original task.",
|
|
5378
|
+
partialText
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
return void 0;
|
|
5382
|
+
}
|
|
5383
|
+
function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs) {
|
|
5384
|
+
const partial = continuation.partialText?.trim().slice(-6e3);
|
|
5385
|
+
return [
|
|
5386
|
+
`Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
|
|
5387
|
+
"Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
|
|
5388
|
+
'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
|
|
5389
|
+
"If parallel help would materially improve the outcome and mail_send is available, send the leader an `ask` that names the exact helper task; do not spawn agents yourself.",
|
|
5390
|
+
`Original task:
|
|
5391
|
+
${originalTask}`,
|
|
5392
|
+
`Prior checkpoint summary:
|
|
5393
|
+
${continuation.summary}`,
|
|
5394
|
+
`Remaining work:
|
|
5395
|
+
${continuation.remainingWork}`,
|
|
5396
|
+
partial ? `Prior worker's last useful output:
|
|
5397
|
+
${partial}` : ""
|
|
5398
|
+
].filter(Boolean).join("\n\n");
|
|
5399
|
+
}
|
|
5400
|
+
function instantiateRosterConfig(role, base, requestedTimeoutMs, defaultTimeoutMs) {
|
|
4915
5401
|
const withBudget = applyRosterBudget({ ...base, role });
|
|
5402
|
+
const rosterTimeoutMs = FLEET_ROSTER_BUDGETS[role]?.timeoutMs;
|
|
4916
5403
|
return {
|
|
4917
5404
|
...withBudget,
|
|
5405
|
+
// Without an explicit host wait, apply the role's tuned multi-hour
|
|
5406
|
+
// wall-clock budget. With an explicit wait, leave this unset so the buffer
|
|
5407
|
+
// logic above derives a slightly shorter child budget without clamping the
|
|
5408
|
+
// role defaults used by ordinary calls.
|
|
5409
|
+
timeoutMs: requestedTimeoutMs === void 0 ? rosterTimeoutMs ?? defaultTimeoutMs : void 0,
|
|
4918
5410
|
// Give each spawn a fresh id so parallel or repeated delegates
|
|
4919
5411
|
// can use the same role safely.
|
|
4920
5412
|
id: `${role}-${randomUUID4().slice(0, 8)}`
|
|
@@ -5100,13 +5592,13 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
5100
5592
|
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
5101
5593
|
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
5102
5594
|
function matchesPattern(filePath) {
|
|
5103
|
-
const
|
|
5104
|
-
if (plainPatterns.includes(
|
|
5595
|
+
const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
5596
|
+
if (plainPatterns.includes(basename7)) return true;
|
|
5105
5597
|
for (const gp of globPatterns) {
|
|
5106
5598
|
const regex = new RegExp(
|
|
5107
5599
|
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
5108
5600
|
);
|
|
5109
|
-
if (regex.test(
|
|
5601
|
+
if (regex.test(basename7)) return true;
|
|
5110
5602
|
}
|
|
5111
5603
|
return false;
|
|
5112
5604
|
}
|
|
@@ -9011,7 +9503,7 @@ var Director = class _Director {
|
|
|
9011
9503
|
this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
9012
9504
|
this.taskResultNotifier = opts.taskResultNotifier;
|
|
9013
9505
|
this.subagentIdleTimeoutMs = typeof opts.subagentIdleTimeoutMs === "number" && Number.isFinite(opts.subagentIdleTimeoutMs) && opts.subagentIdleTimeoutMs >= 0 ? opts.subagentIdleTimeoutMs : void 0;
|
|
9014
|
-
this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ??
|
|
9506
|
+
this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? true;
|
|
9015
9507
|
this.sharedScratchpadPath = opts.sharedScratchpadPath ?? null;
|
|
9016
9508
|
this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
|
|
9017
9509
|
this.maxSpawnDepth = opts.fleetManager?.maxSpawnDepth ?? resolveMaxSpawnDepth(opts.maxSpawnDepth);
|
|
@@ -9022,7 +9514,7 @@ var Director = class _Director {
|
|
|
9022
9514
|
this.dispatchClassifier = opts.dispatchClassifier;
|
|
9023
9515
|
this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
|
|
9024
9516
|
this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
|
|
9025
|
-
this.maxBudgetExtensions = opts.maxBudgetExtensions ??
|
|
9517
|
+
this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 12;
|
|
9026
9518
|
this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
|
|
9027
9519
|
this.maxContext = opts.maxContext ?? 128e3;
|
|
9028
9520
|
this.modelMatrix = opts.modelMatrix;
|
|
@@ -9271,7 +9763,7 @@ var Director = class _Director {
|
|
|
9271
9763
|
payload.deny();
|
|
9272
9764
|
return;
|
|
9273
9765
|
}
|
|
9274
|
-
if (decision.optionId === "stop"
|
|
9766
|
+
if (decision.optionId === "stop") {
|
|
9275
9767
|
payload.deny();
|
|
9276
9768
|
return;
|
|
9277
9769
|
}
|
|
@@ -9799,6 +10291,7 @@ var Director = class _Director {
|
|
|
9799
10291
|
await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
|
|
9800
10292
|
await this.stateCheckpoint.releaseLock().catch((err) => this.logShutdownError("state_checkpoint_lock_release", err));
|
|
9801
10293
|
}
|
|
10294
|
+
this.largeAnswerStore.clear();
|
|
9802
10295
|
}
|
|
9803
10296
|
/**
|
|
9804
10297
|
* Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
|
|
@@ -10000,9 +10493,18 @@ var Director = class _Director {
|
|
|
10000
10493
|
}
|
|
10001
10494
|
async terminate(subagentId) {
|
|
10002
10495
|
await this.coordinator.stop(subagentId);
|
|
10496
|
+
void this.remove(subagentId).catch(
|
|
10497
|
+
(err) => this.logShutdownError("terminate_remove", err)
|
|
10498
|
+
);
|
|
10003
10499
|
}
|
|
10004
10500
|
async terminateAll() {
|
|
10501
|
+
const ids = this.status().subagents.map((s) => s.id);
|
|
10005
10502
|
await this.coordinator.stopAll();
|
|
10503
|
+
for (const id of ids) {
|
|
10504
|
+
void this.remove(id).catch(
|
|
10505
|
+
(err) => this.logShutdownError("terminate_all_remove", err)
|
|
10506
|
+
);
|
|
10507
|
+
}
|
|
10006
10508
|
}
|
|
10007
10509
|
async remove(subagentId) {
|
|
10008
10510
|
this.clearSubagentIdleRetirement(subagentId);
|
|
@@ -10042,7 +10544,7 @@ var Director = class _Director {
|
|
|
10042
10544
|
const timer = setTimeout(() => {
|
|
10043
10545
|
this.subagentIdleTimers.delete(subagentId);
|
|
10044
10546
|
const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
|
|
10045
|
-
if (
|
|
10547
|
+
if (entry?.status !== "idle") return;
|
|
10046
10548
|
if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
|
|
10047
10549
|
this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
|
|
10048
10550
|
return;
|
|
@@ -11634,6 +12136,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11634
12136
|
projectRoot;
|
|
11635
12137
|
checkpointCas;
|
|
11636
12138
|
isSessionInUse;
|
|
12139
|
+
logger;
|
|
11637
12140
|
/**
|
|
11638
12141
|
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
11639
12142
|
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
@@ -11659,6 +12162,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11659
12162
|
this.events = opts.events;
|
|
11660
12163
|
this.secretScrubber = opts.secretScrubber;
|
|
11661
12164
|
this.isSessionInUse = opts.isSessionInUse;
|
|
12165
|
+
this.logger = opts.logger;
|
|
12166
|
+
}
|
|
12167
|
+
/**
|
|
12168
|
+
* Emit a structured warning. Uses the configured Logger when available;
|
|
12169
|
+
* falls back to console.warn(JSON) so warnings are never silently dropped.
|
|
12170
|
+
*/
|
|
12171
|
+
logWarn(msg, ctx) {
|
|
12172
|
+
if (this.logger) {
|
|
12173
|
+
this.logger.warn(msg, ctx);
|
|
12174
|
+
} else {
|
|
12175
|
+
console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
12176
|
+
}
|
|
11662
12177
|
}
|
|
11663
12178
|
/**
|
|
11664
12179
|
* Clear the load() cache. Useful for testing or when the caller knows
|
|
@@ -11760,12 +12275,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11760
12275
|
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
11761
12276
|
return writer;
|
|
11762
12277
|
} catch (err) {
|
|
11763
|
-
await handle.close().catch((e) =>
|
|
11764
|
-
|
|
11765
|
-
event: "session_store.handle_close_failed",
|
|
11766
|
-
|
|
11767
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11768
|
-
})));
|
|
12278
|
+
await handle.close().catch((e) => this.logWarn(
|
|
12279
|
+
"Session handle close failed",
|
|
12280
|
+
{ event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
|
|
12281
|
+
));
|
|
11769
12282
|
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
11770
12283
|
throw err;
|
|
11771
12284
|
}
|
|
@@ -11896,12 +12409,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11896
12409
|
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
11897
12410
|
return { writer, data: resumedData };
|
|
11898
12411
|
} catch (err) {
|
|
11899
|
-
await handle.close().catch((e) =>
|
|
11900
|
-
|
|
11901
|
-
event: "session_store.handle_close_failed",
|
|
11902
|
-
|
|
11903
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11904
|
-
})));
|
|
12412
|
+
await handle.close().catch((e) => this.logWarn(
|
|
12413
|
+
"Session handle close failed",
|
|
12414
|
+
{ event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
|
|
12415
|
+
));
|
|
11905
12416
|
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
11906
12417
|
throw err;
|
|
11907
12418
|
}
|
|
@@ -12522,13 +13033,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12522
13033
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
12523
13034
|
const msg = toErrorMessage(err);
|
|
12524
13035
|
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
event: "session_store.manifest_write_failed",
|
|
12528
|
-
|
|
12529
|
-
message: msg,
|
|
12530
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
12531
|
-
}));
|
|
13036
|
+
this.logWarn(
|
|
13037
|
+
"Session manifest write failed",
|
|
13038
|
+
{ event: "session_store.manifest_write_failed", sessionId: id, message: msg }
|
|
13039
|
+
);
|
|
12532
13040
|
});
|
|
12533
13041
|
outcome = "failure";
|
|
12534
13042
|
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
@@ -13320,6 +13828,148 @@ import { randomUUID as randomUUID10 } from "node:crypto";
|
|
|
13320
13828
|
import * as fsp11 from "node:fs/promises";
|
|
13321
13829
|
import * as path14 from "node:path";
|
|
13322
13830
|
|
|
13831
|
+
// src/coordination/mailbox-constants.ts
|
|
13832
|
+
var AGENT_STALE_MS = 6e4;
|
|
13833
|
+
var CLIENT_STALE_MS = 6e4;
|
|
13834
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
13835
|
+
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
13836
|
+
var LINE_SEPARATOR = "\n";
|
|
13837
|
+
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
13838
|
+
var AUTO_COMPACT_INTERVAL_MS = 3e5;
|
|
13839
|
+
var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
|
|
13840
|
+
var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
|
|
13841
|
+
|
|
13842
|
+
// src/coordination/mailbox-message-codec.ts
|
|
13843
|
+
var MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
13844
|
+
"note",
|
|
13845
|
+
"ask",
|
|
13846
|
+
"assign",
|
|
13847
|
+
"steer",
|
|
13848
|
+
"btw",
|
|
13849
|
+
"broadcast",
|
|
13850
|
+
"status",
|
|
13851
|
+
"result",
|
|
13852
|
+
"review",
|
|
13853
|
+
"control"
|
|
13854
|
+
]);
|
|
13855
|
+
var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
|
|
13856
|
+
var TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
13857
|
+
"pending",
|
|
13858
|
+
"in_progress",
|
|
13859
|
+
"completed",
|
|
13860
|
+
"failed",
|
|
13861
|
+
"idle",
|
|
13862
|
+
"running",
|
|
13863
|
+
"streaming",
|
|
13864
|
+
"waiting_user",
|
|
13865
|
+
"error",
|
|
13866
|
+
"offline",
|
|
13867
|
+
"busy"
|
|
13868
|
+
]);
|
|
13869
|
+
function isRecord3(value) {
|
|
13870
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13871
|
+
}
|
|
13872
|
+
function requiredString(record, key) {
|
|
13873
|
+
const value = record[key];
|
|
13874
|
+
if (typeof value !== "string") {
|
|
13875
|
+
throw new TypeError(`mailbox message field "${key}" must be a string`);
|
|
13876
|
+
}
|
|
13877
|
+
return value;
|
|
13878
|
+
}
|
|
13879
|
+
function optionalString2(record, key) {
|
|
13880
|
+
const value = record[key];
|
|
13881
|
+
if (value === void 0) return {};
|
|
13882
|
+
if (typeof value !== "string") {
|
|
13883
|
+
throw new TypeError(`mailbox message field "${key}" must be a string when present`);
|
|
13884
|
+
}
|
|
13885
|
+
return { [key]: value };
|
|
13886
|
+
}
|
|
13887
|
+
function parseMessageType(value) {
|
|
13888
|
+
if (value === "info") return "note";
|
|
13889
|
+
if (value === "task") return "assign";
|
|
13890
|
+
if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
|
|
13891
|
+
return value;
|
|
13892
|
+
}
|
|
13893
|
+
throw new TypeError('mailbox message field "type" is invalid');
|
|
13894
|
+
}
|
|
13895
|
+
function normalizeMailboxMessageType(value) {
|
|
13896
|
+
return parseMessageType(value);
|
|
13897
|
+
}
|
|
13898
|
+
function parsePriority(value) {
|
|
13899
|
+
if (typeof value === "string" && PRIORITIES.has(value)) {
|
|
13900
|
+
return value;
|
|
13901
|
+
}
|
|
13902
|
+
if (typeof value === "string") return "normal";
|
|
13903
|
+
throw new TypeError('mailbox message field "priority" must be a string');
|
|
13904
|
+
}
|
|
13905
|
+
function parseReadReceipts(record, to) {
|
|
13906
|
+
const value = record["readBy"];
|
|
13907
|
+
if (value === void 0) {
|
|
13908
|
+
const legacyReadAt = record["readAt"];
|
|
13909
|
+
return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
|
|
13910
|
+
}
|
|
13911
|
+
if (!isRecord3(value)) {
|
|
13912
|
+
throw new TypeError('mailbox message field "readBy" must be an object');
|
|
13913
|
+
}
|
|
13914
|
+
const receipts = {};
|
|
13915
|
+
for (const [agentId, timestamp] of Object.entries(value)) {
|
|
13916
|
+
if (typeof timestamp !== "string") {
|
|
13917
|
+
throw new TypeError("mailbox message read receipt timestamps must be strings");
|
|
13918
|
+
}
|
|
13919
|
+
receipts[agentId] = timestamp;
|
|
13920
|
+
}
|
|
13921
|
+
return receipts;
|
|
13922
|
+
}
|
|
13923
|
+
function parseTaskContext(value) {
|
|
13924
|
+
if (value === void 0) return void 0;
|
|
13925
|
+
if (!isRecord3(value)) {
|
|
13926
|
+
throw new TypeError('mailbox message field "taskContext" must be an object');
|
|
13927
|
+
}
|
|
13928
|
+
const status = value["status"];
|
|
13929
|
+
if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
|
|
13930
|
+
throw new TypeError("mailbox message taskContext status is invalid");
|
|
13931
|
+
}
|
|
13932
|
+
return {
|
|
13933
|
+
...optionalString2(value, "agentRole"),
|
|
13934
|
+
...optionalString2(value, "agentName"),
|
|
13935
|
+
...optionalString2(value, "taskId"),
|
|
13936
|
+
...status === void 0 ? {} : { status }
|
|
13937
|
+
};
|
|
13938
|
+
}
|
|
13939
|
+
function parseMailboxMessage(value) {
|
|
13940
|
+
if (!isRecord3(value)) throw new TypeError("mailbox message must be an object");
|
|
13941
|
+
const to = value["to"] === void 0 ? "" : requiredString(value, "to");
|
|
13942
|
+
const completed = value["completed"];
|
|
13943
|
+
if (typeof completed !== "boolean") {
|
|
13944
|
+
throw new TypeError('mailbox message field "completed" must be a boolean');
|
|
13945
|
+
}
|
|
13946
|
+
const taskContext = parseTaskContext(value["taskContext"]);
|
|
13947
|
+
return {
|
|
13948
|
+
id: requiredString(value, "id"),
|
|
13949
|
+
from: requiredString(value, "from"),
|
|
13950
|
+
to,
|
|
13951
|
+
type: parseMessageType(value["type"]),
|
|
13952
|
+
subject: requiredString(value, "subject"),
|
|
13953
|
+
body: requiredString(value, "body"),
|
|
13954
|
+
priority: parsePriority(value["priority"]),
|
|
13955
|
+
readBy: parseReadReceipts(value, to),
|
|
13956
|
+
completed,
|
|
13957
|
+
timestamp: requiredString(value, "timestamp"),
|
|
13958
|
+
...optionalString2(value, "completedBy"),
|
|
13959
|
+
...optionalString2(value, "outcome"),
|
|
13960
|
+
...optionalString2(value, "completedAt"),
|
|
13961
|
+
...optionalString2(value, "deletedAt"),
|
|
13962
|
+
...optionalString2(value, "deletedBy"),
|
|
13963
|
+
...optionalString2(value, "replyTo"),
|
|
13964
|
+
...optionalString2(value, "senderSessionId"),
|
|
13965
|
+
...optionalString2(value, "expiresAt"),
|
|
13966
|
+
...taskContext === void 0 ? {} : { taskContext }
|
|
13967
|
+
};
|
|
13968
|
+
}
|
|
13969
|
+
function parseMailboxMessageLine(line) {
|
|
13970
|
+
return parseMailboxMessage(JSON.parse(line));
|
|
13971
|
+
}
|
|
13972
|
+
|
|
13323
13973
|
// src/coordination/mailbox-types.ts
|
|
13324
13974
|
function normalizeRecipient(to) {
|
|
13325
13975
|
return to.trim().toLowerCase() === "all" ? "*" : to.trim();
|
|
@@ -13328,13 +13978,6 @@ function normalizeRecipient(to) {
|
|
|
13328
13978
|
// src/coordination/global-mailbox.ts
|
|
13329
13979
|
var MAILBOX_FILE = "_mailbox.jsonl";
|
|
13330
13980
|
var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
|
|
13331
|
-
var AGENT_STALE_MS = 6e4;
|
|
13332
|
-
var AGENT_PURGE_MS = 864e5;
|
|
13333
|
-
var CLIENT_STALE_MS = 6e4;
|
|
13334
|
-
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
13335
|
-
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
13336
|
-
var LINE_SEPARATOR = "\n";
|
|
13337
|
-
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
13338
13981
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
13339
13982
|
return path14.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
13340
13983
|
}
|
|
@@ -13349,6 +13992,12 @@ var GlobalMailbox = class {
|
|
|
13349
13992
|
_events;
|
|
13350
13993
|
/** Optional HQ publisher for cross-project command-center telemetry. */
|
|
13351
13994
|
_hqPublisher;
|
|
13995
|
+
/**
|
|
13996
|
+
* Optional SSE event emitter for real-time push to HTTP bridge clients.
|
|
13997
|
+
* When present, `send`/`ack`/`softDelete`/`restore` emit events that
|
|
13998
|
+
* connected SSE subscribers receive instantly.
|
|
13999
|
+
*/
|
|
14000
|
+
eventEmitter;
|
|
13352
14001
|
/**
|
|
13353
14002
|
* Local cache of the agent registry to avoid re-reading on every call.
|
|
13354
14003
|
* Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
|
|
@@ -13394,17 +14043,44 @@ var GlobalMailbox = class {
|
|
|
13394
14043
|
* that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
|
|
13395
14044
|
*/
|
|
13396
14045
|
_readChain = Promise.resolve([]);
|
|
14046
|
+
/**
|
|
14047
|
+
* Recipient → Set of indices into `_messageCache`. Maintained alongside
|
|
14048
|
+
* the cache so `query({ to })` and `unreadCount(agentId)` can skip the
|
|
14049
|
+
* O(N) full scan and iterate only the messages addressed to the target
|
|
14050
|
+
* recipient (plus broadcasts `*`).
|
|
14051
|
+
*
|
|
14052
|
+
* The index is rebuilt whenever the cache array is replaced
|
|
14053
|
+
* (`_setMessageCache`), extended when new messages are pushed
|
|
14054
|
+
* (`_pushToCache`, `_readNewMessagesOnly`), and cleared on `close()`.
|
|
14055
|
+
* It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
|
|
14056
|
+
* or never populated).
|
|
14057
|
+
*
|
|
14058
|
+
* Safety: array indices are valid only as long as the cache array
|
|
14059
|
+
* reference hasn't changed. Since we rebuild the index in the same
|
|
14060
|
+
* synchronous step that replaces the cache, a reader that calls
|
|
14061
|
+
* `_readMessagesCached()` either gets the old array + old index, or
|
|
14062
|
+
* the new array + new index — never a mismatch.
|
|
14063
|
+
*/
|
|
14064
|
+
_recipientIndex = null;
|
|
14065
|
+
/**
|
|
14066
|
+
* Sender → Set of indices into `_messageCache`. Same lifecycle and
|
|
14067
|
+
* safety properties as `_recipientIndex`, but keyed on `msg.from`
|
|
14068
|
+
* so `query({ from })` can skip the full scan.
|
|
14069
|
+
*/
|
|
14070
|
+
_senderIndex = null;
|
|
13397
14071
|
/**
|
|
13398
14072
|
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
13399
14073
|
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
13400
14074
|
* @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
|
|
14075
|
+
* @param eventEmitter — optional SSE event emitter for HTTP bridge push
|
|
13401
14076
|
*/
|
|
13402
|
-
constructor(projectDir, events, hqPublisher) {
|
|
14077
|
+
constructor(projectDir, events, hqPublisher, eventEmitter) {
|
|
13403
14078
|
this.messagePath = path14.join(projectDir, MAILBOX_FILE);
|
|
13404
14079
|
this.registryPath = path14.join(projectDir, "_mailbox.registry.json");
|
|
13405
14080
|
this.clientRegistryPath = path14.join(projectDir, CLIENT_REGISTRY_FILE);
|
|
13406
14081
|
this._events = events;
|
|
13407
14082
|
this._hqPublisher = hqPublisher;
|
|
14083
|
+
this.eventEmitter = eventEmitter;
|
|
13408
14084
|
}
|
|
13409
14085
|
get hqMailboxId() {
|
|
13410
14086
|
return `${path14.basename(path14.dirname(this.messagePath))}:mailbox`;
|
|
@@ -13433,7 +14109,7 @@ var GlobalMailbox = class {
|
|
|
13433
14109
|
// "all" is an accepted spelling of the broadcast address — canonical
|
|
13434
14110
|
// form on disk is '*' so every query/checker matches it.
|
|
13435
14111
|
to: normalizeRecipient(input.to),
|
|
13436
|
-
type: input.type,
|
|
14112
|
+
type: normalizeMailboxMessageType(input.type),
|
|
13437
14113
|
subject: input.subject,
|
|
13438
14114
|
body: input.body,
|
|
13439
14115
|
priority: input.priority ?? "normal",
|
|
@@ -13441,7 +14117,8 @@ var GlobalMailbox = class {
|
|
|
13441
14117
|
completed: false,
|
|
13442
14118
|
timestamp: now,
|
|
13443
14119
|
replyTo: input.replyTo,
|
|
13444
|
-
taskContext: input.taskContext
|
|
14120
|
+
taskContext: input.taskContext,
|
|
14121
|
+
expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
|
|
13445
14122
|
};
|
|
13446
14123
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
13447
14124
|
await fsp11.mkdir(path14.dirname(this.messagePath), { recursive: true });
|
|
@@ -13456,21 +14133,49 @@ var GlobalMailbox = class {
|
|
|
13456
14133
|
message: msg
|
|
13457
14134
|
});
|
|
13458
14135
|
this.publishHqMailboxSnapshot();
|
|
14136
|
+
this._events?.emitCustom("mailbox.message_sent", {
|
|
14137
|
+
messageId: msg.id,
|
|
14138
|
+
from: msg.from,
|
|
14139
|
+
to: msg.to,
|
|
14140
|
+
type: msg.type,
|
|
14141
|
+
subject: msg.subject
|
|
14142
|
+
});
|
|
14143
|
+
this.eventEmitter?.emit({
|
|
14144
|
+
type: "message.sent",
|
|
14145
|
+
messageId: msg.id,
|
|
14146
|
+
from: msg.from,
|
|
14147
|
+
to: msg.to,
|
|
14148
|
+
timestamp: now
|
|
14149
|
+
});
|
|
13459
14150
|
return msg;
|
|
13460
14151
|
}
|
|
13461
14152
|
async query(q) {
|
|
14153
|
+
const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
|
|
13462
14154
|
const all = await this._readMessagesCached();
|
|
13463
14155
|
const limit = q.limit ?? 50;
|
|
13464
14156
|
const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
|
|
13465
14157
|
const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
|
|
13466
14158
|
const out = [];
|
|
13467
|
-
|
|
13468
|
-
|
|
14159
|
+
let candidates;
|
|
14160
|
+
if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
|
|
14161
|
+
const indices = /* @__PURE__ */ new Set();
|
|
14162
|
+
const direct = this._recipientIndex.get(q.to);
|
|
14163
|
+
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
14164
|
+
const broadcasts = this._recipientIndex.get("*");
|
|
14165
|
+
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
14166
|
+
candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
|
|
14167
|
+
} else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
|
|
14168
|
+
const indices = this._senderIndex.get(q.from);
|
|
14169
|
+
candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
|
|
14170
|
+
} else {
|
|
14171
|
+
candidates = all;
|
|
14172
|
+
}
|
|
14173
|
+
for (const m of candidates) {
|
|
13469
14174
|
if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
|
|
13470
14175
|
if (q.from !== void 0 && m.from !== q.from) continue;
|
|
13471
14176
|
if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
|
|
13472
14177
|
if (q.incompleteOnly && m.completed) continue;
|
|
13473
|
-
if (
|
|
14178
|
+
if (queryType !== void 0 && m.type !== queryType) continue;
|
|
13474
14179
|
if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
|
|
13475
14180
|
continue;
|
|
13476
14181
|
}
|
|
@@ -13528,6 +14233,13 @@ var GlobalMailbox = class {
|
|
|
13528
14233
|
action: message.completed ? "message.completed" : "message.read",
|
|
13529
14234
|
message
|
|
13530
14235
|
});
|
|
14236
|
+
this.eventEmitter?.emit({
|
|
14237
|
+
type: "message.acked",
|
|
14238
|
+
messageId: message.id,
|
|
14239
|
+
from: message.from,
|
|
14240
|
+
to: message.to,
|
|
14241
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
14242
|
+
});
|
|
13531
14243
|
}
|
|
13532
14244
|
if (updated.length > 0) this.publishHqMailboxSnapshot();
|
|
13533
14245
|
return updated;
|
|
@@ -13535,10 +14247,22 @@ var GlobalMailbox = class {
|
|
|
13535
14247
|
async unreadCount(forAgentId) {
|
|
13536
14248
|
const all = await this._readMessagesCached();
|
|
13537
14249
|
let count = 0;
|
|
13538
|
-
|
|
13539
|
-
const
|
|
13540
|
-
|
|
13541
|
-
|
|
14250
|
+
if (this._recipientIndex !== null) {
|
|
14251
|
+
const indices = /* @__PURE__ */ new Set();
|
|
14252
|
+
const direct = this._recipientIndex.get(forAgentId);
|
|
14253
|
+
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
14254
|
+
const broadcasts = this._recipientIndex.get("*");
|
|
14255
|
+
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
14256
|
+
for (const i of indices) {
|
|
14257
|
+
const m = all[i];
|
|
14258
|
+
if (!(forAgentId in m.readBy) && !m.completed) count++;
|
|
14259
|
+
}
|
|
14260
|
+
} else {
|
|
14261
|
+
for (let i = 0; i < all.length; i++) {
|
|
14262
|
+
const m = all[i];
|
|
14263
|
+
if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
|
|
14264
|
+
count++;
|
|
14265
|
+
}
|
|
13542
14266
|
}
|
|
13543
14267
|
}
|
|
13544
14268
|
return count;
|
|
@@ -13548,6 +14272,7 @@ var GlobalMailbox = class {
|
|
|
13548
14272
|
await withFileLock(this.messagePath, async () => {
|
|
13549
14273
|
const all = await this._readMessagesFresh();
|
|
13550
14274
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14275
|
+
let changed = false;
|
|
13551
14276
|
for (const m of all) {
|
|
13552
14277
|
if (m.id !== mailId) continue;
|
|
13553
14278
|
if (m.deletedAt !== void 0) {
|
|
@@ -13557,19 +14282,30 @@ var GlobalMailbox = class {
|
|
|
13557
14282
|
m.deletedAt = now;
|
|
13558
14283
|
m.deletedBy = by;
|
|
13559
14284
|
updated = m;
|
|
14285
|
+
changed = true;
|
|
14286
|
+
}
|
|
14287
|
+
if (changed) {
|
|
14288
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
14289
|
+
await fsp11.writeFile(this.messagePath, serialized, "utf8");
|
|
13560
14290
|
}
|
|
13561
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
13562
|
-
await fsp11.writeFile(this.messagePath, serialized, "utf8");
|
|
13563
14291
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
13564
14292
|
this._setMessageCache(all, mtimeMs, size);
|
|
13565
14293
|
});
|
|
13566
14294
|
if (updated !== null) {
|
|
14295
|
+
const msg = updated;
|
|
13567
14296
|
this.publishHqMailboxEvent({
|
|
13568
14297
|
mailboxId: this.hqMailboxId,
|
|
13569
14298
|
action: "message.updated",
|
|
13570
|
-
message:
|
|
14299
|
+
message: msg
|
|
13571
14300
|
});
|
|
13572
14301
|
this.publishHqMailboxSnapshot();
|
|
14302
|
+
this.eventEmitter?.emit({
|
|
14303
|
+
type: "message.deleted",
|
|
14304
|
+
messageId: msg.id,
|
|
14305
|
+
from: msg.from,
|
|
14306
|
+
to: msg.to,
|
|
14307
|
+
timestamp: msg.deletedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
14308
|
+
});
|
|
13573
14309
|
}
|
|
13574
14310
|
return updated;
|
|
13575
14311
|
}
|
|
@@ -13577,24 +14313,40 @@ var GlobalMailbox = class {
|
|
|
13577
14313
|
let updated = null;
|
|
13578
14314
|
await withFileLock(this.messagePath, async () => {
|
|
13579
14315
|
const all = await this._readMessagesFresh();
|
|
14316
|
+
let changed = false;
|
|
13580
14317
|
for (const m of all) {
|
|
13581
14318
|
if (m.id !== mailId) continue;
|
|
14319
|
+
if (m.deletedAt === void 0 && m.deletedBy === void 0) {
|
|
14320
|
+
updated = m;
|
|
14321
|
+
continue;
|
|
14322
|
+
}
|
|
13582
14323
|
delete m.deletedAt;
|
|
13583
14324
|
delete m.deletedBy;
|
|
13584
14325
|
updated = m;
|
|
14326
|
+
changed = true;
|
|
14327
|
+
}
|
|
14328
|
+
if (changed) {
|
|
14329
|
+
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
14330
|
+
await fsp11.writeFile(this.messagePath, serialized, "utf8");
|
|
13585
14331
|
}
|
|
13586
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
13587
|
-
await fsp11.writeFile(this.messagePath, serialized, "utf8");
|
|
13588
14332
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
13589
14333
|
this._setMessageCache(all, mtimeMs, size);
|
|
13590
14334
|
});
|
|
13591
14335
|
if (updated !== null) {
|
|
14336
|
+
const msg = updated;
|
|
13592
14337
|
this.publishHqMailboxEvent({
|
|
13593
14338
|
mailboxId: this.hqMailboxId,
|
|
13594
14339
|
action: "message.updated",
|
|
13595
|
-
message:
|
|
14340
|
+
message: msg
|
|
13596
14341
|
});
|
|
13597
14342
|
this.publishHqMailboxSnapshot();
|
|
14343
|
+
this.eventEmitter?.emit({
|
|
14344
|
+
type: "message.restored",
|
|
14345
|
+
messageId: msg.id,
|
|
14346
|
+
from: msg.from,
|
|
14347
|
+
to: msg.to,
|
|
14348
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
14349
|
+
});
|
|
13598
14350
|
}
|
|
13599
14351
|
return updated;
|
|
13600
14352
|
}
|
|
@@ -13727,8 +14479,19 @@ var GlobalMailbox = class {
|
|
|
13727
14479
|
}
|
|
13728
14480
|
async getAgentStatuses() {
|
|
13729
14481
|
await this._ensureRegistry();
|
|
13730
|
-
|
|
14482
|
+
let registry = await this._readRegistry();
|
|
14483
|
+
const before = registry.size;
|
|
13731
14484
|
this._pruneStaleInPlace(registry);
|
|
14485
|
+
if (registry.size < before) {
|
|
14486
|
+
await withFileLock(this.registryPath, async () => {
|
|
14487
|
+
const fresh = await this._readRegistry({ fresh: true });
|
|
14488
|
+
this._pruneStaleInPlace(fresh);
|
|
14489
|
+
this._registryCache = fresh;
|
|
14490
|
+
this._registryCacheAt = Date.now();
|
|
14491
|
+
await this._writeRegistry(fresh);
|
|
14492
|
+
registry = fresh;
|
|
14493
|
+
});
|
|
14494
|
+
}
|
|
13732
14495
|
const now = Date.now();
|
|
13733
14496
|
return Array.from(registry.values()).map((a) => ({
|
|
13734
14497
|
agentId: a.agentId,
|
|
@@ -13780,6 +14543,20 @@ var GlobalMailbox = class {
|
|
|
13780
14543
|
});
|
|
13781
14544
|
this.publishHqMailboxSnapshot();
|
|
13782
14545
|
}
|
|
14546
|
+
async deregisterClient(clientId) {
|
|
14547
|
+
await this._ensureClientRegistry();
|
|
14548
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
14549
|
+
const registry = await this._readClientRegistry({ fresh: true });
|
|
14550
|
+
this._pruneStaleClientsInPlace(registry);
|
|
14551
|
+
registry.delete(clientId);
|
|
14552
|
+
this._clientRegistryCache = registry;
|
|
14553
|
+
this._clientRegistryCacheAt = Date.now();
|
|
14554
|
+
await this._writeClientRegistry(registry);
|
|
14555
|
+
});
|
|
14556
|
+
this._lastClientHeartbeat.delete(clientId);
|
|
14557
|
+
this._events?.emitCustom("mailbox.client_deregistered", { clientId });
|
|
14558
|
+
this.publishHqMailboxSnapshot();
|
|
14559
|
+
}
|
|
13783
14560
|
async clientHeartbeat(input) {
|
|
13784
14561
|
const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
|
|
13785
14562
|
const now = Date.now();
|
|
@@ -13809,7 +14586,11 @@ var GlobalMailbox = class {
|
|
|
13809
14586
|
async getClientStatuses() {
|
|
13810
14587
|
await this._ensureClientRegistry();
|
|
13811
14588
|
const registry = await this._readClientRegistry();
|
|
14589
|
+
const before = registry.size;
|
|
13812
14590
|
this._pruneStaleClientsInPlace(registry);
|
|
14591
|
+
if (registry.size < before) {
|
|
14592
|
+
await this._writeClientRegistry(registry);
|
|
14593
|
+
}
|
|
13813
14594
|
const now = Date.now();
|
|
13814
14595
|
return Array.from(registry.values()).map((c) => ({
|
|
13815
14596
|
clientId: c.clientId,
|
|
@@ -13821,6 +14602,32 @@ var GlobalMailbox = class {
|
|
|
13821
14602
|
pid: c.pid
|
|
13822
14603
|
})).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
13823
14604
|
}
|
|
14605
|
+
/**
|
|
14606
|
+
* Explicitly purge stale clients from the registry and write back to disk.
|
|
14607
|
+
* Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
|
|
14608
|
+
* Returns the number of entries purged.
|
|
14609
|
+
*
|
|
14610
|
+
* Idempotent — safe to call regularly. The same pruning runs implicitly
|
|
14611
|
+
* inside registerClient, clientHeartbeat, and getClientStatuses, but if
|
|
14612
|
+
* no client registers or heartbeats for a long time, this public method
|
|
14613
|
+
* ensures the file gets cleaned up on demand.
|
|
14614
|
+
*/
|
|
14615
|
+
async purgeClients() {
|
|
14616
|
+
await this._ensureClientRegistry();
|
|
14617
|
+
let purged = 0;
|
|
14618
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
14619
|
+
const registry = await this._readClientRegistry({ fresh: true });
|
|
14620
|
+
const before = registry.size;
|
|
14621
|
+
this._pruneStaleClientsInPlace(registry);
|
|
14622
|
+
purged = before - registry.size;
|
|
14623
|
+
if (purged > 0) {
|
|
14624
|
+
this._clientRegistryCache = registry;
|
|
14625
|
+
this._clientRegistryCacheAt = Date.now();
|
|
14626
|
+
await this._writeClientRegistry(registry);
|
|
14627
|
+
}
|
|
14628
|
+
});
|
|
14629
|
+
return purged;
|
|
14630
|
+
}
|
|
13824
14631
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
13825
14632
|
async close() {
|
|
13826
14633
|
this._registryCache = null;
|
|
@@ -13828,6 +14635,8 @@ var GlobalMailbox = class {
|
|
|
13828
14635
|
this._messageCache = null;
|
|
13829
14636
|
this._messageCacheMtime = -1;
|
|
13830
14637
|
this._messageCacheSize = -1;
|
|
14638
|
+
this._recipientIndex = null;
|
|
14639
|
+
this._senderIndex = null;
|
|
13831
14640
|
}
|
|
13832
14641
|
async clearAll() {
|
|
13833
14642
|
await withFileLock(this.messagePath, async () => {
|
|
@@ -13876,6 +14685,100 @@ var GlobalMailbox = class {
|
|
|
13876
14685
|
remaining
|
|
13877
14686
|
};
|
|
13878
14687
|
}
|
|
14688
|
+
// ── Auto-compaction ─────────────────────────────────────────────────────
|
|
14689
|
+
_autoCompactTimer = null;
|
|
14690
|
+
async autoCompact(opts) {
|
|
14691
|
+
const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
|
|
14692
|
+
const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
|
|
14693
|
+
const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
|
|
14694
|
+
const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
14695
|
+
let expiredPurged = 0;
|
|
14696
|
+
let readByAllPurged = 0;
|
|
14697
|
+
let completedPurged = 0;
|
|
14698
|
+
let incompletePurged = 0;
|
|
14699
|
+
let remaining = 0;
|
|
14700
|
+
let onlineAgentIds = null;
|
|
14701
|
+
try {
|
|
14702
|
+
const statuses = await this.getAgentStatuses();
|
|
14703
|
+
const online = statuses.filter((s) => s.online);
|
|
14704
|
+
onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
|
|
14705
|
+
} catch {
|
|
14706
|
+
onlineAgentIds = null;
|
|
14707
|
+
}
|
|
14708
|
+
await withFileLock(this.messagePath, async () => {
|
|
14709
|
+
const all = await this._readMessagesFresh();
|
|
14710
|
+
const now = Date.now();
|
|
14711
|
+
const cutoffReadAge = now - readMaxAgeMs;
|
|
14712
|
+
const cutoffCompleted = now - completedMaxAgeMs;
|
|
14713
|
+
const cutoffIncomplete = now - incompleteMaxAgeMs;
|
|
14714
|
+
const cutoffDefaultTtl = now - defaultTtlMs;
|
|
14715
|
+
const kept = [];
|
|
14716
|
+
for (const msg of all) {
|
|
14717
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
14718
|
+
if (msg.expiresAt !== void 0) {
|
|
14719
|
+
if (new Date(msg.expiresAt).getTime() < now) {
|
|
14720
|
+
expiredPurged++;
|
|
14721
|
+
continue;
|
|
14722
|
+
}
|
|
14723
|
+
} else if (msgTime < cutoffDefaultTtl) {
|
|
14724
|
+
expiredPurged++;
|
|
14725
|
+
continue;
|
|
14726
|
+
}
|
|
14727
|
+
if (onlineAgentIds !== null && !msg.completed) {
|
|
14728
|
+
const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
|
|
14729
|
+
if (readByAll) {
|
|
14730
|
+
const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
|
|
14731
|
+
const latestRead = Math.max(...readTimes);
|
|
14732
|
+
if (latestRead < cutoffReadAge) {
|
|
14733
|
+
readByAllPurged++;
|
|
14734
|
+
continue;
|
|
14735
|
+
}
|
|
14736
|
+
}
|
|
14737
|
+
}
|
|
14738
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
14739
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
14740
|
+
completedPurged++;
|
|
14741
|
+
continue;
|
|
14742
|
+
}
|
|
14743
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
14744
|
+
incompletePurged++;
|
|
14745
|
+
continue;
|
|
14746
|
+
}
|
|
14747
|
+
kept.push(msg);
|
|
14748
|
+
}
|
|
14749
|
+
remaining = kept.length;
|
|
14750
|
+
if (kept.length < all.length) {
|
|
14751
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
14752
|
+
await fsp11.writeFile(this.messagePath, content, "utf8");
|
|
14753
|
+
}
|
|
14754
|
+
const { mtimeMs, size } = await this._statMessageFile();
|
|
14755
|
+
this._setMessageCache(kept, mtimeMs, size);
|
|
14756
|
+
});
|
|
14757
|
+
const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
|
|
14758
|
+
return {
|
|
14759
|
+
expiredRemoved: expiredPurged,
|
|
14760
|
+
readByAllRemoved: readByAllPurged,
|
|
14761
|
+
stalePurged: completedPurged + incompletePurged,
|
|
14762
|
+
totalRemoved,
|
|
14763
|
+
remaining
|
|
14764
|
+
};
|
|
14765
|
+
}
|
|
14766
|
+
startAutoCompactTimer(opts) {
|
|
14767
|
+
if (this._autoCompactTimer !== null) {
|
|
14768
|
+
clearInterval(this._autoCompactTimer);
|
|
14769
|
+
}
|
|
14770
|
+
const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
|
|
14771
|
+
const timer = setInterval(() => {
|
|
14772
|
+
this.autoCompact(opts).catch(() => {
|
|
14773
|
+
});
|
|
14774
|
+
}, intervalMs);
|
|
14775
|
+
timer.unref?.();
|
|
14776
|
+
this._autoCompactTimer = timer;
|
|
14777
|
+
return () => {
|
|
14778
|
+
clearInterval(timer);
|
|
14779
|
+
this._autoCompactTimer = null;
|
|
14780
|
+
};
|
|
14781
|
+
}
|
|
13879
14782
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
13880
14783
|
/**
|
|
13881
14784
|
* Read all messages from the JSONL file. Always reads + parses the file.
|
|
@@ -13908,44 +14811,59 @@ var GlobalMailbox = class {
|
|
|
13908
14811
|
const buf = Buffer.alloc(tailLen);
|
|
13909
14812
|
await fd.read(buf, 0, tailLen, oldSize);
|
|
13910
14813
|
const tail = buf.toString("utf8");
|
|
13911
|
-
|
|
14814
|
+
if (!tail.endsWith(LINE_SEPARATOR)) {
|
|
14815
|
+
const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR);
|
|
14816
|
+
if (lastSeparatorIdx === -1) {
|
|
14817
|
+
return this._messageCache;
|
|
14818
|
+
}
|
|
14819
|
+
const completeTail = tail.slice(0, lastSeparatorIdx + 1);
|
|
14820
|
+
this._appendIncrementalLines(completeTail);
|
|
14821
|
+
this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
|
|
14822
|
+
return this._messageCache;
|
|
14823
|
+
}
|
|
14824
|
+
this._appendIncrementalLines(tail);
|
|
14825
|
+
this._messageCacheSize = newSize;
|
|
14826
|
+
return this._messageCache;
|
|
14827
|
+
}
|
|
14828
|
+
_appendIncrementalLines(raw) {
|
|
14829
|
+
for (const line of raw.split(LINE_SEPARATOR)) {
|
|
13912
14830
|
if (!line.trim()) continue;
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
delete parsed["readAt"];
|
|
14831
|
+
const message = this._parseLine(line);
|
|
14832
|
+
if (message === null) continue;
|
|
14833
|
+
this._messageCache.push(message);
|
|
14834
|
+
const idx = this._messageCache.length - 1;
|
|
14835
|
+
if (this._recipientIndex !== null) {
|
|
14836
|
+
let set = this._recipientIndex.get(message.to);
|
|
14837
|
+
if (set === void 0) {
|
|
14838
|
+
set = /* @__PURE__ */ new Set();
|
|
14839
|
+
this._recipientIndex.set(message.to, set);
|
|
13923
14840
|
}
|
|
13924
|
-
|
|
13925
|
-
}
|
|
14841
|
+
set.add(idx);
|
|
14842
|
+
}
|
|
14843
|
+
if (this._senderIndex !== null) {
|
|
14844
|
+
let set = this._senderIndex.get(message.from);
|
|
14845
|
+
if (set === void 0) {
|
|
14846
|
+
set = /* @__PURE__ */ new Set();
|
|
14847
|
+
this._senderIndex.set(message.from, set);
|
|
14848
|
+
}
|
|
14849
|
+
set.add(idx);
|
|
13926
14850
|
}
|
|
13927
14851
|
}
|
|
13928
|
-
|
|
14852
|
+
}
|
|
14853
|
+
_parseLine(line) {
|
|
14854
|
+
try {
|
|
14855
|
+
return parseMailboxMessageLine(line);
|
|
14856
|
+
} catch {
|
|
14857
|
+
return null;
|
|
14858
|
+
}
|
|
13929
14859
|
}
|
|
13930
14860
|
/** Parse a JSONL string into MailboxMessage[], including migration. */
|
|
13931
14861
|
_parseLines(raw) {
|
|
13932
14862
|
const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
13933
14863
|
const messages = [];
|
|
13934
14864
|
for (const line of lines) {
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
if (!parsed["readBy"]) {
|
|
13938
|
-
const readBy = {};
|
|
13939
|
-
if (parsed["read"] && parsed["readAt"]) {
|
|
13940
|
-
readBy[parsed["to"]] = parsed["readAt"];
|
|
13941
|
-
}
|
|
13942
|
-
parsed["readBy"] = readBy;
|
|
13943
|
-
delete parsed["read"];
|
|
13944
|
-
delete parsed["readAt"];
|
|
13945
|
-
}
|
|
13946
|
-
messages.push(parsed);
|
|
13947
|
-
} catch {
|
|
13948
|
-
}
|
|
14865
|
+
const message = this._parseLine(line);
|
|
14866
|
+
if (message !== null) messages.push(message);
|
|
13949
14867
|
}
|
|
13950
14868
|
return messages;
|
|
13951
14869
|
}
|
|
@@ -14033,7 +14951,6 @@ var GlobalMailbox = class {
|
|
|
14033
14951
|
try {
|
|
14034
14952
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
14035
14953
|
this._messageCacheMtime = st.mtimeMs;
|
|
14036
|
-
this._messageCacheSize = st.size;
|
|
14037
14954
|
return updated;
|
|
14038
14955
|
} finally {
|
|
14039
14956
|
await fd.close();
|
|
@@ -14070,11 +14987,46 @@ var GlobalMailbox = class {
|
|
|
14070
14987
|
this._messageCache = null;
|
|
14071
14988
|
this._messageCacheMtime = -1;
|
|
14072
14989
|
this._messageCacheSize = -1;
|
|
14990
|
+
this._recipientIndex = null;
|
|
14991
|
+
this._senderIndex = null;
|
|
14073
14992
|
return;
|
|
14074
14993
|
}
|
|
14075
14994
|
this._messageCache = messages;
|
|
14076
14995
|
this._messageCacheMtime = mtime;
|
|
14077
14996
|
this._messageCacheSize = size;
|
|
14997
|
+
this._rebuildRecipientIndex();
|
|
14998
|
+
}
|
|
14999
|
+
/**
|
|
15000
|
+
* Build the recipient AND sender indexes from the current `_messageCache`.
|
|
15001
|
+
* Called only from `_setMessageCache` so both indexes are always in sync
|
|
15002
|
+
* with a freshly-replaced cache array.
|
|
15003
|
+
*/
|
|
15004
|
+
_rebuildRecipientIndex() {
|
|
15005
|
+
const cache = this._messageCache;
|
|
15006
|
+
if (cache === null) {
|
|
15007
|
+
this._recipientIndex = null;
|
|
15008
|
+
this._senderIndex = null;
|
|
15009
|
+
return;
|
|
15010
|
+
}
|
|
15011
|
+
const recIdx = /* @__PURE__ */ new Map();
|
|
15012
|
+
const sndIdx = /* @__PURE__ */ new Map();
|
|
15013
|
+
for (let i = 0; i < cache.length; i++) {
|
|
15014
|
+
const msg = cache[i];
|
|
15015
|
+
let rSet = recIdx.get(msg.to);
|
|
15016
|
+
if (rSet === void 0) {
|
|
15017
|
+
rSet = /* @__PURE__ */ new Set();
|
|
15018
|
+
recIdx.set(msg.to, rSet);
|
|
15019
|
+
}
|
|
15020
|
+
rSet.add(i);
|
|
15021
|
+
let sSet = sndIdx.get(msg.from);
|
|
15022
|
+
if (sSet === void 0) {
|
|
15023
|
+
sSet = /* @__PURE__ */ new Set();
|
|
15024
|
+
sndIdx.set(msg.from, sSet);
|
|
15025
|
+
}
|
|
15026
|
+
sSet.add(i);
|
|
15027
|
+
}
|
|
15028
|
+
this._recipientIndex = recIdx;
|
|
15029
|
+
this._senderIndex = sndIdx;
|
|
14078
15030
|
}
|
|
14079
15031
|
/**
|
|
14080
15032
|
* Append a single just-sent message to the in-memory cache without
|
|
@@ -14101,6 +15053,23 @@ var GlobalMailbox = class {
|
|
|
14101
15053
|
return;
|
|
14102
15054
|
}
|
|
14103
15055
|
this._messageCache.push(msg);
|
|
15056
|
+
const newIdx = this._messageCache.length - 1;
|
|
15057
|
+
if (this._recipientIndex !== null) {
|
|
15058
|
+
let set = this._recipientIndex.get(msg.to);
|
|
15059
|
+
if (set === void 0) {
|
|
15060
|
+
set = /* @__PURE__ */ new Set();
|
|
15061
|
+
this._recipientIndex.set(msg.to, set);
|
|
15062
|
+
}
|
|
15063
|
+
set.add(newIdx);
|
|
15064
|
+
}
|
|
15065
|
+
if (this._senderIndex !== null) {
|
|
15066
|
+
let set = this._senderIndex.get(msg.from);
|
|
15067
|
+
if (set === void 0) {
|
|
15068
|
+
set = /* @__PURE__ */ new Set();
|
|
15069
|
+
this._senderIndex.set(msg.from, set);
|
|
15070
|
+
}
|
|
15071
|
+
set.add(newIdx);
|
|
15072
|
+
}
|
|
14104
15073
|
this._messageCacheMtime = mtime;
|
|
14105
15074
|
this._messageCacheSize = size;
|
|
14106
15075
|
}
|
|
@@ -14132,21 +15101,12 @@ var GlobalMailbox = class {
|
|
|
14132
15101
|
}
|
|
14133
15102
|
}
|
|
14134
15103
|
_pruneStaleInPlace(registry) {
|
|
14135
|
-
const
|
|
14136
|
-
const purgeCutoff = Date.now() - AGENT_PURGE_MS;
|
|
14137
|
-
const toDelete = [];
|
|
15104
|
+
const cutoff = Date.now() - AGENT_STALE_MS;
|
|
14138
15105
|
for (const [id, agent] of registry) {
|
|
14139
|
-
const lastSeen =
|
|
14140
|
-
if (lastSeen <
|
|
14141
|
-
|
|
14142
|
-
continue;
|
|
15106
|
+
const lastSeen = Date.parse(agent.lastSeenAt);
|
|
15107
|
+
if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
|
|
15108
|
+
registry.delete(id);
|
|
14143
15109
|
}
|
|
14144
|
-
if (lastSeen < staleCutoff) {
|
|
14145
|
-
agent.status = "idle";
|
|
14146
|
-
}
|
|
14147
|
-
}
|
|
14148
|
-
for (const id of toDelete) {
|
|
14149
|
-
registry.delete(id);
|
|
14150
15110
|
}
|
|
14151
15111
|
}
|
|
14152
15112
|
async _writeRegistry(registry) {
|
|
@@ -14188,9 +15148,10 @@ var GlobalMailbox = class {
|
|
|
14188
15148
|
}
|
|
14189
15149
|
_pruneStaleClientsInPlace(registry) {
|
|
14190
15150
|
const cutoff = Date.now() - CLIENT_STALE_MS;
|
|
14191
|
-
for (const client of registry
|
|
14192
|
-
|
|
14193
|
-
|
|
15151
|
+
for (const [id, client] of registry) {
|
|
15152
|
+
const lastSeen = Date.parse(client.lastSeenAt);
|
|
15153
|
+
if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
|
|
15154
|
+
registry.delete(id);
|
|
14194
15155
|
}
|
|
14195
15156
|
}
|
|
14196
15157
|
}
|
|
@@ -14248,7 +15209,7 @@ function makeMailboxTool(opts = {}) {
|
|
|
14248
15209
|
description: "Which mailbox operation to perform."
|
|
14249
15210
|
},
|
|
14250
15211
|
to: { type: "string", description: "Recipient agent id, or '*' / 'all' for broadcast." },
|
|
14251
|
-
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review"], description: "Message type." },
|
|
15212
|
+
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Message type." },
|
|
14252
15213
|
subject: { type: "string", description: "Short subject line." },
|
|
14253
15214
|
body: { type: "string", description: "Full message content." },
|
|
14254
15215
|
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
@@ -15302,14 +16263,18 @@ var DefaultMailbox = class {
|
|
|
15302
16263
|
_messageCache = null;
|
|
15303
16264
|
_messageCacheMtime = -1;
|
|
15304
16265
|
_messageCacheSize = -1;
|
|
16266
|
+
/** True when the cache is only the newest MESSAGE_CACHE_MAX_ENTRIES records. */
|
|
16267
|
+
_messageCacheTruncated = false;
|
|
15305
16268
|
/** Primary index: recipient → Set of messages (points into _messageCache). */
|
|
15306
16269
|
_byTo = /* @__PURE__ */ new Map();
|
|
15307
16270
|
/** Secondary index: sender → Set of messages (points into _messageCache). */
|
|
15308
16271
|
_byFrom = /* @__PURE__ */ new Map();
|
|
15309
16272
|
/** Counts malformed JSONL lines skipped during parsing for observability. */
|
|
15310
16273
|
_corruptionCount = 0;
|
|
15311
|
-
|
|
16274
|
+
logger;
|
|
16275
|
+
constructor(sessionDir, opts) {
|
|
15312
16276
|
this.filePath = path15.join(sessionDir, MAILBOX_FILE2);
|
|
16277
|
+
this.logger = opts?.logger;
|
|
15313
16278
|
}
|
|
15314
16279
|
get mailboxPath() {
|
|
15315
16280
|
return this.filePath;
|
|
@@ -15327,7 +16292,7 @@ var DefaultMailbox = class {
|
|
|
15327
16292
|
// "all" is an accepted spelling of the broadcast address — canonical
|
|
15328
16293
|
// form on disk is '*' so every query/checker matches it.
|
|
15329
16294
|
to: normalizeRecipient(input.to),
|
|
15330
|
-
type: input.type,
|
|
16295
|
+
type: normalizeMailboxMessageType(input.type),
|
|
15331
16296
|
subject: input.subject,
|
|
15332
16297
|
body: input.body,
|
|
15333
16298
|
priority: input.priority ?? "normal",
|
|
@@ -15350,12 +16315,14 @@ var DefaultMailbox = class {
|
|
|
15350
16315
|
}
|
|
15351
16316
|
// ── Query ─────────────────────────────────────────────────────────────
|
|
15352
16317
|
async query(q) {
|
|
16318
|
+
const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
|
|
15353
16319
|
const needFullScan = q.unreadBy !== void 0 || q.since !== void 0;
|
|
16320
|
+
const cached = await this._readAllCached(false);
|
|
15354
16321
|
let candidates;
|
|
16322
|
+
let candidatesComplete = !this._messageCacheTruncated || cached !== this._messageCache;
|
|
15355
16323
|
if (needFullScan) {
|
|
15356
|
-
candidates =
|
|
16324
|
+
candidates = cached;
|
|
15357
16325
|
} else {
|
|
15358
|
-
await this._readAllCached();
|
|
15359
16326
|
if (q.to !== void 0) {
|
|
15360
16327
|
const direct = this._byTo.get(q.to);
|
|
15361
16328
|
const broadcast = this._byTo.get("*");
|
|
@@ -15363,10 +16330,12 @@ var DefaultMailbox = class {
|
|
|
15363
16330
|
if (direct) for (const m of direct) combined.set(m.id, m);
|
|
15364
16331
|
if (broadcast) for (const m of broadcast) combined.set(m.id, m);
|
|
15365
16332
|
candidates = Array.from(combined.values());
|
|
16333
|
+
candidatesComplete = !this._messageCacheTruncated;
|
|
15366
16334
|
} else if (q.from !== void 0) {
|
|
15367
16335
|
candidates = Array.from(this._byFrom.get(q.from) ?? []);
|
|
16336
|
+
candidatesComplete = !this._messageCacheTruncated;
|
|
15368
16337
|
} else {
|
|
15369
|
-
candidates =
|
|
16338
|
+
candidates = cached;
|
|
15370
16339
|
}
|
|
15371
16340
|
}
|
|
15372
16341
|
const limit = q.limit ?? 50;
|
|
@@ -15377,27 +16346,37 @@ var DefaultMailbox = class {
|
|
|
15377
16346
|
if (q.from !== void 0 && msg.from !== q.from) return false;
|
|
15378
16347
|
if (q.unreadBy !== void 0 && q.unreadBy in msg.readBy) return false;
|
|
15379
16348
|
if (q.incompleteOnly && msg.completed) return false;
|
|
15380
|
-
if (
|
|
16349
|
+
if (queryType !== void 0 && msg.type !== queryType) return false;
|
|
15381
16350
|
if (order !== null && (order[msg.priority] ?? 1) < minPriorityRank)
|
|
15382
16351
|
return false;
|
|
15383
16352
|
if (q.since !== void 0 && msg.timestamp <= q.since) return false;
|
|
15384
16353
|
return true;
|
|
15385
16354
|
};
|
|
15386
|
-
|
|
16355
|
+
const selectNewest = (source) => {
|
|
15387
16356
|
const out = [];
|
|
15388
|
-
for (let i =
|
|
15389
|
-
const msg =
|
|
16357
|
+
for (let i = source.length - 1; i >= 0 && out.length < limit; i--) {
|
|
16358
|
+
const msg = source[i];
|
|
15390
16359
|
if (passes(msg)) out.push(msg);
|
|
15391
16360
|
}
|
|
15392
16361
|
return out;
|
|
15393
|
-
}
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
}
|
|
15399
|
-
|
|
15400
|
-
|
|
16362
|
+
};
|
|
16363
|
+
if (order === null) {
|
|
16364
|
+
const out = selectNewest(candidates);
|
|
16365
|
+
if (out.length >= limit || candidatesComplete) return out;
|
|
16366
|
+
return selectNewest(await this._readAll());
|
|
16367
|
+
}
|
|
16368
|
+
const selectByPriority = (source) => {
|
|
16369
|
+
const filtered2 = [];
|
|
16370
|
+
for (let i = source.length - 1; i >= 0; i--) {
|
|
16371
|
+
const msg = source[i];
|
|
16372
|
+
if (passes(msg)) filtered2.push(msg);
|
|
16373
|
+
}
|
|
16374
|
+
filtered2.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
16375
|
+
return filtered2.slice(0, limit);
|
|
16376
|
+
};
|
|
16377
|
+
const filtered = selectByPriority(candidates);
|
|
16378
|
+
if (filtered.length >= limit || candidatesComplete) return filtered;
|
|
16379
|
+
return selectByPriority(await this._readAll());
|
|
15401
16380
|
}
|
|
15402
16381
|
// ── Ack ───────────────────────────────────────────────────────────────
|
|
15403
16382
|
async ack(input) {
|
|
@@ -15454,7 +16433,7 @@ var DefaultMailbox = class {
|
|
|
15454
16433
|
for (const m of all) {
|
|
15455
16434
|
if (m.type !== "status") continue;
|
|
15456
16435
|
const existing = latest.get(m.from);
|
|
15457
|
-
if (existing && m.timestamp
|
|
16436
|
+
if (existing && m.timestamp < existing.lastActivityAt) continue;
|
|
15458
16437
|
latest.set(m.from, {
|
|
15459
16438
|
agentId: m.from,
|
|
15460
16439
|
name: m.taskContext?.agentName ?? m.from,
|
|
@@ -15496,6 +16475,7 @@ var DefaultMailbox = class {
|
|
|
15496
16475
|
this._messageCache = null;
|
|
15497
16476
|
this._messageCacheMtime = -1;
|
|
15498
16477
|
this._messageCacheSize = -1;
|
|
16478
|
+
this._messageCacheTruncated = false;
|
|
15499
16479
|
this._byTo.clear();
|
|
15500
16480
|
this._byFrom.clear();
|
|
15501
16481
|
}
|
|
@@ -15547,13 +16527,32 @@ var DefaultMailbox = class {
|
|
|
15547
16527
|
};
|
|
15548
16528
|
}
|
|
15549
16529
|
// ── Client registry stubs (not applicable per-session) ─────────────────
|
|
16530
|
+
async autoCompact(_opts) {
|
|
16531
|
+
const purged = await this.purgeStale(_opts);
|
|
16532
|
+
return {
|
|
16533
|
+
readByAllRemoved: 0,
|
|
16534
|
+
expiredRemoved: 0,
|
|
16535
|
+
stalePurged: purged.totalPurged,
|
|
16536
|
+
totalRemoved: purged.totalPurged,
|
|
16537
|
+
remaining: purged.remaining
|
|
16538
|
+
};
|
|
16539
|
+
}
|
|
16540
|
+
startAutoCompactTimer(_opts) {
|
|
16541
|
+
return () => {
|
|
16542
|
+
};
|
|
16543
|
+
}
|
|
15550
16544
|
async registerClient(_input) {
|
|
15551
16545
|
}
|
|
15552
16546
|
async clientHeartbeat(_input) {
|
|
15553
16547
|
}
|
|
16548
|
+
async deregisterClient(_clientId) {
|
|
16549
|
+
}
|
|
15554
16550
|
async getClientStatuses() {
|
|
15555
16551
|
return [];
|
|
15556
16552
|
}
|
|
16553
|
+
async purgeClients() {
|
|
16554
|
+
return 0;
|
|
16555
|
+
}
|
|
15557
16556
|
// ── Internal ──────────────────────────────────────────────────────────
|
|
15558
16557
|
async _readAll() {
|
|
15559
16558
|
try {
|
|
@@ -15567,60 +16566,62 @@ var DefaultMailbox = class {
|
|
|
15567
16566
|
/**
|
|
15568
16567
|
* Read only newly-appended bytes from the file and append them to the
|
|
15569
16568
|
* in-memory cache, avoiding a full re-read when the file only grew.
|
|
16569
|
+
*
|
|
16570
|
+
* Guards against a half-written tail: when the tail doesn't end with
|
|
16571
|
+
* LINE_SEPARATOR, another process may be mid-append. Parsing a partial
|
|
16572
|
+
* JSON line would throw (caught below), but worse: the NEXT incremental
|
|
16573
|
+
* read would skip that same line because the size tracker already advanced.
|
|
15570
16574
|
*/
|
|
15571
16575
|
async _readNewMessagesOnly(fd, oldSize, newSize) {
|
|
15572
16576
|
const tailLen = newSize - oldSize;
|
|
15573
16577
|
const buf = Buffer.alloc(tailLen);
|
|
15574
16578
|
await fd.read(buf, 0, tailLen, oldSize);
|
|
15575
16579
|
const tail = buf.toString("utf8");
|
|
15576
|
-
|
|
16580
|
+
if (!tail.endsWith(LINE_SEPARATOR2)) {
|
|
16581
|
+
const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR2);
|
|
16582
|
+
if (lastSeparatorIdx === -1) {
|
|
16583
|
+
return this._messageCache;
|
|
16584
|
+
}
|
|
16585
|
+
const completeTail = tail.slice(0, lastSeparatorIdx + 1);
|
|
16586
|
+
this._appendIncrementalLines(completeTail);
|
|
16587
|
+
this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
|
|
16588
|
+
return this._messageCache;
|
|
16589
|
+
}
|
|
16590
|
+
this._appendIncrementalLines(tail);
|
|
16591
|
+
this._messageCacheSize = newSize;
|
|
16592
|
+
return this._messageCache;
|
|
16593
|
+
}
|
|
16594
|
+
_appendIncrementalLines(raw) {
|
|
16595
|
+
for (const line of raw.split(LINE_SEPARATOR2)) {
|
|
15577
16596
|
if (!line.trim()) continue;
|
|
15578
|
-
|
|
15579
|
-
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
} catch (err) {
|
|
15593
|
-
this._corruptionCount++;
|
|
15594
|
-
console.debug(
|
|
15595
|
-
`[mailbox] skipped malformed line during incremental read: ${err.message}`
|
|
16597
|
+
const msg = this._parseLine(line, "incremental read");
|
|
16598
|
+
if (msg === null) continue;
|
|
16599
|
+
this._pushToCache(msg);
|
|
16600
|
+
}
|
|
16601
|
+
}
|
|
16602
|
+
_parseLine(line, source) {
|
|
16603
|
+
try {
|
|
16604
|
+
return parseMailboxMessageLine(line);
|
|
16605
|
+
} catch (err) {
|
|
16606
|
+
this._corruptionCount++;
|
|
16607
|
+
if (this.logger) {
|
|
16608
|
+
this.logger.debug(
|
|
16609
|
+
"Skipped malformed mailbox line",
|
|
16610
|
+
{ source, error: err.message, corruptionCount: this._corruptionCount }
|
|
15596
16611
|
);
|
|
16612
|
+
} else {
|
|
16613
|
+
console.debug(`[mailbox] skipped malformed line during ${source}: ${err.message}`);
|
|
15597
16614
|
}
|
|
16615
|
+
return null;
|
|
15598
16616
|
}
|
|
15599
|
-
return this._messageCache;
|
|
15600
16617
|
}
|
|
15601
16618
|
/** Parse a JSONL string into MailboxMessage[], including migration. */
|
|
15602
16619
|
_parseLines(raw) {
|
|
15603
16620
|
const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
|
|
15604
16621
|
const messages = [];
|
|
15605
16622
|
for (const line of lines) {
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
if (!parsed["readBy"]) {
|
|
15609
|
-
const readBy = {};
|
|
15610
|
-
if (parsed["read"] && parsed["readAt"]) {
|
|
15611
|
-
readBy[parsed["to"] ?? "unknown"] = parsed["readAt"];
|
|
15612
|
-
}
|
|
15613
|
-
parsed["readBy"] = readBy;
|
|
15614
|
-
delete parsed["read"];
|
|
15615
|
-
delete parsed["readAt"];
|
|
15616
|
-
}
|
|
15617
|
-
messages.push(parsed);
|
|
15618
|
-
} catch (err) {
|
|
15619
|
-
this._corruptionCount++;
|
|
15620
|
-
console.debug(
|
|
15621
|
-
`[mailbox] skipped malformed line during full parse: ${err.message}`
|
|
15622
|
-
);
|
|
15623
|
-
}
|
|
16623
|
+
const message = this._parseLine(line, "full parse");
|
|
16624
|
+
if (message !== null) messages.push(message);
|
|
15624
16625
|
}
|
|
15625
16626
|
return messages;
|
|
15626
16627
|
}
|
|
@@ -15641,10 +16642,11 @@ var DefaultMailbox = class {
|
|
|
15641
16642
|
throw err;
|
|
15642
16643
|
}
|
|
15643
16644
|
}
|
|
15644
|
-
async _readAllCached() {
|
|
16645
|
+
async _readAllCached(requireComplete = true) {
|
|
15645
16646
|
try {
|
|
15646
16647
|
const st = await fsp12.stat(this.filePath);
|
|
15647
16648
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
16649
|
+
if (requireComplete && this._messageCacheTruncated) return this._readAll();
|
|
15648
16650
|
return this._messageCache;
|
|
15649
16651
|
}
|
|
15650
16652
|
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
@@ -15652,7 +16654,7 @@ var DefaultMailbox = class {
|
|
|
15652
16654
|
try {
|
|
15653
16655
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
15654
16656
|
this._messageCacheMtime = st.mtimeMs;
|
|
15655
|
-
this.
|
|
16657
|
+
if (requireComplete && this._messageCacheTruncated) return this._readAll();
|
|
15656
16658
|
return updated;
|
|
15657
16659
|
} finally {
|
|
15658
16660
|
await fd.close();
|
|
@@ -15670,28 +16672,18 @@ var DefaultMailbox = class {
|
|
|
15670
16672
|
}
|
|
15671
16673
|
}
|
|
15672
16674
|
_setMessageCache(messages, mtime, size) {
|
|
15673
|
-
|
|
15674
|
-
|
|
15675
|
-
|
|
15676
|
-
this._messageCacheSize = -1;
|
|
15677
|
-
this._byTo.clear();
|
|
15678
|
-
this._byFrom.clear();
|
|
15679
|
-
return;
|
|
15680
|
-
}
|
|
15681
|
-
this._messageCache = messages;
|
|
15682
|
-
this._buildIndexes(messages);
|
|
16675
|
+
this._messageCacheTruncated = messages.length > MESSAGE_CACHE_MAX_ENTRIES2;
|
|
16676
|
+
this._messageCache = this._messageCacheTruncated ? messages.slice(-MESSAGE_CACHE_MAX_ENTRIES2) : messages;
|
|
16677
|
+
this._buildIndexes(this._messageCache);
|
|
15683
16678
|
this._messageCacheMtime = mtime;
|
|
15684
16679
|
this._messageCacheSize = size;
|
|
15685
16680
|
}
|
|
15686
16681
|
_pushToCache(msg) {
|
|
15687
16682
|
if (this._messageCache === null) return;
|
|
15688
16683
|
if (this._messageCache.length >= MESSAGE_CACHE_MAX_ENTRIES2) {
|
|
15689
|
-
this._messageCache
|
|
15690
|
-
this.
|
|
15691
|
-
this.
|
|
15692
|
-
this._byTo.clear();
|
|
15693
|
-
this._byFrom.clear();
|
|
15694
|
-
return;
|
|
16684
|
+
const evicted = this._messageCache.shift();
|
|
16685
|
+
if (evicted !== void 0) this._unindexMsg(evicted);
|
|
16686
|
+
this._messageCacheTruncated = true;
|
|
15695
16687
|
}
|
|
15696
16688
|
this._messageCache.push(msg);
|
|
15697
16689
|
this._indexMsg(msg);
|
|
@@ -15719,6 +16711,15 @@ var DefaultMailbox = class {
|
|
|
15719
16711
|
this._byFrom.set(msg.from, /* @__PURE__ */ new Set([msg]));
|
|
15720
16712
|
}
|
|
15721
16713
|
}
|
|
16714
|
+
/** Remove an evicted message from both indexes without rebuilding them. */
|
|
16715
|
+
_unindexMsg(msg) {
|
|
16716
|
+
const toSet = this._byTo.get(msg.to);
|
|
16717
|
+
toSet?.delete(msg);
|
|
16718
|
+
if (toSet?.size === 0) this._byTo.delete(msg.to);
|
|
16719
|
+
const fromSet = this._byFrom.get(msg.from);
|
|
16720
|
+
fromSet?.delete(msg);
|
|
16721
|
+
if (fromSet?.size === 0) this._byFrom.delete(msg.from);
|
|
16722
|
+
}
|
|
15722
16723
|
};
|
|
15723
16724
|
|
|
15724
16725
|
// src/coordination/mailbox-actions.ts
|
|
@@ -16018,7 +17019,7 @@ async function saveLog(storageDir, log) {
|
|
|
16018
17019
|
await fs2.rename(tmp, logPath(storageDir));
|
|
16019
17020
|
}
|
|
16020
17021
|
function detectEcosystem(manifestPath) {
|
|
16021
|
-
const name = path16.basename(manifestPath).toLowerCase();
|
|
17022
|
+
const name = path16.win32.basename(manifestPath).toLowerCase();
|
|
16022
17023
|
if (name === "package.json") return "npm";
|
|
16023
17024
|
if (name === "go.mod") return "go";
|
|
16024
17025
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -16381,7 +17382,8 @@ function isProcessAlive(pid) {
|
|
|
16381
17382
|
encoding: "utf-8",
|
|
16382
17383
|
stdio: ["pipe", "pipe", "pipe"]
|
|
16383
17384
|
});
|
|
16384
|
-
|
|
17385
|
+
const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
|
|
17386
|
+
return match !== null && match[1] === String(pid);
|
|
16385
17387
|
} catch {
|
|
16386
17388
|
return false;
|
|
16387
17389
|
}
|
|
@@ -16586,7 +17588,7 @@ var AdaptiveConcurrencyController = class {
|
|
|
16586
17588
|
// src/coordination/agent-monitor.ts
|
|
16587
17589
|
import * as fs3 from "node:fs/promises";
|
|
16588
17590
|
import * as path18 from "node:path";
|
|
16589
|
-
var AgentMonitorService = class {
|
|
17591
|
+
var AgentMonitorService = class _AgentMonitorService {
|
|
16590
17592
|
_fleetBus;
|
|
16591
17593
|
_events;
|
|
16592
17594
|
_sessionId;
|
|
@@ -16596,6 +17598,15 @@ var AgentMonitorService = class {
|
|
|
16596
17598
|
_onEntry;
|
|
16597
17599
|
/** Per-subagent virtual sessions. */
|
|
16598
17600
|
_sessions = /* @__PURE__ */ new Map();
|
|
17601
|
+
/**
|
|
17602
|
+
* Per-subagent OPEN streaming segment. Provider text/thinking deltas
|
|
17603
|
+
* arrive word-by-word; writing one transcript entry per delta fragments
|
|
17604
|
+
* every surface (ring, JSONL, HQ) into unreadable single-word lines.
|
|
17605
|
+
* Instead, consecutive same-kind/same-iteration deltas append into ONE
|
|
17606
|
+
* entry that is already in the ring (live views see it grow in place);
|
|
17607
|
+
* the JSONL line is written once, when the segment closes.
|
|
17608
|
+
*/
|
|
17609
|
+
_openStreams = /* @__PURE__ */ new Map();
|
|
16599
17610
|
/** Disposers for FleetBus subscriptions, keyed by subagentId. */
|
|
16600
17611
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
16601
17612
|
/** Generic fleet-wide subscription disposer. */
|
|
@@ -16658,6 +17669,9 @@ var AgentMonitorService = class {
|
|
|
16658
17669
|
stop() {
|
|
16659
17670
|
if (!this._started) return;
|
|
16660
17671
|
this._started = false;
|
|
17672
|
+
for (const subagentId of [...this._openStreams.keys()]) {
|
|
17673
|
+
this._closeStream(subagentId);
|
|
17674
|
+
}
|
|
16661
17675
|
if (this._fleetDisposer) {
|
|
16662
17676
|
this._fleetDisposer();
|
|
16663
17677
|
this._fleetDisposer = void 0;
|
|
@@ -16726,6 +17740,7 @@ var AgentMonitorService = class {
|
|
|
16726
17740
|
}
|
|
16727
17741
|
/** Drop a retired subagent from every live in-memory monitor surface. */
|
|
16728
17742
|
removeSubagent(subagentId) {
|
|
17743
|
+
this._closeStream(subagentId);
|
|
16729
17744
|
this._sessions.delete(subagentId);
|
|
16730
17745
|
const dispose = this._subscriptions.get(subagentId);
|
|
16731
17746
|
if (dispose) dispose();
|
|
@@ -16739,31 +17754,13 @@ var AgentMonitorService = class {
|
|
|
16739
17754
|
case "provider.text_delta": {
|
|
16740
17755
|
const text = payload.text;
|
|
16741
17756
|
if (!text || text.length === 0) return;
|
|
16742
|
-
|
|
16743
|
-
this._addEntry(subagentId, {
|
|
16744
|
-
id: this._uid(),
|
|
16745
|
-
subagentId,
|
|
16746
|
-
agentName: session.agentName,
|
|
16747
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16748
|
-
kind: "text",
|
|
16749
|
-
content: text,
|
|
16750
|
-
iteration
|
|
16751
|
-
});
|
|
17757
|
+
this._appendStreamDelta(subagentId, session, "text", text, payload.iteration ?? 0);
|
|
16752
17758
|
break;
|
|
16753
17759
|
}
|
|
16754
17760
|
case "provider.thinking_delta": {
|
|
16755
17761
|
const text = payload.text;
|
|
16756
17762
|
if (!text || text.length === 0) return;
|
|
16757
|
-
|
|
16758
|
-
this._addEntry(subagentId, {
|
|
16759
|
-
id: this._uid(),
|
|
16760
|
-
subagentId,
|
|
16761
|
-
agentName: session.agentName,
|
|
16762
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16763
|
-
kind: "thinking",
|
|
16764
|
-
content: text,
|
|
16765
|
-
iteration
|
|
16766
|
-
});
|
|
17763
|
+
this._appendStreamDelta(subagentId, session, "thinking", text, payload.iteration ?? 0);
|
|
16767
17764
|
break;
|
|
16768
17765
|
}
|
|
16769
17766
|
case "tool.started": {
|
|
@@ -16820,15 +17817,66 @@ var AgentMonitorService = class {
|
|
|
16820
17817
|
}
|
|
16821
17818
|
}
|
|
16822
17819
|
}
|
|
17820
|
+
/** Max characters accumulated into one streaming segment before rolling over. */
|
|
17821
|
+
static _MAX_SEGMENT_CHARS = 2e4;
|
|
17822
|
+
/**
|
|
17823
|
+
* Fold a provider text/thinking delta into the subagent's open streaming
|
|
17824
|
+
* segment, or open a new one. A segment breaks on kind change, iteration
|
|
17825
|
+
* change, size cap, or any non-delta entry (see `_addEntry`). The entry
|
|
17826
|
+
* is pushed to the ring when the segment OPENS (live views poll the ring
|
|
17827
|
+
* and watch it grow in place); the JSONL line AND the timeline/HQ
|
|
17828
|
+
* announcement happen once, on close, with the COMPLETE segment — so
|
|
17829
|
+
* downstream consumers get one whole message instead of a word-per-event
|
|
17830
|
+
* firehose (or, worse, only the first word).
|
|
17831
|
+
*/
|
|
17832
|
+
_appendStreamDelta(subagentId, session, kind, text, iteration) {
|
|
17833
|
+
const open7 = this._openStreams.get(subagentId);
|
|
17834
|
+
if (open7 && open7.entry.kind === kind && open7.entry.iteration === iteration && open7.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
|
|
17835
|
+
open7.entry.content += text;
|
|
17836
|
+
return;
|
|
17837
|
+
}
|
|
17838
|
+
this._closeStream(subagentId);
|
|
17839
|
+
const entry = {
|
|
17840
|
+
id: this._uid(),
|
|
17841
|
+
subagentId,
|
|
17842
|
+
agentName: session.agentName,
|
|
17843
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17844
|
+
kind,
|
|
17845
|
+
content: text,
|
|
17846
|
+
iteration
|
|
17847
|
+
};
|
|
17848
|
+
this._openStreams.set(subagentId, { entry });
|
|
17849
|
+
session.transcript.push(entry);
|
|
17850
|
+
if (session.transcript.length > this._maxEntries) {
|
|
17851
|
+
session.transcript.splice(0, session.transcript.length - this._maxEntries);
|
|
17852
|
+
}
|
|
17853
|
+
}
|
|
17854
|
+
/**
|
|
17855
|
+
* Close the subagent's open streaming segment: persist it to JSONL and
|
|
17856
|
+
* announce the completed entry on the local bus + HQ callback.
|
|
17857
|
+
*/
|
|
17858
|
+
_closeStream(subagentId) {
|
|
17859
|
+
const open7 = this._openStreams.get(subagentId);
|
|
17860
|
+
if (!open7) return;
|
|
17861
|
+
this._openStreams.delete(subagentId);
|
|
17862
|
+
this._appendToFile(subagentId, open7.entry).catch(() => {
|
|
17863
|
+
});
|
|
17864
|
+
this._emitEntry(open7.entry);
|
|
17865
|
+
}
|
|
16823
17866
|
_addEntry(subagentId, entry) {
|
|
16824
17867
|
const session = this._sessions.get(subagentId);
|
|
16825
17868
|
if (!session) return;
|
|
17869
|
+
this._closeStream(subagentId);
|
|
16826
17870
|
session.transcript.push(entry);
|
|
16827
17871
|
if (session.transcript.length > this._maxEntries) {
|
|
16828
17872
|
session.transcript.splice(0, session.transcript.length - this._maxEntries);
|
|
16829
17873
|
}
|
|
16830
17874
|
this._appendToFile(subagentId, entry).catch(() => {
|
|
16831
17875
|
});
|
|
17876
|
+
this._emitEntry(entry);
|
|
17877
|
+
}
|
|
17878
|
+
/** Announce a transcript entry on the local bus + HQ callback. */
|
|
17879
|
+
_emitEntry(entry) {
|
|
16832
17880
|
this._events.emit("agent.timeline.message", {
|
|
16833
17881
|
sessionId: this._sessionId,
|
|
16834
17882
|
subagentId: entry.subagentId,
|
|
@@ -16855,8 +17903,11 @@ var AgentMonitorService = class {
|
|
|
16855
17903
|
_formatToolUse(name, input) {
|
|
16856
17904
|
if (input === void 0) return `${name}()`;
|
|
16857
17905
|
const body = this._stringifyForTimeline(input);
|
|
16858
|
-
|
|
16859
|
-
|
|
17906
|
+
if (!body) return `${name}()`;
|
|
17907
|
+
const inline = body.replace(/\s+/g, " ").trim();
|
|
17908
|
+
const head = inline.length <= 160 ? `${name}(${inline})` : `${name}(${inline.slice(0, 159)}\u2026)`;
|
|
17909
|
+
return `${head}
|
|
17910
|
+
${body}`;
|
|
16860
17911
|
}
|
|
16861
17912
|
_formatToolResult(summary, output, outputBytes) {
|
|
16862
17913
|
if (!output) return summary;
|
|
@@ -16893,8 +17944,6 @@ var AutonomousBrain = class {
|
|
|
16893
17944
|
maxRetries;
|
|
16894
17945
|
consensusRiskThreshold;
|
|
16895
17946
|
selfImprove;
|
|
16896
|
-
/** Decision history for self-improvement and audit. */
|
|
16897
|
-
decisionHistory = [];
|
|
16898
17947
|
/** Tracks failure patterns for self-improvement. */
|
|
16899
17948
|
failurePatterns = /* @__PURE__ */ new Map();
|
|
16900
17949
|
RISK_ORDER = ["low", "medium", "high", "critical"];
|
|
@@ -17206,7 +18255,6 @@ ${ctx.error}`);
|
|
|
17206
18255
|
madeAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17207
18256
|
context: input.context
|
|
17208
18257
|
});
|
|
17209
|
-
this.decisionHistory.push(node);
|
|
17210
18258
|
return node;
|
|
17211
18259
|
}
|
|
17212
18260
|
_inferRoles(task) {
|
|
@@ -17235,7 +18283,8 @@ import { randomUUID as randomUUID16 } from "node:crypto";
|
|
|
17235
18283
|
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
17236
18284
|
import * as fsp14 from "node:fs/promises";
|
|
17237
18285
|
import * as path19 from "node:path";
|
|
17238
|
-
var
|
|
18286
|
+
var DEFAULT_MAX_NODES = 2e3;
|
|
18287
|
+
var KnowledgeGraph = class _KnowledgeGraph {
|
|
17239
18288
|
nodes = /* @__PURE__ */ new Map();
|
|
17240
18289
|
index = /* @__PURE__ */ new Map();
|
|
17241
18290
|
// tag/field → node ids
|
|
@@ -17243,6 +18292,7 @@ var KnowledgeGraph = class {
|
|
|
17243
18292
|
pendingDeliveries = /* @__PURE__ */ new Map();
|
|
17244
18293
|
filePath;
|
|
17245
18294
|
graphFilePath;
|
|
18295
|
+
maxNodes;
|
|
17246
18296
|
/**
|
|
17247
18297
|
* Stable per-node insertion sequence. `nodes` (a Map) preserves insertion
|
|
17248
18298
|
* order even across `update()` (Map.set on an existing key keeps its slot),
|
|
@@ -17261,9 +18311,10 @@ var KnowledgeGraph = class {
|
|
|
17261
18311
|
getIndex() {
|
|
17262
18312
|
return this.index;
|
|
17263
18313
|
}
|
|
17264
|
-
constructor(sessionDir) {
|
|
18314
|
+
constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES) {
|
|
17265
18315
|
this.filePath = path19.join(sessionDir, "_knowledge_graph");
|
|
17266
18316
|
this.graphFilePath = path19.join(this.filePath, "graph.jsonl");
|
|
18317
|
+
this.maxNodes = maxNodes;
|
|
17267
18318
|
}
|
|
17268
18319
|
// ── Write ──────────────────────────────────────────────────────────────
|
|
17269
18320
|
/**
|
|
@@ -17275,6 +18326,7 @@ var KnowledgeGraph = class {
|
|
|
17275
18326
|
this.nodes.set(full.id, full);
|
|
17276
18327
|
this._trackSeq(full.id);
|
|
17277
18328
|
this._addToIndex(full, this._indexKeys(full));
|
|
18329
|
+
this._prune();
|
|
17278
18330
|
await this._persist(full);
|
|
17279
18331
|
this._deliver(full);
|
|
17280
18332
|
return full;
|
|
@@ -17287,10 +18339,48 @@ var KnowledgeGraph = class {
|
|
|
17287
18339
|
const updated = { ...existing, ...patch };
|
|
17288
18340
|
this.nodes.set(id, updated);
|
|
17289
18341
|
this._addToIndex(updated, this._indexKeys(updated));
|
|
18342
|
+
this._prune();
|
|
17290
18343
|
this._deliver(updated);
|
|
17291
18344
|
await this._append(updated);
|
|
17292
18345
|
return updated;
|
|
17293
18346
|
}
|
|
18347
|
+
/**
|
|
18348
|
+
* True when the node is in a terminal / settled state — no future mutations
|
|
18349
|
+
* are expected. Such nodes are the first to be evicted when the in-memory
|
|
18350
|
+
* cap is reached. The JSONL file on disk retains the full history.
|
|
18351
|
+
*/
|
|
18352
|
+
static _isTerminal(node) {
|
|
18353
|
+
if (node.type === "goal") return node.status === "done" || node.status === "failed";
|
|
18354
|
+
if (node.type === "change") return node.status === "rejected" || node.status === "rolled_back";
|
|
18355
|
+
if (node.type === "vote") return true;
|
|
18356
|
+
return false;
|
|
18357
|
+
}
|
|
18358
|
+
/**
|
|
18359
|
+
* Evict oldest terminal-state nodes when the in-memory cap is exceeded.
|
|
18360
|
+
* The JSONL file is the authoritative record; eviction only affects memory.
|
|
18361
|
+
*/
|
|
18362
|
+
_prune() {
|
|
18363
|
+
if (this.nodes.size <= this.maxNodes) return;
|
|
18364
|
+
const toEvict = this.nodes.size - this.maxNodes;
|
|
18365
|
+
const terminal = [];
|
|
18366
|
+
for (const [id, node] of this.nodes) {
|
|
18367
|
+
if (_KnowledgeGraph._isTerminal(node)) terminal.push({ id, seq: this.seq.get(id) ?? 0 });
|
|
18368
|
+
}
|
|
18369
|
+
terminal.sort((a, b) => a.seq - b.seq);
|
|
18370
|
+
const evicted = new Set(terminal.slice(0, toEvict).map((e) => e.id));
|
|
18371
|
+
if (evicted.size === 0) return;
|
|
18372
|
+
for (const id of evicted) {
|
|
18373
|
+
const node = this.nodes.get(id);
|
|
18374
|
+
if (!node) continue;
|
|
18375
|
+
this._removeFromIndex(node, this._indexKeys(node));
|
|
18376
|
+
this.nodes.delete(id);
|
|
18377
|
+
this.seq.delete(id);
|
|
18378
|
+
for (const pending of this.pendingDeliveries.values()) {
|
|
18379
|
+
const idx = pending.findIndex((n) => n.id === id);
|
|
18380
|
+
if (idx >= 0) pending.splice(idx, 1);
|
|
18381
|
+
}
|
|
18382
|
+
}
|
|
18383
|
+
}
|
|
17294
18384
|
// ── Read ───────────────────────────────────────────────────────────────
|
|
17295
18385
|
get(id) {
|
|
17296
18386
|
return this.nodes.get(id);
|
|
@@ -18723,6 +19813,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
18723
19813
|
mailbox;
|
|
18724
19814
|
events;
|
|
18725
19815
|
onCoordinatorEvent;
|
|
19816
|
+
logger;
|
|
18726
19817
|
running = false;
|
|
18727
19818
|
iterationCount = 0;
|
|
18728
19819
|
lastSyncAt = 0;
|
|
@@ -18739,6 +19830,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
18739
19830
|
this.mailbox = opts.mailbox ?? void 0;
|
|
18740
19831
|
this.events = opts.events ?? void 0;
|
|
18741
19832
|
this.onCoordinatorEvent = opts.onCoordinatorEvent;
|
|
19833
|
+
this.logger = opts.logger;
|
|
18742
19834
|
this.graph = new KnowledgeGraph(opts.sessionDir);
|
|
18743
19835
|
this.dag = new TaskDAG();
|
|
18744
19836
|
this.auction = new TaskAuctioneer({
|
|
@@ -18897,15 +19989,22 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
18897
19989
|
stop() {
|
|
18898
19990
|
if (!this.running) return;
|
|
18899
19991
|
this.running = false;
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
event: "autonomous_coordinator.stop_signal",
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
19992
|
+
if (this.logger) {
|
|
19993
|
+
this.logger.error(
|
|
19994
|
+
"Stop signal received \u2014 shutting down",
|
|
19995
|
+
{ event: "autonomous_coordinator.stop_signal", iteration: this.iterationCount }
|
|
19996
|
+
);
|
|
19997
|
+
} else {
|
|
19998
|
+
console.error(
|
|
19999
|
+
JSON.stringify({
|
|
20000
|
+
level: "error",
|
|
20001
|
+
event: "autonomous_coordinator.stop_signal",
|
|
20002
|
+
message: "stop signal received \u2014 shutting down",
|
|
20003
|
+
iteration: this.iterationCount,
|
|
20004
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20005
|
+
})
|
|
20006
|
+
);
|
|
20007
|
+
}
|
|
18909
20008
|
}
|
|
18910
20009
|
/**
|
|
18911
20010
|
* Report that a terminal worker (not a Director subagent) completed a claimed
|
|
@@ -19353,6 +20452,7 @@ export {
|
|
|
19353
20452
|
AutonomousCoordinator,
|
|
19354
20453
|
BUG_HUNTER_AGENT,
|
|
19355
20454
|
BUILD_AGENTS,
|
|
20455
|
+
BrainDecisionLedger,
|
|
19356
20456
|
BrainDecisionQueue,
|
|
19357
20457
|
BrainMonitor,
|
|
19358
20458
|
BudgetExceededError,
|
|
@@ -19375,6 +20475,7 @@ export {
|
|
|
19375
20475
|
DefaultMultiAgentCoordinator,
|
|
19376
20476
|
Director,
|
|
19377
20477
|
DirectorAlertLevel,
|
|
20478
|
+
EscalationRoutingBrainArbiter,
|
|
19378
20479
|
FLEET_ROSTER,
|
|
19379
20480
|
FLEET_ROSTER_BUDGETS,
|
|
19380
20481
|
FLEET_ROSTER_WITHACP,
|
|
@@ -19424,12 +20525,14 @@ export {
|
|
|
19424
20525
|
applyRosterBudget,
|
|
19425
20526
|
attachAutoExtend,
|
|
19426
20527
|
attachDepWatcherBridge,
|
|
20528
|
+
brainDecisionKey,
|
|
19427
20529
|
buildDownAlert,
|
|
19428
20530
|
buildRecoveryAlert,
|
|
19429
20531
|
composeDirectorPrompt,
|
|
19430
20532
|
composeSubagentPrompt,
|
|
19431
20533
|
createAgentMonitorService,
|
|
19432
20534
|
createDelegateTool,
|
|
20535
|
+
createLedgerGuardBrainArbiter,
|
|
19433
20536
|
createMailboxHooks,
|
|
19434
20537
|
createMessage,
|
|
19435
20538
|
detectEcosystem,
|
|
@@ -19493,6 +20596,7 @@ export {
|
|
|
19493
20596
|
scoreAgents,
|
|
19494
20597
|
startPackageOutdatedWatcher,
|
|
19495
20598
|
subagentNeedsWorktree,
|
|
20599
|
+
terminalPolicyDecision,
|
|
19496
20600
|
updatePackageOutdatedStatus,
|
|
19497
20601
|
validateWatchdogOptions,
|
|
19498
20602
|
withDisabledToolFiltering,
|