@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
  */
@@ -15,10 +16,14 @@ import { RunStore } from './run-store.js';
15
16
  import { EventLog } from './event-log.js';
16
17
  import { runWorkflow } from './runner.js';
17
18
  import { buildTaskPrompt } from './task-prompt-builder.js';
19
+ import { Orchestrator } from './orchestrator.js';
20
+ import { AIRouterImpl } from './ai-router.js';
21
+ import { InstanceManager } from './instance-manager.js';
22
+ import { ProfileStore } from './profile-store.js';
18
23
  // ---------------------------------------------------------------------------
19
24
  // Constants
20
25
  // ---------------------------------------------------------------------------
21
- const BOT_LOOP_SLEEP_MS = 2000;
26
+ const DISPATCH_LOOP_SLEEP_MS = 2000;
22
27
  const SWARM_STATE_FILE = 'swarm.json';
23
28
  // ---------------------------------------------------------------------------
24
29
  // SwarmController
@@ -30,11 +35,18 @@ export class SwarmController {
30
35
  statePath;
31
36
  taskStore;
32
37
  eventLog;
38
+ orchestrator;
39
+ instanceManager;
40
+ profileStore;
33
41
  state;
34
- /** Active bot loop abort controllers (one per spawned loop). */
35
- botAborts = new Map();
36
- /** Promises for active bot loops (resolved when the loop exits). */
37
- botLoopPromises = new Map();
42
+ /** Abort controller for the single dispatch loop. */
43
+ dispatchAbort = null;
44
+ /** Promise for the dispatch loop (resolved when the loop exits). */
45
+ dispatchLoopPromise = null;
46
+ /** Promises for currently executing tasks (instanceId -> Promise). */
47
+ executionPromises = new Map();
48
+ /** Map of profileId -> BotRegistration for workflow execution. */
49
+ profileBotMap = new Map();
38
50
  // -----------------------------------------------------------------------
39
51
  // Singleton
40
52
  // -----------------------------------------------------------------------
@@ -59,8 +71,16 @@ export class SwarmController {
59
71
  this.statePath = path.join(this.weaverDir, SWARM_STATE_FILE);
60
72
  this.taskStore = new TaskStore(projectDir);
61
73
  this.eventLog = new SwarmEventLog(projectDir);
74
+ this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
75
+ this.instanceManager = new InstanceManager();
76
+ this.profileStore = new ProfileStore(projectDir);
62
77
  // Load persisted state or create default
63
78
  this.state = this._loadState();
79
+ // Crash recovery: no dispatch loop survives a process restart, so reset
80
+ // any stale active status back to idle so start() is not short-circuited.
81
+ if (this.state.status === 'running' || this.state.status === 'paused' || this.state.status === 'stopping') {
82
+ this.state.status = 'idle';
83
+ }
64
84
  this._persist();
65
85
  }
66
86
  // -----------------------------------------------------------------------
@@ -82,27 +102,39 @@ export class SwarmController {
82
102
  // Discover bots from registry
83
103
  const registry = new BotRegistry(this.projectDir);
84
104
  const bots = registry.list();
85
- // Reset bot slots bots beyond maxConcurrent start as 'stopped'
86
- this.state.bots = {};
87
- for (let i = 0; i < bots.length; i++) {
88
- const bot = bots[i];
89
- this.state.bots[bot.id] = {
90
- botId: bot.id,
91
- botName: bot.name,
92
- status: i < this.state.maxConcurrent ? 'idle' : 'stopped',
93
- tokensUsed: 0,
94
- cost: 0,
95
- };
105
+ // Ensure default profiles exist for all registered bots
106
+ this.profileStore.ensureDefaultProfiles(bots.map((b) => ({ id: b.id, name: b.name, icon: b.icon, color: b.color })));
107
+ // Load profiles and build profile-to-bot mapping
108
+ const profiles = this.profileStore.list();
109
+ this.profileBotMap.clear();
110
+ for (const profile of profiles) {
111
+ const bot = bots.find((b) => b.id === profile.botId);
112
+ if (bot)
113
+ this.profileBotMap.set(profile.id, bot);
114
+ }
115
+ // Stop any existing instances
116
+ this.instanceManager.stopAll();
117
+ // Spawn initial instances per profile (up to maxConcurrent total)
118
+ let totalSpawned = 0;
119
+ for (const profile of profiles) {
120
+ if (totalSpawned >= this.state.maxConcurrent)
121
+ break;
122
+ // Spawn at least minInstances, or 1 if minInstances is 0
123
+ const toSpawn = Math.max(profile.minInstances, 1);
124
+ const clamped = Math.min(toSpawn, this.state.maxConcurrent - totalSpawned);
125
+ for (let i = 0; i < clamped; i++) {
126
+ this.instanceManager.spawn(profile);
127
+ totalSpawned++;
128
+ }
96
129
  }
130
+ // Populate state fields
131
+ this._syncInstancesState();
97
132
  this.state.status = 'running';
98
133
  this.state.startedAt = new Date().toISOString();
99
134
  this._persist();
100
135
  this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now() });
101
- // Spawn bot loops (up to maxConcurrent)
102
- const botsToSpawn = bots.slice(0, this.state.maxConcurrent);
103
- for (const bot of botsToSpawn) {
104
- this._spawnBotLoop(bot);
105
- }
136
+ // Start the centralized dispatch loop
137
+ this._startDispatchLoop();
106
138
  }
