@agent-relay/dashboard-server 2.0.62 → 2.0.63
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/server.js +185 -93
- package/dist/server.js.map +1 -1
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.d.ts.map +1 -1
- package/out/404.html +1 -1
- package/out/_next/static/chunks/873-6b31247a84ec58c2.js +1 -0
- package/out/about.html +1 -1
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
- package/out/blog.html +1 -1
- package/out/blog.txt +1 -1
- package/out/careers.html +1 -1
- package/out/careers.txt +1 -1
- package/out/changelog.html +1 -1
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +1 -1
- package/out/complete-profile.html +1 -1
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +1 -1
- package/out/contact.txt +1 -1
- package/out/docs.html +1 -1
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +1 -1
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +1 -1
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +1 -1
- package/out/pricing.html +1 -1
- package/out/pricing.txt +1 -1
- package/out/privacy.html +1 -1
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +1 -1
- package/out/security.txt +1 -1
- package/out/signup.html +1 -1
- package/out/signup.txt +1 -1
- package/out/terms.html +1 -1
- package/out/terms.txt +1 -1
- package/package.json +10 -10
- package/out/_next/static/chunks/873-609efa769bceebeb.js +0 -1
- /package/out/_next/static/{dZJGDdzFNEYs9W9huG7Nx → Ip08bs-aI4i94zrABOaVi}/_buildManifest.js +0 -0
- /package/out/_next/static/{dZJGDdzFNEYs9W9huG7Nx → Ip08bs-aI4i94zrABOaVi}/_ssgManifest.js +0 -0
package/dist/server.js
CHANGED
|
@@ -318,7 +318,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
318
318
|
const options = typeof portOrOptions === 'number'
|
|
319
319
|
? { port: portOrOptions, dataDir: dataDirArg, teamDir: teamDirArg, dbPath: dbPathArg }
|
|
320
320
|
: portOrOptions;
|
|
321
|
-
const { port, dataDir, teamDir, dbPath, enableSpawner, projectRoot, tmuxSession, onMarkSpawning, onClearSpawning, verbose } = options;
|
|
321
|
+
const { port, dataDir, teamDir, dbPath, enableSpawner, projectRoot, tmuxSession, onMarkSpawning, onClearSpawning, verbose, spawnManager: externalSpawnManager } = options;
|
|
322
322
|
// Debug logging helper - only logs when verbose is true or VERBOSE env var is set
|
|
323
323
|
const isVerbose = verbose || process.env.VERBOSE === 'true';
|
|
324
324
|
const debug = (message) => {
|
|
@@ -483,9 +483,12 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
483
483
|
// Use detectWorkspacePath to find the actual repo directory in cloud workspaces
|
|
484
484
|
const workspacePath = detectWorkspacePath(projectRoot || dataDir);
|
|
485
485
|
console.log(`[dashboard] Workspace path: ${workspacePath}`);
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
486
|
+
// When an external SpawnManager is provided (from the daemon), use it for read operations
|
|
487
|
+
// (logs, worker listing, hasWorker) and route spawn/release through the SDK client.
|
|
488
|
+
// This solves relay-pty binary resolution issues in npx/global installs.
|
|
489
|
+
// Fall back to creating a local AgentSpawner only when no SpawnManager is provided.
|
|
490
|
+
const useExternalSpawnManager = !!externalSpawnManager;
|
|
491
|
+
const spawner = enableSpawner && !useExternalSpawnManager
|
|
489
492
|
? new AgentSpawner({
|
|
490
493
|
projectRoot: workspacePath,
|
|
491
494
|
tmuxSession,
|
|
@@ -494,8 +497,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
494
497
|
onClearSpawning,
|
|
495
498
|
})
|
|
496
499
|
: undefined;
|
|
500
|
+
// spawnReader provides read-only access to spawner state.
|
|
501
|
+
// Uses the external SpawnManager when available (daemon co-located),
|
|
502
|
+
// otherwise falls back to the local AgentSpawner.
|
|
503
|
+
const spawnReader = externalSpawnManager || spawner;
|
|
504
|
+
if (useExternalSpawnManager) {
|
|
505
|
+
console.log('[dashboard] Using daemon SpawnManager for spawn operations (SDK-routed)');
|
|
506
|
+
}
|
|
497
507
|
// Initialize cloud persistence and memory monitoring if enabled (RELAY_CLOUD_ENABLED=true)
|
|
498
|
-
|
|
508
|
+
// Only needed for local AgentSpawner; daemon's SpawnManager handles its own cloud persistence
|
|
509
|
+
if (spawner && !useExternalSpawnManager) {
|
|
499
510
|
// Use workspace ID from env or generate from project root
|
|
500
511
|
const workspaceId = process.env.RELAY_WORKSPACE_ID ||
|
|
501
512
|
crypto.createHash('sha256').update(projectRoot || dataDir).digest('hex').slice(0, 36);
|
|
@@ -506,28 +517,27 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
506
517
|
}).catch((err) => {
|
|
507
518
|
console.warn('[dashboard] Failed to initialize cloud persistence:', err);
|
|
508
519
|
});
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
520
|
+
}
|
|
521
|
+
// Initialize memory monitoring for cloud deployments
|
|
522
|
+
if (spawnReader && (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true')) {
|
|
523
|
+
try {
|
|
524
|
+
const memoryMonitor = getMemoryMonitor({
|
|
525
|
+
checkIntervalMs: 10000, // Check every 10 seconds
|
|
526
|
+
enableTrendAnalysis: true,
|
|
527
|
+
enableProactiveAlerts: true,
|
|
528
|
+
});
|
|
529
|
+
memoryMonitor.start();
|
|
530
|
+
console.log('[dashboard] Memory monitoring enabled');
|
|
531
|
+
// Register existing workers with memory monitor
|
|
532
|
+
const workers = spawnReader.getActiveWorkers();
|
|
533
|
+
for (const worker of workers) {
|
|
534
|
+
if (worker.pid) {
|
|
535
|
+
memoryMonitor.register(worker.name, worker.pid);
|
|
526
536
|
}
|
|
527
537
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
console.warn('[dashboard] Failed to initialize memory monitoring:', err);
|
|
531
541
|
}
|
|
532
542
|
}
|
|
533
543
|
process.on('uncaughtException', (err) => {
|
|
@@ -1162,8 +1172,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1162
1172
|
}
|
|
1163
1173
|
}
|
|
1164
1174
|
// Check spawner's active workers (they have accurate team info for spawned agents)
|
|
1165
|
-
if (
|
|
1166
|
-
const activeWorkers =
|
|
1175
|
+
if (spawnReader) {
|
|
1176
|
+
const activeWorkers = spawnReader.getActiveWorkers();
|
|
1167
1177
|
for (const worker of activeWorkers) {
|
|
1168
1178
|
if (worker.team === teamName) {
|
|
1169
1179
|
members.add(worker.name);
|
|
@@ -1466,8 +1476,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1466
1476
|
// Helper to check if an agent name is internal/system (should be hidden from UI)
|
|
1467
1477
|
// Convention: agent names starting with __ are internal (e.g., __spawner__, __DashboardBridge__)
|
|
1468
1478
|
const isInternalAgent = (name) => {
|
|
1479
|
+
if (name === '__cli_sender__')
|
|
1480
|
+
return false;
|
|
1469
1481
|
return name.startsWith('__');
|
|
1470
1482
|
};
|
|
1483
|
+
// Display-name remapping for CLI sender (used across message and history endpoints)
|
|
1484
|
+
const remapAgentName = (name) => {
|
|
1485
|
+
if (name === '__cli_sender__')
|
|
1486
|
+
return 'CLI';
|
|
1487
|
+
return name;
|
|
1488
|
+
};
|
|
1471
1489
|
const buildThreadSummaryMap = (rows) => {
|
|
1472
1490
|
const summaries = new Map();
|
|
1473
1491
|
for (const row of rows) {
|
|
@@ -1514,6 +1532,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1514
1532
|
let attachments;
|
|
1515
1533
|
let channel;
|
|
1516
1534
|
let effectiveFrom = row.from;
|
|
1535
|
+
let effectiveTo = row.to;
|
|
1517
1536
|
if (row.data && typeof row.data === 'object') {
|
|
1518
1537
|
if ('attachments' in row.data) {
|
|
1519
1538
|
attachments = row.data.attachments;
|
|
@@ -1527,9 +1546,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1527
1546
|
effectiveFrom = row.data.senderName;
|
|
1528
1547
|
}
|
|
1529
1548
|
}
|
|
1549
|
+
effectiveFrom = remapAgentName(effectiveFrom);
|
|
1550
|
+
effectiveTo = remapAgentName(effectiveTo);
|
|
1530
1551
|
return {
|
|
1531
1552
|
from: effectiveFrom,
|
|
1532
|
-
to:
|
|
1553
|
+
to: effectiveTo,
|
|
1533
1554
|
content: row.body,
|
|
1534
1555
|
timestamp: new Date(row.ts).toISOString(),
|
|
1535
1556
|
id: row.id,
|
|
@@ -1776,8 +1797,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
1776
1797
|
}
|
|
1777
1798
|
}
|
|
1778
1799
|
// Mark spawned agents with isSpawned flag and team
|
|
1779
|
-
if (
|
|
1780
|
-
const activeWorkers =
|
|
1800
|
+
if (spawnReader) {
|
|
1801
|
+
const activeWorkers = spawnReader.getActiveWorkers();
|
|
1781
1802
|
for (const worker of activeWorkers) {
|
|
1782
1803
|
const agent = agentsMap.get(worker.name);
|
|
1783
1804
|
if (agent) {
|
|
@@ -2186,7 +2207,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2186
2207
|
};
|
|
2187
2208
|
// Helper to subscribe to an agent (async to handle spawn timing)
|
|
2188
2209
|
const subscribeToAgent = async (agentName) => {
|
|
2189
|
-
let isSpawned =
|
|
2210
|
+
let isSpawned = spawnReader?.hasWorker(agentName) ?? false;
|
|
2190
2211
|
const isDaemon = isDaemonConnected(agentName);
|
|
2191
2212
|
// Check if agent exists (either spawned or daemon-connected)
|
|
2192
2213
|
if (!isSpawned && !isDaemon) {
|
|
@@ -2203,14 +2224,14 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2203
2224
|
// poll to handle race condition between spawn API returning and
|
|
2204
2225
|
// WebSocket connection. This is common for setup agents (__setup__*).
|
|
2205
2226
|
// Longer timeout for CLI auth flows (Cursor, etc.) which can take time to initialize.
|
|
2206
|
-
if (!isSpawned && isDaemon &&
|
|
2227
|
+
if (!isSpawned && isDaemon && spawnReader) {
|
|
2207
2228
|
const isSetupAgent = agentName.startsWith('__setup__');
|
|
2208
2229
|
const maxWaitMs = isSetupAgent ? 90000 : 5000; // 90s for setup agents (CLI auth can be slow), 5s otherwise
|
|
2209
2230
|
const pollIntervalMs = 100;
|
|
2210
2231
|
const startTime = Date.now();
|
|
2211
2232
|
while (Date.now() - startTime < maxWaitMs) {
|
|
2212
2233
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
2213
|
-
isSpawned =
|
|
2234
|
+
isSpawned = spawnReader.hasWorker(agentName);
|
|
2214
2235
|
if (isSpawned) {
|
|
2215
2236
|
console.log(`[dashboard] Agent ${agentName} appeared in spawner after ${Date.now() - startTime}ms`);
|
|
2216
2237
|
break;
|
|
@@ -2228,9 +2249,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2228
2249
|
}
|
|
2229
2250
|
logSubscriptions.get(agentName).add(ws);
|
|
2230
2251
|
debug(`[dashboard] Client subscribed to logs for: ${agentName} (spawned: ${isSpawned}, daemon: ${isDaemon})`);
|
|
2231
|
-
if (isSpawned &&
|
|
2252
|
+
if (isSpawned && spawnReader) {
|
|
2232
2253
|
// Send initial log history for spawned agents (5000 lines to match xterm scrollback capacity)
|
|
2233
|
-
const lines =
|
|
2254
|
+
const lines = spawnReader.getWorkerOutput(agentName, 5000);
|
|
2234
2255
|
ws.send(JSON.stringify({
|
|
2235
2256
|
type: 'history',
|
|
2236
2257
|
agent: agentName,
|
|
@@ -2326,8 +2347,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2326
2347
|
return;
|
|
2327
2348
|
}
|
|
2328
2349
|
// Check if this is a spawned agent (we can only send input to spawned agents)
|
|
2329
|
-
if (
|
|
2330
|
-
const success =
|
|
2350
|
+
if (spawnReader?.hasWorker(agentName)) {
|
|
2351
|
+
const success = spawnReader.sendWorkerInput(agentName, msg.data);
|
|
2331
2352
|
if (!success) {
|
|
2332
2353
|
console.warn(`[dashboard] Failed to send input to agent ${agentName}`);
|
|
2333
2354
|
}
|
|
@@ -2438,10 +2459,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2438
2459
|
}
|
|
2439
2460
|
});
|
|
2440
2461
|
};
|
|
2441
|
-
// Helper to broadcast channel messages to all
|
|
2442
|
-
//
|
|
2462
|
+
// Helper to broadcast channel messages to all connected clients
|
|
2463
|
+
// Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
|
|
2443
2464
|
const broadcastChannelMessage = (message) => {
|
|
2444
2465
|
const payload = JSON.stringify(message);
|
|
2466
|
+
// Broadcast to main WebSocket clients (local mode)
|
|
2467
|
+
wss.clients.forEach((client) => {
|
|
2468
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2469
|
+
client.send(payload);
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
// Also broadcast to presence WebSocket clients (cloud mode)
|
|
2445
2473
|
wssPresence.clients.forEach((client) => {
|
|
2446
2474
|
if (client.readyState === WebSocket.OPEN) {
|
|
2447
2475
|
client.send(payload);
|
|
@@ -2829,8 +2857,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
2829
2857
|
try {
|
|
2830
2858
|
const availableAgents = [];
|
|
2831
2859
|
// Get spawned agents from spawner
|
|
2832
|
-
if (
|
|
2833
|
-
const activeWorkers =
|
|
2860
|
+
if (spawnReader) {
|
|
2861
|
+
const activeWorkers = spawnReader.getActiveWorkers();
|
|
2834
2862
|
for (const worker of activeWorkers) {
|
|
2835
2863
|
availableAgents.push({
|
|
2836
2864
|
id: worker.name,
|
|
@@ -3234,6 +3262,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3234
3262
|
if (workspaceId && data?._workspaceId && data._workspaceId !== workspaceId) {
|
|
3235
3263
|
return false;
|
|
3236
3264
|
}
|
|
3265
|
+
// Filter out internal/system agents (e.g., __system__, __spawner__)
|
|
3266
|
+
if (isInternalAgent(m.from))
|
|
3267
|
+
return false;
|
|
3237
3268
|
// Accept message if it has _isChannelMessage flag OR if it's addressed to a channel
|
|
3238
3269
|
return Boolean(data?._isChannelMessage) || (m.to && m.to.startsWith('#'));
|
|
3239
3270
|
});
|
|
@@ -3960,8 +3991,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
3960
3991
|
try {
|
|
3961
3992
|
const agents = [];
|
|
3962
3993
|
// Get metrics from spawner's active workers
|
|
3963
|
-
if (
|
|
3964
|
-
const activeWorkers =
|
|
3994
|
+
if (spawnReader) {
|
|
3995
|
+
const activeWorkers = spawnReader.getActiveWorkers();
|
|
3965
3996
|
for (const worker of activeWorkers) {
|
|
3966
3997
|
// Get memory and CPU usage
|
|
3967
3998
|
let rssBytes = 0;
|
|
@@ -4059,8 +4090,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4059
4090
|
const totalCrashes24h = 0;
|
|
4060
4091
|
let totalAlerts24h = 0;
|
|
4061
4092
|
// Get spawned agent count
|
|
4062
|
-
if (
|
|
4063
|
-
const workers =
|
|
4093
|
+
if (spawnReader) {
|
|
4094
|
+
const workers = spawnReader.getActiveWorkers();
|
|
4064
4095
|
agentCount = workers.length;
|
|
4065
4096
|
// Check for high memory usage
|
|
4066
4097
|
for (const worker of workers) {
|
|
@@ -4293,8 +4324,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4293
4324
|
}
|
|
4294
4325
|
const result = messages.map(m => ({
|
|
4295
4326
|
id: m.id,
|
|
4296
|
-
from: m.from,
|
|
4297
|
-
to: m.to,
|
|
4327
|
+
from: remapAgentName(m.from),
|
|
4328
|
+
to: remapAgentName(m.to),
|
|
4298
4329
|
content: m.body,
|
|
4299
4330
|
timestamp: new Date(m.ts).toISOString(),
|
|
4300
4331
|
thread: m.thread,
|
|
@@ -4329,8 +4360,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4329
4360
|
// Skip messages from/to internal system agents (e.g., __spawner__)
|
|
4330
4361
|
if (isInternalAgent(msg.from) || isInternalAgent(msg.to))
|
|
4331
4362
|
continue;
|
|
4332
|
-
// Create normalized key (sorted participants)
|
|
4333
|
-
const participants = [msg.from, msg.to].sort();
|
|
4363
|
+
// Create normalized key (sorted participants, with display names)
|
|
4364
|
+
const participants = [remapAgentName(msg.from), remapAgentName(msg.to)].sort();
|
|
4334
4365
|
const key = participants.join(':');
|
|
4335
4366
|
const existing = conversationMap.get(key);
|
|
4336
4367
|
if (existing) {
|
|
@@ -4438,19 +4469,19 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4438
4469
|
* - raw: If 'true', return raw output instead of cleaned lines
|
|
4439
4470
|
*/
|
|
4440
4471
|
app.get('/api/logs/:name', (req, res) => {
|
|
4441
|
-
if (!
|
|
4472
|
+
if (!spawnReader) {
|
|
4442
4473
|
return res.status(503).json({ error: 'Spawner not enabled' });
|
|
4443
4474
|
}
|
|
4444
4475
|
const { name } = req.params;
|
|
4445
4476
|
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 500;
|
|
4446
4477
|
const raw = req.query.raw === 'true';
|
|
4447
4478
|
// Check if worker exists
|
|
4448
|
-
if (!
|
|
4479
|
+
if (!spawnReader.hasWorker(name)) {
|
|
4449
4480
|
return res.status(404).json({ error: `Agent ${name} not found` });
|
|
4450
4481
|
}
|
|
4451
4482
|
try {
|
|
4452
4483
|
if (raw) {
|
|
4453
|
-
const output =
|
|
4484
|
+
const output = spawnReader.getWorkerRawOutput(name);
|
|
4454
4485
|
res.json({
|
|
4455
4486
|
name,
|
|
4456
4487
|
raw: true,
|
|
@@ -4459,7 +4490,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4459
4490
|
});
|
|
4460
4491
|
}
|
|
4461
4492
|
else {
|
|
4462
|
-
const lines =
|
|
4493
|
+
const lines = spawnReader.getWorkerOutput(name, limit);
|
|
4463
4494
|
res.json({
|
|
4464
4495
|
name,
|
|
4465
4496
|
raw: false,
|
|
@@ -4478,11 +4509,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4478
4509
|
* GET /api/logs - List all agents with available logs
|
|
4479
4510
|
*/
|
|
4480
4511
|
app.get('/api/logs', (req, res) => {
|
|
4481
|
-
if (!
|
|
4512
|
+
if (!spawnReader) {
|
|
4482
4513
|
return res.status(503).json({ error: 'Spawner not enabled' });
|
|
4483
4514
|
}
|
|
4484
4515
|
try {
|
|
4485
|
-
const workers =
|
|
4516
|
+
const workers = spawnReader.getActiveWorkers();
|
|
4486
4517
|
const agents = workers.map(w => ({
|
|
4487
4518
|
name: w.name,
|
|
4488
4519
|
cli: w.cli,
|
|
@@ -4513,7 +4544,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4513
4544
|
* Body: { name: string, cli?: string, task?: string, team?: string, spawnerName?, cwd?, interactive?, shadowMode?, shadowAgent?, shadowOf?, shadowTriggers?, shadowSpeakOn? }
|
|
4514
4545
|
*/
|
|
4515
4546
|
app.post('/api/spawn', async (req, res) => {
|
|
4516
|
-
if (!spawner) {
|
|
4547
|
+
if (!spawner && !useExternalSpawnManager) {
|
|
4517
4548
|
return res.status(503).json({
|
|
4518
4549
|
success: false,
|
|
4519
4550
|
error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
|
|
@@ -4527,23 +4558,56 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4527
4558
|
});
|
|
4528
4559
|
}
|
|
4529
4560
|
try {
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4561
|
+
let result;
|
|
4562
|
+
if (useExternalSpawnManager) {
|
|
4563
|
+
// Route spawn through SDK → daemon socket → SpawnManager
|
|
4564
|
+
// This ensures relay-pty binary resolution works regardless of install method
|
|
4565
|
+
const client = await getRelayClient('Dashboard');
|
|
4566
|
+
if (!client) {
|
|
4567
|
+
return res.status(503).json({
|
|
4568
|
+
success: false,
|
|
4569
|
+
error: 'Not connected to relay daemon',
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
// spawnerName/userId/includeWorkflowConventions need SDK >= 2.2.0 types
|
|
4573
|
+
// Cast removed once SDK is republished with these fields
|
|
4574
|
+
result = await client.spawn({
|
|
4575
|
+
name,
|
|
4576
|
+
cli,
|
|
4577
|
+
task,
|
|
4578
|
+
team: team || undefined,
|
|
4579
|
+
cwd: cwd || undefined,
|
|
4580
|
+
interactive,
|
|
4581
|
+
shadowMode,
|
|
4582
|
+
shadowAgent,
|
|
4583
|
+
shadowOf,
|
|
4584
|
+
shadowTriggers,
|
|
4585
|
+
shadowSpeakOn,
|
|
4586
|
+
spawnerName: spawnerName || undefined,
|
|
4587
|
+
userId: typeof userId === 'string' ? userId : undefined,
|
|
4588
|
+
includeWorkflowConventions: true,
|
|
4589
|
+
});
|
|
4590
|
+
}
|
|
4591
|
+
else {
|
|
4592
|
+
// Fall back to local AgentSpawner (standalone mode)
|
|
4593
|
+
const request = {
|
|
4594
|
+
name,
|
|
4595
|
+
cli,
|
|
4596
|
+
task,
|
|
4597
|
+
team: team || undefined,
|
|
4598
|
+
spawnerName: spawnerName || undefined,
|
|
4599
|
+
cwd: cwd || undefined,
|
|
4600
|
+
interactive,
|
|
4601
|
+
shadowMode,
|
|
4602
|
+
shadowAgent,
|
|
4603
|
+
shadowOf,
|
|
4604
|
+
shadowTriggers,
|
|
4605
|
+
shadowSpeakOn,
|
|
4606
|
+
userId: typeof userId === 'string' ? userId : undefined,
|
|
4607
|
+
includeWorkflowConventions: true,
|
|
4608
|
+
};
|
|
4609
|
+
result = await spawner.spawn(request);
|
|
4610
|
+
}
|
|
4547
4611
|
if (result.success) {
|
|
4548
4612
|
// Broadcast update to WebSocket clients
|
|
4549
4613
|
broadcastData().catch(() => { });
|
|
@@ -4573,7 +4637,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4573
4637
|
* Body: { cli?: string }
|
|
4574
4638
|
*/
|
|
4575
4639
|
app.post('/api/spawn/architect', async (req, res) => {
|
|
4576
|
-
if (!spawner) {
|
|
4640
|
+
if (!spawner && !useExternalSpawnManager) {
|
|
4577
4641
|
return res.status(503).json({
|
|
4578
4642
|
success: false,
|
|
4579
4643
|
error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
|
|
@@ -4581,7 +4645,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
|
|
|
4581
4645
|
}
|
|
4582
4646
|
const { cli = 'claude' } = req.body;
|
|
4583
4647
|
// Check if Architect already exists
|
|
4584
|
-
const activeWorkers =
|
|
4648
|
+
const activeWorkers = spawnReader?.getActiveWorkers() || [];
|
|
4585
4649
|
if (activeWorkers.some(w => w.name.toLowerCase() === 'architect')) {
|
|
4586
4650
|
return res.status(409).json({
|
|
4587
4651
|
success: false,
|
|
@@ -4659,12 +4723,27 @@ Then output: \`->relay-file:all\`
|
|
|
4659
4723
|
|
|
4660
4724
|
Start by greeting the project leads and asking for status updates.`;
|
|
4661
4725
|
try {
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4726
|
+
let result;
|
|
4727
|
+
if (useExternalSpawnManager) {
|
|
4728
|
+
const client = await getRelayClient('Dashboard');
|
|
4729
|
+
if (!client) {
|
|
4730
|
+
return res.status(503).json({ success: false, error: 'Not connected to relay daemon' });
|
|
4731
|
+
}
|
|
4732
|
+
result = await client.spawn({
|
|
4733
|
+
name: 'Architect',
|
|
4734
|
+
cli,
|
|
4735
|
+
task: architectPrompt,
|
|
4736
|
+
includeWorkflowConventions: true,
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
else {
|
|
4740
|
+
result = await spawner.spawn({
|
|
4741
|
+
name: 'Architect',
|
|
4742
|
+
cli,
|
|
4743
|
+
task: architectPrompt,
|
|
4744
|
+
includeWorkflowConventions: true,
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4668
4747
|
if (result.success) {
|
|
4669
4748
|
broadcastData().catch(() => { });
|
|
4670
4749
|
}
|
|
@@ -4694,8 +4773,8 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4694
4773
|
// Collect agents from all available sources
|
|
4695
4774
|
const agentsByName = new Map();
|
|
4696
4775
|
// Source 1: Spawner's active workers (authoritative for spawned agents)
|
|
4697
|
-
if (
|
|
4698
|
-
for (const worker of
|
|
4776
|
+
if (spawnReader) {
|
|
4777
|
+
for (const worker of spawnReader.getActiveWorkers()) {
|
|
4699
4778
|
agentsByName.set(worker.name, {
|
|
4700
4779
|
name: worker.name,
|
|
4701
4780
|
cli: worker.cli,
|
|
@@ -4742,7 +4821,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4742
4821
|
agents,
|
|
4743
4822
|
// Include source info for debugging
|
|
4744
4823
|
sources: {
|
|
4745
|
-
spawnerEnabled: !!
|
|
4824
|
+
spawnerEnabled: !!spawnReader,
|
|
4746
4825
|
daemonAgentsFile: fs.existsSync(agentsPath),
|
|
4747
4826
|
},
|
|
4748
4827
|
});
|
|
@@ -4751,7 +4830,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4751
4830
|
* DELETE /api/spawned/:name - Release a spawned agent
|
|
4752
4831
|
*/
|
|
4753
4832
|
app.delete('/api/spawned/:name', async (req, res) => {
|
|
4754
|
-
if (!spawner) {
|
|
4833
|
+
if (!spawner && !useExternalSpawnManager) {
|
|
4755
4834
|
return res.status(503).json({
|
|
4756
4835
|
success: false,
|
|
4757
4836
|
error: 'Spawner not enabled',
|
|
@@ -4759,7 +4838,19 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4759
4838
|
}
|
|
4760
4839
|
const { name } = req.params;
|
|
4761
4840
|
try {
|
|
4762
|
-
|
|
4841
|
+
let released;
|
|
4842
|
+
if (useExternalSpawnManager) {
|
|
4843
|
+
// Route release through SDK → daemon socket → SpawnManager
|
|
4844
|
+
const client = await getRelayClient('Dashboard');
|
|
4845
|
+
if (!client) {
|
|
4846
|
+
return res.status(503).json({ success: false, error: 'Not connected to relay daemon' });
|
|
4847
|
+
}
|
|
4848
|
+
const result = await client.release(name);
|
|
4849
|
+
released = result.success;
|
|
4850
|
+
}
|
|
4851
|
+
else {
|
|
4852
|
+
released = await spawner.release(name);
|
|
4853
|
+
}
|
|
4763
4854
|
if (released) {
|
|
4764
4855
|
broadcastData().catch(() => { });
|
|
4765
4856
|
// Broadcast agent_released event to activity feed
|
|
@@ -4792,7 +4883,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4792
4883
|
* This is useful for breaking agents out of stuck loops without terminating them.
|
|
4793
4884
|
*/
|
|
4794
4885
|
app.post('/api/agents/by-name/:name/interrupt', (req, res) => {
|
|
4795
|
-
if (!
|
|
4886
|
+
if (!spawnReader) {
|
|
4796
4887
|
return res.status(503).json({
|
|
4797
4888
|
success: false,
|
|
4798
4889
|
error: 'Spawner not enabled',
|
|
@@ -4800,7 +4891,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4800
4891
|
}
|
|
4801
4892
|
const { name } = req.params;
|
|
4802
4893
|
// Check if agent exists
|
|
4803
|
-
if (!
|
|
4894
|
+
if (!spawnReader.hasWorker(name)) {
|
|
4804
4895
|
return res.status(404).json({
|
|
4805
4896
|
success: false,
|
|
4806
4897
|
error: `Agent ${name} not found or not spawned`,
|
|
@@ -4809,7 +4900,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
4809
4900
|
try {
|
|
4810
4901
|
// Send ESC ESC sequence to interrupt the agent
|
|
4811
4902
|
// ESC = 0x1b in hexadecimal
|
|
4812
|
-
const success =
|
|
4903
|
+
const success = spawnReader.sendWorkerInput(name, '\x1b\x1b');
|
|
4813
4904
|
if (success) {
|
|
4814
4905
|
console.log(`[api] Sent interrupt (ESC ESC) to agent ${name}`);
|
|
4815
4906
|
res.json({
|
|
@@ -5162,7 +5253,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5162
5253
|
*/
|
|
5163
5254
|
app.get('/api/fleet/servers', async (_req, res) => {
|
|
5164
5255
|
const servers = [];
|
|
5165
|
-
const localAgents =
|
|
5256
|
+
const localAgents = spawnReader?.getActiveWorkers() || [];
|
|
5166
5257
|
const agentStatuses = await loadAgentStatuses();
|
|
5167
5258
|
let hasBridgeProjects = false;
|
|
5168
5259
|
// Check for bridge connections first
|
|
@@ -5246,7 +5337,7 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5246
5337
|
* GET /api/fleet/stats - Get aggregate fleet statistics
|
|
5247
5338
|
*/
|
|
5248
5339
|
app.get('/api/fleet/stats', async (_req, res) => {
|
|
5249
|
-
const localAgents =
|
|
5340
|
+
const localAgents = spawnReader?.getActiveWorkers() || [];
|
|
5250
5341
|
const agentStatuses = await loadAgentStatuses();
|
|
5251
5342
|
const totalAgents = localAgents.length;
|
|
5252
5343
|
let onlineAgents = 0;
|
|
@@ -5551,8 +5642,9 @@ Start by greeting the project leads and asking for status updates.`;
|
|
|
5551
5642
|
const listenCallback = async () => {
|
|
5552
5643
|
console.log(`Dashboard running at http://${host || 'localhost'}:${availablePort} (build: cloud-channels-v2)`);
|
|
5553
5644
|
console.log(`Monitoring: ${dataDir}`);
|
|
5554
|
-
// Set the dashboard port on spawner so spawned agents can use the API for nested spawns
|
|
5555
|
-
|
|
5645
|
+
// Set the dashboard port on local spawner so spawned agents can use the API for nested spawns
|
|
5646
|
+
// Not needed when using external SpawnManager (daemon handles this)
|
|
5647
|
+
if (spawner && !useExternalSpawnManager) {
|
|
5556
5648
|
spawner.setDashboardPort(availablePort);
|
|
5557
5649
|
}
|
|
5558
5650
|
// Start health worker on separate thread for reliable health checks
|