@wrongstack/core 0.287.0 → 0.291.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-status-tracker.d.ts +13 -0
- package/dist/agent-status-tracker.d.ts.map +1 -1
- package/dist/boot.d.ts.map +1 -1
- package/dist/chronicle/context.d.ts +13 -0
- package/dist/chronicle/context.d.ts.map +1 -0
- package/dist/chronicle/decision-adapter.d.ts +13 -0
- package/dist/chronicle/decision-adapter.d.ts.map +1 -0
- package/dist/chronicle/domain-adapter.d.ts +13 -0
- package/dist/chronicle/domain-adapter.d.ts.map +1 -0
- package/dist/chronicle/file-observer.d.ts +20 -0
- package/dist/chronicle/file-observer.d.ts.map +1 -0
- package/dist/chronicle/health-monitor.d.ts +11 -0
- package/dist/chronicle/health-monitor.d.ts.map +1 -0
- package/dist/chronicle/identity.d.ts +18 -0
- package/dist/chronicle/identity.d.ts.map +1 -0
- package/dist/chronicle/index.d.ts +16 -0
- package/dist/chronicle/index.d.ts.map +1 -0
- package/dist/chronicle/index.js +2555 -0
- package/dist/chronicle/index.js.map +7 -0
- package/dist/chronicle/journal.d.ts +81 -0
- package/dist/chronicle/journal.d.ts.map +1 -0
- package/dist/chronicle/process-adapter.d.ts +14 -0
- package/dist/chronicle/process-adapter.d.ts.map +1 -0
- package/dist/chronicle/prompt-manifest.d.ts +34 -0
- package/dist/chronicle/prompt-manifest.d.ts.map +1 -0
- package/dist/chronicle/provider-adapter.d.ts +13 -0
- package/dist/chronicle/provider-adapter.d.ts.map +1 -0
- package/dist/chronicle/query.d.ts +108 -0
- package/dist/chronicle/query.d.ts.map +1 -0
- package/dist/chronicle/rollup-adapter.d.ts +14 -0
- package/dist/chronicle/rollup-adapter.d.ts.map +1 -0
- package/dist/chronicle/stream-adapter.d.ts +13 -0
- package/dist/chronicle/stream-adapter.d.ts.map +1 -0
- package/dist/chronicle/tool-adapter.d.ts +15 -0
- package/dist/chronicle/tool-adapter.d.ts.map +1 -0
- package/dist/chronicle/types.d.ts +82 -0
- package/dist/chronicle/types.d.ts.map +1 -0
- package/dist/coordination/agent-monitor.d.ts +40 -0
- package/dist/coordination/agent-monitor.d.ts.map +1 -1
- package/dist/coordination/agent-subagent-runner.d.ts +7 -1
- package/dist/coordination/agent-subagent-runner.d.ts.map +1 -1
- package/dist/coordination/agents/phase6-domain.d.ts.map +1 -1
- package/dist/coordination/autonomous-coordinator.d.ts +2 -0
- package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
- package/dist/coordination/brain-monitor.d.ts +1 -1
- package/dist/coordination/brain.d.ts +3 -2
- package/dist/coordination/brain.d.ts.map +1 -1
- package/dist/coordination/collab-debug.d.ts +0 -9
- package/dist/coordination/collab-debug.d.ts.map +1 -1
- package/dist/coordination/coordinator/error-classifier.d.ts.map +1 -1
- package/dist/coordination/dep-watcher.d.ts +2 -1
- package/dist/coordination/dep-watcher.d.ts.map +1 -1
- package/dist/coordination/director/director-collab.d.ts +1 -1
- package/dist/coordination/director/director-collab.d.ts.map +1 -1
- package/dist/coordination/director-tools.d.ts.map +1 -1
- package/dist/coordination/director.d.ts +10 -1
- package/dist/coordination/director.d.ts.map +1 -1
- package/dist/coordination/file-author-tracker.d.ts.map +1 -1
- package/dist/coordination/fleet-bus.d.ts.map +1 -1
- package/dist/coordination/fleet-event-validation.d.ts +0 -7
- package/dist/coordination/fleet-event-validation.d.ts.map +1 -1
- package/dist/coordination/fleet-spawn.d.ts.map +1 -1
- package/dist/coordination/fleet-supervisor.d.ts +1 -1
- package/dist/coordination/fleet-supervisor.d.ts.map +1 -1
- package/dist/coordination/fleet.d.ts +0 -44
- package/dist/coordination/fleet.d.ts.map +1 -1
- package/dist/coordination/global-mailbox.d.ts +7 -1
- package/dist/coordination/global-mailbox.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +5 -3
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +2426 -299
- package/dist/coordination/index.js.map +4 -4
- package/dist/coordination/knowledge-graph.d.ts +2 -1
- package/dist/coordination/knowledge-graph.d.ts.map +1 -1
- package/dist/coordination/large-answer-store.d.ts +0 -6
- package/dist/coordination/large-answer-store.d.ts.map +1 -1
- package/dist/coordination/mail-tools.d.ts +2 -2
- package/dist/coordination/mail-tools.d.ts.map +1 -1
- package/dist/coordination/mailbox-hooks.d.ts +2 -0
- package/dist/coordination/mailbox-hooks.d.ts.map +1 -1
- package/dist/coordination/mailbox-http-router.d.ts +43 -0
- package/dist/coordination/mailbox-http-router.d.ts.map +1 -0
- package/dist/coordination/mailbox-message-codec.d.ts +35 -0
- package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
- package/dist/coordination/mailbox-tool.d.ts +1 -1
- package/dist/coordination/mailbox-tool.d.ts.map +1 -1
- package/dist/coordination/mailbox-types.d.ts +170 -10
- package/dist/coordination/mailbox-types.d.ts.map +1 -1
- package/dist/coordination/mailbox.d.ts +1 -1
- package/dist/coordination/mailbox.d.ts.map +1 -1
- package/dist/coordination/package-author-tracker.d.ts.map +1 -1
- package/dist/coordination/provider-status-tracker.d.ts +236 -0
- package/dist/coordination/provider-status-tracker.d.ts.map +1 -0
- package/dist/coordination/subagent-budget.d.ts +2 -1
- package/dist/coordination/subagent-budget.d.ts.map +1 -1
- package/dist/core/agent-loop.d.ts +8 -3
- package/dist/core/agent-loop.d.ts.map +1 -1
- package/dist/core/agent-response.d.ts +5 -4
- package/dist/core/agent-response.d.ts.map +1 -1
- package/dist/core/agent-tools.d.ts +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent.d.ts +4 -2
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/context.d.ts +55 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/fallback-model.d.ts +14 -8
- package/dist/core/fallback-model.d.ts.map +1 -1
- package/dist/core/fallback-profile-manager.d.ts +118 -0
- package/dist/core/fallback-profile-manager.d.ts.map +1 -0
- package/dist/core/fleet-pulse.d.ts +2 -1
- package/dist/core/fleet-pulse.d.ts.map +1 -1
- package/dist/core/iteration-limit.d.ts +2 -1
- package/dist/core/iteration-limit.d.ts.map +1 -1
- package/dist/core/mailbox-loop.d.ts +16 -1
- package/dist/core/mailbox-loop.d.ts.map +1 -1
- package/dist/core/model-availability-calendar.d.ts +27 -0
- package/dist/core/model-availability-calendar.d.ts.map +1 -0
- package/dist/core/provider-runner.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +2 -1
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/defaults/index.js +1920 -598
- package/dist/defaults/index.js.map +4 -4
- package/dist/execution/auto-compaction-middleware.d.ts +3 -2
- package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
- package/dist/execution/compaction-core.d.ts.map +1 -1
- package/dist/execution/council-brain.d.ts +28 -49
- package/dist/execution/council-brain.d.ts.map +1 -1
- package/dist/execution/council-orchestrator.d.ts +30 -0
- package/dist/execution/council-orchestrator.d.ts.map +1 -1
- package/dist/execution/enhance-recovery.d.ts +8 -0
- package/dist/execution/enhance-recovery.d.ts.map +1 -1
- package/dist/execution/error-handler.d.ts +2 -2
- package/dist/execution/error-handler.d.ts.map +1 -1
- package/dist/execution/eternal-autonomy.d.ts.map +1 -1
- package/dist/execution/index.js +2600 -1903
- package/dist/execution/index.js.map +4 -4
- package/dist/execution/intelligent-compactor.d.ts +0 -16
- package/dist/execution/intelligent-compactor.d.ts.map +1 -1
- package/dist/execution/model-runtime.d.ts.map +1 -1
- package/dist/execution/one-shot-llm.d.ts +9 -1
- package/dist/execution/one-shot-llm.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +36 -0
- package/dist/execution/prompt-enhancer.d.ts.map +1 -1
- package/dist/execution/prompt-enhancer.js +158 -10
- package/dist/execution/prompt-enhancer.js.map +2 -2
- package/dist/execution/retry-policy.d.ts +0 -13
- package/dist/execution/retry-policy.d.ts.map +1 -1
- package/dist/execution/skill-loader.d.ts.map +1 -1
- package/dist/execution/tool-executor.d.ts.map +1 -1
- package/dist/goal/checkpoint.d.ts.map +1 -0
- package/dist/{autophase/auto-phase-planner.d.ts → goal/goal-planner.d.ts} +8 -8
- package/dist/goal/goal-planner.d.ts.map +1 -0
- package/dist/{autophase/auto-phase-runner.d.ts → goal/goal-runner.d.ts} +10 -10
- package/dist/goal/goal-runner.d.ts.map +1 -0
- package/dist/{autophase → goal}/index.d.ts +3 -3
- package/dist/goal/index.d.ts.map +1 -0
- package/dist/goal/phase-graph-builder.d.ts.map +1 -0
- package/dist/{autophase → goal}/phase-orchestrator.d.ts +2 -2
- package/dist/goal/phase-orchestrator.d.ts.map +1 -0
- package/dist/goal/phase-store.d.ts.map +1 -0
- package/dist/{autophase → goal}/types.d.ts +2 -2
- package/dist/goal/types.d.ts.map +1 -0
- package/dist/hq/auth-store.d.ts.map +1 -1
- package/dist/hq/commands.d.ts +2 -1
- package/dist/hq/commands.d.ts.map +1 -1
- package/dist/hq/exposure.d.ts +66 -0
- package/dist/hq/exposure.d.ts.map +1 -0
- package/dist/hq/index.d.ts +1 -0
- package/dist/hq/index.d.ts.map +1 -1
- package/dist/hq/index.js +190 -21
- package/dist/hq/index.js.map +4 -4
- package/dist/hq/mailbox-mapper.d.ts +13 -1
- package/dist/hq/mailbox-mapper.d.ts.map +1 -1
- package/dist/hq/protocol/browser.d.ts +9 -1
- package/dist/hq/protocol/browser.d.ts.map +1 -1
- package/dist/hq/protocol/client.d.ts +2 -2
- package/dist/hq/protocol/client.d.ts.map +1 -1
- package/dist/hq/protocol/core.d.ts.map +1 -1
- package/dist/hq/protocol/mailbox.d.ts +6 -0
- package/dist/hq/protocol/mailbox.d.ts.map +1 -1
- package/dist/hq/protocol/session.d.ts +4 -0
- package/dist/hq/protocol/session.d.ts.map +1 -1
- package/dist/hq/publisher.d.ts.map +1 -1
- package/dist/index.d.ts +34 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30287 -22214
- package/dist/index.js.map +4 -4
- package/dist/infrastructure/index.js +60 -5
- package/dist/infrastructure/index.js.map +2 -2
- package/dist/kernel/events/agent-events.d.ts +33 -1
- package/dist/kernel/events/agent-events.d.ts.map +1 -1
- package/dist/kernel/events/file-events.d.ts +65 -0
- package/dist/kernel/events/file-events.d.ts.map +1 -0
- package/dist/kernel/events/memory-events.d.ts +36 -0
- package/dist/kernel/events/memory-events.d.ts.map +1 -1
- package/dist/kernel/events/network-events.d.ts +60 -0
- package/dist/kernel/events/network-events.d.ts.map +1 -0
- package/dist/kernel/events/process-events.d.ts +44 -0
- package/dist/kernel/events/process-events.d.ts.map +1 -0
- package/dist/kernel/events/provider-events.d.ts +89 -0
- package/dist/kernel/events/provider-events.d.ts.map +1 -1
- package/dist/kernel/events/tool-events.d.ts +10 -0
- package/dist/kernel/events/tool-events.d.ts.map +1 -1
- package/dist/kernel/events/worktree-events.d.ts +1 -1
- package/dist/kernel/events.d.ts +14 -1
- package/dist/kernel/events.d.ts.map +1 -1
- package/dist/kernel/index.js +2 -1
- package/dist/kernel/index.js.map +2 -2
- package/dist/kernel/tokens.d.ts +3 -1
- package/dist/kernel/tokens.d.ts.map +1 -1
- package/dist/mailbox-attach.d.ts.map +1 -1
- package/dist/models/index.js.map +2 -2
- package/dist/observability/index.d.ts +3 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +278 -0
- package/dist/observability/index.js.map +4 -4
- package/dist/observability/network-telemetry.d.ts +15 -0
- package/dist/observability/network-telemetry.d.ts.map +1 -0
- package/dist/observability/process-telemetry.d.ts +18 -0
- package/dist/observability/process-telemetry.d.ts.map +1 -0
- package/dist/observability/redact-command.d.ts +20 -0
- package/dist/observability/redact-command.d.ts.map +1 -0
- package/dist/plugins/auto-review-plugin.d.ts +56 -3
- package/dist/plugins/auto-review-plugin.d.ts.map +1 -1
- package/dist/plugins/chimera-plugin.d.ts +164 -5
- package/dist/plugins/chimera-plugin.d.ts.map +1 -1
- package/dist/plugins/review-context-builder.d.ts +52 -0
- package/dist/plugins/review-context-builder.d.ts.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +186 -9
- package/dist/security/index.js.map +4 -4
- package/dist/security/kanban-boundary.d.ts +10 -0
- package/dist/security/kanban-boundary.d.ts.map +1 -0
- package/dist/security/permission-policy.d.ts +1 -0
- package/dist/security/permission-policy.d.ts.map +1 -1
- package/dist/session-registry.d.ts +80 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/skills/index.js.map +2 -2
- package/dist/storage/config-loader.d.ts +7 -0
- package/dist/storage/config-loader.d.ts.map +1 -1
- package/dist/storage/file-session-writer.d.ts +20 -7
- package/dist/storage/file-session-writer.d.ts.map +1 -1
- package/dist/storage/goal-coordination.d.ts +40 -0
- package/dist/storage/goal-coordination.d.ts.map +1 -0
- package/dist/storage/goal-store.d.ts +9 -5
- package/dist/storage/goal-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1066 -226
- package/dist/storage/index.js.map +4 -4
- package/dist/storage/memory-consolidator.d.ts +49 -4
- package/dist/storage/memory-consolidator.d.ts.map +1 -1
- package/dist/storage/memory-graph-backend.d.ts +11 -0
- package/dist/storage/memory-graph-backend.d.ts.map +1 -1
- package/dist/storage/provider-config-watcher.d.ts +19 -0
- package/dist/storage/provider-config-watcher.d.ts.map +1 -1
- package/dist/storage/session-event-bridge.d.ts +2 -2
- package/dist/storage/session-event-bridge.d.ts.map +1 -1
- package/dist/storage/session-resume-validation.d.ts +10 -0
- package/dist/storage/session-resume-validation.d.ts.map +1 -1
- package/dist/storage/session-rewind-apply.d.ts +53 -0
- package/dist/storage/session-rewind-apply.d.ts.map +1 -0
- package/dist/storage/session-rewinder.d.ts.map +1 -1
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/tasking/index.js.map +1 -1
- package/dist/tasking/task-store.d.ts +1 -1
- package/dist/tools/council-tool.d.ts +3 -0
- package/dist/tools/council-tool.d.ts.map +1 -1
- package/dist/tools/fallback-manage-tools.d.ts +82 -0
- package/dist/tools/fallback-manage-tools.d.ts.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3423 -192
- package/dist/tools/index.js.map +4 -4
- package/dist/tools/one-shot-llm-tool.d.ts +2 -0
- package/dist/tools/one-shot-llm-tool.d.ts.map +1 -1
- package/dist/types/config.d.ts +76 -7
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/file-event-record.d.ts +68 -0
- package/dist/types/file-event-record.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -8
- package/dist/types/index.js.map +3 -3
- package/dist/types/memory.d.ts +12 -0
- package/dist/types/memory.d.ts.map +1 -1
- package/dist/types/multi-agent.d.ts +12 -1
- package/dist/types/multi-agent.d.ts.map +1 -1
- package/dist/types/one-shot-llm.d.ts +16 -7
- package/dist/types/one-shot-llm.d.ts.map +1 -1
- package/dist/types/provider.d.ts +1 -1
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/session.d.ts +68 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/tool-executor.d.ts +2 -0
- package/dist/types/tool-executor.d.ts.map +1 -1
- package/dist/types/tool.d.ts +5 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/index.d.ts +10 -10
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +497 -254
- package/dist/utils/index.js.map +4 -4
- package/dist/utils/message-invariants.d.ts +9 -0
- package/dist/utils/message-invariants.d.ts.map +1 -1
- package/dist/utils/safe-json.d.ts +1 -1
- package/dist/utils/safe-json.d.ts.map +1 -1
- package/dist/utils/term.d.ts +279 -0
- package/dist/utils/term.d.ts.map +1 -1
- package/dist/utils/tool-wire-compact.d.ts +9 -0
- package/dist/utils/tool-wire-compact.d.ts.map +1 -1
- package/dist/utils/wstack-paths.d.ts +12 -2
- package/dist/utils/wstack-paths.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.d.ts +1 -1
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/instructions/agents/ios.md +113 -0
- package/instructions/agents/search.md +1 -1
- package/instructions/llm/chimera-review.md +66 -9
- package/instructions/llm/memory-consolidator.md +4 -12
- package/instructions/llm/prompt-enhancer.md +3 -1
- package/instructions/sections/tool/common-patterns.md +3 -3
- package/instructions/system.md +40 -5
- package/package.json +7 -3
- package/skills/auto-review/SKILL.md +176 -0
- package/skills/mnemosyne/SKILL.md +156 -0
- package/skills/mnemosyne/instructions/agent-prompt.md +200 -0
- package/dist/autophase/auto-phase-planner.d.ts.map +0 -1
- package/dist/autophase/auto-phase-runner.d.ts.map +0 -1
- package/dist/autophase/checkpoint.d.ts.map +0 -1
- package/dist/autophase/index.d.ts.map +0 -1
- package/dist/autophase/phase-graph-builder.d.ts.map +0 -1
- package/dist/autophase/phase-orchestrator.d.ts.map +0 -1
- package/dist/autophase/phase-store.d.ts.map +0 -1
- package/dist/autophase/types.d.ts.map +0 -1
- /package/dist/{autophase → goal}/checkpoint.d.ts +0 -0
- /package/dist/{autophase → goal}/phase-graph-builder.d.ts +0 -0
- /package/dist/{autophase → goal}/phase-store.d.ts +0 -0
- /package/instructions/{autophase → goal}/phase-planner.md +0 -0
|
@@ -57,7 +57,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
57
57
|
const dir = path.dirname(targetPath);
|
|
58
58
|
await fs.mkdir(dir, { recursive: true });
|
|
59
59
|
const lockPath = path.join(dir, `.${path.basename(targetPath)}.lock`);
|
|
60
|
-
const timeoutMs = opts.timeoutMs ??
|
|
60
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
61
61
|
const staleMs = opts.staleMs ?? 3e4;
|
|
62
62
|
const started = Date.now();
|
|
63
63
|
let handle;
|
|
@@ -67,6 +67,13 @@ async function withFileLock(targetPath, fn, opts = {}) {
|
|
|
67
67
|
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
68
68
|
break;
|
|
69
69
|
} catch (err) {
|
|
70
|
+
if (handle) {
|
|
71
|
+
await handle.close().catch(() => {
|
|
72
|
+
});
|
|
73
|
+
await fs.unlink(lockPath).catch(() => {
|
|
74
|
+
});
|
|
75
|
+
handle = void 0;
|
|
76
|
+
}
|
|
70
77
|
const code = err.code;
|
|
71
78
|
if (code === "ENOENT") {
|
|
72
79
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -449,6 +456,16 @@ async function expandGlob(pattern) {
|
|
|
449
456
|
}
|
|
450
457
|
|
|
451
458
|
// src/utils/message-invariants.ts
|
|
459
|
+
function hasMeaningfulContent(content) {
|
|
460
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
461
|
+
return content.some((block) => {
|
|
462
|
+
if (block.type === "text") return block.text.trim().length > 0;
|
|
463
|
+
if (block.type === "thinking") {
|
|
464
|
+
return block.thinking.trim().length > 0 || Boolean(block.signature);
|
|
465
|
+
}
|
|
466
|
+
return true;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
452
469
|
function repairToolUseAdjacency(messages) {
|
|
453
470
|
const removedToolUses = [];
|
|
454
471
|
const removedToolResults = [];
|
|
@@ -536,13 +553,12 @@ function mapContent(msg, fn) {
|
|
|
536
553
|
return { ...msg, content: next };
|
|
537
554
|
}
|
|
538
555
|
function isEmptyMessage(msg) {
|
|
539
|
-
|
|
540
|
-
return msg.content.length === 0;
|
|
556
|
+
return !hasMeaningfulContent(msg.content);
|
|
541
557
|
}
|
|
542
558
|
|
|
543
559
|
// src/utils/safe-json.ts
|
|
544
560
|
function safeParse(input, maxBytes = 5e6) {
|
|
545
|
-
if (input
|
|
561
|
+
if (Buffer.byteLength(input, "utf8") > maxBytes) {
|
|
546
562
|
return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
|
|
547
563
|
}
|
|
548
564
|
try {
|
|
@@ -598,6 +614,11 @@ function invalid(sessionId) {
|
|
|
598
614
|
});
|
|
599
615
|
}
|
|
600
616
|
|
|
617
|
+
// src/utils/string.ts
|
|
618
|
+
function truncate(s, max) {
|
|
619
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
620
|
+
}
|
|
621
|
+
|
|
601
622
|
// src/utils/ulid.ts
|
|
602
623
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
603
624
|
var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
@@ -626,11 +647,6 @@ function ulid(seedTime = Date.now()) {
|
|
|
626
647
|
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN);
|
|
627
648
|
}
|
|
628
649
|
|
|
629
|
-
// src/utils/string.ts
|
|
630
|
-
function truncate(s, max) {
|
|
631
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
650
|
// src/utils/wstack-paths.ts
|
|
635
651
|
import { createHash } from "node:crypto";
|
|
636
652
|
import * as fs2 from "node:fs";
|
|
@@ -1617,8 +1633,8 @@ function stringArray(value) {
|
|
|
1617
1633
|
|
|
1618
1634
|
// src/coordination/agent-subagent-runner.ts
|
|
1619
1635
|
function withDisabledToolFiltering(factory) {
|
|
1620
|
-
return async (config) => {
|
|
1621
|
-
const result = await factory(config);
|
|
1636
|
+
return async (config, task) => {
|
|
1637
|
+
const result = await factory(config, task);
|
|
1622
1638
|
const disabled = config.disabledTools ?? [];
|
|
1623
1639
|
if (disabled.length === 0) return result;
|
|
1624
1640
|
const registry = result.agent.tools;
|
|
@@ -1634,10 +1650,36 @@ function makeAgentSubagentRunner(opts) {
|
|
|
1634
1650
|
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
1635
1651
|
return async (task, ctx) => {
|
|
1636
1652
|
const taskStartedAt = Date.now();
|
|
1637
|
-
const factoryResult = await opts.factory(ctx.config);
|
|
1653
|
+
const factoryResult = await opts.factory(ctx.config, task);
|
|
1638
1654
|
const { agent, events } = factoryResult;
|
|
1655
|
+
const taskContext = task.context ?? {};
|
|
1656
|
+
const hasExplicitTelemetryTask = typeof taskContext["telemetryTaskId"] === "string";
|
|
1657
|
+
const telemetryTaskId = hasExplicitTelemetryTask ? taskContext["telemetryTaskId"] : task.id;
|
|
1658
|
+
const telemetryRunId = typeof taskContext["telemetryRunId"] === "string" ? taskContext["telemetryRunId"] : void 0;
|
|
1659
|
+
const telemetryBoardId = typeof taskContext["telemetryBoardId"] === "string" ? taskContext["telemetryBoardId"] : void 0;
|
|
1639
1660
|
const agentContext = agent.ctx;
|
|
1640
|
-
if (agentContext?.meta)
|
|
1661
|
+
if (agentContext?.meta) {
|
|
1662
|
+
agentContext.meta["subagentTaskId"] = task.id;
|
|
1663
|
+
const kanbanContext = taskContext["kanban"];
|
|
1664
|
+
if (kanbanContext && typeof kanbanContext === "object") {
|
|
1665
|
+
agentContext.meta["kanban"] = kanbanContext;
|
|
1666
|
+
}
|
|
1667
|
+
if (hasExplicitTelemetryTask) {
|
|
1668
|
+
agentContext.meta["telemetryTaskId"] = telemetryTaskId;
|
|
1669
|
+
if (telemetryRunId) agentContext.meta["telemetryRunId"] = telemetryRunId;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
if (hasExplicitTelemetryTask) {
|
|
1673
|
+
agentContext?.setCurrentKanbanTask?.(telemetryTaskId, telemetryBoardId);
|
|
1674
|
+
} else {
|
|
1675
|
+
const kanbanContext = taskContext["kanban"];
|
|
1676
|
+
if (kanbanContext && typeof kanbanContext === "object") {
|
|
1677
|
+
const kanban = kanbanContext;
|
|
1678
|
+
const boardId = typeof kanban["boardId"] === "string" ? kanban["boardId"] : void 0;
|
|
1679
|
+
const taskId = typeof kanban["taskId"] === "string" ? kanban["taskId"] : void 0;
|
|
1680
|
+
if (taskId) agentContext?.setCurrentKanbanTask?.(taskId, boardId);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1641
1683
|
const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
|
|
1642
1684
|
const aborter = new AbortController();
|
|
1643
1685
|
ctx.budget._events = events;
|
|
@@ -1662,6 +1704,27 @@ function makeAgentSubagentRunner(opts) {
|
|
|
1662
1704
|
const unsub = [];
|
|
1663
1705
|
unsub.push(
|
|
1664
1706
|
events.on("tool.executed", (e) => {
|
|
1707
|
+
if (hasExplicitTelemetryTask) {
|
|
1708
|
+
opts.hostEvents?.emit("subagent.tool_executed", {
|
|
1709
|
+
sessionId: e.sessionId,
|
|
1710
|
+
subagentId: ctx.subagentId,
|
|
1711
|
+
agentSessionId: e.sessionId,
|
|
1712
|
+
agentName: e.agentName ?? ctx.config.name,
|
|
1713
|
+
taskId: telemetryTaskId,
|
|
1714
|
+
runId: telemetryRunId,
|
|
1715
|
+
traceId: e.traceId,
|
|
1716
|
+
id: e.id,
|
|
1717
|
+
name: e.name,
|
|
1718
|
+
durationMs: e.durationMs,
|
|
1719
|
+
ok: e.ok,
|
|
1720
|
+
// Raw tool inputs may contain credentials or sensitive arguments.
|
|
1721
|
+
// Task telemetry crosses from the isolated worker bus to the shared
|
|
1722
|
+
// host bus, so expose only execution metadata at this boundary.
|
|
1723
|
+
outputBytes: e.outputBytes,
|
|
1724
|
+
outputTokens: e.outputTokens,
|
|
1725
|
+
outputLines: e.outputLines
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1665
1728
|
try {
|
|
1666
1729
|
ctx.budget.recordToolCall();
|
|
1667
1730
|
} catch (eb) {
|
|
@@ -1673,6 +1736,17 @@ function makeAgentSubagentRunner(opts) {
|
|
|
1673
1736
|
lastToolFailed = null;
|
|
1674
1737
|
}
|
|
1675
1738
|
}),
|
|
1739
|
+
events.on("file.event", (e) => {
|
|
1740
|
+
if (!hasExplicitTelemetryTask) return;
|
|
1741
|
+
if (e.runId === telemetryRunId && e.taskId === telemetryTaskId) return;
|
|
1742
|
+
opts.hostEvents?.emit("file.event", {
|
|
1743
|
+
...e,
|
|
1744
|
+
scope: "task",
|
|
1745
|
+
taskId: telemetryTaskId,
|
|
1746
|
+
boardId: telemetryBoardId,
|
|
1747
|
+
runId: telemetryRunId
|
|
1748
|
+
});
|
|
1749
|
+
}),
|
|
1676
1750
|
events.on("provider.response", (e) => {
|
|
1677
1751
|
try {
|
|
1678
1752
|
ctx.budget.recordUsage(e.usage);
|
|
@@ -2966,6 +3040,40 @@ var DOMAIN_AGENTS = [
|
|
|
2966
3040
|
"information architecture"
|
|
2967
3041
|
]
|
|
2968
3042
|
}
|
|
3043
|
+
},
|
|
3044
|
+
{
|
|
3045
|
+
config: {
|
|
3046
|
+
id: "ios",
|
|
3047
|
+
name: "iOS",
|
|
3048
|
+
role: "ios",
|
|
3049
|
+
tools: [...TOOLS.build, "fetch"],
|
|
3050
|
+
prompt: agentPrompt("ios")
|
|
3051
|
+
},
|
|
3052
|
+
budget: HEAVY_BUDGET,
|
|
3053
|
+
capability: {
|
|
3054
|
+
phase: "domain",
|
|
3055
|
+
summary: "Apple-platform app development in Swift against the latest Xcode/iOS-SDK line: SwiftUI, UIKit, SwiftData, concurrency, accessibility, and App Store submission.",
|
|
3056
|
+
keywords: [
|
|
3057
|
+
"ios",
|
|
3058
|
+
"iphone",
|
|
3059
|
+
"ipad",
|
|
3060
|
+
"ipados",
|
|
3061
|
+
"watchos",
|
|
3062
|
+
"tvos",
|
|
3063
|
+
"visionos",
|
|
3064
|
+
"macos",
|
|
3065
|
+
"swift",
|
|
3066
|
+
"swiftui",
|
|
3067
|
+
"uikit",
|
|
3068
|
+
"xcode",
|
|
3069
|
+
"swiftdata",
|
|
3070
|
+
"app store",
|
|
3071
|
+
"app intents",
|
|
3072
|
+
"swift package manager",
|
|
3073
|
+
"cocoapods",
|
|
3074
|
+
"foundation models"
|
|
3075
|
+
]
|
|
3076
|
+
}
|
|
2969
3077
|
}
|
|
2970
3078
|
];
|
|
2971
3079
|
|
|
@@ -4544,12 +4652,12 @@ var CollabSession = class extends EventEmitter {
|
|
|
4544
4652
|
for (const obj of this.extractJsonObjects(text)) {
|
|
4545
4653
|
const type = "finding" in obj ? "bug.found" : "plan" in obj ? "refactor.plan" : "evaluation" in obj ? "critic.evaluation" : null;
|
|
4546
4654
|
if (!type) continue;
|
|
4547
|
-
const
|
|
4655
|
+
const validationError2 = validateFleetEventEmission(
|
|
4548
4656
|
type,
|
|
4549
4657
|
obj,
|
|
4550
4658
|
this.roleFromSubagentId(result.subagentId) ?? void 0
|
|
4551
4659
|
);
|
|
4552
|
-
if (
|
|
4660
|
+
if (validationError2) continue;
|
|
4553
4661
|
this.fleetBus.emit({
|
|
4554
4662
|
subagentId: result.subagentId,
|
|
4555
4663
|
taskId: result.taskId,
|
|
@@ -4700,7 +4808,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
|
|
|
4700
4808
|
level: "warning" /* WARNING */,
|
|
4701
4809
|
message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
|
|
4702
4810
|
budgetKind: payload.kind,
|
|
4703
|
-
|
|
4811
|
+
// `used` is elapsed milliseconds for timeout kinds. `timeoutMs` is the
|
|
4812
|
+
// negotiation response deadline (normally 60s), not agent runtime.
|
|
4813
|
+
elapsedMs: payload.kind === "timeout" || payload.kind === "idle_timeout" ? payload.used : void 0,
|
|
4704
4814
|
limit: payload.limit,
|
|
4705
4815
|
btwNotes
|
|
4706
4816
|
};
|
|
@@ -4724,12 +4834,10 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
|
|
|
4724
4834
|
return;
|
|
4725
4835
|
}
|
|
4726
4836
|
this.lastTimeoutProgress.set(e.subagentId, progress);
|
|
4727
|
-
const newLimit = Math.min(
|
|
4728
|
-
Math.ceil((payload.timeoutMs ?? payload.limit) * 2),
|
|
4729
|
-
24 * 60 * 6e4
|
|
4730
|
-
);
|
|
4837
|
+
const newLimit = Math.min(Math.ceil(payload.limit * 2), 24 * 60 * 6e4);
|
|
4731
4838
|
setImmediate(() => {
|
|
4732
|
-
payload.
|
|
4839
|
+
const field = payload.kind === "timeout" ? "timeoutMs" : "idleTimeoutMs";
|
|
4840
|
+
payload.extend({ [field]: newLimit });
|
|
4733
4841
|
});
|
|
4734
4842
|
return;
|
|
4735
4843
|
}
|
|
@@ -4989,7 +5097,11 @@ var FLEET_ROSTER_BUDGETS = {
|
|
|
4989
5097
|
idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
|
|
4990
5098
|
maxIterations: 1e3,
|
|
4991
5099
|
maxToolCalls: 2500,
|
|
4992
|
-
|
|
5100
|
+
// Monitoring passes routinely include a large system prompt plus fleet
|
|
5101
|
+
// context. 30k was below observed single-pass usage and therefore triggered
|
|
5102
|
+
// a post-response negotiation every run; 96k leaves headroom while the cost
|
|
5103
|
+
// and tool/iteration caps still bound runaway work.
|
|
5104
|
+
maxTokens: 96e3,
|
|
4993
5105
|
maxCostUsd: 0.5
|
|
4994
5106
|
},
|
|
4995
5107
|
...Object.fromEntries(
|
|
@@ -6207,6 +6319,8 @@ function rosterSummaryFromConfigs(roster) {
|
|
|
6207
6319
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
6208
6320
|
import {
|
|
6209
6321
|
claimReadyTask,
|
|
6322
|
+
describeKanbanBoundary,
|
|
6323
|
+
heartbeatTaskAssignment,
|
|
6210
6324
|
listReadyTasks,
|
|
6211
6325
|
updateTaskAssignment
|
|
6212
6326
|
} from "@wrongstack/kanban";
|
|
@@ -6961,13 +7075,19 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
6961
7075
|
const projectRoot = ctx.projectRoot;
|
|
6962
7076
|
if (!projectRoot) return { error: "kanban_queue requires ctx.projectRoot." };
|
|
6963
7077
|
const maxTasks = Math.max(1, Math.min(20, Math.floor(i.maxTasks ?? 1)));
|
|
6964
|
-
const
|
|
7078
|
+
const baseLeaseTtlMs = Math.max(1e3, Math.floor(i.leaseTtlMs ?? 5 * 60 * 1e3));
|
|
7079
|
+
const leaseTtlMs = i.awaitCompletion === true && i.timeoutMs !== void 0 ? Math.max(baseLeaseTtlMs, i.timeoutMs + 6e4) : baseLeaseTtlMs;
|
|
7080
|
+
const heartbeatIntervalMs = Math.max(
|
|
7081
|
+
5e3,
|
|
7082
|
+
i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
|
|
7083
|
+
);
|
|
7084
|
+
const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
|
|
6965
7085
|
const claimedAt = nowIso();
|
|
6966
7086
|
const leaseSeeding = {
|
|
6967
7087
|
leaseId: randomUUID5(),
|
|
6968
7088
|
claimedAt,
|
|
6969
7089
|
heartbeatAt: claimedAt,
|
|
6970
|
-
leaseExpiresAt: new Date(Date.now() +
|
|
7090
|
+
leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
|
|
6971
7091
|
};
|
|
6972
7092
|
const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await listReadyTasks(projectRoot, {
|
|
6973
7093
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {}
|
|
@@ -6975,9 +7095,11 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
6975
7095
|
const dispatches = [];
|
|
6976
7096
|
const errors = [];
|
|
6977
7097
|
const resultFailures = [];
|
|
7098
|
+
const budgetRejectedTaskIds = /* @__PURE__ */ new Set();
|
|
6978
7099
|
for (let index = 0; index < maxTasks; index++) {
|
|
6979
7100
|
const candidateTaskId = candidateTaskIds?.[index];
|
|
6980
7101
|
if (candidateTaskIds && !candidateTaskId) break;
|
|
7102
|
+
if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
|
|
6981
7103
|
const claim = await claimReadyTask(projectRoot, {
|
|
6982
7104
|
...i.boardId !== void 0 ? { boardId: i.boardId } : {},
|
|
6983
7105
|
...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
|
|
@@ -7002,14 +7124,22 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
7002
7124
|
if (costCeiling !== void 0) {
|
|
7003
7125
|
const remaining = director.getRemainingBudgetUsd();
|
|
7004
7126
|
if (remaining !== void 0 && remaining < costCeiling) {
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7127
|
+
budgetRejectedTaskIds.add(claim.task.id);
|
|
7128
|
+
const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
|
|
7129
|
+
await updateTaskAssignment(
|
|
7130
|
+
projectRoot,
|
|
7131
|
+
claim.board.id,
|
|
7132
|
+
claim.task.id,
|
|
7133
|
+
{
|
|
7134
|
+
...claim.task.assignment ?? {},
|
|
7135
|
+
status: "failed",
|
|
7136
|
+
error: budgetError
|
|
7137
|
+
},
|
|
7138
|
+
{ expectedLeaseId: leaseSeeding.leaseId }
|
|
7139
|
+
);
|
|
7010
7140
|
errors.push({
|
|
7011
7141
|
taskId: claim.task.id,
|
|
7012
|
-
error:
|
|
7142
|
+
error: budgetError
|
|
7013
7143
|
});
|
|
7014
7144
|
continue;
|
|
7015
7145
|
}
|
|
@@ -7017,31 +7147,75 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
7017
7147
|
try {
|
|
7018
7148
|
const config = buildKanbanSubagentConfig(claim.task, i, roster);
|
|
7019
7149
|
subagentId = await director.spawn(config);
|
|
7020
|
-
|
|
7021
|
-
|
|
7150
|
+
const dispatchTaskId = randomUUID5();
|
|
7151
|
+
const taskSpec = {
|
|
7152
|
+
id: dispatchTaskId,
|
|
7022
7153
|
subagentId,
|
|
7023
7154
|
description: buildKanbanFleetTaskPrompt(claim.board, claim.task, {
|
|
7024
|
-
|
|
7025
|
-
|
|
7155
|
+
// The worker must heartbeat against the lease it actually holds.
|
|
7156
|
+
// effectiveLeaseTtlMs is the real lease window (raised from the
|
|
7157
|
+
// requested TTL when needed so the first heartbeat always lands
|
|
7158
|
+
// before expiry), and the computed heartbeatIntervalMs is always
|
|
7159
|
+
// strictly shorter than that window. Passing the raw requested
|
|
7160
|
+
// values here would give the worker a 60s cadence against a
|
|
7161
|
+
// 10s lease, letting the lease expire before its first renewal.
|
|
7162
|
+
heartbeatIntervalMs,
|
|
7163
|
+
leaseTtlMs: effectiveLeaseTtlMs,
|
|
7026
7164
|
leaseId: leaseSeeding.leaseId,
|
|
7027
7165
|
leaseExpiresAt: leaseSeeding.leaseExpiresAt
|
|
7028
7166
|
}),
|
|
7029
7167
|
...i.maxToolCalls !== void 0 ? { maxToolCalls: i.maxToolCalls } : {},
|
|
7030
7168
|
...i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : {},
|
|
7031
7169
|
context: {
|
|
7170
|
+
...claim.task.type !== void 0 ? { taskType: claim.task.type } : {},
|
|
7032
7171
|
kanban: {
|
|
7033
7172
|
boardId: claim.board.id,
|
|
7034
7173
|
taskId: claim.task.id,
|
|
7035
|
-
origin: claim.task.origin
|
|
7174
|
+
origin: claim.task.origin,
|
|
7175
|
+
projectRoot
|
|
7036
7176
|
}
|
|
7037
7177
|
}
|
|
7038
|
-
}
|
|
7039
|
-
await updateTaskAssignment(
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7178
|
+
};
|
|
7179
|
+
const dispatched = await updateTaskAssignment(
|
|
7180
|
+
projectRoot,
|
|
7181
|
+
claim.board.id,
|
|
7182
|
+
claim.task.id,
|
|
7183
|
+
{
|
|
7184
|
+
...claim.task.assignment ?? {},
|
|
7185
|
+
status: "running",
|
|
7186
|
+
subagentId,
|
|
7187
|
+
runTaskId: dispatchTaskId,
|
|
7188
|
+
// Renew the lease from the actual dispatch moment, not from claim
|
|
7189
|
+
// time. The claim-time lease starts before spawn + queueing, so
|
|
7190
|
+
// `timeoutMs + 60_000` can expire while the worker is still
|
|
7191
|
+
// queued or running, letting recover_stale reclaim and duplicate
|
|
7192
|
+
// the assignment (see auto-extend.ts: timeout extensions can push
|
|
7193
|
+
// execution well past the original timeoutMs). Both heartbeatAt
|
|
7194
|
+
// and leaseExpiresAt are stamped from now so the worker has a
|
|
7195
|
+
// full lease window from the moment it was actually dispatched.
|
|
7196
|
+
heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7197
|
+
leaseExpiresAt: new Date(Date.now() + effectiveLeaseTtlMs).toISOString()
|
|
7198
|
+
},
|
|
7199
|
+
{ expectedLeaseId: leaseSeeding.leaseId }
|
|
7200
|
+
);
|
|
7201
|
+
if (dispatched === null) {
|
|
7202
|
+
try {
|
|
7203
|
+
await director.terminate(subagentId);
|
|
7204
|
+
} catch (cleanupErr) {
|
|
7205
|
+
errors.push({
|
|
7206
|
+
taskId: claim.task.id,
|
|
7207
|
+
error: `Lease lost during dispatch; orphan subagent ${subagentId} termination failed: ${toErrorMessage(cleanupErr)}`
|
|
7208
|
+
});
|
|
7209
|
+
continue;
|
|
7210
|
+
}
|
|
7211
|
+
errors.push({
|
|
7212
|
+
taskId: claim.task.id,
|
|
7213
|
+
error: `Lease lost during dispatch (reassigned by recovery); subagent ${subagentId} terminated.`
|
|
7214
|
+
});
|
|
7215
|
+
continue;
|
|
7216
|
+
}
|
|
7217
|
+
await director.assign(taskSpec);
|
|
7218
|
+
runTaskId = dispatchTaskId;
|
|
7045
7219
|
dispatches.push({
|
|
7046
7220
|
boardId: claim.board.id,
|
|
7047
7221
|
taskId: claim.task.id,
|
|
@@ -7060,28 +7234,97 @@ function makeKanbanQueueTool(director, roster) {
|
|
|
7060
7234
|
message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
|
|
7061
7235
|
}
|
|
7062
7236
|
}
|
|
7063
|
-
await updateTaskAssignment(
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7237
|
+
await updateTaskAssignment(
|
|
7238
|
+
projectRoot,
|
|
7239
|
+
claim.board.id,
|
|
7240
|
+
claim.task.id,
|
|
7241
|
+
{
|
|
7242
|
+
...claim.task.assignment ?? {},
|
|
7243
|
+
status: "failed",
|
|
7244
|
+
...subagentId !== void 0 ? { subagentId } : {},
|
|
7245
|
+
...runTaskId !== void 0 ? { runTaskId } : {},
|
|
7246
|
+
error: message
|
|
7247
|
+
},
|
|
7248
|
+
{ expectedLeaseId: leaseSeeding.leaseId }
|
|
7249
|
+
);
|
|
7070
7250
|
errors.push({ taskId: claim.task.id, error: message });
|
|
7071
7251
|
}
|
|
7072
7252
|
}
|
|
7253
|
+
if (!i.awaitCompletion && dispatches.length > 0 && typeof director.fleet?.subscribe === "function") {
|
|
7254
|
+
const ourLeaseId = leaseSeeding.leaseId;
|
|
7255
|
+
const disposers = /* @__PURE__ */ new Map();
|
|
7256
|
+
const renewalStartedAt = Date.now();
|
|
7257
|
+
const maxRenewalWindowMs = 24 * 60 * 6e4;
|
|
7258
|
+
const stopAll = () => {
|
|
7259
|
+
clearInterval(renewalTimer);
|
|
7260
|
+
for (const dispose of disposers.values()) dispose();
|
|
7261
|
+
disposers.clear();
|
|
7262
|
+
};
|
|
7263
|
+
const renewalTimer = setInterval(() => {
|
|
7264
|
+
if (disposers.size === 0 || Date.now() - renewalStartedAt > maxRenewalWindowMs) {
|
|
7265
|
+
stopAll();
|
|
7266
|
+
return;
|
|
7267
|
+
}
|
|
7268
|
+
const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
|
|
7269
|
+
for (const dispatch of dispatches) {
|
|
7270
|
+
if (!disposers.has(dispatch.runTaskId)) continue;
|
|
7271
|
+
void heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
|
|
7272
|
+
leaseExpiresAt: refreshedExpiry,
|
|
7273
|
+
expectedLeaseId: ourLeaseId
|
|
7274
|
+
}).catch(() => {
|
|
7275
|
+
});
|
|
7276
|
+
}
|
|
7277
|
+
}, heartbeatIntervalMs);
|
|
7278
|
+
renewalTimer.unref?.();
|
|
7279
|
+
for (const dispatch of dispatches) {
|
|
7280
|
+
const dispose = director.fleet.subscribe(dispatch.subagentId, (event) => {
|
|
7281
|
+
if (event.type !== "subagent.completed" && event.type !== "subagent.stopped") return;
|
|
7282
|
+
if (event.type === "subagent.completed" && event.taskId !== dispatch.runTaskId) return;
|
|
7283
|
+
disposers.get(dispatch.runTaskId)?.();
|
|
7284
|
+
disposers.delete(dispatch.runTaskId);
|
|
7285
|
+
if (disposers.size === 0) clearInterval(renewalTimer);
|
|
7286
|
+
});
|
|
7287
|
+
disposers.set(dispatch.runTaskId, dispose);
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7073
7290
|
let results;
|
|
7074
7291
|
if (i.awaitCompletion && dispatches.length > 0) {
|
|
7075
|
-
|
|
7292
|
+
const ourLeaseId = leaseSeeding.leaseId;
|
|
7293
|
+
const runTaskIds = new Set(dispatches.map((dispatch) => dispatch.runTaskId));
|
|
7294
|
+
const heartbeatTimer = setInterval(async () => {
|
|
7295
|
+
const refreshedExpiry = new Date(Date.now() + effectiveLeaseTtlMs).toISOString();
|
|
7296
|
+
for (const dispatch of dispatches) {
|
|
7297
|
+
if (!runTaskIds.has(dispatch.runTaskId)) continue;
|
|
7298
|
+
try {
|
|
7299
|
+
await heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
|
|
7300
|
+
leaseExpiresAt: refreshedExpiry,
|
|
7301
|
+
expectedLeaseId: ourLeaseId
|
|
7302
|
+
});
|
|
7303
|
+
} catch {
|
|
7304
|
+
}
|
|
7305
|
+
}
|
|
7306
|
+
}, heartbeatIntervalMs);
|
|
7307
|
+
try {
|
|
7308
|
+
results = await director.awaitTasks(dispatches.map((dispatch) => dispatch.runTaskId));
|
|
7309
|
+
} finally {
|
|
7310
|
+
clearInterval(heartbeatTimer);
|
|
7311
|
+
}
|
|
7076
7312
|
for (const result of results) {
|
|
7077
7313
|
const dispatch = dispatches.find((candidate) => candidate.runTaskId === result.taskId);
|
|
7078
7314
|
if (!dispatch) continue;
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7315
|
+
runTaskIds.delete(dispatch.runTaskId);
|
|
7316
|
+
await updateTaskAssignment(
|
|
7317
|
+
projectRoot,
|
|
7318
|
+
dispatch.boardId,
|
|
7319
|
+
dispatch.taskId,
|
|
7320
|
+
{
|
|
7321
|
+
status: result.status === "success" ? "completed" : "failed",
|
|
7322
|
+
subagentId: result.subagentId,
|
|
7323
|
+
runTaskId: result.taskId,
|
|
7324
|
+
...result.status === "success" ? { lastResult: resultToText(result) } : { error: resultErrorText(result) }
|
|
7325
|
+
},
|
|
7326
|
+
{ expectedLeaseId: ourLeaseId }
|
|
7327
|
+
);
|
|
7085
7328
|
if (result.status !== "success") {
|
|
7086
7329
|
resultFailures.push({
|
|
7087
7330
|
taskId: dispatch.taskId,
|
|
@@ -7189,6 +7432,16 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
|
|
|
7189
7432
|
task.origin.phaseId ? `phaseId: ${task.origin.phaseId}` : "",
|
|
7190
7433
|
task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
|
|
7191
7434
|
].filter(Boolean).join("\n") : "";
|
|
7435
|
+
const boundaries = [
|
|
7436
|
+
board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
|
|
7437
|
+
task.boundary?.enabled ? `task: ${describeKanbanBoundary(task.boundary)}` : "",
|
|
7438
|
+
...(board.boundary?.allow ?? []).map(
|
|
7439
|
+
(selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
|
|
7440
|
+
),
|
|
7441
|
+
...(task.boundary?.allow ?? []).map(
|
|
7442
|
+
(selector) => `task allow ${selector.access} ${selector.kind}:${selector.path}`
|
|
7443
|
+
)
|
|
7444
|
+
].filter(Boolean);
|
|
7192
7445
|
return [
|
|
7193
7446
|
"You are processing a claimed WrongStack Kanban task.",
|
|
7194
7447
|
"",
|
|
@@ -7211,6 +7464,8 @@ ${checks}` : "",
|
|
|
7211
7464
|
metrics ? `Goal metrics:
|
|
7212
7465
|
${metrics}` : "",
|
|
7213
7466
|
task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
|
|
7467
|
+
boundaries.length ? `BOUNDARY CONTRACT (enforced by the tool runtime; do not attempt to bypass):
|
|
7468
|
+
${boundaries.map((line) => `- ${line}`).join("\n")}` : "",
|
|
7214
7469
|
"Lease contract (Sprint 1):",
|
|
7215
7470
|
`- leaseId: ${lease.leaseId}`,
|
|
7216
7471
|
`- claimedAt: ${task.assignment?.claimedAt ?? "<unknown>"}`,
|
|
@@ -7223,10 +7478,24 @@ ${metrics}` : "",
|
|
|
7223
7478
|
`- maxAttempts: ${task.assignment?.maxAttempts ?? "<unset>"}`,
|
|
7224
7479
|
`- costCeilingUsd: ${task.assignment?.costCeilingUsd ?? task.costCeilingUsd ?? "<unset>"}`,
|
|
7225
7480
|
"",
|
|
7226
|
-
"Work this task end-to-end.
|
|
7227
|
-
`
|
|
7228
|
-
|
|
7229
|
-
|
|
7481
|
+
"Work this task end-to-end. Treat the board as a live plan, not a frozen assignment snapshot.",
|
|
7482
|
+
`Before material work and whenever evidence changes the plan, call kanban with action "get_board" and boardId "${board.id}" to reassess current tasks, dependencies, priorities, and peer changes.`,
|
|
7483
|
+
"You may use kanban add_task, update_task, split_task, merge_tasks, move_task, delete_task, or dependency actions when reassessment shows the plan should change. Preserve traceability and do not keep obsolete work merely because it existed at dispatch time.",
|
|
7484
|
+
"",
|
|
7485
|
+
"FIELD MAINTENANCE \u2014 keep every task detail current:",
|
|
7486
|
+
'- Call "update_task" to refresh description, priority, type, labels, or estimatedHours as the work evolves.',
|
|
7487
|
+
'- Call "add_note" with author and content to record progress, decisions, or roadblock context.',
|
|
7488
|
+
'- Call "add_check" / "update_check" with description and status (pending/passed/failed) to track acceptance criteria.',
|
|
7489
|
+
'- Call "add_goal_metric" / "update_goal_metric" with name and current/target to report measurable progress.',
|
|
7490
|
+
'- Call "add_link" with url and type (issue/pr/doc/commit/design/file/url/other) to attach evidence.',
|
|
7491
|
+
'- Call "update_task" to set actualHours when you have a final time estimate.',
|
|
7492
|
+
"Do not leave stale or placeholder field values \u2014 the kanban board is the shared source of truth for every stakeholder.",
|
|
7493
|
+
"",
|
|
7494
|
+
"LEASE & LIFECYCLE:",
|
|
7495
|
+
"Every successful board mutation updates shared pending work and notifies the owning session. Re-read the board after mutations and adapt to changes made by other agents; never assume your initial todo list remains authoritative.",
|
|
7496
|
+
`When you start, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", assignmentStatus "running", and expectedLeaseId "${lease.leaseId}". Include subagentId and runTaskId when you have them. The expectedLeaseId fence makes the write a safe no-op if recover_stale already reassigned the task because your lease expired.`,
|
|
7497
|
+
`To stay alive in the queue, call kanban with action "heartbeat_assignment", boardId "${board.id}", taskId "${task.id}", heartbeatAt set to the current time, and expectedLeaseId "${lease.leaseId}". Cadence must be <= heartbeatIntervalMs and well before leaseExpiresAt. The expectedLeaseId fence makes the heartbeat a safe no-op if the lease was reassigned, so a stale worker cannot renew a successor's lease.`,
|
|
7498
|
+
`When you finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", assignmentStatus "completed" or "failed", and expectedLeaseId "${lease.leaseId}". Include lastResult or error. Populate every relevant detail field before the terminal mark_assignment so the card's final state is complete.`,
|
|
7230
7499
|
`If you cannot finish in time, call kanban with action "heartbeat_assignment" to extend the lease, or with action "release_task" to release so another worker can claim. Do NOT silently abandon the assignment.`,
|
|
7231
7500
|
'On failure the host may call kanban with action "recover_stale" (mode: retry/release/fail); respect its decisions and do not duplicate work in parallel.',
|
|
7232
7501
|
"When finished, report what changed, what you verified, and any remaining blockers."
|
|
@@ -7621,8 +7890,8 @@ function makeFleetEmitTool(director) {
|
|
|
7621
7890
|
async execute(input, ctx) {
|
|
7622
7891
|
const i = input;
|
|
7623
7892
|
const role = ctx.meta["agentRole"];
|
|
7624
|
-
const
|
|
7625
|
-
if (
|
|
7893
|
+
const validationError2 = validateFleetEventEmission(i.type, i.payload ?? {}, role);
|
|
7894
|
+
if (validationError2) return { ok: false, error: validationError2 };
|
|
7626
7895
|
const callerId = ctx.agentId && ctx.agentId !== "unknown" ? ctx.agentId : director.id;
|
|
7627
7896
|
const taskId = ctx.meta["subagentTaskId"];
|
|
7628
7897
|
director.fleet.emit({
|
|
@@ -7710,25 +7979,32 @@ var FleetBus = class {
|
|
|
7710
7979
|
}
|
|
7711
7980
|
emit(event) {
|
|
7712
7981
|
const byId = this.byId.get(event.subagentId);
|
|
7713
|
-
if (byId)
|
|
7714
|
-
|
|
7982
|
+
if (byId && byId.size > 0) {
|
|
7983
|
+
const snapshot = [...byId];
|
|
7984
|
+
for (const h of snapshot) {
|
|
7715
7985
|
try {
|
|
7716
7986
|
h(event);
|
|
7717
7987
|
} catch {
|
|
7718
7988
|
}
|
|
7719
7989
|
}
|
|
7990
|
+
}
|
|
7720
7991
|
const byType = this.byType.get(event.type);
|
|
7721
|
-
if (byType)
|
|
7722
|
-
|
|
7992
|
+
if (byType && byType.size > 0) {
|
|
7993
|
+
const snapshot = [...byType];
|
|
7994
|
+
for (const h of snapshot) {
|
|
7723
7995
|
try {
|
|
7724
7996
|
h(event);
|
|
7725
7997
|
} catch {
|
|
7726
7998
|
}
|
|
7727
7999
|
}
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
8000
|
+
}
|
|
8001
|
+
if (this.any.size > 0) {
|
|
8002
|
+
const snapshot = [...this.any];
|
|
8003
|
+
for (const h of snapshot) {
|
|
8004
|
+
try {
|
|
8005
|
+
h(event);
|
|
8006
|
+
} catch {
|
|
8007
|
+
}
|
|
7732
8008
|
}
|
|
7733
8009
|
}
|
|
7734
8010
|
}
|
|
@@ -7913,18 +8189,20 @@ function hashStr(s) {
|
|
|
7913
8189
|
// src/types/provider.ts
|
|
7914
8190
|
var CONTEXT_OVERFLOW_RE = /context.length|context.window|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|context_length_exceeded/i;
|
|
7915
8191
|
var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
|
|
8192
|
+
var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit/i;
|
|
7916
8193
|
function classifyProviderError(status, body, message) {
|
|
7917
8194
|
const type = body?.type;
|
|
8195
|
+
const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
|
|
7918
8196
|
if (status === 0) return "network";
|
|
7919
8197
|
if (status === 408) return "timeout";
|
|
7920
8198
|
if (status === 599) return "stream_hang";
|
|
8199
|
+
if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
|
|
7921
8200
|
if (type === "rate_limit_error" || status === 429) return "rate_limit";
|
|
7922
8201
|
if (type === "overloaded_error" || status === 529) return "overloaded";
|
|
7923
8202
|
if (status >= 500) return "server";
|
|
7924
8203
|
if (type === "authentication_error" || type === "permission_error" || status === 401 || status === 403) {
|
|
7925
8204
|
return "auth";
|
|
7926
8205
|
}
|
|
7927
|
-
const text = [message, body?.message, type, body?.raw].filter(Boolean).join("\n");
|
|
7928
8206
|
if (type === "content_filter" || CONTENT_FILTER_RE.test(text)) return "content_filter";
|
|
7929
8207
|
if (status === 413 || status >= 400 && CONTEXT_OVERFLOW_RE.test(text)) {
|
|
7930
8208
|
return "context_overflow";
|
|
@@ -7999,6 +8277,7 @@ var KIND_TO_CODE = {
|
|
|
7999
8277
|
network: ERROR_CODES.PROVIDER_NETWORK_ERROR,
|
|
8000
8278
|
timeout: ERROR_CODES.PROVIDER_NETWORK_ERROR,
|
|
8001
8279
|
rate_limit: ERROR_CODES.PROVIDER_RATE_LIMITED,
|
|
8280
|
+
quota_exhausted: ERROR_CODES.PROVIDER_RATE_LIMITED,
|
|
8002
8281
|
auth: ERROR_CODES.PROVIDER_AUTH_FAILED,
|
|
8003
8282
|
overloaded: ERROR_CODES.PROVIDER_OVERLOADED,
|
|
8004
8283
|
context_overflow: ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW,
|
|
@@ -8012,6 +8291,311 @@ function kindToCode(kind) {
|
|
|
8012
8291
|
return KIND_TO_CODE[kind];
|
|
8013
8292
|
}
|
|
8014
8293
|
|
|
8294
|
+
// src/core/model-availability-calendar.ts
|
|
8295
|
+
function logicalCalendarTarget(providerId, model) {
|
|
8296
|
+
if (providerId !== "omniroute") return { providerId, model };
|
|
8297
|
+
const slash = model.indexOf("/");
|
|
8298
|
+
return slash > 0 && slash < model.length - 1 ? { providerId: model.slice(0, slash), model: model.slice(slash + 1) } : { providerId, model };
|
|
8299
|
+
}
|
|
8300
|
+
var WEEKDAYS = {
|
|
8301
|
+
Sun: 0,
|
|
8302
|
+
Mon: 1,
|
|
8303
|
+
Tue: 2,
|
|
8304
|
+
Wed: 3,
|
|
8305
|
+
Thu: 4,
|
|
8306
|
+
Fri: 5,
|
|
8307
|
+
Sat: 6
|
|
8308
|
+
};
|
|
8309
|
+
function minuteOfDay(value) {
|
|
8310
|
+
const match = /^(\d{2}):(\d{2})$/.exec(value);
|
|
8311
|
+
if (!match) return void 0;
|
|
8312
|
+
const hour = Number(match[1]);
|
|
8313
|
+
const minute = Number(match[2]);
|
|
8314
|
+
if (hour > 23 || minute > 59) return void 0;
|
|
8315
|
+
return hour * 60 + minute;
|
|
8316
|
+
}
|
|
8317
|
+
function clockAt(date, timezone) {
|
|
8318
|
+
try {
|
|
8319
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
8320
|
+
timeZone: timezone,
|
|
8321
|
+
weekday: "short",
|
|
8322
|
+
hour: "2-digit",
|
|
8323
|
+
minute: "2-digit",
|
|
8324
|
+
hourCycle: "h23"
|
|
8325
|
+
}).formatToParts(date);
|
|
8326
|
+
const weekday = parts.find((part) => part.type === "weekday")?.value;
|
|
8327
|
+
const hour = Number(parts.find((part) => part.type === "hour")?.value);
|
|
8328
|
+
const minute = Number(parts.find((part) => part.type === "minute")?.value);
|
|
8329
|
+
if (!weekday || WEEKDAYS[weekday] === void 0 || !Number.isFinite(hour + minute))
|
|
8330
|
+
return void 0;
|
|
8331
|
+
return { day: WEEKDAYS[weekday], minute: hour * 60 + minute };
|
|
8332
|
+
} catch {
|
|
8333
|
+
return void 0;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
function targetMatches(rule, providerId, model) {
|
|
8337
|
+
if (rule.provider && rule.provider !== providerId) return false;
|
|
8338
|
+
if (rule.model && rule.model !== model) return false;
|
|
8339
|
+
return Boolean(rule.provider || rule.model);
|
|
8340
|
+
}
|
|
8341
|
+
function timeMatches(rule, day, minute) {
|
|
8342
|
+
const start = minuteOfDay(rule.start);
|
|
8343
|
+
const end = minuteOfDay(rule.end);
|
|
8344
|
+
if (start === void 0 || end === void 0) return false;
|
|
8345
|
+
const days = rule.days?.length ? new Set(rule.days) : void 0;
|
|
8346
|
+
if (start === end) return !days || days.has(day);
|
|
8347
|
+
if (start < end) return (!days || days.has(day)) && minute >= start && minute < end;
|
|
8348
|
+
if (minute >= start) return !days || days.has(day);
|
|
8349
|
+
const previousDay = (day + 6) % 7;
|
|
8350
|
+
return minute < end && (!days || days.has(previousDay));
|
|
8351
|
+
}
|
|
8352
|
+
function evaluateModelCalendar(rules, providerId, model, at = /* @__PURE__ */ new Date()) {
|
|
8353
|
+
({ providerId, model } = logicalCalendarTarget(providerId, model));
|
|
8354
|
+
const allowRules = [];
|
|
8355
|
+
let allowMatched = false;
|
|
8356
|
+
for (const rule of rules ?? []) {
|
|
8357
|
+
if (rule.enabled === false || !targetMatches(rule, providerId, model)) continue;
|
|
8358
|
+
const clock = clockAt(at, rule.timezone);
|
|
8359
|
+
if (!clock || minuteOfDay(rule.start) === void 0 || minuteOfDay(rule.end) === void 0)
|
|
8360
|
+
continue;
|
|
8361
|
+
if (rule.mode === "allow_only") {
|
|
8362
|
+
allowRules.push(rule);
|
|
8363
|
+
if (timeMatches(rule, clock.day, clock.minute)) allowMatched = true;
|
|
8364
|
+
} else if (timeMatches(rule, clock.day, clock.minute)) {
|
|
8365
|
+
return { allowed: false, rule };
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
if (allowRules.length > 0 && !allowMatched) return { allowed: false, rule: allowRules[0] };
|
|
8369
|
+
return { allowed: true };
|
|
8370
|
+
}
|
|
8371
|
+
|
|
8372
|
+
// src/core/fallback-profile-manager.ts
|
|
8373
|
+
function hasText(value) {
|
|
8374
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
8375
|
+
}
|
|
8376
|
+
function providerHasKey(entry) {
|
|
8377
|
+
if (!entry) return false;
|
|
8378
|
+
if (hasText(entry.apiKey)) return true;
|
|
8379
|
+
if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => hasText(k?.apiKey))) return true;
|
|
8380
|
+
if (Array.isArray(entry.envVars) && entry.envVars.some((v) => hasText(process.env[v])))
|
|
8381
|
+
return true;
|
|
8382
|
+
return false;
|
|
8383
|
+
}
|
|
8384
|
+
function visibleProviderModels(config, providerId, providerModels) {
|
|
8385
|
+
const entry = config.providers?.[providerId];
|
|
8386
|
+
return entry?.models !== void 0 ? [...entry.models] : providerModels;
|
|
8387
|
+
}
|
|
8388
|
+
function buildProfiles(config) {
|
|
8389
|
+
const entries = /* @__PURE__ */ new Map();
|
|
8390
|
+
for (const [name, chain] of Object.entries(config.fallbackProfiles ?? {})) {
|
|
8391
|
+
if (Array.isArray(chain) && chain.length > 0) {
|
|
8392
|
+
entries.set(name, Object.freeze([...chain]));
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
return entries;
|
|
8396
|
+
}
|
|
8397
|
+
var FallbackProfileManager = class {
|
|
8398
|
+
/** Immutable snapshot of config.fallbackProfiles for the active config. */
|
|
8399
|
+
profiles;
|
|
8400
|
+
/** Active frozen config snapshot for provider lookups. */
|
|
8401
|
+
config;
|
|
8402
|
+
/** Optional shared runtime status tracker. */
|
|
8403
|
+
statusTracker;
|
|
8404
|
+
constructor(config, opts) {
|
|
8405
|
+
this.config = config;
|
|
8406
|
+
this.profiles = buildProfiles(config);
|
|
8407
|
+
this.statusTracker = opts?.statusTracker;
|
|
8408
|
+
}
|
|
8409
|
+
/**
|
|
8410
|
+
* Bind (or replace) the shared runtime status tracker.
|
|
8411
|
+
* Called by the boot path after the tracker is created.
|
|
8412
|
+
*/
|
|
8413
|
+
setStatusTracker(tracker) {
|
|
8414
|
+
this.statusTracker = tracker;
|
|
8415
|
+
}
|
|
8416
|
+
// ── Profile existence ──────────────────────────────────────────────────
|
|
8417
|
+
hasProfile(name) {
|
|
8418
|
+
return this.profiles.has(name);
|
|
8419
|
+
}
|
|
8420
|
+
listProfiles() {
|
|
8421
|
+
return Object.freeze([...this.profiles.keys()]);
|
|
8422
|
+
}
|
|
8423
|
+
// ── Resolution ─────────────────────────────────────────────────────────
|
|
8424
|
+
/**
|
|
8425
|
+
* Resolve a named fallback profile to a validated, provider-filtered chain.
|
|
8426
|
+
*
|
|
8427
|
+
* Returns an empty chain when:
|
|
8428
|
+
* - The profile doesn't exist.
|
|
8429
|
+
* - Every entry's provider is missing, has no key, or has no matching model.
|
|
8430
|
+
*
|
|
8431
|
+
* @param name - Profile name from config.fallbackProfiles.
|
|
8432
|
+
* @param defaultProvider - Used when an entry has no explicit provider.
|
|
8433
|
+
* @param exclude - Optional { providerId, model } to skip (avoid self-fallback).
|
|
8434
|
+
*/
|
|
8435
|
+
resolve(name, opts = {}) {
|
|
8436
|
+
const defaultProvider = opts.defaultProvider ?? this.config.provider;
|
|
8437
|
+
const chain = this.profiles.get(name);
|
|
8438
|
+
if (!chain) return FREEZER_EMPTY;
|
|
8439
|
+
const excludeKey = opts.exclude ? `${opts.exclude.providerId}/${opts.exclude.model}` : void 0;
|
|
8440
|
+
const resolved = [];
|
|
8441
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8442
|
+
for (const ref of chain) {
|
|
8443
|
+
const parsed = parseModelRef(ref);
|
|
8444
|
+
if (!parsed.model) continue;
|
|
8445
|
+
const providerId = parsed.provider ?? defaultProvider;
|
|
8446
|
+
const key = `${providerId}/${parsed.model}`;
|
|
8447
|
+
if (seen.has(key)) continue;
|
|
8448
|
+
seen.add(key);
|
|
8449
|
+
if (excludeKey && key === excludeKey) continue;
|
|
8450
|
+
const health = this.checkProvider(providerId);
|
|
8451
|
+
if (!health.usable) continue;
|
|
8452
|
+
if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
|
|
8453
|
+
if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
|
|
8454
|
+
continue;
|
|
8455
|
+
const allowedModels = this.config.providers?.[providerId]?.models;
|
|
8456
|
+
if (allowedModels && !allowedModels.includes(parsed.model)) continue;
|
|
8457
|
+
resolved.push({
|
|
8458
|
+
providerId,
|
|
8459
|
+
model: parsed.model,
|
|
8460
|
+
providerSwitched: providerId !== (opts.exclude?.providerId ?? this.config.provider)
|
|
8461
|
+
});
|
|
8462
|
+
}
|
|
8463
|
+
return Object.freeze(resolved);
|
|
8464
|
+
}
|
|
8465
|
+
/**
|
|
8466
|
+
* Resolve the effective fallback chain for a session: explicit fallbackModels
|
|
8467
|
+
* first, then named profile, then smart default (unless disabled).
|
|
8468
|
+
*
|
|
8469
|
+
* Mirrors the previous `effectiveFallbackChain()` logic but centralized.
|
|
8470
|
+
*/
|
|
8471
|
+
resolveEffective(opts = {}) {
|
|
8472
|
+
if (opts.fallbackModels && opts.fallbackModels.length > 0) {
|
|
8473
|
+
const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
|
|
8474
|
+
if (resolved.length > 0) return resolved;
|
|
8475
|
+
}
|
|
8476
|
+
if (opts.fallbackProfile) {
|
|
8477
|
+
const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
|
|
8478
|
+
if (resolved.length > 0) return resolved;
|
|
8479
|
+
}
|
|
8480
|
+
if (opts.fallbackAuto !== false) {
|
|
8481
|
+
return this.smartDefault(opts.exclude);
|
|
8482
|
+
}
|
|
8483
|
+
return FREEZER_EMPTY;
|
|
8484
|
+
}
|
|
8485
|
+
// ── Provider availability (read-only) ──────────────────────────────────
|
|
8486
|
+
checkProvider(providerId) {
|
|
8487
|
+
const entry = this.config.providers?.[providerId];
|
|
8488
|
+
const isPrimary = providerId === this.config.provider;
|
|
8489
|
+
const hasKey = providerHasKey(entry) || isPrimary && hasText(this.config.apiKey);
|
|
8490
|
+
const hasEndpoint = hasText(entry?.baseUrl) || isPrimary && hasText(this.config.baseUrl);
|
|
8491
|
+
const hasModels = Array.isArray(entry?.models) && entry.models.length > 0 || isPrimary && hasText(this.config.model);
|
|
8492
|
+
return Object.freeze({
|
|
8493
|
+
providerId,
|
|
8494
|
+
hasKey,
|
|
8495
|
+
hasEndpoint,
|
|
8496
|
+
hasModels,
|
|
8497
|
+
usable: hasKey || hasEndpoint
|
|
8498
|
+
});
|
|
8499
|
+
}
|
|
8500
|
+
// ── Rebuild on config change ───────────────────────────────────────────
|
|
8501
|
+
/**
|
|
8502
|
+
* Atomically replace the active immutable snapshot while preserving this
|
|
8503
|
+
* service identity for every injected consumer.
|
|
8504
|
+
*/
|
|
8505
|
+
reload(newConfig) {
|
|
8506
|
+
this.config = newConfig;
|
|
8507
|
+
this.profiles = buildProfiles(newConfig);
|
|
8508
|
+
}
|
|
8509
|
+
// ── Internal helpers ───────────────────────────────────────────────────
|
|
8510
|
+
/**
|
|
8511
|
+
* Resolve an array of model ref strings (from explicit fallbackModels config).
|
|
8512
|
+
* Public so consumers like one-shot-llm can use it directly.
|
|
8513
|
+
*/
|
|
8514
|
+
resolveRefs(refs, exclude) {
|
|
8515
|
+
const excludeKey = exclude ? `${exclude.providerId}/${exclude.model}` : void 0;
|
|
8516
|
+
const resolved = [];
|
|
8517
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8518
|
+
for (const ref of refs) {
|
|
8519
|
+
const parsed = parseModelRef(ref);
|
|
8520
|
+
if (!parsed.model) continue;
|
|
8521
|
+
const providerId = parsed.provider ?? this.config.provider;
|
|
8522
|
+
const key = `${providerId}/${parsed.model}`;
|
|
8523
|
+
if (seen.has(key)) continue;
|
|
8524
|
+
seen.add(key);
|
|
8525
|
+
if (excludeKey && key === excludeKey) continue;
|
|
8526
|
+
if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
|
|
8527
|
+
if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
|
|
8528
|
+
continue;
|
|
8529
|
+
resolved.push({
|
|
8530
|
+
providerId,
|
|
8531
|
+
model: parsed.model,
|
|
8532
|
+
providerSwitched: providerId !== (exclude?.providerId ?? this.config.provider)
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8535
|
+
return Object.freeze(resolved);
|
|
8536
|
+
}
|
|
8537
|
+
/**
|
|
8538
|
+
* Derive a smart default chain from configured providers when nothing
|
|
8539
|
+
* explicit is set. Same-provider alternatives first, then cross-provider.
|
|
8540
|
+
* Limited to 4 entries to avoid burning through models on a transient blip.
|
|
8541
|
+
*/
|
|
8542
|
+
smartDefault(exclude) {
|
|
8543
|
+
const leaderProvider = this.config.provider;
|
|
8544
|
+
const leaderModel = this.config.model;
|
|
8545
|
+
const providers = this.config.providers ?? {};
|
|
8546
|
+
const favoriteSet = new Set(
|
|
8547
|
+
(this.config.favoriteModels ?? []).map((ref) => {
|
|
8548
|
+
const p = parseModelRef(ref);
|
|
8549
|
+
return `${p.provider ?? leaderProvider}/${p.model}`;
|
|
8550
|
+
})
|
|
8551
|
+
);
|
|
8552
|
+
const hasFavorites = favoriteSet.size > 0;
|
|
8553
|
+
const favoritesOnly = this.config.favoriteModelsOnly === true;
|
|
8554
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8555
|
+
const favorites = [];
|
|
8556
|
+
const sameProvider = [];
|
|
8557
|
+
const crossProvider = [];
|
|
8558
|
+
const excludeKey = exclude ? `${exclude.providerId}/${exclude.model}` : void 0;
|
|
8559
|
+
const ids = Object.keys(providers).sort(
|
|
8560
|
+
(a, b) => a === leaderProvider ? -1 : b === leaderProvider ? 1 : a.localeCompare(b)
|
|
8561
|
+
);
|
|
8562
|
+
for (const id of ids) {
|
|
8563
|
+
const entry = providers[id];
|
|
8564
|
+
if (!this.checkProvider(id).usable) continue;
|
|
8565
|
+
const models = visibleProviderModels(this.config, id, entry?.models ?? []);
|
|
8566
|
+
for (const model of models) {
|
|
8567
|
+
if (id === leaderProvider && model === leaderModel) continue;
|
|
8568
|
+
const ref = `${id}/${model}`;
|
|
8569
|
+
if (seen.has(ref)) continue;
|
|
8570
|
+
seen.add(ref);
|
|
8571
|
+
if (excludeKey && ref === excludeKey) continue;
|
|
8572
|
+
if (this.statusTracker && !this.statusTracker.isAvailable(id, model)) continue;
|
|
8573
|
+
if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, id, model).allowed)
|
|
8574
|
+
continue;
|
|
8575
|
+
if (favoriteSet.has(ref)) {
|
|
8576
|
+
favorites.push(ref);
|
|
8577
|
+
continue;
|
|
8578
|
+
}
|
|
8579
|
+
if (favoritesOnly && hasFavorites) continue;
|
|
8580
|
+
(id === leaderProvider ? sameProvider : crossProvider).push(ref);
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
const MAX = 4;
|
|
8584
|
+
const all = [...favorites, ...sameProvider, ...crossProvider].slice(0, MAX);
|
|
8585
|
+
return Object.freeze(
|
|
8586
|
+
all.map((ref) => {
|
|
8587
|
+
const p = parseModelRef(ref);
|
|
8588
|
+
return {
|
|
8589
|
+
providerId: p.provider ?? leaderProvider,
|
|
8590
|
+
model: p.model,
|
|
8591
|
+
providerSwitched: (p.provider ?? leaderProvider) !== (exclude?.providerId ?? leaderProvider)
|
|
8592
|
+
};
|
|
8593
|
+
})
|
|
8594
|
+
);
|
|
8595
|
+
}
|
|
8596
|
+
};
|
|
8597
|
+
var FREEZER_EMPTY = Object.freeze([]);
|
|
8598
|
+
|
|
8015
8599
|
// src/core/fallback-model.ts
|
|
8016
8600
|
function parseModelRef(ref) {
|
|
8017
8601
|
const trimmed = ref.trim();
|
|
@@ -8030,8 +8614,8 @@ function parseModelRef(ref) {
|
|
|
8030
8614
|
}
|
|
8031
8615
|
function fallbackProfileChain(config, profileName) {
|
|
8032
8616
|
if (!profileName) return [];
|
|
8033
|
-
const
|
|
8034
|
-
return
|
|
8617
|
+
const mgr = new FallbackProfileManager(config);
|
|
8618
|
+
return mgr.resolve(profileName).map((e) => `${e.providerId}/${e.model}`);
|
|
8035
8619
|
}
|
|
8036
8620
|
var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
|
|
8037
8621
|
|
|
@@ -8265,6 +8849,7 @@ function classifySubagentError(err, hints = {}) {
|
|
|
8265
8849
|
function providerErrorToSubagentError(err, message, cause) {
|
|
8266
8850
|
switch (err.kind) {
|
|
8267
8851
|
case "rate_limit":
|
|
8852
|
+
case "quota_exhausted":
|
|
8268
8853
|
return {
|
|
8269
8854
|
kind: "provider_rate_limit",
|
|
8270
8855
|
message,
|
|
@@ -9652,6 +10237,8 @@ var Director = class _Director {
|
|
|
9652
10237
|
collab;
|
|
9653
10238
|
/** Prevents large `ask_subagent` answers from bloating the leader's context window. */
|
|
9654
10239
|
largeAnswerStore;
|
|
10240
|
+
/** Shared provider/model status tracker, or undefined. */
|
|
10241
|
+
statusTracker;
|
|
9655
10242
|
constructor(opts) {
|
|
9656
10243
|
this.id = opts.config.coordinatorId || randomUUID7();
|
|
9657
10244
|
this.brain = opts.brain;
|
|
@@ -9690,6 +10277,7 @@ var Director = class _Director {
|
|
|
9690
10277
|
opts.checkpointDebounceMs ?? 250
|
|
9691
10278
|
) : null;
|
|
9692
10279
|
this.fleetManager = opts.fleetManager;
|
|
10280
|
+
this.statusTracker = opts.statusTracker;
|
|
9693
10281
|
this.logger = opts.logger;
|
|
9694
10282
|
if (this.sharedScratchpadPath) {
|
|
9695
10283
|
void fsp5.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
|
|
@@ -9812,6 +10400,14 @@ var Director = class _Director {
|
|
|
9812
10400
|
title
|
|
9813
10401
|
}
|
|
9814
10402
|
);
|
|
10403
|
+
if (failed) {
|
|
10404
|
+
void this.appendSessionEvent({
|
|
10405
|
+
type: "agent_error",
|
|
10406
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10407
|
+
agentId: r.subagentId,
|
|
10408
|
+
error: errorString ?? r.status
|
|
10409
|
+
});
|
|
10410
|
+
}
|
|
9815
10411
|
if (this.fleetManager) {
|
|
9816
10412
|
void this.fleetManager.flushManifest();
|
|
9817
10413
|
} else {
|
|
@@ -9894,53 +10490,53 @@ var Director = class _Director {
|
|
|
9894
10490
|
payload.extend(extra);
|
|
9895
10491
|
});
|
|
9896
10492
|
};
|
|
9897
|
-
if (this.brain) {
|
|
9898
|
-
|
|
9899
|
-
id: `director-budget-${e.subagentId}-${payload.kind}`,
|
|
9900
|
-
sessionId: this.currentSessionId(),
|
|
9901
|
-
source: "director",
|
|
9902
|
-
question: `Should the director extend the ${payload.kind} budget for subagent ${e.subagentId}?`,
|
|
9903
|
-
context: [
|
|
9904
|
-
e.taskId ? `Task id: ${e.taskId}` : void 0,
|
|
9905
|
-
`Used: ${payload.used}`,
|
|
9906
|
-
`Limit: ${payload.limit}`,
|
|
9907
|
-
`Prior extensions for this kind: ${prior}`
|
|
9908
|
-
].filter(Boolean).join("\n"),
|
|
9909
|
-
risk: payload.kind === "cost" ? "high" : "medium",
|
|
9910
|
-
fallback: "continue",
|
|
9911
|
-
options: [
|
|
9912
|
-
{
|
|
9913
|
-
id: "extend",
|
|
9914
|
-
label: "Grant the director default budget extension",
|
|
9915
|
-
consequence: "The subagent continues with a larger per-kind budget.",
|
|
9916
|
-
risk: payload.kind === "cost" ? "high" : "medium",
|
|
9917
|
-
recommended: true
|
|
9918
|
-
},
|
|
9919
|
-
{
|
|
9920
|
-
id: "stop",
|
|
9921
|
-
label: "Stop this subagent at the current budget limit",
|
|
9922
|
-
consequence: "The current task will fail or stop due to budget pressure.",
|
|
9923
|
-
risk: "low"
|
|
9924
|
-
}
|
|
9925
|
-
]
|
|
9926
|
-
}).then((decision) => {
|
|
9927
|
-
if (decision.type === "deny") {
|
|
9928
|
-
payload.deny();
|
|
9929
|
-
return;
|
|
9930
|
-
}
|
|
9931
|
-
if (decision.type === "ask_human") {
|
|
9932
|
-
payload.deny();
|
|
9933
|
-
return;
|
|
9934
|
-
}
|
|
9935
|
-
if (decision.optionId === "stop") {
|
|
9936
|
-
payload.deny();
|
|
9937
|
-
return;
|
|
9938
|
-
}
|
|
9939
|
-
grantExtension();
|
|
9940
|
-
}).catch(() => payload.deny());
|
|
10493
|
+
if (payload.kind !== "cost" || !this.brain) {
|
|
10494
|
+
grantExtension();
|
|
9941
10495
|
return;
|
|
9942
10496
|
}
|
|
9943
|
-
|
|
10497
|
+
void this.brain.decide({
|
|
10498
|
+
id: `director-budget-${e.subagentId}-${payload.kind}`,
|
|
10499
|
+
sessionId: this.currentSessionId(),
|
|
10500
|
+
source: "director",
|
|
10501
|
+
question: `Should the director extend the ${payload.kind} budget for subagent ${e.subagentId}?`,
|
|
10502
|
+
context: [
|
|
10503
|
+
e.taskId ? `Task id: ${e.taskId}` : void 0,
|
|
10504
|
+
`Used: ${payload.used}`,
|
|
10505
|
+
`Limit: ${payload.limit}`,
|
|
10506
|
+
`Prior extensions for this kind: ${prior}`
|
|
10507
|
+
].filter(Boolean).join("\n"),
|
|
10508
|
+
risk: "high",
|
|
10509
|
+
fallback: "continue",
|
|
10510
|
+
options: [
|
|
10511
|
+
{
|
|
10512
|
+
id: "extend",
|
|
10513
|
+
label: "Grant the director default budget extension",
|
|
10514
|
+
consequence: "The subagent continues with a larger cost budget.",
|
|
10515
|
+
risk: "high",
|
|
10516
|
+
recommended: true
|
|
10517
|
+
},
|
|
10518
|
+
{
|
|
10519
|
+
id: "stop",
|
|
10520
|
+
label: "Stop this subagent at the current budget limit",
|
|
10521
|
+
consequence: "The current task will fail or stop due to budget pressure.",
|
|
10522
|
+
risk: "low"
|
|
10523
|
+
}
|
|
10524
|
+
]
|
|
10525
|
+
}).then((decision) => {
|
|
10526
|
+
if (decision.type === "deny") {
|
|
10527
|
+
payload.deny();
|
|
10528
|
+
return;
|
|
10529
|
+
}
|
|
10530
|
+
if (decision.type === "ask_human") {
|
|
10531
|
+
payload.deny();
|
|
10532
|
+
return;
|
|
10533
|
+
}
|
|
10534
|
+
if (decision.optionId === "stop") {
|
|
10535
|
+
payload.deny();
|
|
10536
|
+
return;
|
|
10537
|
+
}
|
|
10538
|
+
grantExtension();
|
|
10539
|
+
}).catch(() => payload.deny());
|
|
9944
10540
|
});
|
|
9945
10541
|
}
|
|
9946
10542
|
/**
|
|
@@ -10198,9 +10794,20 @@ var Director = class _Director {
|
|
|
10198
10794
|
if (entry?.model) {
|
|
10199
10795
|
config.model = entry.model;
|
|
10200
10796
|
if (entry.provider) config.provider = entry.provider;
|
|
10797
|
+
if (entry.fallbackProfile) config.fallbackProfile = entry.fallbackProfile;
|
|
10798
|
+
if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
|
|
10799
|
+
} else if (entry?.fallbackProfile) {
|
|
10800
|
+
config.fallbackProfile = entry.fallbackProfile;
|
|
10201
10801
|
if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
|
|
10202
10802
|
}
|
|
10203
10803
|
}
|
|
10804
|
+
if (this.statusTracker && config.provider && config.model) {
|
|
10805
|
+
if (!this.statusTracker.isAvailable(config.provider, config.model)) {
|
|
10806
|
+
this.logger?.warn(
|
|
10807
|
+
`spawn: resolved model "${config.provider}/${config.model}" for role "${config.role ?? "?"}" is blocked by the status tracker. The subagent will attempt its fallback chain.`
|
|
10808
|
+
);
|
|
10809
|
+
}
|
|
10810
|
+
}
|
|
10204
10811
|
}
|
|
10205
10812
|
enforceSpawnCaps(config) {
|
|
10206
10813
|
if (this.fleetManager) {
|
|
@@ -10708,6 +11315,11 @@ var Director = class _Director {
|
|
|
10708
11315
|
}
|
|
10709
11316
|
async remove(subagentId) {
|
|
10710
11317
|
this.clearSubagentIdleRetirement(subagentId);
|
|
11318
|
+
void this.appendSessionEvent({
|
|
11319
|
+
type: "agent_stopped",
|
|
11320
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11321
|
+
agentId: subagentId
|
|
11322
|
+
});
|
|
10711
11323
|
await this.coordinator.remove(subagentId);
|
|
10712
11324
|
const bridge = this.subagentBridges.get(subagentId);
|
|
10713
11325
|
if (bridge) {
|
|
@@ -11082,24 +11694,24 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11082
11694
|
/**
|
|
11083
11695
|
* Scrub secrets out of conversation-turn events before they are observed
|
|
11084
11696
|
* for the summary, written to the JSONL log, or surfaced on resume. Only
|
|
11085
|
-
*
|
|
11086
|
-
*
|
|
11087
|
-
*
|
|
11088
|
-
* also strips transient token-cache fields even when no scrubber is set.
|
|
11697
|
+
* Conversation events carry free-form user/model text. Snapshot and exact
|
|
11698
|
+
* message-journal normalization also strips transient token-cache fields
|
|
11699
|
+
* even when no scrubber is set.
|
|
11089
11700
|
*/
|
|
11090
11701
|
scrubEvent(event) {
|
|
11091
11702
|
const s = this.secretScrubber;
|
|
11092
|
-
|
|
11703
|
+
const persistMessage = (message) => {
|
|
11704
|
+
const { _estTokens: _ignored, ...persisted } = message;
|
|
11093
11705
|
return {
|
|
11094
|
-
...
|
|
11095
|
-
|
|
11096
|
-
const { _estTokens: _ignored, ...persisted } = message;
|
|
11097
|
-
return {
|
|
11098
|
-
...persisted,
|
|
11099
|
-
content: typeof persisted.content === "string" ? s?.scrub(persisted.content) ?? persisted.content : s?.scrubObject(persisted.content) ?? persisted.content
|
|
11100
|
-
};
|
|
11101
|
-
})
|
|
11706
|
+
...persisted,
|
|
11707
|
+
content: typeof persisted.content === "string" ? s?.scrub(persisted.content) ?? persisted.content : s?.scrubObject(persisted.content) ?? persisted.content
|
|
11102
11708
|
};
|
|
11709
|
+
};
|
|
11710
|
+
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
11711
|
+
return { ...event, messages: event.messages.map(persistMessage) };
|
|
11712
|
+
}
|
|
11713
|
+
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
11714
|
+
return { ...event, message: persistMessage(event.message) };
|
|
11103
11715
|
}
|
|
11104
11716
|
if (!s) return event;
|
|
11105
11717
|
if (event.type === "user_input") {
|
|
@@ -11154,7 +11766,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11154
11766
|
this.bufferSynchronousEvent(event);
|
|
11155
11767
|
}
|
|
11156
11768
|
recordFileObservation(input) {
|
|
11157
|
-
if (!input.path || !/^[a-f\d]{64}$/i.test(input.hash) || !Number.isFinite(input.mtimeMs))
|
|
11769
|
+
if (!input.path || !/^[a-f\d]{64}$/i.test(input.hash) || !Number.isFinite(input.mtimeMs))
|
|
11770
|
+
return;
|
|
11158
11771
|
this.bufferSynchronousEvent({
|
|
11159
11772
|
type: "file_observation",
|
|
11160
11773
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -11316,12 +11929,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11316
11929
|
const now = Date.now();
|
|
11317
11930
|
if (now - this.lastAppendWarnAt > 5e3) {
|
|
11318
11931
|
const suppressed = this.appendFailCount - 1;
|
|
11319
|
-
|
|
11320
|
-
console.warn(
|
|
11321
|
-
"[session] flush failed:",
|
|
11322
|
-
toErrorMessage(err),
|
|
11323
|
-
tail
|
|
11324
|
-
);
|
|
11932
|
+
console.warn(JSON.stringify({ level: "warn", event: "session.flush_failed", sessionId: this.id, message: toErrorMessage(err), ...suppressed > 0 ? { suppressed } : {}, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
11325
11933
|
this.lastAppendWarnAt = now;
|
|
11326
11934
|
this.appendFailCount = 0;
|
|
11327
11935
|
}
|
|
@@ -11341,6 +11949,43 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11341
11949
|
});
|
|
11342
11950
|
}
|
|
11343
11951
|
}
|
|
11952
|
+
/**
|
|
11953
|
+
* Rebuild every accumulated summary counter by replaying the JSONL as it now
|
|
11954
|
+
* stands on disk.
|
|
11955
|
+
*
|
|
11956
|
+
* Only truncation needs this: the counters are fed by `observeForSummary` as
|
|
11957
|
+
* events go by and cannot un-count. Reading the whole file is fine here —
|
|
11958
|
+
* rewind is a rare, explicitly user-driven operation, and the alternative
|
|
11959
|
+
* (reversing each counter per discarded event) has to stay in lockstep with
|
|
11960
|
+
* observeForSummary forever.
|
|
11961
|
+
*
|
|
11962
|
+
* `title` is deliberately re-derived too: the first surviving user_input may
|
|
11963
|
+
* differ once earlier prompts are gone.
|
|
11964
|
+
*/
|
|
11965
|
+
async recomputeSummaryFromDisk() {
|
|
11966
|
+
if (!this.filePath) return;
|
|
11967
|
+
const raw = await fsp6.readFile(this.filePath, "utf8").catch(() => null);
|
|
11968
|
+
if (raw === null) return;
|
|
11969
|
+
this.iterationCount = 0;
|
|
11970
|
+
this.toolCallCount = 0;
|
|
11971
|
+
this.toolErrorCount = 0;
|
|
11972
|
+
this.toolBreakdown = {};
|
|
11973
|
+
this.fileChangeCount = 0;
|
|
11974
|
+
this.compactionCount = 0;
|
|
11975
|
+
this.outcome = void 0;
|
|
11976
|
+
this.openToolUses = /* @__PURE__ */ new Set();
|
|
11977
|
+
this.tokenIn = 0;
|
|
11978
|
+
this.tokenOut = 0;
|
|
11979
|
+
this.summary = { ...this.summary, title: "(empty session)", tokenTotal: 0 };
|
|
11980
|
+
for (const line of raw.split("\n")) {
|
|
11981
|
+
const trimmed = line.trim();
|
|
11982
|
+
if (trimmed.length === 0) continue;
|
|
11983
|
+
try {
|
|
11984
|
+
this.observeForSummary(JSON.parse(trimmed));
|
|
11985
|
+
} catch {
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11344
11989
|
observeForSummary(event) {
|
|
11345
11990
|
if (event.type === "llm_response") {
|
|
11346
11991
|
for (const block of event.content) {
|
|
@@ -11391,10 +12036,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11391
12036
|
}
|
|
11392
12037
|
async doClose() {
|
|
11393
12038
|
if (this.pendingFileSnapshots.length > 0) {
|
|
11394
|
-
await this.writeFileSnapshot(
|
|
11395
|
-
this.activePromptIndex ?? 0,
|
|
11396
|
-
[...this.pendingFileSnapshots]
|
|
11397
|
-
);
|
|
12039
|
+
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
11398
12040
|
this.pendingFileSnapshots = [];
|
|
11399
12041
|
}
|
|
11400
12042
|
this.closed = true;
|
|
@@ -11514,7 +12156,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11514
12156
|
* so post-checkpoint content is never read or parsed — O(1) memory instead
|
|
11515
12157
|
* of O(N) JSON.parse calls over the full file.
|
|
11516
12158
|
*/
|
|
11517
|
-
async truncateToCheckpoint(targetPromptIndex) {
|
|
12159
|
+
async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
|
|
11518
12160
|
if (!this.filePath) return 0;
|
|
11519
12161
|
if (this.flushTimer) {
|
|
11520
12162
|
clearTimeout(this.flushTimer);
|
|
@@ -11525,7 +12167,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11525
12167
|
const CHUNK_SIZE = 65536;
|
|
11526
12168
|
let fd;
|
|
11527
12169
|
let fileOffset = 0;
|
|
11528
|
-
let
|
|
12170
|
+
let pendingLine = Buffer.alloc(0);
|
|
11529
12171
|
let checkpointByteOffset = -1;
|
|
11530
12172
|
let removedCount = 0;
|
|
11531
12173
|
let targetCheckpointSeen = false;
|
|
@@ -11535,17 +12177,18 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11535
12177
|
const buf = Buffer.alloc(CHUNK_SIZE);
|
|
11536
12178
|
const { bytesRead } = await fd.read(buf, 0, CHUNK_SIZE, fileOffset);
|
|
11537
12179
|
if (bytesRead === 0) break;
|
|
12180
|
+
const carriedBytes = pendingLine.length;
|
|
12181
|
+
const chunk = carriedBytes === 0 ? buf.subarray(0, bytesRead) : Buffer.concat([pendingLine, buf.subarray(0, bytesRead)]);
|
|
12182
|
+
const chunkFileOffset = fileOffset - carriedBytes;
|
|
11538
12183
|
let chunkPos = 0;
|
|
11539
|
-
while (chunkPos <
|
|
11540
|
-
const idx =
|
|
11541
|
-
if (idx === -1)
|
|
11542
|
-
|
|
11543
|
-
break;
|
|
11544
|
-
}
|
|
12184
|
+
while (chunkPos < chunk.length) {
|
|
12185
|
+
const idx = chunk.indexOf("\n", chunkPos);
|
|
12186
|
+
if (idx === -1) break;
|
|
12187
|
+
const lineStartOffset = chunkFileOffset + chunkPos;
|
|
11545
12188
|
if (checkpointByteOffset !== -1) {
|
|
11546
12189
|
removedCount++;
|
|
11547
12190
|
} else {
|
|
11548
|
-
const lineBytes =
|
|
12191
|
+
const lineBytes = chunk.subarray(chunkPos, idx);
|
|
11549
12192
|
const line = new TextDecoder("utf-8", { fatal: false }).decode(lineBytes);
|
|
11550
12193
|
if (line.trim()) {
|
|
11551
12194
|
try {
|
|
@@ -11571,12 +12214,9 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11571
12214
|
}
|
|
11572
12215
|
}
|
|
11573
12216
|
chunkPos = idx + 1;
|
|
11574
|
-
lineStartOffset = fileOffset + chunkPos;
|
|
11575
12217
|
}
|
|
12218
|
+
pendingLine = chunk.subarray(chunkPos);
|
|
11576
12219
|
fileOffset += bytesRead;
|
|
11577
|
-
if (chunkPos >= bytesRead) {
|
|
11578
|
-
lineStartOffset = fileOffset;
|
|
11579
|
-
}
|
|
11580
12220
|
}
|
|
11581
12221
|
} finally {
|
|
11582
12222
|
await fd?.close();
|
|
@@ -11656,17 +12296,19 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11656
12296
|
this.handle = await fsp6.open(this.filePath, "a", 384).catch(() => this.handle);
|
|
11657
12297
|
throw err;
|
|
11658
12298
|
}
|
|
12299
|
+
await this.recomputeSummaryFromDisk();
|
|
12300
|
+
const reverted = [...revertedFiles];
|
|
11659
12301
|
await this.append({
|
|
11660
12302
|
type: "rewound",
|
|
11661
12303
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11662
12304
|
toPromptIndex: targetPromptIndex,
|
|
11663
|
-
revertedFiles:
|
|
12305
|
+
revertedFiles: reverted
|
|
11664
12306
|
});
|
|
11665
12307
|
this.activePromptIndex = targetPromptIndex;
|
|
11666
12308
|
this.events?.emit("session.rewound", {
|
|
11667
12309
|
sessionId: this.id,
|
|
11668
12310
|
toPromptIndex: targetPromptIndex,
|
|
11669
|
-
revertedFiles:
|
|
12311
|
+
revertedFiles: reverted,
|
|
11670
12312
|
removedEvents: removedCount
|
|
11671
12313
|
});
|
|
11672
12314
|
return removedCount;
|
|
@@ -11707,7 +12349,11 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11707
12349
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11708
12350
|
context
|
|
11709
12351
|
});
|
|
11710
|
-
this.events?.emit("in_flight.started", {
|
|
12352
|
+
this.events?.emit("in_flight.started", {
|
|
12353
|
+
sessionId: this.id,
|
|
12354
|
+
context,
|
|
12355
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
12356
|
+
});
|
|
11711
12357
|
}
|
|
11712
12358
|
/**
|
|
11713
12359
|
* Close the in-flight marker. Idempotent in spirit
|
|
@@ -11718,10 +12364,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11718
12364
|
*/
|
|
11719
12365
|
async clearInFlightMarker(reason) {
|
|
11720
12366
|
if (this.pendingFileSnapshots.length > 0) {
|
|
11721
|
-
await this.writeFileSnapshot(
|
|
11722
|
-
this.activePromptIndex ?? 0,
|
|
11723
|
-
[...this.pendingFileSnapshots]
|
|
11724
|
-
);
|
|
12367
|
+
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
11725
12368
|
this.pendingFileSnapshots = [];
|
|
11726
12369
|
}
|
|
11727
12370
|
await this.append({
|
|
@@ -11729,7 +12372,11 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11729
12372
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11730
12373
|
reason
|
|
11731
12374
|
});
|
|
11732
|
-
this.events?.emit("in_flight.ended", {
|
|
12375
|
+
this.events?.emit("in_flight.ended", {
|
|
12376
|
+
sessionId: this.id,
|
|
12377
|
+
reason,
|
|
12378
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
12379
|
+
});
|
|
11733
12380
|
}
|
|
11734
12381
|
};
|
|
11735
12382
|
|
|
@@ -12120,6 +12767,13 @@ function defaultRunGit(args, cwd) {
|
|
|
12120
12767
|
});
|
|
12121
12768
|
}
|
|
12122
12769
|
|
|
12770
|
+
// src/storage/session-id.ts
|
|
12771
|
+
function generateSessionId(startedAt, _model) {
|
|
12772
|
+
const date = startedAt.slice(0, 10);
|
|
12773
|
+
const seedTime = Number.isNaN(Date.parse(startedAt)) ? Date.now() : Date.parse(startedAt);
|
|
12774
|
+
return `${date}/sess_${ulid(seedTime)}`;
|
|
12775
|
+
}
|
|
12776
|
+
|
|
12123
12777
|
// src/storage/session-resume-validation.ts
|
|
12124
12778
|
import { createHash as createHash3 } from "node:crypto";
|
|
12125
12779
|
import * as fsp8 from "node:fs/promises";
|
|
@@ -12229,12 +12883,14 @@ function formatResumeValidationNotice(validation, projectRoot) {
|
|
|
12229
12883
|
...shown
|
|
12230
12884
|
].join("\n");
|
|
12231
12885
|
}
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12886
|
+
function formatInterruptedToolNotice(pendingToolUseCount) {
|
|
12887
|
+
if (!Number.isFinite(pendingToolUseCount) || pendingToolUseCount <= 0) return null;
|
|
12888
|
+
const plural = pendingToolUseCount === 1 ? "tool call was" : "tool calls were";
|
|
12889
|
+
return [
|
|
12890
|
+
"[SESSION RESUME INTERRUPTED WORK]",
|
|
12891
|
+
`The previous run left ${pendingToolUseCount} ${plural} in flight \u2014 no result was recorded before it stopped (crash or interrupt).`,
|
|
12892
|
+
"Those interrupted tool calls were removed from the restored conversation and were NOT re-executed. Re-run the work if it still needs doing."
|
|
12893
|
+
].join("\n");
|
|
12238
12894
|
}
|
|
12239
12895
|
|
|
12240
12896
|
// src/storage/session-summary.ts
|
|
@@ -12280,18 +12936,27 @@ function isReplayableMessage(value) {
|
|
|
12280
12936
|
const candidate = value;
|
|
12281
12937
|
return (candidate.role === "user" || candidate.role === "assistant" || candidate.role === "system") && (typeof candidate.content === "string" || Array.isArray(candidate.content));
|
|
12282
12938
|
}
|
|
12939
|
+
function trackMessageToolState(message, openToolUses) {
|
|
12940
|
+
if (!Array.isArray(message.content)) return;
|
|
12941
|
+
for (const block of message.content) {
|
|
12942
|
+
if (block.type === "tool_use") openToolUses.add(block.id);
|
|
12943
|
+
else if (block.type === "tool_result") openToolUses.delete(block.tool_use_id);
|
|
12944
|
+
}
|
|
12945
|
+
}
|
|
12946
|
+
function replayableMessage(value, fallbackTs) {
|
|
12947
|
+
if (!isReplayableMessage(value)) return null;
|
|
12948
|
+
const { _estTokens: _ignored, ...message } = value;
|
|
12949
|
+
return message.ts === void 0 && fallbackTs ? { ...message, ts: fallbackTs } : message;
|
|
12950
|
+
}
|
|
12283
12951
|
function applyContextSnapshot(target, openToolUses, snapshot) {
|
|
12284
12952
|
if (!Array.isArray(snapshot) || !snapshot.every(isReplayableMessage)) return false;
|
|
12285
12953
|
target.length = 0;
|
|
12286
12954
|
openToolUses.clear();
|
|
12287
12955
|
for (const raw of snapshot) {
|
|
12288
|
-
const
|
|
12956
|
+
const message = replayableMessage(raw);
|
|
12957
|
+
if (!message) return false;
|
|
12289
12958
|
target.push(message);
|
|
12290
|
-
|
|
12291
|
-
for (const block of message.content) {
|
|
12292
|
-
if (block.type === "tool_use") openToolUses.add(block.id);
|
|
12293
|
-
else if (block.type === "tool_result") openToolUses.delete(block.tool_use_id);
|
|
12294
|
-
}
|
|
12959
|
+
trackMessageToolState(message, openToolUses);
|
|
12295
12960
|
}
|
|
12296
12961
|
return true;
|
|
12297
12962
|
}
|
|
@@ -12444,10 +13109,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12444
13109
|
handle = await fsp9.open(file, "a", 384);
|
|
12445
13110
|
} catch (err) {
|
|
12446
13111
|
this.emitError(id, file, "create", toErrorMessage(err), false);
|
|
12447
|
-
throw new Error(
|
|
12448
|
-
`Failed to open session file: ${toErrorMessage(err)}`,
|
|
12449
|
-
{ cause: err }
|
|
12450
|
-
);
|
|
13112
|
+
throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
|
|
12451
13113
|
}
|
|
12452
13114
|
try {
|
|
12453
13115
|
const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
|
|
@@ -12460,10 +13122,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12460
13122
|
this.emitWrite(id, file, "create", "success", Date.now() - t0);
|
|
12461
13123
|
return writer;
|
|
12462
13124
|
} catch (err) {
|
|
12463
|
-
await handle.close().catch(
|
|
12464
|
-
"Session handle close failed",
|
|
12465
|
-
|
|
12466
|
-
|
|
13125
|
+
await handle.close().catch(
|
|
13126
|
+
(e) => this.logWarn("Session handle close failed", {
|
|
13127
|
+
event: "session_store.handle_close_failed",
|
|
13128
|
+
message: e instanceof Error ? e.message : String(e)
|
|
13129
|
+
})
|
|
13130
|
+
);
|
|
12467
13131
|
this.emitError(id, file, "create", toErrorMessage(err), true);
|
|
12468
13132
|
throw err;
|
|
12469
13133
|
}
|
|
@@ -12482,9 +13146,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12482
13146
|
}
|
|
12483
13147
|
}
|
|
12484
13148
|
if (boundary === -1) {
|
|
12485
|
-
throw new Error(
|
|
12486
|
-
`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`
|
|
12487
|
-
);
|
|
13149
|
+
throw new Error(`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`);
|
|
12488
13150
|
}
|
|
12489
13151
|
}
|
|
12490
13152
|
const parentPrefix = parent.events.slice(0, boundary + 1);
|
|
@@ -12531,7 +13193,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12531
13193
|
}
|
|
12532
13194
|
async materializeWorkspaceCheckpoint(checkpoint, targetRoot) {
|
|
12533
13195
|
if (!this.checkpointCas) {
|
|
12534
|
-
throw new Error(
|
|
13196
|
+
throw new Error(
|
|
13197
|
+
"Workspace checkpoint materialization requires a projectRoot-aware session store"
|
|
13198
|
+
);
|
|
12535
13199
|
}
|
|
12536
13200
|
return this.checkpointCas.materialize(checkpoint, targetRoot);
|
|
12537
13201
|
}
|
|
@@ -12539,34 +13203,44 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12539
13203
|
const file = this.sessionPath(id, ".jsonl");
|
|
12540
13204
|
const t0 = Date.now();
|
|
12541
13205
|
const data = await this.load(id);
|
|
12542
|
-
|
|
13206
|
+
const noticeMessages = [];
|
|
13207
|
+
let resumeValidation;
|
|
12543
13208
|
if (this.projectRoot) {
|
|
12544
13209
|
try {
|
|
12545
|
-
|
|
13210
|
+
resumeValidation = await validateResumeFileObservations(data.events, this.projectRoot);
|
|
12546
13211
|
const notice = formatResumeValidationNotice(resumeValidation, this.projectRoot);
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
]
|
|
12555
|
-
} : {}
|
|
12556
|
-
};
|
|
13212
|
+
if (notice) {
|
|
13213
|
+
noticeMessages.push({
|
|
13214
|
+
role: "system",
|
|
13215
|
+
content: notice,
|
|
13216
|
+
ts: resumeValidation.checkedAt
|
|
13217
|
+
});
|
|
13218
|
+
}
|
|
12557
13219
|
} catch (err) {
|
|
12558
13220
|
this.emitError(id, file, "resume_validation", toErrorMessage(err), true);
|
|
12559
13221
|
}
|
|
12560
13222
|
}
|
|
13223
|
+
const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
|
|
13224
|
+
if (interruptedNotice) {
|
|
13225
|
+
noticeMessages.push({
|
|
13226
|
+
role: "system",
|
|
13227
|
+
content: interruptedNotice,
|
|
13228
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
13229
|
+
});
|
|
13230
|
+
}
|
|
13231
|
+
const resumedData = {
|
|
13232
|
+
...data,
|
|
13233
|
+
...resumeValidation ? { resumeValidation } : {},
|
|
13234
|
+
...noticeMessages.length > 0 ? { messages: [...data.messages, ...noticeMessages] } : {}
|
|
13235
|
+
};
|
|
12561
13236
|
let handle;
|
|
12562
13237
|
try {
|
|
12563
13238
|
handle = await fsp9.open(file, "a", 384);
|
|
12564
13239
|
} catch (err) {
|
|
12565
13240
|
this.emitError(id, file, "resume", toErrorMessage(err), false);
|
|
12566
|
-
throw new Error(
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
);
|
|
13241
|
+
throw new Error(`Failed to open session "${id}" for append: ${toErrorMessage(err)}`, {
|
|
13242
|
+
cause: err
|
|
13243
|
+
});
|
|
12570
13244
|
}
|
|
12571
13245
|
try {
|
|
12572
13246
|
const writer = new FileSessionWriter(
|
|
@@ -12594,10 +13268,12 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12594
13268
|
this.emitWrite(id, file, "resume", "success", Date.now() - t0);
|
|
12595
13269
|
return { writer, data: resumedData };
|
|
12596
13270
|
} catch (err) {
|
|
12597
|
-
await handle.close().catch(
|
|
12598
|
-
"Session handle close failed",
|
|
12599
|
-
|
|
12600
|
-
|
|
13271
|
+
await handle.close().catch(
|
|
13272
|
+
(e) => this.logWarn("Session handle close failed", {
|
|
13273
|
+
event: "session_store.handle_close_failed",
|
|
13274
|
+
message: e instanceof Error ? e.message : String(e)
|
|
13275
|
+
})
|
|
13276
|
+
);
|
|
12601
13277
|
this.emitError(id, file, "resume", toErrorMessage(err), true);
|
|
12602
13278
|
throw err;
|
|
12603
13279
|
}
|
|
@@ -12648,6 +13324,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12648
13324
|
let sessionForkedEvent;
|
|
12649
13325
|
const messages = mode.full ? [] : void 0;
|
|
12650
13326
|
const openToolUses = mode.full ? /* @__PURE__ */ new Set() : void 0;
|
|
13327
|
+
let exactJournalActive = false;
|
|
12651
13328
|
let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
12652
13329
|
const stream = createReadStream(file, { encoding: "utf8" });
|
|
12653
13330
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
@@ -12672,20 +13349,59 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12672
13349
|
sessionForkedEvent = ev;
|
|
12673
13350
|
}
|
|
12674
13351
|
if (mode.full && messages !== void 0 && openToolUses !== void 0) {
|
|
12675
|
-
if (ev.type === "
|
|
13352
|
+
if (ev.type === "message_appended" && ev.version === 1) {
|
|
13353
|
+
const message = replayableMessage(ev.message, ev.ts);
|
|
13354
|
+
if (message) {
|
|
13355
|
+
if (!exactJournalActive) {
|
|
13356
|
+
messages.length = 0;
|
|
13357
|
+
openToolUses.clear();
|
|
13358
|
+
exactJournalActive = true;
|
|
13359
|
+
}
|
|
13360
|
+
messages.push(message);
|
|
13361
|
+
trackMessageToolState(message, openToolUses);
|
|
13362
|
+
} else {
|
|
13363
|
+
this.events?.emit("session.damaged", {
|
|
13364
|
+
sessionId: id,
|
|
13365
|
+
detail: "Ignored malformed message_appended event"
|
|
13366
|
+
});
|
|
13367
|
+
}
|
|
13368
|
+
} else if (ev.type === "message_updated" && ev.version === 1) {
|
|
13369
|
+
const message = replayableMessage(ev.message, ev.ts);
|
|
13370
|
+
if (message && exactJournalActive && ev.index >= 0 && ev.index < messages.length) {
|
|
13371
|
+
messages[ev.index] = message;
|
|
13372
|
+
openToolUses.clear();
|
|
13373
|
+
for (const current of messages) trackMessageToolState(current, openToolUses);
|
|
13374
|
+
} else {
|
|
13375
|
+
this.events?.emit("session.damaged", {
|
|
13376
|
+
sessionId: id,
|
|
13377
|
+
detail: `Ignored malformed message_updated event at index ${ev.index}`
|
|
13378
|
+
});
|
|
13379
|
+
}
|
|
13380
|
+
} else if (ev.type === "messages_replaced" && ev.version === 1) {
|
|
13381
|
+
if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
|
|
13382
|
+
exactJournalActive = true;
|
|
13383
|
+
} else {
|
|
13384
|
+
this.events?.emit("session.damaged", {
|
|
13385
|
+
sessionId: id,
|
|
13386
|
+
detail: "Ignored malformed messages_replaced event"
|
|
13387
|
+
});
|
|
13388
|
+
}
|
|
13389
|
+
} else if (ev.type === "context_snapshot") {
|
|
12676
13390
|
if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
|
|
12677
13391
|
this.events?.emit("session.damaged", {
|
|
12678
13392
|
sessionId: id,
|
|
12679
13393
|
detail: "Ignored malformed context_snapshot event"
|
|
12680
13394
|
});
|
|
12681
13395
|
}
|
|
12682
|
-
} else if (ev.type === "user_input") {
|
|
13396
|
+
} else if (!exactJournalActive && ev.type === "user_input") {
|
|
12683
13397
|
openToolUses.clear();
|
|
12684
13398
|
messages.push({ role: "user", content: ev.content, ts: ev.ts });
|
|
12685
13399
|
} else if (ev.type === "llm_response") {
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
13400
|
+
if (!exactJournalActive) {
|
|
13401
|
+
messages.push({ role: "assistant", content: ev.content, ts: ev.ts });
|
|
13402
|
+
for (const b of ev.content) {
|
|
13403
|
+
if (b.type === "tool_use") openToolUses.add(b.id);
|
|
13404
|
+
}
|
|
12689
13405
|
}
|
|
12690
13406
|
usage = {
|
|
12691
13407
|
input: usage.input + (ev.usage.input ?? 0),
|
|
@@ -12693,7 +13409,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12693
13409
|
cacheRead: (usage.cacheRead ?? 0) + (ev.usage.cacheRead ?? 0),
|
|
12694
13410
|
cacheWrite: (usage.cacheWrite ?? 0) + (ev.usage.cacheWrite ?? 0)
|
|
12695
13411
|
};
|
|
12696
|
-
} else if (ev.type === "tool_result") {
|
|
13412
|
+
} else if (!exactJournalActive && ev.type === "tool_result") {
|
|
12697
13413
|
if (!openToolUses.has(ev.id)) {
|
|
12698
13414
|
this.events?.emit("session.damaged", {
|
|
12699
13415
|
sessionId: id,
|
|
@@ -12765,7 +13481,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12765
13481
|
} : void 0
|
|
12766
13482
|
};
|
|
12767
13483
|
const toolCallEnds = extractToolCallEnds(events);
|
|
12768
|
-
const
|
|
13484
|
+
const pendingToolUseCount = openToolUses && openToolUses.size > 0 ? openToolUses.size : void 0;
|
|
13485
|
+
const data = {
|
|
13486
|
+
metadata: meta,
|
|
13487
|
+
events,
|
|
13488
|
+
messages: finalMessages,
|
|
13489
|
+
usage,
|
|
13490
|
+
toolCallEnds,
|
|
13491
|
+
...pendingToolUseCount !== void 0 ? { pendingToolUseCount } : {}
|
|
13492
|
+
};
|
|
12769
13493
|
if (mode.full) {
|
|
12770
13494
|
if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
|
|
12771
13495
|
const oldest = this._loadCache.keys().next().value;
|
|
@@ -12781,7 +13505,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12781
13505
|
errorMsg = toErrorMessage(err);
|
|
12782
13506
|
throw err;
|
|
12783
13507
|
} finally {
|
|
12784
|
-
this.emitRead(
|
|
13508
|
+
this.emitRead(
|
|
13509
|
+
id,
|
|
13510
|
+
file,
|
|
13511
|
+
mode.full ? "load" : "load_events_only",
|
|
13512
|
+
outcome,
|
|
13513
|
+
Date.now() - t0,
|
|
13514
|
+
errorMsg
|
|
13515
|
+
);
|
|
12785
13516
|
if (cacheHit) {
|
|
12786
13517
|
this.events?.emit("storage.cache_hit", {
|
|
12787
13518
|
sessionId: id,
|
|
@@ -12983,7 +13714,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
12983
13714
|
outcome = "failure";
|
|
12984
13715
|
errorMsg = toErrorMessage(err);
|
|
12985
13716
|
} finally {
|
|
12986
|
-
this.emitWrite(
|
|
13717
|
+
this.emitWrite(
|
|
13718
|
+
"~compact~",
|
|
13719
|
+
this.indexFile,
|
|
13720
|
+
"compact",
|
|
13721
|
+
outcome,
|
|
13722
|
+
Date.now() - t0,
|
|
13723
|
+
void 0,
|
|
13724
|
+
errorMsg
|
|
13725
|
+
);
|
|
12987
13726
|
}
|
|
12988
13727
|
}
|
|
12989
13728
|
/**
|
|
@@ -13055,7 +13794,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
13055
13794
|
*/
|
|
13056
13795
|
async rebuildIndex() {
|
|
13057
13796
|
const ids = await this.collectSessionIds(this.dir);
|
|
13058
|
-
const summaries = await Promise.all(
|
|
13797
|
+
const summaries = await Promise.all(
|
|
13798
|
+
ids.map((id) => this.summaryFor(id).catch(() => null))
|
|
13799
|
+
);
|
|
13059
13800
|
const valid = summaries.filter((s) => s !== null);
|
|
13060
13801
|
const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
|
|
13061
13802
|
await withFileLock(this.indexFile, async () => {
|
|
@@ -13096,7 +13837,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
13096
13837
|
const shardKeys = [""];
|
|
13097
13838
|
for (const entry of entries) {
|
|
13098
13839
|
if (entry.name.startsWith(".") && entry.name !== ".wrongstack") continue;
|
|
13099
|
-
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
13840
|
+
if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
|
|
13841
|
+
continue;
|
|
13100
13842
|
if (entry.isDirectory()) shardKeys.push(entry.name);
|
|
13101
13843
|
}
|
|
13102
13844
|
return shardKeys;
|
|
@@ -13218,10 +13960,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
13218
13960
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
13219
13961
|
const msg = toErrorMessage(err);
|
|
13220
13962
|
this.emitError(id, manifest, "summary_fallback", msg, true);
|
|
13221
|
-
this.logWarn(
|
|
13222
|
-
|
|
13223
|
-
|
|
13224
|
-
|
|
13963
|
+
this.logWarn("Session manifest write failed", {
|
|
13964
|
+
event: "session_store.manifest_write_failed",
|
|
13965
|
+
sessionId: id,
|
|
13966
|
+
message: msg
|
|
13967
|
+
});
|
|
13225
13968
|
});
|
|
13226
13969
|
outcome = "failure";
|
|
13227
13970
|
errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
|
|
@@ -13328,24 +14071,28 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
13328
14071
|
if (r.status === "rejected") {
|
|
13329
14072
|
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
|
|
13330
14073
|
if (r.reason?.code !== "ENOENT") {
|
|
13331
|
-
console.warn(
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
14074
|
+
console.warn(
|
|
14075
|
+
JSON.stringify({
|
|
14076
|
+
level: "warn",
|
|
14077
|
+
event: "session_store.delete_failed",
|
|
14078
|
+
sessionId: id,
|
|
14079
|
+
message: msg,
|
|
14080
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
14081
|
+
})
|
|
14082
|
+
);
|
|
13338
14083
|
}
|
|
13339
14084
|
}
|
|
13340
14085
|
}
|
|
13341
14086
|
await fsp9.rm(sessDir, { recursive: true, force: true }).catch((err) => {
|
|
13342
|
-
console.warn(
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
14087
|
+
console.warn(
|
|
14088
|
+
JSON.stringify({
|
|
14089
|
+
level: "warn",
|
|
14090
|
+
event: "session_store.rmdir_failed",
|
|
14091
|
+
sessionId: id,
|
|
14092
|
+
message: toErrorMessage(err),
|
|
14093
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
14094
|
+
})
|
|
14095
|
+
);
|
|
13349
14096
|
});
|
|
13350
14097
|
await this.writeTombstone(id);
|
|
13351
14098
|
}
|
|
@@ -14052,6 +14799,145 @@ var AUTO_COMPACT_INTERVAL_MS = 3e5;
|
|
|
14052
14799
|
var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
|
|
14053
14800
|
var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
|
|
14054
14801
|
|
|
14802
|
+
// src/coordination/mailbox-types.ts
|
|
14803
|
+
var MAILBOX_TYPE_PROPERTIES = {
|
|
14804
|
+
note: {
|
|
14805
|
+
category: "informational",
|
|
14806
|
+
expectsReply: false,
|
|
14807
|
+
requiresAction: false,
|
|
14808
|
+
backgroundEligible: false,
|
|
14809
|
+
outOfBand: false,
|
|
14810
|
+
renderPriority: 20,
|
|
14811
|
+
recipientObligation: "Read for context; no reply needed.",
|
|
14812
|
+
senderGuidance: "General-purpose FYI. Use when no more specific type applies."
|
|
14813
|
+
},
|
|
14814
|
+
ask: {
|
|
14815
|
+
category: "actionable",
|
|
14816
|
+
expectsReply: true,
|
|
14817
|
+
requiresAction: true,
|
|
14818
|
+
backgroundEligible: true,
|
|
14819
|
+
outOfBand: false,
|
|
14820
|
+
renderPriority: 10,
|
|
14821
|
+
recipientObligation: "Answer as soon as possible \u2014 the sender is waiting.",
|
|
14822
|
+
senderGuidance: "Blocking question. Only use when you need an answer to proceed."
|
|
14823
|
+
},
|
|
14824
|
+
assign: {
|
|
14825
|
+
category: "actionable",
|
|
14826
|
+
expectsReply: false,
|
|
14827
|
+
requiresAction: true,
|
|
14828
|
+
backgroundEligible: true,
|
|
14829
|
+
outOfBand: false,
|
|
14830
|
+
renderPriority: 10,
|
|
14831
|
+
recipientObligation: "Accept or decline; act on it when current operation allows.",
|
|
14832
|
+
senderGuidance: 'Task delegation. Must be directed to a specific recipient (not "*").'
|
|
14833
|
+
},
|
|
14834
|
+
steer: {
|
|
14835
|
+
category: "actionable",
|
|
14836
|
+
expectsReply: false,
|
|
14837
|
+
requiresAction: true,
|
|
14838
|
+
backgroundEligible: true,
|
|
14839
|
+
outOfBand: false,
|
|
14840
|
+
renderPriority: 0,
|
|
14841
|
+
// Always rendered first
|
|
14842
|
+
recipientObligation: "Pause current approach, adjust per instruction, then resume.",
|
|
14843
|
+
senderGuidance: "Mid-task direction change. The recipient is already working on something."
|
|
14844
|
+
},
|
|
14845
|
+
btw: {
|
|
14846
|
+
category: "informational",
|
|
14847
|
+
expectsReply: false,
|
|
14848
|
+
requiresAction: false,
|
|
14849
|
+
backgroundEligible: false,
|
|
14850
|
+
outOfBand: false,
|
|
14851
|
+
renderPriority: 30,
|
|
14852
|
+
recipientObligation: "Absorb the information and stay on current task; no reply needed.",
|
|
14853
|
+
senderGuidance: "Low-priority aside. Non-urgent info that can wait."
|
|
14854
|
+
},
|
|
14855
|
+
broadcast: {
|
|
14856
|
+
category: "routing",
|
|
14857
|
+
expectsReply: false,
|
|
14858
|
+
requiresAction: false,
|
|
14859
|
+
backgroundEligible: false,
|
|
14860
|
+
outOfBand: false,
|
|
14861
|
+
renderPriority: 20,
|
|
14862
|
+
recipientObligation: 'Read if addressed to you (direct recipient, alias, or "*").',
|
|
14863
|
+
senderGuidance: 'Multi-recipient envelope. Auto-selected when to is "*" or "@session".'
|
|
14864
|
+
},
|
|
14865
|
+
status: {
|
|
14866
|
+
category: "informational",
|
|
14867
|
+
expectsReply: false,
|
|
14868
|
+
requiresAction: false,
|
|
14869
|
+
backgroundEligible: false,
|
|
14870
|
+
outOfBand: false,
|
|
14871
|
+
renderPriority: 40,
|
|
14872
|
+
recipientObligation: "Use to avoid redundant work; never act on as a task or question.",
|
|
14873
|
+
senderGuidance: "Agent/system status update. Machine-generated, not for human-originated messages."
|
|
14874
|
+
},
|
|
14875
|
+
result: {
|
|
14876
|
+
category: "informational",
|
|
14877
|
+
expectsReply: false,
|
|
14878
|
+
requiresAction: false,
|
|
14879
|
+
backgroundEligible: true,
|
|
14880
|
+
outOfBand: false,
|
|
14881
|
+
renderPriority: 10,
|
|
14882
|
+
recipientObligation: "Factor into next decision; treat as evidence, not a new task.",
|
|
14883
|
+
senderGuidance: "Task completion notice. Share the outcome of finished work."
|
|
14884
|
+
},
|
|
14885
|
+
review: {
|
|
14886
|
+
category: "actionable",
|
|
14887
|
+
expectsReply: false,
|
|
14888
|
+
requiresAction: true,
|
|
14889
|
+
backgroundEligible: true,
|
|
14890
|
+
outOfBand: false,
|
|
14891
|
+
renderPriority: 10,
|
|
14892
|
+
recipientObligation: "Inspect when convenient; no immediate reply required.",
|
|
14893
|
+
senderGuidance: "Passive review request (code/doc/PR). No reply required."
|
|
14894
|
+
},
|
|
14895
|
+
control: {
|
|
14896
|
+
category: "control_signal",
|
|
14897
|
+
expectsReply: false,
|
|
14898
|
+
requiresAction: false,
|
|
14899
|
+
backgroundEligible: false,
|
|
14900
|
+
outOfBand: true,
|
|
14901
|
+
renderPriority: 999,
|
|
14902
|
+
// Never rendered
|
|
14903
|
+
recipientObligation: 'Handled by the agent loop, NOT folded into conversation. "interrupt" causes cooperative halt.',
|
|
14904
|
+
senderGuidance: "RESERVED for runtime use. Agents must NOT send control messages."
|
|
14905
|
+
}
|
|
14906
|
+
};
|
|
14907
|
+
function validateSendType(type, to) {
|
|
14908
|
+
if (type === "control") {
|
|
14909
|
+
throw new TypeError(
|
|
14910
|
+
'Type "control" is reserved for runtime use and cannot be set by agents'
|
|
14911
|
+
);
|
|
14912
|
+
}
|
|
14913
|
+
const isMultiRecipient = to === "*" || to.startsWith("@session:");
|
|
14914
|
+
if (type === "assign" && isMultiRecipient) {
|
|
14915
|
+
throw new TypeError(
|
|
14916
|
+
`Type "assign" requires a specific recipient \u2014 multi-recipient target "${to}" is ambiguous`
|
|
14917
|
+
);
|
|
14918
|
+
}
|
|
14919
|
+
if (type === "steer" && isMultiRecipient) {
|
|
14920
|
+
throw new TypeError(
|
|
14921
|
+
`Type "steer" requires a specific recipient \u2014 multi-recipient target "${to}" is ambiguous`
|
|
14922
|
+
);
|
|
14923
|
+
}
|
|
14924
|
+
}
|
|
14925
|
+
var SESSION_RECIPIENT_PREFIX = "@session:";
|
|
14926
|
+
function sessionRecipient(sessionId) {
|
|
14927
|
+
const normalizedSessionId = sessionId.trim();
|
|
14928
|
+
if (!normalizedSessionId) {
|
|
14929
|
+
throw new TypeError('sessionId is required for the "@session" recipient');
|
|
14930
|
+
}
|
|
14931
|
+
return `${SESSION_RECIPIENT_PREFIX}${normalizedSessionId}`;
|
|
14932
|
+
}
|
|
14933
|
+
function normalizeRecipient(to, sessionId) {
|
|
14934
|
+
const trimmed = to.trim();
|
|
14935
|
+
const normalized = trimmed.toLowerCase();
|
|
14936
|
+
if (normalized === "all") return "*";
|
|
14937
|
+
if (normalized === "@session") return sessionRecipient(sessionId ?? "");
|
|
14938
|
+
return trimmed;
|
|
14939
|
+
}
|
|
14940
|
+
|
|
14055
14941
|
// src/coordination/mailbox-message-codec.ts
|
|
14056
14942
|
var MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
14057
14943
|
"note",
|
|
@@ -14108,6 +14994,19 @@ function parseMessageType(value) {
|
|
|
14108
14994
|
function normalizeMailboxMessageType(value) {
|
|
14109
14995
|
return parseMessageType(value);
|
|
14110
14996
|
}
|
|
14997
|
+
function resolveSendType(type, to) {
|
|
14998
|
+
const normalizedTo = normalizeRecipient(to);
|
|
14999
|
+
const resolved = type ?? (normalizedTo === "*" || normalizedTo.startsWith("@session:") ? "broadcast" : "note");
|
|
15000
|
+
validateSendType(resolved, normalizedTo);
|
|
15001
|
+
return resolved;
|
|
15002
|
+
}
|
|
15003
|
+
function resolveSendTypeSafe(type, to) {
|
|
15004
|
+
try {
|
|
15005
|
+
return { ok: true, type: resolveSendType(type, to) };
|
|
15006
|
+
} catch (err) {
|
|
15007
|
+
return { ok: false, error: err.message };
|
|
15008
|
+
}
|
|
15009
|
+
}
|
|
14111
15010
|
function parsePriority(value) {
|
|
14112
15011
|
if (typeof value === "string" && PRIORITIES.has(value)) {
|
|
14113
15012
|
return value;
|
|
@@ -14219,11 +15118,6 @@ function serializeAckRecord(ack) {
|
|
|
14219
15118
|
return JSON.stringify(ack) + "\n";
|
|
14220
15119
|
}
|
|
14221
15120
|
|
|
14222
|
-
// src/coordination/mailbox-types.ts
|
|
14223
|
-
function normalizeRecipient(to) {
|
|
14224
|
-
return to.trim().toLowerCase() === "all" ? "*" : to.trim();
|
|
14225
|
-
}
|
|
14226
|
-
|
|
14227
15121
|
// src/coordination/global-mailbox.ts
|
|
14228
15122
|
var MAILBOX_FILE = "_mailbox.jsonl";
|
|
14229
15123
|
var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
|
|
@@ -14355,9 +15249,9 @@ var GlobalMailbox = class {
|
|
|
14355
15249
|
const msg = {
|
|
14356
15250
|
id: randomUUID10(),
|
|
14357
15251
|
from: input.from,
|
|
14358
|
-
// "all"
|
|
14359
|
-
//
|
|
14360
|
-
to: normalizeRecipient(input.to),
|
|
15252
|
+
// "all" maps to project broadcast; "@session" is resolved against the
|
|
15253
|
+
// sender's session before persistence so recipients can match it exactly.
|
|
15254
|
+
to: normalizeRecipient(input.to, input.senderSessionId),
|
|
14361
15255
|
type: normalizeMailboxMessageType(input.type),
|
|
14362
15256
|
subject: input.subject,
|
|
14363
15257
|
body: input.body,
|
|
@@ -14367,6 +15261,7 @@ var GlobalMailbox = class {
|
|
|
14367
15261
|
timestamp: now,
|
|
14368
15262
|
replyTo: input.replyTo,
|
|
14369
15263
|
taskContext: input.taskContext,
|
|
15264
|
+
senderSessionId: input.senderSessionId,
|
|
14370
15265
|
expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
|
|
14371
15266
|
};
|
|
14372
15267
|
const line = JSON.stringify(msg) + LINE_SEPARATOR;
|
|
@@ -14422,6 +15317,7 @@ var GlobalMailbox = class {
|
|
|
14422
15317
|
for (const m of candidates) {
|
|
14423
15318
|
if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
|
|
14424
15319
|
if (q.from !== void 0 && m.from !== q.from) continue;
|
|
15320
|
+
if (q.sessionId !== void 0 && m.senderSessionId !== q.sessionId) continue;
|
|
14425
15321
|
if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
|
|
14426
15322
|
if (q.incompleteOnly && m.completed) continue;
|
|
14427
15323
|
if (queryType !== void 0 && m.type !== queryType) continue;
|
|
@@ -14551,8 +15447,9 @@ var GlobalMailbox = class {
|
|
|
14551
15447
|
if (updated.length > 0) this.publishHqMailboxSnapshot();
|
|
14552
15448
|
return updated;
|
|
14553
15449
|
}
|
|
14554
|
-
async unreadCount(forAgentId) {
|
|
15450
|
+
async unreadCount(forAgentId, sessionId) {
|
|
14555
15451
|
const all = await this._readMessagesCached();
|
|
15452
|
+
const scopedSessionRecipient = sessionId === void 0 ? void 0 : sessionRecipient(sessionId);
|
|
14556
15453
|
let count = 0;
|
|
14557
15454
|
if (this._recipientIndex !== null) {
|
|
14558
15455
|
const indices = /* @__PURE__ */ new Set();
|
|
@@ -14560,6 +15457,10 @@ var GlobalMailbox = class {
|
|
|
14560
15457
|
if (direct !== void 0) for (const i of direct) indices.add(i);
|
|
14561
15458
|
const broadcasts = this._recipientIndex.get("*");
|
|
14562
15459
|
if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
|
|
15460
|
+
if (scopedSessionRecipient !== void 0) {
|
|
15461
|
+
const sessionBroadcasts = this._recipientIndex.get(scopedSessionRecipient);
|
|
15462
|
+
if (sessionBroadcasts !== void 0) for (const i of sessionBroadcasts) indices.add(i);
|
|
15463
|
+
}
|
|
14563
15464
|
for (const i of indices) {
|
|
14564
15465
|
const m = all[i];
|
|
14565
15466
|
if (!(forAgentId in m.readBy) && !m.completed) count++;
|
|
@@ -14567,9 +15468,7 @@ var GlobalMailbox = class {
|
|
|
14567
15468
|
} else {
|
|
14568
15469
|
for (let i = 0; i < all.length; i++) {
|
|
14569
15470
|
const m = all[i];
|
|
14570
|
-
if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed)
|
|
14571
|
-
count++;
|
|
14572
|
-
}
|
|
15471
|
+
if ((m.to === forAgentId || m.to === "*" || m.to === scopedSessionRecipient) && !(forAgentId in m.readBy) && !m.completed) count++;
|
|
14573
15472
|
}
|
|
14574
15473
|
}
|
|
14575
15474
|
return count;
|
|
@@ -14673,7 +15572,7 @@ var GlobalMailbox = class {
|
|
|
14673
15572
|
toolCalls: 0,
|
|
14674
15573
|
registeredAt: now,
|
|
14675
15574
|
lastSeenAt: now,
|
|
14676
|
-
pid: input.pid,
|
|
15575
|
+
pid: input.pid ?? process.pid,
|
|
14677
15576
|
source: input.source
|
|
14678
15577
|
};
|
|
14679
15578
|
await withFileLock(this.registryPath, async () => {
|
|
@@ -14705,7 +15604,7 @@ var GlobalMailbox = class {
|
|
|
14705
15604
|
lastActivityAt: now,
|
|
14706
15605
|
lastSeenAt: now,
|
|
14707
15606
|
online: true,
|
|
14708
|
-
pid: input.pid,
|
|
15607
|
+
pid: input.pid ?? process.pid,
|
|
14709
15608
|
...input.source !== void 0 ? { source: input.source } : {}
|
|
14710
15609
|
}
|
|
14711
15610
|
});
|
|
@@ -14832,7 +15731,7 @@ var GlobalMailbox = class {
|
|
|
14832
15731
|
source: input.source,
|
|
14833
15732
|
registeredAt: now,
|
|
14834
15733
|
lastSeenAt: now,
|
|
14835
|
-
pid: input.pid
|
|
15734
|
+
pid: input.pid ?? process.pid
|
|
14836
15735
|
};
|
|
14837
15736
|
await withFileLock(this.clientRegistryPath, async () => {
|
|
14838
15737
|
const registry = await this._readClientRegistry({ fresh: true });
|
|
@@ -15342,6 +16241,48 @@ var GlobalMailbox = class {
|
|
|
15342
16241
|
async _ensureRegistry() {
|
|
15343
16242
|
await fsp11.mkdir(path14.dirname(this.registryPath), { recursive: true });
|
|
15344
16243
|
}
|
|
16244
|
+
/** Minimal shape-check for a deserialized agent registry entry.
|
|
16245
|
+
* Logs a warning and returns `false` for structurally invalid data
|
|
16246
|
+
* so a single corrupted entry doesn't break the entire registry. */
|
|
16247
|
+
_parseAgentEntry(value) {
|
|
16248
|
+
if (typeof value !== "object" || value === null) return null;
|
|
16249
|
+
const v = value;
|
|
16250
|
+
if (typeof v.agentId !== "string") return null;
|
|
16251
|
+
if (typeof v.sessionId !== "string") return null;
|
|
16252
|
+
const statuses = ["idle", "busy", "running", "streaming", "waiting_user", "error"];
|
|
16253
|
+
const status = typeof v.status === "string" && statuses.includes(v.status) ? v.status : "idle";
|
|
16254
|
+
return {
|
|
16255
|
+
...v,
|
|
16256
|
+
agentId: v.agentId,
|
|
16257
|
+
sessionId: v.sessionId,
|
|
16258
|
+
name: typeof v.name === "string" ? v.name : v.agentId,
|
|
16259
|
+
registeredAt: typeof v.registeredAt === "string" ? v.registeredAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
16260
|
+
lastSeenAt: typeof v.lastSeenAt === "string" ? v.lastSeenAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
16261
|
+
iterations: typeof v.iterations === "number" && Number.isFinite(v.iterations) ? v.iterations : 0,
|
|
16262
|
+
toolCalls: typeof v.toolCalls === "number" && Number.isFinite(v.toolCalls) ? v.toolCalls : 0,
|
|
16263
|
+
pid: typeof v.pid === "number" && Number.isFinite(v.pid) ? v.pid : 0,
|
|
16264
|
+
status
|
|
16265
|
+
};
|
|
16266
|
+
}
|
|
16267
|
+
/** Minimal shape-check for a deserialized client registry entry. */
|
|
16268
|
+
_parseClientEntry(value) {
|
|
16269
|
+
if (typeof value !== "object" || value === null) return null;
|
|
16270
|
+
const v = value;
|
|
16271
|
+
if (typeof v.clientId !== "string") return null;
|
|
16272
|
+
if (typeof v.sessionId !== "string") return null;
|
|
16273
|
+
const sources = ["repl", "tui", "webui", "http"];
|
|
16274
|
+
const source = typeof v.source === "string" && sources.includes(v.source) ? v.source : "http";
|
|
16275
|
+
return {
|
|
16276
|
+
...v,
|
|
16277
|
+
clientId: v.clientId,
|
|
16278
|
+
sessionId: v.sessionId,
|
|
16279
|
+
name: typeof v.name === "string" ? v.name : v.clientId,
|
|
16280
|
+
registeredAt: typeof v.registeredAt === "string" ? v.registeredAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
16281
|
+
lastSeenAt: typeof v.lastSeenAt === "string" ? v.lastSeenAt : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
16282
|
+
pid: typeof v.pid === "number" && Number.isFinite(v.pid) ? v.pid : 0,
|
|
16283
|
+
source
|
|
16284
|
+
};
|
|
16285
|
+
}
|
|
15345
16286
|
async _readRegistry(opts) {
|
|
15346
16287
|
if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
|
|
15347
16288
|
return new Map(this._registryCache);
|
|
@@ -15351,7 +16292,12 @@ var GlobalMailbox = class {
|
|
|
15351
16292
|
const data = JSON.parse(raw);
|
|
15352
16293
|
const map = /* @__PURE__ */ new Map();
|
|
15353
16294
|
for (const [id, agent] of Object.entries(data)) {
|
|
15354
|
-
|
|
16295
|
+
const parsed = this._parseAgentEntry(agent);
|
|
16296
|
+
if (parsed) {
|
|
16297
|
+
map.set(id, parsed);
|
|
16298
|
+
} else {
|
|
16299
|
+
console.warn(JSON.stringify({ level: "warn", event: "mailbox.registry.invalid_agent", message: `Skipping malformed agent entry "${id}" in registry`, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
16300
|
+
}
|
|
15355
16301
|
}
|
|
15356
16302
|
this._registryCache = map;
|
|
15357
16303
|
this._registryCacheAt = Date.now();
|
|
@@ -15397,7 +16343,12 @@ var GlobalMailbox = class {
|
|
|
15397
16343
|
const data = JSON.parse(raw);
|
|
15398
16344
|
const map = /* @__PURE__ */ new Map();
|
|
15399
16345
|
for (const [id, client] of Object.entries(data)) {
|
|
15400
|
-
|
|
16346
|
+
const parsed = this._parseClientEntry(client);
|
|
16347
|
+
if (parsed) {
|
|
16348
|
+
map.set(id, parsed);
|
|
16349
|
+
} else {
|
|
16350
|
+
console.warn(JSON.stringify({ level: "warn", event: "mailbox.registry.invalid_client", message: `Skipping malformed client entry "${id}" in client registry`, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
16351
|
+
}
|
|
15401
16352
|
}
|
|
15402
16353
|
this._clientRegistryCache = map;
|
|
15403
16354
|
this._clientRegistryCacheAt = Date.now();
|
|
@@ -15457,10 +16408,10 @@ function makeMailboxTool(opts = {}) {
|
|
|
15457
16408
|
});
|
|
15458
16409
|
const agentId = opts.agentId ?? "leader";
|
|
15459
16410
|
const sessionId = opts.sessionId ?? "default";
|
|
15460
|
-
const shortHint = "Sub-commands: check (unread), send (
|
|
16411
|
+
const shortHint = "Sub-commands: check (unread), send (exact/base/@session/project broadcast), ack (read/complete), query (filter), status (all agents), online (active only), unread (count).";
|
|
15461
16412
|
return {
|
|
15462
16413
|
name: "mailbox",
|
|
15463
|
-
description:
|
|
16414
|
+
description: 'Low-level inter-agent mailbox with 7 actions (check, send, ack, query, status, online, unread). For most use cases, prefer the simpler `mail_send` and `mail_inbox` tools \u2014 they cover the common send/read operations with a cleaner interface. This tool is for advanced queries (filter by sender, sender session, priority, since-timestamp), agent status/online lists, and message ack/control. Use to="@session" only for session-local coordination; use "*" or a base alias when another session may be affected.',
|
|
15464
16415
|
usageHint: shortHint + " For simple send/read, use mail_send / mail_inbox instead.",
|
|
15465
16416
|
category: "Coordination",
|
|
15466
16417
|
permission: "auto",
|
|
@@ -15474,8 +16425,8 @@ function makeMailboxTool(opts = {}) {
|
|
|
15474
16425
|
enum: ["check", "send", "ack", "query", "status", "online", "unread"],
|
|
15475
16426
|
description: "Which mailbox operation to perform."
|
|
15476
16427
|
},
|
|
15477
|
-
to: { type: "string", description: "Recipient agent id, or '*' / 'all' for broadcast." },
|
|
15478
|
-
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "
|
|
16428
|
+
to: { type: "string", description: "Recipient agent id, base alias, '@session' for the sender's session, or '*' / 'all' for project broadcast." },
|
|
16429
|
+
type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Required message intent. Actionable: ask (blocking question), assign (task delegation, must have specific to), steer (mid-course direction), review (passive ask). Informational: note (general FYI), btw (low-priority aside), result (completion notice), status (system update). Routing: broadcast (multi-recipient). Reserved: control (runtime only, agents cannot send)." },
|
|
15479
16430
|
subject: { type: "string", description: "Short subject line." },
|
|
15480
16431
|
body: { type: "string", description: "Full message content." },
|
|
15481
16432
|
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
@@ -15490,6 +16441,7 @@ function makeMailboxTool(opts = {}) {
|
|
|
15490
16441
|
from: { type: "string", description: "Filter by sender." },
|
|
15491
16442
|
minPriority: { type: "string", enum: ["low", "normal", "high"] },
|
|
15492
16443
|
since: { type: "string", description: "ISO8601 timestamp \u2014 only messages after this." },
|
|
16444
|
+
sessionId: { type: "string", description: "Filter query results by sender session id." },
|
|
15493
16445
|
limit: { type: "number", description: "Max messages to return." }
|
|
15494
16446
|
},
|
|
15495
16447
|
required: ["action"]
|
|
@@ -15519,7 +16471,7 @@ function makeMailboxTool(opts = {}) {
|
|
|
15519
16471
|
}
|
|
15520
16472
|
switch (action) {
|
|
15521
16473
|
case "check":
|
|
15522
|
-
return executeCheck(mb, callerId, [baseCallerId], i);
|
|
16474
|
+
return executeCheck(mb, callerId, callerSessionId, [baseCallerId], i);
|
|
15523
16475
|
case "send":
|
|
15524
16476
|
return executeSend(mb, callerId, callerSessionId, i);
|
|
15525
16477
|
case "ack":
|
|
@@ -15531,19 +16483,23 @@ function makeMailboxTool(opts = {}) {
|
|
|
15531
16483
|
case "online":
|
|
15532
16484
|
return executeOnline(mb);
|
|
15533
16485
|
case "unread":
|
|
15534
|
-
return executeUnread(mb, callerId, [baseCallerId]);
|
|
16486
|
+
return executeUnread(mb, callerId, callerSessionId, [baseCallerId]);
|
|
15535
16487
|
default:
|
|
15536
16488
|
return { ok: false, error: `Unknown action: "${action}". Use check, send, ack, query, status, online, or unread.` };
|
|
15537
16489
|
}
|
|
15538
16490
|
}
|
|
15539
16491
|
};
|
|
15540
16492
|
}
|
|
15541
|
-
async function executeCheck(mb, agentId, aliases, i) {
|
|
16493
|
+
async function executeCheck(mb, agentId, sessionId, aliases, i) {
|
|
15542
16494
|
const limit = i.limit ?? 20;
|
|
15543
16495
|
const markRead = i.markRead ?? true;
|
|
15544
16496
|
const completed = i.completed ?? false;
|
|
15545
16497
|
const outcome = i.outcome;
|
|
15546
|
-
const targets = [
|
|
16498
|
+
const targets = [
|
|
16499
|
+
agentId,
|
|
16500
|
+
...aliases.filter((al) => al && al !== agentId),
|
|
16501
|
+
`@session:${sessionId}`
|
|
16502
|
+
];
|
|
15547
16503
|
const batches = await Promise.all(
|
|
15548
16504
|
targets.map(
|
|
15549
16505
|
(to) => mb.query({ to, unreadBy: agentId, limit, minPriority: "low" }).catch(() => [])
|
|
@@ -15571,7 +16527,7 @@ async function executeCheck(mb, agentId, aliases, i) {
|
|
|
15571
16527
|
summary: acked.length === 0 ? "No unread messages." : `${acked.length} unread message(s)${markRead ? " (marked read)" : ""}${completed ? " (completed)" : ""}.`
|
|
15572
16528
|
};
|
|
15573
16529
|
}
|
|
15574
|
-
async function executeSend(mb, agentId,
|
|
16530
|
+
async function executeSend(mb, agentId, sessionId, i) {
|
|
15575
16531
|
const to = i.to;
|
|
15576
16532
|
const tp = i.type;
|
|
15577
16533
|
const subject = i.subject;
|
|
@@ -15580,14 +16536,23 @@ async function executeSend(mb, agentId, _sessionId, i) {
|
|
|
15580
16536
|
if (!tp) return { ok: false, error: '"type" is required.' };
|
|
15581
16537
|
if (!subject) return { ok: false, error: '"subject" is required.' };
|
|
15582
16538
|
if (body === void 0 || body === null) return { ok: false, error: '"body" is required.' };
|
|
16539
|
+
let normalizedTo;
|
|
16540
|
+
try {
|
|
16541
|
+
normalizedTo = normalizeRecipient(to, sessionId);
|
|
16542
|
+
} catch (err) {
|
|
16543
|
+
return { ok: false, error: `"to" is invalid: ${toErrorMessage(err)}` };
|
|
16544
|
+
}
|
|
16545
|
+
const typeResult = resolveSendTypeSafe(tp, normalizedTo);
|
|
16546
|
+
if (!typeResult.ok) return { ok: false, error: `"type" is invalid: ${typeResult.error}` };
|
|
15583
16547
|
const msg = await mb.send({
|
|
15584
16548
|
from: agentId,
|
|
15585
|
-
to,
|
|
15586
|
-
type:
|
|
16549
|
+
to: normalizedTo,
|
|
16550
|
+
type: typeResult.type,
|
|
15587
16551
|
subject,
|
|
15588
16552
|
body,
|
|
15589
16553
|
priority: i.priority ?? "normal",
|
|
15590
|
-
replyTo: i.replyTo
|
|
16554
|
+
replyTo: i.replyTo,
|
|
16555
|
+
senderSessionId: sessionId
|
|
15591
16556
|
});
|
|
15592
16557
|
return {
|
|
15593
16558
|
ok: true,
|
|
@@ -15630,6 +16595,7 @@ async function executeQuery(mb, i) {
|
|
|
15630
16595
|
type: i.type,
|
|
15631
16596
|
minPriority: i.minPriority,
|
|
15632
16597
|
since: i.since,
|
|
16598
|
+
sessionId: i.sessionId,
|
|
15633
16599
|
limit
|
|
15634
16600
|
});
|
|
15635
16601
|
return { ok: true, count: messages.length, messages, summary: `${messages.length} message(s).` };
|
|
@@ -15676,8 +16642,12 @@ async function executeOnline(mb) {
|
|
|
15676
16642
|
summary: `${agents.length} online agent(s).`
|
|
15677
16643
|
};
|
|
15678
16644
|
}
|
|
15679
|
-
async function executeUnread(mb, agentId, aliases = []) {
|
|
15680
|
-
const targets = [
|
|
16645
|
+
async function executeUnread(mb, agentId, sessionId, aliases = []) {
|
|
16646
|
+
const targets = [
|
|
16647
|
+
agentId,
|
|
16648
|
+
...aliases.filter((al) => al && al !== agentId),
|
|
16649
|
+
`@session:${sessionId}`
|
|
16650
|
+
];
|
|
15681
16651
|
const batches = await Promise.all(
|
|
15682
16652
|
targets.map((to) => mb.query({ to, unreadBy: agentId, limit: 200 }).catch(() => []))
|
|
15683
16653
|
);
|
|
@@ -16357,6 +17327,591 @@ Idle workers: ${idleWorkerIds.join(", ")}`,
|
|
|
16357
17327
|
}
|
|
16358
17328
|
};
|
|
16359
17329
|
|
|
17330
|
+
// src/coordination/mailbox-http-router.ts
|
|
17331
|
+
import { timingSafeEqual } from "node:crypto";
|
|
17332
|
+
var MAILBOX_HTTP_MAX_BODY_BYTES = 256 * 1024;
|
|
17333
|
+
var MAILBOX_HTTP_RATE_LIMIT_PER_MINUTE = 120;
|
|
17334
|
+
var MAILBOX_HTTP_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
17335
|
+
function authorizeMailboxBearerToken(request, expectedToken) {
|
|
17336
|
+
const header = request.headers.authorization;
|
|
17337
|
+
if (typeof header !== "string") return { allowed: false };
|
|
17338
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
17339
|
+
if (match === null) return { allowed: false };
|
|
17340
|
+
const presented = Buffer.from(match[1] ?? "", "utf8");
|
|
17341
|
+
const expected = Buffer.from(expectedToken, "utf8");
|
|
17342
|
+
if (presented.length !== expected.length || !timingSafeEqual(presented, expected)) {
|
|
17343
|
+
return { allowed: false };
|
|
17344
|
+
}
|
|
17345
|
+
return { allowed: true, rateLimitKey: expectedToken };
|
|
17346
|
+
}
|
|
17347
|
+
var MailboxHttpRateLimiter = class {
|
|
17348
|
+
constructor(limit = MAILBOX_HTTP_RATE_LIMIT_PER_MINUTE, windowMs = MAILBOX_HTTP_RATE_LIMIT_WINDOW_MS) {
|
|
17349
|
+
this.limit = limit;
|
|
17350
|
+
this.windowMs = windowMs;
|
|
17351
|
+
}
|
|
17352
|
+
limit;
|
|
17353
|
+
windowMs;
|
|
17354
|
+
hits = /* @__PURE__ */ new Map();
|
|
17355
|
+
allow(key) {
|
|
17356
|
+
const now = Date.now();
|
|
17357
|
+
const cutoff = now - this.windowMs;
|
|
17358
|
+
const fresh = (this.hits.get(key) ?? []).filter((timestamp) => timestamp > cutoff);
|
|
17359
|
+
if (fresh.length >= this.limit) {
|
|
17360
|
+
this.hits.set(key, fresh);
|
|
17361
|
+
return false;
|
|
17362
|
+
}
|
|
17363
|
+
fresh.push(now);
|
|
17364
|
+
this.hits.set(key, fresh);
|
|
17365
|
+
return true;
|
|
17366
|
+
}
|
|
17367
|
+
cleanup() {
|
|
17368
|
+
const cutoff = Date.now() - this.windowMs;
|
|
17369
|
+
for (const [key, timestamps] of this.hits) {
|
|
17370
|
+
const fresh = timestamps.filter((timestamp) => timestamp > cutoff);
|
|
17371
|
+
if (fresh.length === 0) this.hits.delete(key);
|
|
17372
|
+
else this.hits.set(key, fresh);
|
|
17373
|
+
}
|
|
17374
|
+
}
|
|
17375
|
+
};
|
|
17376
|
+
function createMailboxHttpRouter(options) {
|
|
17377
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAILBOX_HTTP_MAX_BODY_BYTES;
|
|
17378
|
+
const closeSseStreams = /* @__PURE__ */ new Set();
|
|
17379
|
+
return {
|
|
17380
|
+
async handle(request, response, routePath) {
|
|
17381
|
+
try {
|
|
17382
|
+
const url = routePath ?? request.url ?? "/";
|
|
17383
|
+
const method = request.method ?? "GET";
|
|
17384
|
+
if (method === "GET" && url === "/healthz") {
|
|
17385
|
+
writeJson(response, 200, { ok: true });
|
|
17386
|
+
return;
|
|
17387
|
+
}
|
|
17388
|
+
const access2 = options.authorize ? await options.authorize(request) : { allowed: true };
|
|
17389
|
+
if (!access2.allowed) {
|
|
17390
|
+
const forwardedFor = request.headers["x-forwarded-for"];
|
|
17391
|
+
const clientIp = (Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor)?.split(",")[0]?.trim() ?? request.socket?.remoteAddress ?? "unknown";
|
|
17392
|
+
console.warn(JSON.stringify({
|
|
17393
|
+
level: "warn",
|
|
17394
|
+
event: "mailbox.http_auth_failure",
|
|
17395
|
+
message: `Mailbox HTTP auth rejected for ${request.method ?? "?"} ${request.url ?? "?"} from ${clientIp}`,
|
|
17396
|
+
method: request.method,
|
|
17397
|
+
url: request.url,
|
|
17398
|
+
clientIp,
|
|
17399
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
17400
|
+
}));
|
|
17401
|
+
writeJson(
|
|
17402
|
+
response,
|
|
17403
|
+
access2.status ?? 401,
|
|
17404
|
+
access2.body ?? {
|
|
17405
|
+
error: { code: "UNAUTHORIZED", message: "invalid or missing bearer token" }
|
|
17406
|
+
}
|
|
17407
|
+
);
|
|
17408
|
+
return;
|
|
17409
|
+
}
|
|
17410
|
+
if (options.rateLimiter && access2.rateLimitKey !== void 0 && !options.rateLimiter.allow(access2.rateLimitKey)) {
|
|
17411
|
+
writeJson(response, 429, {
|
|
17412
|
+
error: {
|
|
17413
|
+
code: "RATE_LIMITED",
|
|
17414
|
+
message: `rate limit exceeded: max ${options.rateLimiter.limit} requests per ${options.rateLimiter.windowMs / 1e3}s`
|
|
17415
|
+
}
|
|
17416
|
+
});
|
|
17417
|
+
return;
|
|
17418
|
+
}
|
|
17419
|
+
await dispatchMailboxRoute(
|
|
17420
|
+
options.mailbox,
|
|
17421
|
+
options.eventEmitter,
|
|
17422
|
+
request,
|
|
17423
|
+
response,
|
|
17424
|
+
method,
|
|
17425
|
+
url,
|
|
17426
|
+
maxBodyBytes,
|
|
17427
|
+
closeSseStreams
|
|
17428
|
+
);
|
|
17429
|
+
} catch (error) {
|
|
17430
|
+
const code = error instanceof MailboxHttpValidationError ? "VALIDATION_ERROR" : "INTERNAL_ERROR";
|
|
17431
|
+
const status = code === "VALIDATION_ERROR" ? 400 : 500;
|
|
17432
|
+
writeJson(response, status, {
|
|
17433
|
+
error: {
|
|
17434
|
+
code,
|
|
17435
|
+
message: error instanceof Error ? error.message : "unknown error"
|
|
17436
|
+
}
|
|
17437
|
+
});
|
|
17438
|
+
}
|
|
17439
|
+
},
|
|
17440
|
+
close() {
|
|
17441
|
+
for (const close of [...closeSseStreams]) close();
|
|
17442
|
+
closeSseStreams.clear();
|
|
17443
|
+
}
|
|
17444
|
+
};
|
|
17445
|
+
}
|
|
17446
|
+
async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, method, url, maxBodyBytes, closeSseStreams) {
|
|
17447
|
+
if (method === "POST" && url === "/mailbox/send") {
|
|
17448
|
+
const input = validateSend(await readJsonBody(request, maxBodyBytes));
|
|
17449
|
+
writeJson(response, 201, await mailbox.send(input));
|
|
17450
|
+
return;
|
|
17451
|
+
}
|
|
17452
|
+
if (method === "POST" && url === "/mailbox/query") {
|
|
17453
|
+
const messages = await mailbox.query(validateQuery(await readJsonBody(request, maxBodyBytes)));
|
|
17454
|
+
writeJson(response, 200, { data: messages, count: messages.length });
|
|
17455
|
+
return;
|
|
17456
|
+
}
|
|
17457
|
+
if (method === "POST" && url === "/mailbox/check") {
|
|
17458
|
+
const result = await checkMailbox(
|
|
17459
|
+
mailbox,
|
|
17460
|
+
validateCheck(await readJsonBody(request, maxBodyBytes))
|
|
17461
|
+
);
|
|
17462
|
+
writeJson(response, 200, result);
|
|
17463
|
+
return;
|
|
17464
|
+
}
|
|
17465
|
+
if (method === "POST" && url === "/mailbox/ack") {
|
|
17466
|
+
const updated = await mailbox.ack(validateAck(await readJsonBody(request, maxBodyBytes)));
|
|
17467
|
+
writeJson(response, 200, { updated });
|
|
17468
|
+
return;
|
|
17469
|
+
}
|
|
17470
|
+
if (method === "POST" && url === "/mailbox/ack-many") {
|
|
17471
|
+
const updated = await mailbox.ackMany(
|
|
17472
|
+
validateAckMany(await readJsonBody(request, maxBodyBytes))
|
|
17473
|
+
);
|
|
17474
|
+
writeJson(response, 200, { updated, count: updated.length });
|
|
17475
|
+
return;
|
|
17476
|
+
}
|
|
17477
|
+
if (method === "POST" && url === "/mailbox/unread-count") {
|
|
17478
|
+
const body = await readJsonBody(request, maxBodyBytes);
|
|
17479
|
+
writeJson(response, 200, { count: await mailbox.unreadCount(requireString(body, "forAgentId")) });
|
|
17480
|
+
return;
|
|
17481
|
+
}
|
|
17482
|
+
if (method === "POST" && url === "/mailbox/agents/register") {
|
|
17483
|
+
await mailbox.registerAgent(validateAgentRegistration(await readJsonBody(request, maxBodyBytes)));
|
|
17484
|
+
writeJson(response, 200, { ok: true });
|
|
17485
|
+
return;
|
|
17486
|
+
}
|
|
17487
|
+
if (method === "POST" && url === "/mailbox/agents/heartbeat") {
|
|
17488
|
+
await mailbox.heartbeat(validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes)));
|
|
17489
|
+
writeJson(response, 200, { ok: true });
|
|
17490
|
+
return;
|
|
17491
|
+
}
|
|
17492
|
+
if (method === "POST" && url === "/mailbox/register-client") {
|
|
17493
|
+
await mailbox.registerClient(validateClientRegistration(await readJsonBody(request, maxBodyBytes)));
|
|
17494
|
+
writeJson(response, 200, { ok: true });
|
|
17495
|
+
return;
|
|
17496
|
+
}
|
|
17497
|
+
if (method === "POST" && url === "/mailbox/heartbeat") {
|
|
17498
|
+
await mailbox.clientHeartbeat(validateClientHeartbeat(await readJsonBody(request, maxBodyBytes)));
|
|
17499
|
+
writeJson(response, 200, { ok: true });
|
|
17500
|
+
return;
|
|
17501
|
+
}
|
|
17502
|
+
if (method === "POST" && url === "/mailbox/purge-clients") {
|
|
17503
|
+
writeJson(response, 200, { ok: true, purged: await mailbox.purgeClients() });
|
|
17504
|
+
return;
|
|
17505
|
+
}
|
|
17506
|
+
if (method === "GET" && url === "/mailbox/agents") {
|
|
17507
|
+
const agents = await mailbox.getAgentStatuses();
|
|
17508
|
+
writeJson(response, 200, { data: agents, count: agents.length });
|
|
17509
|
+
return;
|
|
17510
|
+
}
|
|
17511
|
+
if (method === "GET" && url === "/mailbox/agents/online") {
|
|
17512
|
+
const agents = await mailbox.getOnlineAgents();
|
|
17513
|
+
writeJson(response, 200, { data: agents, count: agents.length });
|
|
17514
|
+
return;
|
|
17515
|
+
}
|
|
17516
|
+
if (method === "GET" && url === "/mailbox/events" && eventEmitter) {
|
|
17517
|
+
handleSse(request, response, eventEmitter, closeSseStreams);
|
|
17518
|
+
return;
|
|
17519
|
+
}
|
|
17520
|
+
writeJson(response, 404, {
|
|
17521
|
+
error: { code: "NOT_FOUND", message: `no route for ${method} ${url}` }
|
|
17522
|
+
});
|
|
17523
|
+
}
|
|
17524
|
+
function handleSse(request, response, eventEmitter, closeSseStreams) {
|
|
17525
|
+
response.writeHead(200, {
|
|
17526
|
+
"Content-Type": "text/event-stream",
|
|
17527
|
+
"Cache-Control": "no-store",
|
|
17528
|
+
Connection: "keep-alive",
|
|
17529
|
+
"X-Accel-Buffering": "no"
|
|
17530
|
+
});
|
|
17531
|
+
response.write(": connected\n\n");
|
|
17532
|
+
const unsubscribe = eventEmitter.subscribe((event) => {
|
|
17533
|
+
try {
|
|
17534
|
+
response.write(`data: ${JSON.stringify(event)}
|
|
17535
|
+
|
|
17536
|
+
`);
|
|
17537
|
+
} catch {
|
|
17538
|
+
unsubscribe();
|
|
17539
|
+
}
|
|
17540
|
+
});
|
|
17541
|
+
const keepAlive = setInterval(() => {
|
|
17542
|
+
try {
|
|
17543
|
+
response.write(": keepalive\n\n");
|
|
17544
|
+
} catch {
|
|
17545
|
+
clearInterval(keepAlive);
|
|
17546
|
+
}
|
|
17547
|
+
}, 15e3);
|
|
17548
|
+
let closed = false;
|
|
17549
|
+
const close = () => {
|
|
17550
|
+
if (closed) return;
|
|
17551
|
+
closed = true;
|
|
17552
|
+
clearInterval(keepAlive);
|
|
17553
|
+
unsubscribe();
|
|
17554
|
+
closeSseStreams.delete(close);
|
|
17555
|
+
try {
|
|
17556
|
+
response.end();
|
|
17557
|
+
} catch {
|
|
17558
|
+
}
|
|
17559
|
+
};
|
|
17560
|
+
closeSseStreams.add(close);
|
|
17561
|
+
request.once("close", close);
|
|
17562
|
+
}
|
|
17563
|
+
async function readJsonBody(request, maxBodyBytes) {
|
|
17564
|
+
const lengthHeader = request.headers["content-length"];
|
|
17565
|
+
if (typeof lengthHeader === "string") {
|
|
17566
|
+
const declared = Number.parseInt(lengthHeader, 10);
|
|
17567
|
+
if (Number.isInteger(declared) && declared > maxBodyBytes) {
|
|
17568
|
+
throw validationError(`request body too large: ${declared} bytes (max ${maxBodyBytes})`);
|
|
17569
|
+
}
|
|
17570
|
+
}
|
|
17571
|
+
const chunks = [];
|
|
17572
|
+
let total = 0;
|
|
17573
|
+
for await (const chunk of request) {
|
|
17574
|
+
const buffer = chunk;
|
|
17575
|
+
total += buffer.length;
|
|
17576
|
+
if (total > maxBodyBytes) {
|
|
17577
|
+
throw validationError(`request body too large: > ${maxBodyBytes} bytes`);
|
|
17578
|
+
}
|
|
17579
|
+
chunks.push(buffer);
|
|
17580
|
+
}
|
|
17581
|
+
if (total === 0) return {};
|
|
17582
|
+
try {
|
|
17583
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
17584
|
+
} catch (error) {
|
|
17585
|
+
throw validationError(
|
|
17586
|
+
`invalid JSON body: ${error instanceof Error ? error.message : String(error)}`
|
|
17587
|
+
);
|
|
17588
|
+
}
|
|
17589
|
+
}
|
|
17590
|
+
function writeJson(response, status, body) {
|
|
17591
|
+
response.writeHead(status, {
|
|
17592
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
17593
|
+
"Cache-Control": "no-store"
|
|
17594
|
+
});
|
|
17595
|
+
response.end(JSON.stringify(body));
|
|
17596
|
+
}
|
|
17597
|
+
var MailboxHttpValidationError = class extends Error {
|
|
17598
|
+
};
|
|
17599
|
+
function validationError(message) {
|
|
17600
|
+
return new MailboxHttpValidationError(message);
|
|
17601
|
+
}
|
|
17602
|
+
function requireString(object, key) {
|
|
17603
|
+
if (typeof object !== "object" || object === null) {
|
|
17604
|
+
throw validationError("expected JSON object body");
|
|
17605
|
+
}
|
|
17606
|
+
const value = object[key];
|
|
17607
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
17608
|
+
throw validationError(`field "${key}" is required (string)`);
|
|
17609
|
+
}
|
|
17610
|
+
return value;
|
|
17611
|
+
}
|
|
17612
|
+
function requireNumber(object, key) {
|
|
17613
|
+
if (typeof object !== "object" || object === null) {
|
|
17614
|
+
throw validationError("expected JSON object body");
|
|
17615
|
+
}
|
|
17616
|
+
const value = object[key];
|
|
17617
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
17618
|
+
throw validationError(`field "${key}" is required (integer)`);
|
|
17619
|
+
}
|
|
17620
|
+
return value;
|
|
17621
|
+
}
|
|
17622
|
+
function optionalString3(object, key) {
|
|
17623
|
+
if (typeof object !== "object" || object === null) return void 0;
|
|
17624
|
+
const value = object[key];
|
|
17625
|
+
if (value === void 0) return void 0;
|
|
17626
|
+
if (value === null) {
|
|
17627
|
+
throw validationError(`field "${key}" must not be null when present`);
|
|
17628
|
+
}
|
|
17629
|
+
if (typeof value !== "string") {
|
|
17630
|
+
throw validationError(`field "${key}" must be a string when present`);
|
|
17631
|
+
}
|
|
17632
|
+
return value;
|
|
17633
|
+
}
|
|
17634
|
+
function optionalNumber(object, key) {
|
|
17635
|
+
if (typeof object !== "object" || object === null) return void 0;
|
|
17636
|
+
const value = object[key];
|
|
17637
|
+
if (value === void 0 || value === null) return void 0;
|
|
17638
|
+
if (typeof value !== "number") {
|
|
17639
|
+
throw validationError(`field "${key}" must be a number when present`);
|
|
17640
|
+
}
|
|
17641
|
+
return value;
|
|
17642
|
+
}
|
|
17643
|
+
function optionalBoolean(object, key) {
|
|
17644
|
+
if (typeof object !== "object" || object === null) return void 0;
|
|
17645
|
+
const value = object[key];
|
|
17646
|
+
if (value === void 0 || value === null) return void 0;
|
|
17647
|
+
if (typeof value !== "boolean") {
|
|
17648
|
+
throw validationError(`field "${key}" must be a boolean when present`);
|
|
17649
|
+
}
|
|
17650
|
+
return value;
|
|
17651
|
+
}
|
|
17652
|
+
var VALID_TYPES = new Set(Object.keys(MAILBOX_TYPE_PROPERTIES));
|
|
17653
|
+
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
|
|
17654
|
+
var RESERVED_FROM_IDS = /* @__PURE__ */ new Set([
|
|
17655
|
+
"leader",
|
|
17656
|
+
"fleet",
|
|
17657
|
+
"hq",
|
|
17658
|
+
"mailbox-bridge",
|
|
17659
|
+
"mailbox-bridge-watchdog",
|
|
17660
|
+
"tech-stack-consumer",
|
|
17661
|
+
"*"
|
|
17662
|
+
]);
|
|
17663
|
+
var RESERVED_READER_IDS = /* @__PURE__ */ new Set([
|
|
17664
|
+
"leader",
|
|
17665
|
+
"fleet",
|
|
17666
|
+
"hq",
|
|
17667
|
+
"mailbox-bridge",
|
|
17668
|
+
"mailbox-bridge-watchdog",
|
|
17669
|
+
"tech-stack-consumer"
|
|
17670
|
+
]);
|
|
17671
|
+
function validateReaderId(id) {
|
|
17672
|
+
const base = id.split("@")[0].toLowerCase();
|
|
17673
|
+
if (RESERVED_READER_IDS.has(base)) {
|
|
17674
|
+
throw validationError(`"readerId" must not use reserved internal agent id "${base}"`);
|
|
17675
|
+
}
|
|
17676
|
+
}
|
|
17677
|
+
function validateSend(body) {
|
|
17678
|
+
if (typeof body !== "object" || body === null) {
|
|
17679
|
+
throw validationError("expected JSON object body");
|
|
17680
|
+
}
|
|
17681
|
+
const object = body;
|
|
17682
|
+
const type = requireString(object, "type");
|
|
17683
|
+
if (!VALID_TYPES.has(type)) {
|
|
17684
|
+
throw validationError(`field "type" must be one of ${[...VALID_TYPES].join(", ")}`);
|
|
17685
|
+
}
|
|
17686
|
+
const priority = optionalString3(object, "priority");
|
|
17687
|
+
if (priority !== void 0 && !VALID_PRIORITIES.has(priority)) {
|
|
17688
|
+
throw validationError(`field "priority" must be one of ${[...VALID_PRIORITIES].join(", ")}`);
|
|
17689
|
+
}
|
|
17690
|
+
const from = requireString(object, "from");
|
|
17691
|
+
const fromBase = from.split("@")[0].toLowerCase();
|
|
17692
|
+
if (RESERVED_FROM_IDS.has(fromBase)) {
|
|
17693
|
+
throw validationError(
|
|
17694
|
+
`field "from" must not use reserved internal agent id "${fromBase}" \u2014 external agents must use their own identity`
|
|
17695
|
+
);
|
|
17696
|
+
}
|
|
17697
|
+
const to = normalizeRecipient(requireString(object, "to"));
|
|
17698
|
+
try {
|
|
17699
|
+
resolveSendType(type, to);
|
|
17700
|
+
} catch (err) {
|
|
17701
|
+
throw validationError(`field "type" is invalid: ${err.message}`);
|
|
17702
|
+
}
|
|
17703
|
+
const result = {
|
|
17704
|
+
from,
|
|
17705
|
+
to,
|
|
17706
|
+
type,
|
|
17707
|
+
subject: requireString(object, "subject"),
|
|
17708
|
+
body: requireString(object, "body"),
|
|
17709
|
+
priority
|
|
17710
|
+
};
|
|
17711
|
+
const replyTo = optionalString3(object, "replyTo");
|
|
17712
|
+
if (replyTo !== void 0) result.replyTo = replyTo;
|
|
17713
|
+
const ttlMs = optionalNumber(object, "ttlMs");
|
|
17714
|
+
if (ttlMs !== void 0) {
|
|
17715
|
+
if (!Number.isInteger(ttlMs) || ttlMs < 1) {
|
|
17716
|
+
throw validationError('field "ttlMs" must be a positive integer when present');
|
|
17717
|
+
}
|
|
17718
|
+
result.ttlMs = ttlMs;
|
|
17719
|
+
}
|
|
17720
|
+
return result;
|
|
17721
|
+
}
|
|
17722
|
+
function validateQuery(body) {
|
|
17723
|
+
if (typeof body !== "object" || body === null) {
|
|
17724
|
+
throw validationError("expected JSON object body");
|
|
17725
|
+
}
|
|
17726
|
+
const object = body;
|
|
17727
|
+
const result = {};
|
|
17728
|
+
const to = optionalString3(object, "to");
|
|
17729
|
+
const from = optionalString3(object, "from");
|
|
17730
|
+
const unreadBy = optionalString3(object, "unreadBy");
|
|
17731
|
+
const type = optionalString3(object, "type");
|
|
17732
|
+
const minPriority = optionalString3(object, "minPriority");
|
|
17733
|
+
const since = optionalString3(object, "since");
|
|
17734
|
+
const limit = optionalNumber(object, "limit");
|
|
17735
|
+
if (limit !== void 0) {
|
|
17736
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
17737
|
+
throw validationError('field "limit" must be a positive integer when present');
|
|
17738
|
+
}
|
|
17739
|
+
}
|
|
17740
|
+
const incompleteOnly = optionalBoolean(object, "incompleteOnly");
|
|
17741
|
+
if (to !== void 0) result.to = to;
|
|
17742
|
+
if (from !== void 0) result.from = from;
|
|
17743
|
+
if (unreadBy !== void 0) result.unreadBy = unreadBy;
|
|
17744
|
+
if (type !== void 0) {
|
|
17745
|
+
if (!VALID_TYPES.has(type)) {
|
|
17746
|
+
throw validationError(`field "type" must be one of ${[...VALID_TYPES].join(", ")}`);
|
|
17747
|
+
}
|
|
17748
|
+
result.type = type;
|
|
17749
|
+
}
|
|
17750
|
+
if (minPriority !== void 0) {
|
|
17751
|
+
if (!VALID_PRIORITIES.has(minPriority)) {
|
|
17752
|
+
throw validationError(
|
|
17753
|
+
`field "minPriority" must be one of ${[...VALID_PRIORITIES].join(", ")}`
|
|
17754
|
+
);
|
|
17755
|
+
}
|
|
17756
|
+
result.minPriority = minPriority;
|
|
17757
|
+
}
|
|
17758
|
+
if (since !== void 0) result.since = since;
|
|
17759
|
+
if (limit !== void 0) result.limit = limit;
|
|
17760
|
+
if (incompleteOnly !== void 0) result.incompleteOnly = incompleteOnly;
|
|
17761
|
+
return result;
|
|
17762
|
+
}
|
|
17763
|
+
async function checkMailbox(mailbox, input) {
|
|
17764
|
+
const limit = input.limit ?? 20;
|
|
17765
|
+
const markRead = input.markRead ?? true;
|
|
17766
|
+
const completed = input.completed ?? false;
|
|
17767
|
+
const targets = input.baseId !== void 0 && input.baseId !== input.agentId ? [input.agentId, input.baseId] : [input.agentId];
|
|
17768
|
+
const batches = await Promise.all(
|
|
17769
|
+
targets.map((to) => mailbox.query({ to, unreadBy: input.agentId, limit }))
|
|
17770
|
+
);
|
|
17771
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17772
|
+
const messages = batches.flat().filter((message) => {
|
|
17773
|
+
if (seen.has(message.id) || message.from === input.agentId) return false;
|
|
17774
|
+
seen.add(message.id);
|
|
17775
|
+
return true;
|
|
17776
|
+
}).slice(0, limit);
|
|
17777
|
+
const data = markRead || completed ? await mailbox.ackMany({
|
|
17778
|
+
acks: messages.map((message) => ({
|
|
17779
|
+
messageId: message.id,
|
|
17780
|
+
readerId: input.agentId,
|
|
17781
|
+
read: markRead,
|
|
17782
|
+
completed,
|
|
17783
|
+
outcome: completed ? input.outcome : void 0
|
|
17784
|
+
}))
|
|
17785
|
+
}) : messages;
|
|
17786
|
+
return { data, count: data.length };
|
|
17787
|
+
}
|
|
17788
|
+
function validateCheck(body) {
|
|
17789
|
+
if (typeof body !== "object" || body === null) {
|
|
17790
|
+
throw validationError("expected JSON object body");
|
|
17791
|
+
}
|
|
17792
|
+
const object = body;
|
|
17793
|
+
const result = { agentId: requireString(object, "agentId") };
|
|
17794
|
+
const baseId = optionalString3(object, "baseId");
|
|
17795
|
+
const limit = optionalNumber(object, "limit");
|
|
17796
|
+
const markRead = optionalBoolean(object, "markRead");
|
|
17797
|
+
const completed = optionalBoolean(object, "completed");
|
|
17798
|
+
const outcome = optionalString3(object, "outcome");
|
|
17799
|
+
if (baseId !== void 0) result.baseId = baseId;
|
|
17800
|
+
if (limit !== void 0) {
|
|
17801
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
17802
|
+
throw validationError('field "limit" must be a positive integer when present');
|
|
17803
|
+
}
|
|
17804
|
+
result.limit = limit;
|
|
17805
|
+
}
|
|
17806
|
+
if (markRead !== void 0) result.markRead = markRead;
|
|
17807
|
+
if (completed !== void 0) result.completed = completed;
|
|
17808
|
+
if (outcome !== void 0) result.outcome = outcome;
|
|
17809
|
+
return result;
|
|
17810
|
+
}
|
|
17811
|
+
function validateAck(body) {
|
|
17812
|
+
if (typeof body !== "object" || body === null) {
|
|
17813
|
+
throw validationError("expected JSON object body");
|
|
17814
|
+
}
|
|
17815
|
+
const object = body;
|
|
17816
|
+
const readerId = requireString(object, "readerId");
|
|
17817
|
+
validateReaderId(readerId);
|
|
17818
|
+
const result = {
|
|
17819
|
+
messageId: requireString(object, "messageId"),
|
|
17820
|
+
readerId
|
|
17821
|
+
};
|
|
17822
|
+
const read = optionalBoolean(object, "read");
|
|
17823
|
+
const completed = optionalBoolean(object, "completed");
|
|
17824
|
+
const outcome = optionalString3(object, "outcome");
|
|
17825
|
+
if (read !== void 0) result.read = read;
|
|
17826
|
+
if (completed !== void 0) result.completed = completed;
|
|
17827
|
+
if (outcome !== void 0) result.outcome = outcome;
|
|
17828
|
+
return result;
|
|
17829
|
+
}
|
|
17830
|
+
function validateAckMany(body) {
|
|
17831
|
+
if (typeof body !== "object" || body === null) {
|
|
17832
|
+
throw validationError("expected JSON object body");
|
|
17833
|
+
}
|
|
17834
|
+
const raw = body["acks"];
|
|
17835
|
+
if (!Array.isArray(raw)) throw validationError('field "acks" is required (array)');
|
|
17836
|
+
return { acks: raw.map((entry) => validateAck(entry)) };
|
|
17837
|
+
}
|
|
17838
|
+
function validateAgentRegistration(body) {
|
|
17839
|
+
if (typeof body !== "object" || body === null) {
|
|
17840
|
+
throw validationError("expected JSON object body");
|
|
17841
|
+
}
|
|
17842
|
+
const object = body;
|
|
17843
|
+
const agentId = requireString(object, "agentId");
|
|
17844
|
+
validateReaderId(agentId);
|
|
17845
|
+
const pid = requireNumber(object, "pid");
|
|
17846
|
+
if (!Number.isInteger(pid) || pid < 1) {
|
|
17847
|
+
throw validationError('field "pid" must be a positive integer');
|
|
17848
|
+
}
|
|
17849
|
+
const result = {
|
|
17850
|
+
agentId,
|
|
17851
|
+
sessionId: optionalString3(object, "sessionId") ?? "external",
|
|
17852
|
+
name: requireString(object, "name"),
|
|
17853
|
+
pid,
|
|
17854
|
+
source: "http"
|
|
17855
|
+
};
|
|
17856
|
+
const role = optionalString3(object, "role");
|
|
17857
|
+
if (role !== void 0) result.role = role;
|
|
17858
|
+
return result;
|
|
17859
|
+
}
|
|
17860
|
+
function validateAgentHeartbeat(body) {
|
|
17861
|
+
if (typeof body !== "object" || body === null) {
|
|
17862
|
+
throw validationError("expected JSON object body");
|
|
17863
|
+
}
|
|
17864
|
+
const object = body;
|
|
17865
|
+
const result = { agentId: requireString(object, "agentId") };
|
|
17866
|
+
const status = optionalString3(object, "status");
|
|
17867
|
+
const currentTool = optionalString3(object, "currentTool");
|
|
17868
|
+
const currentTask = optionalString3(object, "currentTask");
|
|
17869
|
+
const iterations = optionalNumber(object, "iterations");
|
|
17870
|
+
const toolCalls = optionalNumber(object, "toolCalls");
|
|
17871
|
+
if (status !== void 0) result.status = status;
|
|
17872
|
+
if (currentTool !== void 0) result.currentTool = currentTool;
|
|
17873
|
+
if (currentTask !== void 0) result.currentTask = currentTask;
|
|
17874
|
+
if (iterations !== void 0) {
|
|
17875
|
+
if (!Number.isInteger(iterations) || iterations < 0) {
|
|
17876
|
+
throw validationError('field "iterations" must be a non-negative integer when present');
|
|
17877
|
+
}
|
|
17878
|
+
result.iterations = iterations;
|
|
17879
|
+
}
|
|
17880
|
+
if (toolCalls !== void 0) {
|
|
17881
|
+
if (!Number.isInteger(toolCalls) || toolCalls < 0) {
|
|
17882
|
+
throw validationError('field "toolCalls" must be a non-negative integer when present');
|
|
17883
|
+
}
|
|
17884
|
+
result.toolCalls = toolCalls;
|
|
17885
|
+
}
|
|
17886
|
+
return result;
|
|
17887
|
+
}
|
|
17888
|
+
function validateClientRegistration(body) {
|
|
17889
|
+
if (typeof body !== "object" || body === null) {
|
|
17890
|
+
throw validationError("expected JSON object body");
|
|
17891
|
+
}
|
|
17892
|
+
const object = body;
|
|
17893
|
+
const pid = requireNumber(object, "pid");
|
|
17894
|
+
if (!Number.isInteger(pid) || pid < 1) {
|
|
17895
|
+
throw validationError('field "pid" must be a positive integer');
|
|
17896
|
+
}
|
|
17897
|
+
return {
|
|
17898
|
+
clientId: requireString(object, "clientId"),
|
|
17899
|
+
sessionId: optionalString3(object, "sessionId") ?? "external",
|
|
17900
|
+
name: requireString(object, "name"),
|
|
17901
|
+
source: "http",
|
|
17902
|
+
pid
|
|
17903
|
+
};
|
|
17904
|
+
}
|
|
17905
|
+
function validateClientHeartbeat(body) {
|
|
17906
|
+
if (typeof body !== "object" || body === null) {
|
|
17907
|
+
throw validationError("expected JSON object body");
|
|
17908
|
+
}
|
|
17909
|
+
const object = body;
|
|
17910
|
+
const clientId = requireString(object, "clientId");
|
|
17911
|
+
const sessionId = optionalString3(object, "sessionId");
|
|
17912
|
+
return sessionId ? { clientId, sessionId } : { clientId };
|
|
17913
|
+
}
|
|
17914
|
+
|
|
16360
17915
|
// src/coordination/mail-tools.ts
|
|
16361
17916
|
function makeResolver(opts) {
|
|
16362
17917
|
return opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
|
|
@@ -16381,7 +17936,7 @@ function makeMailSendTool(opts = {}) {
|
|
|
16381
17936
|
const resolveMailbox = makeResolver(opts);
|
|
16382
17937
|
return {
|
|
16383
17938
|
name: "mail_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
|
|
17939
|
+
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. to="*" broadcasts to everyone; to="@session" reaches agents in your current session; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Use project-wide scope (`*` or a base alias) whenever another session may be affected. Recipients see your mail automatically before their next step. \n\n\u2500\u2500 TYPE SEMANTICS \u2500\u2500\nThe `type` parameter determines how the recipient must handle your message:\n actionable (requires a response):\n ask \u2014 blocking question, sender waits for an answer\n assign \u2014 task delegation, act when current op allows (requires specific to, NOT "*")\n steer \u2014 mid-task direction change, recipient adjusts course NOW\n review \u2014 passive ask, inspect when convenient, no reply needed\n informational (consume for context):\n note \u2014 general FYI (default for directed sends)\n btw \u2014 low-priority aside, absorb and stay on task\n result \u2014 subagent completion notice, factor into next decision\n status \u2014 agent/system status update, avoid redundant work\n broadcast \u2014 multi-recipient envelope (default when to="*" or "@session")\n control (reserved for runtime use \u2014 agents cannot send this type)\n\nWhen no `type` is provided: broadcast for `*`/`@session`, otherwise note.\nassign/steer with to="*" is rejected (ambiguous). control is rejected (runtime-reserved).\n\nType determines dispatch: steer renders first, control is out-of-band, actionable types show "Action required" footer.',
|
|
16385
17940
|
usageHint: 'mail_send to="<id>" type="review" body="please skim <file>"',
|
|
16386
17941
|
category: "Coordination",
|
|
16387
17942
|
permission: "auto",
|
|
@@ -16392,7 +17947,7 @@ function makeMailSendTool(opts = {}) {
|
|
|
16392
17947
|
properties: {
|
|
16393
17948
|
to: {
|
|
16394
17949
|
type: "string",
|
|
16395
|
-
description: 'Recipient: exact agent id ("leader@a1b2c3d4"), base alias ("leader"), or "*" / "all" for everyone.'
|
|
17950
|
+
description: 'Recipient: exact agent id ("leader@a1b2c3d4"), base alias ("leader"), "@session" for your current session, or "*" / "all" for everyone.'
|
|
16396
17951
|
},
|
|
16397
17952
|
subject: { type: "string", description: "Short subject line." },
|
|
16398
17953
|
body: { type: "string", description: "The message." },
|
|
@@ -16414,18 +17969,22 @@ function makeMailSendTool(opts = {}) {
|
|
|
16414
17969
|
if (!rawTo || !subject || body === void 0 || body === null) {
|
|
16415
17970
|
return { ok: false, error: '"to", "subject" and "body" are required.' };
|
|
16416
17971
|
}
|
|
16417
|
-
const to = normalizeRecipient(rawTo);
|
|
16418
17972
|
const mb = resolveMailbox(ctx);
|
|
16419
17973
|
const identity = await register(mb, ctx);
|
|
16420
|
-
const
|
|
17974
|
+
const to = normalizeRecipient(rawTo, identity.sessionId);
|
|
17975
|
+
const resolvedType = resolveSendType(
|
|
17976
|
+
i.type,
|
|
17977
|
+
to
|
|
17978
|
+
);
|
|
16421
17979
|
const msg = await mb.send({
|
|
16422
17980
|
from: identity.callerId,
|
|
16423
17981
|
to,
|
|
16424
|
-
type,
|
|
17982
|
+
type: resolvedType,
|
|
16425
17983
|
subject,
|
|
16426
17984
|
body,
|
|
16427
17985
|
priority: i.priority ?? "normal",
|
|
16428
|
-
replyTo: i.replyTo
|
|
17986
|
+
replyTo: i.replyTo,
|
|
17987
|
+
senderSessionId: identity.sessionId
|
|
16429
17988
|
});
|
|
16430
17989
|
return {
|
|
16431
17990
|
ok: true,
|
|
@@ -16441,7 +18000,7 @@ function makeMailInboxTool(opts = {}) {
|
|
|
16441
18000
|
const resolveMailbox = makeResolver(opts);
|
|
16442
18001
|
return {
|
|
16443
18002
|
name: "mail_inbox",
|
|
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.',
|
|
18003
|
+
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"), to your current session, and project 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.',
|
|
16445
18004
|
usageHint: 'mail_inbox (optionally: limit=10, markRead=false to peek, completed=true outcome="handled")',
|
|
16446
18005
|
category: "Coordination",
|
|
16447
18006
|
permission: "auto",
|
|
@@ -16475,6 +18034,7 @@ function makeMailInboxTool(opts = {}) {
|
|
|
16475
18034
|
const identity = await register(mb, ctx);
|
|
16476
18035
|
const targets = [identity.callerId];
|
|
16477
18036
|
if (identity.baseId !== identity.callerId) targets.push(identity.baseId);
|
|
18037
|
+
targets.push(`@session:${identity.sessionId}`);
|
|
16478
18038
|
const batches = await Promise.all(
|
|
16479
18039
|
targets.map(
|
|
16480
18040
|
(to) => mb.query({ to, unreadBy: identity.callerId, limit }).catch(() => [])
|
|
@@ -16555,9 +18115,8 @@ var DefaultMailbox = class {
|
|
|
16555
18115
|
const msg = {
|
|
16556
18116
|
id: randomUUID12(),
|
|
16557
18117
|
from: input.from,
|
|
16558
|
-
//
|
|
16559
|
-
|
|
16560
|
-
to: normalizeRecipient(input.to),
|
|
18118
|
+
// Resolve project/session aliases before persisting the message.
|
|
18119
|
+
to: normalizeRecipient(input.to, input.senderSessionId),
|
|
16561
18120
|
type: normalizeMailboxMessageType(input.type),
|
|
16562
18121
|
subject: input.subject,
|
|
16563
18122
|
body: input.body,
|
|
@@ -16566,7 +18125,8 @@ var DefaultMailbox = class {
|
|
|
16566
18125
|
completed: false,
|
|
16567
18126
|
timestamp: now,
|
|
16568
18127
|
replyTo: input.replyTo,
|
|
16569
|
-
taskContext: input.taskContext
|
|
18128
|
+
taskContext: input.taskContext,
|
|
18129
|
+
senderSessionId: input.senderSessionId
|
|
16570
18130
|
};
|
|
16571
18131
|
const line = JSON.stringify(msg) + LINE_SEPARATOR2;
|
|
16572
18132
|
await fsp12.mkdir(path15.dirname(this.filePath), { recursive: true });
|
|
@@ -16610,6 +18170,7 @@ var DefaultMailbox = class {
|
|
|
16610
18170
|
const passes = (msg) => {
|
|
16611
18171
|
if (q.to !== void 0 && msg.to !== q.to && msg.to !== "*") return false;
|
|
16612
18172
|
if (q.from !== void 0 && msg.from !== q.from) return false;
|
|
18173
|
+
if (q.sessionId !== void 0 && msg.senderSessionId !== q.sessionId) return false;
|
|
16613
18174
|
if (q.unreadBy !== void 0 && q.unreadBy in msg.readBy) return false;
|
|
16614
18175
|
if (q.incompleteOnly && msg.completed) return false;
|
|
16615
18176
|
if (queryType !== void 0 && msg.type !== queryType) return false;
|
|
@@ -16731,10 +18292,11 @@ var DefaultMailbox = class {
|
|
|
16731
18292
|
}
|
|
16732
18293
|
async heartbeat(_input) {
|
|
16733
18294
|
}
|
|
16734
|
-
async unreadCount(forAgentId) {
|
|
18295
|
+
async unreadCount(forAgentId, sessionId) {
|
|
16735
18296
|
const all = await this._readAllCached();
|
|
18297
|
+
const scopedSessionRecipient = sessionId === void 0 ? void 0 : sessionRecipient(sessionId);
|
|
16736
18298
|
return all.filter(
|
|
16737
|
-
(m) => (m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed
|
|
18299
|
+
(m) => (m.to === forAgentId || m.to === "*" || m.to === scopedSessionRecipient) && !(forAgentId in m.readBy) && !m.completed
|
|
16738
18300
|
).length;
|
|
16739
18301
|
}
|
|
16740
18302
|
async close() {
|
|
@@ -17213,7 +18775,7 @@ function validateWatchdogOptions(cfg) {
|
|
|
17213
18775
|
|
|
17214
18776
|
// src/coordination/mailbox-hooks.ts
|
|
17215
18777
|
function createMailboxHooks(opts) {
|
|
17216
|
-
const { mailbox, agentId, notifyNewMail = true, heartbeat = true } = opts;
|
|
18778
|
+
const { mailbox, agentId, sessionId, notifyNewMail = true, heartbeat = true } = opts;
|
|
17217
18779
|
let lastUnreadCount = -1;
|
|
17218
18780
|
return {
|
|
17219
18781
|
/**
|
|
@@ -17222,7 +18784,7 @@ function createMailboxHooks(opts) {
|
|
|
17222
18784
|
*/
|
|
17223
18785
|
async beforeTool(events) {
|
|
17224
18786
|
try {
|
|
17225
|
-
const count = await mailbox.unreadCount(agentId);
|
|
18787
|
+
const count = await mailbox.unreadCount(agentId, sessionId);
|
|
17226
18788
|
if (notifyNewMail && count !== lastUnreadCount) {
|
|
17227
18789
|
lastUnreadCount = count;
|
|
17228
18790
|
events.emit("mailbox.unread_count", { agentId, count });
|
|
@@ -17279,10 +18841,8 @@ async function loadLog(storageDir, projectRoot) {
|
|
|
17279
18841
|
}
|
|
17280
18842
|
}
|
|
17281
18843
|
async function saveLog(storageDir, log) {
|
|
17282
|
-
await
|
|
17283
|
-
|
|
17284
|
-
await fs3.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
|
|
17285
|
-
await fs3.rename(tmp, logPath(storageDir));
|
|
18844
|
+
await atomicWrite(logPath(storageDir), `${JSON.stringify(log, null, 2)}
|
|
18845
|
+
`);
|
|
17286
18846
|
}
|
|
17287
18847
|
function detectEcosystem(manifestPath) {
|
|
17288
18848
|
const name = path16.win32.basename(manifestPath).toLowerCase();
|
|
@@ -17677,6 +19237,477 @@ async function probeHealthz(url) {
|
|
|
17677
19237
|
}
|
|
17678
19238
|
}
|
|
17679
19239
|
|
|
19240
|
+
// src/coordination/provider-status-tracker.ts
|
|
19241
|
+
var DEFAULTS2 = {
|
|
19242
|
+
degradedAfterFailures: 2,
|
|
19243
|
+
degradedDurationMs: 3e4,
|
|
19244
|
+
blockAfterRateLimitHits: 3,
|
|
19245
|
+
blockAfterFailures: 5,
|
|
19246
|
+
blockDurationMs: 3e5,
|
|
19247
|
+
quotaBlockDurationMs: 9e5,
|
|
19248
|
+
recoverAfterSuccesses: 3,
|
|
19249
|
+
maxErrorHistory: 50
|
|
19250
|
+
};
|
|
19251
|
+
var ProviderModelStatusTracker = class {
|
|
19252
|
+
cfg;
|
|
19253
|
+
map = /* @__PURE__ */ new Map();
|
|
19254
|
+
events;
|
|
19255
|
+
constructor(opts) {
|
|
19256
|
+
this.cfg = { ...DEFAULTS2, ...opts?.config };
|
|
19257
|
+
this.events = opts?.events;
|
|
19258
|
+
}
|
|
19259
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
19260
|
+
/** Resolve the logical health/display identity without changing wire routing. */
|
|
19261
|
+
logicalIdentity(providerId, model) {
|
|
19262
|
+
return statusIdentity(providerId, model);
|
|
19263
|
+
}
|
|
19264
|
+
/**
|
|
19265
|
+
* Record a successful provider call. Resets consecutive failure counters
|
|
19266
|
+
* and may transition out of degraded/blocked.
|
|
19267
|
+
*/
|
|
19268
|
+
recordSuccess(providerId, model, _meta) {
|
|
19269
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19270
|
+
const key = pairKey(providerId, model);
|
|
19271
|
+
const s = this.getOrCreate(key, providerId, model);
|
|
19272
|
+
s.consecutiveFailures = 0;
|
|
19273
|
+
s.consecutiveSuccesses += 1;
|
|
19274
|
+
s.totalSuccesses += 1;
|
|
19275
|
+
s.lastSuccessAt = Date.now();
|
|
19276
|
+
if (s.state !== "healthy" && s.consecutiveSuccesses >= this.cfg.recoverAfterSuccesses) {
|
|
19277
|
+
const oldState = s.state;
|
|
19278
|
+
s.state = "healthy";
|
|
19279
|
+
s.stateExpiresAt = null;
|
|
19280
|
+
this.emitStatusChanged(providerId, model, oldState, "healthy", "success_streak_recovery");
|
|
19281
|
+
}
|
|
19282
|
+
}
|
|
19283
|
+
/**
|
|
19284
|
+
* Record a failed provider call. Transitions state based on thresholds.
|
|
19285
|
+
*
|
|
19286
|
+
* @returns The new state after recording this failure.
|
|
19287
|
+
*/
|
|
19288
|
+
recordFailure(providerId, model, kind, status, message, meta) {
|
|
19289
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19290
|
+
const key = pairKey(providerId, model);
|
|
19291
|
+
const s = this.getOrCreate(key, providerId, model);
|
|
19292
|
+
const now = Date.now();
|
|
19293
|
+
const previousExpiry = s.stateExpiresAt;
|
|
19294
|
+
s.consecutiveFailures += 1;
|
|
19295
|
+
s.totalFailures += 1;
|
|
19296
|
+
s.consecutiveSuccesses = 0;
|
|
19297
|
+
s.lastFailureAt = now;
|
|
19298
|
+
s.lastErrorKind = kind;
|
|
19299
|
+
s.lastErrorMessage = message;
|
|
19300
|
+
s.lastErrorStatus = status;
|
|
19301
|
+
if (meta?.sessionId) s.lastSessionId = meta.sessionId;
|
|
19302
|
+
if (meta?.agentId) s.lastAgentId = meta.agentId;
|
|
19303
|
+
if (s.firstFailureAt === null) s.firstFailureAt = now;
|
|
19304
|
+
switch (kind) {
|
|
19305
|
+
case "rate_limit":
|
|
19306
|
+
case "quota_exhausted":
|
|
19307
|
+
s.rateLimitHits += 1;
|
|
19308
|
+
break;
|
|
19309
|
+
case "overloaded":
|
|
19310
|
+
s.overloadedHits += 1;
|
|
19311
|
+
break;
|
|
19312
|
+
case "server":
|
|
19313
|
+
case "stream_hang":
|
|
19314
|
+
s.serverErrors += 1;
|
|
19315
|
+
break;
|
|
19316
|
+
default:
|
|
19317
|
+
s.otherErrors += 1;
|
|
19318
|
+
}
|
|
19319
|
+
const entry = Object.freeze({
|
|
19320
|
+
timestamp: now,
|
|
19321
|
+
kind,
|
|
19322
|
+
status,
|
|
19323
|
+
message,
|
|
19324
|
+
sessionId: meta?.sessionId,
|
|
19325
|
+
agentId: meta?.agentId,
|
|
19326
|
+
retryAfterMs: meta?.retryAfterMs
|
|
19327
|
+
});
|
|
19328
|
+
s.recentErrors.unshift(entry);
|
|
19329
|
+
if (s.recentErrors.length > this.cfg.maxErrorHistory) {
|
|
19330
|
+
s.recentErrors = s.recentErrors.slice(0, this.cfg.maxErrorHistory);
|
|
19331
|
+
}
|
|
19332
|
+
let newState = s.state;
|
|
19333
|
+
let reason = "";
|
|
19334
|
+
const quotaExhausted = kind === "quota_exhausted" || isQuotaExhausted(kind, status, message);
|
|
19335
|
+
if (quotaExhausted) {
|
|
19336
|
+
newState = "blocked";
|
|
19337
|
+
reason = "quota_exhausted";
|
|
19338
|
+
s.stateExpiresAt = now + this.cfg.quotaBlockDurationMs;
|
|
19339
|
+
}
|
|
19340
|
+
if (!quotaExhausted && s.state === "healthy") {
|
|
19341
|
+
if (s.consecutiveFailures >= this.cfg.degradedAfterFailures) {
|
|
19342
|
+
newState = "degraded";
|
|
19343
|
+
reason = `consecutive_failures_${s.consecutiveFailures}`;
|
|
19344
|
+
s.stateExpiresAt = now + this.cfg.degradedDurationMs;
|
|
19345
|
+
}
|
|
19346
|
+
}
|
|
19347
|
+
if (!quotaExhausted && (s.state === "degraded" || s.state === "healthy")) {
|
|
19348
|
+
if (s.rateLimitHits >= this.cfg.blockAfterRateLimitHits) {
|
|
19349
|
+
newState = "blocked";
|
|
19350
|
+
reason = `rate_limit_threshold_${this.cfg.blockAfterRateLimitHits}`;
|
|
19351
|
+
s.stateExpiresAt = now + this.cfg.blockDurationMs;
|
|
19352
|
+
} else if (s.consecutiveFailures >= this.cfg.blockAfterFailures) {
|
|
19353
|
+
newState = "blocked";
|
|
19354
|
+
reason = `consecutive_failures_${s.consecutiveFailures}`;
|
|
19355
|
+
s.stateExpiresAt = now + this.cfg.blockDurationMs;
|
|
19356
|
+
}
|
|
19357
|
+
}
|
|
19358
|
+
if (newState !== "healthy" && meta?.retryAfterMs && meta.retryAfterMs > 0) {
|
|
19359
|
+
const hintExpiry = now + meta.retryAfterMs;
|
|
19360
|
+
if (s.stateExpiresAt === null || hintExpiry > s.stateExpiresAt) {
|
|
19361
|
+
s.stateExpiresAt = hintExpiry;
|
|
19362
|
+
}
|
|
19363
|
+
}
|
|
19364
|
+
if (s.state !== newState) {
|
|
19365
|
+
const oldState = s.state;
|
|
19366
|
+
s.state = newState;
|
|
19367
|
+
this.emitStatusChanged(providerId, model, oldState, newState, reason);
|
|
19368
|
+
} else if (s.state !== "healthy" && s.stateExpiresAt !== previousExpiry) {
|
|
19369
|
+
this.emitStatusChanged(providerId, model, s.state, s.state, "cooldown_extended");
|
|
19370
|
+
}
|
|
19371
|
+
return newState;
|
|
19372
|
+
}
|
|
19373
|
+
/**
|
|
19374
|
+
* Check if a (providerId, model) pair is currently usable.
|
|
19375
|
+
* Returns `true` when healthy or degraded (degraded is still usable,
|
|
19376
|
+
* just known flaky). Returns `false` when blocked or when the error
|
|
19377
|
+
* kind is `auth` (no point retrying ever).
|
|
19378
|
+
*/
|
|
19379
|
+
isAvailable(providerId, model) {
|
|
19380
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19381
|
+
const key = pairKey(providerId, model);
|
|
19382
|
+
const s = this.map.get(key);
|
|
19383
|
+
if (!s) return true;
|
|
19384
|
+
if (s.state === "blocked" && s.stateExpiresAt !== null && Date.now() >= s.stateExpiresAt) {
|
|
19385
|
+
const oldState = s.state;
|
|
19386
|
+
s.state = "healthy";
|
|
19387
|
+
s.stateExpiresAt = null;
|
|
19388
|
+
s.consecutiveFailures = Math.max(0, s.consecutiveFailures - 1);
|
|
19389
|
+
this.emitStatusChanged(providerId, model, oldState, "healthy", "cooldown_expired");
|
|
19390
|
+
return true;
|
|
19391
|
+
}
|
|
19392
|
+
if (s.state === "degraded" && s.stateExpiresAt !== null && Date.now() >= s.stateExpiresAt) {
|
|
19393
|
+
const oldState = s.state;
|
|
19394
|
+
s.state = "healthy";
|
|
19395
|
+
s.stateExpiresAt = null;
|
|
19396
|
+
this.emitStatusChanged(providerId, model, oldState, "healthy", "degraded_timeout_expired");
|
|
19397
|
+
return true;
|
|
19398
|
+
}
|
|
19399
|
+
return s.state !== "blocked";
|
|
19400
|
+
}
|
|
19401
|
+
/**
|
|
19402
|
+
* Check if a (providerId, model) pair is currently rate-limited.
|
|
19403
|
+
* This is a stronger signal than just `!isAvailable()` — it tells
|
|
19404
|
+
* callers that requests should be re-tried after a delay rather
|
|
19405
|
+
* than skipped permanently.
|
|
19406
|
+
*/
|
|
19407
|
+
isRateLimited(providerId, model) {
|
|
19408
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19409
|
+
const key = pairKey(providerId, model);
|
|
19410
|
+
const s = this.map.get(key);
|
|
19411
|
+
if (!s) return false;
|
|
19412
|
+
return s.lastErrorKind === "rate_limit" && s.state !== "healthy";
|
|
19413
|
+
}
|
|
19414
|
+
/**
|
|
19415
|
+
* Get the full status for a (providerId, model) pair, or `undefined`
|
|
19416
|
+
* if no failures have been recorded.
|
|
19417
|
+
*/
|
|
19418
|
+
getStatus(providerId, model) {
|
|
19419
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19420
|
+
const key = pairKey(providerId, model);
|
|
19421
|
+
const s = this.map.get(key);
|
|
19422
|
+
if (!s) return void 0;
|
|
19423
|
+
this.isAvailable(providerId, model);
|
|
19424
|
+
return this.freezeStatus(key, s);
|
|
19425
|
+
}
|
|
19426
|
+
/**
|
|
19427
|
+
* Returns a snapshot of ALL tracked provider/model statuses.
|
|
19428
|
+
* Useful for the `/provider-status` command and WebUI.
|
|
19429
|
+
*/
|
|
19430
|
+
getAllStatuses() {
|
|
19431
|
+
const out = [];
|
|
19432
|
+
for (const [key, s] of this.map) {
|
|
19433
|
+
const [providerId, model] = unpairKey(key);
|
|
19434
|
+
this.isAvailable(providerId, model);
|
|
19435
|
+
out.push(this.freezeStatus(key, s));
|
|
19436
|
+
}
|
|
19437
|
+
return out;
|
|
19438
|
+
}
|
|
19439
|
+
/**
|
|
19440
|
+
* Move every expired waiting-room entry back to healthy. Runtime routing
|
|
19441
|
+
* already performs this check lazily; this explicit sweep lets a UI/status
|
|
19442
|
+
* timer refresh the room even while no model calls are being made.
|
|
19443
|
+
*
|
|
19444
|
+
* @returns Number of entries released by this sweep.
|
|
19445
|
+
*/
|
|
19446
|
+
sweepExpired() {
|
|
19447
|
+
let released = 0;
|
|
19448
|
+
for (const [key, s] of this.map) {
|
|
19449
|
+
if (s.state === "healthy" || s.stateExpiresAt === null || Date.now() < s.stateExpiresAt)
|
|
19450
|
+
continue;
|
|
19451
|
+
const [providerId, model] = unpairKey(key);
|
|
19452
|
+
const previous = s.state;
|
|
19453
|
+
s.state = "healthy";
|
|
19454
|
+
s.stateExpiresAt = null;
|
|
19455
|
+
s.consecutiveFailures = 0;
|
|
19456
|
+
s.consecutiveSuccesses = 0;
|
|
19457
|
+
this.emitStatusChanged(providerId, model, previous, "healthy", "waiting_room_expired");
|
|
19458
|
+
released += 1;
|
|
19459
|
+
}
|
|
19460
|
+
return released;
|
|
19461
|
+
}
|
|
19462
|
+
/**
|
|
19463
|
+
* Release one entry for an immediate half-open probe on its next real use.
|
|
19464
|
+
* History and totals are retained so operators do not lose diagnostics.
|
|
19465
|
+
*/
|
|
19466
|
+
retryNow(providerId, model) {
|
|
19467
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19468
|
+
const s = this.map.get(pairKey(providerId, model));
|
|
19469
|
+
if (!s || s.state === "healthy") return false;
|
|
19470
|
+
const previous = s.state;
|
|
19471
|
+
s.state = "healthy";
|
|
19472
|
+
s.stateExpiresAt = null;
|
|
19473
|
+
s.consecutiveFailures = 0;
|
|
19474
|
+
s.consecutiveSuccesses = 0;
|
|
19475
|
+
this.emitStatusChanged(providerId, model, previous, "healthy", "manual_half_open");
|
|
19476
|
+
return true;
|
|
19477
|
+
}
|
|
19478
|
+
/**
|
|
19479
|
+
* Returns only currently blocked entries.
|
|
19480
|
+
*/
|
|
19481
|
+
getBlocked() {
|
|
19482
|
+
return this.getAllStatuses().filter((s) => s.state === "blocked");
|
|
19483
|
+
}
|
|
19484
|
+
/**
|
|
19485
|
+
* Returns only currently degraded entries.
|
|
19486
|
+
*/
|
|
19487
|
+
getDegraded() {
|
|
19488
|
+
return this.getAllStatuses().filter((s) => s.state === "degraded");
|
|
19489
|
+
}
|
|
19490
|
+
/**
|
|
19491
|
+
* Filter an array of objects that have `providerId` and `model` fields,
|
|
19492
|
+
* removing entries whose (providerId, model) is blocked.
|
|
19493
|
+
*/
|
|
19494
|
+
filterAvailable(entries) {
|
|
19495
|
+
return entries.filter((e) => this.isAvailable(e.providerId, e.model));
|
|
19496
|
+
}
|
|
19497
|
+
/**
|
|
19498
|
+
* Check if a (providerId, model) pair would be blocked, WITHOUT the
|
|
19499
|
+
* side-effect of auto-recovering stale entries. Used in hot paths
|
|
19500
|
+
* where we don't want to mutate state during a quick peek.
|
|
19501
|
+
*/
|
|
19502
|
+
isBlocked(providerId, model) {
|
|
19503
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19504
|
+
const key = pairKey(providerId, model);
|
|
19505
|
+
const s = this.map.get(key);
|
|
19506
|
+
if (!s) return false;
|
|
19507
|
+
if (s.state !== "blocked") return false;
|
|
19508
|
+
if (s.stateExpiresAt !== null && Date.now() >= s.stateExpiresAt) {
|
|
19509
|
+
return false;
|
|
19510
|
+
}
|
|
19511
|
+
return true;
|
|
19512
|
+
}
|
|
19513
|
+
/**
|
|
19514
|
+
* Reset tracking for a specific (providerId, model) pair, or for ALL
|
|
19515
|
+
* pairs when both arguments are omitted.
|
|
19516
|
+
*/
|
|
19517
|
+
clear(providerId, model) {
|
|
19518
|
+
if (providerId && model) {
|
|
19519
|
+
({ providerId, model } = statusIdentity(providerId, model));
|
|
19520
|
+
const key = pairKey(providerId, model);
|
|
19521
|
+
const old = this.map.get(key);
|
|
19522
|
+
if (old && old.state !== "healthy") {
|
|
19523
|
+
this.emitStatusChanged(providerId, model, old.state, "healthy", "manual_clear");
|
|
19524
|
+
}
|
|
19525
|
+
this.map.delete(key);
|
|
19526
|
+
} else {
|
|
19527
|
+
for (const [key, s] of this.map) {
|
|
19528
|
+
if (s.state !== "healthy") {
|
|
19529
|
+
const [pid, mdl] = unpairKey(key);
|
|
19530
|
+
this.emitStatusChanged(pid, mdl, s.state, "healthy", "manual_clear_all");
|
|
19531
|
+
}
|
|
19532
|
+
}
|
|
19533
|
+
this.map.clear();
|
|
19534
|
+
}
|
|
19535
|
+
}
|
|
19536
|
+
/**
|
|
19537
|
+
* Get a JSON-safe snapshot suitable for WebUI rendering.
|
|
19538
|
+
* Includes summary stats + per-pair details.
|
|
19539
|
+
*/
|
|
19540
|
+
getSnapshot() {
|
|
19541
|
+
const all = this.getAllStatuses();
|
|
19542
|
+
const healthy = [];
|
|
19543
|
+
const degraded = [];
|
|
19544
|
+
const blocked = [];
|
|
19545
|
+
for (const s of all) {
|
|
19546
|
+
if (s.state === "blocked") blocked.push(s);
|
|
19547
|
+
else if (s.state === "degraded") degraded.push(s);
|
|
19548
|
+
else healthy.push(s);
|
|
19549
|
+
}
|
|
19550
|
+
return {
|
|
19551
|
+
totalPairs: all.length,
|
|
19552
|
+
healthy: healthy.length,
|
|
19553
|
+
degraded: degraded.length,
|
|
19554
|
+
blocked: blocked.length,
|
|
19555
|
+
totalFailures: all.reduce((sum, s) => sum + s.totalFailures, 0),
|
|
19556
|
+
totalRateLimits: all.reduce((sum, s) => sum + s.rateLimitHits, 0),
|
|
19557
|
+
statuses: all
|
|
19558
|
+
};
|
|
19559
|
+
}
|
|
19560
|
+
/** Restore non-expired waiting-room entries from a previous process. */
|
|
19561
|
+
restoreSnapshot(value) {
|
|
19562
|
+
if (!value || typeof value !== "object") return 0;
|
|
19563
|
+
const statuses = value.statuses;
|
|
19564
|
+
if (!Array.isArray(statuses)) return 0;
|
|
19565
|
+
let restored = 0;
|
|
19566
|
+
const now = Date.now();
|
|
19567
|
+
for (const raw of statuses) {
|
|
19568
|
+
if (!raw || typeof raw !== "object") continue;
|
|
19569
|
+
const item = raw;
|
|
19570
|
+
if (typeof item["providerId"] !== "string" || typeof item["model"] !== "string") continue;
|
|
19571
|
+
if (item["state"] !== "blocked" && item["state"] !== "degraded") continue;
|
|
19572
|
+
if (typeof item["stateExpiresAt"] !== "number" || item["stateExpiresAt"] <= now) continue;
|
|
19573
|
+
const { providerId, model } = statusIdentity(item["providerId"], item["model"]);
|
|
19574
|
+
const s = this.getOrCreate(pairKey(providerId, model), providerId, model);
|
|
19575
|
+
s.state = item["state"];
|
|
19576
|
+
s.stateExpiresAt = item["stateExpiresAt"];
|
|
19577
|
+
s.consecutiveFailures = safeCount(item["consecutiveFailures"]);
|
|
19578
|
+
s.totalFailures = safeCount(item["totalFailures"]);
|
|
19579
|
+
s.rateLimitHits = safeCount(item["rateLimitHits"]);
|
|
19580
|
+
s.lastFailureAt = safeTimestamp(item["lastFailureAt"]);
|
|
19581
|
+
s.lastErrorStatus = safeNullableNumber(item["lastErrorStatus"]);
|
|
19582
|
+
s.lastErrorMessage = typeof item["lastErrorMessage"] === "string" ? item["lastErrorMessage"] : null;
|
|
19583
|
+
s.lastErrorKind = isProviderErrorKind(item["lastErrorKind"]) ? item["lastErrorKind"] : null;
|
|
19584
|
+
restored += 1;
|
|
19585
|
+
}
|
|
19586
|
+
return restored;
|
|
19587
|
+
}
|
|
19588
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
19589
|
+
getOrCreate(key, _providerId, _model) {
|
|
19590
|
+
let s = this.map.get(key);
|
|
19591
|
+
if (!s) {
|
|
19592
|
+
s = {
|
|
19593
|
+
state: "healthy",
|
|
19594
|
+
consecutiveFailures: 0,
|
|
19595
|
+
totalFailures: 0,
|
|
19596
|
+
rateLimitHits: 0,
|
|
19597
|
+
overloadedHits: 0,
|
|
19598
|
+
serverErrors: 0,
|
|
19599
|
+
otherErrors: 0,
|
|
19600
|
+
consecutiveSuccesses: 0,
|
|
19601
|
+
totalSuccesses: 0,
|
|
19602
|
+
lastSuccessAt: null,
|
|
19603
|
+
firstFailureAt: null,
|
|
19604
|
+
lastFailureAt: null,
|
|
19605
|
+
stateExpiresAt: null,
|
|
19606
|
+
lastErrorKind: null,
|
|
19607
|
+
lastErrorMessage: null,
|
|
19608
|
+
lastErrorStatus: null,
|
|
19609
|
+
lastSessionId: null,
|
|
19610
|
+
lastAgentId: null,
|
|
19611
|
+
recentErrors: []
|
|
19612
|
+
};
|
|
19613
|
+
this.map.set(key, s);
|
|
19614
|
+
}
|
|
19615
|
+
return s;
|
|
19616
|
+
}
|
|
19617
|
+
freezeStatus(key, s) {
|
|
19618
|
+
const [providerId, model] = unpairKey(key);
|
|
19619
|
+
return Object.freeze({
|
|
19620
|
+
providerId,
|
|
19621
|
+
model,
|
|
19622
|
+
state: s.state,
|
|
19623
|
+
consecutiveFailures: s.consecutiveFailures,
|
|
19624
|
+
totalFailures: s.totalFailures,
|
|
19625
|
+
rateLimitHits: s.rateLimitHits,
|
|
19626
|
+
overloadedHits: s.overloadedHits,
|
|
19627
|
+
serverErrors: s.serverErrors,
|
|
19628
|
+
otherErrors: s.otherErrors,
|
|
19629
|
+
consecutiveSuccesses: s.consecutiveSuccesses,
|
|
19630
|
+
totalSuccesses: s.totalSuccesses,
|
|
19631
|
+
lastSuccessAt: s.lastSuccessAt,
|
|
19632
|
+
firstFailureAt: s.firstFailureAt,
|
|
19633
|
+
lastFailureAt: s.lastFailureAt,
|
|
19634
|
+
stateExpiresAt: s.stateExpiresAt,
|
|
19635
|
+
lastErrorKind: s.lastErrorKind,
|
|
19636
|
+
lastErrorMessage: s.lastErrorMessage,
|
|
19637
|
+
lastErrorStatus: s.lastErrorStatus,
|
|
19638
|
+
lastSessionId: s.lastSessionId,
|
|
19639
|
+
lastAgentId: s.lastAgentId,
|
|
19640
|
+
recentErrors: Object.freeze([...s.recentErrors])
|
|
19641
|
+
});
|
|
19642
|
+
}
|
|
19643
|
+
emitStatusChanged(providerId, model, oldState, newState, reason) {
|
|
19644
|
+
if (!this.events) return;
|
|
19645
|
+
try {
|
|
19646
|
+
this.events.emit("provider.status_changed", {
|
|
19647
|
+
providerId,
|
|
19648
|
+
model,
|
|
19649
|
+
oldState,
|
|
19650
|
+
newState,
|
|
19651
|
+
reason,
|
|
19652
|
+
timestamp: Date.now(),
|
|
19653
|
+
stateExpiresAt: this.map.get(pairKey(providerId, model))?.stateExpiresAt ?? void 0
|
|
19654
|
+
});
|
|
19655
|
+
} catch {
|
|
19656
|
+
}
|
|
19657
|
+
}
|
|
19658
|
+
};
|
|
19659
|
+
var KEY_SEP = "\0";
|
|
19660
|
+
function statusIdentity(providerId, model) {
|
|
19661
|
+
if (providerId !== "omniroute") return { providerId, model };
|
|
19662
|
+
const slash = model.indexOf("/");
|
|
19663
|
+
if (slash <= 0 || slash === model.length - 1) return { providerId, model };
|
|
19664
|
+
return {
|
|
19665
|
+
providerId: model.slice(0, slash),
|
|
19666
|
+
model: model.slice(slash + 1)
|
|
19667
|
+
};
|
|
19668
|
+
}
|
|
19669
|
+
function pairKey(providerId, model) {
|
|
19670
|
+
return `${providerId}${KEY_SEP}${model}`;
|
|
19671
|
+
}
|
|
19672
|
+
function unpairKey(key) {
|
|
19673
|
+
const idx = key.indexOf(KEY_SEP);
|
|
19674
|
+
if (idx === -1) return [key, ""];
|
|
19675
|
+
return [key.slice(0, idx), key.slice(idx + 1)];
|
|
19676
|
+
}
|
|
19677
|
+
var QUOTA_EXHAUSTED_RE2 = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit/i;
|
|
19678
|
+
function isQuotaExhausted(kind, status, message) {
|
|
19679
|
+
if (status === 402) return true;
|
|
19680
|
+
if (kind !== "rate_limit" && kind !== "quota_exhausted" && kind !== "invalid_request" && kind !== "auth")
|
|
19681
|
+
return false;
|
|
19682
|
+
return QUOTA_EXHAUSTED_RE2.test(message);
|
|
19683
|
+
}
|
|
19684
|
+
function safeCount(value) {
|
|
19685
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
|
|
19686
|
+
}
|
|
19687
|
+
function safeTimestamp(value) {
|
|
19688
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
19689
|
+
}
|
|
19690
|
+
function safeNullableNumber(value) {
|
|
19691
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
19692
|
+
}
|
|
19693
|
+
var PROVIDER_ERROR_KINDS = /* @__PURE__ */ new Set([
|
|
19694
|
+
"rate_limit",
|
|
19695
|
+
"quota_exhausted",
|
|
19696
|
+
"overloaded",
|
|
19697
|
+
"server",
|
|
19698
|
+
"timeout",
|
|
19699
|
+
"network",
|
|
19700
|
+
"stream_hang",
|
|
19701
|
+
"auth",
|
|
19702
|
+
"context_overflow",
|
|
19703
|
+
"content_filter",
|
|
19704
|
+
"invalid_request",
|
|
19705
|
+
"unknown"
|
|
19706
|
+
]);
|
|
19707
|
+
function isProviderErrorKind(value) {
|
|
19708
|
+
return typeof value === "string" && PROVIDER_ERROR_KINDS.has(value);
|
|
19709
|
+
}
|
|
19710
|
+
|
|
17680
19711
|
// src/coordination/adaptive-concurrency.ts
|
|
17681
19712
|
var DEFAULT_CONFIG = Object.freeze({
|
|
17682
19713
|
enabled: false,
|
|
@@ -17873,6 +19904,14 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
17873
19904
|
* the JSONL line is written once, when the segment closes.
|
|
17874
19905
|
*/
|
|
17875
19906
|
_openStreams = /* @__PURE__ */ new Map();
|
|
19907
|
+
/**
|
|
19908
|
+
* FIFO chain of in-flight transcript appends. Writes stay fire-and-forget so
|
|
19909
|
+
* a slow disk never stalls a subagent, but `close()` must be able to wait for
|
|
19910
|
+
* them: without this the process can exit between `stop()` flushing the open
|
|
19911
|
+
* segments and those appends reaching disk, losing exactly the text that was
|
|
19912
|
+
* on screen. Mirrors FleetManager.closeManifest()'s drain.
|
|
19913
|
+
*/
|
|
19914
|
+
_writeChain = Promise.resolve();
|
|
17876
19915
|
/** Disposers for FleetBus subscriptions, keyed by subagentId. */
|
|
17877
19916
|
_subscriptions = /* @__PURE__ */ new Map();
|
|
17878
19917
|
/** Generic fleet-wide subscription disposer. */
|
|
@@ -17904,6 +19943,58 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
17904
19943
|
getAllSessions() {
|
|
17905
19944
|
return Array.from(this._sessions.values());
|
|
17906
19945
|
}
|
|
19946
|
+
/**
|
|
19947
|
+
* Rebuild the virtual sessions from the on-disk transcripts.
|
|
19948
|
+
*
|
|
19949
|
+
* The ring only holds what THIS process observed, so after a resume it is
|
|
19950
|
+
* empty and `getAllSessions()` reports no subagents even though every
|
|
19951
|
+
* transcript is sitting in `transcriptsDir`. This reads them back so a
|
|
19952
|
+
* resumed surface can show the prior run's subagent work.
|
|
19953
|
+
*
|
|
19954
|
+
* Entries already in the ring win: a live subagent's in-memory segment is
|
|
19955
|
+
* newer than its JSONL, which is only written when a segment closes.
|
|
19956
|
+
*/
|
|
19957
|
+
async loadSessionsFromDisk() {
|
|
19958
|
+
let subagentIds;
|
|
19959
|
+
try {
|
|
19960
|
+
const dirents = await fs4.readdir(this._transcriptsDir, { withFileTypes: true });
|
|
19961
|
+
subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
19962
|
+
} catch {
|
|
19963
|
+
return this.getAllSessions();
|
|
19964
|
+
}
|
|
19965
|
+
for (const subagentId of subagentIds) {
|
|
19966
|
+
if (this._sessions.has(subagentId)) continue;
|
|
19967
|
+
const transcript = await this._readTranscriptFile(subagentId);
|
|
19968
|
+
if (transcript.length === 0) continue;
|
|
19969
|
+
const first = expectDefined(transcript[0]);
|
|
19970
|
+
const last = expectDefined(transcript[transcript.length - 1]);
|
|
19971
|
+
this._sessions.set(subagentId, {
|
|
19972
|
+
subagentId,
|
|
19973
|
+
agentName: last.agentName || first.agentName || subagentId,
|
|
19974
|
+
createdAt: first.ts,
|
|
19975
|
+
// The run is over as far as this process is concerned; nothing on disk
|
|
19976
|
+
// says whether it finished or was killed mid-flight.
|
|
19977
|
+
status: "restored",
|
|
19978
|
+
transcript: transcript.slice(-this._maxEntries)
|
|
19979
|
+
});
|
|
19980
|
+
}
|
|
19981
|
+
return this.getAllSessions();
|
|
19982
|
+
}
|
|
19983
|
+
async _readTranscriptFile(subagentId) {
|
|
19984
|
+
const file = path18.join(this._transcriptsDir, subagentId, "transcript.jsonl");
|
|
19985
|
+
const raw = await fs4.readFile(file, "utf8").catch(() => null);
|
|
19986
|
+
if (raw === null) return [];
|
|
19987
|
+
const out = [];
|
|
19988
|
+
for (const line of raw.split("\n")) {
|
|
19989
|
+
const trimmed = line.trim();
|
|
19990
|
+
if (trimmed.length === 0) continue;
|
|
19991
|
+
try {
|
|
19992
|
+
out.push(JSON.parse(trimmed));
|
|
19993
|
+
} catch {
|
|
19994
|
+
}
|
|
19995
|
+
}
|
|
19996
|
+
return out;
|
|
19997
|
+
}
|
|
17907
19998
|
/** Get a specific agent's virtual session, or undefined. */
|
|
17908
19999
|
getSession(subagentId) {
|
|
17909
20000
|
return this._sessions.get(subagentId);
|
|
@@ -17931,6 +20022,21 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
17931
20022
|
this._routeEvent(event.subagentId, event.type, event.payload);
|
|
17932
20023
|
});
|
|
17933
20024
|
}
|
|
20025
|
+
/**
|
|
20026
|
+
* Stop listening and wait for every queued transcript write to reach disk.
|
|
20027
|
+
*
|
|
20028
|
+
* `stop()` alone only *starts* the flush of the open segments; the appends are
|
|
20029
|
+
* fire-and-forget, so a caller that exits right after it loses precisely the
|
|
20030
|
+
* text that was streaming. Shutdown paths should await this instead.
|
|
20031
|
+
*
|
|
20032
|
+
* A hard kill (SIGKILL, power loss) still loses the open segments — they live
|
|
20033
|
+
* only in the ring until a segment closes. Bounding that would mean writing
|
|
20034
|
+
* each delta, which fragments every reply into single-word JSONL lines.
|
|
20035
|
+
*/
|
|
20036
|
+
async close() {
|
|
20037
|
+
this.stop();
|
|
20038
|
+
await this._writeChain;
|
|
20039
|
+
}
|
|
17934
20040
|
/** Stop listening and clean up all subscriptions. */
|
|
17935
20041
|
stop() {
|
|
17936
20042
|
if (!this._started) return;
|
|
@@ -18125,8 +20231,7 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
18125
20231
|
const open6 = this._openStreams.get(subagentId);
|
|
18126
20232
|
if (!open6) return;
|
|
18127
20233
|
this._openStreams.delete(subagentId);
|
|
18128
|
-
this.
|
|
18129
|
-
});
|
|
20234
|
+
this._enqueueWrite(subagentId, open6.entry);
|
|
18130
20235
|
this._emitEntry(open6.entry);
|
|
18131
20236
|
}
|
|
18132
20237
|
_addEntry(subagentId, entry) {
|
|
@@ -18137,8 +20242,7 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
18137
20242
|
if (session.transcript.length > this._maxEntries) {
|
|
18138
20243
|
session.transcript.splice(0, session.transcript.length - this._maxEntries);
|
|
18139
20244
|
}
|
|
18140
|
-
this.
|
|
18141
|
-
});
|
|
20245
|
+
this._enqueueWrite(subagentId, entry);
|
|
18142
20246
|
this._emitEntry(entry);
|
|
18143
20247
|
}
|
|
18144
20248
|
/** Announce a transcript entry on the local bus + HQ callback. */
|
|
@@ -18152,10 +20256,23 @@ var AgentMonitorService = class _AgentMonitorService {
|
|
|
18152
20256
|
iteration: entry.iteration,
|
|
18153
20257
|
ts: entry.ts,
|
|
18154
20258
|
toolName: entry.toolName,
|
|
20259
|
+
toolOk: entry.toolOk,
|
|
18155
20260
|
costUsd: entry.costUsd
|
|
18156
20261
|
});
|
|
18157
20262
|
this._onEntry?.(entry);
|
|
18158
20263
|
}
|
|
20264
|
+
/**
|
|
20265
|
+
* Queue a transcript append. Serialized so two entries for the same subagent
|
|
20266
|
+
* cannot interleave into a torn line, and retained on `_writeChain` so
|
|
20267
|
+
* `close()` can wait for them. Never rejects — a transcript write failing must
|
|
20268
|
+
* not take down the agent it is watching.
|
|
20269
|
+
*/
|
|
20270
|
+
_enqueueWrite(subagentId, entry) {
|
|
20271
|
+
this._writeChain = this._writeChain.then(
|
|
20272
|
+
() => this._appendToFile(subagentId, entry).catch(() => {
|
|
20273
|
+
})
|
|
20274
|
+
);
|
|
20275
|
+
}
|
|
18159
20276
|
async _appendToFile(subagentId, entry) {
|
|
18160
20277
|
const dir = path18.join(this._transcriptsDir, subagentId);
|
|
18161
20278
|
await fs4.mkdir(dir, { recursive: true });
|
|
@@ -20196,7 +22313,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
20196
22313
|
}
|
|
20197
22314
|
if (this.dag.hasDeadlock()) {
|
|
20198
22315
|
const blocked = this.dag.getBlocked();
|
|
20199
|
-
|
|
22316
|
+
this._busEmit("autonomous:deadlock", { blocked });
|
|
20200
22317
|
this._emit({ type: "deadlock:detected", goalId: blocked[0]?.id ?? "", text: `Deadlock detected: ${blocked.map((n) => n.id).join(", ")}` });
|
|
20201
22318
|
}
|
|
20202
22319
|
break;
|
|
@@ -20217,14 +22334,14 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
|
|
|
20217
22334
|
if (decision.type === "deny") {
|
|
20218
22335
|
const blocked = this.dag.getBlocked();
|
|
20219
22336
|
if (blocked.length > 0 && this.dag.hasDeadlock()) {
|
|
20220
|
-
|
|
22337
|
+
this._busEmit("autonomous:deadlock", { blocked });
|
|
20221
22338
|
this._emit({ type: "deadlock:detected", goalId: blocked[0]?.id ?? "", text: `Deadlock detected: ${blocked.map((n) => n.id).join(", ")}` });
|
|
20222
22339
|
this.running = false;
|
|
20223
22340
|
}
|
|
20224
22341
|
break;
|
|
20225
22342
|
}
|
|
20226
22343
|
if (decision.type === "ask_human") {
|
|
20227
|
-
|
|
22344
|
+
this._busEmit("autonomous:ask_human", { prompt: decision.prompt });
|
|
20228
22345
|
break;
|
|
20229
22346
|
}
|
|
20230
22347
|
if (decision.optionId) {
|
|
@@ -20628,15 +22745,20 @@ ${input.detail}`
|
|
|
20628
22745
|
}
|
|
20629
22746
|
}
|
|
20630
22747
|
}
|
|
22748
|
+
/** Emit on the optional shared bus; no-op when no bus was wired. */
|
|
22749
|
+
_busEmit(type, payload) {
|
|
22750
|
+
if (!this.events) return;
|
|
22751
|
+
this.events.emit(type, payload);
|
|
22752
|
+
}
|
|
20631
22753
|
_onDagEvent(event) {
|
|
20632
22754
|
if (event.type === "node:ready") {
|
|
20633
22755
|
const node = this.dag.getNode(event.nodeId);
|
|
20634
22756
|
if (node) {
|
|
20635
|
-
|
|
22757
|
+
this._busEmit("autonomous:task_ready", { taskId: event.nodeId, description: node.description });
|
|
20636
22758
|
}
|
|
20637
22759
|
}
|
|
20638
22760
|
if (event.type === "graph:done") {
|
|
20639
|
-
|
|
22761
|
+
this._busEmit("autonomous:all_done", this.getStats());
|
|
20640
22762
|
}
|
|
20641
22763
|
}
|
|
20642
22764
|
_onSubagentTerminated(e) {
|
|
@@ -20708,7 +22830,6 @@ export {
|
|
|
20708
22830
|
ACP_AGENTS,
|
|
20709
22831
|
AGENTS_BY_PHASE,
|
|
20710
22832
|
AGENT_CATALOG,
|
|
20711
|
-
TOOLS as AGENT_TOOL_PRESETS,
|
|
20712
22833
|
ALL_AGENT_DEFINITIONS,
|
|
20713
22834
|
ALL_FLEET_AGENTS,
|
|
20714
22835
|
AUDIT_LOG_AGENT,
|
|
@@ -20768,17 +22889,23 @@ export {
|
|
|
20768
22889
|
MAILBOX_HEALTH_DEFAULT_FROM,
|
|
20769
22890
|
MAILBOX_HEALTH_DEFAULT_INTERVAL_MS,
|
|
20770
22891
|
MAILBOX_HEALTH_DEFAULT_TIMEOUT_MS,
|
|
22892
|
+
MAILBOX_HTTP_MAX_BODY_BYTES,
|
|
22893
|
+
MAILBOX_HTTP_RATE_LIMIT_PER_MINUTE,
|
|
22894
|
+
MAILBOX_HTTP_RATE_LIMIT_WINDOW_MS,
|
|
20771
22895
|
MATRIX_PHASE_KEYS,
|
|
20772
22896
|
MAX_SUBAGENT_STRUCTURED_REPORT_CHARS,
|
|
20773
22897
|
MEDIUM_BUDGET,
|
|
20774
22898
|
META_AGENTS,
|
|
20775
22899
|
MailboxHealthWatchdog,
|
|
22900
|
+
MailboxHttpRateLimiter,
|
|
20776
22901
|
NULL_FLEET_BUS,
|
|
20777
22902
|
ObservableBrainArbiter,
|
|
20778
22903
|
PLANNING_AGENTS,
|
|
22904
|
+
ProviderModelStatusTracker,
|
|
20779
22905
|
REFACTOR_PLANNER_AGENT,
|
|
20780
22906
|
REVIEW_AGENTS,
|
|
20781
22907
|
SECURITY_SCANNER_AGENT,
|
|
22908
|
+
SESSION_RECIPIENT_PREFIX,
|
|
20782
22909
|
SUBAGENT_STRUCTURED_REPORT_META_KEY,
|
|
20783
22910
|
SubagentBudget,
|
|
20784
22911
|
TIMEOUT_PREEMPT_FRACTION,
|
|
@@ -20791,6 +22918,7 @@ export {
|
|
|
20791
22918
|
applyRosterBudget,
|
|
20792
22919
|
attachAutoExtend,
|
|
20793
22920
|
attachDepWatcherBridge,
|
|
22921
|
+
authorizeMailboxBearerToken,
|
|
20794
22922
|
brainDecisionKey,
|
|
20795
22923
|
buildDownAlert,
|
|
20796
22924
|
buildRecoveryAlert,
|
|
@@ -20800,6 +22928,7 @@ export {
|
|
|
20800
22928
|
createDelegateTool,
|
|
20801
22929
|
createLedgerGuardBrainArbiter,
|
|
20802
22930
|
createMailboxHooks,
|
|
22931
|
+
createMailboxHttpRouter,
|
|
20803
22932
|
createMessage,
|
|
20804
22933
|
detectEcosystem,
|
|
20805
22934
|
dispatchAgent,
|
|
@@ -20822,11 +22951,8 @@ export {
|
|
|
20822
22951
|
makeDependencyWatcherConfig,
|
|
20823
22952
|
makeDirectorSessionFactory,
|
|
20824
22953
|
makeFleetEmitTool,
|
|
20825
|
-
makeFleetTool as makeFleetHealthTool,
|
|
20826
|
-
makeFleetTool as makeFleetSessionTool,
|
|
20827
22954
|
makeFleetStatusTool,
|
|
20828
22955
|
makeFleetTool,
|
|
20829
|
-
makeFleetTool as makeFleetUsageTool,
|
|
20830
22956
|
makeKanbanQueueTool,
|
|
20831
22957
|
makeLLMClassifier,
|
|
20832
22958
|
makeMailInboxTool,
|
|
@@ -20860,6 +22986,7 @@ export {
|
|
|
20860
22986
|
rosterSummaryFromConfigs,
|
|
20861
22987
|
sameModelReference,
|
|
20862
22988
|
scoreAgents,
|
|
22989
|
+
sessionRecipient,
|
|
20863
22990
|
startPackageOutdatedWatcher,
|
|
20864
22991
|
subagentNeedsWorktree,
|
|
20865
22992
|
terminalPolicyDecision,
|