@synergenius/flow-weaver-pack-weaver 0.9.62 → 0.9.78

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.
Files changed (162) hide show
  1. package/dist/ai-chat-provider.d.ts +12 -0
  2. package/dist/ai-chat-provider.d.ts.map +1 -1
  3. package/dist/ai-chat-provider.js +173 -19
  4. package/dist/ai-chat-provider.js.map +1 -1
  5. package/dist/bot/agent-loop.d.ts +20 -0
  6. package/dist/bot/agent-loop.d.ts.map +1 -0
  7. package/dist/bot/agent-loop.js +331 -0
  8. package/dist/bot/agent-loop.js.map +1 -0
  9. package/dist/bot/ai-router.d.ts +19 -0
  10. package/dist/bot/ai-router.d.ts.map +1 -0
  11. package/dist/bot/ai-router.js +104 -0
  12. package/dist/bot/ai-router.js.map +1 -0
  13. package/dist/bot/bot-registry.js +2 -2
  14. package/dist/bot/bot-registry.js.map +1 -1
  15. package/dist/bot/conversation-store.d.ts +1 -0
  16. package/dist/bot/conversation-store.d.ts.map +1 -1
  17. package/dist/bot/conversation-store.js.map +1 -1
  18. package/dist/bot/improve-loop.js.map +1 -1
  19. package/dist/bot/instance-manager.d.ts +31 -0
  20. package/dist/bot/instance-manager.d.ts.map +1 -0
  21. package/dist/bot/instance-manager.js +115 -0
  22. package/dist/bot/instance-manager.js.map +1 -0
  23. package/dist/bot/orchestrator.d.ts +36 -0
  24. package/dist/bot/orchestrator.d.ts.map +1 -0
  25. package/dist/bot/orchestrator.js +176 -0
  26. package/dist/bot/orchestrator.js.map +1 -0
  27. package/dist/bot/profile-store.d.ts +36 -0
  28. package/dist/bot/profile-store.d.ts.map +1 -0
  29. package/dist/bot/profile-store.js +208 -0
  30. package/dist/bot/profile-store.js.map +1 -0
  31. package/dist/bot/profile-types.d.ts +126 -0
  32. package/dist/bot/profile-types.d.ts.map +1 -0
  33. package/dist/bot/profile-types.js +7 -0
  34. package/dist/bot/profile-types.js.map +1 -0
  35. package/dist/bot/session-state.d.ts +25 -0
  36. package/dist/bot/session-state.d.ts.map +1 -0
  37. package/dist/bot/session-state.js +110 -0
  38. package/dist/bot/session-state.js.map +1 -0
  39. package/dist/bot/swarm-controller.d.ts +37 -21
  40. package/dist/bot/swarm-controller.d.ts.map +1 -1
  41. package/dist/bot/swarm-controller.js +344 -163
  42. package/dist/bot/swarm-controller.js.map +1 -1
  43. package/dist/bot/task-prompt-builder.d.ts +2 -1
  44. package/dist/bot/task-prompt-builder.d.ts.map +1 -1
  45. package/dist/bot/task-prompt-builder.js +33 -10
  46. package/dist/bot/task-prompt-builder.js.map +1 -1
  47. package/dist/bot/task-queue.d.ts +46 -0
  48. package/dist/bot/task-queue.d.ts.map +1 -0
  49. package/dist/bot/task-queue.js +237 -0
  50. package/dist/bot/task-queue.js.map +1 -0
  51. package/dist/bot/task-store.d.ts +1 -6
  52. package/dist/bot/task-store.d.ts.map +1 -1
  53. package/dist/bot/task-store.js +27 -78
  54. package/dist/bot/task-store.js.map +1 -1
  55. package/dist/bot/task-types.d.ts +8 -4
  56. package/dist/bot/task-types.d.ts.map +1 -1
  57. package/dist/cli-handlers.d.ts.map +1 -1
  58. package/dist/cli-handlers.js +2 -3
  59. package/dist/cli-handlers.js.map +1 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.d.ts.map +1 -0
  62. package/dist/cli.js +749 -0
  63. package/dist/cli.js.map +1 -0
  64. package/dist/docs/docs/weaver-bot-usage.md +35 -18
  65. package/dist/docs/docs/weaver-config.md +20 -0
  66. package/dist/docs/docs/weaver-task-queue.md +31 -19
  67. package/dist/docs/weaver-config.md +15 -9
  68. package/dist/mcp-tools.d.ts +17 -0
  69. package/dist/mcp-tools.d.ts.map +1 -1
  70. package/dist/mcp-tools.js +98 -232
  71. package/dist/mcp-tools.js.map +1 -1
  72. package/dist/node-types/orchestrator-dispatch.d.ts +17 -0
  73. package/dist/node-types/orchestrator-dispatch.d.ts.map +1 -0
  74. package/dist/node-types/orchestrator-dispatch.js +63 -0
  75. package/dist/node-types/orchestrator-dispatch.js.map +1 -0
  76. package/dist/node-types/orchestrator-load-state.d.ts +16 -0
  77. package/dist/node-types/orchestrator-load-state.d.ts.map +1 -0
  78. package/dist/node-types/orchestrator-load-state.js +60 -0
  79. package/dist/node-types/orchestrator-load-state.js.map +1 -0
  80. package/dist/node-types/orchestrator-route.d.ts +16 -0
  81. package/dist/node-types/orchestrator-route.d.ts.map +1 -0
  82. package/dist/node-types/orchestrator-route.js +28 -0
  83. package/dist/node-types/orchestrator-route.js.map +1 -0
  84. package/dist/node-types/receive-task.d.ts +2 -3
  85. package/dist/node-types/receive-task.d.ts.map +1 -1
  86. package/dist/node-types/receive-task.js +3 -28
  87. package/dist/node-types/receive-task.js.map +1 -1
  88. package/dist/templates/weaver-template.d.ts +11 -0
  89. package/dist/templates/weaver-template.d.ts.map +1 -0
  90. package/dist/templates/weaver-template.js +53 -0
  91. package/dist/templates/weaver-template.js.map +1 -0
  92. package/dist/ui/bot-constants.d.ts +14 -0
  93. package/dist/ui/bot-constants.d.ts.map +1 -0
  94. package/dist/ui/bot-constants.js +189 -0
  95. package/dist/ui/bot-constants.js.map +1 -0
  96. package/dist/ui/bot-panel.js +51 -90
  97. package/dist/ui/bot-slot-card.js +87 -122
  98. package/dist/ui/budget-bar.js +5 -3
  99. package/dist/ui/chat-task-result.js +4 -7
  100. package/dist/ui/decision-log.js +136 -0
  101. package/dist/ui/profile-card.js +158 -0
  102. package/dist/ui/profile-editor.js +597 -0
  103. package/dist/ui/swarm-controls.js +36 -27
  104. package/dist/ui/swarm-dashboard.js +2034 -736
  105. package/dist/ui/task-create-form.js +39 -116
  106. package/dist/ui/task-detail-view.js +490 -239
  107. package/dist/ui/task-pool-list.js +69 -94
  108. package/dist/workflows/orchestrator.d.ts +21 -0
  109. package/dist/workflows/orchestrator.d.ts.map +1 -0
  110. package/dist/workflows/orchestrator.js +281 -0
  111. package/dist/workflows/orchestrator.js.map +1 -0
  112. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  113. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  114. package/dist/workflows/weaver-bot-session.js +68 -0
  115. package/dist/workflows/weaver-bot-session.js.map +1 -0
  116. package/dist/workflows/weaver.d.ts +24 -0
  117. package/dist/workflows/weaver.d.ts.map +1 -0
  118. package/dist/workflows/weaver.js +28 -0
  119. package/dist/workflows/weaver.js.map +1 -0
  120. package/flowweaver.manifest.json +253 -66
  121. package/package.json +1 -1
  122. package/src/ai-chat-provider.ts +184 -18
  123. package/src/bot/ai-router.ts +132 -0
  124. package/src/bot/bot-registry.ts +2 -2
  125. package/src/bot/conversation-store.ts +2 -1
  126. package/src/bot/improve-loop.ts +6 -6
  127. package/src/bot/instance-manager.ts +128 -0
  128. package/src/bot/orchestrator.ts +244 -0
  129. package/src/bot/profile-store.ts +225 -0
  130. package/src/bot/profile-types.ts +141 -0
  131. package/src/bot/swarm-controller.ts +385 -186
  132. package/src/bot/task-prompt-builder.ts +37 -6
  133. package/src/bot/task-store.ts +28 -89
  134. package/src/bot/task-types.ts +10 -4
  135. package/src/cli-handlers.ts +2 -3
  136. package/src/docs/weaver-bot-usage.md +35 -18
  137. package/src/docs/weaver-config.md +20 -0
  138. package/src/docs/weaver-task-queue.md +31 -19
  139. package/src/mcp-tools.ts +129 -320
  140. package/src/node-types/orchestrator-dispatch.ts +71 -0
  141. package/src/node-types/orchestrator-load-state.ts +66 -0
  142. package/src/node-types/orchestrator-route.ts +33 -0
  143. package/src/node-types/receive-task.ts +3 -26
  144. package/src/ui/bot-constants.ts +192 -0
  145. package/src/ui/bot-panel.tsx +55 -79
  146. package/src/ui/bot-slot-card.tsx +69 -117
  147. package/src/ui/budget-bar.tsx +5 -3
  148. package/src/ui/chat-task-result.tsx +6 -9
  149. package/src/ui/decision-log.tsx +148 -0
  150. package/src/ui/profile-card.tsx +157 -0
  151. package/src/ui/profile-editor.tsx +384 -0
  152. package/src/ui/swarm-controls.tsx +35 -31
  153. package/src/ui/swarm-dashboard.tsx +409 -80
  154. package/src/ui/task-create-form.tsx +29 -119
  155. package/src/ui/task-detail-view.tsx +461 -215
  156. package/src/ui/task-pool-list.tsx +74 -95
  157. package/src/workflows/orchestrator.ts +302 -0
  158. package/dist/docs/weaver-bot-usage.md +0 -34
  159. package/dist/docs/weaver-genesis.md +0 -32
  160. package/dist/docs/weaver-task-queue.md +0 -34
  161. package/src/bot/error-guide.ts +0 -4
  162. package/src/bot/retry-utils.ts +0 -4
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * SwarmController — multi-bot orchestrator for the Weaver Swarm.
3
3
  *