107
139
  async pause() {
108
140
  if (this.state.status !== 'running')
@@ -110,34 +142,39 @@ export class SwarmController {
110
142
  this.state.status = 'paused';
111
143
  this._persist();
112
144
  this.eventLog.emit({ type: 'swarm-paused', timestamp: Date.now() });
113
- // Bots will check the paused status and wind down after their current task
145
+ // The dispatch loop will check paused status and skip routing
114
146
  }
115
147
  async stop() {
116
148
  if (this.state.status === 'idle')
117
149
  return;
118
150
  this.state.status = 'stopping';
119
151
  this._persist();
120
- // Signal all bot loops to stop
121
- for (const [, abort] of this.botAborts) {
122
- abort.abort();
152
+ // Signal the dispatch loop to stop
153
+ if (this.dispatchAbort) {
154
+ this.dispatchAbort.abort();
123
155
  }
124
- // Wait for all bot loops to finish
125
- const loopPromises = Array.from(this.botLoopPromises.values());
126
- await Promise.allSettled(loopPromises);
127
- // Mark all bots as stopped
128
- for (const slot of Object.values(this.state.bots)) {
129
- slot.status = 'stopped';
130
- slot.currentTaskId = undefined;
131
- slot.currentRunId = undefined;
156
+ // Wait for the dispatch loop to finish
157
+ if (this.dispatchLoopPromise) {
158
+ await this.dispatchLoopPromise;
132
159
  }
160
+ // Wait for all running executions to finish
161
+ const execPromises = Array.from(this.executionPromises.values());
162
+ await Promise.allSettled(execPromises);
163
+ // Stop all instances
164
+ this.instanceManager.stopAll();
165
+ // Update state
166
+ this._syncInstancesState();
133
167
  this.state.status = 'idle';
134
168
  this._persist();
135
- this.botAborts.clear();
136
- this.botLoopPromises.clear();
169
+ this.dispatchAbort = null;
170
+ this.dispatchLoopPromise = null;
171
+ this.executionPromises.clear();
137
172
  this.eventLog.emit({ type: 'swarm-stopped', timestamp: Date.now() });
138
173
  }
139
174
  getStatus() {
140
- return { ...this.state, bots: { ...this.state.bots } };
175
+ // Sync instances from InstanceManager before returning
176
+ this._syncInstancesState();
177
+ return { ...this.state, instances: { ...this.state.instances } };
141
178
  }
142
179
  configure(config) {
143
180
  if (config.maxConcurrent !== undefined)
@@ -160,12 +197,11 @@ export class SwarmController {
160
197
  // Crash recovery
161
198
  // -----------------------------------------------------------------------
162
199
  async recover() {
163
- // Reset all bots to idle
164
- for (const slot of Object.values(this.state.bots)) {
165
- if (slot.status === 'executing') {
166
- slot.status = 'idle';
167
- slot.currentTaskId = undefined;
168
- slot.currentRunId = undefined;
200
+ // Reset all instances to idle
201
+ const allInstances = this.instanceManager.listAll();
202
+ for (const inst of allInstances) {
203
+ if (inst.status === 'executing') {
204
+ this.instanceManager.markIdle(inst.instanceId, false);
169
205
  }
170
206
  }
171
207
  // Reset in-progress tasks
@@ -186,18 +222,18 @@ export class SwarmController {
186
222
  }
187
223
  // Reset swarm status to idle
188
224
  this.state.status = 'idle';
225
+ this._syncInstancesState();
189
226
  this._persist();
190
227
  }
191
228
  // -----------------------------------------------------------------------
192
229
  // Token/cost recording
193
230
  // -----------------------------------------------------------------------
194
- recordTokenUsage(botId, _taskId, tokensUsed, cost) {
195
- // Update bot slot
196
- const slot = this.state.bots[botId];
197
- if (slot) {
198
- slot.tokensUsed += tokensUsed;
199
- slot.cost += cost;
231
+ recordTokenUsage(instanceId, _taskId, tokensUsed, cost) {
232
+ // Update instance in InstanceManager
233
+ try {
234
+ this.instanceManager.recordUsage(instanceId, tokensUsed, cost);
200
235
  }
236
+ catch { /* instance may not exist if stopped */ }
201
237
  // Update session budgets
202
238
  this.state.budgets.session.usedTokens += tokensUsed;
203
239
  this.state.budgets.session.usedCost += cost;
@@ -211,41 +247,53 @@ export class SwarmController {
211
247
  this.eventLog.emit({
212
248
  type: 'budget-updated',
213
249
  timestamp: Date.now(),
214
- data: { botId, tokensUsed, cost },
250
+ data: { botId: instanceId, tokensUsed, cost },
215
251
  });
216
252
  }
217
253
  // -----------------------------------------------------------------------
218
- // Bot loop
254
+ // Orchestrator accessors
255
+ // -----------------------------------------------------------------------
256
+ getProfileStore() {
257
+ return this.profileStore;
258
+ }
259
+ getInstanceManager() {
260
+ return this.instanceManager;
261
+ }
262
+ getOrchestrator() {
263
+ return this.orchestrator;
264
+ }
265
+ getOrchestratorStatus() {
266
+ const decisions = this.orchestrator.getDecisionLog();
267
+ const stats = {
268
+ totalDecisions: decisions.length,
269
+ assignmentsByMethod: {},
270
+ };
271
+ for (const d of decisions) {
272
+ stats.assignmentsByMethod[d.method] = (stats.assignmentsByMethod[d.method] ?? 0) + 1;
273
+ }
274
+ return { decisions, stats };
275
+ }
276
+ // -----------------------------------------------------------------------
277
+ // Dispatch loop
219
278
  // -----------------------------------------------------------------------
220
- _spawnBotLoop(bot) {
279
+ _startDispatchLoop() {
221
280
  const abort = new AbortController();
222
- this.botAborts.set(bot.id, abort);
223
- const loopPromise = this._botLoop(bot, abort.signal).catch(() => {
224
- // Swallow errors in bot loopsthey self-heal on next iteration
281
+ this.dispatchAbort = abort;
282
+ this.dispatchLoopPromise = this._dispatchLoop(abort.signal).catch(() => {
283
+ // Swallow errors in dispatch loop — self-heals on next cycle
225
284
  });
226
- this.botLoopPromises.set(bot.id, loopPromise);
227
285
  }
228
- async _botLoop(bot, signal) {
229
- const botId = bot.id;
286
+ async _dispatchLoop(signal) {
230
287
  while (!signal.aborted) {
231
288
  // Check if swarm is still running
232
289
  if (this.state.status === 'stopping' || this.state.status === 'idle')
233
290
  break;
234
- // Check if paused
291
+ // Check if paused — sleep and try again
235
292
  if (this.state.status === 'paused') {
236
- this._updateSlot(botId, { status: 'paused' });
237
- await this._sleep(BOT_LOOP_SLEEP_MS, signal);
293
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
238
294
  continue;
239
295
  }
240
- // Check per-bot steering
241
- await this._checkSteering(botId);
242
- // Check if this bot is individually paused
243
- const slot = this.state.bots[botId];
244
- if (slot?.status === 'paused') {
245
- await this._sleep(BOT_LOOP_SLEEP_MS, signal);
246
- continue;
247
- }
248
- // Budget check — before claiming a task
296
+ // Budget check before routing
249
297
  if (this._isBudgetExceeded()) {
250
298
  this.state.status = 'paused';
251
299
  this._persist();
@@ -256,86 +304,131 @@ export class SwarmController {
256
304
  });
257
305
  break;
258
306
  }
259
- // Claim next task
260
- const task = await this.taskStore.claimNext(botId);
261
- if (!task) {
262
- this._updateSlot(botId, { status: 'idle', currentTaskId: undefined, currentRunId: undefined });
263
- await this._sleep(BOT_LOOP_SLEEP_MS, signal);
264
- continue;
307
+ // Check instance-level steering
308
+ const allInstances = this.instanceManager.listAll();
309
+ for (const inst of allInstances) {
310
+ await this._checkSteering(inst.instanceId);
265
311
  }
266
- // Execute the task
267
- this._updateSlot(botId, { status: 'executing', currentTaskId: task.id });
268
- this.eventLog.emit({
269
- type: 'task-claimed',
270
- timestamp: Date.now(),
271
- data: { botId, taskId: task.id, taskTitle: task.title },
312
+ // Collect pending tasks (and all tasks for dependency checks)
313
+ const pendingTasks = await this.taskStore.list({ status: ['pending', 'failed'] });
314
+ const allTasks = await this.taskStore.list();
315
+ const routableTasks = pendingTasks.filter((t) => {
316
+ // Skip parent tasks
317
+ if (t.isParent)
318
+ return false;
319
+ // Skip tasks that have exhausted retries
320
+ if (t.status === 'failed' && t.attempt >= t.maxAttempts)
321
+ return false;
322
+ // Skip tasks over per-task budget
323
+ if (t.budgetTokens !== undefined && t.tokensUsed >= t.budgetTokens)
324
+ return false;
325
+ if (t.budgetCost !== undefined && t.costUsed >= t.budgetCost)
326
+ return false;
327
+ // Skip tasks whose dependencies are not all done
328
+ if (t.dependsOn && t.dependsOn.length > 0) {
329
+ const allDepsDone = t.dependsOn.every((depId) => {
330
+ const dep = allTasks.find((d) => d.id === depId);
331
+ return dep && dep.status === 'done';
332
+ });
333
+ if (!allDepsDone)
334
+ return false;
335
+ }
336
+ return true;
272
337
  });
273
- let result;
274
- let runId;
275
- const startTime = Date.now();
276
- try {
277
- runId = RunStore.newId();
278
- this._updateSlot(botId, { currentRunId: runId });
279
- // Build prompt from task context
280
- const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
281
- const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
282
- const prompt = buildTaskPrompt(task, parentTask, siblingTasks);
283
- // Create per-run event log
284
- const runEventLog = new EventLog(runId);
285
- // Execute workflow
286
- result = await runWorkflow(bot.filePath, {
287
- runId,
288
- taskId: task.id,
289
- botId,
290
- params: { taskJson: prompt, projectDir: this.projectDir },
291
- eventLog: runEventLog,
292
- });
293
- runEventLog.done();
294
- }
295
- catch (err) {
296
- result = {
297
- success: false,
298
- summary: (err instanceof Error ? err.message : String(err)),
299
- outcome: 'error',
300
- };
301
- runId = runId ?? 'error-' + Date.now();
338
+ if (routableTasks.length === 0) {
339
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
340
+ continue;
302
341
  }
303
- const durationMs = Date.now() - startTime;
304
- // Extract run summary
305
- const tokensUsed = (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0);
306
- const costUsed = result.cost?.totalCost ?? 0;
307
- const runSummary = {
308
- runId: runId,
309
- botId,
310
- outcome: result.success ? 'success' : 'failed',
311
- summary: result.summary || (result.success ? 'Completed' : 'Failed'),
312
- filesModified: [],
313
- error: result.success ? undefined : (result.summary || 'Unknown error'),
314
- durationMs,
315
- tokensUsed,
316
- cost: costUsed,
342
+ // Build orchestrator input
343
+ const profiles = this.profileStore.list();
344
+ const instances = this.instanceManager.listAll();
345
+ const { workspace, session } = this.state.budgets;
346
+ const budgetRemaining = {
347
+ tokens: this._remainingBudget(session.limitTokens, session.usedTokens, workspace.limitTokens, workspace.usedTokens),
348
+ cost: this._remainingBudget(session.limitCost, session.usedCost, workspace.limitCost, workspace.usedCost),
317
349
  };
318
- // Release task
319
- const releaseStatus = result.success ? 'done' : 'failed';
320
- await this.taskStore.release(task.id, releaseStatus, runSummary);
321
- // Record token usage
322
- this.recordTokenUsage(botId, task.id, tokensUsed, costUsed);
323
- // Update stats
324
- if (result.success) {
325
- this.state.tasksCompleted += 1;
350
+ const input = {
351
+ pendingTasks: routableTasks.map((t) => ({
352
+ id: t.id,
353
+ title: t.title,
354
+ description: t.description,
355
+ priority: t.priority,
356
+ complexity: t.complexity ?? 'moderate',
357
+ assignedProfile: t.assignedProfile,
358
+ context: {
359
+ runSummaries: t.context.runSummaries.map((rs) => ({
360
+ outcome: rs.outcome,
361
+ botId: rs.botId,
362
+ })),
363
+ },
364
+ })),
365
+ profiles,
366
+ instances,
367
+ budgetRemaining,
368
+ };
369
+ // Route
370
+ const output = await this.orchestrator.route(input);
371
+ // Apply scale actions
372
+ for (const sa of output.scaleActions) {
373
+ const profile = profiles.find((p) => p.id === sa.profileId);
374
+ if (profile) {
375
+ this.instanceManager.scaleTo(profile, sa.targetInstances);
376
+ }
377
+ }
378
+ // Apply assignments
379
+ for (const assignment of output.assignments) {
380
+ const profile = profiles.find((p) => p.id === assignment.profileId);
381
+ const bot = this.profileBotMap.get(assignment.profileId);
382
+ if (!profile || !bot)
383
+ continue;
384
+ try {
385
+ // Assign task in TaskStore
386
+ await this.taskStore.assignToInstance(assignment.taskId, assignment.instanceId, assignment.profileId);
387
+ // Mark instance as executing and store runId on task for live streaming
388
+ const runId = RunStore.newId();
389
+ this.instanceManager.markExecuting(assignment.instanceId, assignment.taskId, runId);
390
+ await this.taskStore.update(assignment.taskId, { currentRunId: runId });
391
+ // Sync state for dashboard
392
+ this._syncInstancesState();
393
+ this._persist();
394
+ this.eventLog.emit({
395
+ type: 'task-claimed',
396
+ timestamp: Date.now(),
397
+ data: { botId: assignment.instanceId, taskId: assignment.taskId, profileId: assignment.profileId, method: assignment.method },
398
+ });
399
+ // Spawn execution (non-blocking)
400
+ const execPromise = this._executeTask(assignment.instanceId, assignment.taskId, runId, bot, profile).catch(() => {
401
+ // errors handled inside _executeTask
402
+ });
403
+ this.executionPromises.set(assignment.instanceId, execPromise);
404
+ }
405
+ catch {
406
+ // Assignment failed (task may have been grabbed by another cycle) — skip
407
+ }
408
+ }
409
+ // Wait for completions via Promise.race with a sleep timeout
410
+ if (this.executionPromises.size > 0) {
411
+ const sleepPromise = this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
412
+ await Promise.race([
413
+ Promise.race(Array.from(this.executionPromises.values())),
414
+ sleepPromise,
415
+ ]);
326
416
  }
327
417
  else {
328
- this.state.tasksFailed += 1;
418
+ await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
329
419
  }
330
- // Emit task event
331
- this.eventLog.emit({
332
- type: result.success ? 'task-done' : 'task-failed',
333
- timestamp: Date.now(),
334
- data: { botId, taskId: task.id, taskTitle: task.title, outcome: runSummary.outcome },
335
- });
336
- // Reset bot slot
337
- this._updateSlot(botId, { status: 'idle', currentTaskId: undefined, currentRunId: undefined });
338
- // Post-completion budget check
420
+ // Clean up completed execution promises
421
+ for (const [instanceId, promise] of this.executionPromises) {
422
+ // Check if resolved by racing with an immediately resolved promise
423
+ const result = await Promise.race([
424
+ promise.then(() => 'done'),
425
+ Promise.resolve('pending'),
426
+ ]);
427
+ if (result === 'done') {
428
+ this.executionPromises.delete(instanceId);
429
+ }
430
+ }
431
+ // Post-cycle budget check
339
432
  if (this._isBudgetExceeded()) {
340
433
  this.state.status = 'paused';
341
434
  this._persist();
@@ -346,10 +439,84 @@ export class SwarmController {
346
439
  });
347
440
  break;
348
441
  }
442
+ this._syncInstancesState();
349
443
  this._persist();
350
444
  }
351
- // Mark bot as stopped when loop exits
352
- this._updateSlot(botId, { status: 'stopped', currentTaskId: undefined, currentRunId: undefined });
445
+ }
446
+ // -----------------------------------------------------------------------
447
+ // Task execution
448
+ // -----------------------------------------------------------------------
449
+ async _executeTask(instanceId, taskId, runId, bot, profile) {
450
+ const startTime = Date.now();
451
+ let result;
452
+ try {
453
+ const task = await this.taskStore.get(taskId);
454
+ if (!task)
455
+ throw new Error(`Task not found: ${taskId}`);
456
+ // Build prompt from task context
457
+ const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
458
+ const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
459
+ const prompt = buildTaskPrompt(task, parentTask, siblingTasks, profile.preferences);
460
+ // Create per-run event log
461
+ const runEventLog = new EventLog(runId);
462
+ // Execute workflow
463
+ result = await runWorkflow(bot.filePath, {
464
+ runId,
465
+ taskId,
466
+ botId: instanceId,
467
+ config: { provider: 'auto' },
468
+ params: { taskJson: prompt, projectDir: this.projectDir },
469
+ eventLog: runEventLog,
470
+ });
471
+ runEventLog.done();
472
+ }
473
+ catch (err) {
474
+ result = {
475
+ success: false,
476
+ summary: (err instanceof Error ? err.message : String(err)),
477
+ outcome: 'error',
478
+ };
479
+ }
480
+ const durationMs = Date.now() - startTime;
481
+ // Extract run summary
482
+ const tokensUsed = Math.max(0, (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0));
483
+ const costUsed = Math.max(0, result.cost?.totalCost ?? 0);
484
+ const runSummary = {
485
+ runId,
486
+ botId: instanceId,
487
+ outcome: result.success ? 'success' : 'failed',
488
+ summary: result.summary || (result.success ? 'Completed' : 'Failed'),
489
+ filesModified: [],
490
+ error: result.success ? undefined : (result.summary || 'Unknown error'),
491
+ durationMs,
492
+ tokensUsed,
493
+ cost: costUsed,
494
+ };
495
+ // Release task
496
+ const releaseStatus = result.success ? 'done' : 'failed';
497
+ await this.taskStore.release(taskId, releaseStatus, runSummary);
498
+ // Record token usage
499
+ this.recordTokenUsage(instanceId, taskId, tokensUsed, costUsed);
500
+ // Update stats
501
+ if (result.success) {
502
+ this.state.tasksCompleted += 1;
503
+ }
504
+ else {
505
+ this.state.tasksFailed += 1;
506
+ }
507
+ // Mark instance as idle
508
+ try {
509
+ this.instanceManager.markIdle(instanceId, result.success);
510
+ }
511
+ catch { /* instance may have been stopped */ }
512
+ // Emit task event
513
+ this.eventLog.emit({
514
+ type: result.success ? 'task-done' : 'task-failed',
515
+ timestamp: Date.now(),
516
+ data: { botId: instanceId, taskId, outcome: runSummary.outcome },
517
+ });
518
+ this._syncInstancesState();
519
+ this._persist();
353
520
  }
354
521
  // -----------------------------------------------------------------------
355
522
  // Budget checks
@@ -368,11 +535,22 @@ export class SwarmController {
368
535
  return true;
369
536
  return false;
370
537
  }
538
+ /** Calculate remaining budget across session and workspace. */
539
+ _remainingBudget(sessionLimit, sessionUsed, workspaceLimit, workspaceUsed) {
540
+ const remaining = [];
541
+ if (sessionLimit > 0)
542
+ remaining.push(sessionLimit - sessionUsed);
543
+ if (workspaceLimit > 0)
544
+ remaining.push(workspaceLimit - workspaceUsed);
545
+ if (remaining.length === 0)
546
+ return Number.MAX_SAFE_INTEGER;
547
+ return Math.min(...remaining);
548
+ }
371
549
  // -----------------------------------------------------------------------
372
550
  // Steering
373
551
  // -----------------------------------------------------------------------
374
- async _checkSteering(botId) {
375
- const steerPath = path.join(this.weaverDir, `steer-${botId}.json`);
552
+ async _checkSteering(instanceId) {
553
+ const steerPath = path.join(this.weaverDir, `steer-${instanceId}.json`);
376
554
  try {
377
555
  if (!fs.existsSync(steerPath))
378
556
  return;
@@ -381,29 +559,27 @@ export class SwarmController {
381
559
  const command = JSON.parse(raw);
382
560
  switch (command.command) {
383
561
  case 'pause':
384
- this._updateSlot(botId, { status: 'paused' });
385
- this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId } });
562
+ this.eventLog.emit({ type: 'bot-paused', timestamp: Date.now(), data: { botId: instanceId } });
386
563
  break;
387
564
  case 'resume':
388
- this._updateSlot(botId, { status: 'idle' });
389
565
  break;
390
566
  case 'cancel':
391
- // The current run will complete and then the bot won't pick up new tasks while stopping
392
- this._updateSlot(botId, { status: 'stopped' });
567
+ this.instanceManager.stop(instanceId);
393
568
  break;
394
569
  }
395
570
  }
396
571
  catch { /* steer file read error — skip */ }
397
572
  }
398
573
  // -----------------------------------------------------------------------
399
- // Slot management
574
+ // State sync — instances field
400
575
  // -----------------------------------------------------------------------
401
- _updateSlot(botId, patch) {
402
- const slot = this.state.bots[botId];
403
- if (!slot)
404
- return;
405
- Object.assign(slot, patch);
406
- this._persist();
576
+ _syncInstancesState() {
577
+ const allInstances = this.instanceManager.listAll();
578
+ // Build instances record
579
+ this.state.instances = {};
580
+ for (const inst of allInstances) {
581
+ this.state.instances[inst.instanceId] = inst;
582
+ }
407
583
  }
408
584
  // -----------------------------------------------------------------------
409
585
  // State persistence
@@ -412,7 +588,10 @@ export class SwarmController {
412
588
  try {
413
589
  if (fs.existsSync(this.statePath)) {
414
590
  const raw = fs.readFileSync(this.statePath, 'utf-8');
415
- return JSON.parse(raw);
591
+ const data = JSON.parse(raw);
592
+ if (!data.instances)
593
+ data.instances = {};
594
+ return data;
416
595
  }
417
596
  }
418
597
  catch { /* corrupt file — use defaults */ }
@@ -421,7 +600,7 @@ export class SwarmController {
421
600
  _defaultState() {
422
601
  return {
423
602
  status: 'idle',
424
- bots: {},
603
+ instances: {},
425
604
  maxConcurrent: 3,
426
605
  budgets: {
427
606
  workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
@@ -441,7 +620,9 @@ export class SwarmController {
441
620
  fs.writeFileSync(tmpPath, JSON.stringify(this.state, null, 2), 'utf-8');
442
621
  fs.renameSync(tmpPath, this.statePath);
443
622
  }
444
- catch { /* non-fatal persistence error */ }
623
+ catch (err) {
624
+ console.error('[swarm] Failed to persist state:', err instanceof Error ? err.message : err);
625
+ }
445
626
  }
446
627
  // -----------------------------------------------------------------------
447
628
  // Utilities