@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.
- 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 +173 -19
- 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/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/improve-loop.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/session-state.d.ts +25 -0
- package/dist/bot/session-state.d.ts.map +1 -0
- package/dist/bot/session-state.js +110 -0
- package/dist/bot/session-state.js.map +1 -0
- package/dist/bot/swarm-controller.d.ts +37 -21
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +344 -163
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-prompt-builder.d.ts +2 -1
- package/dist/bot/task-prompt-builder.d.ts.map +1 -1
- package/dist/bot/task-prompt-builder.js +33 -10
- package/dist/bot/task-prompt-builder.js.map +1 -1
- package/dist/bot/task-queue.d.ts +46 -0
- package/dist/bot/task-queue.d.ts.map +1 -0
- package/dist/bot/task-queue.js +237 -0
- package/dist/bot/task-queue.js.map +1 -0
- package/dist/bot/task-store.d.ts +1 -6
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +27 -78
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/task-types.d.ts +8 -4
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +2 -3
- 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/mcp-tools.d.ts +17 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +98 -232
- package/dist/mcp-tools.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 -28
- 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-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 +51 -90
- package/dist/ui/bot-slot-card.js +87 -122
- package/dist/ui/budget-bar.js +5 -3
- package/dist/ui/chat-task-result.js +4 -7
- 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 +36 -27
- package/dist/ui/swarm-dashboard.js +2034 -736
- package/dist/ui/task-create-form.js +39 -116
- package/dist/ui/task-detail-view.js +490 -239
- package/dist/ui/task-pool-list.js +69 -94
- 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 +253 -66
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +184 -18
- package/src/bot/ai-router.ts +132 -0
- package/src/bot/bot-registry.ts +2 -2
- package/src/bot/conversation-store.ts +2 -1
- package/src/bot/improve-loop.ts +6 -6
- 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/swarm-controller.ts +385 -186
- package/src/bot/task-prompt-builder.ts +37 -6
- package/src/bot/task-store.ts +28 -89
- package/src/bot/task-types.ts +10 -4
- package/src/cli-handlers.ts +2 -3
- 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/mcp-tools.ts +129 -320
- 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 -26
- package/src/ui/bot-constants.ts +192 -0
- package/src/ui/bot-panel.tsx +55 -79
- package/src/ui/bot-slot-card.tsx +69 -117
- package/src/ui/budget-bar.tsx +5 -3
- package/src/ui/chat-task-result.tsx +6 -9
- 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 +35 -31
- package/src/ui/swarm-dashboard.tsx +409 -80
- package/src/ui/task-create-form.tsx +29 -119
- package/src/ui/task-detail-view.tsx +461 -215
- package/src/ui/task-pool-list.tsx +74 -95
- 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/src/bot/error-guide.ts +0 -4
- 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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
102
|
-
private
|
|
103
|
-
/**
|
|
104
|
-
private
|
|
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
|
-
//
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
//
|
|
178
|
-
|
|
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
|
-
//
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
// Signal the dispatch loop to stop
|
|
234
|
+
if (this.dispatchAbort) {
|
|
235
|
+
this.dispatchAbort.abort();
|
|
204
236
|
}
|
|
205
237
|
|
|
206
|
-
// Wait for
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
221
|
-
this.
|
|
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
|
-
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
281
|
-
// Update
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
//
|
|
347
|
+
// Orchestrator accessors
|
|
311
348
|
// -----------------------------------------------------------------------
|
|
312
349
|
|
|
313
|
-
|
|
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.
|
|
380
|
+
this.dispatchAbort = abort;
|
|
316
381
|
|
|
317
|
-
|
|
318
|
-
// Swallow errors in
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
this.
|
|
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
|
-
//
|
|
368
|
-
this.
|
|
369
|
-
this.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
//
|
|
433
|
-
this.
|
|
473
|
+
// Route
|
|
474
|
+
const output = await this.orchestrator.route(input);
|
|
434
475
|
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
//
|
|
450
|
-
this.
|
|
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-
|
|
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
|
-
|
|
468
|
-
|
|
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(
|
|
494
|
-
const steerPath = path.join(this.weaverDir, `steer-${
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
710
|
+
// State sync — instances field
|
|
519
711
|
// -----------------------------------------------------------------------
|
|
520
712
|
|
|
521
|
-
private
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.error('[swarm] Failed to persist state:', err instanceof Error ? err.message : err);
|
|
764
|
+
}
|
|
566
765
|
}
|
|
567
766
|
|
|
568
767
|
// -----------------------------------------------------------------------
|