4
- * Manages concurrent async bot loops, budget enforcement, crash recovery,
5
- * dependency resolution, and state persistence.
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.
6
7
  *
7
8
  * Singleton per workspace via `SwarmController.getInstance(projectDir)`.
8
9
  */
@@ -18,6 +19,11 @@ import { EventLog } from './event-log.js';
18
19
  import { runRegistry } from './run-registry.js';
19
20
  import { runWorkflow } from './runner.js';
20
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';
21
27
  import type { Task, RunSummary } from './task-types.js';
22
28
  import type { WorkflowResult } from './types.js';
23
29
 
@@ -25,17 +31,6 @@ import type { WorkflowResult } from './types.js';
25
31
  // Types
26
32
  // ---------------------------------------------------------------------------
27
33
 
28
- export interface BotSlot {
29
- botId: string;
30
- botName: string;
31
- status: 'idle' | 'executing' | 'paused' | 'stopped';
32
- currentTaskId?: string;
33
- currentRunId?: string;
34
- startedAt?: string;
35
- tokensUsed: number;
36
- cost: number;
37
- }
38
-
39
34
  export interface SwarmBudgets {
40
35
  workspace: { limitTokens: number; usedTokens: number; limitCost: number; usedCost: number };
41
36
  session: { limitTokens: number; usedTokens: number; limitCost: number; usedCost: number };
@@ -45,7 +40,8 @@ export interface SwarmState {
45
40
  status: 'idle' | 'running' | 'paused' | 'stopping';
46
41
  startedAt?: string;
47
42
 
48
- bots: Record<string, BotSlot>;
43
+ /** Active bot instances managed by InstanceManager. */
44
+ instances: Record<string, BotInstance>;
49
45
  maxConcurrent: number;
50
46
 
51
47
  budgets: SwarmBudgets;
@@ -80,7 +76,7 @@ export interface SwarmStartConfig {
80
76
  // Constants
81
77
  // ---------------------------------------------------------------------------
82
78
 
83
- const BOT_LOOP_SLEEP_MS = 2000;
79
+ const DISPATCH_LOOP_SLEEP_MS = 2000;
84
80
  const SWARM_STATE_FILE = 'swarm.json';
85
81
 
86
82
  // ---------------------------------------------------------------------------
@@ -95,13 +91,21 @@ export class SwarmController {
95
91
  private readonly statePath: string;
96
92
  private readonly taskStore: TaskStore;
97
93
  private readonly eventLog: SwarmEventLog;
94
+ private readonly orchestrator: Orchestrator;
95
+ private readonly instanceManager: InstanceManager;
96
+ private readonly profileStore: ProfileStore;
98
97
 
99
98
  private state: SwarmState;
100
99
 
101
- /** Active bot loop abort controllers (one per spawned loop). */
102
- private botAborts = new Map<string, AbortController>();
103
- /** Promises for active bot loops (resolved when the loop exits). */
104
- private botLoopPromises = new Map<string, Promise<void>>();
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>();
105
109
 
106
110
  // -----------------------------------------------------------------------
107
111
  // Singleton
@@ -129,9 +133,19 @@ export class SwarmController {
129
133
  this.statePath = path.join(this.weaverDir, SWARM_STATE_FILE);
130
134
  this.taskStore = new TaskStore(projectDir);
131
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);
132
139
 
133
140
  // Load persisted state or create default
134
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
+
135
149
  this._persist();
136
150
  }
137
151
 
@@ -155,30 +169,48 @@ export class SwarmController {
155
169
  const registry = new BotRegistry(this.projectDir);
156
170
  const bots = registry.list();
157
171
 
158
- // Reset bot slots bots beyond maxConcurrent start as 'stopped'
159
- this.state.bots = {};
160
- for (let i = 0; i < bots.length; i++) {
161
- const bot = bots[i]!;
162
- this.state.bots[bot.id] = {
163
- botId: bot.id,
164
- botName: bot.name,
165
- status: i < this.state.maxConcurrent ? 'idle' : 'stopped',
166
- tokensUsed: 0,
167
- cost: 0,
168
- };
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);
169
183
  }
170
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
+
171
206
  this.state.status = 'running';
172
207
  this.state.startedAt = new Date().toISOString();
173
208
  this._persist();
174
209
 
175
210
  this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now() });
176
211
 
177
- // Spawn bot loops (up to maxConcurrent)
178
- const botsToSpawn = bots.slice(0, this.state.maxConcurrent);
179
- for (const bot of botsToSpawn) {
180
- this._spawnBotLoop(bot);
181
- }
212
+ // Start the centralized dispatch loop
213
+ this._startDispatchLoop();
182
214
  }
