@wrongstack/core 0.285.0 → 0.287.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 +46 -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 +8 -14
- package/dist/coordination/delegate-tool.d.ts.map +1 -1
- package/dist/coordination/director/director-btw-notes.d.ts +20 -0
- package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
- package/dist/coordination/director/director-collab.d.ts +40 -0
- package/dist/coordination/director/director-collab.d.ts.map +1 -0
- package/dist/coordination/director.d.ts +28 -9
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/fleet-manager.d.ts +21 -0
- package/dist/coordination/fleet-manager.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +6 -7
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +96 -21
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/icoordinator.d.ts +1 -3
- package/dist/coordination/icoordinator.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 +2176 -806
- 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/mail-tools.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 +28 -0
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
- package/dist/coordination/mailbox-types.d.ts +130 -5
- 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/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-response.d.ts +10 -1
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent.d.ts +8 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/context.d.ts +25 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/conversation-state.d.ts +6 -0
- package/dist/core/conversation-state.d.ts.map +1 -1
- package/dist/core/fallback-model.d.ts.map +1 -1
- package/dist/core/mailbox-loop.d.ts +12 -5
- package/dist/core/mailbox-loop.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +14 -6
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/defaults/index.d.ts +0 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +1690 -1998
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/auto-compaction-middleware.d.ts +8 -1
- package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
- 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/compaction-core.d.ts.map +1 -1
- package/dist/execution/council-brain.d.ts +79 -0
- package/dist/execution/council-brain.d.ts.map +1 -0
- package/dist/execution/council-orchestrator.d.ts +32 -0
- package/dist/execution/council-orchestrator.d.ts.map +1 -0
- package/dist/execution/council-personas.d.ts +19 -0
- package/dist/execution/council-personas.d.ts.map +1 -0
- package/dist/execution/council-profiles.d.ts +34 -0
- package/dist/execution/council-profiles.d.ts.map +1 -0
- package/dist/execution/council-prompts.d.ts +21 -0
- package/dist/execution/council-prompts.d.ts.map +1 -0
- package/dist/execution/council-resolution.d.ts +57 -0
- package/dist/execution/council-resolution.d.ts.map +1 -0
- package/dist/execution/design-kit-loader.d.ts.map +1 -1
- package/dist/execution/enhance-recovery.d.ts +35 -0
- package/dist/execution/enhance-recovery.d.ts.map +1 -0
- package/dist/execution/error-handler.d.ts.map +1 -1
- 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 +11 -1
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js +3076 -526
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/intelligent-compactor.d.ts +18 -7
- package/dist/execution/intelligent-compactor.d.ts.map +1 -1
- package/dist/execution/one-shot-llm.d.ts +52 -0
- package/dist/execution/one-shot-llm.d.ts.map +1 -0
- package/dist/execution/parallel-eternal-engine.d.ts +12 -0
- package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +23 -4
- package/dist/execution/prompt-enhancer.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.js +24 -6
- package/dist/execution/prompt-enhancer.js.map +2 -2
- package/dist/execution/selective-compactor.d.ts +4 -4
- package/dist/execution/strategy-compactor.d.ts +7 -0
- package/dist/execution/strategy-compactor.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts +19 -5
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/runner.d.ts.map +1 -1
- package/dist/hooks/shell-executor.d.ts.map +1 -1
- package/dist/hq/alerts.d.ts +5 -0
- package/dist/hq/alerts.d.ts.map +1 -1
- package/dist/hq/auth-store.d.ts +38 -10
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/commands.d.ts +8 -4
- package/dist/hq/commands.d.ts.map +1 -1
- package/dist/hq/cost-bridge.d.ts +5 -3
- package/dist/hq/cost-bridge.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/fleet-bridge.d.ts.map +1 -1
- package/dist/hq/index.d.ts +0 -1
- package/dist/hq/index.d.ts.map +1 -1
- package/dist/hq/index.js +1016 -283
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/persistence.d.ts +42 -0
- package/dist/hq/persistence.d.ts.map +1 -1
- package/dist/hq/protocol/brain.d.ts +24 -0
- package/dist/hq/protocol/brain.d.ts.map +1 -0
- package/dist/hq/protocol/browser.d.ts +12 -0
- package/dist/hq/protocol/browser.d.ts.map +1 -0
- package/dist/hq/protocol/client.d.ts +69 -0
- package/dist/hq/protocol/client.d.ts.map +1 -0
- package/dist/hq/protocol/core.d.ts +130 -0
- package/dist/hq/protocol/core.d.ts.map +1 -0
- package/dist/hq/protocol/fleet.d.ts +73 -0
- package/dist/hq/protocol/fleet.d.ts.map +1 -0
- package/dist/hq/protocol/mailbox.d.ts +76 -0
- package/dist/hq/protocol/mailbox.d.ts.map +1 -0
- package/dist/hq/protocol/mcp.d.ts +51 -0
- package/dist/hq/protocol/mcp.d.ts.map +1 -0
- package/dist/hq/protocol/project.d.ts +26 -0
- package/dist/hq/protocol/project.d.ts.map +1 -0
- package/dist/hq/protocol/session.d.ts +153 -0
- package/dist/hq/protocol/session.d.ts.map +1 -0
- package/dist/hq/protocol/tool.d.ts +31 -0
- package/dist/hq/protocol/tool.d.ts.map +1 -0
- package/dist/hq/protocol.d.ts +10 -559
- 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 +24 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7676 -3432
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +13 -11
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/infrastructure/token-counter.d.ts.map +1 -1
- package/dist/kernel/events/agent-events.d.ts +283 -0
- package/dist/kernel/events/agent-events.d.ts.map +1 -0
- package/dist/kernel/events/brain-events.d.ts +176 -0
- package/dist/kernel/events/brain-events.d.ts.map +1 -0
- package/dist/kernel/events/fleet-events.d.ts +39 -0
- package/dist/kernel/events/fleet-events.d.ts.map +1 -0
- package/dist/kernel/events/memory-events.d.ts +165 -0
- package/dist/kernel/events/memory-events.d.ts.map +1 -0
- package/dist/kernel/events/provider-events.d.ts +98 -0
- package/dist/kernel/events/provider-events.d.ts.map +1 -0
- package/dist/kernel/events/sdd-events.d.ts +118 -0
- package/dist/kernel/events/sdd-events.d.ts.map +1 -0
- package/dist/kernel/events/session-events.d.ts +227 -0
- package/dist/kernel/events/session-events.d.ts.map +1 -0
- package/dist/kernel/events/tool-events.d.ts +185 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -0
- package/dist/kernel/events/worktree-events.d.ts +58 -0
- package/dist/kernel/events/worktree-events.d.ts.map +1 -0
- package/dist/kernel/events.d.ts +12 -1282
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js +22 -2
- package/dist/kernel/index.js.map +2 -2
- package/dist/kernel/run-controller.d.ts +15 -0
- package/dist/kernel/run-controller.d.ts.map +1 -1
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/models/index.js +19 -6
- package/dist/models/index.js.map +2 -2
- package/dist/models/llm-selector.d.ts +7 -0
- package/dist/models/llm-selector.d.ts.map +1 -1
- package/dist/observability/index.js +16 -15
- package/dist/observability/index.js.map +2 -2
- package/dist/plugins/auto-review-plugin.d.ts +36 -0
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
- package/dist/plugins/chimera-plugin.d.ts +8 -0
- package/dist/plugins/chimera-plugin.d.ts.map +1 -1
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +260 -23
- 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-scrubber.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/skills/index.js.map +2 -2
- package/dist/storage/config-loader.d.ts +10 -0
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/goal-kanban.d.ts +69 -0
- package/dist/storage/goal-kanban.d.ts.map +1 -0
- package/dist/storage/goal-store.d.ts +1 -1
- package/dist/storage/goal-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +572 -879
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/memory-consolidator.d.ts +9 -1
- package/dist/storage/memory-consolidator.d.ts.map +1 -1
- 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/tools/council-tool.d.ts +25 -0
- package/dist/tools/council-tool.d.ts.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1884 -12
- package/dist/tools/index.js.map +4 -4
- package/dist/tools/one-shot-llm-tool.d.ts +51 -0
- package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
- package/dist/types/config.d.ts +236 -4
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/context-window.d.ts.map +1 -1
- package/dist/types/council.d.ts +134 -0
- package/dist/types/council.d.ts.map +1 -0
- package/dist/types/default-config.d.ts +3 -3
- package/dist/types/hooks.d.ts +14 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +40 -8
- package/dist/types/index.js.map +3 -3
- 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/one-shot-llm.d.ts +128 -0
- package/dist/types/one-shot-llm.d.ts.map +1 -0
- package/dist/types/plugin.d.ts +6 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/provider.d.ts +17 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/system-prompt.d.ts.map +1 -1
- package/dist/types/tool-executor.d.ts +10 -0
- package/dist/types/tool-executor.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/atomic-write.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 +156 -11
- package/dist/utils/index.js.map +4 -4
- package/dist/utils/tool-wire-compact.d.ts +1 -0
- package/dist/utils/tool-wire-compact.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +20 -4
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- 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/cli/next-task-predictor.md +5 -3
- package/instructions/coordination/subagent-baseline.md +17 -2
- package/instructions/leader-after-task.md +19 -8
- package/instructions/llm/council-judge.md +19 -0
- package/instructions/llm/council-voter.md +22 -0
- package/instructions/modes/architect.md +14 -9
- package/instructions/modes/audit-lite.md +9 -7
- package/instructions/modes/brief-system.md +12 -12
- package/instructions/modes/brief.md +7 -5
- package/instructions/modes/code-auditor.md +13 -8
- package/instructions/modes/code-reviewer.md +13 -8
- package/instructions/modes/debug-lite.md +7 -6
- package/instructions/modes/debugger.md +13 -9
- package/instructions/modes/devops.md +13 -9
- package/instructions/modes/plan-lite.md +7 -6
- package/instructions/modes/refactor-lite.md +7 -6
- package/instructions/modes/refactorer.md +14 -9
- package/instructions/modes/research-lite.md +8 -6
- package/instructions/modes/research-web.md +18 -55
- package/instructions/modes/review-lite.md +7 -6
- package/instructions/modes/teach-system.md +15 -15
- package/instructions/modes/teach.md +13 -12
- package/instructions/modes/test-lite.md +6 -5
- package/instructions/modes/tester.md +13 -9
- package/instructions/modes/ui-design.md +14 -8
- package/instructions/sections/tool/commit-hygiene.md +1 -7
- package/instructions/sections/tool/common-patterns.md +7 -1
- package/instructions/sections/tool/delegation-full.md +3 -31
- package/instructions/sections/tool/mailbox-compact.md +1 -1
- package/instructions/sections/tool/mailbox-full.md +6 -22
- package/instructions/system.md +294 -8
- package/package.json +2 -2
- package/skills/docker-deploy/SKILL.md +1 -1
- package/skills/mailbox-bridge/SKILL.md +16 -7
- package/skills/output-standards/SKILL.md +16 -16
- package/skills/wrongstack-mailbox/SKILL.md +57 -12
- package/dist/coordination/director-construction.d.ts +0 -90
- package/dist/coordination/director-construction.d.ts.map +0 -1
- package/dist/hq/agent-bridge.d.ts +0 -27
- package/dist/hq/agent-bridge.d.ts.map +0 -1
- 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
- package/dist/storage/memory-store.d.ts +0 -119
- package/dist/storage/memory-store.d.ts.map +0 -1
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
// src/utils/atomic-write.ts
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
|
+
import { watch as watchDir } from "node:fs";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
9
10
|
const dir = path.dirname(targetPath);
|
|
@@ -71,7 +72,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
71
72
|
await fs.mkdir(dir, { recursive: true });
|
|
72
73
|
continue;
|
|
73
74
|
}
|
|
74
|
-
if (code !== "EEXIST") throw err;
|
|
75
|
+
if (code !== "EEXIST" && code !== "EPERM") throw err;
|
|
75
76
|
try {
|
|
76
77
|
const stat9 = await fs.stat(lockPath);
|
|
77
78
|
if (Date.now() - stat9.mtimeMs > staleMs) {
|
|
@@ -81,7 +82,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
81
82
|
} catch {
|
|
82
83
|
continue;
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
+
const elapsed = Date.now() - started;
|
|
86
|
+
if (elapsed >= timeoutMs) {
|
|
85
87
|
throw new FsError({
|
|
86
88
|
message: `Timed out waiting for file lock: ${targetPath}`,
|
|
87
89
|
code: "FS_ATOMIC_WRITE_FAILED",
|
|
@@ -89,7 +91,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
89
91
|
context: { timeoutMs }
|
|
90
92
|
});
|
|
91
93
|
}
|
|
92
|
-
await
|
|
94
|
+
await waitForLockRelease(lockPath, timeoutMs - elapsed);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
try {
|
|
@@ -105,6 +107,50 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
async function waitForLockRelease(lockPath, remainingMs) {
|
|
111
|
+
const parentDir = path.dirname(lockPath);
|
|
112
|
+
const lockName = path.basename(lockPath);
|
|
113
|
+
const intervalMs = Math.min(remainingMs, 100);
|
|
114
|
+
return new Promise((resolve9) => {
|
|
115
|
+
let settled = false;
|
|
116
|
+
let watcher = null;
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
settled = true;
|
|
119
|
+
watcher?.close();
|
|
120
|
+
resolve9();
|
|
121
|
+
}, intervalMs);
|
|
122
|
+
try {
|
|
123
|
+
watcher = watchDir(parentDir, (eventType, filename) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
if (filename === lockName && (eventType === "rename" || eventType === "change")) {
|
|
126
|
+
settled = true;
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
watcher?.close();
|
|
129
|
+
resolve9();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
if (!settled) {
|
|
135
|
+
settled = true;
|
|
136
|
+
setTimeout(resolve9, Math.min(remainingMs, 25));
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
fs.access(lockPath).then(
|
|
141
|
+
() => {
|
|
142
|
+
},
|
|
143
|
+
() => {
|
|
144
|
+
if (!settled) {
|
|
145
|
+
settled = true;
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
watcher?.close();
|
|
148
|
+
resolve9();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
108
154
|
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
109
155
|
async function renameWithRetry(from, to) {
|
|
110
156
|
if (process.platform !== "win32") {
|
|
@@ -329,9 +375,17 @@ function globToRegex(pat) {
|
|
|
329
375
|
return new RegExp(re + "$");
|
|
330
376
|
}
|
|
331
377
|
function baseDir(pat) {
|
|
332
|
-
let
|
|
333
|
-
|
|
334
|
-
|
|
378
|
+
let firstGlob = pat.length;
|
|
379
|
+
for (let i = 0; i < pat.length; i++) {
|
|
380
|
+
if (GLOB_CHARS.has(expectDefined(pat[i]))) {
|
|
381
|
+
firstGlob = i;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const cut = Math.max(
|
|
386
|
+
pat.lastIndexOf(SEP, firstGlob - 1),
|
|
387
|
+
pat.lastIndexOf("/", firstGlob - 1)
|
|
388
|
+
);
|
|
335
389
|
return cut < 0 ? "." : pat.slice(0, cut);
|
|
336
390
|
}
|
|
337
391
|
async function expandGlob(pattern) {
|
|
@@ -579,11 +633,31 @@ function truncate(s, max) {
|
|
|
579
633
|
|
|
580
634
|
// src/utils/wstack-paths.ts
|
|
581
635
|
import { createHash } from "node:crypto";
|
|
636
|
+
import * as fs2 from "node:fs";
|
|
582
637
|
import * as os from "node:os";
|
|
583
638
|
import * as path3 from "node:path";
|
|
639
|
+
function canonicalProjectRoot(absRoot) {
|
|
640
|
+
const checkoutRoot = path3.resolve(absRoot);
|
|
641
|
+
const dotGit = path3.join(checkoutRoot, ".git");
|
|
642
|
+
try {
|
|
643
|
+
if (!fs2.statSync(dotGit).isFile()) return checkoutRoot;
|
|
644
|
+
const gitDirLine = fs2.readFileSync(dotGit, "utf8").trim();
|
|
645
|
+
const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
|
|
646
|
+
if (!match?.[1]) return checkoutRoot;
|
|
647
|
+
const gitDir = path3.resolve(checkoutRoot, match[1].trim());
|
|
648
|
+
const commonDirFile = path3.join(gitDir, "commondir");
|
|
649
|
+
if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
|
|
650
|
+
const commonDir = path3.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
|
|
651
|
+
if (path3.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
|
|
652
|
+
return path3.dirname(commonDir);
|
|
653
|
+
} catch {
|
|
654
|
+
return checkoutRoot;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
584
657
|
function projectSlug(absRoot) {
|
|
585
|
-
const
|
|
586
|
-
const
|
|
658
|
+
const identityRoot = canonicalProjectRoot(absRoot);
|
|
659
|
+
const base = slugify(path3.basename(identityRoot));
|
|
660
|
+
const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
|
|
587
661
|
return `${base}-${hash}`;
|
|
588
662
|
}
|
|
589
663
|
function slugify(name) {
|
|
@@ -1417,9 +1491,11 @@ function normalizeSubagentStructuredReport(value) {
|
|
|
1417
1491
|
const filesExamined = stringArray(value["files_examined"]);
|
|
1418
1492
|
const nextSteps = stringArray(value["suggested_next_steps"]);
|
|
1419
1493
|
const confidence = value["confidence"];
|
|
1494
|
+
const completion = value["completion"];
|
|
1495
|
+
const remainingWork = optionalString(value["remaining_work"]);
|
|
1420
1496
|
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
1497
|
(item) => item.trim().length > MAX_ITEM_CHARS
|
|
1422
|
-
) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
1498
|
+
) || 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
1499
|
return void 0;
|
|
1424
1500
|
}
|
|
1425
1501
|
const report = {
|
|
@@ -1427,7 +1503,9 @@ function normalizeSubagentStructuredReport(value) {
|
|
|
1427
1503
|
findings: findings.map((item) => item.trim()).filter(Boolean),
|
|
1428
1504
|
files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
|
|
1429
1505
|
confidence,
|
|
1430
|
-
suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean)
|
|
1506
|
+
suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
|
|
1507
|
+
...completion === "complete" || completion === "partial" ? { completion } : {},
|
|
1508
|
+
...remainingWork ? { remaining_work: remainingWork } : {}
|
|
1431
1509
|
};
|
|
1432
1510
|
return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
|
|
1433
1511
|
}
|
|
@@ -1440,6 +1518,8 @@ function formatSubagentStructuredReport(report) {
|
|
|
1440
1518
|
const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
|
|
1441
1519
|
return [
|
|
1442
1520
|
report.summary,
|
|
1521
|
+
report.completion ? `Completion: ${report.completion}` : void 0,
|
|
1522
|
+
report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
|
|
1443
1523
|
`
|
|
1444
1524
|
Findings:
|
|
1445
1525
|
${findings}`,
|
|
@@ -1449,7 +1529,7 @@ Files examined: ${files}`,
|
|
|
1449
1529
|
`
|
|
1450
1530
|
Suggested next steps:
|
|
1451
1531
|
${next}`
|
|
1452
|
-
].join("\n");
|
|
1532
|
+
].filter((line) => typeof line === "string").join("\n");
|
|
1453
1533
|
}
|
|
1454
1534
|
function makeSubagentResultTool() {
|
|
1455
1535
|
return {
|
|
@@ -1489,6 +1569,16 @@ function makeSubagentResultTool() {
|
|
|
1489
1569
|
maxItems: MAX_NEXT_STEPS,
|
|
1490
1570
|
items: { type: "string" },
|
|
1491
1571
|
description: "Concrete follow-ups; use an empty array when none remain."
|
|
1572
|
+
},
|
|
1573
|
+
completion: {
|
|
1574
|
+
type: "string",
|
|
1575
|
+
enum: ["complete", "partial"],
|
|
1576
|
+
description: "Use `partial` only when the task is intentionally stopping at a clean checkpoint and another worker should continue."
|
|
1577
|
+
},
|
|
1578
|
+
remaining_work: {
|
|
1579
|
+
type: "string",
|
|
1580
|
+
maxLength: MAX_SUMMARY_CHARS,
|
|
1581
|
+
description: "Concrete work left for a successor. Required by runtime validation when completion is `partial`."
|
|
1492
1582
|
}
|
|
1493
1583
|
},
|
|
1494
1584
|
required: ["summary", "findings", "files_examined", "confidence", "suggested_next_steps"],
|
|
@@ -1518,6 +1608,9 @@ function isRecord(value) {
|
|
|
1518
1608
|
function nonEmptyString(value) {
|
|
1519
1609
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1520
1610
|
}
|
|
1611
|
+
function optionalString(value) {
|
|
1612
|
+
return value === void 0 ? void 0 : nonEmptyString(value);
|
|
1613
|
+
}
|
|
1521
1614
|
function stringArray(value) {
|
|
1522
1615
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
|
|
1523
1616
|
}
|
|
@@ -1800,7 +1893,7 @@ var TOOLS = {
|
|
|
1800
1893
|
};
|
|
1801
1894
|
|
|
1802
1895
|
// src/coordination/agents/agent-prompts.ts
|
|
1803
|
-
import { readFileSync, statSync } from "node:fs";
|
|
1896
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
1804
1897
|
import * as os2 from "node:os";
|
|
1805
1898
|
import * as path4 from "node:path";
|
|
1806
1899
|
import { fileURLToPath } from "node:url";
|
|
@@ -1815,7 +1908,7 @@ function agentPrompt(id) {
|
|
|
1815
1908
|
let resolved = "";
|
|
1816
1909
|
for (const dir of agentPromptDirCandidates(envDir)) {
|
|
1817
1910
|
try {
|
|
1818
|
-
resolved =
|
|
1911
|
+
resolved = readFileSync2(path4.join(dir, fileName), "utf8").trimEnd();
|
|
1819
1912
|
break;
|
|
1820
1913
|
} catch {
|
|
1821
1914
|
}
|
|
@@ -1845,7 +1938,7 @@ function agentPromptDirCandidates(envDir) {
|
|
|
1845
1938
|
}
|
|
1846
1939
|
function isDirectory(candidate) {
|
|
1847
1940
|
try {
|
|
1848
|
-
return
|
|
1941
|
+
return statSync2(candidate).isDirectory();
|
|
1849
1942
|
} catch {
|
|
1850
1943
|
return false;
|
|
1851
1944
|
}
|
|
@@ -3458,7 +3551,12 @@ var BrainDecisionQueue = class {
|
|
|
3458
3551
|
if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
|
|
3459
3552
|
entry.timer = setTimeout(() => {
|
|
3460
3553
|
this.pending.delete(request.id);
|
|
3461
|
-
resolve9(
|
|
3554
|
+
resolve9(
|
|
3555
|
+
this.opts.onTimeout?.(request) ?? {
|
|
3556
|
+
type: "deny",
|
|
3557
|
+
reason: "Brain human decision timed out."
|
|
3558
|
+
}
|
|
3559
|
+
);
|
|
3462
3560
|
}, this.opts.timeoutMs);
|
|
3463
3561
|
}
|
|
3464
3562
|
this.pending.set(request.id, entry);
|
|
@@ -3475,6 +3573,46 @@ var BrainDecisionQueue = class {
|
|
|
3475
3573
|
}
|
|
3476
3574
|
}
|
|
3477
3575
|
};
|
|
3576
|
+
function terminalPolicyDecision(request) {
|
|
3577
|
+
const recommended = request.options?.find((option) => option.recommended);
|
|
3578
|
+
if (recommended && (request.risk === "low" || request.risk === "medium")) {
|
|
3579
|
+
return {
|
|
3580
|
+
type: "answer",
|
|
3581
|
+
optionId: recommended.id,
|
|
3582
|
+
text: recommended.label,
|
|
3583
|
+
rationale: `Headless terminal policy: caller-recommended option accepted at ${request.risk} risk (no human available by configuration).`
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
if (request.fallback === "continue") {
|
|
3587
|
+
return {
|
|
3588
|
+
type: "answer",
|
|
3589
|
+
text: "Continue with the caller default.",
|
|
3590
|
+
rationale: "Headless terminal policy: request declares continue as its safe fallback."
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
return {
|
|
3594
|
+
type: "deny",
|
|
3595
|
+
reason: `Headless terminal policy: no safe automatic option for "${request.question}" (risk: ${request.risk}). The proposed action was not taken.`
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
var EscalationRoutingBrainArbiter = class {
|
|
3599
|
+
constructor(inner, queue, getMode) {
|
|
3600
|
+
this.inner = inner;
|
|
3601
|
+
this.queue = queue;
|
|
3602
|
+
this.getMode = getMode;
|
|
3603
|
+
}
|
|
3604
|
+
inner;
|
|
3605
|
+
queue;
|
|
3606
|
+
getMode;
|
|
3607
|
+
async decide(request) {
|
|
3608
|
+
const decision = await this.inner.decide(request);
|
|
3609
|
+
if (decision.type !== "ask_human") return decision;
|
|
3610
|
+
if (this.getMode() === "interactive" && this.queue) {
|
|
3611
|
+
return this.queue.requestHumanDecision(request);
|
|
3612
|
+
}
|
|
3613
|
+
return terminalPolicyDecision(request);
|
|
3614
|
+
}
|
|
3615
|
+
};
|
|
3478
3616
|
var HumanEscalatingBrainArbiter = class {
|
|
3479
3617
|
constructor(inner, queue) {
|
|
3480
3618
|
this.inner = inner;
|
|
@@ -3550,29 +3688,348 @@ function formatHumanPrompt(request) {
|
|
|
3550
3688
|
return lines.join("\n");
|
|
3551
3689
|
}
|
|
3552
3690
|
|
|
3691
|
+
// src/coordination/brain-ledger.ts
|
|
3692
|
+
import { appendFile, mkdir as mkdir2, readFile } from "node:fs/promises";
|
|
3693
|
+
import { dirname as dirname4 } from "node:path";
|
|
3694
|
+
var QUESTION_MAX = 200;
|
|
3695
|
+
var DETAIL_MAX = 240;
|
|
3696
|
+
var truncate2 = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
3697
|
+
function brainDecisionKey(source, question) {
|
|
3698
|
+
const normalized = question.toLowerCase().replace(/[^\s]*\d[^\s]*/g, "#").replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3699
|
+
return `${source ?? "?"}:${normalized}`;
|
|
3700
|
+
}
|
|
3701
|
+
function fmtAge(ms) {
|
|
3702
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
3703
|
+
if (s < 60) return `${s}s ago`;
|
|
3704
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
3705
|
+
return `${Math.round(s / 3600)}h ago`;
|
|
3706
|
+
}
|
|
3707
|
+
function createLedgerGuardBrainArbiter(opts) {
|
|
3708
|
+
const denyAfter = opts.denyAfter ?? 3;
|
|
3709
|
+
return {
|
|
3710
|
+
async decide(request) {
|
|
3711
|
+
if (denyAfter > 0) {
|
|
3712
|
+
const streak = opts.failureStreakFor(request);
|
|
3713
|
+
if (streak >= denyAfter) {
|
|
3714
|
+
return {
|
|
3715
|
+
type: "deny",
|
|
3716
|
+
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)})`
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
return opts.inner.decide(request);
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
var BrainDecisionLedger = class {
|
|
3725
|
+
constructor(opts) {
|
|
3726
|
+
this.opts = opts;
|
|
3727
|
+
this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
|
|
3728
|
+
this.retryWindowMs = opts.interventionRetryWindowMs ?? 10 * 6e4;
|
|
3729
|
+
this.now = opts.now ?? Date.now;
|
|
3730
|
+
}
|
|
3731
|
+
opts;
|
|
3732
|
+
entries = [];
|
|
3733
|
+
outcomeByRequest = /* @__PURE__ */ new Map();
|
|
3734
|
+
/** subagentId → decision request ids awaiting that subagent's task result. */
|
|
3735
|
+
pendingBudgetOutcomes = /* @__PURE__ */ new Map();
|
|
3736
|
+
lastInterventionByKind = /* @__PURE__ */ new Map();
|
|
3737
|
+
unsubscribers = [];
|
|
3738
|
+
/** Serialized fire-and-forget writes — awaited by `stop()` (drain). */
|
|
3739
|
+
writeChain = Promise.resolve();
|
|
3740
|
+
dirReady = false;
|
|
3741
|
+
maxMemoryEntries;
|
|
3742
|
+
retryWindowMs;
|
|
3743
|
+
now;
|
|
3744
|
+
/**
|
|
3745
|
+
* Subscribe to the EventBus and seed the in-memory ring from the JSONL
|
|
3746
|
+
* tail. The returned promise resolves once the seed load finished
|
|
3747
|
+
* (best-effort — a missing/corrupt file is an empty history, not an error).
|
|
3748
|
+
*/
|
|
3749
|
+
async start() {
|
|
3750
|
+
const { events } = this.opts;
|
|
3751
|
+
this.unsubscribers.push(
|
|
3752
|
+
events.on("brain.decision_answered", (e) => {
|
|
3753
|
+
this.record({
|
|
3754
|
+
at: e.at,
|
|
3755
|
+
sessionId: e.sessionId,
|
|
3756
|
+
requestId: e.request.id,
|
|
3757
|
+
kind: "answered",
|
|
3758
|
+
source: e.request.source,
|
|
3759
|
+
risk: e.request.risk,
|
|
3760
|
+
question: truncate2(e.request.question, QUESTION_MAX),
|
|
3761
|
+
optionId: e.decision.type === "answer" ? e.decision.optionId : void 0,
|
|
3762
|
+
detail: e.decision.type === "answer" ? truncate2(e.decision.rationale ?? e.decision.text, DETAIL_MAX) : void 0
|
|
3763
|
+
});
|
|
3764
|
+
}),
|
|
3765
|
+
events.on("brain.decision_denied", (e) => {
|
|
3766
|
+
this.record({
|
|
3767
|
+
at: e.at,
|
|
3768
|
+
sessionId: e.sessionId,
|
|
3769
|
+
requestId: e.request.id,
|
|
3770
|
+
kind: "denied",
|
|
3771
|
+
source: e.request.source,
|
|
3772
|
+
risk: e.request.risk,
|
|
3773
|
+
question: truncate2(e.request.question, QUESTION_MAX),
|
|
3774
|
+
detail: e.decision.type === "deny" ? truncate2(e.decision.reason, DETAIL_MAX) : void 0
|
|
3775
|
+
});
|
|
3776
|
+
}),
|
|
3777
|
+
events.on("brain.decision_ask_human", (e) => {
|
|
3778
|
+
this.record({
|
|
3779
|
+
at: e.at,
|
|
3780
|
+
sessionId: e.sessionId,
|
|
3781
|
+
requestId: e.request.id,
|
|
3782
|
+
kind: "ask_human",
|
|
3783
|
+
source: e.request.source,
|
|
3784
|
+
risk: e.request.risk,
|
|
3785
|
+
question: truncate2(e.request.question, QUESTION_MAX)
|
|
3786
|
+
});
|
|
3787
|
+
}),
|
|
3788
|
+
events.on("brain.intervention", (e) => {
|
|
3789
|
+
this.record({
|
|
3790
|
+
at: e.at,
|
|
3791
|
+
sessionId: e.sessionId,
|
|
3792
|
+
requestId: e.request.id,
|
|
3793
|
+
kind: "intervention",
|
|
3794
|
+
source: e.request.source,
|
|
3795
|
+
risk: e.request.risk,
|
|
3796
|
+
question: truncate2(`[${e.kind}] ${e.request.question}`, QUESTION_MAX),
|
|
3797
|
+
detail: e.intervened ? "steered" : "observed only"
|
|
3798
|
+
});
|
|
3799
|
+
if (!e.intervened) return;
|
|
3800
|
+
const prev = this.lastInterventionByKind.get(e.kind);
|
|
3801
|
+
if (prev && e.at - prev.at <= this.retryWindowMs) {
|
|
3802
|
+
this.recordOutcome(
|
|
3803
|
+
prev.requestId,
|
|
3804
|
+
"failure",
|
|
3805
|
+
`same signal (${e.kind}) re-triggered ${fmtAge(e.at - prev.at).replace(" ago", " later")}`,
|
|
3806
|
+
e.sessionId
|
|
3807
|
+
);
|
|
3808
|
+
}
|
|
3809
|
+
this.lastInterventionByKind.set(e.kind, { requestId: e.request.id, at: e.at });
|
|
3810
|
+
}),
|
|
3811
|
+
// ── Outcome correlation: budget extensions ────────────────────────
|
|
3812
|
+
events.on("subagent.budget_extended", (e) => {
|
|
3813
|
+
const requestId = `director-budget-${e.subagentId}-${e.kind}`;
|
|
3814
|
+
if (!this.entries.some((entry) => entry.requestId === requestId)) return;
|
|
3815
|
+
let set = this.pendingBudgetOutcomes.get(e.subagentId);
|
|
3816
|
+
if (!set) {
|
|
3817
|
+
set = /* @__PURE__ */ new Set();
|
|
3818
|
+
this.pendingBudgetOutcomes.set(e.subagentId, set);
|
|
3819
|
+
}
|
|
3820
|
+
set.add(requestId);
|
|
3821
|
+
}),
|
|
3822
|
+
events.on("subagent.task_completed", (e) => {
|
|
3823
|
+
const pending = this.pendingBudgetOutcomes.get(e.subagentId);
|
|
3824
|
+
if (!pending) return;
|
|
3825
|
+
this.pendingBudgetOutcomes.delete(e.subagentId);
|
|
3826
|
+
const outcome = e.status === "success" ? "success" : "failure";
|
|
3827
|
+
for (const requestId of pending) {
|
|
3828
|
+
this.recordOutcome(
|
|
3829
|
+
requestId,
|
|
3830
|
+
outcome,
|
|
3831
|
+
`budget-extended subagent's task ${e.status}`,
|
|
3832
|
+
e.sessionId
|
|
3833
|
+
);
|
|
3834
|
+
}
|
|
3835
|
+
})
|
|
3836
|
+
);
|
|
3837
|
+
await this.load();
|
|
3838
|
+
}
|
|
3839
|
+
/** Unsubscribe and drain pending writes. Safe to call twice. */
|
|
3840
|
+
async stop() {
|
|
3841
|
+
for (const off of this.unsubscribers) off();
|
|
3842
|
+
this.unsubscribers.length = 0;
|
|
3843
|
+
await this.writeChain.catch(() => {
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Attach a real-world outcome to an earlier decision. Appended to the
|
|
3848
|
+
* JSONL, folded into future digests, and re-emitted as `brain.outcome`.
|
|
3849
|
+
*/
|
|
3850
|
+
recordOutcome(requestId, outcome, detail, sessionId) {
|
|
3851
|
+
this.outcomeByRequest.set(requestId, outcome);
|
|
3852
|
+
this.record({
|
|
3853
|
+
at: this.now(),
|
|
3854
|
+
sessionId,
|
|
3855
|
+
requestId,
|
|
3856
|
+
kind: "outcome",
|
|
3857
|
+
outcome,
|
|
3858
|
+
detail: truncate2(detail, DETAIL_MAX)
|
|
3859
|
+
});
|
|
3860
|
+
this.opts.events.emit("brain.outcome", {
|
|
3861
|
+
sessionId,
|
|
3862
|
+
requestId,
|
|
3863
|
+
outcome,
|
|
3864
|
+
detail,
|
|
3865
|
+
at: this.now()
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
/** Last `n` ledger rows, oldest first. */
|
|
3869
|
+
tail(n) {
|
|
3870
|
+
return this.entries.slice(-n);
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Consecutive most-recent FAILURE outcomes among answered decisions of
|
|
3874
|
+
* this request's group. Decisions whose outcome is still unknown are
|
|
3875
|
+
* skipped (they neither extend nor break the streak); the first known
|
|
3876
|
+
* SUCCESS ends it. Powers the deterministic ledger guard: "the last K
|
|
3877
|
+
* times we approved this, it went wrong — stop approving".
|
|
3878
|
+
*/
|
|
3879
|
+
failureStreakFor(request) {
|
|
3880
|
+
const key = brainDecisionKey(request.source, request.question);
|
|
3881
|
+
let streak = 0;
|
|
3882
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
3883
|
+
const e = this.entries[i];
|
|
3884
|
+
if (e?.kind !== "answered" || e.requestId === request.id || e.question === void 0 || brainDecisionKey(e.source, e.question) !== key) {
|
|
3885
|
+
continue;
|
|
3886
|
+
}
|
|
3887
|
+
const outcome = this.outcomeByRequest.get(e.requestId);
|
|
3888
|
+
if (outcome === void 0) continue;
|
|
3889
|
+
if (outcome === "success") break;
|
|
3890
|
+
streak += 1;
|
|
3891
|
+
}
|
|
3892
|
+
return streak;
|
|
3893
|
+
}
|
|
3894
|
+
/**
|
|
3895
|
+
* Short decision-history block for the LLM/council prompt: how similar
|
|
3896
|
+
* past decisions went and — when known — how they turned OUT. Returns
|
|
3897
|
+
* undefined when there is no relevant history (no prompt noise).
|
|
3898
|
+
*/
|
|
3899
|
+
digestFor(request) {
|
|
3900
|
+
const key = brainDecisionKey(request.source, request.question);
|
|
3901
|
+
const related = this.entries.filter(
|
|
3902
|
+
(e) => (e.kind === "answered" || e.kind === "denied") && e.requestId !== request.id && e.question !== void 0 && brainDecisionKey(e.source, e.question) === key
|
|
3903
|
+
);
|
|
3904
|
+
if (related.length === 0) return void 0;
|
|
3905
|
+
const recent = related.slice(-5).reverse();
|
|
3906
|
+
const now = this.now();
|
|
3907
|
+
const lines = recent.map((e) => {
|
|
3908
|
+
const what = e.kind === "denied" ? "denied" : e.optionId ? `chose [${e.optionId}]` : "answered";
|
|
3909
|
+
const outcome = this.outcomeByRequest.get(e.requestId);
|
|
3910
|
+
const suffix = outcome === "failure" ? " \u2192 later FAILED" : outcome === "success" ? " \u2192 later succeeded" : "";
|
|
3911
|
+
return `- ${fmtAge(now - e.at)}: ${what}${suffix}`;
|
|
3912
|
+
});
|
|
3913
|
+
const approved = related.filter((e) => e.kind === "answered");
|
|
3914
|
+
const failedAfter = approved.filter(
|
|
3915
|
+
(e) => this.outcomeByRequest.get(e.requestId) === "failure"
|
|
3916
|
+
).length;
|
|
3917
|
+
const summary = `Summary: ${related.length} similar decision(s); ${approved.length} answered${failedAfter > 0 ? ` (${failedAfter} later failed)` : ""}, ${related.length - approved.length} denied.`;
|
|
3918
|
+
return [...lines, summary].join("\n");
|
|
3919
|
+
}
|
|
3920
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
3921
|
+
record(entry) {
|
|
3922
|
+
this.entries.push(entry);
|
|
3923
|
+
if (this.entries.length > this.maxMemoryEntries) {
|
|
3924
|
+
const evictedRequestIds = /* @__PURE__ */ new Set();
|
|
3925
|
+
const evictCount = this.entries.length - this.maxMemoryEntries;
|
|
3926
|
+
const evicted = this.entries.splice(0, evictCount);
|
|
3927
|
+
for (const e of evicted) {
|
|
3928
|
+
if (e.requestId) evictedRequestIds.add(e.requestId);
|
|
3929
|
+
}
|
|
3930
|
+
if (evictedRequestIds.size > 0) {
|
|
3931
|
+
const remainingRequestIds = /* @__PURE__ */ new Set();
|
|
3932
|
+
for (const e of this.entries) {
|
|
3933
|
+
if (e.requestId) remainingRequestIds.add(e.requestId);
|
|
3934
|
+
}
|
|
3935
|
+
for (const id of evictedRequestIds) {
|
|
3936
|
+
if (!remainingRequestIds.has(id)) this.outcomeByRequest.delete(id);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
this.append(entry);
|
|
3941
|
+
}
|
|
3942
|
+
append(entry) {
|
|
3943
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
3944
|
+
if (!this.dirReady) {
|
|
3945
|
+
await mkdir2(dirname4(this.opts.filePath), { recursive: true });
|
|
3946
|
+
this.dirReady = true;
|
|
3947
|
+
}
|
|
3948
|
+
await appendFile(this.opts.filePath, `${JSON.stringify(entry)}
|
|
3949
|
+
`, "utf8");
|
|
3950
|
+
}).catch(() => {
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
/** Seed the in-memory ring from the JSONL tail (cross-session learning). */
|
|
3954
|
+
async load() {
|
|
3955
|
+
let raw;
|
|
3956
|
+
try {
|
|
3957
|
+
raw = await readFile(this.opts.filePath, "utf8");
|
|
3958
|
+
} catch {
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
3962
|
+
const seed = [];
|
|
3963
|
+
for (const line of lines.slice(-this.maxMemoryEntries)) {
|
|
3964
|
+
try {
|
|
3965
|
+
const parsed = JSON.parse(line);
|
|
3966
|
+
if (typeof parsed?.requestId === "string" && typeof parsed.at === "number") {
|
|
3967
|
+
seed.push(parsed);
|
|
3968
|
+
}
|
|
3969
|
+
} catch {
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
this.entries.unshift(...seed);
|
|
3973
|
+
if (this.entries.length > this.maxMemoryEntries) {
|
|
3974
|
+
this.entries.splice(0, this.entries.length - this.maxMemoryEntries);
|
|
3975
|
+
}
|
|
3976
|
+
for (const e of this.entries) {
|
|
3977
|
+
if (e.kind === "outcome" && e.outcome) this.outcomeByRequest.set(e.requestId, e.outcome);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
};
|
|
3981
|
+
|
|
3553
3982
|
// src/coordination/brain-monitor.ts
|
|
3554
3983
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3984
|
+
var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "multi_edit", "multiedit", "str_replace"]);
|
|
3985
|
+
function editedPath(input) {
|
|
3986
|
+
if (!input || typeof input !== "object") return void 0;
|
|
3987
|
+
const r = input;
|
|
3988
|
+
const candidate = r["file_path"] ?? r["path"] ?? r["filePath"] ?? r["file"];
|
|
3989
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
|
|
3990
|
+
}
|
|
3555
3991
|
var BrainMonitor = class {
|
|
3556
3992
|
constructor(opts) {
|
|
3557
3993
|
this.opts = opts;
|
|
3558
3994
|
this.toolFailureStreak = opts.toolFailureStreak ?? 3;
|
|
3559
3995
|
this.errorStormCount = opts.errorStormCount ?? 4;
|
|
3560
3996
|
this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
|
|
3997
|
+
this.stallMs = opts.stallMs ?? 3e5;
|
|
3998
|
+
this.stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 3e4;
|
|
3999
|
+
this.fileChurnThreshold = opts.fileChurnThreshold ?? 5;
|
|
4000
|
+
this.fileChurnWindowMs = opts.fileChurnWindowMs ?? 6e5;
|
|
3561
4001
|
this.cooldownMs = opts.cooldownMs ?? 12e4;
|
|
3562
4002
|
}
|
|
3563
4003
|
opts;
|
|
3564
4004
|
failStreaks = /* @__PURE__ */ new Map();
|
|
3565
4005
|
errorTimestamps = [];
|
|
4006
|
+
editTimestamps = /* @__PURE__ */ new Map();
|
|
3566
4007
|
lastEngagedAt = /* @__PURE__ */ new Map();
|
|
3567
4008
|
unsubscribers = [];
|
|
3568
4009
|
engaging = false;
|
|
4010
|
+
activeRuns = 0;
|
|
4011
|
+
lastProgressAt = 0;
|
|
4012
|
+
stallTimer;
|
|
3569
4013
|
toolFailureStreak;
|
|
3570
4014
|
errorStormCount;
|
|
3571
4015
|
errorStormWindowMs;
|
|
4016
|
+
stallMs;
|
|
4017
|
+
stallCheckIntervalMs;
|
|
4018
|
+
fileChurnThreshold;
|
|
4019
|
+
fileChurnWindowMs;
|
|
3572
4020
|
cooldownMs;
|
|
4021
|
+
/** Resolve the leader's own session id for event filtering. */
|
|
4022
|
+
resolveLeaderSessionId() {
|
|
4023
|
+
const id = this.opts.leaderSessionId;
|
|
4024
|
+
return typeof id === "function" ? id() : id;
|
|
4025
|
+
}
|
|
3573
4026
|
start() {
|
|
3574
4027
|
this.unsubscribers.push(
|
|
3575
4028
|
this.opts.events.on("tool.executed", (e) => {
|
|
4029
|
+
const leaderSid = this.resolveLeaderSessionId();
|
|
4030
|
+
if (leaderSid && e.sessionId && e.sessionId !== leaderSid) return;
|
|
4031
|
+
this.lastProgressAt = Date.now();
|
|
4032
|
+
this.trackFileChurn(e.name, e.ok, e.input);
|
|
3576
4033
|
if (e.ok) {
|
|
3577
4034
|
this.failStreaks.delete(e.name);
|
|
3578
4035
|
return;
|
|
@@ -3592,8 +4049,50 @@ var BrainMonitor = class {
|
|
|
3592
4049
|
}
|
|
3593
4050
|
})
|
|
3594
4051
|
);
|
|
4052
|
+
if (this.stallMs > 0) {
|
|
4053
|
+
this.unsubscribers.push(
|
|
4054
|
+
this.opts.events.on("agent.run.started", (e) => {
|
|
4055
|
+
const lsid = this.resolveLeaderSessionId();
|
|
4056
|
+
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
4057
|
+
this.activeRuns += 1;
|
|
4058
|
+
this.lastProgressAt = Date.now();
|
|
4059
|
+
}),
|
|
4060
|
+
this.opts.events.on("agent.run.completed", (e) => {
|
|
4061
|
+
const lsid = this.resolveLeaderSessionId();
|
|
4062
|
+
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
4063
|
+
this.activeRuns = Math.max(0, this.activeRuns - 1);
|
|
4064
|
+
}),
|
|
4065
|
+
this.opts.events.on("agent.run.error", (e) => {
|
|
4066
|
+
const lsid = this.resolveLeaderSessionId();
|
|
4067
|
+
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
4068
|
+
this.activeRuns = Math.max(0, this.activeRuns - 1);
|
|
4069
|
+
}),
|
|
4070
|
+
this.opts.events.on("iteration.started", (e) => {
|
|
4071
|
+
const lsid = this.resolveLeaderSessionId();
|
|
4072
|
+
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
4073
|
+
this.lastProgressAt = Date.now();
|
|
4074
|
+
})
|
|
4075
|
+
);
|
|
4076
|
+
this.stallTimer = setInterval(() => {
|
|
4077
|
+
if (this.activeRuns === 0 || this.lastProgressAt === 0) return;
|
|
4078
|
+
const idleMs = Date.now() - this.lastProgressAt;
|
|
4079
|
+
if (idleMs < this.stallMs) return;
|
|
4080
|
+
this.lastProgressAt = Date.now();
|
|
4081
|
+
void this.engage("agent_stall", {
|
|
4082
|
+
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?`,
|
|
4083
|
+
context: [
|
|
4084
|
+
`Active runs: ${this.activeRuns}`,
|
|
4085
|
+
`Idle for: ${Math.round(idleMs / 1e3)}s`,
|
|
4086
|
+
`Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
|
|
4087
|
+
].join("\n")
|
|
4088
|
+
});
|
|
4089
|
+
}, this.stallCheckIntervalMs);
|
|
4090
|
+
this.stallTimer.unref?.();
|
|
4091
|
+
}
|
|
3595
4092
|
this.unsubscribers.push(
|
|
3596
4093
|
this.opts.events.on("error", (e) => {
|
|
4094
|
+
const lsid = this.resolveLeaderSessionId();
|
|
4095
|
+
if (lsid && e.sessionId && e.sessionId !== lsid) return;
|
|
3597
4096
|
const now = Date.now();
|
|
3598
4097
|
this.errorTimestamps.push(now);
|
|
3599
4098
|
this.errorTimestamps = this.errorTimestamps.filter(
|
|
@@ -3616,6 +4115,37 @@ var BrainMonitor = class {
|
|
|
3616
4115
|
this.unsubscribers.length = 0;
|
|
3617
4116
|
this.failStreaks.clear();
|
|
3618
4117
|
this.errorTimestamps = [];
|
|
4118
|
+
this.editTimestamps.clear();
|
|
4119
|
+
if (this.stallTimer) {
|
|
4120
|
+
clearInterval(this.stallTimer);
|
|
4121
|
+
this.stallTimer = void 0;
|
|
4122
|
+
}
|
|
4123
|
+
this.activeRuns = 0;
|
|
4124
|
+
this.lastProgressAt = 0;
|
|
4125
|
+
}
|
|
4126
|
+
/** Sliding-window count of successful edits per file → churn signal. */
|
|
4127
|
+
trackFileChurn(toolName, ok, input) {
|
|
4128
|
+
if (!ok || !FILE_EDIT_TOOLS.has(toolName.toLowerCase())) return;
|
|
4129
|
+
const path20 = editedPath(input);
|
|
4130
|
+
if (!path20) return;
|
|
4131
|
+
const now = Date.now();
|
|
4132
|
+
const stamps = (this.editTimestamps.get(path20) ?? []).filter(
|
|
4133
|
+
(t) => now - t <= this.fileChurnWindowMs
|
|
4134
|
+
);
|
|
4135
|
+
stamps.push(now);
|
|
4136
|
+
if (stamps.length >= this.fileChurnThreshold) {
|
|
4137
|
+
this.editTimestamps.delete(path20);
|
|
4138
|
+
void this.engage("file_churn", {
|
|
4139
|
+
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?`,
|
|
4140
|
+
context: [
|
|
4141
|
+
`File: ${path20}`,
|
|
4142
|
+
`Edits in window: ${stamps.length}`,
|
|
4143
|
+
`Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
|
|
4144
|
+
].join("\n")
|
|
4145
|
+
});
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
this.editTimestamps.set(path20, stamps);
|
|
3619
4149
|
}
|
|
3620
4150
|
async engage(kind, input) {
|
|
3621
4151
|
const last = this.lastEngagedAt.get(kind) ?? 0;
|
|
@@ -4422,76 +4952,45 @@ import * as path5 from "node:path";
|
|
|
4422
4952
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4423
4953
|
|
|
4424
4954
|
// 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
|
-
};
|
|
4955
|
+
function defineAgent(id, name, promptRole = id, provider) {
|
|
4956
|
+
return {
|
|
4957
|
+
id,
|
|
4958
|
+
name,
|
|
4959
|
+
role: id,
|
|
4960
|
+
prompt: agentPrompt(promptRole),
|
|
4961
|
+
...provider === void 0 ? {} : { provider }
|
|
4962
|
+
};
|
|
4963
|
+
}
|
|
4964
|
+
var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
|
|
4965
|
+
var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
|
|
4966
|
+
var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
|
|
4967
|
+
var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
|
|
4968
|
+
var SHADOW_AGENT = defineAgent("shadow-agent", "Shadow");
|
|
4969
|
+
var CRITIC_AGENT = defineAgent("critic", "Critic");
|
|
4471
4970
|
var FLEET_ROSTER = {
|
|
4472
4971
|
"audit-log": AUDIT_LOG_AGENT,
|
|
4473
4972
|
"bug-hunter": BUG_HUNTER_AGENT,
|
|
4474
4973
|
"refactor-planner": REFACTOR_PLANNER_AGENT,
|
|
4475
4974
|
"security-scanner": SECURITY_SCANNER_AGENT,
|
|
4476
|
-
|
|
4975
|
+
critic: CRITIC_AGENT,
|
|
4477
4976
|
"shadow-agent": SHADOW_AGENT,
|
|
4478
4977
|
...Object.fromEntries(
|
|
4479
4978
|
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
|
|
4480
4979
|
)
|
|
4481
4980
|
};
|
|
4482
|
-
var DEFAULT_IDLE_TIMEOUT_MS =
|
|
4981
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4483
4982
|
var FLEET_ROSTER_BUDGETS = {
|
|
4484
|
-
"audit-log": { timeoutMs:
|
|
4485
|
-
"bug-hunter": { timeoutMs:
|
|
4486
|
-
"refactor-planner": { timeoutMs:
|
|
4487
|
-
"security-scanner": { timeoutMs:
|
|
4488
|
-
|
|
4983
|
+
"audit-log": { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2500, maxToolCalls: 7500 },
|
|
4984
|
+
"bug-hunter": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
|
|
4985
|
+
"refactor-planner": { timeoutMs: 4 * 60 * 60 * 1e3, maxIterations: 3e3, maxToolCalls: 9e3 },
|
|
4986
|
+
"security-scanner": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
|
|
4987
|
+
critic: { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2e3, maxToolCalls: 6e3 },
|
|
4489
4988
|
"shadow-agent": {
|
|
4490
4989
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
4491
|
-
maxIterations:
|
|
4492
|
-
maxToolCalls:
|
|
4493
|
-
maxTokens:
|
|
4494
|
-
maxCostUsd:
|
|
4990
|
+
maxIterations: 1e3,
|
|
4991
|
+
maxToolCalls: 2500,
|
|
4992
|
+
maxTokens: 3e4,
|
|
4993
|
+
maxCostUsd: 0.5
|
|
4495
4994
|
},
|
|
4496
4995
|
...Object.fromEntries(
|
|
4497
4996
|
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
|
|
@@ -4499,8 +4998,8 @@ var FLEET_ROSTER_BUDGETS = {
|
|
|
4499
4998
|
};
|
|
4500
4999
|
var GENERIC_SUBAGENT_BUDGET = {
|
|
4501
5000
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
4502
|
-
maxIterations:
|
|
4503
|
-
maxToolCalls:
|
|
5001
|
+
maxIterations: 2500,
|
|
5002
|
+
maxToolCalls: 7500
|
|
4504
5003
|
};
|
|
4505
5004
|
function applyRosterBudget(cfg) {
|
|
4506
5005
|
const roleBudget = cfg.role ? FLEET_ROSTER_BUDGETS[cfg.role] : void 0;
|
|
@@ -4522,41 +5021,11 @@ function applyRosterBudget(cfg) {
|
|
|
4522
5021
|
};
|
|
4523
5022
|
}
|
|
4524
5023
|
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
|
-
};
|
|
5024
|
+
var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
|
|
5025
|
+
var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
|
|
5026
|
+
var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
|
|
5027
|
+
var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
|
|
5028
|
+
var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
|
|
4560
5029
|
var ACP_AGENTS = [
|
|
4561
5030
|
CLINE_AGENT,
|
|
4562
5031
|
GEMINI_CLI_AGENT,
|
|
@@ -4564,11 +5033,14 @@ var ACP_AGENTS = [
|
|
|
4564
5033
|
OPENHANDS_AGENT,
|
|
4565
5034
|
GOOSE_AGENT
|
|
4566
5035
|
];
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
5036
|
+
var ACP_AGENT_BUDGET = {
|
|
5037
|
+
timeoutMs: 10 * 60 * 60 * 1e3,
|
|
5038
|
+
maxIterations: 8e3,
|
|
5039
|
+
maxToolCalls: 2e4
|
|
5040
|
+
};
|
|
5041
|
+
for (const agent of ACP_AGENTS) {
|
|
5042
|
+
FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
|
|
5043
|
+
}
|
|
4572
5044
|
var FLEET_ROSTER_WITHACP = {
|
|
4573
5045
|
...FLEET_ROSTER,
|
|
4574
5046
|
...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
|
|
@@ -4576,75 +5048,81 @@ var FLEET_ROSTER_WITHACP = {
|
|
|
4576
5048
|
|
|
4577
5049
|
// src/coordination/delegate-tool.ts
|
|
4578
5050
|
function createDelegateTool(opts) {
|
|
4579
|
-
const defaultTimeoutMs = opts.defaultTimeoutMs ??
|
|
5051
|
+
const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
|
|
4580
5052
|
const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
|
|
4581
5053
|
const inputSchema = {
|
|
4582
5054
|
type: "object",
|
|
4583
5055
|
properties: {
|
|
4584
5056
|
task: {
|
|
4585
5057
|
type: "string",
|
|
4586
|
-
description: "What the subagent should do \u2014 natural language, complete sentence(s).
|
|
5058
|
+
description: "What the subagent should do \u2014 natural language, complete sentence(s)."
|
|
4587
5059
|
},
|
|
4588
5060
|
role: {
|
|
4589
5061
|
type: "string",
|
|
4590
|
-
description: rosterIds.length > 0 ?
|
|
4591
|
-
enum: rosterIds.length > 0 ? rosterIds : void 0
|
|
5062
|
+
description: rosterIds.length > 0 ? "Roster role id. Common: bug-hunter, security-scanner, refactor-planner, critic, audit-log, executor, shadow-agent, architect." : "No roster configured \u2014 pass `name` instead."
|
|
4592
5063
|
},
|
|
4593
5064
|
name: {
|
|
4594
5065
|
type: "string",
|
|
4595
|
-
description: "Display name for free-form subagents (
|
|
5066
|
+
description: "Display name for free-form subagents (no roster role). Required when `role` is omitted."
|
|
4596
5067
|
},
|
|
4597
5068
|
provider: {
|
|
4598
5069
|
type: "string",
|
|
4599
|
-
description: 'Provider id (e.g. "anthropic", "openai"). Defaults to
|
|
5070
|
+
description: 'Provider id (e.g. "anthropic", "openai"). Defaults to host provider.'
|
|
4600
5071
|
},
|
|
4601
5072
|
model: {
|
|
4602
5073
|
type: "string",
|
|
4603
|
-
description: "Model id within the provider. Defaults to
|
|
5074
|
+
description: "Model id within the provider. Defaults to host model."
|
|
4604
5075
|
},
|
|
4605
5076
|
systemPromptOverride: {
|
|
4606
5077
|
type: "string",
|
|
4607
|
-
description: "
|
|
5078
|
+
description: "Extra prompt text appended to the role baseline."
|
|
4608
5079
|
},
|
|
4609
5080
|
timeoutMs: {
|
|
4610
5081
|
type: "number",
|
|
4611
5082
|
minimum: 1,
|
|
4612
|
-
description: `Wall-clock budget
|
|
5083
|
+
description: `Wall-clock budget in ms (default ${Math.round(defaultTimeoutMs / 1e3 / 60)} min). No hard cap \u2014 set as high as the task needs.`
|
|
4613
5084
|
},
|
|
4614
5085
|
maxIterations: {
|
|
4615
5086
|
type: "number",
|
|
4616
5087
|
minimum: 1,
|
|
4617
|
-
description: "Maximum LLM iterations
|
|
5088
|
+
description: "Maximum LLM iterations. Unset = role default. Raise for deep multi-step tasks."
|
|
4618
5089
|
},
|
|
4619
5090
|
maxToolCalls: {
|
|
4620
5091
|
type: "number",
|
|
4621
5092
|
minimum: 1,
|
|
4622
|
-
description: "Maximum
|
|
5093
|
+
description: "Maximum tool invocations. Unset = role default. Raise for file-heavy tasks."
|
|
4623
5094
|
},
|
|
4624
5095
|
idleTimeoutMs: {
|
|
4625
5096
|
type: "number",
|
|
4626
5097
|
minimum: 1,
|
|
4627
|
-
description: "Idle timeout in ms
|
|
5098
|
+
description: "Idle timeout in ms. Resets on activity. Unset = role default."
|
|
4628
5099
|
},
|
|
4629
5100
|
maxTokens: {
|
|
4630
5101
|
type: "number",
|
|
4631
5102
|
minimum: 1,
|
|
4632
|
-
description: "
|
|
5103
|
+
description: "Max total tokens (input+output). Unset = role default."
|
|
4633
5104
|
},
|
|
4634
5105
|
maxCostUsd: {
|
|
4635
5106
|
type: "number",
|
|
4636
5107
|
minimum: 0,
|
|
4637
|
-
description: "
|
|
5108
|
+
description: "Max estimated USD cost. Unset = role default."
|
|
5109
|
+
},
|
|
5110
|
+
maxHandoffs: {
|
|
5111
|
+
type: "number",
|
|
5112
|
+
minimum: 0,
|
|
5113
|
+
maximum: 8,
|
|
5114
|
+
description: "Max fresh-worker continuations after budget exhaustion. Default 1. Each gets the prior partial report."
|
|
4638
5115
|
}
|
|
4639
5116
|
},
|
|
4640
5117
|
required: ["task"]
|
|
4641
5118
|
};
|
|
4642
5119
|
return {
|
|
4643
5120
|
name: "delegate",
|
|
4644
|
-
description: "Hand a
|
|
4645
|
-
usageHint: "Set `task` to a complete instruction.
|
|
5121
|
+
description: "Hand a piece of work to a subagent and wait for its result. Has own context, LLM call, auto-extending budget. Can continue remaining work with a fresh subagent on partial completion (maxHandoffs, default 1). Workers cannot recursively spawn. Use roster roles when possible; otherwise pass `name` + `task`.",
|
|
5122
|
+
usageHint: "Set `task` to a complete instruction. Pick `role` from roster or pass `name` for free-form. Override `timeoutMs`/`maxIterations`/`maxToolCalls` only when needed.",
|
|
4646
5123
|
permission: "auto",
|
|
4647
5124
|
mutating: false,
|
|
5125
|
+
managesOwnTimeout: true,
|
|
4648
5126
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
4649
5127
|
inputSchema,
|
|
4650
5128
|
async execute(input, _ctx, execOpts) {
|
|
@@ -4668,10 +5146,9 @@ function createDelegateTool(opts) {
|
|
|
4668
5146
|
director = await opts.host.promoteToDirector();
|
|
4669
5147
|
}
|
|
4670
5148
|
if (!director) {
|
|
4671
|
-
const reason = opts.host.getPromotionBlockReason?.();
|
|
4672
5149
|
return {
|
|
4673
5150
|
ok: false,
|
|
4674
|
-
error:
|
|
5151
|
+
error: "Director could not be activated \u2014 fleet orchestration is unavailable."
|
|
4675
5152
|
};
|
|
4676
5153
|
}
|
|
4677
5154
|
const timeoutMs = i.timeoutMs ?? defaultTimeoutMs;
|
|
@@ -4684,7 +5161,7 @@ function createDelegateTool(opts) {
|
|
|
4684
5161
|
error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
|
|
4685
5162
|
};
|
|
4686
5163
|
}
|
|
4687
|
-
cfg = instantiateRosterConfig(i.role, base);
|
|
5164
|
+
cfg = instantiateRosterConfig(i.role, base, i.timeoutMs, defaultTimeoutMs);
|
|
4688
5165
|
if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
|
|
4689
5166
|
if (i.provider) cfg.provider = i.provider;
|
|
4690
5167
|
if (i.model) cfg.model = i.model;
|
|
@@ -4723,172 +5200,175 @@ function createDelegateTool(opts) {
|
|
|
4723
5200
|
cfg.timeoutMs = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
|
|
4724
5201
|
}
|
|
4725
5202
|
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
5203
|
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,
|
|
5204
|
+
const maxHandoffs = Math.min(8, Math.max(0, Math.floor(i.maxHandoffs ?? 1)));
|
|
5205
|
+
const handoffs = [];
|
|
5206
|
+
let delegatedTask = i.task;
|
|
5207
|
+
let handoffCount = 0;
|
|
5208
|
+
for (; ; ) {
|
|
5209
|
+
const attemptConfig = handoffCount === 0 ? cfg : freshHandoffConfig(cfg, i.role, handoffCount);
|
|
5210
|
+
const subagentId = await dir.spawn(attemptConfig);
|
|
5211
|
+
const taskId = await dir.assign({
|
|
5212
|
+
id: randomUUID4(),
|
|
5213
|
+
description: delegatedTask,
|
|
4783
5214
|
subagentId
|
|
4784
5215
|
});
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
stopReason: "aborted",
|
|
4788
|
-
error: "Delegated task aborted \u2014 the run was interrupted.",
|
|
5216
|
+
const result = await awaitDelegateAttempt(
|
|
5217
|
+
dir,
|
|
4789
5218
|
subagentId,
|
|
4790
5219
|
taskId,
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
5220
|
+
timeoutMs,
|
|
5221
|
+
abortSignal
|
|
5222
|
+
);
|
|
5223
|
+
if ("__aborted" in result) {
|
|
5224
|
+
try {
|
|
5225
|
+
await dir.terminate(subagentId);
|
|
5226
|
+
} catch {
|
|
5227
|
+
}
|
|
5228
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
5229
|
+
opts.events?.emit("delegate.completed", {
|
|
5230
|
+
sessionId,
|
|
5231
|
+
target,
|
|
5232
|
+
task: i.task,
|
|
5233
|
+
ok: false,
|
|
5234
|
+
status: "aborted",
|
|
5235
|
+
summary: `[${target}] aborted \u2014 the run was interrupted`,
|
|
5236
|
+
durationMs: 0,
|
|
5237
|
+
iterations: partial2?.events ?? 0,
|
|
5238
|
+
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5239
|
+
subagentId
|
|
5240
|
+
});
|
|
5241
|
+
return {
|
|
5242
|
+
ok: false,
|
|
5243
|
+
stopReason: "aborted",
|
|
5244
|
+
error: "Delegated task aborted \u2014 the run was interrupted.",
|
|
5245
|
+
subagentId,
|
|
5246
|
+
taskId,
|
|
5247
|
+
partial: partial2,
|
|
5248
|
+
...handoffs.length > 0 ? { handoffs } : {}
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
5251
|
+
if ("__timeout" in result) {
|
|
5252
|
+
try {
|
|
5253
|
+
await dir.terminate(subagentId);
|
|
5254
|
+
} catch {
|
|
5255
|
+
}
|
|
5256
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
5257
|
+
opts.events?.emit("delegate.completed", {
|
|
5258
|
+
sessionId,
|
|
5259
|
+
target,
|
|
5260
|
+
task: i.task,
|
|
5261
|
+
ok: false,
|
|
5262
|
+
status: "host_timeout",
|
|
5263
|
+
summary: `[${target}] timed out \u2014 no progress within ${Math.round(timeoutMs / 1e3)}s`,
|
|
5264
|
+
durationMs: timeoutMs,
|
|
5265
|
+
iterations: partial2?.events ?? 0,
|
|
5266
|
+
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5267
|
+
subagentId
|
|
5268
|
+
});
|
|
5269
|
+
return {
|
|
5270
|
+
ok: false,
|
|
5271
|
+
stopReason: "host_timeout",
|
|
5272
|
+
error: `Subagent timed out: it did not finish or report progress within ${timeoutMs}ms.`,
|
|
5273
|
+
hint: "Raise timeoutMs for unusually long single operations, or split the remaining work.",
|
|
5274
|
+
subagentId,
|
|
5275
|
+
taskId,
|
|
5276
|
+
partial: partial2,
|
|
5277
|
+
...handoffs.length > 0 ? { handoffs } : {}
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
if ("__emptyResult" in result) {
|
|
5281
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
5282
|
+
opts.events?.emit("delegate.completed", {
|
|
5283
|
+
sessionId,
|
|
5284
|
+
target,
|
|
5285
|
+
task: i.task,
|
|
5286
|
+
ok: false,
|
|
5287
|
+
status: "empty_result",
|
|
5288
|
+
summary: `[${target}] completed without a task result`,
|
|
5289
|
+
durationMs: 0,
|
|
5290
|
+
iterations: partial2?.events ?? 0,
|
|
5291
|
+
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
5292
|
+
subagentId
|
|
5293
|
+
});
|
|
5294
|
+
return {
|
|
5295
|
+
ok: false,
|
|
5296
|
+
stopReason: "error",
|
|
5297
|
+
error: "Director returned no task result for the delegated task.",
|
|
5298
|
+
hint: "Check fleet state, then retry or reassign the task.",
|
|
5299
|
+
subagentId,
|
|
5300
|
+
taskId,
|
|
5301
|
+
partial: partial2,
|
|
5302
|
+
...handoffs.length > 0 ? { handoffs } : {}
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
5305
|
+
const partial = result.status === "success" ? void 0 : result.partial ? {
|
|
5306
|
+
lastAssistantText: result.partial.text,
|
|
5307
|
+
toolUsesObserved: result.toolCalls,
|
|
5308
|
+
events: result.iterations
|
|
5309
|
+
} : await readSubagentPartial(opts, subagentId);
|
|
5310
|
+
const continuation = continuationFor(result, partial, attemptConfig);
|
|
5311
|
+
if (continuation && handoffCount < maxHandoffs) {
|
|
5312
|
+
handoffs.push({
|
|
5313
|
+
fromSubagentId: result.subagentId,
|
|
5314
|
+
fromTaskId: result.taskId,
|
|
5315
|
+
status: result.status,
|
|
5316
|
+
errorKind: result.error?.kind,
|
|
5317
|
+
summary: continuation.summary,
|
|
5318
|
+
remainingWork: continuation.remainingWork
|
|
5319
|
+
});
|
|
5320
|
+
handoffCount += 1;
|
|
5321
|
+
delegatedTask = buildHandoffTask(i.task, continuation, handoffCount, maxHandoffs);
|
|
5322
|
+
continue;
|
|
5323
|
+
}
|
|
5324
|
+
const incomplete = result.report?.completion === "partial";
|
|
5325
|
+
const baseStopReason = incomplete ? "handoff_limit" : result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
|
|
5326
|
+
const errorKind = result.error?.kind;
|
|
5327
|
+
const retryable = result.error?.retryable;
|
|
5328
|
+
const backoffMs = result.error?.backoffMs;
|
|
5329
|
+
const summary = incomplete ? `[${target}] partial checkpoint \u2014 handoff limit reached after ${handoffCount} continuation(s)` : buildDelegateSummary(i.role, result);
|
|
5330
|
+
let costUsd;
|
|
5331
|
+
try {
|
|
5332
|
+
costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
|
|
5333
|
+
} catch {
|
|
5334
|
+
costUsd = void 0;
|
|
5335
|
+
}
|
|
4796
5336
|
opts.events?.emit("delegate.completed", {
|
|
4797
5337
|
sessionId,
|
|
4798
5338
|
target,
|
|
4799
5339
|
task: i.task,
|
|
4800
|
-
ok:
|
|
4801
|
-
status: "
|
|
4802
|
-
summary
|
|
4803
|
-
durationMs:
|
|
4804
|
-
iterations:
|
|
4805
|
-
toolCalls:
|
|
4806
|
-
|
|
5340
|
+
ok: result.status === "success" && !incomplete,
|
|
5341
|
+
status: incomplete ? "partial" : result.status,
|
|
5342
|
+
summary,
|
|
5343
|
+
durationMs: result.durationMs,
|
|
5344
|
+
iterations: result.iterations,
|
|
5345
|
+
toolCalls: result.toolCalls,
|
|
5346
|
+
costUsd,
|
|
5347
|
+
subagentId: result.subagentId
|
|
4807
5348
|
});
|
|
4808
5349
|
return {
|
|
4809
|
-
ok:
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5350
|
+
ok: result.status === "success" && !incomplete,
|
|
5351
|
+
status: incomplete ? "partial" : result.status,
|
|
5352
|
+
stopReason: baseStopReason,
|
|
5353
|
+
errorKind,
|
|
5354
|
+
retryable,
|
|
5355
|
+
backoffMs,
|
|
5356
|
+
subagentId: result.subagentId,
|
|
5357
|
+
taskId: result.taskId,
|
|
5358
|
+
result: result.result,
|
|
5359
|
+
report: result.report,
|
|
5360
|
+
error: result.error,
|
|
5361
|
+
iterations: result.iterations,
|
|
5362
|
+
toolCalls: result.toolCalls,
|
|
5363
|
+
durationMs: result.durationMs,
|
|
5364
|
+
...partial ? { partial } : {},
|
|
5365
|
+
...handoffs.length > 0 ? { handoffs } : {},
|
|
5366
|
+
...incomplete ? {
|
|
5367
|
+
hint: "A clean partial checkpoint remains. Reinvoke delegate with a larger maxHandoffs or assign report.remaining_work explicitly."
|
|
5368
|
+
} : hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
|
|
5369
|
+
summary
|
|
4816
5370
|
};
|
|
4817
5371
|
}
|
|
4818
|
-
if ("__emptyResult" in result) {
|
|
4819
|
-
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
4820
|
-
opts.events?.emit("delegate.completed", {
|
|
4821
|
-
sessionId,
|
|
4822
|
-
target,
|
|
4823
|
-
task: i.task,
|
|
4824
|
-
ok: false,
|
|
4825
|
-
status: "empty_result",
|
|
4826
|
-
summary: `[${target}] completed without a task result`,
|
|
4827
|
-
durationMs: 0,
|
|
4828
|
-
iterations: partial2?.events ?? 0,
|
|
4829
|
-
toolCalls: partial2?.toolUsesObserved ?? 0,
|
|
4830
|
-
subagentId
|
|
4831
|
-
});
|
|
4832
|
-
return {
|
|
4833
|
-
ok: false,
|
|
4834
|
-
stopReason: "error",
|
|
4835
|
-
error: "Director returned no task result for the delegated task.",
|
|
4836
|
-
hint: "Check fleet state with /fleet status, then retry or reassign the task.",
|
|
4837
|
-
subagentId,
|
|
4838
|
-
taskId,
|
|
4839
|
-
partial: partial2
|
|
4840
|
-
};
|
|
4841
|
-
}
|
|
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
5372
|
} catch (err) {
|
|
4893
5373
|
const message = toErrorMessage(err);
|
|
4894
5374
|
opts.events?.emit("delegate.completed", {
|
|
@@ -4911,10 +5391,103 @@ function createDelegateTool(opts) {
|
|
|
4911
5391
|
}
|
|
4912
5392
|
};
|
|
4913
5393
|
}
|
|
4914
|
-
function
|
|
5394
|
+
async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
|
|
5395
|
+
return new Promise((resolve9) => {
|
|
5396
|
+
let settled = false;
|
|
5397
|
+
let timer;
|
|
5398
|
+
let offAbort = () => {
|
|
5399
|
+
};
|
|
5400
|
+
const finish = (value) => {
|
|
5401
|
+
if (settled) return;
|
|
5402
|
+
settled = true;
|
|
5403
|
+
if (timer) clearTimeout(timer);
|
|
5404
|
+
offTool();
|
|
5405
|
+
offIter();
|
|
5406
|
+
offProgress();
|
|
5407
|
+
offAbort();
|
|
5408
|
+
resolve9(value);
|
|
5409
|
+
};
|
|
5410
|
+
const arm = () => {
|
|
5411
|
+
if (timer) clearTimeout(timer);
|
|
5412
|
+
timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
|
|
5413
|
+
};
|
|
5414
|
+
const bump = (event) => {
|
|
5415
|
+
if (event.subagentId === subagentId) arm();
|
|
5416
|
+
};
|
|
5417
|
+
const offTool = director.fleet.filter("tool.executed", bump);
|
|
5418
|
+
const offIter = director.fleet.filter("iteration.started", bump);
|
|
5419
|
+
const offProgress = director.fleet.filter("tool.progress", bump);
|
|
5420
|
+
if (abortSignal) {
|
|
5421
|
+
const onAbort = () => finish({ __aborted: true });
|
|
5422
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
5423
|
+
offAbort = () => abortSignal.removeEventListener("abort", onAbort);
|
|
5424
|
+
if (abortSignal.aborted) onAbort();
|
|
5425
|
+
}
|
|
5426
|
+
arm();
|
|
5427
|
+
director.awaitTasks([taskId]).then((results) => finish(results[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
function freshHandoffConfig(cfg, role, handoffCount) {
|
|
5431
|
+
return {
|
|
5432
|
+
...cfg,
|
|
5433
|
+
id: role ? `${role}-${randomUUID4().slice(0, 8)}` : `${cfg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "subagent"}-handoff-${handoffCount}-${randomUUID4().slice(0, 6)}`
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
function continuationFor(result, partial, config) {
|
|
5437
|
+
if (result.report?.completion === "partial" && result.report.remaining_work) {
|
|
5438
|
+
return {
|
|
5439
|
+
summary: result.report.summary,
|
|
5440
|
+
remainingWork: result.report.remaining_work,
|
|
5441
|
+
partialText: typeof result.result === "string" ? result.result : partial?.lastAssistantText
|
|
5442
|
+
};
|
|
5443
|
+
}
|
|
5444
|
+
const budgetKinds = /* @__PURE__ */ new Set([
|
|
5445
|
+
"budget_iterations",
|
|
5446
|
+
"budget_tool_calls",
|
|
5447
|
+
"budget_tokens",
|
|
5448
|
+
"budget_cost",
|
|
5449
|
+
"budget_timeout"
|
|
5450
|
+
]);
|
|
5451
|
+
const partialText = result.partial?.text ?? partial?.lastAssistantText;
|
|
5452
|
+
const mayHaveIsolatedWrites = config.worktree !== false && config.worktree !== "off" && (!config.tools || config.tools.some(
|
|
5453
|
+
(tool) => ["write", "edit", "replace", "patch", "bash", "exec", "install", "format"].includes(tool)
|
|
5454
|
+
));
|
|
5455
|
+
if (!mayHaveIsolatedWrites && result.status !== "success" && result.error?.kind && budgetKinds.has(result.error.kind) && partialText) {
|
|
5456
|
+
return {
|
|
5457
|
+
summary: `Prior worker stopped at ${result.error.kind} after ${result.iterations} iterations and ${result.toolCalls} tool calls.`,
|
|
5458
|
+
remainingWork: "Inspect the existing workspace and finish only the work that remains from the original task.",
|
|
5459
|
+
partialText
|
|
5460
|
+
};
|
|
5461
|
+
}
|
|
5462
|
+
return void 0;
|
|
5463
|
+
}
|
|
5464
|
+
function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs) {
|
|
5465
|
+
const partial = continuation.partialText?.trim().slice(-6e3);
|
|
5466
|
+
return [
|
|
5467
|
+
`Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
|
|
5468
|
+
"Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
|
|
5469
|
+
'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
|
|
5470
|
+
"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.",
|
|
5471
|
+
`Original task:
|
|
5472
|
+
${originalTask}`,
|
|
5473
|
+
`Prior checkpoint summary:
|
|
5474
|
+
${continuation.summary}`,
|
|
5475
|
+
`Remaining work:
|
|
5476
|
+
${continuation.remainingWork}`,
|
|
5477
|
+
partial ? `Prior worker's last useful output:
|
|
5478
|
+
${partial}` : ""
|
|
5479
|
+
].filter(Boolean).join("\n\n");
|
|
5480
|
+
}
|
|
5481
|
+
function instantiateRosterConfig(role, base, requestedTimeoutMs, defaultTimeoutMs) {
|
|
4915
5482
|
const withBudget = applyRosterBudget({ ...base, role });
|
|
5483
|
+
const rosterTimeoutMs = FLEET_ROSTER_BUDGETS[role]?.timeoutMs;
|
|
4916
5484
|
return {
|
|
4917
5485
|
...withBudget,
|
|
5486
|
+
// Without an explicit host wait, apply the role's tuned multi-hour
|
|
5487
|
+
// wall-clock budget. With an explicit wait, leave this unset so the buffer
|
|
5488
|
+
// logic above derives a slightly shorter child budget without clamping the
|
|
5489
|
+
// role defaults used by ordinary calls.
|
|
5490
|
+
timeoutMs: requestedTimeoutMs === void 0 ? rosterTimeoutMs ?? defaultTimeoutMs : void 0,
|
|
4918
5491
|
// Give each spawn a fresh id so parallel or repeated delegates
|
|
4919
5492
|
// can use the same role safely.
|
|
4920
5493
|
id: `${role}-${randomUUID4().slice(0, 8)}`
|
|
@@ -5100,13 +5673,13 @@ function makeDependencyWatcherConfig(opts) {
|
|
|
5100
5673
|
const globPatterns = patterns.filter((p) => p.includes("*"));
|
|
5101
5674
|
const plainPatterns = patterns.filter((p) => !p.includes("*"));
|
|
5102
5675
|
function matchesPattern(filePath) {
|
|
5103
|
-
const
|
|
5104
|
-
if (plainPatterns.includes(
|
|
5676
|
+
const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
|
|
5677
|
+
if (plainPatterns.includes(basename7)) return true;
|
|
5105
5678
|
for (const gp of globPatterns) {
|
|
5106
5679
|
const regex = new RegExp(
|
|
5107
5680
|
"^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
5108
5681
|
);
|
|
5109
|
-
if (regex.test(
|
|
5682
|
+
if (regex.test(basename7)) return true;
|
|
5110
5683
|
}
|
|
5111
5684
|
return false;
|
|
5112
5685
|
}
|
|
@@ -5446,8 +6019,86 @@ var FleetContextOverflowError = class extends Error {
|
|
|
5446
6019
|
}
|
|
5447
6020
|
};
|
|
5448
6021
|
|
|
6022
|
+
// src/coordination/director/director-collab.ts
|
|
6023
|
+
var DirectorCollabController = class {
|
|
6024
|
+
constructor(deps) {
|
|
6025
|
+
this.deps = deps;
|
|
6026
|
+
}
|
|
6027
|
+
deps;
|
|
6028
|
+
/** Active collab sessions tracked by sessionId (see spawn).
|
|
6029
|
+
* The tuple holds the session and its Director-registered listener unsubs.
|
|
6030
|
+
* Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
|
|
6031
|
+
* on CollabSession (EventEmitter) across many spawn() calls. */
|
|
6032
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
6033
|
+
async spawn(options) {
|
|
6034
|
+
const session = new CollabSession(this.deps.director, this.deps.fleet, {
|
|
6035
|
+
...options,
|
|
6036
|
+
onBudgetWarning: (alert) => {
|
|
6037
|
+
return options.onBudgetWarning?.(alert) ?? "ignore";
|
|
6038
|
+
}
|
|
6039
|
+
});
|
|
6040
|
+
const doneHandler = () => this.activeSessions.delete(session.sessionId);
|
|
6041
|
+
const errorHandler = () => this.activeSessions.delete(session.sessionId);
|
|
6042
|
+
session.on("session.done", doneHandler);
|
|
6043
|
+
session.on("session.error", errorHandler);
|
|
6044
|
+
const unsubs = [
|
|
6045
|
+
() => session.off("session.done", doneHandler),
|
|
6046
|
+
() => session.off("session.error", errorHandler)
|
|
6047
|
+
];
|
|
6048
|
+
this.activeSessions.set(session.sessionId, { session, unsubs });
|
|
6049
|
+
return session.start();
|
|
6050
|
+
}
|
|
6051
|
+
cancel(sessionId, reason = "Director cancelled") {
|
|
6052
|
+
const entry = this.activeSessions.get(sessionId);
|
|
6053
|
+
if (!entry || entry.session.isCancelled()) return;
|
|
6054
|
+
for (const unsub of entry.unsubs) unsub();
|
|
6055
|
+
entry.session.cancel(reason);
|
|
6056
|
+
for (const [_role, subagentId] of entry.session.getSubagentIds()) {
|
|
6057
|
+
this.deps.coordinator.stop(subagentId).catch((err) => {
|
|
6058
|
+
this.deps.logger?.debug(
|
|
6059
|
+
`stop subagent ${subagentId} failed (may have already completed)`,
|
|
6060
|
+
{
|
|
6061
|
+
subagentId,
|
|
6062
|
+
err: err instanceof Error ? err.message : String(err)
|
|
6063
|
+
}
|
|
6064
|
+
);
|
|
6065
|
+
});
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
onAlert(handler) {
|
|
6069
|
+
return this.deps.fleet.filter("collab.warning", (e) => {
|
|
6070
|
+
handler(e.payload);
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
activeSessionIds() {
|
|
6074
|
+
return Array.from(this.activeSessions.keys());
|
|
6075
|
+
}
|
|
6076
|
+
};
|
|
6077
|
+
|
|
6078
|
+
// src/coordination/director/director-btw-notes.ts
|
|
6079
|
+
var DirectorBtwNotes = class {
|
|
6080
|
+
notes = [];
|
|
6081
|
+
/** Stash a note (trimmed; empty ignored). Returns the pending count. Cap 20. */
|
|
6082
|
+
add(note) {
|
|
6083
|
+
const trimmed = note.trim();
|
|
6084
|
+
if (!trimmed) return this.notes.length;
|
|
6085
|
+
this.notes = [...this.notes, trimmed].slice(-20);
|
|
6086
|
+
return this.notes.length;
|
|
6087
|
+
}
|
|
6088
|
+
/** Read and clear all pending notes, in FIFO order (empty array when none). */
|
|
6089
|
+
drain() {
|
|
6090
|
+
const notes = this.notes;
|
|
6091
|
+
this.notes = [];
|
|
6092
|
+
return notes;
|
|
6093
|
+
}
|
|
6094
|
+
/** Peek at pending notes without consuming them. */
|
|
6095
|
+
peek() {
|
|
6096
|
+
return [...this.notes];
|
|
6097
|
+
}
|
|
6098
|
+
};
|
|
6099
|
+
|
|
5449
6100
|
// src/utils/instruction-file.ts
|
|
5450
|
-
import { readFileSync as
|
|
6101
|
+
import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
|
|
5451
6102
|
import * as path6 from "node:path";
|
|
5452
6103
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5453
6104
|
var textCache = /* @__PURE__ */ new Map();
|
|
@@ -5458,7 +6109,7 @@ function readBundledInstructionText(relativePath) {
|
|
|
5458
6109
|
let resolved = "";
|
|
5459
6110
|
for (const root of instructionRootCandidates()) {
|
|
5460
6111
|
try {
|
|
5461
|
-
resolved =
|
|
6112
|
+
resolved = readFileSync3(path6.join(root, relativePath), "utf8").trimEnd();
|
|
5462
6113
|
break;
|
|
5463
6114
|
} catch {
|
|
5464
6115
|
}
|
|
@@ -5485,7 +6136,7 @@ function instructionRootCandidates() {
|
|
|
5485
6136
|
}
|
|
5486
6137
|
function isDirectory2(candidate) {
|
|
5487
6138
|
try {
|
|
5488
|
-
return
|
|
6139
|
+
return statSync3(candidate).isDirectory();
|
|
5489
6140
|
} catch {
|
|
5490
6141
|
return false;
|
|
5491
6142
|
}
|
|
@@ -8993,13 +9644,12 @@ var Director = class _Director {
|
|
|
8993
9644
|
* coordinator from keeping workers alive for tasks that will never arrive.
|
|
8994
9645
|
*/
|
|
8995
9646
|
workCompleteFlag = false;
|
|
8996
|
-
/** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote).
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
*
|
|
9001
|
-
|
|
9002
|
-
_activeCollabSessions = /* @__PURE__ */ new Map();
|
|
9647
|
+
/** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote).
|
|
9648
|
+
* Owned by DirectorBtwNotes (R4); the public btw methods delegate to it. */
|
|
9649
|
+
btwNotes = new DirectorBtwNotes();
|
|
9650
|
+
/** Owns active collab-debug sessions (spawn/cancel/alert/list). Extracted
|
|
9651
|
+
* from Director in R4; the public collab methods below delegate to it. */
|
|
9652
|
+
collab;
|
|
9003
9653
|
/** Prevents large `ask_subagent` answers from bloating the leader's context window. */
|
|
9004
9654
|
largeAnswerStore;
|
|
9005
9655
|
constructor(opts) {
|
|
@@ -9011,7 +9661,7 @@ var Director = class _Director {
|
|
|
9011
9661
|
this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
9012
9662
|
this.taskResultNotifier = opts.taskResultNotifier;
|
|
9013
9663
|
this.subagentIdleTimeoutMs = typeof opts.subagentIdleTimeoutMs === "number" && Number.isFinite(opts.subagentIdleTimeoutMs) && opts.subagentIdleTimeoutMs >= 0 ? opts.subagentIdleTimeoutMs : void 0;
|
|
9014
|
-
this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ??
|
|
9664
|
+
this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? true;
|
|
9015
9665
|
this.sharedScratchpadPath = opts.sharedScratchpadPath ?? null;
|
|
9016
9666
|
this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
|
|
9017
9667
|
this.maxSpawnDepth = opts.fleetManager?.maxSpawnDepth ?? resolveMaxSpawnDepth(opts.maxSpawnDepth);
|
|
@@ -9022,7 +9672,7 @@ var Director = class _Director {
|
|
|
9022
9672
|
this.dispatchClassifier = opts.dispatchClassifier;
|
|
9023
9673
|
this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
|
|
9024
9674
|
this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
|
|
9025
|
-
this.maxBudgetExtensions = opts.maxBudgetExtensions ??
|
|
9675
|
+
this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 12;
|
|
9026
9676
|
this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
|
|
9027
9677
|
this.maxContext = opts.maxContext ?? 128e3;
|
|
9028
9678
|
this.modelMatrix = opts.modelMatrix;
|
|
@@ -9076,92 +9726,103 @@ var Director = class _Director {
|
|
|
9076
9726
|
);
|
|
9077
9727
|
this.coordinator.setFleetBus(this.fleet);
|
|
9078
9728
|
this.fleetManager?.setCoordinator(this.coordinator);
|
|
9079
|
-
this.taskCompletedListener = (payload) =>
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9729
|
+
this.taskCompletedListener = (payload) => this.handleTaskCompleted(payload);
|
|
9730
|
+
this.coordinator.on("task.completed", this.taskCompletedListener);
|
|
9731
|
+
this.wireBudgetPolicy();
|
|
9732
|
+
this.largeAnswerStore = new LargeAnswerStore(2e3);
|
|
9733
|
+
this.collab = new DirectorCollabController({
|
|
9734
|
+
director: this,
|
|
9735
|
+
fleet: this.fleet,
|
|
9736
|
+
coordinator: this.coordinator,
|
|
9737
|
+
logger: this.logger
|
|
9738
|
+
});
|
|
9739
|
+
}
|
|
9740
|
+
handleTaskCompleted(payload) {
|
|
9741
|
+
const r = payload.result;
|
|
9742
|
+
const internalTask = this.internalTaskIds.delete(r.taskId);
|
|
9743
|
+
if (!internalTask) {
|
|
9744
|
+
this.completed.set(r.taskId, r);
|
|
9745
|
+
if (this.completed.size > _Director.MAX_COMPLETED) {
|
|
9746
|
+
const toDelete = this.completed.size - _Director.MAX_COMPLETED;
|
|
9747
|
+
const keys = [...this.completed.keys()].slice(0, toDelete);
|
|
9748
|
+
for (const k of keys) this.completed.delete(k);
|
|
9749
|
+
}
|
|
9750
|
+
}
|
|
9751
|
+
const waiter = this.taskWaiters.get(r.taskId);
|
|
9752
|
+
if (waiter) {
|
|
9753
|
+
waiter.resolve(r);
|
|
9754
|
+
this.taskWaiters.delete(r.taskId);
|
|
9755
|
+
}
|
|
9756
|
+
let anyConsumed = false;
|
|
9757
|
+
for (const aw of [...this.anyWaiters]) {
|
|
9758
|
+
if (!aw.ids.has(r.taskId)) continue;
|
|
9759
|
+
if (aw.timer) clearTimeout(aw.timer);
|
|
9760
|
+
this.anyWaiters.delete(aw);
|
|
9761
|
+
aw.resolve(r);
|
|
9762
|
+
anyConsumed = true;
|
|
9763
|
+
}
|
|
9764
|
+
const consumedInBand = !!waiter || anyConsumed;
|
|
9765
|
+
if (internalTask) return;
|
|
9766
|
+
const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
|
|
9767
|
+
if (!consumedInBand && this.taskResultNotifier) {
|
|
9768
|
+
const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
|
|
9769
|
+
try {
|
|
9770
|
+
void Promise.resolve(
|
|
9771
|
+
this.taskResultNotifier({
|
|
9772
|
+
taskId: r.taskId,
|
|
9773
|
+
title,
|
|
9774
|
+
status: r.status,
|
|
9775
|
+
subagentId: r.subagentId,
|
|
9776
|
+
subagentName: this.manifestEntries.get(r.subagentId)?.name,
|
|
9777
|
+
resultText,
|
|
9778
|
+
errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
|
|
9779
|
+
partialText: r.partial?.text,
|
|
9780
|
+
report: r.report,
|
|
9781
|
+
iterations: r.iterations,
|
|
9782
|
+
toolCalls: r.toolCalls,
|
|
9783
|
+
durationMs: r.durationMs
|
|
9784
|
+
})
|
|
9785
|
+
).catch(() => {
|
|
9786
|
+
});
|
|
9787
|
+
} catch {
|
|
9128
9788
|
}
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
);
|
|
9154
|
-
if (this.fleetManager) {
|
|
9155
|
-
void this.fleetManager.flushManifest();
|
|
9156
|
-
} else {
|
|
9157
|
-
this.scheduleManifest();
|
|
9789
|
+
}
|
|
9790
|
+
const failed = r.status !== "success";
|
|
9791
|
+
const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
|
|
9792
|
+
this.stateCheckpoint?.recordTaskStatus(r.taskId, {
|
|
9793
|
+
status: failed ? r.status : "completed",
|
|
9794
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9795
|
+
iterations: r.iterations,
|
|
9796
|
+
toolCalls: r.toolCalls,
|
|
9797
|
+
durationMs: r.durationMs,
|
|
9798
|
+
error: errorString
|
|
9799
|
+
});
|
|
9800
|
+
this.stateCheckpoint?.setUsage(this.usage.snapshot());
|
|
9801
|
+
void this.appendSessionEvent(
|
|
9802
|
+
failed ? {
|
|
9803
|
+
type: "task_failed",
|
|
9804
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9805
|
+
taskId: r.taskId,
|
|
9806
|
+
title,
|
|
9807
|
+
error: errorString ?? r.status
|
|
9808
|
+
} : {
|
|
9809
|
+
type: "task_completed",
|
|
9810
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9811
|
+
taskId: r.taskId,
|
|
9812
|
+
title
|
|
9158
9813
|
}
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9814
|
+
);
|
|
9815
|
+
if (this.fleetManager) {
|
|
9816
|
+
void this.fleetManager.flushManifest();
|
|
9817
|
+
} else {
|
|
9818
|
+
this.scheduleManifest();
|
|
9819
|
+
}
|
|
9820
|
+
this.armSubagentIdleRetirement(
|
|
9821
|
+
r.subagentId,
|
|
9822
|
+
this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
|
|
9823
|
+
);
|
|
9824
|
+
}
|
|
9825
|
+
wireBudgetPolicy() {
|
|
9165
9826
|
const extendCounts = /* @__PURE__ */ new Map();
|
|
9166
9827
|
const progressBySubagent = /* @__PURE__ */ new Map();
|
|
9167
9828
|
const lastTimeoutProgress = /* @__PURE__ */ new Map();
|
|
@@ -9271,7 +9932,7 @@ var Director = class _Director {
|
|
|
9271
9932
|
payload.deny();
|
|
9272
9933
|
return;
|
|
9273
9934
|
}
|
|
9274
|
-
if (decision.optionId === "stop"
|
|
9935
|
+
if (decision.optionId === "stop") {
|
|
9275
9936
|
payload.deny();
|
|
9276
9937
|
return;
|
|
9277
9938
|
}
|
|
@@ -9281,7 +9942,6 @@ var Director = class _Director {
|
|
|
9281
9942
|
}
|
|
9282
9943
|
grantExtension();
|
|
9283
9944
|
});
|
|
9284
|
-
this.largeAnswerStore = new LargeAnswerStore(2e3);
|
|
9285
9945
|
}
|
|
9286
9946
|
/**
|
|
9287
9947
|
* Record a granted budget extension and broadcast it on the FleetBus so
|
|
@@ -9339,10 +9999,7 @@ var Director = class _Director {
|
|
|
9339
9999
|
* programmatically without going through the slash-command path.
|
|
9340
10000
|
*/
|
|
9341
10001
|
setLeaderBtwNote(note) {
|
|
9342
|
-
|
|
9343
|
-
if (!trimmed) return this._leaderBtwNotes.length;
|
|
9344
|
-
this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
|
|
9345
|
-
return this._leaderBtwNotes.length;
|
|
10002
|
+
return this.btwNotes.add(note);
|
|
9346
10003
|
}
|
|
9347
10004
|
/**
|
|
9348
10005
|
* Read and clear all pending /btw notes the leader has stashed.
|
|
@@ -9353,16 +10010,14 @@ var Director = class _Director {
|
|
|
9353
10010
|
* to cancel the collab session or let it continue.
|
|
9354
10011
|
*/
|
|
9355
10012
|
getLeaderBtwNotes() {
|
|
9356
|
-
|
|
9357
|
-
this._leaderBtwNotes = [];
|
|
9358
|
-
return notes;
|
|
10013
|
+
return this.btwNotes.drain();
|
|
9359
10014
|
}
|
|
9360
10015
|
/**
|
|
9361
10016
|
* Peek at pending /btw notes without consuming them.
|
|
9362
10017
|
* Useful for UI to show "N pending notes" without clearing them.
|
|
9363
10018
|
*/
|
|
9364
10019
|
peekLeaderBtwNotes() {
|
|
9365
|
-
return
|
|
10020
|
+
return this.btwNotes.peek();
|
|
9366
10021
|
}
|
|
9367
10022
|
/**
|
|
9368
10023
|
* Drain (read + clear) all /btw notes in one call.
|
|
@@ -9382,18 +10037,7 @@ var Director = class _Director {
|
|
|
9382
10037
|
* The CollabDebugReport will reflect 'cancelled' disposition when awaited.
|
|
9383
10038
|
*/
|
|
9384
10039
|
cancelCollabSession(sessionId, reason = "Director cancelled") {
|
|
9385
|
-
|
|
9386
|
-
if (!entry || entry.session.isCancelled()) return;
|
|
9387
|
-
for (const unsub of entry.unsubs) unsub();
|
|
9388
|
-
entry.session.cancel(reason);
|
|
9389
|
-
for (const [_role, subagentId] of entry.session.getSubagentIds()) {
|
|
9390
|
-
this.coordinator.stop(subagentId).catch((err) => {
|
|
9391
|
-
this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
|
|
9392
|
-
subagentId,
|
|
9393
|
-
err: err instanceof Error ? err.message : String(err)
|
|
9394
|
-
});
|
|
9395
|
-
});
|
|
9396
|
-
}
|
|
10040
|
+
this.collab.cancel(sessionId, reason);
|
|
9397
10041
|
}
|
|
9398
10042
|
/**
|
|
9399
10043
|
* Subscribe a callback to be notified whenever a collab session raises
|
|
@@ -9402,9 +10046,7 @@ var Director = class _Director {
|
|
|
9402
10046
|
* Returns an unsubscribe function.
|
|
9403
10047
|
*/
|
|
9404
10048
|
onCollabAlert(handler) {
|
|
9405
|
-
return this.
|
|
9406
|
-
handler(e.payload);
|
|
9407
|
-
});
|
|
10049
|
+
return this.collab.onAlert(handler);
|
|
9408
10050
|
}
|
|
9409
10051
|
/**
|
|
9410
10052
|
* Returns all active (not yet settled) collab session ids.
|
|
@@ -9412,7 +10054,7 @@ var Director = class _Director {
|
|
|
9412
10054
|
* the host to know what can be cancelled.
|
|
9413
10055
|
*/
|
|
9414
10056
|
activeCollabSessions() {
|
|
9415
|
-
return
|
|
10057
|
+
return this.collab.activeSessionIds();
|
|
9416
10058
|
}
|
|
9417
10059
|
/** Best-effort session-writer append. Swallows failures — the director
|
|
9418
10060
|
* must not break a fleet run because the session JSONL handle closed. */
|
|
@@ -9478,6 +10120,77 @@ var Director = class _Director {
|
|
|
9478
10120
|
);
|
|
9479
10121
|
}
|
|
9480
10122
|
const config = { ...callerConfig };
|
|
10123
|
+
this.resolveSpawnModel(config);
|
|
10124
|
+
this.enforceSpawnCaps(config);
|
|
10125
|
+
let result;
|
|
10126
|
+
this.assignSpawnNickname(config);
|
|
10127
|
+
this.applySpawnLineage(config);
|
|
10128
|
+
result = await this.coordinator.spawn(config);
|
|
10129
|
+
if (this.fleetManager) {
|
|
10130
|
+
this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
|
|
10131
|
+
} else {
|
|
10132
|
+
this.spawnCount += 1;
|
|
10133
|
+
this.subagentMeta.set(result.subagentId, {
|
|
10134
|
+
provider: config.provider,
|
|
10135
|
+
model: config.model
|
|
10136
|
+
});
|
|
10137
|
+
if (priceLookup && config.provider && config.model) {
|
|
10138
|
+
this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
10141
|
+
const subagentBridge = new InMemoryAgentBridge(
|
|
10142
|
+
{ agentId: result.subagentId, coordinatorId: this.id },
|
|
10143
|
+
this.transport
|
|
10144
|
+
);
|
|
10145
|
+
this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
|
|
10146
|
+
this.subagentBridges.set(result.subagentId, subagentBridge);
|
|
10147
|
+
this.fleet.emit({
|
|
10148
|
+
subagentId: result.subagentId,
|
|
10149
|
+
ts: Date.now(),
|
|
10150
|
+
type: "subagent.spawned",
|
|
10151
|
+
payload: {
|
|
10152
|
+
subagentId: result.subagentId,
|
|
10153
|
+
taskId: "",
|
|
10154
|
+
// taskId will be set when assign() is called
|
|
10155
|
+
name: config.name,
|
|
10156
|
+
role: config.role,
|
|
10157
|
+
provider: config.provider,
|
|
10158
|
+
model: config.model
|
|
10159
|
+
}
|
|
10160
|
+
});
|
|
10161
|
+
if (!this.fleetManager) {
|
|
10162
|
+
this.manifestEntries.set(result.subagentId, {
|
|
10163
|
+
subagentId: result.subagentId,
|
|
10164
|
+
name: config.name,
|
|
10165
|
+
role: config.role,
|
|
10166
|
+
provider: config.provider,
|
|
10167
|
+
model: config.model,
|
|
10168
|
+
taskIds: []
|
|
10169
|
+
});
|
|
10170
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10171
|
+
this.stateCheckpoint?.recordSpawn(
|
|
10172
|
+
{
|
|
10173
|
+
id: result.subagentId,
|
|
10174
|
+
name: config.name,
|
|
10175
|
+
role: config.role,
|
|
10176
|
+
provider: config.provider,
|
|
10177
|
+
model: config.model,
|
|
10178
|
+
spawnedAt
|
|
10179
|
+
},
|
|
10180
|
+
this.spawnCount
|
|
10181
|
+
);
|
|
10182
|
+
void this.appendSessionEvent({
|
|
10183
|
+
type: "agent_spawned",
|
|
10184
|
+
ts: spawnedAt,
|
|
10185
|
+
agentId: result.subagentId,
|
|
10186
|
+
role: config.role ?? config.name
|
|
10187
|
+
});
|
|
10188
|
+
this.scheduleManifest();
|
|
10189
|
+
}
|
|
10190
|
+
this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
|
|
10191
|
+
return result.subagentId;
|
|
10192
|
+
}
|
|
10193
|
+
resolveSpawnModel(config) {
|
|
9481
10194
|
if (!config.model && this.modelMatrix) {
|
|
9482
10195
|
const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
|
|
9483
10196
|
const resolution = resolveModelMatrixResolution(matrix, config.role);
|
|
@@ -9488,6 +10201,8 @@ var Director = class _Director {
|
|
|
9488
10201
|
if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
|
|
9489
10202
|
}
|
|
9490
10203
|
}
|
|
10204
|
+
}
|
|
10205
|
+
enforceSpawnCaps(config) {
|
|
9491
10206
|
if (this.fleetManager) {
|
|
9492
10207
|
const rejection = this.fleetManager.canSpawn(config);
|
|
9493
10208
|
if (rejection) {
|
|
@@ -9530,7 +10245,8 @@ var Director = class _Director {
|
|
|
9530
10245
|
}
|
|
9531
10246
|
}
|
|
9532
10247
|
}
|
|
9533
|
-
|
|
10248
|
+
}
|
|
10249
|
+
assignSpawnNickname(config) {
|
|
9534
10250
|
const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
|
|
9535
10251
|
if (needsNickname) {
|
|
9536
10252
|
const role = config.role ?? "subagent";
|
|
@@ -9542,6 +10258,8 @@ var Director = class _Director {
|
|
|
9542
10258
|
this._usedNicknames.add(key);
|
|
9543
10259
|
}
|
|
9544
10260
|
}
|
|
10261
|
+
}
|
|
10262
|
+
applySpawnLineage(config) {
|
|
9545
10263
|
const budget = this.fleetManager ? this.fleetManager.budgetSnapshot() : (() => {
|
|
9546
10264
|
const total = this.usage.snapshot().total;
|
|
9547
10265
|
const usedTokens = (total?.input ?? 0) + (total?.output ?? 0);
|
|
@@ -9570,70 +10288,6 @@ var Director = class _Director {
|
|
|
9570
10288
|
...Number.isFinite(budget.remainingCostUsd) ? { remainingCostUsd: budget.remainingCostUsd } : {}
|
|
9571
10289
|
}
|
|
9572
10290
|
};
|
|
9573
|
-
result = await this.coordinator.spawn(config);
|
|
9574
|
-
if (this.fleetManager) {
|
|
9575
|
-
this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
|
|
9576
|
-
} else {
|
|
9577
|
-
this.spawnCount += 1;
|
|
9578
|
-
this.subagentMeta.set(result.subagentId, {
|
|
9579
|
-
provider: config.provider,
|
|
9580
|
-
model: config.model
|
|
9581
|
-
});
|
|
9582
|
-
if (priceLookup && config.provider && config.model) {
|
|
9583
|
-
this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
|
|
9584
|
-
}
|
|
9585
|
-
}
|
|
9586
|
-
const subagentBridge = new InMemoryAgentBridge(
|
|
9587
|
-
{ agentId: result.subagentId, coordinatorId: this.id },
|
|
9588
|
-
this.transport
|
|
9589
|
-
);
|
|
9590
|
-
this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
|
|
9591
|
-
this.subagentBridges.set(result.subagentId, subagentBridge);
|
|
9592
|
-
this.fleet.emit({
|
|
9593
|
-
subagentId: result.subagentId,
|
|
9594
|
-
ts: Date.now(),
|
|
9595
|
-
type: "subagent.spawned",
|
|
9596
|
-
payload: {
|
|
9597
|
-
subagentId: result.subagentId,
|
|
9598
|
-
taskId: "",
|
|
9599
|
-
// taskId will be set when assign() is called
|
|
9600
|
-
name: config.name,
|
|
9601
|
-
role: config.role,
|
|
9602
|
-
provider: config.provider,
|
|
9603
|
-
model: config.model
|
|
9604
|
-
}
|
|
9605
|
-
});
|
|
9606
|
-
if (!this.fleetManager) {
|
|
9607
|
-
this.manifestEntries.set(result.subagentId, {
|
|
9608
|
-
subagentId: result.subagentId,
|
|
9609
|
-
name: config.name,
|
|
9610
|
-
role: config.role,
|
|
9611
|
-
provider: config.provider,
|
|
9612
|
-
model: config.model,
|
|
9613
|
-
taskIds: []
|
|
9614
|
-
});
|
|
9615
|
-
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9616
|
-
this.stateCheckpoint?.recordSpawn(
|
|
9617
|
-
{
|
|
9618
|
-
id: result.subagentId,
|
|
9619
|
-
name: config.name,
|
|
9620
|
-
role: config.role,
|
|
9621
|
-
provider: config.provider,
|
|
9622
|
-
model: config.model,
|
|
9623
|
-
spawnedAt
|
|
9624
|
-
},
|
|
9625
|
-
this.spawnCount
|
|
9626
|
-
);
|
|
9627
|
-
void this.appendSessionEvent({
|
|
9628
|
-
type: "agent_spawned",
|
|
9629
|
-
ts: spawnedAt,
|
|
9630
|
-
agentId: result.subagentId,
|
|
9631
|
-
role: config.role ?? config.name
|
|
9632
|
-
});
|
|
9633
|
-
this.scheduleManifest();
|
|
9634
|
-
}
|
|
9635
|
-
this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
|
|
9636
|
-
return result.subagentId;
|
|
9637
10291
|
}
|
|
9638
10292
|
/**
|
|
9639
10293
|
* Synchronously ask a subagent something via the bridge. Sends a
|
|
@@ -9758,6 +10412,23 @@ var Director = class _Director {
|
|
|
9758
10412
|
await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
9759
10413
|
return this.manifestPath;
|
|
9760
10414
|
}
|
|
10415
|
+
/**
|
|
10416
|
+
* Cancel the pending manifest debounce timer and drain any in-flight
|
|
10417
|
+
* manifest write so that, once this resolves, no Director-owned manifest
|
|
10418
|
+
* write is armed or running.
|
|
10419
|
+
*
|
|
10420
|
+
* Unlike `shutdown()`, this does NOT tear down the coordinator, bridges,
|
|
10421
|
+
* or waiters — it is the lightweight quiesce that `MultiAgentHost.stopAll()`
|
|
10422
|
+
* needs. `stopAll()` flushes the FleetManager's manifest but never touched
|
|
10423
|
+
* the Director's own writer, leaving its armed debounce timer to fire ~2s
|
|
10424
|
+
* later. Under CPU starvation that late, un-awaited `atomicWrite` (temp
|
|
10425
|
+
* sibling + rename) races a caller that deletes the manifest directory,
|
|
10426
|
+
* producing an `ENOTEMPTY` on the parent `rmdir` (Windows especially).
|
|
10427
|
+
*/
|
|
10428
|
+
async quiesceManifest() {
|
|
10429
|
+
this.clearManifestTimer();
|
|
10430
|
+
await this.manifestWriteChain.catch(() => void 0);
|
|
10431
|
+
}
|
|
9761
10432
|
/**
|
|
9762
10433
|
* Tear down the director: stop every subagent, close every bridge
|
|
9763
10434
|
* endpoint, and (when configured) write the final manifest. Idempotent
|
|
@@ -9769,8 +10440,17 @@ var Director = class _Director {
|
|
|
9769
10440
|
this.coordinator.off("task.completed", this.taskCompletedListener);
|
|
9770
10441
|
this.taskCompletedListener = null;
|
|
9771
10442
|
}
|
|
9772
|
-
for (const aw of this.anyWaiters) {
|
|
10443
|
+
for (const aw of [...this.anyWaiters]) {
|
|
9773
10444
|
if (aw.timer) clearTimeout(aw.timer);
|
|
10445
|
+
const firstId = [...aw.ids][0];
|
|
10446
|
+
aw.resolve({
|
|
10447
|
+
taskId: firstId ?? "unknown",
|
|
10448
|
+
subagentId: "director",
|
|
10449
|
+
status: "stopped",
|
|
10450
|
+
iterations: 0,
|
|
10451
|
+
toolCalls: 0,
|
|
10452
|
+
durationMs: 0
|
|
10453
|
+
});
|
|
9774
10454
|
}
|
|
9775
10455
|
this.anyWaiters.clear();
|
|
9776
10456
|
if (this.toolExecFilter) {
|
|
@@ -9784,6 +10464,17 @@ var Director = class _Director {
|
|
|
9784
10464
|
for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
|
|
9785
10465
|
this.subagentIdleTimers.clear();
|
|
9786
10466
|
await this.coordinator.stopAll();
|
|
10467
|
+
for (const [id, waiter] of this.taskWaiters) {
|
|
10468
|
+
waiter.resolve({
|
|
10469
|
+
taskId: id,
|
|
10470
|
+
subagentId: "director",
|
|
10471
|
+
status: "stopped",
|
|
10472
|
+
iterations: 0,
|
|
10473
|
+
toolCalls: 0,
|
|
10474
|
+
durationMs: 0
|
|
10475
|
+
});
|
|
10476
|
+
}
|
|
10477
|
+
this.taskWaiters.clear();
|
|
9787
10478
|
for (const b of this.subagentBridges.values()) {
|
|
9788
10479
|
await b.stop().catch((err) => this.logShutdownError("subagent_bridge_stop", err));
|
|
9789
10480
|
}
|
|
@@ -9791,6 +10482,7 @@ var Director = class _Director {
|
|
|
9791
10482
|
await this.bridge.stop().catch((err) => this.logShutdownError("director_bridge_stop", err));
|
|
9792
10483
|
if (this.fleetManager) {
|
|
9793
10484
|
await this.fleetManager.flushManifest().catch((err) => this.logShutdownError("fleet_manifest_flush", err));
|
|
10485
|
+
await this.manifestWriteChain.catch(() => void 0);
|
|
9794
10486
|
} else if (this.manifestPath) {
|
|
9795
10487
|
await this.writeManifest().catch((err) => this.logShutdownError("manifest_write", err));
|
|
9796
10488
|
}
|
|
@@ -9799,6 +10491,7 @@ var Director = class _Director {
|
|
|
9799
10491
|
await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
|
|
9800
10492
|
await this.stateCheckpoint.releaseLock().catch((err) => this.logShutdownError("state_checkpoint_lock_release", err));
|
|
9801
10493
|
}
|
|
10494
|
+
this.largeAnswerStore.clear();
|
|
9802
10495
|
}
|
|
9803
10496
|
/**
|
|
9804
10497
|
* Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
|
|
@@ -10000,9 +10693,18 @@ var Director = class _Director {
|
|
|
10000
10693
|
}
|
|
10001
10694
|
async terminate(subagentId) {
|
|
10002
10695
|
await this.coordinator.stop(subagentId);
|
|
10696
|
+
void this.remove(subagentId).catch(
|
|
10697
|
+
(err) => this.logShutdownError("terminate_remove", err)
|
|
10698
|
+
);
|
|
10003
10699
|
}
|
|
10004
10700
|
async terminateAll() {
|
|
10701
|
+
const ids = this.status().subagents.map((s) => s.id);
|
|
10005
10702
|
await this.coordinator.stopAll();
|
|
10703
|
+
for (const id of ids) {
|
|
10704
|
+
void this.remove(id).catch(
|
|
10705
|
+
(err) => this.logShutdownError("terminate_all_remove", err)
|
|
10706
|
+
);
|
|
10707
|
+
}
|
|
10006
10708
|
}
|
|
10007
10709
|
async remove(subagentId) {
|
|
10008
10710
|
this.clearSubagentIdleRetirement(subagentId);
|
|
@@ -10042,7 +10744,7 @@ var Director = class _Director {
|
|
|
10042
10744
|
const timer = setTimeout(() => {
|
|
10043
10745
|
this.subagentIdleTimers.delete(subagentId);
|
|
10044
10746
|
const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
|
|
10045
|
-
if (
|
|
10747
|
+
if (entry?.status !== "idle") return;
|
|
10046
10748
|
if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
|
|
10047
10749
|
this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
|
|
10048
10750
|
return;
|
|
@@ -10246,22 +10948,7 @@ var Director = class _Director {
|
|
|
10246
10948
|
* three agents complete or the session times out.
|
|
10247
10949
|
*/
|
|
10248
10950
|
async spawnCollab(options) {
|
|
10249
|
-
|
|
10250
|
-
...options,
|
|
10251
|
-
onBudgetWarning: (alert) => {
|
|
10252
|
-
return options.onBudgetWarning?.(alert) ?? "ignore";
|
|
10253
|
-
}
|
|
10254
|
-
});
|
|
10255
|
-
const doneHandler = () => this._activeCollabSessions.delete(session.sessionId);
|
|
10256
|
-
const errorHandler = () => this._activeCollabSessions.delete(session.sessionId);
|
|
10257
|
-
session.on("session.done", doneHandler);
|
|
10258
|
-
session.on("session.error", errorHandler);
|
|
10259
|
-
const unsubs = [
|
|
10260
|
-
() => session.off("session.done", doneHandler),
|
|
10261
|
-
() => session.off("session.error", errorHandler)
|
|
10262
|
-
];
|
|
10263
|
-
this._activeCollabSessions.set(session.sessionId, { session, unsubs });
|
|
10264
|
-
return session.start();
|
|
10951
|
+
return this.collab.spawn(options);
|
|
10265
10952
|
}
|
|
10266
10953
|
/**
|
|
10267
10954
|
* Resume from a prior checkpoint snapshot (loaded via
|
|
@@ -11634,6 +12321,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11634
12321
|
projectRoot;
|
|
11635
12322
|
checkpointCas;
|
|
11636
12323
|
isSessionInUse;
|
|
12324
|
+
logger;
|
|
11637
12325
|
/**
|
|
11638
12326
|
* In-memory cache for load() results, keyed by session ID. The cache is
|
|
11639
12327
|
* invalidated when the file's mtimeMs or size changes (indicating the
|
|
@@ -11659,6 +12347,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11659
12347
|
this.events = opts.events;
|
|
11660
12348
|
this.secretScrubber = opts.secretScrubber;
|
|
11661
12349
|
this.isSessionInUse = opts.isSessionInUse;
|
|
12350
|
+
this.logger = opts.logger;
|
|
12351
|
+
}
|
|
12352
|
+
/**
|
|
12353
|
+
* Emit a structured warning. Uses the configured Logger when available;
|
|
12354
|
+
* falls back to console.warn(JSON) so warnings are never silently dropped.
|
|
12355
|
+
*/
|
|
12356
|
+
logWarn(msg, ctx) {
|
|
12357
|
+
if (this.logger) {
|
|
12358
|
+
this.logger.warn(msg, ctx);
|
|
12359
|
+
} else {
|
|
12360
|
+
console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
12361
|
+
}
|
|
11662
12362
|
}
|
|
11663
12363
|
/**
|
|
11664
12364
|
* Clear the load() cache. Useful for testing or when the caller knows
|
|
@@ -11760,12 +12460,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11760
12460
|
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
11761
12461
|
return writer;
|
|
11762
12462
|
} catch (err) {
|
|
11763
|
-
await handle.close().catch((e) =>
|
|
11764
|
-
|
|
11765
|
-
event: "session_store.handle_close_failed",
|
|
11766
|
-
|
|
11767
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11768
|
-
})));
|
|
12463
|
+
await handle.close().catch((e) => this.logWarn(
|
|
12464
|
+
"Session handle close failed",
|
|
12465
|
+
{ event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
|
|
12466
|
+
));
|
|
11769
12467
|
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
11770
12468
|
throw err;
|
|
11771
12469
|
}
|
|
@@ -11896,12 +12594,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
11896
12594
|
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
11897
12595
|
return { writer, data: resumedData };
|
|
11898
12596
|
} catch (err) {
|
|
11899
|
-
await handle.close().catch((e) =>
|
|
11900
|
-
|
|
11901
|
-
event: "session_store.handle_close_failed",
|
|
11902
|
-
|
|
11903
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
11904
|
-
})));
|
|
12597
|
+
await handle.close().catch((e) => this.logWarn(
|
|
12598
|
+
"Session handle close failed",
|
|
12599
|
+
{ event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
|
|
12600
|
+
));
|
|
11905
12601
|
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
11906
12602
|
throw err;
|
|
11907
12603
|
}
|
|
@@ -12522,13 +13218,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12522
13218
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
12523
13219
|
const msg = toErrorMessage(err);
|
|
12524
13220
|
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
|
-
}));
|
|
13221
|
+
this.logWarn(
|
|
13222
|
+
"Session manifest write failed",
|
|
13223
|
+
{ event: "session_store.manifest_write_failed", sessionId: id, message: msg }
|
|
13224
|
+
);
|
|
12532
13225
|
});
|
|
12533
13226
|
outcome = "failure";
|
|
12534
13227
|
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
@@ -12938,6 +13631,16 @@ var FleetManager = class {
|
|
|
12938
13631
|
manifestTimer = null;
|
|
12939
13632
|
manifestWriteChain = Promise.resolve();
|
|
12940
13633
|
disposed = false;
|
|
13634
|
+
/**
|
|
13635
|
+
* Teardown freeze. Set by `closeManifest()` after the final manifest is
|
|
13636
|
+
* flushed; makes any subsequent `scheduleManifest`/`writeManifest`/
|
|
13637
|
+
* `flushManifest` a no-op so a late fire-and-forget write (e.g. the
|
|
13638
|
+
* task-completion flush the Director issues via `void flushManifest()`)
|
|
13639
|
+
* cannot land an `atomicWrite` in the manifest directory while a caller is
|
|
13640
|
+
* deleting it. Cleared by `recordSpawn` so a stop-then-spawn-again host
|
|
13641
|
+
* resumes writing.
|
|
13642
|
+
*/
|
|
13643
|
+
closing = false;
|
|
12941
13644
|
manifestDebounceMs;
|
|
12942
13645
|
/** Fleet-wide cost cap. Infinity = no cap. Distinct from SubagentBudget limits,
|
|
12943
13646
|
* which track per-subagent spend — this field caps the entire fleet total. */
|
|
@@ -13119,6 +13822,7 @@ var FleetManager = class {
|
|
|
13119
13822
|
* @param priceLookup Optional per-subagent pricing data
|
|
13120
13823
|
*/
|
|
13121
13824
|
recordSpawn(subagentId, config, priceLookup) {
|
|
13825
|
+
this.closing = false;
|
|
13122
13826
|
this.spawnCount += 1;
|
|
13123
13827
|
this.subagentMeta.set(subagentId, {
|
|
13124
13828
|
provider: config.provider,
|
|
@@ -13156,6 +13860,7 @@ var FleetManager = class {
|
|
|
13156
13860
|
}
|
|
13157
13861
|
async writeManifest() {
|
|
13158
13862
|
if (!this.manifestPath) return null;
|
|
13863
|
+
if (this.closing) return this.manifestPath;
|
|
13159
13864
|
this.clearManifestTimer();
|
|
13160
13865
|
const write = this.manifestWriteChain.catch(() => void 0).then(() => this.writeManifestNow());
|
|
13161
13866
|
this.manifestWriteChain = write.catch(() => void 0);
|
|
@@ -13204,7 +13909,7 @@ var FleetManager = class {
|
|
|
13204
13909
|
* When `manifestDebounceMs` is 0, writes are synchronous (no debounce).
|
|
13205
13910
|
*/
|
|
13206
13911
|
scheduleManifest() {
|
|
13207
|
-
if (this.disposed) return;
|
|
13912
|
+
if (this.disposed || this.closing) return;
|
|
13208
13913
|
if (!this.manifestPath) return;
|
|
13209
13914
|
if (this.manifestDebounceMs === 0) {
|
|
13210
13915
|
void this.writeManifest().catch((err) => {
|
|
@@ -13235,6 +13940,22 @@ var FleetManager = class {
|
|
|
13235
13940
|
process.emitWarning(`FleetManager manifest write failed: ${detail}`, "FleetManagerWarning");
|
|
13236
13941
|
});
|
|
13237
13942
|
}
|
|
13943
|
+
/**
|
|
13944
|
+
* Teardown flush for callers that will delete the manifest directory next
|
|
13945
|
+
* (host `stopAll`/`dispose`). Writes the final manifest, then freezes the
|
|
13946
|
+
* writer so no later fire-and-forget write can start, and drains anything
|
|
13947
|
+
* still in flight. After this resolves it is safe to `rm -rf` the manifest
|
|
13948
|
+
* directory: no `atomicWrite` (temp sibling + rename) can race the removal.
|
|
13949
|
+
*
|
|
13950
|
+
* The freeze is lifted by the next `recordSpawn`, so a stop-then-spawn-again
|
|
13951
|
+
* host keeps persisting manifests.
|
|
13952
|
+
*/
|
|
13953
|
+
async closeManifest() {
|
|
13954
|
+
if (!this.manifestPath) return;
|
|
13955
|
+
await this.flushManifest();
|
|
13956
|
+
this.closing = true;
|
|
13957
|
+
await this.manifestWriteChain.catch(() => void 0);
|
|
13958
|
+
}
|
|
13238
13959
|
clearManifestTimer() {
|
|
13239
13960
|
if (!this.manifestTimer) return;
|
|
13240
13961
|
clearTimeout(this.manifestTimer);
|
|
@@ -13320,6 +14041,184 @@ import { randomUUID as randomUUID10 } from "node:crypto";
|
|
|
13320
14041
|
import * as fsp11 from "node:fs/promises";
|
|
13321
14042
|
import * as path14 from "node:path";
|
|
13322
14043
|
|
|
14044
|
+
// src/coordination/mailbox-constants.ts
|
|
14045
|
+
var AGENT_STALE_MS = 6e4;
|
|
14046
|
+
var CLIENT_STALE_MS = 6e4;
|
|
14047
|
+
var HEARTBEAT_THROTTLE_MS = 5e3;
|
|
14048
|
+
var REGISTRY_CACHE_TTL_MS = 2e3;
|
|
14049
|
+
var LINE_SEPARATOR = "\n";
|
|
14050
|
+
var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
|
|
14051
|
+
var AUTO_COMPACT_INTERVAL_MS = 3e5;
|
|
14052
|
+
var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
|
|
14053
|
+
var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
|
|
14054
|
+
|
|
14055
|
+
// src/coordination/mailbox-message-codec.ts
|
|
14056
|
+
var MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
14057
|
+
"note",
|
|
14058
|
+
"ask",
|
|
14059
|
+
"assign",
|
|
14060
|
+
"steer",
|
|
14061
|
+
"btw",
|
|
14062
|
+
"broadcast",
|
|
14063
|
+
"status",
|
|
14064
|
+
"result",
|
|
14065
|
+
"review",
|
|
14066
|
+
"control"
|
|
14067
|
+
]);
|
|
14068
|
+
var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
|
|
14069
|
+
var TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
14070
|
+
"pending",
|
|
14071
|
+
"in_progress",
|
|
14072
|
+
"completed",
|
|
14073
|
+
"failed",
|
|
14074
|
+
"idle",
|
|
14075
|
+
"running",
|
|
14076
|
+
"streaming",
|
|
14077
|
+
"waiting_user",
|
|
14078
|
+
"error",
|
|
14079
|
+
"offline",
|
|
14080
|
+
"busy"
|
|
14081
|
+
]);
|
|
14082
|
+
function isRecord3(value) {
|
|
14083
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14084
|
+
}
|
|
14085
|
+
function requiredString(record, key) {
|
|
14086
|
+
const value = record[key];
|
|
14087
|
+
if (typeof value !== "string") {
|
|
14088
|
+
throw new TypeError(`mailbox message field "${key}" must be a string`);
|
|
14089
|
+
}
|
|
14090
|
+
return value;
|
|
14091
|
+
}
|
|
14092
|
+
function optionalString2(record, key) {
|
|
14093
|
+
const value = record[key];
|
|
14094
|
+
if (value === void 0) return {};
|
|
14095
|
+
if (typeof value !== "string") {
|
|
14096
|
+
throw new TypeError(`mailbox message field "${key}" must be a string when present`);
|
|
14097
|
+
}
|
|
14098
|
+
return { [key]: value };
|
|
14099
|
+
}
|
|
14100
|
+
function parseMessageType(value) {
|
|
14101
|
+
if (value === "info") return "note";
|
|
14102
|
+
if (value === "task") return "assign";
|
|
14103
|
+
if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
|
|
14104
|
+
return value;
|
|
14105
|
+
}
|
|
14106
|
+
throw new TypeError('mailbox message field "type" is invalid');
|
|
14107
|
+
}
|
|
14108
|
+
function normalizeMailboxMessageType(value) {
|
|
14109
|
+
return parseMessageType(value);
|
|
14110
|
+
}
|
|
14111
|
+
function parsePriority(value) {
|
|
14112
|
+
if (typeof value === "string" && PRIORITIES.has(value)) {
|
|
14113
|
+
return value;
|
|
14114
|
+
}
|
|
14115
|
+
if (typeof value === "string") return "normal";
|
|
14116
|
+
throw new TypeError('mailbox message field "priority" must be a string');
|
|
14117
|
+
}
|
|
14118
|
+
function parseReadReceipts(record, to) {
|
|
14119
|
+
const value = record["readBy"];
|
|
14120
|
+
if (value === void 0) {
|
|
14121
|
+
const legacyReadAt = record["readAt"];
|
|
14122
|
+
return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
|
|
14123
|
+
}
|
|
14124
|
+
if (!isRecord3(value)) {
|
|
14125
|
+
throw new TypeError('mailbox message field "readBy" must be an object');
|
|
14126
|
+
}
|
|
14127
|
+
const receipts = {};
|
|
14128
|
+
for (const [agentId, timestamp] of Object.entries(value)) {
|
|
14129
|
+
if (typeof timestamp !== "string") {
|
|
14130
|
+
throw new TypeError("mailbox message read receipt timestamps must be strings");
|
|
14131
|
+
}
|
|
14132
|
+
receipts[agentId] = timestamp;
|
|
14133
|
+
}
|
|
14134
|
+
return receipts;
|
|
14135
|
+
}
|
|
14136
|
+
function parseTaskContext(value) {
|
|
14137
|
+
if (value === void 0) return void 0;
|
|
14138
|
+
if (!isRecord3(value)) {
|
|
14139
|
+
throw new TypeError('mailbox message field "taskContext" must be an object');
|
|
14140
|
+
}
|
|
14141
|
+
const status = value["status"];
|
|
14142
|
+
if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
|
|
14143
|
+
throw new TypeError("mailbox message taskContext status is invalid");
|
|
14144
|
+
}
|
|
14145
|
+
return {
|
|
14146
|
+
...optionalString2(value, "agentRole"),
|
|
14147
|
+
...optionalString2(value, "agentName"),
|
|
14148
|
+
...optionalString2(value, "taskId"),
|
|
14149
|
+
...status === void 0 ? {} : { status }
|
|
14150
|
+
};
|
|
14151
|
+
}
|
|
14152
|
+
function parseMailboxMessage(value) {
|
|
14153
|
+
if (!isRecord3(value)) throw new TypeError("mailbox message must be an object");
|
|
14154
|
+
const to = value["to"] === void 0 ? "" : requiredString(value, "to");
|
|
14155
|
+
const completed = value["completed"];
|
|
14156
|
+
if (typeof completed !== "boolean") {
|
|
14157
|
+
throw new TypeError('mailbox message field "completed" must be a boolean');
|
|
14158
|
+
}
|
|
14159
|
+
const taskContext = parseTaskContext(value["taskContext"]);
|
|
14160
|
+
return {
|
|
14161
|
+
id: requiredString(value, "id"),
|
|
14162
|
+
from: requiredString(value, "from"),
|
|
14163
|
+
to,
|
|
14164
|
+
type: parseMessageType(value["type"]),
|
|
14165
|
+
subject: requiredString(value, "subject"),
|
|
14166
|
+
body: requiredString(value, "body"),
|
|
14167
|
+
priority: parsePriority(value["priority"]),
|
|
14168
|
+
readBy: parseReadReceipts(value, to),
|
|
14169
|
+
completed,
|
|
14170
|
+
timestamp: requiredString(value, "timestamp"),
|
|
14171
|
+
...optionalString2(value, "completedBy"),
|
|
14172
|
+
...optionalString2(value, "outcome"),
|
|
14173
|
+
...optionalString2(value, "completedAt"),
|
|
14174
|
+
...optionalString2(value, "deletedAt"),
|
|
14175
|
+
...optionalString2(value, "deletedBy"),
|
|
14176
|
+
...optionalString2(value, "replyTo"),
|
|
14177
|
+
...optionalString2(value, "senderSessionId"),
|
|
14178
|
+
...optionalString2(value, "expiresAt"),
|
|
14179
|
+
...taskContext === void 0 ? {} : { taskContext }
|
|
14180
|
+
};
|
|
14181
|
+
}
|
|
14182
|
+
function parseMailboxMessageLine(line) {
|
|
14183
|
+
return parseMailboxMessage(JSON.parse(line));
|
|
14184
|
+
}
|
|
14185
|
+
function isAckRecord(value) {
|
|
14186
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && value["__ack"] === true;
|
|
14187
|
+
}
|
|
14188
|
+
function parseMailboxLine(line) {
|
|
14189
|
+
try {
|
|
14190
|
+
const parsed = JSON.parse(line);
|
|
14191
|
+
if (isAckRecord(parsed)) return parsed;
|
|
14192
|
+
return parseMailboxMessage(parsed);
|
|
14193
|
+
} catch {
|
|
14194
|
+
return null;
|
|
14195
|
+
}
|
|
14196
|
+
}
|
|
14197
|
+
function applyAckToMessage(msg, ack) {
|
|
14198
|
+
if (ack.read && !(ack.readerId in msg.readBy)) {
|
|
14199
|
+
msg.readBy[ack.readerId] = ack.timestamp;
|
|
14200
|
+
}
|
|
14201
|
+
if (ack.completed && !msg.completed) {
|
|
14202
|
+
msg.completed = true;
|
|
14203
|
+
msg.completedBy = ack.completedBy ?? ack.readerId;
|
|
14204
|
+
msg.completedAt = ack.timestamp;
|
|
14205
|
+
}
|
|
14206
|
+
if (ack.outcome !== void 0 && msg.outcome !== ack.outcome) {
|
|
14207
|
+
msg.outcome = ack.outcome;
|
|
14208
|
+
}
|
|
14209
|
+
if (ack.deleted === true) {
|
|
14210
|
+
msg.deletedAt = ack.timestamp;
|
|
14211
|
+
msg.deletedBy = ack.deletedBy ?? ack.readerId;
|
|
14212
|
+
}
|
|
14213
|
+
if (ack.deleted === false) {
|
|
14214
|
+
delete msg.deletedAt;
|
|
14215
|
+
delete msg.deletedBy;
|
|
14216
|
+
}
|
|
14217
|
+
}
|
|
14218
|
+
function serializeAckRecord(ack) {
|
|
14219
|
+
return JSON.stringify(ack) + "\n";
|
|
14220
|
+
}
|
|
14221
|
+
|
|
13323
14222
|
// src/coordination/mailbox-types.ts
|
|
13324
14223
|
function normalizeRecipient(to) {
|
|
13325
14224
|
return to.trim().toLowerCase() === "all" ? "*" : to.trim();
|
|
@@ -13328,13 +14227,6 @@ function normalizeRecipient(to) {
|
|
|
13328
14227
|
// src/coordination/global-mailbox.ts
|
|
13329
14228
|
var MAILBOX_FILE = "_mailbox.jsonl";
|
|
13330
14229
|
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
14230
|
function resolveProjectDir(projectRoot, globalRoot) {
|
|
13339
14231
|
return path14.join(globalRoot, "projects", projectSlug(projectRoot));
|
|
13340
14232
|
}
|
|
@@ -13349,6 +14241,12 @@ var GlobalMailbox = class {
|
|
|
13349
14241
|
_events;
|
|
13350
14242
|
/** Optional HQ publisher for cross-project command-center telemetry. */
|
|
13351
14243
|
_hqPublisher;
|
|
14244
|
+
/**
|
|
14245
|
+
* Optional SSE event emitter for real-time push to HTTP bridge clients.
|
|
14246
|
+
* When present, `send`/`ack`/`softDelete`/`restore` emit events that
|
|
14247
|
+
* connected SSE subscribers receive instantly.
|
|
14248
|
+
*/
|
|
14249
|
+
eventEmitter;
|
|
13352
14250
|
/**
|
|
13353
14251
|
* Local cache of the agent registry to avoid re-reading on every call.
|
|
13354
14252
|
* Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
|
|
@@ -13391,20 +14289,47 @@ var GlobalMailbox = class {
|
|
|
13391
14289
|
* appended message). The chain runs each read to completion before
|
|
13392
14290
|
* the next starts, in issue order — readers are read-only and never
|
|
13393
14291
|
* conflict with each other on content, only on the cache mutation
|
|
13394
|
-
* that follows the read. Pattern mirrors
|
|
14292
|
+
* that follows the read. Pattern mirrors a serialized mutation chain.
|
|
13395
14293
|
*/
|
|
13396
14294
|
_readChain = Promise.resolve([]);
|
|
14295
|
+
/**
|
|
14296
|
+
* Recipient → Set of indices into `_messageCache`. Maintained alongside
|
|
14297
|
+
* the cache so `query({ to })` and `unreadCount(agentId)` can skip the
|
|
14298
|
+
* O(N) full scan and iterate only the messages addressed to the target
|
|
14299
|
+
* recipient (plus broadcasts `*`).
|
|
14300
|
+
*
|
|
14301
|
+
* The index is rebuilt whenever the cache array is replaced
|
|
14302
|
+
* (`_setMessageCache`), extended when new messages are pushed
|
|
14303
|
+
* (`_pushToCache`), and cleared on `close()`.
|
|
14304
|
+
* It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
|
|
14305
|
+
* or never populated).
|
|
14306
|
+
*
|
|
14307
|
+
* Safety: array indices are valid only as long as the cache array
|
|
14308
|
+
* reference hasn't changed. Since we rebuild the index in the same
|
|
14309
|
+
* synchronous step that replaces the cache, a reader that calls
|
|
14310
|
+
* `_readMessagesCached()` either gets the old array + old index, or
|
|
14311
|
+
* the new array + new index — never a mismatch.
|
|
14312
|
+
*/
|
|
14313
|
+
_recipientIndex = null;
|
|
14314
|
+
/**
|
|
14315
|
+
* Sender → Set of indices into `_messageCache`. Same lifecycle and
|
|
14316
|
+
* safety properties as `_recipientIndex`, but keyed on `msg.from`
|
|
14317
|
+
* so `query({ from })` can skip the full scan.
|
|
14318
|
+
*/
|
|
14319
|
+
_senderIndex = null;
|
|
13397
14320
|
/**
|
|
13398
14321
|
* @param projectDir — `~/.wrongstack/projects/<slug>/`
|
|
13399
14322
|
* @param events — optional EventBus for real-time TUI/WebUI notifications
|
|
13400
14323
|
* @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
|
|
14324
|
+
* @param eventEmitter — optional SSE event emitter for HTTP bridge push
|
|
13401
14325
|
*/
|
|
13402
|
-
constructor(projectDir, events, hqPublisher) {
|
|
14326
|
+
constructor(projectDir, events, hqPublisher, eventEmitter) {
|
|
13403
14327
|
this.messagePath = path14.join(projectDir, MAILBOX_FILE);
|
|
13404
14328
|
this.registryPath = path14.join(projectDir, "_mailbox.registry.json");
|
|
13405
14329
|
this.clientRegistryPath = path14.join(projectDir, CLIENT_REGISTRY_FILE);
|
|
13406
14330
|
this._events = events;
|
|
13407
14331
|
this._hqPublisher = hqPublisher;
|
|
14332
|
+
this.eventEmitter = eventEmitter;
|
|
13408
14333
|
}
|
|
13409
14334
|
get hqMailboxId() {
|
|
13410
14335
|
return `${path14.basename(path14.dirname(this.messagePath))}:mailbox`;
|
|
@@ -13433,7 +14358,7 @@ var GlobalMailbox = class {
|
|
|
13433
14358
|
// "all" is an accepted spelling of the broadcast address — canonical
|
|
13434
14359
|
// form on disk is '*' so every query/checker matches it.
|
|
13435
14360
|
to: normalizeRecipient(input.to),
|
|
13436
|
-
type: input.type,
|
|
14361
|
+
type: normalizeMailboxMessageType(input.type),
|
|
13437
14362
|
subject: input.subject,
|
|
13438
14363
|
body: input.body,
|
|
13439
14364
|
priority: input.priority ?? "normal",
|
|
@@ -13441,7 +14366,8 @@ var GlobalMailbox = class {
|
|
|
13441
14366
|
completed: false,
|
|
13442
14367
|
timestamp: now,
|
|
13443
14368
|
replyTo: input.replyTo,
|
|
13444
|
-
taskContext: input.taskContext
|
|
14369
|
+
taskContext: input.taskContext,
|
|
14370
|
+
expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
|
|
13445
14371
|
};
|
|
13446
14372
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
13447
14373
|
await fsp11.mkdir(path14.dirname(this.messagePath), { recursive: true });
|
|
@@ -13456,21 +14382,49 @@ var GlobalMailbox = class {
|
|
|
13456
14382
|
message: msg
|
|
13457
14383
|
});
|
|
13458
14384
|
this.publishHqMailboxSnapshot();
|
|
14385
|
+
this._events?.emitCustom("mailbox.message_sent", {
|
|
14386
|
+
messageId: msg.id,
|
|
14387
|
+
from: msg.from,
|
|
14388
|
+
to: msg.to,
|
|
14389
|
+
type: msg.type,
|
|
14390
|
+
subject: msg.subject
|
|
14391
|
+
});
|
|
14392
|
+
this.eventEmitter?.emit({
|
|
14393
|
+
type: "message.sent",
|
|
14394
|
+
messageId: msg.id,
|
|
14395
|
+
from: msg.from,
|
|
14396
|
+
to: msg.to,
|
|
14397
|
+
timestamp: now
|
|
14398
|
+
});
|
|
13459
14399
|
return msg;
|
|
13460
14400
|
}
|
|
13461
14401
|
async query(q) {
|
|
14402
|
+
const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
|
|
13462
14403
|
const all = await this._readMessagesCached();
|
|
13463
14404
|
const limit = q.limit ?? 50;
|
|
13464
14405
|
const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
|
|
13465
14406
|
const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
|
|
13466
14407
|
const out = [];
|
|
13467
|
-
|
|
13468
|
-
|
|
14408
|
+
let candidates;
|
|
14409
|
+
if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
|
|
14410
|
+
const indices = /* @__PURE__ */ new Set();
|
|
14411
|
+
const direct = this._recipientIndex.get(q.to);
|
|
14412
|
+
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
14413
|
+
const broadcasts = this._recipientIndex.get("*");
|
|
14414
|
+
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
14415
|
+
candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
|
|
14416
|
+
} else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
|
|
14417
|
+
const indices = this._senderIndex.get(q.from);
|
|
14418
|
+
candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
|
|
14419
|
+
} else {
|
|
14420
|
+
candidates = all;
|
|
14421
|
+
}
|
|
14422
|
+
for (const m of candidates) {
|
|
13469
14423
|
if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
|
|
13470
14424
|
if (q.from !== void 0 && m.from !== q.from) continue;
|
|
13471
14425
|
if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
|
|
13472
14426
|
if (q.incompleteOnly && m.completed) continue;
|
|
13473
|
-
if (
|
|
14427
|
+
if (queryType !== void 0 && m.type !== queryType) continue;
|
|
13474
14428
|
if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
|
|
13475
14429
|
continue;
|
|
13476
14430
|
}
|
|
@@ -13485,42 +14439,100 @@ var GlobalMailbox = class {
|
|
|
13485
14439
|
const updated = await this.ackMany({ acks: [input] });
|
|
13486
14440
|
return updated.length > 0 ? updated[0] : null;
|
|
13487
14441
|
}
|
|
14442
|
+
/** Diagnostic counters exposed for test/tool introspection. */
|
|
14443
|
+
diag = {
|
|
14444
|
+
ackManyCacheDesync: 0,
|
|
14445
|
+
ackManyPreLockMtime: 0,
|
|
14446
|
+
ackManyPostLockMtime: 0,
|
|
14447
|
+
ackManyPreLockSize: 0,
|
|
14448
|
+
ackManyPostLockSize: 0,
|
|
14449
|
+
ackManyMessageCount: 0,
|
|
14450
|
+
ackManyAckCount: 0,
|
|
14451
|
+
cacheHitCount: 0,
|
|
14452
|
+
cacheMissCount: 0,
|
|
14453
|
+
cacheDesyncNoOp: 0,
|
|
14454
|
+
applyAckTargetMissing: 0,
|
|
14455
|
+
setMessageCacheCount: 0,
|
|
14456
|
+
pushToCacheCount: 0
|
|
14457
|
+
};
|
|
13488
14458
|
async ackMany(input) {
|
|
13489
14459
|
if (input.acks.length === 0) return [];
|
|
14460
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14461
|
+
const ackRecords = [];
|
|
13490
14462
|
const updated = [];
|
|
14463
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
13491
14464
|
const byId = /* @__PURE__ */ new Map();
|
|
13492
14465
|
for (const a of input.acks) {
|
|
13493
14466
|
byId.set(a.messageId, a);
|
|
14467
|
+
targetIds.add(a.messageId);
|
|
13494
14468
|
}
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
msg.completedBy = a.readerId;
|
|
13510
|
-
msg.completedAt = now;
|
|
13511
|
-
changed = true;
|
|
13512
|
-
}
|
|
13513
|
-
if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
|
|
13514
|
-
msg.outcome = a.outcome;
|
|
13515
|
-
changed = true;
|
|
13516
|
-
}
|
|
14469
|
+
const preLockMtime = this._messageCacheMtime;
|
|
14470
|
+
const preLockSize = this._messageCacheSize;
|
|
14471
|
+
this.diag.ackManyPreLockMtime = preLockMtime;
|
|
14472
|
+
this.diag.ackManyPreLockSize = preLockSize;
|
|
14473
|
+
const all = await this._readMessagesCached();
|
|
14474
|
+
for (const msg of all) {
|
|
14475
|
+
const a = byId.get(msg.id);
|
|
14476
|
+
if (!a) continue;
|
|
14477
|
+
const needsRead = a.read !== false && !(a.readerId in msg.readBy);
|
|
14478
|
+
const needsComplete = a.completed && !msg.completed;
|
|
14479
|
+
const needsOutcome = a.outcome !== void 0 && msg.outcome !== a.outcome;
|
|
14480
|
+
if (!needsRead && !needsComplete && !needsOutcome) {
|
|
14481
|
+
updated.push({ ...msg, readBy: { ...msg.readBy } });
|
|
14482
|
+
continue;
|
|
13517
14483
|
}
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
14484
|
+
const copy = { ...msg, readBy: { ...msg.readBy } };
|
|
14485
|
+
if (needsRead) copy.readBy[a.readerId] = now;
|
|
14486
|
+
if (needsComplete) {
|
|
14487
|
+
copy.completed = true;
|
|
14488
|
+
copy.completedBy = a.readerId;
|
|
14489
|
+
copy.completedAt = now;
|
|
14490
|
+
}
|
|
14491
|
+
if (needsOutcome) copy.outcome = a.outcome;
|
|
14492
|
+
updated.push(copy);
|
|
14493
|
+
ackRecords.push({
|
|
14494
|
+
__ack: true,
|
|
14495
|
+
messageId: msg.id,
|
|
14496
|
+
readerId: a.readerId,
|
|
14497
|
+
timestamp: now,
|
|
14498
|
+
read: a.read !== false,
|
|
14499
|
+
completed: a.completed,
|
|
14500
|
+
completedBy: a.completed ? a.readerId : void 0,
|
|
14501
|
+
outcome: a.outcome
|
|
14502
|
+
});
|
|
14503
|
+
}
|
|
14504
|
+
if (ackRecords.length === 0) {
|
|
14505
|
+
if (updated.length > 0) {
|
|
14506
|
+
for (const message of updated) {
|
|
14507
|
+
this.publishHqMailboxEvent({
|
|
14508
|
+
mailboxId: this.hqMailboxId,
|
|
14509
|
+
action: message.completed ? "message.completed" : "message.read",
|
|
14510
|
+
message
|
|
14511
|
+
});
|
|
14512
|
+
}
|
|
13521
14513
|
}
|
|
14514
|
+
return updated;
|
|
14515
|
+
}
|
|
14516
|
+
const serialized = ackRecords.map((a) => serializeAckRecord(a)).join("");
|
|
14517
|
+
await withFileLock(this.messagePath, async () => {
|
|
14518
|
+
await fsp11.appendFile(this.messagePath, serialized, "utf8");
|
|
13522
14519
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
13523
|
-
|
|
14520
|
+
for (const ack of ackRecords) {
|
|
14521
|
+
this._applyAckToCache(ack);
|
|
14522
|
+
}
|
|
14523
|
+
this._messageCacheMtime = mtimeMs;
|
|
14524
|
+
this._messageCacheSize = size;
|
|
14525
|
+
this.diag.ackManyPostLockMtime = mtimeMs;
|
|
14526
|
+
this.diag.ackManyPostLockSize = size;
|
|
14527
|
+
this.diag.ackManyMessageCount = this._messageCache?.length ?? -1;
|
|
14528
|
+
this.diag.ackManyAckCount = ackRecords.length;
|
|
14529
|
+
const expectedMinSize = preLockSize + serialized.length;
|
|
14530
|
+
if (preLockSize > 0 && size > expectedMinSize) {
|
|
14531
|
+
const extraBytes = size - expectedMinSize;
|
|
14532
|
+
console.error(
|
|
14533
|
+
`[MAILBOX-DIAG] ackMany: post-lock size ${size} exceeds expected ${expectedMinSize} by ${extraBytes}B (mtime ${preLockMtime}->${mtimeMs}). Cache may be missing cross-process messages. diag.ackManyCacheDesync=${++this.diag.ackManyCacheDesync}`
|
|
14534
|
+
);
|
|
14535
|
+
}
|
|
13524
14536
|
});
|
|
13525
14537
|
for (const message of updated) {
|
|
13526
14538
|
this.publishHqMailboxEvent({
|
|
@@ -13528,6 +14540,13 @@ var GlobalMailbox = class {
|
|
|
13528
14540
|
action: message.completed ? "message.completed" : "message.read",
|
|
13529
14541
|
message
|
|
13530
14542
|
});
|
|
14543
|
+
this.eventEmitter?.emit({
|
|
14544
|
+
type: "message.acked",
|
|
14545
|
+
messageId: message.id,
|
|
14546
|
+
from: message.from,
|
|
14547
|
+
to: message.to,
|
|
14548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
14549
|
+
});
|
|
13531
14550
|
}
|
|
13532
14551
|
if (updated.length > 0) this.publishHqMailboxSnapshot();
|
|
13533
14552
|
return updated;
|
|
@@ -13535,68 +14554,108 @@ var GlobalMailbox = class {
|
|
|
13535
14554
|
async unreadCount(forAgentId) {
|
|
13536
14555
|
const all = await this._readMessagesCached();
|
|
13537
14556
|
let count = 0;
|
|
13538
|
-
|
|
13539
|
-
const
|
|
13540
|
-
|
|
13541
|
-
|
|
14557
|
+
if (this._recipientIndex !== null) {
|
|
14558
|
+
const indices = /* @__PURE__ */ new Set();
|
|
14559
|
+
const direct = this._recipientIndex.get(forAgentId);
|
|
14560
|
+
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
14561
|
+
const broadcasts = this._recipientIndex.get("*");
|
|
14562
|
+
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
14563
|
+
for (const i of indices) {
|
|
14564
|
+
const m = all[i];
|
|
14565
|
+
if (!(forAgentId in m.readBy) && !m.completed) count++;
|
|
14566
|
+
}
|
|
14567
|
+
} else {
|
|
14568
|
+
for (let i = 0; i < all.length; i++) {
|
|
14569
|
+
const m = all[i];
|
|
14570
|
+
if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
|
|
14571
|
+
count++;
|
|
14572
|
+
}
|
|
13542
14573
|
}
|
|
13543
14574
|
}
|
|
13544
14575
|
return count;
|
|
13545
14576
|
}
|
|
13546
14577
|
async softDelete(mailId, by) {
|
|
13547
|
-
|
|
14578
|
+
const all = await this._readMessagesCached();
|
|
14579
|
+
const target = all.find((m) => m.id === mailId);
|
|
14580
|
+
if (target === void 0) return null;
|
|
14581
|
+
if (target.deletedAt !== void 0) {
|
|
14582
|
+
return { ...target, readBy: { ...target.readBy } };
|
|
14583
|
+
}
|
|
14584
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14585
|
+
const ack = {
|
|
14586
|
+
__ack: true,
|
|
14587
|
+
messageId: mailId,
|
|
14588
|
+
readerId: by,
|
|
14589
|
+
timestamp: now,
|
|
14590
|
+
read: true,
|
|
14591
|
+
deleted: true,
|
|
14592
|
+
deletedBy: by
|
|
14593
|
+
};
|
|
13548
14594
|
await withFileLock(this.messagePath, async () => {
|
|
13549
|
-
|
|
13550
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13551
|
-
for (const m of all) {
|
|
13552
|
-
if (m.id !== mailId) continue;
|
|
13553
|
-
if (m.deletedAt !== void 0) {
|
|
13554
|
-
updated = m;
|
|
13555
|
-
continue;
|
|
13556
|
-
}
|
|
13557
|
-
m.deletedAt = now;
|
|
13558
|
-
m.deletedBy = by;
|
|
13559
|
-
updated = m;
|
|
13560
|
-
}
|
|
13561
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
13562
|
-
await fsp11.writeFile(this.messagePath, serialized, "utf8");
|
|
14595
|
+
await fsp11.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
|
|
13563
14596
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
13564
|
-
this.
|
|
14597
|
+
this._applyAckToCache(ack);
|
|
14598
|
+
this._messageCacheMtime = mtimeMs;
|
|
14599
|
+
this._messageCacheSize = size;
|
|
13565
14600
|
});
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
}
|
|
13574
|
-
|
|
14601
|
+
const msg = { ...target, readBy: { ...target.readBy } };
|
|
14602
|
+
msg.deletedAt = now;
|
|
14603
|
+
msg.deletedBy = by;
|
|
14604
|
+
this.publishHqMailboxEvent({
|
|
14605
|
+
mailboxId: this.hqMailboxId,
|
|
14606
|
+
action: "message.updated",
|
|
14607
|
+
message: msg
|
|
14608
|
+
});
|
|
14609
|
+
this.publishHqMailboxSnapshot();
|
|
14610
|
+
this.eventEmitter?.emit({
|
|
14611
|
+
type: "message.deleted",
|
|
14612
|
+
messageId: msg.id,
|
|
14613
|
+
from: msg.from,
|
|
14614
|
+
to: msg.to,
|
|
14615
|
+
timestamp: msg.deletedAt ?? now
|
|
14616
|
+
});
|
|
14617
|
+
return msg;
|
|
13575
14618
|
}
|
|
13576
14619
|
async restore(mailId) {
|
|
13577
|
-
|
|
14620
|
+
const all = await this._readMessagesCached();
|
|
14621
|
+
const target = all.find((m) => m.id === mailId);
|
|
14622
|
+
if (target === void 0) return null;
|
|
14623
|
+
if (target.deletedAt === void 0 && target.deletedBy === void 0) {
|
|
14624
|
+
return { ...target, readBy: { ...target.readBy } };
|
|
14625
|
+
}
|
|
14626
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14627
|
+
const ack = {
|
|
14628
|
+
__ack: true,
|
|
14629
|
+
messageId: mailId,
|
|
14630
|
+
readerId: "",
|
|
14631
|
+
timestamp: now,
|
|
14632
|
+
read: true,
|
|
14633
|
+
deleted: false
|
|
14634
|
+
};
|
|
13578
14635
|
await withFileLock(this.messagePath, async () => {
|
|
13579
|
-
|
|
13580
|
-
for (const m of all) {
|
|
13581
|
-
if (m.id !== mailId) continue;
|
|
13582
|
-
delete m.deletedAt;
|
|
13583
|
-
delete m.deletedBy;
|
|
13584
|
-
updated = m;
|
|
13585
|
-
}
|
|
13586
|
-
const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
13587
|
-
await fsp11.writeFile(this.messagePath, serialized, "utf8");
|
|
14636
|
+
await fsp11.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
|
|
13588
14637
|
const { mtimeMs, size } = await this._statMessageFile();
|
|
13589
|
-
this.
|
|
14638
|
+
this._applyAckToCache(ack);
|
|
14639
|
+
this._messageCacheMtime = mtimeMs;
|
|
14640
|
+
this._messageCacheSize = size;
|
|
14641
|
+
});
|
|
14642
|
+
const msg = { ...target, readBy: { ...target.readBy } };
|
|
14643
|
+
delete msg.deletedAt;
|
|
14644
|
+
delete msg.deletedBy;
|
|
14645
|
+
this.publishHqMailboxEvent({
|
|
14646
|
+
mailboxId: this.hqMailboxId,
|
|
14647
|
+
action: "message.updated",
|
|
14648
|
+
message: msg
|
|
13590
14649
|
});
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
}
|
|
13599
|
-
return
|
|
14650
|
+
this.publishHqMailboxSnapshot();
|
|
14651
|
+
this.eventEmitter?.emit({
|
|
14652
|
+
type: "message.restored",
|
|
14653
|
+
messageId: msg.id,
|
|
14654
|
+
from: msg.from,
|
|
14655
|
+
to: msg.to,
|
|
14656
|
+
timestamp: now
|
|
14657
|
+
});
|
|
14658
|
+
return msg;
|
|
13600
14659
|
}
|
|
13601
14660
|
// ── Agent registry ──────────────────────────────────────────────────────
|
|
13602
14661
|
async registerAgent(input) {
|
|
@@ -13727,8 +14786,19 @@ var GlobalMailbox = class {
|
|
|
13727
14786
|
}
|
|
13728
14787
|
async getAgentStatuses() {
|
|
13729
14788
|
await this._ensureRegistry();
|
|
13730
|
-
|
|
14789
|
+
let registry = await this._readRegistry();
|
|
14790
|
+
const before = registry.size;
|
|
13731
14791
|
this._pruneStaleInPlace(registry);
|
|
14792
|
+
if (registry.size < before) {
|
|
14793
|
+
await withFileLock(this.registryPath, async () => {
|
|
14794
|
+
const fresh = await this._readRegistry({ fresh: true });
|
|
14795
|
+
this._pruneStaleInPlace(fresh);
|
|
14796
|
+
this._registryCache = fresh;
|
|
14797
|
+
this._registryCacheAt = Date.now();
|
|
14798
|
+
await this._writeRegistry(fresh);
|
|
14799
|
+
registry = fresh;
|
|
14800
|
+
});
|
|
14801
|
+
}
|
|
13732
14802
|
const now = Date.now();
|
|
13733
14803
|
return Array.from(registry.values()).map((a) => ({
|
|
13734
14804
|
agentId: a.agentId,
|
|
@@ -13780,6 +14850,20 @@ var GlobalMailbox = class {
|
|
|
13780
14850
|
});
|
|
13781
14851
|
this.publishHqMailboxSnapshot();
|
|
13782
14852
|
}
|
|
14853
|
+
async deregisterClient(clientId) {
|
|
14854
|
+
await this._ensureClientRegistry();
|
|
14855
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
14856
|
+
const registry = await this._readClientRegistry({ fresh: true });
|
|
14857
|
+
this._pruneStaleClientsInPlace(registry);
|
|
14858
|
+
registry.delete(clientId);
|
|
14859
|
+
this._clientRegistryCache = registry;
|
|
14860
|
+
this._clientRegistryCacheAt = Date.now();
|
|
14861
|
+
await this._writeClientRegistry(registry);
|
|
14862
|
+
});
|
|
14863
|
+
this._lastClientHeartbeat.delete(clientId);
|
|
14864
|
+
this._events?.emitCustom("mailbox.client_deregistered", { clientId });
|
|
14865
|
+
this.publishHqMailboxSnapshot();
|
|
14866
|
+
}
|
|
13783
14867
|
async clientHeartbeat(input) {
|
|
13784
14868
|
const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
|
|
13785
14869
|
const now = Date.now();
|
|
@@ -13808,8 +14892,19 @@ var GlobalMailbox = class {
|
|
|
13808
14892
|
}
|
|
13809
14893
|
async getClientStatuses() {
|
|
13810
14894
|
await this._ensureClientRegistry();
|
|
13811
|
-
|
|
14895
|
+
let registry = await this._readClientRegistry();
|
|
14896
|
+
const before = registry.size;
|
|
13812
14897
|
this._pruneStaleClientsInPlace(registry);
|
|
14898
|
+
if (registry.size < before) {
|
|
14899
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
14900
|
+
registry = await this._readClientRegistry({ fresh: true });
|
|
14901
|
+
const postLockBefore = registry.size;
|
|
14902
|
+
this._pruneStaleClientsInPlace(registry);
|
|
14903
|
+
if (registry.size < postLockBefore) {
|
|
14904
|
+
await this._writeClientRegistry(registry);
|
|
14905
|
+
}
|
|
14906
|
+
});
|
|
14907
|
+
}
|
|
13813
14908
|
const now = Date.now();
|
|
13814
14909
|
return Array.from(registry.values()).map((c) => ({
|
|
13815
14910
|
clientId: c.clientId,
|
|
@@ -13821,6 +14916,32 @@ var GlobalMailbox = class {
|
|
|
13821
14916
|
pid: c.pid
|
|
13822
14917
|
})).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
13823
14918
|
}
|
|
14919
|
+
/**
|
|
14920
|
+
* Explicitly purge stale clients from the registry and write back to disk.
|
|
14921
|
+
* Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
|
|
14922
|
+
* Returns the number of entries purged.
|
|
14923
|
+
*
|
|
14924
|
+
* Idempotent — safe to call regularly. The same pruning runs implicitly
|
|
14925
|
+
* inside registerClient, clientHeartbeat, and getClientStatuses, but if
|
|
14926
|
+
* no client registers or heartbeats for a long time, this public method
|
|
14927
|
+
* ensures the file gets cleaned up on demand.
|
|
14928
|
+
*/
|
|
14929
|
+
async purgeClients() {
|
|
14930
|
+
await this._ensureClientRegistry();
|
|
14931
|
+
let purged = 0;
|
|
14932
|
+
await withFileLock(this.clientRegistryPath, async () => {
|
|
14933
|
+
const registry = await this._readClientRegistry({ fresh: true });
|
|
14934
|
+
const before = registry.size;
|
|
14935
|
+
this._pruneStaleClientsInPlace(registry);
|
|
14936
|
+
purged = before - registry.size;
|
|
14937
|
+
if (purged > 0) {
|
|
14938
|
+
this._clientRegistryCache = registry;
|
|
14939
|
+
this._clientRegistryCacheAt = Date.now();
|
|
14940
|
+
await this._writeClientRegistry(registry);
|
|
14941
|
+
}
|
|
14942
|
+
});
|
|
14943
|
+
return purged;
|
|
14944
|
+
}
|
|
13824
14945
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
13825
14946
|
async close() {
|
|
13826
14947
|
this._registryCache = null;
|
|
@@ -13828,6 +14949,8 @@ var GlobalMailbox = class {
|
|
|
13828
14949
|
this._messageCache = null;
|
|
13829
14950
|
this._messageCacheMtime = -1;
|
|
13830
14951
|
this._messageCacheSize = -1;
|
|
14952
|
+
this._recipientIndex = null;
|
|
14953
|
+
this._senderIndex = null;
|
|
13831
14954
|
}
|
|
13832
14955
|
async clearAll() {
|
|
13833
14956
|
await withFileLock(this.messagePath, async () => {
|
|
@@ -13876,6 +14999,100 @@ var GlobalMailbox = class {
|
|
|
13876
14999
|
remaining
|
|
13877
15000
|
};
|
|
13878
15001
|
}
|
|
15002
|
+
// ── Auto-compaction ─────────────────────────────────────────────────────
|
|
15003
|
+
_autoCompactTimer = null;
|
|
15004
|
+
async autoCompact(opts) {
|
|
15005
|
+
const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
|
|
15006
|
+
const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
|
|
15007
|
+
const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
|
|
15008
|
+
const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
|
|
15009
|
+
let expiredPurged = 0;
|
|
15010
|
+
let readByAllPurged = 0;
|
|
15011
|
+
let completedPurged = 0;
|
|
15012
|
+
let incompletePurged = 0;
|
|
15013
|
+
let remaining = 0;
|
|
15014
|
+
let onlineAgentIds = null;
|
|
15015
|
+
try {
|
|
15016
|
+
const statuses = await this.getAgentStatuses();
|
|
15017
|
+
const online = statuses.filter((s) => s.online);
|
|
15018
|
+
onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
|
|
15019
|
+
} catch {
|
|
15020
|
+
onlineAgentIds = null;
|
|
15021
|
+
}
|
|
15022
|
+
await withFileLock(this.messagePath, async () => {
|
|
15023
|
+
const all = await this._readMessagesFresh();
|
|
15024
|
+
const now = Date.now();
|
|
15025
|
+
const cutoffReadAge = now - readMaxAgeMs;
|
|
15026
|
+
const cutoffCompleted = now - completedMaxAgeMs;
|
|
15027
|
+
const cutoffIncomplete = now - incompleteMaxAgeMs;
|
|
15028
|
+
const cutoffDefaultTtl = now - defaultTtlMs;
|
|
15029
|
+
const kept = [];
|
|
15030
|
+
for (const msg of all) {
|
|
15031
|
+
const msgTime = new Date(msg.timestamp).getTime();
|
|
15032
|
+
if (msg.expiresAt !== void 0) {
|
|
15033
|
+
if (new Date(msg.expiresAt).getTime() < now) {
|
|
15034
|
+
expiredPurged++;
|
|
15035
|
+
continue;
|
|
15036
|
+
}
|
|
15037
|
+
} else if (msgTime < cutoffDefaultTtl) {
|
|
15038
|
+
expiredPurged++;
|
|
15039
|
+
continue;
|
|
15040
|
+
}
|
|
15041
|
+
if (onlineAgentIds !== null && !msg.completed) {
|
|
15042
|
+
const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
|
|
15043
|
+
if (readByAll) {
|
|
15044
|
+
const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
|
|
15045
|
+
const latestRead = Math.max(...readTimes);
|
|
15046
|
+
if (latestRead < cutoffReadAge) {
|
|
15047
|
+
readByAllPurged++;
|
|
15048
|
+
continue;
|
|
15049
|
+
}
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
|
|
15053
|
+
if (msg.completed && completedTime < cutoffCompleted) {
|
|
15054
|
+
completedPurged++;
|
|
15055
|
+
continue;
|
|
15056
|
+
}
|
|
15057
|
+
if (!msg.completed && msgTime < cutoffIncomplete) {
|
|
15058
|
+
incompletePurged++;
|
|
15059
|
+
continue;
|
|
15060
|
+
}
|
|
15061
|
+
kept.push(msg);
|
|
15062
|
+
}
|
|
15063
|
+
remaining = kept.length;
|
|
15064
|
+
if (kept.length < all.length) {
|
|
15065
|
+
const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
|
|
15066
|
+
await fsp11.writeFile(this.messagePath, content, "utf8");
|
|
15067
|
+
}
|
|
15068
|
+
const { mtimeMs, size } = await this._statMessageFile();
|
|
15069
|
+
this._setMessageCache(kept, mtimeMs, size);
|
|
15070
|
+
});
|
|
15071
|
+
const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
|
|
15072
|
+
return {
|
|
15073
|
+
expiredRemoved: expiredPurged,
|
|
15074
|
+
readByAllRemoved: readByAllPurged,
|
|
15075
|
+
stalePurged: completedPurged + incompletePurged,
|
|
15076
|
+
totalRemoved,
|
|
15077
|
+
remaining
|
|
15078
|
+
};
|
|
15079
|
+
}
|
|
15080
|
+
startAutoCompactTimer(opts) {
|
|
15081
|
+
if (this._autoCompactTimer !== null) {
|
|
15082
|
+
clearInterval(this._autoCompactTimer);
|
|
15083
|
+
}
|
|
15084
|
+
const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
|
|
15085
|
+
const timer = setInterval(() => {
|
|
15086
|
+
this.autoCompact(opts).catch(() => {
|
|
15087
|
+
});
|
|
15088
|
+
}, intervalMs);
|
|
15089
|
+
timer.unref?.();
|
|
15090
|
+
this._autoCompactTimer = timer;
|
|
15091
|
+
return () => {
|
|
15092
|
+
clearInterval(timer);
|
|
15093
|
+
this._autoCompactTimer = null;
|
|
15094
|
+
};
|
|
15095
|
+
}
|
|
13879
15096
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
13880
15097
|
/**
|
|
13881
15098
|
* Read all messages from the JSONL file. Always reads + parses the file.
|
|
@@ -13892,61 +15109,23 @@ var GlobalMailbox = class {
|
|
|
13892
15109
|
throw err;
|
|
13893
15110
|
}
|
|
13894
15111
|
}
|
|
13895
|
-
/**
|
|
13896
|
-
* Read only newly-appended bytes from the file and append them to the
|
|
13897
|
-
* in-memory cache. This avoids re-reading and re-parsing the entire file
|
|
13898
|
-
* when another process appended messages since our last read.
|
|
13899
|
-
*
|
|
13900
|
-
* Only safe when the file grew in size (messages are append-only). When
|
|
13901
|
-
* the file was rewritten (ack/purge changed existing content), callers
|
|
13902
|
-
* must fall back to {@link _readMessages}.
|
|
13903
|
-
*
|
|
13904
|
-
* @returns The (now up-to-date) message cache.
|
|
13905
|
-
*/
|
|
13906
|
-
async _readNewMessagesOnly(fd, oldSize, newSize) {
|
|
13907
|
-
const tailLen = newSize - oldSize;
|
|
13908
|
-
const buf = Buffer.alloc(tailLen);
|
|
13909
|
-
await fd.read(buf, 0, tailLen, oldSize);
|
|
13910
|
-
const tail = buf.toString("utf8");
|
|
13911
|
-
for (const line of tail.split(LINE_SEPARATOR)) {
|
|
13912
|
-
if (!line.trim()) continue;
|
|
13913
|
-
try {
|
|
13914
|
-
const parsed = JSON.parse(line);
|
|
13915
|
-
if (!parsed["readBy"]) {
|
|
13916
|
-
const readBy = {};
|
|
13917
|
-
if (parsed["read"] && parsed["readAt"]) {
|
|
13918
|
-
readBy[parsed["to"]] = parsed["readAt"];
|
|
13919
|
-
}
|
|
13920
|
-
parsed["readBy"] = readBy;
|
|
13921
|
-
delete parsed["read"];
|
|
13922
|
-
delete parsed["readAt"];
|
|
13923
|
-
}
|
|
13924
|
-
this._messageCache.push(parsed);
|
|
13925
|
-
} catch {
|
|
13926
|
-
}
|
|
13927
|
-
}
|
|
13928
|
-
return this._messageCache;
|
|
13929
|
-
}
|
|
13930
|
-
/** Parse a JSONL string into MailboxMessage[], including migration. */
|
|
15112
|
+
/** Parse a JSONL string into MailboxMessage[], applying ack records. */
|
|
13931
15113
|
_parseLines(raw) {
|
|
13932
15114
|
const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
|
|
13933
15115
|
const messages = [];
|
|
15116
|
+
const acks = [];
|
|
13934
15117
|
for (const line of lines) {
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
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
|
-
}
|
|
15118
|
+
const parsed = parseMailboxLine(line);
|
|
15119
|
+
if (isAckRecord(parsed)) {
|
|
15120
|
+
acks.push(parsed);
|
|
15121
|
+
} else if (parsed !== null) {
|
|
13946
15122
|
messages.push(parsed);
|
|
13947
|
-
} catch {
|
|
13948
15123
|
}
|
|
13949
15124
|
}
|
|
15125
|
+
for (const ack of acks) {
|
|
15126
|
+
const target = messages.find((m) => m.id === ack.messageId);
|
|
15127
|
+
if (target) applyAckToMessage(target, ack);
|
|
15128
|
+
}
|
|
13950
15129
|
return messages;
|
|
13951
15130
|
}
|
|
13952
15131
|
/**
|
|
@@ -14028,17 +15207,6 @@ var GlobalMailbox = class {
|
|
|
14028
15207
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
14029
15208
|
return this._messageCache;
|
|
14030
15209
|
}
|
|
14031
|
-
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
14032
|
-
const fd = await fsp11.open(this.messagePath, "r");
|
|
14033
|
-
try {
|
|
14034
|
-
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
14035
|
-
this._messageCacheMtime = st.mtimeMs;
|
|
14036
|
-
this._messageCacheSize = st.size;
|
|
14037
|
-
return updated;
|
|
14038
|
-
} finally {
|
|
14039
|
-
await fd.close();
|
|
14040
|
-
}
|
|
14041
|
-
}
|
|
14042
15210
|
const all = await this._readMessages();
|
|
14043
15211
|
this._setMessageCache(all, st.mtimeMs, st.size);
|
|
14044
15212
|
return all;
|
|
@@ -14070,11 +15238,61 @@ var GlobalMailbox = class {
|
|
|
14070
15238
|
this._messageCache = null;
|
|
14071
15239
|
this._messageCacheMtime = -1;
|
|
14072
15240
|
this._messageCacheSize = -1;
|
|
15241
|
+
this._recipientIndex = null;
|
|
15242
|
+
this._senderIndex = null;
|
|
14073
15243
|
return;
|
|
14074
15244
|
}
|
|
14075
15245
|
this._messageCache = messages;
|
|
14076
15246
|
this._messageCacheMtime = mtime;
|
|
14077
15247
|
this._messageCacheSize = size;
|
|
15248
|
+
this._rebuildRecipientIndex();
|
|
15249
|
+
}
|
|
15250
|
+
/**
|
|
15251
|
+
* Fold a single ack record into the in-memory cache, mutating the target
|
|
15252
|
+
* message in place. Called from `ackMany` after appending the record to
|
|
15253
|
+
* disk so the cache reflects the ack without a full re-read + re-parse.
|
|
15254
|
+
*
|
|
15255
|
+
* An ack only touches `readBy`/`completed*`/`outcome` — none of which
|
|
15256
|
+
* affect the recipient/sender indexes — so no index rebuild is needed.
|
|
15257
|
+
* No-op when the cache is empty or the target message isn't cached.
|
|
15258
|
+
*/
|
|
15259
|
+
_applyAckToCache(ack) {
|
|
15260
|
+
const cache = this._messageCache;
|
|
15261
|
+
if (cache === null) return;
|
|
15262
|
+
const target = cache.find((m) => m.id === ack.messageId);
|
|
15263
|
+
if (target !== void 0) applyAckToMessage(target, ack);
|
|
15264
|
+
}
|
|
15265
|
+
/**
|
|
15266
|
+
* Build the recipient AND sender indexes from the current `_messageCache`.
|
|
15267
|
+
* Called only from `_setMessageCache` so both indexes are always in sync
|
|
15268
|
+
* with a freshly-replaced cache array.
|
|
15269
|
+
*/
|
|
15270
|
+
_rebuildRecipientIndex() {
|
|
15271
|
+
const cache = this._messageCache;
|
|
15272
|
+
if (cache === null) {
|
|
15273
|
+
this._recipientIndex = null;
|
|
15274
|
+
this._senderIndex = null;
|
|
15275
|
+
return;
|
|
15276
|
+
}
|
|
15277
|
+
const recIdx = /* @__PURE__ */ new Map();
|
|
15278
|
+
const sndIdx = /* @__PURE__ */ new Map();
|
|
15279
|
+
for (let i = 0; i < cache.length; i++) {
|
|
15280
|
+
const msg = cache[i];
|
|
15281
|
+
let rSet = recIdx.get(msg.to);
|
|
15282
|
+
if (rSet === void 0) {
|
|
15283
|
+
rSet = /* @__PURE__ */ new Set();
|
|
15284
|
+
recIdx.set(msg.to, rSet);
|
|
15285
|
+
}
|
|
15286
|
+
rSet.add(i);
|
|
15287
|
+
let sSet = sndIdx.get(msg.from);
|
|
15288
|
+
if (sSet === void 0) {
|
|
15289
|
+
sSet = /* @__PURE__ */ new Set();
|
|
15290
|
+
sndIdx.set(msg.from, sSet);
|
|
15291
|
+
}
|
|
15292
|
+
sSet.add(i);
|
|
15293
|
+
}
|
|
15294
|
+
this._recipientIndex = recIdx;
|
|
15295
|
+
this._senderIndex = sndIdx;
|
|
14078
15296
|
}
|
|
14079
15297
|
/**
|
|
14080
15298
|
* Append a single just-sent message to the in-memory cache without
|
|
@@ -14101,6 +15319,23 @@ var GlobalMailbox = class {
|
|
|
14101
15319
|
return;
|
|
14102
15320
|
}
|
|
14103
15321
|
this._messageCache.push(msg);
|
|
15322
|
+
const newIdx = this._messageCache.length - 1;
|
|
15323
|
+
if (this._recipientIndex !== null) {
|
|
15324
|
+
let set = this._recipientIndex.get(msg.to);
|
|
15325
|
+
if (set === void 0) {
|
|
15326
|
+
set = /* @__PURE__ */ new Set();
|
|
15327
|
+
this._recipientIndex.set(msg.to, set);
|
|
15328
|
+
}
|
|
15329
|
+
set.add(newIdx);
|
|
15330
|
+
}
|
|
15331
|
+
if (this._senderIndex !== null) {
|
|
15332
|
+
let set = this._senderIndex.get(msg.from);
|
|
15333
|
+
if (set === void 0) {
|
|
15334
|
+
set = /* @__PURE__ */ new Set();
|
|
15335
|
+
this._senderIndex.set(msg.from, set);
|
|
15336
|
+
}
|
|
15337
|
+
set.add(newIdx);
|
|
15338
|
+
}
|
|
14104
15339
|
this._messageCacheMtime = mtime;
|
|
14105
15340
|
this._messageCacheSize = size;
|
|
14106
15341
|
}
|
|
@@ -14132,22 +15367,13 @@ var GlobalMailbox = class {
|
|
|
14132
15367
|
}
|
|
14133
15368
|
}
|
|
14134
15369
|
_pruneStaleInPlace(registry) {
|
|
14135
|
-
const
|
|
14136
|
-
const purgeCutoff = Date.now() - AGENT_PURGE_MS;
|
|
14137
|
-
const toDelete = [];
|
|
15370
|
+
const cutoff = Date.now() - AGENT_STALE_MS;
|
|
14138
15371
|
for (const [id, agent] of registry) {
|
|
14139
|
-
const lastSeen =
|
|
14140
|
-
if (lastSeen <
|
|
14141
|
-
|
|
14142
|
-
continue;
|
|
14143
|
-
}
|
|
14144
|
-
if (lastSeen < staleCutoff) {
|
|
14145
|
-
agent.status = "idle";
|
|
15372
|
+
const lastSeen = Date.parse(agent.lastSeenAt);
|
|
15373
|
+
if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
|
|
15374
|
+
registry.delete(id);
|
|
14146
15375
|
}
|
|
14147
15376
|
}
|
|
14148
|
-
for (const id of toDelete) {
|
|
14149
|
-
registry.delete(id);
|
|
14150
|
-
}
|
|
14151
15377
|
}
|
|
14152
15378
|
async _writeRegistry(registry) {
|
|
14153
15379
|
const obj = {};
|
|
@@ -14188,9 +15414,10 @@ var GlobalMailbox = class {
|
|
|
14188
15414
|
}
|
|
14189
15415
|
_pruneStaleClientsInPlace(registry) {
|
|
14190
15416
|
const cutoff = Date.now() - CLIENT_STALE_MS;
|
|
14191
|
-
for (const client of registry
|
|
14192
|
-
|
|
14193
|
-
|
|
15417
|
+
for (const [id, client] of registry) {
|
|
15418
|
+
const lastSeen = Date.parse(client.lastSeenAt);
|
|
15419
|
+
if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
|
|
15420
|
+
registry.delete(id);
|
|
14194
15421
|
}
|
|
14195
15422
|
}
|
|
14196
15423
|
}
|
|
@@ -14248,7 +15475,7 @@ function makeMailboxTool(opts = {}) {
|
|
|
14248
15475
|
description: "Which mailbox operation to perform."
|
|
14249
15476
|
},
|
|
14250
15477
|
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." },
|
|
15478
|
+
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Message type." },
|
|
14252
15479
|
subject: { type: "string", description: "Short subject line." },
|
|
14253
15480
|
body: { type: "string", description: "Full message content." },
|
|
14254
15481
|
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
@@ -14490,7 +15717,7 @@ function makeFleetStatusTool(opts = {}) {
|
|
|
14490
15717
|
const resolveMailbox = opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
14491
15718
|
return {
|
|
14492
15719
|
name: "fleet_status",
|
|
14493
|
-
description: "Live snapshot of every agent working on this project (
|
|
15720
|
+
description: "Live snapshot of every agent working on this canonical project (all clients, processes, sessions, branches, and linked Git worktrees): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. To talk to a peer, use mail_send with the returned id.",
|
|
14494
15721
|
usageHint: "fleet_status (optionally: onlineOnly=false to include recently-offline agents)",
|
|
14495
15722
|
category: "Coordination",
|
|
14496
15723
|
permission: "auto",
|
|
@@ -15154,7 +16381,7 @@ function makeMailSendTool(opts = {}) {
|
|
|
15154
16381
|
const resolveMailbox = makeResolver(opts);
|
|
15155
16382
|
return {
|
|
15156
16383
|
name: "mail_send",
|
|
15157
|
-
description: 'Send
|
|
16384
|
+
description: 'Send mail to any agent on this canonical project, across clients, processes, sessions, branches, and linked Git worktrees (CLI, TUI, WebUI, ACP/MCP/HTTP). Use it to hand off work, ask questions, announce what you just did, or request a review (type="review" \u2014 passive ask, no immediate reply required). to="*" broadcasts to everyone; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Recipients see your mail automatically before their next step. Pick the type that matches the intent: note (default), ask (blocking question), assign (task), steer (mid-task direction), result (completion notice), review (passive ask), btw/status/broadcast/control (informational).',
|
|
15158
16385
|
usageHint: 'mail_send to="<id>" type="review" body="please skim <file>"',
|
|
15159
16386
|
category: "Coordination",
|
|
15160
16387
|
permission: "auto",
|
|
@@ -15214,7 +16441,7 @@ function makeMailInboxTool(opts = {}) {
|
|
|
15214
16441
|
const resolveMailbox = makeResolver(opts);
|
|
15215
16442
|
return {
|
|
15216
16443
|
name: "mail_inbox",
|
|
15217
|
-
description: 'Read your unread mail from
|
|
16444
|
+
description: 'Read your unread project-wide mail from agents in any client, session, branch, or linked Git worktree and mark it read. Covers mail addressed to you directly, to your base name (e.g. "leader"), and broadcasts ("*"). Urgent steer/btw mail is already injected automatically \u2014 use this to catch up on notes, questions, handoffs, results, and review requests (type="review" \u2014 passive asks where no reply is required). Best called after a long stretch of tool work. Set completed=true to finish every returned message in the same call.',
|
|
15218
16445
|
usageHint: 'mail_inbox (optionally: limit=10, markRead=false to peek, completed=true outcome="handled")',
|
|
15219
16446
|
category: "Coordination",
|
|
15220
16447
|
permission: "auto",
|
|
@@ -15302,14 +16529,18 @@ var DefaultMailbox = class {
|
|
|
15302
16529
|
_messageCache = null;
|
|
15303
16530
|
_messageCacheMtime = -1;
|
|
15304
16531
|
_messageCacheSize = -1;
|
|
16532
|
+
/** True when the cache is only the newest MESSAGE_CACHE_MAX_ENTRIES records. */
|
|
16533
|
+
_messageCacheTruncated = false;
|
|
15305
16534
|
/** Primary index: recipient → Set of messages (points into _messageCache). */
|
|
15306
16535
|
_byTo = /* @__PURE__ */ new Map();
|
|
15307
16536
|
/** Secondary index: sender → Set of messages (points into _messageCache). */
|
|
15308
16537
|
_byFrom = /* @__PURE__ */ new Map();
|
|
15309
16538
|
/** Counts malformed JSONL lines skipped during parsing for observability. */
|
|
15310
16539
|
_corruptionCount = 0;
|
|
15311
|
-
|
|
16540
|
+
logger;
|
|
16541
|
+
constructor(sessionDir, opts) {
|
|
15312
16542
|
this.filePath = path15.join(sessionDir, MAILBOX_FILE2);
|
|
16543
|
+
this.logger = opts?.logger;
|
|
15313
16544
|
}
|
|
15314
16545
|
get mailboxPath() {
|
|
15315
16546
|
return this.filePath;
|
|
@@ -15327,7 +16558,7 @@ var DefaultMailbox = class {
|
|
|
15327
16558
|
// "all" is an accepted spelling of the broadcast address — canonical
|
|
15328
16559
|
// form on disk is '*' so every query/checker matches it.
|
|
15329
16560
|
to: normalizeRecipient(input.to),
|
|
15330
|
-
type: input.type,
|
|
16561
|
+
type: normalizeMailboxMessageType(input.type),
|
|
15331
16562
|
subject: input.subject,
|
|
15332
16563
|
body: input.body,
|
|
15333
16564
|
priority: input.priority ?? "normal",
|
|
@@ -15350,12 +16581,14 @@ var DefaultMailbox = class {
|
|
|
15350
16581
|
}
|
|
15351
16582
|
// ── Query ─────────────────────────────────────────────────────────────
|
|
15352
16583
|
async query(q) {
|
|
16584
|
+
const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
|
|
15353
16585
|
const needFullScan = q.unreadBy !== void 0 || q.since !== void 0;
|
|
16586
|
+
const cached = await this._readAllCached(false);
|
|
15354
16587
|
let candidates;
|
|
16588
|
+
let candidatesComplete = !this._messageCacheTruncated || cached !== this._messageCache;
|
|
15355
16589
|
if (needFullScan) {
|
|
15356
|
-
candidates =
|
|
16590
|
+
candidates = cached;
|
|
15357
16591
|
} else {
|
|
15358
|
-
await this._readAllCached();
|
|
15359
16592
|
if (q.to !== void 0) {
|
|
15360
16593
|
const direct = this._byTo.get(q.to);
|
|
15361
16594
|
const broadcast = this._byTo.get("*");
|
|
@@ -15363,10 +16596,12 @@ var DefaultMailbox = class {
|
|
|
15363
16596
|
if (direct) for (const m of direct) combined.set(m.id, m);
|
|
15364
16597
|
if (broadcast) for (const m of broadcast) combined.set(m.id, m);
|
|
15365
16598
|
candidates = Array.from(combined.values());
|
|
16599
|
+
candidatesComplete = !this._messageCacheTruncated;
|
|
15366
16600
|
} else if (q.from !== void 0) {
|
|
15367
16601
|
candidates = Array.from(this._byFrom.get(q.from) ?? []);
|
|
16602
|
+
candidatesComplete = !this._messageCacheTruncated;
|
|
15368
16603
|
} else {
|
|
15369
|
-
candidates =
|
|
16604
|
+
candidates = cached;
|
|
15370
16605
|
}
|
|
15371
16606
|
}
|
|
15372
16607
|
const limit = q.limit ?? 50;
|
|
@@ -15377,27 +16612,37 @@ var DefaultMailbox = class {
|
|
|
15377
16612
|
if (q.from !== void 0 && msg.from !== q.from) return false;
|
|
15378
16613
|
if (q.unreadBy !== void 0 && q.unreadBy in msg.readBy) return false;
|
|
15379
16614
|
if (q.incompleteOnly && msg.completed) return false;
|
|
15380
|
-
if (
|
|
16615
|
+
if (queryType !== void 0 && msg.type !== queryType) return false;
|
|
15381
16616
|
if (order !== null && (order[msg.priority] ?? 1) < minPriorityRank)
|
|
15382
16617
|
return false;
|
|
15383
16618
|
if (q.since !== void 0 && msg.timestamp <= q.since) return false;
|
|
15384
16619
|
return true;
|
|
15385
16620
|
};
|
|
15386
|
-
|
|
16621
|
+
const selectNewest = (source) => {
|
|
15387
16622
|
const out = [];
|
|
15388
|
-
for (let i =
|
|
15389
|
-
const msg =
|
|
16623
|
+
for (let i = source.length - 1; i >= 0 && out.length < limit; i--) {
|
|
16624
|
+
const msg = source[i];
|
|
15390
16625
|
if (passes(msg)) out.push(msg);
|
|
15391
16626
|
}
|
|
15392
16627
|
return out;
|
|
15393
|
-
}
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
}
|
|
15399
|
-
|
|
15400
|
-
|
|
16628
|
+
};
|
|
16629
|
+
if (order === null) {
|
|
16630
|
+
const out = selectNewest(candidates);
|
|
16631
|
+
if (out.length >= limit || candidatesComplete) return out;
|
|
16632
|
+
return selectNewest(await this._readAll());
|
|
16633
|
+
}
|
|
16634
|
+
const selectByPriority = (source) => {
|
|
16635
|
+
const filtered2 = [];
|
|
16636
|
+
for (let i = source.length - 1; i >= 0; i--) {
|
|
16637
|
+
const msg = source[i];
|
|
16638
|
+
if (passes(msg)) filtered2.push(msg);
|
|
16639
|
+
}
|
|
16640
|
+
filtered2.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
16641
|
+
return filtered2.slice(0, limit);
|
|
16642
|
+
};
|
|
16643
|
+
const filtered = selectByPriority(candidates);
|
|
16644
|
+
if (filtered.length >= limit || candidatesComplete) return filtered;
|
|
16645
|
+
return selectByPriority(await this._readAll());
|
|
15401
16646
|
}
|
|
15402
16647
|
// ── Ack ───────────────────────────────────────────────────────────────
|
|
15403
16648
|
async ack(input) {
|
|
@@ -15454,7 +16699,7 @@ var DefaultMailbox = class {
|
|
|
15454
16699
|
for (const m of all) {
|
|
15455
16700
|
if (m.type !== "status") continue;
|
|
15456
16701
|
const existing = latest.get(m.from);
|
|
15457
|
-
if (existing && m.timestamp
|
|
16702
|
+
if (existing && m.timestamp < existing.lastActivityAt) continue;
|
|
15458
16703
|
latest.set(m.from, {
|
|
15459
16704
|
agentId: m.from,
|
|
15460
16705
|
name: m.taskContext?.agentName ?? m.from,
|
|
@@ -15496,6 +16741,7 @@ var DefaultMailbox = class {
|
|
|
15496
16741
|
this._messageCache = null;
|
|
15497
16742
|
this._messageCacheMtime = -1;
|
|
15498
16743
|
this._messageCacheSize = -1;
|
|
16744
|
+
this._messageCacheTruncated = false;
|
|
15499
16745
|
this._byTo.clear();
|
|
15500
16746
|
this._byFrom.clear();
|
|
15501
16747
|
}
|
|
@@ -15547,13 +16793,32 @@ var DefaultMailbox = class {
|
|
|
15547
16793
|
};
|
|
15548
16794
|
}
|
|
15549
16795
|
// ── Client registry stubs (not applicable per-session) ─────────────────
|
|
16796
|
+
async autoCompact(_opts) {
|
|
16797
|
+
const purged = await this.purgeStale(_opts);
|
|
16798
|
+
return {
|
|
16799
|
+
readByAllRemoved: 0,
|
|
16800
|
+
expiredRemoved: 0,
|
|
16801
|
+
stalePurged: purged.totalPurged,
|
|
16802
|
+
totalRemoved: purged.totalPurged,
|
|
16803
|
+
remaining: purged.remaining
|
|
16804
|
+
};
|
|
16805
|
+
}
|
|
16806
|
+
startAutoCompactTimer(_opts) {
|
|
16807
|
+
return () => {
|
|
16808
|
+
};
|
|
16809
|
+
}
|
|
15550
16810
|
async registerClient(_input) {
|
|
15551
16811
|
}
|
|
15552
16812
|
async clientHeartbeat(_input) {
|
|
15553
16813
|
}
|
|
16814
|
+
async deregisterClient(_clientId) {
|
|
16815
|
+
}
|
|
15554
16816
|
async getClientStatuses() {
|
|
15555
16817
|
return [];
|
|
15556
16818
|
}
|
|
16819
|
+
async purgeClients() {
|
|
16820
|
+
return 0;
|
|
16821
|
+
}
|
|
15557
16822
|
// ── Internal ──────────────────────────────────────────────────────────
|
|
15558
16823
|
async _readAll() {
|
|
15559
16824
|
try {
|
|
@@ -15567,60 +16832,62 @@ var DefaultMailbox = class {
|
|
|
15567
16832
|
/**
|
|
15568
16833
|
* Read only newly-appended bytes from the file and append them to the
|
|
15569
16834
|
* in-memory cache, avoiding a full re-read when the file only grew.
|
|
16835
|
+
*
|
|
16836
|
+
* Guards against a half-written tail: when the tail doesn't end with
|
|
16837
|
+
* LINE_SEPARATOR, another process may be mid-append. Parsing a partial
|
|
16838
|
+
* JSON line would throw (caught below), but worse: the NEXT incremental
|
|
16839
|
+
* read would skip that same line because the size tracker already advanced.
|
|
15570
16840
|
*/
|
|
15571
16841
|
async _readNewMessagesOnly(fd, oldSize, newSize) {
|
|
15572
16842
|
const tailLen = newSize - oldSize;
|
|
15573
16843
|
const buf = Buffer.alloc(tailLen);
|
|
15574
16844
|
await fd.read(buf, 0, tailLen, oldSize);
|
|
15575
16845
|
const tail = buf.toString("utf8");
|
|
15576
|
-
|
|
16846
|
+
if (!tail.endsWith(LINE_SEPARATOR2)) {
|
|
16847
|
+
const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR2);
|
|
16848
|
+
if (lastSeparatorIdx === -1) {
|
|
16849
|
+
return this._messageCache;
|
|
16850
|
+
}
|
|
16851
|
+
const completeTail = tail.slice(0, lastSeparatorIdx + 1);
|
|
16852
|
+
this._appendIncrementalLines(completeTail);
|
|
16853
|
+
this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
|
|
16854
|
+
return this._messageCache;
|
|
16855
|
+
}
|
|
16856
|
+
this._appendIncrementalLines(tail);
|
|
16857
|
+
this._messageCacheSize = newSize;
|
|
16858
|
+
return this._messageCache;
|
|
16859
|
+
}
|
|
16860
|
+
_appendIncrementalLines(raw) {
|
|
16861
|
+
for (const line of raw.split(LINE_SEPARATOR2)) {
|
|
15577
16862
|
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}`
|
|
16863
|
+
const msg = this._parseLine(line, "incremental read");
|
|
16864
|
+
if (msg === null) continue;
|
|
16865
|
+
this._pushToCache(msg);
|
|
16866
|
+
}
|
|
16867
|
+
}
|
|
16868
|
+
_parseLine(line, source) {
|
|
16869
|
+
try {
|
|
16870
|
+
return parseMailboxMessageLine(line);
|
|
16871
|
+
} catch (err) {
|
|
16872
|
+
this._corruptionCount++;
|
|
16873
|
+
if (this.logger) {
|
|
16874
|
+
this.logger.debug(
|
|
16875
|
+
"Skipped malformed mailbox line",
|
|
16876
|
+
{ source, error: err.message, corruptionCount: this._corruptionCount }
|
|
15596
16877
|
);
|
|
16878
|
+
} else {
|
|
16879
|
+
console.debug(`[mailbox] skipped malformed line during ${source}: ${err.message}`);
|
|
15597
16880
|
}
|
|
16881
|
+
return null;
|
|
15598
16882
|
}
|
|
15599
|
-
return this._messageCache;
|
|
15600
16883
|
}
|
|
15601
16884
|
/** Parse a JSONL string into MailboxMessage[], including migration. */
|
|
15602
16885
|
_parseLines(raw) {
|
|
15603
16886
|
const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
|
|
15604
16887
|
const messages = [];
|
|
15605
16888
|
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
|
-
}
|
|
16889
|
+
const message = this._parseLine(line, "full parse");
|
|
16890
|
+
if (message !== null) messages.push(message);
|
|
15624
16891
|
}
|
|
15625
16892
|
return messages;
|
|
15626
16893
|
}
|
|
@@ -15641,10 +16908,11 @@ var DefaultMailbox = class {
|
|
|
15641
16908
|
throw err;
|
|
15642
16909
|
}
|
|
15643
16910
|
}
|
|
15644
|
-
async _readAllCached() {
|
|
16911
|
+
async _readAllCached(requireComplete = true) {
|
|
15645
16912
|
try {
|
|
15646
16913
|
const st = await fsp12.stat(this.filePath);
|
|
15647
16914
|
if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
|
|
16915
|
+
if (requireComplete && this._messageCacheTruncated) return this._readAll();
|
|
15648
16916
|
return this._messageCache;
|
|
15649
16917
|
}
|
|
15650
16918
|
if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
|
|
@@ -15652,7 +16920,7 @@ var DefaultMailbox = class {
|
|
|
15652
16920
|
try {
|
|
15653
16921
|
const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
|
|
15654
16922
|
this._messageCacheMtime = st.mtimeMs;
|
|
15655
|
-
this.
|
|
16923
|
+
if (requireComplete && this._messageCacheTruncated) return this._readAll();
|
|
15656
16924
|
return updated;
|
|
15657
16925
|
} finally {
|
|
15658
16926
|
await fd.close();
|
|
@@ -15670,28 +16938,18 @@ var DefaultMailbox = class {
|
|
|
15670
16938
|
}
|
|
15671
16939
|
}
|
|
15672
16940
|
_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);
|
|
16941
|
+
this._messageCacheTruncated = messages.length > MESSAGE_CACHE_MAX_ENTRIES2;
|
|
16942
|
+
this._messageCache = this._messageCacheTruncated ? messages.slice(-MESSAGE_CACHE_MAX_ENTRIES2) : messages;
|
|
16943
|
+
this._buildIndexes(this._messageCache);
|
|
15683
16944
|
this._messageCacheMtime = mtime;
|
|
15684
16945
|
this._messageCacheSize = size;
|
|
15685
16946
|
}
|
|
15686
16947
|
_pushToCache(msg) {
|
|
15687
16948
|
if (this._messageCache === null) return;
|
|
15688
16949
|
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;
|
|
16950
|
+
const evicted = this._messageCache.shift();
|
|
16951
|
+
if (evicted !== void 0) this._unindexMsg(evicted);
|
|
16952
|
+
this._messageCacheTruncated = true;
|
|
15695
16953
|
}
|
|
15696
16954
|
this._messageCache.push(msg);
|
|
15697
16955
|
this._indexMsg(msg);
|
|
@@ -15719,6 +16977,15 @@ var DefaultMailbox = class {
|
|
|
15719
16977
|
this._byFrom.set(msg.from, /* @__PURE__ */ new Set([msg]));
|
|
15720
16978
|
}
|
|
15721
16979
|
}
|
|
16980
|
+
/** Remove an evicted message from both indexes without rebuilding them. */
|
|
16981
|
+
_unindexMsg(msg) {
|
|
16982
|
+
const toSet = this._byTo.get(msg.to);
|
|
16983
|
+
toSet?.delete(msg);
|
|
16984
|
+
if (toSet?.size === 0) this._byTo.delete(msg.to);
|
|
16985
|
+
const fromSet = this._byFrom.get(msg.from);
|
|
16986
|
+
fromSet?.delete(msg);
|
|
16987
|
+
if (fromSet?.size === 0) this._byFrom.delete(msg.from);
|
|
16988
|
+
}
|
|
15722
16989
|
};
|
|
15723
16990
|
|
|
15724
16991
|
// src/coordination/mailbox-actions.ts
|
|
@@ -15989,7 +17256,7 @@ function createMailboxHooks(opts) {
|
|
|
15989
17256
|
var NULL_FLEET_BUS = new FleetBus();
|
|
15990
17257
|
|
|
15991
17258
|
// src/coordination/package-author-tracker.ts
|
|
15992
|
-
import * as
|
|
17259
|
+
import * as fs3 from "node:fs/promises";
|
|
15993
17260
|
import * as path16 from "node:path";
|
|
15994
17261
|
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
15995
17262
|
var LOG_FILENAME = "package-authors.json";
|
|
@@ -15998,7 +17265,7 @@ function logPath(storageDir) {
|
|
|
15998
17265
|
}
|
|
15999
17266
|
async function loadLog(storageDir, projectRoot) {
|
|
16000
17267
|
try {
|
|
16001
|
-
const raw = await
|
|
17268
|
+
const raw = await fs3.readFile(logPath(storageDir), "utf-8");
|
|
16002
17269
|
const parsed = JSON.parse(raw);
|
|
16003
17270
|
if (!parsed.entries || !Array.isArray(parsed.entries)) {
|
|
16004
17271
|
return { projectRoot, entries: [] };
|
|
@@ -16012,13 +17279,13 @@ async function loadLog(storageDir, projectRoot) {
|
|
|
16012
17279
|
}
|
|
16013
17280
|
}
|
|
16014
17281
|
async function saveLog(storageDir, log) {
|
|
16015
|
-
await
|
|
17282
|
+
await fs3.mkdir(storageDir, { recursive: true });
|
|
16016
17283
|
const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
|
|
16017
|
-
await
|
|
16018
|
-
await
|
|
17284
|
+
await fs3.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
17285
|
+
await fs3.rename(tmp, logPath(storageDir));
|
|
16019
17286
|
}
|
|
16020
17287
|
function detectEcosystem(manifestPath) {
|
|
16021
|
-
const name = path16.basename(manifestPath).toLowerCase();
|
|
17288
|
+
const name = path16.win32.basename(manifestPath).toLowerCase();
|
|
16022
17289
|
if (name === "package.json") return "npm";
|
|
16023
17290
|
if (name === "go.mod") return "go";
|
|
16024
17291
|
if (name === "cargo.toml") return "cargo";
|
|
@@ -16381,7 +17648,8 @@ function isProcessAlive(pid) {
|
|
|
16381
17648
|
encoding: "utf-8",
|
|
16382
17649
|
stdio: ["pipe", "pipe", "pipe"]
|
|
16383
17650
|
});
|
|
16384
|
-
|
|
17651
|
+
const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
|
|
17652
|
+
return match !== null && match[1] === String(pid);
|
|
16385
17653
|
} catch {
|
|
16386
17654
|
return false;
|
|
16387
17655
|
}
|
|
@@ -16584,9 +17852,9 @@ var AdaptiveConcurrencyController = class {
|
|
|
16584
17852
|
};
|
|
16585
17853
|
|
|
16586
17854
|
// src/coordination/agent-monitor.ts
|
|
16587
|
-
import * as
|
|
17855
|
+
import * as fs4 from "node:fs/promises";
|
|
16588
17856
|
import * as path18 from "node:path";
|
|
16589
|
-
var AgentMonitorService = class {
|
|
17857
|
+
var AgentMonitorService = class _AgentMonitorService {
|
|
16590
17858
|
_fleetBus;
|
|
16591
17859
|
_events;
|
|
16592
17860
|
_sessionId;
|
|
@@ -16596,6 +17864,15 @@ var AgentMonitorService = class {
|
|
|
16596
17864
|
_onEntry;
|
|
16597
17865
|
/** Per-subagent virtual sessions. */
|
|
16598
17866
|
_sessions = /* @__PURE__ */ new Map();
|
|
17867
|
+
/**
|
|
17868
|
+
* Per-subagent OPEN streaming segment. Provider text/thinking deltas
|
|
17869
|
+
* arrive word-by-word; writing one transcript entry per delta fragments
|
|
17870
|
+
* every surface (ring, JSONL, HQ) into unreadable single-word lines.
|
|
17871
|
+
* Instead, consecutive same-kind/same-iteration deltas append into ONE
|
|
17872
|
+
* entry that is already in the ring (live views see it grow in place);
|
|
17873
|
+
* the JSONL line is written once, when the segment closes.
|
|
17874
|
+
*/
|
|
17875
|
+
_openStreams = /* @__PURE__ */ new Map();
|
|
16599
17876
|
/** Disposers for FleetBus subscriptions, keyed by subagentId. */
|
|
16600
17877
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
16601
17878
|
/** Generic fleet-wide subscription disposer. */
|
|
@@ -16658,6 +17935,9 @@ var AgentMonitorService = class {
|
|
|
16658
17935
|
stop() {
|
|
16659
17936
|
if (!this._started) return;
|
|
16660
17937
|
this._started = false;
|
|
17938
|
+
for (const subagentId of [...this._openStreams.keys()]) {
|
|
17939
|
+
this._closeStream(subagentId);
|
|
17940
|
+
}
|
|
16661
17941
|
if (this._fleetDisposer) {
|
|
16662
17942
|
this._fleetDisposer();
|
|
16663
17943
|
this._fleetDisposer = void 0;
|
|
@@ -16726,6 +18006,7 @@ var AgentMonitorService = class {
|
|
|
16726
18006
|
}
|
|
16727
18007
|
/** Drop a retired subagent from every live in-memory monitor surface. */
|
|
16728
18008
|
removeSubagent(subagentId) {
|
|
18009
|
+
this._closeStream(subagentId);
|
|
16729
18010
|
this._sessions.delete(subagentId);
|
|
16730
18011
|
const dispose = this._subscriptions.get(subagentId);
|
|
16731
18012
|
if (dispose) dispose();
|
|
@@ -16739,31 +18020,13 @@ var AgentMonitorService = class {
|
|
|
16739
18020
|
case "provider.text_delta": {
|
|
16740
18021
|
const text = payload.text;
|
|
16741
18022
|
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
|
-
});
|
|
18023
|
+
this._appendStreamDelta(subagentId, session, "text", text, payload.iteration ?? 0);
|
|
16752
18024
|
break;
|
|
16753
18025
|
}
|
|
16754
18026
|
case "provider.thinking_delta": {
|
|
16755
18027
|
const text = payload.text;
|
|
16756
18028
|
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
|
-
});
|
|
18029
|
+
this._appendStreamDelta(subagentId, session, "thinking", text, payload.iteration ?? 0);
|
|
16767
18030
|
break;
|
|
16768
18031
|
}
|
|
16769
18032
|
case "tool.started": {
|
|
@@ -16820,15 +18083,66 @@ var AgentMonitorService = class {
|
|
|
16820
18083
|
}
|
|
16821
18084
|
}
|
|
16822
18085
|
}
|
|
18086
|
+
/** Max characters accumulated into one streaming segment before rolling over. */
|
|
18087
|
+
static _MAX_SEGMENT_CHARS = 2e4;
|
|
18088
|
+
/**
|
|
18089
|
+
* Fold a provider text/thinking delta into the subagent's open streaming
|
|
18090
|
+
* segment, or open a new one. A segment breaks on kind change, iteration
|
|
18091
|
+
* change, size cap, or any non-delta entry (see `_addEntry`). The entry
|
|
18092
|
+
* is pushed to the ring when the segment OPENS (live views poll the ring
|
|
18093
|
+
* and watch it grow in place); the JSONL line AND the timeline/HQ
|
|
18094
|
+
* announcement happen once, on close, with the COMPLETE segment — so
|
|
18095
|
+
* downstream consumers get one whole message instead of a word-per-event
|
|
18096
|
+
* firehose (or, worse, only the first word).
|
|
18097
|
+
*/
|
|
18098
|
+
_appendStreamDelta(subagentId, session, kind, text, iteration) {
|
|
18099
|
+
const open6 = this._openStreams.get(subagentId);
|
|
18100
|
+
if (open6 && open6.entry.kind === kind && open6.entry.iteration === iteration && open6.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
|
|
18101
|
+
open6.entry.content += text;
|
|
18102
|
+
return;
|
|
18103
|
+
}
|
|
18104
|
+
this._closeStream(subagentId);
|
|
18105
|
+
const entry = {
|
|
18106
|
+
id: this._uid(),
|
|
18107
|
+
subagentId,
|
|
18108
|
+
agentName: session.agentName,
|
|
18109
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18110
|
+
kind,
|
|
18111
|
+
content: text,
|
|
18112
|
+
iteration
|
|
18113
|
+
};
|
|
18114
|
+
this._openStreams.set(subagentId, { entry });
|
|
18115
|
+
session.transcript.push(entry);
|
|
18116
|
+
if (session.transcript.length > this._maxEntries) {
|
|
18117
|
+
session.transcript.splice(0, session.transcript.length - this._maxEntries);
|
|
18118
|
+
}
|
|
18119
|
+
}
|
|
18120
|
+
/**
|
|
18121
|
+
* Close the subagent's open streaming segment: persist it to JSONL and
|
|
18122
|
+
* announce the completed entry on the local bus + HQ callback.
|
|
18123
|
+
*/
|
|
18124
|
+
_closeStream(subagentId) {
|
|
18125
|
+
const open6 = this._openStreams.get(subagentId);
|
|
18126
|
+
if (!open6) return;
|
|
18127
|
+
this._openStreams.delete(subagentId);
|
|
18128
|
+
this._appendToFile(subagentId, open6.entry).catch(() => {
|
|
18129
|
+
});
|
|
18130
|
+
this._emitEntry(open6.entry);
|
|
18131
|
+
}
|
|
16823
18132
|
_addEntry(subagentId, entry) {
|
|
16824
18133
|
const session = this._sessions.get(subagentId);
|
|
16825
18134
|
if (!session) return;
|
|
18135
|
+
this._closeStream(subagentId);
|
|
16826
18136
|
session.transcript.push(entry);
|
|
16827
18137
|
if (session.transcript.length > this._maxEntries) {
|
|
16828
18138
|
session.transcript.splice(0, session.transcript.length - this._maxEntries);
|
|
16829
18139
|
}
|
|
16830
18140
|
this._appendToFile(subagentId, entry).catch(() => {
|
|
16831
18141
|
});
|
|
18142
|
+
this._emitEntry(entry);
|
|
18143
|
+
}
|
|
18144
|
+
/** Announce a transcript entry on the local bus + HQ callback. */
|
|
18145
|
+
_emitEntry(entry) {
|
|
16832
18146
|
this._events.emit("agent.timeline.message", {
|
|
16833
18147
|
sessionId: this._sessionId,
|
|
16834
18148
|
subagentId: entry.subagentId,
|
|
@@ -16844,10 +18158,10 @@ var AgentMonitorService = class {
|
|
|
16844
18158
|
}
|
|
16845
18159
|
async _appendToFile(subagentId, entry) {
|
|
16846
18160
|
const dir = path18.join(this._transcriptsDir, subagentId);
|
|
16847
|
-
await
|
|
18161
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
16848
18162
|
const filePath = path18.join(dir, "transcript.jsonl");
|
|
16849
18163
|
const line = JSON.stringify(entry) + "\n";
|
|
16850
|
-
await
|
|
18164
|
+
await fs4.appendFile(filePath, line, { encoding: "utf8" });
|
|
16851
18165
|
}
|
|
16852
18166
|
_uid() {
|
|
16853
18167
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -16855,8 +18169,11 @@ var AgentMonitorService = class {
|
|
|
16855
18169
|
_formatToolUse(name, input) {
|
|
16856
18170
|
if (input === void 0) return `${name}()`;
|
|
16857
18171
|
const body = this._stringifyForTimeline(input);
|
|
16858
|
-
|
|
16859
|
-
|
|
18172
|
+
if (!body) return `${name}()`;
|
|
18173
|
+
const inline = body.replace(/\s+/g, " ").trim();
|
|
18174
|
+
const head = inline.length <= 160 ? `${name}(${inline})` : `${name}(${inline.slice(0, 159)}\u2026)`;
|
|
18175
|
+
return `${head}
|
|
18176
|
+
${body}`;
|
|
16860
18177
|
}
|
|
16861
18178
|
_formatToolResult(summary, output, outputBytes) {
|
|
16862
18179
|
if (!output) return summary;
|
|
@@ -16893,8 +18210,6 @@ var AutonomousBrain = class {
|
|
|
16893
18210
|
maxRetries;
|
|
16894
18211
|
consensusRiskThreshold;
|
|
16895
18212
|
selfImprove;
|
|
16896
|
-
/** Decision history for self-improvement and audit. */
|
|
16897
|
-
decisionHistory = [];
|
|
16898
18213
|
/** Tracks failure patterns for self-improvement. */
|
|
16899
18214
|
failurePatterns = /* @__PURE__ */ new Map();
|
|
16900
18215
|
RISK_ORDER = ["low", "medium", "high", "critical"];
|
|
@@ -17206,7 +18521,6 @@ ${ctx.error}`);
|
|
|
17206
18521
|
madeAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17207
18522
|
context: input.context
|
|
17208
18523
|
});
|
|
17209
|
-
this.decisionHistory.push(node);
|
|
17210
18524
|
return node;
|
|
17211
18525
|
}
|
|
17212
18526
|
_inferRoles(task) {
|
|
@@ -17235,7 +18549,8 @@ import { randomUUID as randomUUID16 } from "node:crypto";
|
|
|
17235
18549
|
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
17236
18550
|
import * as fsp14 from "node:fs/promises";
|
|
17237
18551
|
import * as path19 from "node:path";
|
|
17238
|
-
var
|
|
18552
|
+
var DEFAULT_MAX_NODES = 2e3;
|
|
18553
|
+
var KnowledgeGraph = class _KnowledgeGraph {
|
|
17239
18554
|
nodes = /* @__PURE__ */ new Map();
|
|
17240
18555
|
index = /* @__PURE__ */ new Map();
|
|
17241
18556
|
// tag/field → node ids
|
|
@@ -17243,6 +18558,7 @@ var KnowledgeGraph = class {
|
|
|
17243
18558
|
pendingDeliveries = /* @__PURE__ */ new Map();
|
|
17244
18559
|
filePath;
|
|
17245
18560
|
graphFilePath;
|
|
18561
|
+
maxNodes;
|
|
17246
18562
|
/**
|
|
17247
18563
|
* Stable per-node insertion sequence. `nodes` (a Map) preserves insertion
|
|
17248
18564
|
* order even across `update()` (Map.set on an existing key keeps its slot),
|
|
@@ -17261,9 +18577,10 @@ var KnowledgeGraph = class {
|
|
|
17261
18577
|
getIndex() {
|
|
17262
18578
|
return this.index;
|
|
17263
18579
|
}
|
|
17264
|
-
constructor(sessionDir) {
|
|
18580
|
+
constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES) {
|
|
17265
18581
|
this.filePath = path19.join(sessionDir, "_knowledge_graph");
|
|
17266
18582
|
this.graphFilePath = path19.join(this.filePath, "graph.jsonl");
|
|
18583
|
+
this.maxNodes = maxNodes;
|
|
17267
18584
|
}
|
|
17268
18585
|
// ── Write ──────────────────────────────────────────────────────────────
|
|
17269
18586
|
/**
|
|
@@ -17275,6 +18592,7 @@ var KnowledgeGraph = class {
|
|
|
17275
18592
|
this.nodes.set(full.id, full);
|
|
17276
18593
|
this._trackSeq(full.id);
|
|
17277
18594
|
this._addToIndex(full, this._indexKeys(full));
|
|
18595
|
+
this._prune();
|
|
17278
18596
|
await this._persist(full);
|
|
17279
18597
|
this._deliver(full);
|
|
17280
18598
|
return full;
|
|
@@ -17287,10 +18605,48 @@ var KnowledgeGraph = class {
|
|
|
17287
18605
|
const updated = { ...existing, ...patch };
|
|
17288
18606
|
this.nodes.set(id, updated);
|
|
17289
18607
|
this._addToIndex(updated, this._indexKeys(updated));
|
|
18608
|
+
this._prune();
|
|
17290
18609
|
this._deliver(updated);
|
|
17291
18610
|
await this._append(updated);
|
|
17292
18611
|
return updated;
|
|
17293
18612
|
}
|
|
18613
|
+
/**
|
|
18614
|
+
* True when the node is in a terminal / settled state — no future mutations
|
|
18615
|
+
* are expected. Such nodes are the first to be evicted when the in-memory
|
|
18616
|
+
* cap is reached. The JSONL file on disk retains the full history.
|
|
18617
|
+
*/
|
|
18618
|
+
static _isTerminal(node) {
|
|
18619
|
+
if (node.type === "goal") return node.status === "done" || node.status === "failed";
|
|
18620
|
+
if (node.type === "change") return node.status === "rejected" || node.status === "rolled_back";
|
|
18621
|
+
if (node.type === "vote") return true;
|
|
18622
|
+
return false;
|
|
18623
|
+
}
|
|
18624
|
+
/**
|
|
18625
|
+
* Evict oldest terminal-state nodes when the in-memory cap is exceeded.
|
|
18626
|
+
* The JSONL file is the authoritative record; eviction only affects memory.
|
|
18627
|
+
*/
|
|
18628
|
+
_prune() {
|
|
18629
|
+
if (this.nodes.size <= this.maxNodes) return;
|
|
18630
|
+
const toEvict = this.nodes.size - this.maxNodes;
|
|
18631
|
+
const terminal = [];
|
|
18632
|
+
for (const [id, node] of this.nodes) {
|
|
18633
|
+
if (_KnowledgeGraph._isTerminal(node)) terminal.push({ id, seq: this.seq.get(id) ?? 0 });
|
|
18634
|
+
}
|
|
18635
|
+
terminal.sort((a, b) => a.seq - b.seq);
|
|
18636
|
+
const evicted = new Set(terminal.slice(0, toEvict).map((e) => e.id));
|
|
18637
|
+
if (evicted.size === 0) return;
|
|
18638
|
+
for (const id of evicted) {
|
|
18639
|
+
const node = this.nodes.get(id);
|
|
18640
|
+
if (!node) continue;
|
|
18641
|
+
this._removeFromIndex(node, this._indexKeys(node));
|
|
18642
|
+
this.nodes.delete(id);
|
|
18643
|
+
this.seq.delete(id);
|
|
18644
|
+
for (const pending of this.pendingDeliveries.values()) {
|
|
18645
|
+
const idx = pending.findIndex((n) => n.id === id);
|
|
18646
|
+
if (idx >= 0) pending.splice(idx, 1);
|
|
18647
|
+
}
|
|
18648
|
+
}
|
|
18649
|
+
}
|
|
17294
18650
|
// ── Read ───────────────────────────────────────────────────────────────
|
|
17295
18651
|
get(id) {
|
|
17296
18652
|
return this.nodes.get(id);
|
|
@@ -18723,6 +20079,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
18723
20079
|
mailbox;
|
|
18724
20080
|
events;
|
|
18725
20081
|
onCoordinatorEvent;
|
|
20082
|
+
logger;
|
|
18726
20083
|
running = false;
|
|
18727
20084
|
iterationCount = 0;
|
|
18728
20085
|
lastSyncAt = 0;
|
|
@@ -18739,6 +20096,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
18739
20096
|
this.mailbox = opts.mailbox ?? void 0;
|
|
18740
20097
|
this.events = opts.events ?? void 0;
|
|
18741
20098
|
this.onCoordinatorEvent = opts.onCoordinatorEvent;
|
|
20099
|
+
this.logger = opts.logger;
|
|
18742
20100
|
this.graph = new KnowledgeGraph(opts.sessionDir);
|
|
18743
20101
|
this.dag = new TaskDAG();
|
|
18744
20102
|
this.auction = new TaskAuctioneer({
|
|
@@ -18897,15 +20255,22 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
18897
20255
|
stop() {
|
|
18898
20256
|
if (!this.running) return;
|
|
18899
20257
|
this.running = false;
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
event: "autonomous_coordinator.stop_signal",
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
20258
|
+
if (this.logger) {
|
|
20259
|
+
this.logger.error(
|
|
20260
|
+
"Stop signal received \u2014 shutting down",
|
|
20261
|
+
{ event: "autonomous_coordinator.stop_signal", iteration: this.iterationCount }
|
|
20262
|
+
);
|
|
20263
|
+
} else {
|
|
20264
|
+
console.error(
|
|
20265
|
+
JSON.stringify({
|
|
20266
|
+
level: "error",
|
|
20267
|
+
event: "autonomous_coordinator.stop_signal",
|
|
20268
|
+
message: "stop signal received \u2014 shutting down",
|
|
20269
|
+
iteration: this.iterationCount,
|
|
20270
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20271
|
+
})
|
|
20272
|
+
);
|
|
20273
|
+
}
|
|
18909
20274
|
}
|
|
18910
20275
|
/**
|
|
18911
20276
|
* Report that a terminal worker (not a Director subagent) completed a claimed
|
|
@@ -19353,6 +20718,7 @@ export {
|
|
|
19353
20718
|
AutonomousCoordinator,
|
|
19354
20719
|
BUG_HUNTER_AGENT,
|
|
19355
20720
|
BUILD_AGENTS,
|
|
20721
|
+
BrainDecisionLedger,
|
|
19356
20722
|
BrainDecisionQueue,
|
|
19357
20723
|
BrainMonitor,
|
|
19358
20724
|
BudgetExceededError,
|
|
@@ -19375,6 +20741,7 @@ export {
|
|
|
19375
20741
|
DefaultMultiAgentCoordinator,
|
|
19376
20742
|
Director,
|
|
19377
20743
|
DirectorAlertLevel,
|
|
20744
|
+
EscalationRoutingBrainArbiter,
|
|
19378
20745
|
FLEET_ROSTER,
|
|
19379
20746
|
FLEET_ROSTER_BUDGETS,
|
|
19380
20747
|
FLEET_ROSTER_WITHACP,
|
|
@@ -19424,12 +20791,14 @@ export {
|
|
|
19424
20791
|
applyRosterBudget,
|
|
19425
20792
|
attachAutoExtend,
|
|
19426
20793
|
attachDepWatcherBridge,
|
|
20794
|
+
brainDecisionKey,
|
|
19427
20795
|
buildDownAlert,
|
|
19428
20796
|
buildRecoveryAlert,
|
|
19429
20797
|
composeDirectorPrompt,
|
|
19430
20798
|
composeSubagentPrompt,
|
|
19431
20799
|
createAgentMonitorService,
|
|
19432
20800
|
createDelegateTool,
|
|
20801
|
+
createLedgerGuardBrainArbiter,
|
|
19433
20802
|
createMailboxHooks,
|
|
19434
20803
|
createMessage,
|
|
19435
20804
|
detectEcosystem,
|
|
@@ -19493,6 +20862,7 @@ export {
|
|
|
19493
20862
|
scoreAgents,
|
|
19494
20863
|
startPackageOutdatedWatcher,
|
|
19495
20864
|
subagentNeedsWorktree,
|
|
20865
|
+
terminalPolicyDecision,
|
|
19496
20866
|
updatePackageOutdatedStatus,
|
|
19497
20867
|
validateWatchdogOptions,
|
|
19498
20868
|
withDisabledToolFiltering,
|