@synergenius/flow-weaver-pack-weaver 0.9.59 → 0.9.77
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/ai-chat-provider.d.ts +12 -0
- package/dist/ai-chat-provider.d.ts.map +1 -1
- package/dist/ai-chat-provider.js +351 -335
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/agent-loop.d.ts +20 -0
- package/dist/bot/agent-loop.d.ts.map +1 -0
- package/dist/bot/agent-loop.js +331 -0
- package/dist/bot/agent-loop.js.map +1 -0
- package/dist/bot/ai-router.d.ts +19 -0
- package/dist/bot/ai-router.d.ts.map +1 -0
- package/dist/bot/ai-router.js +104 -0
- package/dist/bot/ai-router.js.map +1 -0
- package/dist/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +49 -33
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/async-mutex.d.ts +13 -0
- package/dist/bot/async-mutex.d.ts.map +1 -0
- package/dist/bot/async-mutex.js +37 -0
- package/dist/bot/async-mutex.js.map +1 -0
- package/dist/bot/bot-manager.d.ts +2 -2
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +3 -3
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/bot-registry.js +2 -2
- package/dist/bot/bot-registry.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +1 -0
- package/dist/bot/conversation-store.d.ts.map +1 -1
- package/dist/bot/conversation-store.js.map +1 -1
- package/dist/bot/dashboard.d.ts.map +1 -1
- package/dist/bot/dashboard.js +17 -8
- package/dist/bot/dashboard.js.map +1 -1
- package/dist/bot/improve-loop.js.map +1 -1
- package/dist/bot/index.d.ts +2 -4
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +1 -2
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/instance-manager.d.ts +31 -0
- package/dist/bot/instance-manager.d.ts.map +1 -0
- package/dist/bot/instance-manager.js +115 -0
- package/dist/bot/instance-manager.js.map +1 -0
- package/dist/bot/orchestrator.d.ts +36 -0
- package/dist/bot/orchestrator.d.ts.map +1 -0
- package/dist/bot/orchestrator.js +176 -0
- package/dist/bot/orchestrator.js.map +1 -0
- package/dist/bot/profile-store.d.ts +36 -0
- package/dist/bot/profile-store.d.ts.map +1 -0
- package/dist/bot/profile-store.js +208 -0
- package/dist/bot/profile-store.js.map +1 -0
- package/dist/bot/profile-types.d.ts +126 -0
- package/dist/bot/profile-types.d.ts.map +1 -0
- package/dist/bot/profile-types.js +7 -0
- package/dist/bot/profile-types.js.map +1 -0
- package/dist/bot/run-store.d.ts.map +1 -1
- package/dist/bot/run-store.js +8 -0
- package/dist/bot/run-store.js.map +1 -1
- package/dist/bot/runner.d.ts +4 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +5 -1
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +109 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -0
- package/dist/bot/swarm-controller.js +640 -0
- package/dist/bot/swarm-controller.js.map +1 -0
- package/dist/bot/swarm-event-log.d.ts +28 -0
- package/dist/bot/swarm-event-log.d.ts.map +1 -0
- package/dist/bot/swarm-event-log.js +54 -0
- package/dist/bot/swarm-event-log.js.map +1 -0
- package/dist/bot/task-prompt-builder.d.ts +22 -0
- package/dist/bot/task-prompt-builder.d.ts.map +1 -0
- package/dist/bot/task-prompt-builder.js +240 -0
- package/dist/bot/task-prompt-builder.js.map +1 -0
- package/dist/bot/task-store.d.ts +21 -0
- package/dist/bot/task-store.d.ts.map +1 -0
- package/dist/bot/task-store.js +364 -0
- package/dist/bot/task-store.js.map +1 -0
- package/dist/bot/task-types.d.ts +79 -0
- package/dist/bot/task-types.d.ts.map +1 -0
- package/dist/bot/task-types.js +6 -0
- package/dist/bot/task-types.js.map +1 -0
- package/dist/bot/types.d.ts +8 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +79 -54
- package/dist/cli-handlers.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +749 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs/docs/weaver-bot-usage.md +35 -18
- package/dist/docs/docs/weaver-config.md +20 -0
- package/dist/docs/docs/weaver-task-queue.md +31 -19
- package/dist/docs/weaver-config.md +15 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-tools.d.ts +17 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +98 -279
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +6 -24
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/orchestrator-dispatch.d.ts +17 -0
- package/dist/node-types/orchestrator-dispatch.d.ts.map +1 -0
- package/dist/node-types/orchestrator-dispatch.js +63 -0
- package/dist/node-types/orchestrator-dispatch.js.map +1 -0
- package/dist/node-types/orchestrator-load-state.d.ts +16 -0
- package/dist/node-types/orchestrator-load-state.d.ts.map +1 -0
- package/dist/node-types/orchestrator-load-state.js +60 -0
- package/dist/node-types/orchestrator-load-state.js.map +1 -0
- package/dist/node-types/orchestrator-route.d.ts +16 -0
- package/dist/node-types/orchestrator-route.d.ts.map +1 -0
- package/dist/node-types/orchestrator-route.js +28 -0
- package/dist/node-types/orchestrator-route.js.map +1 -0
- package/dist/node-types/receive-task.d.ts +2 -3
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +3 -48
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/templates/weaver-template.d.ts +11 -0
- package/dist/templates/weaver-template.d.ts.map +1 -0
- package/dist/templates/weaver-template.js +53 -0
- package/dist/templates/weaver-template.js.map +1 -0
- package/dist/ui/bot-activity.js +2 -2
- package/dist/ui/bot-constants.d.ts +14 -0
- package/dist/ui/bot-constants.d.ts.map +1 -0
- package/dist/ui/bot-constants.js +189 -0
- package/dist/ui/bot-constants.js.map +1 -0
- package/dist/ui/bot-panel.js +207 -245
- package/dist/ui/bot-slot-card.js +141 -0
- package/dist/ui/budget-bar.js +59 -0
- package/dist/ui/chat-task-result.js +178 -0
- package/dist/ui/decision-log.js +136 -0
- package/dist/ui/profile-card.js +158 -0
- package/dist/ui/profile-editor.js +597 -0
- package/dist/ui/swarm-controls.js +245 -0
- package/dist/ui/swarm-dashboard.js +3012 -0
- package/dist/ui/task-create-form.js +98 -0
- package/dist/ui/task-detail-view.js +1044 -0
- package/dist/ui/task-pool-list.js +156 -0
- package/dist/workflows/orchestrator.d.ts +21 -0
- package/dist/workflows/orchestrator.d.ts.map +1 -0
- package/dist/workflows/orchestrator.js +281 -0
- package/dist/workflows/orchestrator.js.map +1 -0
- package/dist/workflows/weaver-bot-session.d.ts +65 -0
- package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
- package/dist/workflows/weaver-bot-session.js +68 -0
- package/dist/workflows/weaver-bot-session.js.map +1 -0
- package/dist/workflows/weaver.d.ts +24 -0
- package/dist/workflows/weaver.d.ts.map +1 -0
- package/dist/workflows/weaver.js +28 -0
- package/dist/workflows/weaver.js.map +1 -0
- package/flowweaver.manifest.json +547 -133
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +378 -371
- package/src/bot/ai-router.ts +132 -0
- package/src/bot/assistant-tools.ts +47 -29
- package/src/bot/async-mutex.ts +37 -0
- package/src/bot/bot-manager.ts +3 -3
- package/src/bot/bot-registry.ts +2 -2
- package/src/bot/conversation-store.ts +2 -1
- package/src/bot/dashboard.ts +17 -8
- package/src/bot/improve-loop.ts +6 -6
- package/src/bot/index.ts +2 -4
- package/src/bot/instance-manager.ts +128 -0
- package/src/bot/orchestrator.ts +244 -0
- package/src/bot/profile-store.ts +225 -0
- package/src/bot/profile-types.ts +141 -0
- package/src/bot/run-store.ts +8 -0
- package/src/bot/runner.ts +9 -1
- package/src/bot/swarm-controller.ts +780 -0
- package/src/bot/swarm-event-log.ts +57 -0
- package/src/bot/task-prompt-builder.ts +309 -0
- package/src/bot/task-store.ts +407 -0
- package/src/bot/task-types.ts +100 -0
- package/src/bot/types.ts +8 -0
- package/src/cli-handlers.ts +78 -53
- package/src/docs/weaver-bot-usage.md +35 -18
- package/src/docs/weaver-config.md +20 -0
- package/src/docs/weaver-task-queue.md +31 -19
- package/src/index.ts +5 -4
- package/src/mcp-tools.ts +129 -372
- package/src/node-types/bot-report.ts +6 -24
- package/src/node-types/orchestrator-dispatch.ts +71 -0
- package/src/node-types/orchestrator-load-state.ts +66 -0
- package/src/node-types/orchestrator-route.ts +33 -0
- package/src/node-types/receive-task.ts +3 -57
- package/src/ui/bot-activity.tsx +2 -2
- package/src/ui/bot-constants.ts +192 -0
- package/src/ui/bot-panel.tsx +213 -247
- package/src/ui/bot-slot-card.tsx +139 -0
- package/src/ui/budget-bar.tsx +30 -0
- package/src/ui/chat-task-result.tsx +236 -0
- package/src/ui/decision-log.tsx +148 -0
- package/src/ui/profile-card.tsx +157 -0
- package/src/ui/profile-editor.tsx +384 -0
- package/src/ui/swarm-controls.tsx +260 -0
- package/src/ui/swarm-dashboard.tsx +647 -0
- package/src/ui/task-create-form.tsx +87 -0
- package/src/ui/task-detail-view.tsx +841 -0
- package/src/ui/task-pool-list.tsx +187 -0
- package/src/workflows/orchestrator.ts +302 -0
- package/dist/docs/weaver-bot-usage.md +0 -34
- package/dist/docs/weaver-genesis.md +0 -32
- package/dist/docs/weaver-task-queue.md +0 -34
- package/dist/ui/bot-workspace.js +0 -1015
- package/dist/ui/chat-bot-result.js +0 -71
- package/dist/ui/queue-input.js +0 -82
- package/dist/ui/session-bar.js +0 -174
- package/src/bot/error-guide.ts +0 -4
- package/src/bot/retry-utils.ts +0 -4
- package/src/bot/session-state.ts +0 -116
- package/src/bot/task-queue.ts +0 -262
- package/src/ui/bot-workspace.tsx +0 -442
- package/src/ui/chat-bot-result.tsx +0 -81
- package/src/ui/queue-input.tsx +0 -56
- package/src/ui/session-bar.tsx +0 -157
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwarmController — multi-bot orchestrator for the Weaver Swarm.
|
|
3
|
+
*
|
|
4
|
+
* Uses a centralized orchestrator dispatch loop instead of per-bot pull loops.
|
|
5
|
+
* The Orchestrator routes pending tasks to the best available bot instance,
|
|
6
|
+
* the InstanceManager handles scaling, and the ProfileStore persists profiles.
|
|
7
|
+
*
|
|
8
|
+
* Singleton per workspace via `SwarmController.getInstance(projectDir)`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { TaskStore } from './task-store.js';
|
|
14
|
+
import { BotRegistry } from './bot-registry.js';
|
|
15
|
+
import type { BotRegistration } from './bot-registry.js';
|
|
16
|
+
import { SwarmEventLog } from './swarm-event-log.js';
|
|
17
|
+
import { RunStore } from './run-store.js';
|
|
18
|
+
import { EventLog } from './event-log.js';
|
|
19
|
+
import { runRegistry } from './run-registry.js';
|
|
20
|
+
import { runWorkflow } from './runner.js';
|
|
21
|
+
import { buildTaskPrompt } from './task-prompt-builder.js';
|
|
22
|
+
import { Orchestrator } from './orchestrator.js';
|
|
23
|
+
import { AIRouterImpl } from './ai-router.js';
|
|
24
|
+
import { InstanceManager } from './instance-manager.js';
|
|
25
|
+
import { ProfileStore } from './profile-store.js';
|
|
26
|
+
import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision } from './profile-types.js';
|
|
27
|
+
import type { Task, RunSummary } from './task-types.js';
|
|
28
|
+
import type { WorkflowResult } from './types.js';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface SwarmBudgets {
|
|
35
|
+
workspace: { limitTokens: number; usedTokens: number; limitCost: number; usedCost: number };
|
|
36
|
+
session: { limitTokens: number; usedTokens: number; limitCost: number; usedCost: number };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SwarmState {
|
|
40
|
+
status: 'idle' | 'running' | 'paused' | 'stopping';
|
|
41
|
+
startedAt?: string;
|
|
42
|
+
|
|
43
|
+
/** Active bot instances managed by InstanceManager. */
|
|
44
|
+
instances: Record<string, BotInstance>;
|
|
45
|
+
maxConcurrent: number;
|
|
46
|
+
|
|
47
|
+
budgets: SwarmBudgets;
|
|
48
|
+
|
|
49
|
+
autoRetry: boolean;
|
|
50
|
+
maxAttemptsDefault: number;
|
|
51
|
+
|
|
52
|
+
// Stats
|
|
53
|
+
tasksCompleted: number;
|
|
54
|
+
tasksFailed: number;
|
|
55
|
+
totalTokensUsed: number;
|
|
56
|
+
totalCost: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SwarmConfig {
|
|
60
|
+
maxConcurrent?: number;
|
|
61
|
+
sessionBudgetTokens?: number;
|
|
62
|
+
sessionBudgetCost?: number;
|
|
63
|
+
workspaceBudgetTokens?: number;
|
|
64
|
+
workspaceBudgetCost?: number;
|
|
65
|
+
autoRetry?: boolean;
|
|
66
|
+
maxAttemptsDefault?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SwarmStartConfig {
|
|
70
|
+
maxConcurrent?: number;
|
|
71
|
+
sessionBudgetTokens?: number;
|
|
72
|
+
sessionBudgetCost?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Constants
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
const DISPATCH_LOOP_SLEEP_MS = 2000;
|
|
80
|
+
const SWARM_STATE_FILE = 'swarm.json';
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// SwarmController
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export class SwarmController {
|
|
87
|
+
private static instances = new Map<string, SwarmController>();
|
|
88
|
+
|
|
89
|
+
private readonly projectDir: string;
|
|
90
|
+
private readonly weaverDir: string;
|
|
91
|
+
private readonly statePath: string;
|
|
92
|
+
private readonly taskStore: TaskStore;
|
|
93
|
+
private readonly eventLog: SwarmEventLog;
|
|
94
|
+
private readonly orchestrator: Orchestrator;
|
|
95
|
+
private readonly instanceManager: InstanceManager;
|
|
96
|
+
private readonly profileStore: ProfileStore;
|
|
97
|
+
|
|
98
|
+
private state: SwarmState;
|
|
99
|
+
|
|
100
|
+
/** Abort controller for the single dispatch loop. */
|
|
101
|
+
private dispatchAbort: AbortController | null = null;
|
|
102
|
+
/** Promise for the dispatch loop (resolved when the loop exits). */
|
|
103
|
+
private dispatchLoopPromise: Promise<void> | null = null;
|
|
104
|
+
/** Promises for currently executing tasks (instanceId -> Promise). */
|
|
105
|
+
private executionPromises = new Map<string, Promise<void>>();
|
|
106
|
+
|
|
107
|
+
/** Map of profileId -> BotRegistration for workflow execution. */
|
|
108
|
+
private profileBotMap = new Map<string, BotRegistration>();
|
|
109
|
+
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
// Singleton
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
static getInstance(projectDir: string): SwarmController {
|
|
115
|
+
const key = path.resolve(projectDir);
|
|
116
|
+
let inst = SwarmController.instances.get(key);
|
|
117
|
+
if (!inst) {
|
|
118
|
+
inst = new SwarmController(key);
|
|
119
|
+
SwarmController.instances.set(key, inst);
|
|
120
|
+
}
|
|
121
|
+
return inst;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Clear all cached instances (used by tests). */
|
|
125
|
+
static clearInstances(): void {
|
|
126
|
+
SwarmController.instances.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private constructor(projectDir: string) {
|
|
130
|
+
this.projectDir = projectDir;
|
|
131
|
+
this.weaverDir = path.join(projectDir, '.weaver');
|
|
132
|
+
if (!fs.existsSync(this.weaverDir)) fs.mkdirSync(this.weaverDir, { recursive: true });
|
|
133
|
+
this.statePath = path.join(this.weaverDir, SWARM_STATE_FILE);
|
|
134
|
+
this.taskStore = new TaskStore(projectDir);
|
|
135
|
+
this.eventLog = new SwarmEventLog(projectDir);
|
|
136
|
+
this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
|
|
137
|
+
this.instanceManager = new InstanceManager();
|
|
138
|
+
this.profileStore = new ProfileStore(projectDir);
|
|
139
|
+
|
|
140
|
+
// Load persisted state or create default
|
|
141
|
+
this.state = this._loadState();
|
|
142
|
+
|
|
143
|
+
// Crash recovery: no dispatch loop survives a process restart, so reset
|
|
144
|
+
// any stale active status back to idle so start() is not short-circuited.
|
|
145
|
+
if (this.state.status === 'running' || this.state.status === 'paused' || this.state.status === 'stopping') {
|
|
146
|
+
this.state.status = 'idle';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._persist();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// -----------------------------------------------------------------------
|
|
153
|
+
// Public API
|
|
154
|
+
// -----------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
async start(config?: SwarmStartConfig): Promise<void> {
|
|
157
|
+
if (this.state.status === 'running') return;
|
|
158
|
+
|
|
159
|
+
// Apply start config
|
|
160
|
+
if (config?.maxConcurrent !== undefined) this.state.maxConcurrent = config.maxConcurrent;
|
|
161
|
+
if (config?.sessionBudgetTokens !== undefined) this.state.budgets.session.limitTokens = config.sessionBudgetTokens;
|
|
162
|
+
if (config?.sessionBudgetCost !== undefined) this.state.budgets.session.limitCost = config.sessionBudgetCost;
|
|
163
|
+
|
|
164
|
+
// Reset session budget counters on new start
|
|
165
|
+
this.state.budgets.session.usedTokens = 0;
|
|
166
|
+
this.state.budgets.session.usedCost = 0;
|
|
167
|
+
|
|
168
|
+
// Discover bots from registry
|
|
169
|
+
const registry = new BotRegistry(this.projectDir);
|
|
170
|
+
const bots = registry.list();
|
|
171
|
+
|
|
172
|
+
// Ensure default profiles exist for all registered bots
|
|
173
|
+
this.profileStore.ensureDefaultProfiles(
|
|
174
|
+
bots.map((b) => ({ id: b.id, name: b.name, icon: b.icon, color: b.color })),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Load profiles and build profile-to-bot mapping
|
|
178
|
+
const profiles = this.profileStore.list();
|
|
179
|
+
this.profileBotMap.clear();
|
|
180
|
+
for (const profile of profiles) {
|
|
181
|
+
const bot = bots.find((b) => b.id === profile.botId);
|
|
182
|
+
if (bot) this.profileBotMap.set(profile.id, bot);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Stop any existing instances
|
|
186
|
+
this.instanceManager.stopAll();
|
|
187
|
+
|
|
188
|
+
// Spawn initial instances per profile (up to maxConcurrent total)
|
|
189
|
+
let totalSpawned = 0;
|
|
190
|
+
for (const profile of profiles) {
|
|
191
|
+
if (totalSpawned >= this.state.maxConcurrent) break;
|
|
192
|
+
|
|
193
|
+
// Spawn at least minInstances, or 1 if minInstances is 0
|
|
194
|
+
const toSpawn = Math.max(profile.minInstances, 1);
|
|
195
|
+
const clamped = Math.min(toSpawn, this.state.maxConcurrent - totalSpawned);
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < clamped; i++) {
|
|
198
|
+
this.instanceManager.spawn(profile);
|
|
199
|
+
totalSpawned++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Populate state fields
|
|
204
|
+
this._syncInstancesState();
|
|
205
|
+
|
|
206
|
+
this.state.status = 'running';
|
|
207
|
+
this.state.startedAt = new Date().toISOString();
|
|
208
|
+
this._persist();
|
|
209
|
+
|
|
210
|
+
this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now() });
|
|
211
|
+
|
|
212
|
+
// Start the centralized dispatch loop
|
|
213
|
+
this._startDispatchLoop();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async pause(): Promise<void> {
|
|
217
|
+
if (this.state.status !== 'running') return;
|
|
218
|
+
|
|
219
|
+
this.state.status = 'paused';
|
|
220
|
+
this._persist();
|
|
221
|
+
|
|
222
|
+
this.eventLog.emit({ type: 'swarm-paused', timestamp: Date.now() });
|
|
223
|
+
|
|
224
|
+
// The dispatch loop will check paused status and skip routing
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async stop(): Promise<void> {
|
|
228
|
+
if (this.state.status === 'idle') return;
|
|
229
|
+
|
|
230
|
+
this.state.status = 'stopping';
|
|
231
|
+
this._persist();
|
|
232
|
+
|
|
233
|
+
// Signal the dispatch loop to stop
|
|
234
|
+
if (this.dispatchAbort) {
|
|
235
|
+
this.dispatchAbort.abort();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Wait for the dispatch loop to finish
|
|
239
|
+
if (this.dispatchLoopPromise) {
|
|
240
|
+
await this.dispatchLoopPromise;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Wait for all running executions to finish
|
|
244
|
+
const execPromises = Array.from(this.executionPromises.values());
|
|
245
|
+
await Promise.allSettled(execPromises);
|
|
246
|
+
|
|
247
|
+
// Stop all instances
|
|
248
|
+
this.instanceManager.stopAll();
|
|
249
|
+
|
|
250
|
+
// Update state
|
|
251
|
+
this._syncInstancesState();
|
|
252
|
+
|
|
253
|
+
this.state.status = 'idle';
|
|
254
|
+
this._persist();
|
|
255
|
+
|
|
256
|
+
this.dispatchAbort = null;
|
|
257
|
+
this.dispatchLoopPromise = null;
|
|
258
|
+
this.executionPromises.clear();
|
|
259
|
+
|
|
260
|
+
this.eventLog.emit({ type: 'swarm-stopped', timestamp: Date.now() });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getStatus(): SwarmState {
|
|
264
|
+
// Sync instances from InstanceManager before returning
|
|
265
|
+
this._syncInstancesState();
|
|
266
|
+
return { ...this.state, instances: { ...this.state.instances } };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
configure(config: SwarmConfig): void {
|
|
270
|
+
if (config.maxConcurrent !== undefined) this.state.maxConcurrent = config.maxConcurrent;
|
|
271
|
+
if (config.sessionBudgetTokens !== undefined) this.state.budgets.session.limitTokens = config.sessionBudgetTokens;
|
|
272
|
+
if (config.sessionBudgetCost !== undefined) this.state.budgets.session.limitCost = config.sessionBudgetCost;
|
|
273
|
+
if (config.workspaceBudgetTokens !== undefined) this.state.budgets.workspace.limitTokens = config.workspaceBudgetTokens;
|
|
274
|
+
if (config.workspaceBudgetCost !== undefined) this.state.budgets.workspace.limitCost = config.workspaceBudgetCost;
|
|
275
|
+
if (config.autoRetry !== undefined) this.state.autoRetry = config.autoRetry;
|
|
276
|
+
if (config.maxAttemptsDefault !== undefined) this.state.maxAttemptsDefault = config.maxAttemptsDefault;
|
|
277
|
+
this._persist();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -----------------------------------------------------------------------
|
|
281
|
+
// Crash recovery
|
|
282
|
+
// -----------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
async recover(): Promise<void> {
|
|
285
|
+
// Reset all instances to idle
|
|
286
|
+
const allInstances = this.instanceManager.listAll();
|
|
287
|
+
for (const inst of allInstances) {
|
|
288
|
+
if (inst.status === 'executing') {
|
|
289
|
+
this.instanceManager.markIdle(inst.instanceId, false);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Reset in-progress tasks
|
|
294
|
+
const tasks = await this.taskStore.list({ status: 'in-progress' });
|
|
295
|
+
for (const task of tasks) {
|
|
296
|
+
if (this.state.autoRetry && task.attempt < task.maxAttempts) {
|
|
297
|
+
await this.taskStore.update(task.id, {
|
|
298
|
+
status: 'pending',
|
|
299
|
+
currentBotId: undefined,
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
await this.taskStore.update(task.id, {
|
|
303
|
+
status: 'failed',
|
|
304
|
+
currentBotId: undefined,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Reset swarm status to idle
|
|
310
|
+
this.state.status = 'idle';
|
|
311
|
+
this._syncInstancesState();
|
|
312
|
+
this._persist();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// -----------------------------------------------------------------------
|
|
316
|
+
// Token/cost recording
|
|
317
|
+
// -----------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
recordTokenUsage(instanceId: string, _taskId: string, tokensUsed: number, cost: number): void {
|
|
320
|
+
// Update instance in InstanceManager
|
|
321
|
+
try {
|
|
322
|
+
this.instanceManager.recordUsage(instanceId, tokensUsed, cost);
|
|
323
|
+
} catch { /* instance may not exist if stopped */ }
|
|
324
|
+
|
|
325
|
+
// Update session budgets
|
|
326
|
+
this.state.budgets.session.usedTokens += tokensUsed;
|
|
327
|
+
this.state.budgets.session.usedCost += cost;
|
|
328
|
+
|
|
329
|
+
// Update workspace budgets
|
|
330
|
+
this.state.budgets.workspace.usedTokens += tokensUsed;
|
|
331
|
+
this.state.budgets.workspace.usedCost += cost;
|
|
332
|
+
|
|
333
|
+
// Update totals
|
|
334
|
+
this.state.totalTokensUsed += tokensUsed;
|
|
335
|
+
this.state.totalCost += cost;
|
|
336
|
+
|
|
337
|
+
this._persist();
|
|
338
|
+
|
|
339
|
+
this.eventLog.emit({
|
|
340
|
+
type: 'budget-updated',
|
|
341
|
+
timestamp: Date.now(),
|
|
342
|
+
data: { botId: instanceId, tokensUsed, cost },
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// -----------------------------------------------------------------------
|
|
347
|
+
// Orchestrator accessors
|
|
348
|
+
// -----------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
getProfileStore(): ProfileStore {
|
|
351
|
+
return this.profileStore;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getInstanceManager(): InstanceManager {
|
|
355
|
+
return this.instanceManager;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getOrchestrator(): Orchestrator {
|
|
359
|
+
return this.orchestrator;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getOrchestratorStatus(): { decisions: OrchestratorDecision[]; stats: { totalDecisions: number; assignmentsByMethod: Record<string, number> } } {
|
|
363
|
+
const decisions = this.orchestrator.getDecisionLog();
|
|
364
|
+
const stats = {
|
|
365
|
+
totalDecisions: decisions.length,
|
|
366
|
+
assignmentsByMethod: {} as Record<string, number>,
|
|
367
|
+
};
|
|
368
|
+
for (const d of decisions) {
|
|
369
|
+
stats.assignmentsByMethod[d.method] = (stats.assignmentsByMethod[d.method] ?? 0) + 1;
|
|
370
|
+
}
|
|
371
|
+
return { decisions, stats };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// -----------------------------------------------------------------------
|
|
375
|
+
// Dispatch loop
|
|
376
|
+
// -----------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
private _startDispatchLoop(): void {
|
|
379
|
+
const abort = new AbortController();
|
|
380
|
+
this.dispatchAbort = abort;
|
|
381
|
+
|
|
382
|
+
this.dispatchLoopPromise = this._dispatchLoop(abort.signal).catch(() => {
|
|
383
|
+
// Swallow errors in dispatch loop — self-heals on next cycle
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async _dispatchLoop(signal: AbortSignal): Promise<void> {
|
|
388
|
+
while (!signal.aborted) {
|
|
389
|
+
// Check if swarm is still running
|
|
390
|
+
if (this.state.status === 'stopping' || this.state.status === 'idle') break;
|
|
391
|
+
|
|
392
|
+
// Check if paused — sleep and try again
|
|
393
|
+
if (this.state.status === 'paused') {
|
|
394
|
+
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Budget check before routing
|
|
399
|
+
if (this._isBudgetExceeded()) {
|
|
400
|
+
this.state.status = 'paused';
|
|
401
|
+
this._persist();
|
|
402
|
+
this.eventLog.emit({
|
|
403
|
+
type: 'swarm-paused',
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
data: { reason: 'budget-exceeded' },
|
|
406
|
+
});
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Check instance-level steering
|
|
411
|
+
const allInstances = this.instanceManager.listAll();
|
|
412
|
+
for (const inst of allInstances) {
|
|
413
|
+
await this._checkSteering(inst.instanceId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Collect pending tasks (and all tasks for dependency checks)
|
|
417
|
+
const pendingTasks = await this.taskStore.list({ status: ['pending', 'failed'] });
|
|
418
|
+
const allTasks = await this.taskStore.list();
|
|
419
|
+
const routableTasks = pendingTasks.filter((t) => {
|
|
420
|
+
// Skip parent tasks
|
|
421
|
+
if (t.isParent) return false;
|
|
422
|
+
// Skip tasks that have exhausted retries
|
|
423
|
+
if (t.status === 'failed' && t.attempt >= t.maxAttempts) return false;
|
|
424
|
+
// Skip tasks over per-task budget
|
|
425
|
+
if (t.budgetTokens !== undefined && t.tokensUsed >= t.budgetTokens) return false;
|
|
426
|
+
if (t.budgetCost !== undefined && t.costUsed >= t.budgetCost) return false;
|
|
427
|
+
// Skip tasks whose dependencies are not all done
|
|
428
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
429
|
+
const allDepsDone = t.dependsOn.every((depId) => {
|
|
430
|
+
const dep = allTasks.find((d) => d.id === depId);
|
|
431
|
+
return dep && dep.status === 'done';
|
|
432
|
+
});
|
|
433
|
+
if (!allDepsDone) return false;
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (routableTasks.length === 0) {
|
|
439
|
+
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Build orchestrator input
|
|
444
|
+
const profiles = this.profileStore.list();
|
|
445
|
+
const instances = this.instanceManager.listAll();
|
|
446
|
+
const { workspace, session } = this.state.budgets;
|
|
447
|
+
|
|
448
|
+
const budgetRemaining = {
|
|
449
|
+
tokens: this._remainingBudget(session.limitTokens, session.usedTokens, workspace.limitTokens, workspace.usedTokens),
|
|
450
|
+
cost: this._remainingBudget(session.limitCost, session.usedCost, workspace.limitCost, workspace.usedCost),
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const input: OrchestratorInput = {
|
|
454
|
+
pendingTasks: routableTasks.map((t) => ({
|
|
455
|
+
id: t.id,
|
|
456
|
+
title: t.title,
|
|
457
|
+
description: t.description,
|
|
458
|
+
priority: t.priority,
|
|
459
|
+
complexity: t.complexity ?? 'moderate',
|
|
460
|
+
assignedProfile: t.assignedProfile,
|
|
461
|
+
context: {
|
|
462
|
+
runSummaries: t.context.runSummaries.map((rs) => ({
|
|
463
|
+
outcome: rs.outcome,
|
|
464
|
+
botId: rs.botId,
|
|
465
|
+
})),
|
|
466
|
+
},
|
|
467
|
+
})),
|
|
468
|
+
profiles,
|
|
469
|
+
instances,
|
|
470
|
+
budgetRemaining,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Route
|
|
474
|
+
const output = await this.orchestrator.route(input);
|
|
475
|
+
|
|
476
|
+
// Apply scale actions
|
|
477
|
+
for (const sa of output.scaleActions) {
|
|
478
|
+
const profile = profiles.find((p) => p.id === sa.profileId);
|
|
479
|
+
if (profile) {
|
|
480
|
+
this.instanceManager.scaleTo(profile, sa.targetInstances);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Apply assignments
|
|
485
|
+
for (const assignment of output.assignments) {
|
|
486
|
+
const profile = profiles.find((p) => p.id === assignment.profileId);
|
|
487
|
+
const bot = this.profileBotMap.get(assignment.profileId);
|
|
488
|
+
if (!profile || !bot) continue;
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
// Assign task in TaskStore
|
|
492
|
+
await this.taskStore.assignToInstance(assignment.taskId, assignment.instanceId, assignment.profileId);
|
|
493
|
+
|
|
494
|
+
// Mark instance as executing and store runId on task for live streaming
|
|
495
|
+
const runId = RunStore.newId();
|
|
496
|
+
this.instanceManager.markExecuting(assignment.instanceId, assignment.taskId, runId);
|
|
497
|
+
await this.taskStore.update(assignment.taskId, { currentRunId: runId });
|
|
498
|
+
|
|
499
|
+
// Sync state for dashboard
|
|
500
|
+
this._syncInstancesState();
|
|
501
|
+
this._persist();
|
|
502
|
+
|
|
503
|
+
this.eventLog.emit({
|
|
504
|
+
type: 'task-claimed',
|
|
505
|
+
timestamp: Date.now(),
|
|
506
|
+
data: { botId: assignment.instanceId, taskId: assignment.taskId, profileId: assignment.profileId, method: assignment.method },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Spawn execution (non-blocking)
|
|
510
|
+
const execPromise = this._executeTask(assignment.instanceId, assignment.taskId, runId, bot, profile).catch(() => {
|
|
511
|
+
// errors handled inside _executeTask
|
|
512
|
+
});
|
|
513
|
+
this.executionPromises.set(assignment.instanceId, execPromise);
|
|
514
|
+
} catch {
|
|
515
|
+
// Assignment failed (task may have been grabbed by another cycle) — skip
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Wait for completions via Promise.race with a sleep timeout
|
|
520
|
+
if (this.executionPromises.size > 0) {
|
|
521
|
+
const sleepPromise = this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
522
|
+
await Promise.race([
|
|
523
|
+
Promise.race(Array.from(this.executionPromises.values())),
|
|
524
|
+
sleepPromise,
|
|
525
|
+
]);
|
|
526
|
+
} else {
|
|
527
|
+
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Clean up completed execution promises
|
|
531
|
+
for (const [instanceId, promise] of this.executionPromises) {
|
|
532
|
+
// Check if resolved by racing with an immediately resolved promise
|
|
533
|
+
const result = await Promise.race([
|
|
534
|
+
promise.then(() => 'done'),
|
|
535
|
+
Promise.resolve('pending'),
|
|
536
|
+
]);
|
|
537
|
+
if (result === 'done') {
|
|
538
|
+
this.executionPromises.delete(instanceId);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Post-cycle budget check
|
|
543
|
+
if (this._isBudgetExceeded()) {
|
|
544
|
+
this.state.status = 'paused';
|
|
545
|
+
this._persist();
|
|
546
|
+
this.eventLog.emit({
|
|
547
|
+
type: 'swarm-paused',
|
|
548
|
+
timestamp: Date.now(),
|
|
549
|
+
data: { reason: 'budget-exceeded' },
|
|
550
|
+
});
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this._syncInstancesState();
|
|
555
|
+
this._persist();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// -----------------------------------------------------------------------
|
|
560
|
+
// Task execution
|
|
561
|
+
// -----------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
private async _executeTask(
|
|
564
|
+
instanceId: string,
|
|
565
|
+
taskId: string,
|
|
566
|
+
runId: string,
|
|
567
|
+
bot: BotRegistration,
|
|
568
|
+
profile: BotProfile,
|
|
569
|
+
): Promise<void> {
|
|
570
|
+
const startTime = Date.now();
|
|
571
|
+
let result: WorkflowResult;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const task = await this.taskStore.get(taskId);
|
|
575
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
576
|
+
|
|
577
|
+
// Build prompt from task context
|
|
578
|
+
const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
|
|
579
|
+
const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
|
|
580
|
+
const prompt = buildTaskPrompt(task, parentTask, siblingTasks, profile.preferences);
|
|
581
|
+
|
|
582
|
+
// Create per-run event log
|
|
583
|
+
const runEventLog = new EventLog(runId);
|
|
584
|
+
|
|
585
|
+
// Execute workflow
|
|
586
|
+
result = await runWorkflow(bot.filePath, {
|
|
587
|
+
runId,
|
|
588
|
+
taskId,
|
|
589
|
+
botId: instanceId,
|
|
590
|
+
config: { provider: 'auto' },
|
|
591
|
+
params: { taskJson: prompt, projectDir: this.projectDir },
|
|
592
|
+
eventLog: runEventLog,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
runEventLog.done();
|
|
596
|
+
} catch (err) {
|
|
597
|
+
result = {
|
|
598
|
+
success: false,
|
|
599
|
+
summary: (err instanceof Error ? err.message : String(err)),
|
|
600
|
+
outcome: 'error',
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const durationMs = Date.now() - startTime;
|
|
605
|
+
|
|
606
|
+
// Extract run summary
|
|
607
|
+
const tokensUsed = Math.max(0, (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0));
|
|
608
|
+
const costUsed = Math.max(0, result.cost?.totalCost ?? 0);
|
|
609
|
+
|
|
610
|
+
const runSummary: RunSummary = {
|
|
611
|
+
runId,
|
|
612
|
+
botId: instanceId,
|
|
613
|
+
outcome: result.success ? 'success' : 'failed',
|
|
614
|
+
summary: result.summary || (result.success ? 'Completed' : 'Failed'),
|
|
615
|
+
filesModified: [],
|
|
616
|
+
error: result.success ? undefined : (result.summary || 'Unknown error'),
|
|
617
|
+
durationMs,
|
|
618
|
+
tokensUsed,
|
|
619
|
+
cost: costUsed,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// Release task
|
|
623
|
+
const releaseStatus = result.success ? 'done' : 'failed';
|
|
624
|
+
await this.taskStore.release(taskId, releaseStatus, runSummary);
|
|
625
|
+
|
|
626
|
+
// Record token usage
|
|
627
|
+
this.recordTokenUsage(instanceId, taskId, tokensUsed, costUsed);
|
|
628
|
+
|
|
629
|
+
// Update stats
|
|
630
|
+
if (result.success) {
|
|
631
|
+
this.state.tasksCompleted += 1;
|
|
632
|
+
} else {
|
|
633
|
+
this.state.tasksFailed += 1;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Mark instance as idle
|
|
637
|
+
try {
|
|
638
|
+
this.instanceManager.markIdle(instanceId, result.success);
|
|
639
|
+
} catch { /* instance may have been stopped */ }
|
|
640
|
+
|
|
641
|
+
// Emit task event
|
|
642
|
+
this.eventLog.emit({
|
|
643
|
+
type: result.success ? 'task-done' : 'task-failed',
|
|
644
|
+
timestamp: Date.now(),
|
|
645
|
+
data: { botId: instanceId, taskId, outcome: runSummary.outcome },
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
this._syncInstancesState();
|
|
649
|
+
this._persist();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// -----------------------------------------------------------------------
|
|
653
|
+
// Budget checks
|
|
654
|
+
// -----------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
private _isBudgetExceeded(): boolean {
|
|
657
|
+
const { workspace, session } = this.state.budgets;
|
|
658
|
+
|
|
659
|
+
// Check session budget
|
|
660
|
+
if (session.limitTokens > 0 && session.usedTokens >= session.limitTokens) return true;
|
|
661
|
+
if (session.limitCost > 0 && session.usedCost >= session.limitCost) return true;
|
|
662
|
+
|
|
663
|
+
// Check workspace budget
|
|
664
|
+
if (workspace.limitTokens > 0 && workspace.usedTokens >= workspace.limitTokens) return true;
|
|
665
|
+
if (workspace.limitCost > 0 && workspace.usedCost >= workspace.limitCost) return true;
|
|
666
|
+
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Calculate remaining budget across session and workspace. */
|
|
671
|
+
private _remainingBudget(
|
|
672
|
+
sessionLimit: number,
|
|
673
|
+
sessionUsed: number,
|
|
674
|
+
workspaceLimit: number,
|
|
675
|
+
workspaceUsed: number,
|
|
676
|
+
): number {
|
|
677
|
+
const remaining: number[] = [];
|
|
678
|
+
if (sessionLimit > 0) remaining.push(sessionLimit - sessionUsed);
|
|
679
|
+
if (workspaceLimit > 0) remaining.push(workspaceLimit - workspaceUsed);
|
|
680
|
+
if (remaining.length === 0) return Number.MAX_SAFE_INTEGER;
|
|
681
|
+
return Math.min(...remaining);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// -----------------------------------------------------------------------
|
|
685
|
+
// Steering
|
|
686
|
+
// -----------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
private async _checkSteering(instanceId: string): Promise<void> {
|
|
689
|
+
const steerPath = path.join(this.weaverDir, `steer-${instanceId}.json`);
|
|
690
|
+
try {
|
|
691
|
+
if (!fs.existsSync(steerPath)) return;
|
|
692
|
+
const raw = fs.readFileSync(steerPath, 'utf-8');
|
|
693
|
+
fs.unlinkSync(steerPath); // consume the command
|
|
694
|
+
const command = JSON.parse(raw);
|
|
695
|
+
|
|
696
|
+
switch (command.command) {
|
|
697
|
+
case 'pause':
|
|
698
|
+
this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId: instanceId } });
|
|
699
|
+
break;
|
|
700
|
+
case 'resume':
|
|
701
|
+
break;
|
|
702
|
+
case 'cancel':
|
|
703
|
+
this.instanceManager.stop(instanceId);
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
} catch { /* steer file read error — skip */ }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// -----------------------------------------------------------------------
|
|
710
|
+
// State sync — instances field
|
|
711
|
+
// -----------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
private _syncInstancesState(): void {
|
|
714
|
+
const allInstances = this.instanceManager.listAll();
|
|
715
|
+
|
|
716
|
+
// Build instances record
|
|
717
|
+
this.state.instances = {};
|
|
718
|
+
for (const inst of allInstances) {
|
|
719
|
+
this.state.instances[inst.instanceId] = inst;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// -----------------------------------------------------------------------
|
|
724
|
+
// State persistence
|
|
725
|
+
// -----------------------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
private _loadState(): SwarmState {
|
|
728
|
+
try {
|
|
729
|
+
if (fs.existsSync(this.statePath)) {
|
|
730
|
+
const raw = fs.readFileSync(this.statePath, 'utf-8');
|
|
731
|
+
const data = JSON.parse(raw) as SwarmState;
|
|
732
|
+
if (!data.instances) data.instances = {};
|
|
733
|
+
return data;
|
|
734
|
+
}
|
|
735
|
+
} catch { /* corrupt file — use defaults */ }
|
|
736
|
+
return this._defaultState();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private _defaultState(): SwarmState {
|
|
740
|
+
return {
|
|
741
|
+
status: 'idle',
|
|
742
|
+
instances: {},
|
|
743
|
+
maxConcurrent: 3,
|
|
744
|
+
budgets: {
|
|
745
|
+
workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
|
|
746
|
+
session: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
|
|
747
|
+
},
|
|
748
|
+
autoRetry: true,
|
|
749
|
+
maxAttemptsDefault: 3,
|
|
750
|
+
tasksCompleted: 0,
|
|
751
|
+
tasksFailed: 0,
|
|
752
|
+
totalTokensUsed: 0,
|
|
753
|
+
totalCost: 0,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private _persist(): void {
|
|
758
|
+
try {
|
|
759
|
+
const tmpPath = this.statePath + '.tmp';
|
|
760
|
+
fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2), 'utf-8');
|
|
761
|
+
fs.renameSync(tmpPath, this.statePath);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.error('[swarm] Failed to persist state:', err instanceof Error ? err.message : err);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// -----------------------------------------------------------------------
|
|
768
|
+
// Utilities
|
|
769
|
+
// -----------------------------------------------------------------------
|
|
770
|
+
|
|
771
|
+
private _sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
772
|
+
return new Promise((resolve) => {
|
|
773
|
+
const timer = setTimeout(resolve, ms);
|
|
774
|
+
signal?.addEventListener('abort', () => {
|
|
775
|
+
clearTimeout(timer);
|
|
776
|
+
resolve();
|
|
777
|
+
}, { once: true });
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|