183
215
 
184
216
  async pause(): Promise<void> {
@@ -189,7 +221,7 @@ export class SwarmController {
189
221
 
190
222
  this.eventLog.emit({ type: 'swarm-paused', timestamp: Date.now() });
191
223
 
192
- // Bots will check the paused status and wind down after their current task
224
+ // The dispatch loop will check paused status and skip routing
193
225
  }
194
226
 
195
227
  async stop(): Promise<void> {
@@ -198,33 +230,40 @@ export class SwarmController {
198
230
  this.state.status = 'stopping';
199
231
  this._persist();
200
232
 
201
- // Signal all bot loops to stop
202
- for (const [, abort] of this.botAborts) {
203
- abort.abort();
233
+ // Signal the dispatch loop to stop
234
+ if (this.dispatchAbort) {
235
+ this.dispatchAbort.abort();
204
236
  }
205
237
 
206
- // Wait for all bot loops to finish
207
- const loopPromises = Array.from(this.botLoopPromises.values());
208
- await Promise.allSettled(loopPromises);
209
-
210
- // Mark all bots as stopped
211
- for (const slot of Object.values(this.state.bots)) {
212
- slot.status = 'stopped';
213
- slot.currentTaskId = undefined;
214
- slot.currentRunId = undefined;
238
+ // Wait for the dispatch loop to finish
239
+ if (this.dispatchLoopPromise) {
240
+ await this.dispatchLoopPromise;
215
241
  }
216
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
+
217
253
  this.state.status = 'idle';
218
254
  this._persist();
219
255
 
220
- this.botAborts.clear();
221
- this.botLoopPromises.clear();
256
+ this.dispatchAbort = null;
257
+ this.dispatchLoopPromise = null;
258
+ this.executionPromises.clear();
222
259
 
223
260
  this.eventLog.emit({ type: 'swarm-stopped', timestamp: Date.now() });
224
261
  }
225
262
 
226
263
  getStatus(): SwarmState {
227
- return { ...this.state, bots: { ...this.state.bots } };
264
+ // Sync instances from InstanceManager before returning
265
+ this._syncInstancesState();
266
+ return { ...this.state, instances: { ...this.state.instances } };
228
267
  }
229
268
 
230
269
  configure(config: SwarmConfig): void {
@@ -243,12 +282,11 @@ export class SwarmController {
243
282
  // -----------------------------------------------------------------------
244
283
 
245
284
  async recover(): Promise<void> {
246
- // Reset all bots to idle
247
- for (const slot of Object.values(this.state.bots)) {
248
- if (slot.status === 'executing') {
249
- slot.status = 'idle';
250
- slot.currentTaskId = undefined;
251
- slot.currentRunId = undefined;
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);
252
290
  }
253
291
  }
254
292
 
@@ -270,6 +308,7 @@ export class SwarmController {
270
308
 
271
309
  // Reset swarm status to idle
272
310
  this.state.status = 'idle';
311
+ this._syncInstancesState();
273
312
  this._persist();
274
313
  }
275
314
 
@@ -277,13 +316,11 @@ export class SwarmController {
277
316
  // Token/cost recording
278
317
  // -----------------------------------------------------------------------
279
318
 
280
- recordTokenUsage(botId: string, _taskId: string, tokensUsed: number, cost: number): void {
281
- // Update bot slot
282
- const slot = this.state.bots[botId];
283
- if (slot) {
284
- slot.tokensUsed += tokensUsed;
285
- slot.cost += cost;
286
- }
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 */ }
287
324
 
288
325
  // Update session budgets
289
326
  this.state.budgets.session.usedTokens += tokensUsed;
@@ -302,49 +339,63 @@ export class SwarmController {
302
339
  this.eventLog.emit({
303
340
  type: 'budget-updated',
304
341
  timestamp: Date.now(),
305
- data: { botId, tokensUsed, cost },
342
+ data: { botId: instanceId, tokensUsed, cost },
306
343
  });
307
344
  }
308
345
 
309
346
  // -----------------------------------------------------------------------
310
- // Bot loop
347
+ // Orchestrator accessors
311
348
  // -----------------------------------------------------------------------
312
349
 
313
- private _spawnBotLoop(bot: BotRegistration): void {
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 {
314
379
  const abort = new AbortController();
315
- this.botAborts.set(bot.id, abort);
380
+ this.dispatchAbort = abort;
316
381
 
317
- const loopPromise = this._botLoop(bot, abort.signal).catch(() => {
318
- // Swallow errors in bot loopsthey self-heal on next iteration
382
+ this.dispatchLoopPromise = this._dispatchLoop(abort.signal).catch(() => {
383
+ // Swallow errors in dispatch loop — self-heals on next cycle
319
384
  });
320
- this.botLoopPromises.set(bot.id, loopPromise);
321
385
  }
322
386
 
323
- private async _botLoop(bot: BotRegistration, signal: AbortSignal): Promise<void> {
324
- const botId = bot.id;
325
-
387
+ private async _dispatchLoop(signal: AbortSignal): Promise<void> {
326
388
  while (!signal.aborted) {
327
389
  // Check if swarm is still running
328
390
  if (this.state.status === 'stopping' || this.state.status === 'idle') break;
329
391
 
330
- // Check if paused
392
+ // Check if paused — sleep and try again
331
393
  if (this.state.status === 'paused') {
332
- this._updateSlot(botId, { status: 'paused' });
333
- await this._sleep(BOT_LOOP_SLEEP_MS, signal);
334
- continue;
335
- }
336
-
337
- // Check per-bot steering
338
- await this._checkSteering(botId);
339
-
340
- // Check if this bot is individually paused
341
- const slot = this.state.bots[botId];
342
- if (slot?.status === 'paused') {
343
- await this._sleep(BOT_LOOP_SLEEP_MS, signal);
394
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
344
395
  continue;
345
396
  }
346
397
 
347
- // Budget check before claiming a task
398
+ // Budget check before routing
348
399
  if (this._isBudgetExceeded()) {
349
400
  this.state.status = 'paused';
350
401
  this._persist();
@@ -356,100 +407,139 @@ export class SwarmController {
356
407
  break;
357
408
  }
358
409
 
359
- // Claim next task
360
- const task = await this.taskStore.claimNext(botId);
361
- if (!task) {
362
- this._updateSlot(botId, { status: 'idle', currentTaskId: undefined, currentRunId: undefined });
363
- await this._sleep(BOT_LOOP_SLEEP_MS, signal);
364
- continue;
410
+ // Check instance-level steering
411
+ const allInstances = this.instanceManager.listAll();
412
+ for (const inst of allInstances) {
413
+ await this._checkSteering(inst.instanceId);
365
414
  }
366
415
 
367
- // Execute the task
368
- this._updateSlot(botId, { status: 'executing', currentTaskId: task.id });
369
- this.eventLog.emit({
370
- type: 'task-claimed',
371
- timestamp: Date.now(),
372
- data: { botId, taskId: task.id, taskTitle: task.title },
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;
373
436
  });
374
437
 
375
- let result: WorkflowResult;
376
- let runId: string;
377
- const startTime = Date.now();
378
-
379
- try {
380
- runId = RunStore.newId();
381
- this._updateSlot(botId, { currentRunId: runId });
382
-
383
- // Build prompt from task context
384
- const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
385
- const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
386
- const prompt = buildTaskPrompt(task, parentTask, siblingTasks);
387
-
388
- // Create per-run event log
389
- const runEventLog = new EventLog(runId);
390
-
391
- // Execute workflow
392
- result = await runWorkflow(bot.filePath, {
393
- runId,
394
- taskId: task.id,
395
- botId,
396
- params: { taskJson: prompt, projectDir: this.projectDir },
397
- eventLog: runEventLog,
398
- });
399
-
400
- runEventLog.done();
401
- } catch (err) {
402
- result = {
403
- success: false,
404
- summary: (err instanceof Error ? err.message : String(err)),
405
- outcome: 'error',
406
- };
407
- runId = runId! ?? 'error-' + Date.now();
438
+ if (routableTasks.length === 0) {
439
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
440
+ continue;
408
441
  }
409
442
 
410
- const durationMs = Date.now() - startTime;
411
-
412
- // Extract run summary
413
- const tokensUsed = (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0);
414
- const costUsed = result.cost?.totalCost ?? 0;
415
-
416
- const runSummary: RunSummary = {
417
- runId: runId!,
418
- botId,
419
- outcome: result.success ? 'success' : 'failed',
420
- summary: result.summary || (result.success ? 'Completed' : 'Failed'),
421
- filesModified: [],
422
- error: result.success ? undefined : (result.summary || 'Unknown error'),
423
- durationMs,
424
- tokensUsed,
425
- cost: costUsed,
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),
426
451
  };
427
452
 
428
- // Release task
429
- const releaseStatus = result.success ? 'done' : 'failed';
430
- await this.taskStore.release(task.id, releaseStatus, runSummary);
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
+ };
431
472
 
432
- // Record token usage
433
- this.recordTokenUsage(botId, task.id, tokensUsed, costUsed);
473
+ // Route
474
+ const output = await this.orchestrator.route(input);
434
475
 
435
- // Update stats
436
- if (result.success) {
437
- this.state.tasksCompleted += 1;
438
- } else {
439
- this.state.tasksFailed += 1;
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
+ }
440
482
  }
441
483
 
442
- // Emit task event
443
- this.eventLog.emit({
444
- type: result.success ? 'task-done' : 'task-failed',
445
- timestamp: Date.now(),
446
- data: { botId, taskId: task.id, taskTitle: task.title, outcome: runSummary.outcome },
447
- });
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
+ }
448
518
 
449
- // Reset bot slot
450
- this._updateSlot(botId, { status: 'idle', currentTaskId: undefined, currentRunId: undefined });
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
+ }
451
541
 
452
- // Post-completion budget check
542
+ // Post-cycle budget check
453
543
  if (this._isBudgetExceeded()) {
454
544
  this.state.status = 'paused';
455
545
  this._persist();
@@ -461,11 +551,102 @@ export class SwarmController {
461
551
  break;
462
552
  }
463
553
 
554
+ this._syncInstancesState();
464
555
  this._persist();
465
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;
466
572
 
467
- // Mark bot as stopped when loop exits
468
- this._updateSlot(botId, { status: 'stopped', currentTaskId: undefined, currentRunId: undefined });
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();
469
650
  }
470
651
 
471
652
  // -----------------------------------------------------------------------
@@ -486,12 +667,26 @@ export class SwarmController {
486
667
  return false;
487
668
  }
488
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
+
489
684
  // -----------------------------------------------------------------------
490
685
  // Steering
491
686
  // -----------------------------------------------------------------------
492
687
 
493
- private async _checkSteering(botId: string): Promise<void> {
494
- const steerPath = path.join(this.weaverDir, `steer-${botId}.json`);
688
+ private async _checkSteering(instanceId: string): Promise<void> {
689
+ const steerPath = path.join(this.weaverDir, `steer-${instanceId}.json`);
495
690
  try {
496
691
  if (!fs.existsSync(steerPath)) return;
497
692
  const raw = fs.readFileSync(steerPath, 'utf-8');
@@ -500,29 +695,29 @@ export class SwarmController {
500
695
 
501
696
  switch (command.command) {
502
697
  case 'pause':
503
- this._updateSlot(botId, { status: 'paused' });
504
- this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId } });
698
+ this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId: instanceId } });
505
699
  break;
506
700
  case 'resume':
507
- this._updateSlot(botId, { status: 'idle' });
508
701
  break;
509
702
  case 'cancel':
510
- // The current run will complete and then the bot won't pick up new tasks while stopping
511
- this._updateSlot(botId, { status: 'stopped' });
703
+ this.instanceManager.stop(instanceId);
512
704
  break;
513
705
  }
514
706
  } catch { /* steer file read error — skip */ }
515
707
  }
516
708
 
517
709
  // -----------------------------------------------------------------------
518
- // Slot management
710
+ // State sync — instances field
519
711
  // -----------------------------------------------------------------------
520
712
 
521
- private _updateSlot(botId: string, patch: Partial<BotSlot>): void {
522
- const slot = this.state.bots[botId];
523
- if (!slot) return;
524
- Object.assign(slot, patch);
525
- this._persist();
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
+ }
526
721
  }
527
722
 
528
723
  // -----------------------------------------------------------------------
@@ -533,7 +728,9 @@ export class SwarmController {
533
728
  try {
534
729
  if (fs.existsSync(this.statePath)) {
535
730
  const raw = fs.readFileSync(this.statePath, 'utf-8');
536
- return JSON.parse(raw) as SwarmState;
731
+ const data = JSON.parse(raw) as SwarmState;
732
+ if (!data.instances) data.instances = {};
733
+ return data;
537
734
  }
538
735
  } catch { /* corrupt file — use defaults */ }
539
736
  return this._defaultState();
@@ -542,7 +739,7 @@ export class SwarmController {
542
739
  private _defaultState(): SwarmState {
543
740
  return {
544
741
  status: 'idle',
545
- bots: {},
742
+ instances: {},
546
743
  maxConcurrent: 3,
547
744
  budgets: {
548
745
  workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
@@ -562,7 +759,9 @@ export class SwarmController {
562
759
  const tmpPath = this.statePath + '.tmp';
563
760
  fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2), 'utf-8');
564
761
  fs.renameSync(tmpPath, this.statePath);
565
- } catch { /* non-fatal persistence error */ }
762
+ } catch (err) {
763
+ console.error('[swarm] Failed to persist state:', err instanceof Error ? err.message : err);
764
+ }
566
765
  }
567
766
 
568
767
  // -----------------------------------------------------------------------