@frumu/tandem-panel 0.4.8 → 0.4.10

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.
@@ -0,0 +1,949 @@
1
+ import { readFile, readdir, stat } from "fs/promises";
2
+ import { resolve } from "path";
3
+ import { deriveRunBudget, inferStatusFromEvents, mapOrchestratorPath } from "../services/orchestratorService.js";
4
+
5
+ export function createSwarmApiHandler(deps) {
6
+ const {
7
+ PORTAL_PORT,
8
+ REPO_ROOT,
9
+ ENGINE_URL,
10
+ swarmState,
11
+ getSwarmRunController,
12
+ upsertSwarmRunController,
13
+ setActiveSwarmRunId,
14
+ isLocalEngineUrl,
15
+ sendJson,
16
+ readJsonBody,
17
+ workspaceExistsAsDirectory,
18
+ loadHiddenSwarmRunIds,
19
+ saveHiddenSwarmRunIds,
20
+ engineRequestJson,
21
+ appendContextRunEvent,
22
+ contextRunStatusToSwarmStatus,
23
+ startSwarm,
24
+ detectExecutorMode,
25
+ startRunExecutor,
26
+ requeueInProgressSteps,
27
+ transitionBlackboardTask,
28
+ contextRunSnapshot,
29
+ contextRunToTasks,
30
+ } = deps;
31
+
32
+ return async function handleSwarmApi(req, res, session) {
33
+ const url = new URL(req.url, `http://127.0.0.1:${PORTAL_PORT}`);
34
+ const routePath = mapOrchestratorPath(url.pathname);
35
+ const resolveRunId = (...values) =>
36
+ values
37
+ .map((value) => String(value || "").trim())
38
+ .find((value) => value.length > 0) || "";
39
+ const controllerFor = (runId = "") => getSwarmRunController(resolveRunId(runId));
40
+ const readRunSetting = (runId, key, fallback) => {
41
+ const controller = controllerFor(runId);
42
+ return controller?.[key] ?? fallback;
43
+ };
44
+ const statusFromRun = async (runId) => {
45
+ if (!runId) return null;
46
+ try {
47
+ const payload = await engineRequestJson(session, `/context/runs/${encodeURIComponent(runId)}`);
48
+ return payload?.run || null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ };
53
+
54
+ if (routePath === "/api/swarm/status" && req.method === "GET") {
55
+ const statusRunId = resolveRunId(url.searchParams.get("runId"), swarmState.runId);
56
+ const run = await statusFromRun(statusRunId);
57
+ if (run) {
58
+ let status = contextRunStatusToSwarmStatus(run.status);
59
+ if (status === "planning") {
60
+ const eventsPayload = await engineRequestJson(
61
+ session,
62
+ `/context/runs/${encodeURIComponent(String(run?.run_id || statusRunId || ""))}/events?tail=60`
63
+ ).catch(() => ({ events: [] }));
64
+ status = contextRunStatusToSwarmStatus(inferStatusFromEvents(status, eventsPayload?.events));
65
+ }
66
+ if (statusRunId) {
67
+ upsertSwarmRunController(statusRunId, {
68
+ status,
69
+ objective: String(run.objective || ""),
70
+ workspaceRoot: String(run.workspace?.canonical_path || swarmState.workspaceRoot || REPO_ROOT),
71
+ repoRoot: String(run.workspace?.canonical_path || swarmState.workspaceRoot || REPO_ROOT),
72
+ });
73
+ }
74
+ } else if (statusRunId) {
75
+ upsertSwarmRunController(statusRunId, {
76
+ status: "idle",
77
+ stoppedAt: Date.now(),
78
+ });
79
+ }
80
+ const controller = controllerFor(statusRunId);
81
+ sendJson(res, 200, {
82
+ ok: true,
83
+ status: controller?.status || swarmState.status,
84
+ objective: controller?.objective || swarmState.objective,
85
+ workspaceRoot: controller?.workspaceRoot || swarmState.workspaceRoot,
86
+ maxTasks: controller?.maxTasks ?? swarmState.maxTasks,
87
+ maxAgents: controller?.maxAgents ?? swarmState.maxAgents,
88
+ workflowId: controller?.workflowId || swarmState.workflowId || "",
89
+ modelProvider: controller?.modelProvider || swarmState.modelProvider || "",
90
+ modelId: controller?.modelId || swarmState.modelId || "",
91
+ resolvedModelProvider: controller?.resolvedModelProvider || swarmState.resolvedModelProvider || "",
92
+ resolvedModelId: controller?.resolvedModelId || swarmState.resolvedModelId || "",
93
+ modelResolutionSource: controller?.modelResolutionSource || swarmState.modelResolutionSource || "none",
94
+ mcpServers: Array.isArray(controller?.mcpServers) ? controller.mcpServers : Array.isArray(swarmState.mcpServers) ? swarmState.mcpServers : [],
95
+ repoRoot: controller?.repoRoot || swarmState.repoRoot || "",
96
+ preflight: swarmState.preflight || null,
97
+ startedAt: controller?.startedAt ?? swarmState.startedAt,
98
+ stoppedAt: controller?.stoppedAt ?? swarmState.stoppedAt,
99
+ runId: statusRunId || swarmState.runId || "",
100
+ attachedPid: controller?.attachedPid || swarmState.attachedPid || null,
101
+ localEngine: isLocalEngineUrl(ENGINE_URL),
102
+ lastError: controller?.lastError || swarmState.lastError || null,
103
+ executorState: controller?.executorState || swarmState.executorState || "idle",
104
+ executorReason: controller?.executorReason || swarmState.executorReason || null,
105
+ executorMode: controller?.executorMode || swarmState.executorMode || "context_steps",
106
+ verificationMode: controller?.verificationMode || swarmState.verificationMode || "strict",
107
+ currentRunId: statusRunId || swarmState.runId || "",
108
+ buildVersion: swarmState.buildVersion || "",
109
+ buildFingerprint: swarmState.buildFingerprint || "",
110
+ buildStartedAt: swarmState.buildStartedAt || null,
111
+ });
112
+ return true;
113
+ }
114
+
115
+ if (routePath === "/api/swarm/runs" && req.method === "GET") {
116
+ const workspace = String(url.searchParams.get("workspace") || "").trim();
117
+ const query = workspace ? `?workspace=${encodeURIComponent(resolve(workspace))}&limit=100` : "?limit=100";
118
+ const payload = await engineRequestJson(session, `/context/runs${query}`).catch(() => ({ runs: [] }));
119
+ const includeHidden = String(url.searchParams.get("include_hidden") || "").trim() === "1";
120
+ const hiddenRunIds = await loadHiddenSwarmRunIds();
121
+ const allRuns = Array.isArray(payload?.runs) ? payload.runs : [];
122
+ const runs = includeHidden
123
+ ? allRuns
124
+ : allRuns.filter((run) => !hiddenRunIds.has(String(run?.run_id || "").trim()));
125
+ const active = runs.filter((run) => {
126
+ const status = String(run?.status || "").toLowerCase();
127
+ return !["completed", "failed", "cancelled"].includes(status);
128
+ });
129
+ sendJson(res, 200, {
130
+ ok: true,
131
+ runs,
132
+ active,
133
+ recent: runs.slice(0, 30),
134
+ hiddenCount: hiddenRunIds.size,
135
+ });
136
+ return true;
137
+ }
138
+
139
+ if (routePath === "/api/swarm/workspaces/list" && req.method === "GET") {
140
+ try {
141
+ const requestedDir = String(url.searchParams.get("dir") || swarmState.workspaceRoot || REPO_ROOT).trim();
142
+ const currentDir = await workspaceExistsAsDirectory(requestedDir);
143
+ if (!currentDir) throw new Error(`Directory not found: ${resolve(requestedDir || REPO_ROOT)}`);
144
+ const entries = await readdir(currentDir, { withFileTypes: true });
145
+ const directories = entries
146
+ .filter((entry) => entry.isDirectory())
147
+ .map((entry) => ({
148
+ name: entry.name,
149
+ path: resolve(currentDir, entry.name),
150
+ }))
151
+ .sort((a, b) => a.name.localeCompare(b.name))
152
+ .slice(0, 500);
153
+ const parent = resolve(currentDir, "..");
154
+ sendJson(res, 200, {
155
+ ok: true,
156
+ dir: currentDir,
157
+ parent: parent === currentDir ? null : parent,
158
+ directories,
159
+ });
160
+ } catch (e) {
161
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
162
+ }
163
+ return true;
164
+ }
165
+
166
+ if (routePath === "/api/swarm/workspaces/files" && req.method === "GET") {
167
+ try {
168
+ const workspaceRootRaw = String(
169
+ url.searchParams.get("workspaceRoot") || swarmState.workspaceRoot || REPO_ROOT
170
+ ).trim();
171
+ const workspaceRoot = await workspaceExistsAsDirectory(workspaceRootRaw);
172
+ if (!workspaceRoot) throw new Error(`Workspace not found: ${resolve(workspaceRootRaw || REPO_ROOT)}`);
173
+ const requestedDir = String(url.searchParams.get("dir") || workspaceRoot).trim();
174
+ const currentDir = await workspaceExistsAsDirectory(requestedDir);
175
+ if (!currentDir) throw new Error(`Directory not found: ${resolve(requestedDir || workspaceRoot)}`);
176
+ if (currentDir !== workspaceRoot && !currentDir.startsWith(`${workspaceRoot}/`)) {
177
+ throw new Error("Directory must be inside workspace root.");
178
+ }
179
+ const entries = await readdir(currentDir, { withFileTypes: true });
180
+ const directories = entries
181
+ .filter((entry) => entry.isDirectory())
182
+ .map((entry) => ({
183
+ name: entry.name,
184
+ path: resolve(currentDir, entry.name),
185
+ }))
186
+ .sort((a, b) => a.name.localeCompare(b.name))
187
+ .slice(0, 300);
188
+ const files = (
189
+ await Promise.all(
190
+ entries
191
+ .filter((entry) => entry.isFile())
192
+ .slice(0, 500)
193
+ .map(async (entry) => {
194
+ const path = resolve(currentDir, entry.name);
195
+ const info = await stat(path).catch(() => null);
196
+ if (!info || !info.isFile()) return null;
197
+ return {
198
+ name: entry.name,
199
+ path,
200
+ size: Number(info.size || 0),
201
+ updatedAt: Number(info.mtimeMs || 0),
202
+ };
203
+ })
204
+ )
205
+ )
206
+ .filter(Boolean)
207
+ .sort((a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0));
208
+ const parent = resolve(currentDir, "..");
209
+ const insideParent =
210
+ parent === workspaceRoot || (parent !== currentDir && parent.startsWith(`${workspaceRoot}/`));
211
+ sendJson(res, 200, {
212
+ ok: true,
213
+ workspaceRoot,
214
+ dir: currentDir,
215
+ parent: insideParent ? parent : null,
216
+ directories,
217
+ files,
218
+ });
219
+ } catch (e) {
220
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
221
+ }
222
+ return true;
223
+ }
224
+
225
+ if (routePath === "/api/swarm/workspaces/read" && req.method === "GET") {
226
+ try {
227
+ const workspaceRootRaw = String(
228
+ url.searchParams.get("workspaceRoot") || swarmState.workspaceRoot || REPO_ROOT
229
+ ).trim();
230
+ const workspaceRoot = await workspaceExistsAsDirectory(workspaceRootRaw);
231
+ if (!workspaceRoot) throw new Error(`Workspace not found: ${resolve(workspaceRootRaw || REPO_ROOT)}`);
232
+ const filePath = resolve(String(url.searchParams.get("path") || "").trim());
233
+ if (!filePath) throw new Error("Missing file path.");
234
+ if (filePath !== workspaceRoot && !filePath.startsWith(`${workspaceRoot}/`)) {
235
+ throw new Error("File must be inside workspace root.");
236
+ }
237
+ const info = await stat(filePath);
238
+ if (!info.isFile()) throw new Error("Not a file.");
239
+ if (info.size > 1024 * 1024) throw new Error("File is too large to preview.");
240
+ const text = await readFile(filePath, "utf8");
241
+ sendJson(res, 200, {
242
+ ok: true,
243
+ workspaceRoot,
244
+ path: filePath,
245
+ size: Number(info.size || 0),
246
+ text,
247
+ });
248
+ } catch (e) {
249
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
250
+ }
251
+ return true;
252
+ }
253
+
254
+ if (routePath === "/api/swarm/runs/hide" && req.method === "POST") {
255
+ try {
256
+ const body = await readJsonBody(req);
257
+ const runIds = (Array.isArray(body?.runIds) ? body.runIds : [])
258
+ .map((id) => String(id || "").trim())
259
+ .filter(Boolean)
260
+ .slice(0, 500);
261
+ if (!runIds.length) throw new Error("Missing runIds");
262
+ const hidden = await loadHiddenSwarmRunIds();
263
+ for (const runId of runIds) hidden.add(runId);
264
+ await saveHiddenSwarmRunIds(hidden);
265
+ if (runIds.includes(String(swarmState.runId || "").trim())) {
266
+ setActiveSwarmRunId("");
267
+ }
268
+ sendJson(res, 200, { ok: true, hiddenCount: hidden.size, hiddenRunIds: runIds });
269
+ } catch (e) {
270
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
271
+ }
272
+ return true;
273
+ }
274
+
275
+ if (routePath === "/api/swarm/runs/unhide" && req.method === "POST") {
276
+ try {
277
+ const body = await readJsonBody(req);
278
+ const runIds = (Array.isArray(body?.runIds) ? body.runIds : [])
279
+ .map((id) => String(id || "").trim())
280
+ .filter(Boolean)
281
+ .slice(0, 500);
282
+ if (!runIds.length) throw new Error("Missing runIds");
283
+ const hidden = await loadHiddenSwarmRunIds();
284
+ for (const runId of runIds) hidden.delete(runId);
285
+ await saveHiddenSwarmRunIds(hidden);
286
+ sendJson(res, 200, { ok: true, hiddenCount: hidden.size, unhiddenRunIds: runIds });
287
+ } catch (e) {
288
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
289
+ }
290
+ return true;
291
+ }
292
+
293
+ if (routePath === "/api/swarm/runs/hide_completed" && req.method === "POST") {
294
+ try {
295
+ const body = await readJsonBody(req);
296
+ const workspace = String(body?.workspace || "").trim();
297
+ const query = workspace ? `?workspace=${encodeURIComponent(resolve(workspace))}&limit=1000` : "?limit=1000";
298
+ const payload = await engineRequestJson(session, `/context/runs${query}`).catch(() => ({ runs: [] }));
299
+ const allRuns = Array.isArray(payload?.runs) ? payload.runs : [];
300
+ const completedRunIds = allRuns
301
+ .filter((run) => {
302
+ const status = String(run?.status || "").toLowerCase();
303
+ return ["completed", "failed", "cancelled"].includes(status);
304
+ })
305
+ .map((run) => String(run?.run_id || "").trim())
306
+ .filter(Boolean);
307
+ const hidden = await loadHiddenSwarmRunIds();
308
+ for (const runId of completedRunIds) hidden.add(runId);
309
+ await saveHiddenSwarmRunIds(hidden);
310
+ if (completedRunIds.includes(String(swarmState.runId || "").trim())) {
311
+ setActiveSwarmRunId("");
312
+ }
313
+ sendJson(res, 200, { ok: true, hiddenCount: hidden.size, hiddenNow: completedRunIds.length });
314
+ } catch (e) {
315
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
316
+ }
317
+ return true;
318
+ }
319
+
320
+ if (routePath === "/api/swarm/start" && req.method === "POST") {
321
+ try {
322
+ const body = await readJsonBody(req);
323
+ const runId = await startSwarm(session, body || {});
324
+ sendJson(res, 200, { ok: true, runId });
325
+ } catch (e) {
326
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
327
+ }
328
+ return true;
329
+ }
330
+
331
+ if (routePath === "/api/swarm/request_revision" && req.method === "POST") {
332
+ try {
333
+ const body = await readJsonBody(req);
334
+ const runId = resolveRunId(body?.runId, swarmState.runId);
335
+ const controller = controllerFor(runId);
336
+ const feedback = String(body?.feedback || "").trim();
337
+ if (!runId) throw new Error("Missing runId");
338
+ if (!feedback) throw new Error("Missing revision feedback");
339
+ const payload = await engineRequestJson(session, `/context/runs/${encodeURIComponent(runId)}`);
340
+ const run = payload?.run || {};
341
+ const objective = String(run?.objective || "").trim();
342
+ const workspaceRoot = String(
343
+ run?.workspace?.canonical_path ||
344
+ run?.workspace_root ||
345
+ controller?.workspaceRoot ||
346
+ swarmState.workspaceRoot ||
347
+ ""
348
+ ).trim();
349
+ if (!objective || !workspaceRoot) {
350
+ throw new Error("Cannot request revision: missing objective/workspace from existing run.");
351
+ }
352
+ await appendContextRunEvent(session, runId, "revision_requested", "planning", {
353
+ feedback,
354
+ }).catch(() => null);
355
+ const revisedObjective = `${objective}\n\nRevision feedback:\n${feedback}`;
356
+ const revisedRunId = await startSwarm(session, {
357
+ workspaceRoot,
358
+ objective: revisedObjective,
359
+ maxTasks: Number(body?.maxTasks || controller?.maxTasks || swarmState.maxTasks || 3),
360
+ maxAgents: Number(body?.maxAgents || controller?.maxAgents || swarmState.maxAgents || 3),
361
+ workflowId: String(
362
+ body?.workflowId ||
363
+ controller?.workflowId ||
364
+ swarmState.workflowId ||
365
+ "swarm.blackboard.default"
366
+ ),
367
+ modelProvider: String(
368
+ run?.model_provider || controller?.modelProvider || swarmState.modelProvider || ""
369
+ ),
370
+ modelId: String(run?.model_id || controller?.modelId || swarmState.modelId || ""),
371
+ mcpServers: Array.isArray(controller?.mcpServers)
372
+ ? controller.mcpServers
373
+ : Array.isArray(swarmState.mcpServers)
374
+ ? swarmState.mcpServers
375
+ : [],
376
+ verificationMode: String(
377
+ body?.verificationMode ||
378
+ controller?.verificationMode ||
379
+ swarmState.verificationMode ||
380
+ "strict"
381
+ ),
382
+ allowLocalPlannerFallback: body?.allowLocalPlannerFallback === true,
383
+ });
384
+ sendJson(res, 200, { ok: true, runId: revisedRunId, previousRunId: runId });
385
+ } catch (e) {
386
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
387
+ }
388
+ return true;
389
+ }
390
+
391
+ if (routePath === "/api/swarm/approve" && req.method === "POST") {
392
+ try {
393
+ const body = await readJsonBody(req);
394
+ const runId = resolveRunId(body?.runId, swarmState.runId);
395
+ const controller = controllerFor(runId);
396
+ if (!runId) throw new Error("Missing runId");
397
+ setActiveSwarmRunId(runId);
398
+ await appendContextRunEvent(session, runId, "plan_approved", "running", {});
399
+ const mode = await detectExecutorMode(session, runId);
400
+ void startRunExecutor(session, runId, {
401
+ mode,
402
+ maxAgents: controller?.maxAgents ?? swarmState.maxAgents,
403
+ workflowId: controller?.workflowId || swarmState.workflowId,
404
+ });
405
+ sendJson(res, 200, { ok: true, runId });
406
+ } catch (e) {
407
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
408
+ }
409
+ return true;
410
+ }
411
+
412
+ if (routePath === "/api/swarm/pause" && req.method === "POST") {
413
+ try {
414
+ const body = await readJsonBody(req);
415
+ const runId = resolveRunId(body?.runId, swarmState.runId);
416
+ if (!runId) throw new Error("Missing runId");
417
+ setActiveSwarmRunId(runId);
418
+ await appendContextRunEvent(session, runId, "run_paused", "paused", {});
419
+ sendJson(res, 200, { ok: true, runId });
420
+ } catch (e) {
421
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
422
+ }
423
+ return true;
424
+ }
425
+
426
+ if (routePath === "/api/swarm/resume" && req.method === "POST") {
427
+ try {
428
+ const body = await readJsonBody(req);
429
+ const runId = resolveRunId(body?.runId, swarmState.runId);
430
+ const controller = controllerFor(runId);
431
+ if (!runId) throw new Error("Missing runId");
432
+ setActiveSwarmRunId(runId);
433
+ await appendContextRunEvent(session, runId, "run_resumed", "running", {});
434
+ const requeued = await requeueInProgressSteps(session, runId);
435
+ const mode = await detectExecutorMode(session, runId);
436
+ const started = await startRunExecutor(session, runId, {
437
+ mode,
438
+ maxAgents: controller?.maxAgents ?? swarmState.maxAgents,
439
+ workflowId: controller?.workflowId || swarmState.workflowId,
440
+ });
441
+ const preview = await engineRequestJson(
442
+ session,
443
+ `/context/runs/${encodeURIComponent(runId)}/driver/next`,
444
+ { method: "POST", body: { dry_run: true } }
445
+ ).catch(() => null);
446
+ sendJson(res, 200, {
447
+ ok: true,
448
+ runId,
449
+ started,
450
+ requeued,
451
+ sessionDispatchOutcome: started ? "started" : "already_running",
452
+ selectedStepId: preview?.selected_step_id || null,
453
+ whyNextStep: preview?.why_next_step || null,
454
+ executorMode: readRunSetting(runId, "executorMode", mode),
455
+ executorState: readRunSetting(runId, "executorState", "idle"),
456
+ executorReason: readRunSetting(runId, "executorReason", null),
457
+ });
458
+ } catch (e) {
459
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
460
+ }
461
+ return true;
462
+ }
463
+
464
+ if (routePath === "/api/swarm/continue" && req.method === "POST") {
465
+ try {
466
+ const body = await readJsonBody(req);
467
+ const runId = resolveRunId(body?.runId, swarmState.runId);
468
+ const controller = controllerFor(runId);
469
+ if (!runId) throw new Error("Missing runId");
470
+ setActiveSwarmRunId(runId);
471
+ await appendContextRunEvent(session, runId, "run_resumed", "running", {
472
+ why_next_step: "manual continue requested",
473
+ });
474
+ const requeued = await requeueInProgressSteps(session, runId);
475
+ const mode = await detectExecutorMode(session, runId);
476
+ const started = await startRunExecutor(session, runId, {
477
+ mode,
478
+ maxAgents: controller?.maxAgents ?? swarmState.maxAgents,
479
+ workflowId: controller?.workflowId || swarmState.workflowId,
480
+ });
481
+ const preview = await engineRequestJson(
482
+ session,
483
+ `/context/runs/${encodeURIComponent(runId)}/driver/next`,
484
+ { method: "POST", body: { dry_run: true } }
485
+ ).catch(() => null);
486
+ sendJson(res, 200, {
487
+ ok: true,
488
+ runId,
489
+ started,
490
+ requeued,
491
+ sessionDispatchOutcome: started ? "started" : "already_running",
492
+ selectedStepId: preview?.selected_step_id || null,
493
+ whyNextStep: preview?.why_next_step || null,
494
+ executorMode: readRunSetting(runId, "executorMode", mode),
495
+ executorState: readRunSetting(runId, "executorState", "idle"),
496
+ executorReason: readRunSetting(runId, "executorReason", null),
497
+ });
498
+ } catch (e) {
499
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
500
+ }
501
+ return true;
502
+ }
503
+
504
+ if ((routePath === "/api/swarm/cancel" || routePath === "/api/swarm/stop") && req.method === "POST") {
505
+ try {
506
+ const body = await readJsonBody(req);
507
+ const runId = resolveRunId(body?.runId, swarmState.runId);
508
+ if (!runId) throw new Error("Missing runId");
509
+ setActiveSwarmRunId(runId);
510
+ await appendContextRunEvent(session, runId, "run_cancelled", "cancelled", {});
511
+ if (swarmState.runId === runId) {
512
+ upsertSwarmRunController(runId, {
513
+ status: "cancelled",
514
+ stoppedAt: Date.now(),
515
+ });
516
+ }
517
+ sendJson(res, 200, { ok: true, runId });
518
+ } catch (e) {
519
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
520
+ }
521
+ return true;
522
+ }
523
+
524
+ if (routePath === "/api/swarm/retry" && req.method === "POST") {
525
+ try {
526
+ const body = await readJsonBody(req);
527
+ const runId = resolveRunId(body?.runId, swarmState.runId);
528
+ const stepId = String(body?.stepId || "").trim();
529
+ if (!runId || !stepId) throw new Error("Missing runId or stepId");
530
+ const controller = controllerFor(runId);
531
+ setActiveSwarmRunId(runId);
532
+ await transitionBlackboardTask(session, runId, { id: stepId }, { action: "retry" }).catch(
533
+ () => null
534
+ );
535
+ await appendContextRunEvent(session, runId, "task_retry_requested", "running", {
536
+ why_next_step: `manual retry requested for ${stepId}`,
537
+ }, stepId);
538
+ const mode = await detectExecutorMode(session, runId);
539
+ const started = await startRunExecutor(session, runId, {
540
+ mode,
541
+ maxAgents: controller?.maxAgents ?? swarmState.maxAgents,
542
+ workflowId: controller?.workflowId || swarmState.workflowId,
543
+ });
544
+ sendJson(res, 200, {
545
+ ok: true,
546
+ runId,
547
+ stepId,
548
+ started,
549
+ sessionDispatchOutcome: started ? "started" : "already_running",
550
+ executorMode: readRunSetting(runId, "executorMode", mode),
551
+ executorState: readRunSetting(runId, "executorState", "idle"),
552
+ executorReason: readRunSetting(runId, "executorReason", null),
553
+ });
554
+ } catch (e) {
555
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
556
+ }
557
+ return true;
558
+ }
559
+
560
+ if (routePath === "/api/swarm/tasks/create" && req.method === "POST") {
561
+ try {
562
+ const body = await readJsonBody(req);
563
+ const runId = resolveRunId(body?.runId, swarmState.runId);
564
+ const tasks = Array.isArray(body?.tasks) ? body.tasks : [];
565
+ if (!runId || !tasks.length) throw new Error("Missing runId or tasks");
566
+ const payload = await engineRequestJson(
567
+ session,
568
+ `/context/runs/${encodeURIComponent(runId)}/tasks`,
569
+ {
570
+ method: "POST",
571
+ body: { tasks },
572
+ }
573
+ );
574
+ sendJson(res, 200, payload);
575
+ } catch (e) {
576
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
577
+ }
578
+ return true;
579
+ }
580
+
581
+ if (routePath === "/api/swarm/tasks/claim" && req.method === "POST") {
582
+ try {
583
+ const body = await readJsonBody(req);
584
+ const runId = resolveRunId(body?.runId, swarmState.runId);
585
+ if (!runId) throw new Error("Missing runId");
586
+ const controller = controllerFor(runId);
587
+ const claimBody = {
588
+ agent_id: String(body?.agentId || "control_panel").trim(),
589
+ command_id: body?.commandId || undefined,
590
+ task_type: body?.taskType || undefined,
591
+ workflow_id: body?.workflowId || controller?.workflowId || undefined,
592
+ lease_ms: Number(body?.leaseMs || 30000),
593
+ };
594
+ const payload = await engineRequestJson(
595
+ session,
596
+ `/context/runs/${encodeURIComponent(runId)}/tasks/claim`,
597
+ {
598
+ method: "POST",
599
+ body: claimBody,
600
+ }
601
+ );
602
+ sendJson(res, 200, payload);
603
+ } catch (e) {
604
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
605
+ }
606
+ return true;
607
+ }
608
+
609
+ if (routePath === "/api/swarm/tasks/transition" && req.method === "POST") {
610
+ try {
611
+ const body = await readJsonBody(req);
612
+ const runId = resolveRunId(body?.runId, swarmState.runId);
613
+ const taskId = String(body?.taskId || "").trim();
614
+ if (!runId || !taskId) throw new Error("Missing runId or taskId");
615
+ const transitionBody = {
616
+ action: body?.action || "status",
617
+ command_id: body?.commandId || undefined,
618
+ expected_task_rev: body?.expectedTaskRev ?? undefined,
619
+ lease_token: body?.leaseToken || undefined,
620
+ agent_id: body?.agentId || undefined,
621
+ status: body?.status || undefined,
622
+ error: body?.error || undefined,
623
+ lease_ms: body?.leaseMs || undefined,
624
+ };
625
+ const payload = await engineRequestJson(
626
+ session,
627
+ `/context/runs/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(taskId)}/transition`,
628
+ {
629
+ method: "POST",
630
+ body: transitionBody,
631
+ }
632
+ );
633
+ sendJson(res, 200, payload);
634
+ } catch (e) {
635
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
636
+ }
637
+ return true;
638
+ }
639
+
640
+ if (routePath.startsWith("/api/swarm/run/") && req.method === "GET") {
641
+ const runId = decodeURIComponent(routePath.replace("/api/swarm/run/", "").trim());
642
+ if (!runId) {
643
+ sendJson(res, 400, { ok: false, error: "Missing run id." });
644
+ return true;
645
+ }
646
+ try {
647
+ const snapshot = await contextRunSnapshot(session, runId);
648
+ const boardTasks = Array.isArray(snapshot?.run?.tasks)
649
+ ? snapshot.run.tasks
650
+ : Array.isArray(snapshot?.blackboard?.tasks)
651
+ ? snapshot.blackboard.tasks
652
+ : [];
653
+ const controller = controllerFor(runId);
654
+ if (boardTasks.length) {
655
+ const workflow = String(boardTasks[0]?.workflow_id || "").trim();
656
+ upsertSwarmRunController(runId, {
657
+ workflowId: workflow || controller?.workflowId || swarmState.workflowId,
658
+ executorMode: "blackboard",
659
+ });
660
+ } else {
661
+ upsertSwarmRunController(runId, { executorMode: "context_steps" });
662
+ }
663
+ const effectiveRunStatus = contextRunStatusToSwarmStatus(
664
+ inferStatusFromEvents(snapshot.run?.status, snapshot.events)
665
+ );
666
+ upsertSwarmRunController(runId, {
667
+ status: effectiveRunStatus,
668
+ objective: String(snapshot.run?.objective || controller?.objective || ""),
669
+ workspaceRoot: String(
670
+ snapshot.run?.workspace?.canonical_path ||
671
+ controller?.workspaceRoot ||
672
+ swarmState.workspaceRoot ||
673
+ ""
674
+ ),
675
+ repoRoot: String(
676
+ snapshot.run?.workspace?.canonical_path ||
677
+ controller?.repoRoot ||
678
+ swarmState.workspaceRoot ||
679
+ ""
680
+ ),
681
+ stoppedAt: ["completed", "failed", "cancelled"].includes(effectiveRunStatus)
682
+ ? Number(snapshot.run?.updated_at_ms || Date.now())
683
+ : null,
684
+ });
685
+ sendJson(res, 200, {
686
+ ok: true,
687
+ run: snapshot.run,
688
+ runStatus: effectiveRunStatus,
689
+ events: snapshot.events,
690
+ blackboard: snapshot.blackboard,
691
+ blackboardPatches: snapshot.blackboardPatches,
692
+ replay: snapshot.replay,
693
+ budget: deriveRunBudget(snapshot.run, snapshot.events, boardTasks),
694
+ tasks: contextRunToTasks(snapshot.run),
695
+ controller: getSwarmRunController(runId),
696
+ });
697
+ } catch (e) {
698
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
699
+ }
700
+ return true;
701
+ }
702
+
703
+ if (routePath === "/api/swarm/snapshot" && req.method === "GET") {
704
+ const runId = resolveRunId(url.searchParams.get("runId"), swarmState.runId);
705
+ if (!runId) {
706
+ sendJson(res, 200, {
707
+ ok: true,
708
+ status: "idle",
709
+ registry: { key: "context.run.steps", value: { version: 1, updatedAtMs: Date.now(), tasks: {} } },
710
+ reasons: [],
711
+ logs: [],
712
+ startedAt: swarmState.startedAt,
713
+ stoppedAt: swarmState.stoppedAt,
714
+ lastError: swarmState.lastError || null,
715
+ });
716
+ return true;
717
+ }
718
+ try {
719
+ const snapshot = await contextRunSnapshot(session, runId);
720
+ const effectiveStatus = contextRunStatusToSwarmStatus(snapshot.run?.status);
721
+ upsertSwarmRunController(runId, {
722
+ registryCache: snapshot.registry,
723
+ logs: snapshot.logs,
724
+ reasons: snapshot.reasons,
725
+ status: effectiveStatus,
726
+ objective: String(snapshot.run?.objective || ""),
727
+ workspaceRoot: String(
728
+ snapshot.run?.workspace?.canonical_path || swarmState.workspaceRoot || ""
729
+ ),
730
+ repoRoot: String(snapshot.run?.workspace?.canonical_path || swarmState.workspaceRoot || ""),
731
+ });
732
+ sendJson(res, 200, {
733
+ ok: true,
734
+ status: effectiveStatus,
735
+ registry: snapshot.registry,
736
+ reasons: snapshot.reasons,
737
+ logs: snapshot.logs,
738
+ run: snapshot.run,
739
+ startedAt: Number(
740
+ snapshot.run?.started_at_ms ||
741
+ readRunSetting(runId, "startedAt", swarmState.startedAt) ||
742
+ Date.now()
743
+ ),
744
+ stoppedAt: isRunTerminalStatus(snapshot.run?.status)
745
+ ? Number(snapshot.run?.updated_at_ms || Date.now())
746
+ : null,
747
+ lastError: readRunSetting(runId, "lastError", swarmState.lastError) || null,
748
+ });
749
+ } catch (e) {
750
+ sendJson(res, 400, { ok: false, error: e instanceof Error ? e.message : String(e) });
751
+ }
752
+ return true;
753
+ }
754
+
755
+ if (routePath === "/api/swarm/events/health" && req.method === "GET") {
756
+ const requestedWorkspace = String(url.searchParams.get("workspace") || "").trim();
757
+ const workspace = String(requestedWorkspace || swarmState.workspaceRoot || REPO_ROOT).trim();
758
+ const runIds = String(url.searchParams.get("runIds") || "")
759
+ .split(",")
760
+ .map((row) => String(row || "").trim())
761
+ .filter(Boolean);
762
+ const query = new URLSearchParams();
763
+ if (workspace) query.set("workspace", workspace);
764
+ if (runIds.length) query.set("run_ids", runIds.join(","));
765
+ query.set("tail", "1");
766
+ const engineProbeUrl = `${ENGINE_URL}/context/runs/events/stream?${query.toString()}`;
767
+ let multiplexAvailable = false;
768
+ let multiplexStatus = 0;
769
+ let multiplexError = "";
770
+ let fallbackRunId = String(
771
+ url.searchParams.get("runId") || runIds[0] || swarmState.runId || ""
772
+ ).trim();
773
+
774
+ try {
775
+ const response = await fetch(engineProbeUrl, {
776
+ method: "GET",
777
+ headers: {
778
+ accept: "text/event-stream",
779
+ authorization: `Bearer ${session.token}`,
780
+ "x-tandem-token": session.token,
781
+ },
782
+ });
783
+ multiplexStatus = Number(response.status || 0);
784
+ multiplexAvailable = response.ok;
785
+ if (!response.ok) {
786
+ multiplexError = `engine returned ${response.status}`;
787
+ }
788
+ response.body?.cancel?.().catch?.(() => null);
789
+ } catch (error) {
790
+ multiplexError = String(error?.message || error || "probe failed");
791
+ }
792
+
793
+ const fallbackAvailable = !!fallbackRunId;
794
+ if (!fallbackRunId) {
795
+ const statusRunId = String(swarmState.runId || "").trim();
796
+ if (statusRunId) fallbackRunId = statusRunId;
797
+ }
798
+
799
+ sendJson(res, 200, {
800
+ ok: true,
801
+ mode: multiplexAvailable ? "multiplex" : "fallback",
802
+ workspace: workspace || null,
803
+ runIds,
804
+ engineUrl: ENGINE_URL,
805
+ engineProbeUrl,
806
+ multiplex: {
807
+ available: multiplexAvailable,
808
+ status: multiplexStatus || null,
809
+ error: multiplexError || null,
810
+ },
811
+ fallback: {
812
+ available: fallbackAvailable,
813
+ runId: fallbackRunId || null,
814
+ endpoint: fallbackRunId ? `/api/orchestrator/events?runId=${encodeURIComponent(fallbackRunId)}` : null,
815
+ },
816
+ });
817
+ return true;
818
+ }
819
+
820
+ if (routePath === "/api/swarm/events" && req.method === "GET") {
821
+ const requestedWorkspace = String(url.searchParams.get("workspace") || "").trim();
822
+ const workspace = String(requestedWorkspace || swarmState.workspaceRoot || REPO_ROOT).trim();
823
+ const runIds = String(url.searchParams.get("runIds") || "")
824
+ .split(",")
825
+ .map((row) => String(row || "").trim())
826
+ .filter(Boolean);
827
+ const runId = String(
828
+ url.searchParams.get("runId") || runIds[0] || swarmState.runId || ""
829
+ ).trim();
830
+ const cursor = String(url.searchParams.get("cursor") || "").trim();
831
+ const tail = String(url.searchParams.get("tail") || "").trim();
832
+
833
+ if (workspace) {
834
+ const query = new URLSearchParams();
835
+ query.set("workspace", workspace);
836
+ const scopedRunIds = runIds.length
837
+ ? runIds
838
+ : runId
839
+ ? [runId]
840
+ : [];
841
+ if (scopedRunIds.length) query.set("run_ids", scopedRunIds.join(","));
842
+ if (cursor) query.set("cursor", cursor);
843
+ if (tail) query.set("tail", tail);
844
+ const targetUrl = `${ENGINE_URL}/context/runs/events/stream?${query.toString()}`;
845
+ try {
846
+ const upstream = await fetch(targetUrl, {
847
+ method: "GET",
848
+ headers: {
849
+ accept: "text/event-stream",
850
+ authorization: `Bearer ${session.token}`,
851
+ "x-tandem-token": session.token,
852
+ },
853
+ });
854
+ if (upstream.ok && upstream.body) {
855
+ res.writeHead(200, {
856
+ "content-type": "text/event-stream",
857
+ "cache-control": "no-cache",
858
+ connection: "keep-alive",
859
+ });
860
+ req.on("close", () => upstream.body?.cancel?.().catch?.(() => null));
861
+ for await (const chunk of upstream.body) {
862
+ if (res.writableEnded || res.destroyed) break;
863
+ res.write(chunk);
864
+ }
865
+ if (!res.writableEnded && !res.destroyed) res.end();
866
+ return true;
867
+ }
868
+ } catch {
869
+ // fall back to legacy single-run poll bridge below
870
+ }
871
+ }
872
+
873
+ res.writeHead(200, {
874
+ "content-type": "text/event-stream",
875
+ "cache-control": "no-cache",
876
+ connection: "keep-alive",
877
+ });
878
+ let closed = false;
879
+ let sinceSeq = 0;
880
+ let sincePatchSeq = 0;
881
+ const close = () => {
882
+ closed = true;
883
+ };
884
+ req.on("close", close);
885
+ res.write(
886
+ `data: ${JSON.stringify({
887
+ kind: "hello",
888
+ ts: Date.now(),
889
+ status: readRunSetting(runId, "status", swarmState.status),
890
+ runId,
891
+ })}\n\n`
892
+ );
893
+ const tick = async () => {
894
+ if (closed || !runId) return;
895
+ try {
896
+ const [eventsPayload, patchesPayload] = await Promise.all([
897
+ engineRequestJson(session, `/context/runs/${encodeURIComponent(runId)}/events?since_seq=${sinceSeq}`),
898
+ engineRequestJson(
899
+ session,
900
+ `/context/runs/${encodeURIComponent(runId)}/blackboard/patches?since_seq=${sincePatchSeq}`
901
+ ).catch(() => ({ patches: [] })),
902
+ ]);
903
+ const events = Array.isArray(eventsPayload?.events) ? eventsPayload.events : [];
904
+ for (const event of events) {
905
+ sinceSeq = Math.max(sinceSeq, Number(event?.seq || 0));
906
+ res.write(
907
+ `data: ${JSON.stringify({
908
+ kind: "context_run_event",
909
+ run_id: runId,
910
+ seq: Number(event?.seq || 0),
911
+ ts_ms: Date.now(),
912
+ payload: event,
913
+ })}\n\n`
914
+ );
915
+ }
916
+ const patches = Array.isArray(patchesPayload?.patches) ? patchesPayload.patches : [];
917
+ for (const patch of patches) {
918
+ sincePatchSeq = Math.max(sincePatchSeq, Number(patch?.seq || 0));
919
+ res.write(
920
+ `data: ${JSON.stringify({
921
+ kind: "blackboard_patch",
922
+ run_id: runId,
923
+ seq: Number(patch?.seq || 0),
924
+ ts_ms: Date.now(),
925
+ payload: patch,
926
+ })}\n\n`
927
+ );
928
+ }
929
+ } catch {
930
+ // ignore transient poll failures
931
+ }
932
+ };
933
+ const interval = setInterval(tick, 1500);
934
+ tick();
935
+ req.on("close", () => clearInterval(interval));
936
+ return true;
937
+ }
938
+
939
+ sendJson(res, 404, { ok: false, error: "Unknown swarm route." });
940
+ return true;
941
+ };
942
+ }
943
+
944
+ function isRunTerminalStatus(status) {
945
+ const normalized = String(status || "")
946
+ .trim()
947
+ .toLowerCase();
948
+ return ["completed", "failed", "cancelled"].includes(normalized);
949
+ }