@agent-relay/dashboard-server 2.0.62 → 2.0.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/server.d.ts.map +1 -1
  2. package/dist/server.js +218 -100
  3. package/dist/server.js.map +1 -1
  4. package/dist/types/index.d.ts +27 -0
  5. package/dist/types/index.d.ts.map +1 -1
  6. package/out/404.html +1 -1
  7. package/out/_next/static/chunks/873-9aee36b975a9556a.js +1 -0
  8. package/out/_next/static/css/2ee05ba949b3ac9f.css +1 -0
  9. package/out/about.html +2 -2
  10. package/out/about.txt +1 -1
  11. package/out/app/onboarding.html +1 -1
  12. package/out/app/onboarding.txt +1 -1
  13. package/out/app.html +1 -1
  14. package/out/app.txt +2 -2
  15. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  16. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  17. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  18. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  19. package/out/blog.html +2 -2
  20. package/out/blog.txt +1 -1
  21. package/out/careers.html +2 -2
  22. package/out/careers.txt +1 -1
  23. package/out/changelog.html +2 -2
  24. package/out/changelog.txt +1 -1
  25. package/out/cloud/link.html +1 -1
  26. package/out/cloud/link.txt +1 -1
  27. package/out/complete-profile.html +2 -2
  28. package/out/complete-profile.txt +1 -1
  29. package/out/connect-repos.html +1 -1
  30. package/out/connect-repos.txt +1 -1
  31. package/out/contact.html +2 -2
  32. package/out/contact.txt +1 -1
  33. package/out/docs.html +2 -2
  34. package/out/docs.txt +1 -1
  35. package/out/history.html +1 -1
  36. package/out/history.txt +1 -1
  37. package/out/index.html +1 -1
  38. package/out/index.txt +2 -2
  39. package/out/login.html +2 -2
  40. package/out/login.txt +1 -1
  41. package/out/metrics.html +1 -1
  42. package/out/metrics.txt +1 -1
  43. package/out/pricing.html +2 -2
  44. package/out/pricing.txt +1 -1
  45. package/out/privacy.html +2 -2
  46. package/out/privacy.txt +1 -1
  47. package/out/providers/setup/claude.html +1 -1
  48. package/out/providers/setup/claude.txt +1 -1
  49. package/out/providers/setup/codex.html +1 -1
  50. package/out/providers/setup/codex.txt +1 -1
  51. package/out/providers/setup/cursor.html +1 -1
  52. package/out/providers/setup/cursor.txt +1 -1
  53. package/out/providers.html +1 -1
  54. package/out/providers.txt +1 -1
  55. package/out/security.html +2 -2
  56. package/out/security.txt +1 -1
  57. package/out/signup.html +2 -2
  58. package/out/signup.txt +1 -1
  59. package/out/terms.html +2 -2
  60. package/out/terms.txt +1 -1
  61. package/package.json +10 -10
  62. package/out/_next/static/chunks/873-609efa769bceebeb.js +0 -1
  63. package/out/_next/static/css/ad96af0f7a47b705.css +0 -1
  64. /package/out/_next/static/{dZJGDdzFNEYs9W9huG7Nx → 6oI4iquYj1QbK8njLsK3s}/_buildManifest.js +0 -0
  65. /package/out/_next/static/{dZJGDdzFNEYs9W9huG7Nx → 6oI4iquYj1QbK8njLsK3s}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA8DA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAgYzD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AACvH,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA8DA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAiYzD,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AACvH,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC"}
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
- // Pass dashboard port to spawner so spawned agents can call spawn/release APIs for nested spawning
487
- // Also pass spawn tracking callbacks so messages can be queued before HELLO completes
488
- const spawner = enableSpawner
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
- if (spawner) {
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
- // Initialize memory monitoring for cloud deployments
510
- // Memory monitoring is enabled by default when cloud is enabled
511
- if (process.env.RELAY_CLOUD_ENABLED === 'true' || process.env.RELAY_MEMORY_MONITORING === 'true') {
512
- try {
513
- const memoryMonitor = getMemoryMonitor({
514
- checkIntervalMs: 10000, // Check every 10 seconds
515
- enableTrendAnalysis: true,
516
- enableProactiveAlerts: true,
517
- });
518
- memoryMonitor.start();
519
- console.log('[dashboard] Memory monitoring enabled');
520
- // Register existing workers with memory monitor
521
- const workers = spawner.getActiveWorkers();
522
- for (const worker of workers) {
523
- if (worker.pid) {
524
- memoryMonitor.register(worker.name, worker.pid);
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
- catch (err) {
529
- console.warn('[dashboard] Failed to initialize memory monitoring:', err);
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) => {
@@ -780,24 +790,43 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
780
790
  // Serve Next.js static export with .html extension handling
781
791
  app.use(express.static(dashboardDir, { extensions: ['html'] }));
782
792
  // Fallback for Next.js pages (e.g., /metrics -> /metrics.html)
783
- // These are needed when a route exists as both a directory and .html file
784
- const sendFileWithFallback = (res, filePath) => {
793
+ // These are needed when a route exists as both a directory and .html file.
794
+ // For /app/* deep links we prefer redirecting to "/" if the export is missing,
795
+ // so users don’t get stuck on a plain-text error on refresh.
796
+ const uiMissingMessage = 'Dashboard UI file not found. Please reinstall using: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash';
797
+ const sendFileOr = (res, filePath, onError) => {
785
798
  res.sendFile(filePath, (err) => {
786
799
  if (err && !res.headersSent) {
787
- res.status(404).send('Dashboard UI file not found. Please reinstall using: curl -fsSL https://raw.githubusercontent.com/AgentWorkforce/relay/main/install.sh | bash');
800
+ onError(err);
801
+ }
802
+ });
803
+ };
804
+ const sendFileOrText404 = (res, filePath, message) => {
805
+ sendFileOr(res, filePath, () => {
806
+ res.status(404).send(message);
807
+ });
808
+ };
809
+ const sendFileOrRedirectRoot = (res, filePath) => {
810
+ sendFileOr(res, filePath, () => {
811
+ // If the app entrypoint isn’t present, try to recover by sending users
812
+ // to the root page (if it exists). Otherwise keep the install hint.
813
+ if (fs.existsSync(path.join(dashboardDir, 'index.html'))) {
814
+ res.redirect(302, '/');
815
+ return;
788
816
  }
817
+ res.status(404).send(uiMissingMessage);
789
818
  });
790
819
  };
791
820
  app.get('/metrics', (req, res) => {
792
- sendFileWithFallback(res, path.join(dashboardDir, 'metrics.html'));
821
+ sendFileOrText404(res, path.join(dashboardDir, 'metrics.html'), uiMissingMessage);
793
822
  });
794
823
  app.get('/app', (req, res) => {
795
- sendFileWithFallback(res, path.join(dashboardDir, 'app.html'));
824
+ sendFileOrRedirectRoot(res, path.join(dashboardDir, 'app.html'));
796
825
  });
797
826
  // Catch-all for /app/* routes - serve app.html and let client-side routing handle it
798
827
  // Express 5 requires named parameter for wildcards
799
828
  app.get('/app/{*path}', (req, res) => {
800
- sendFileWithFallback(res, path.join(dashboardDir, 'app.html'));
829
+ sendFileOrRedirectRoot(res, path.join(dashboardDir, 'app.html'));
801
830
  });
802
831
  }
803
832
  else {
@@ -1162,8 +1191,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1162
1191
  }
1163
1192
  }
1164
1193
  // Check spawner's active workers (they have accurate team info for spawned agents)
1165
- if (spawner) {
1166
- const activeWorkers = spawner.getActiveWorkers();
1194
+ if (spawnReader) {
1195
+ const activeWorkers = spawnReader.getActiveWorkers();
1167
1196
  for (const worker of activeWorkers) {
1168
1197
  if (worker.team === teamName) {
1169
1198
  members.add(worker.name);
@@ -1466,8 +1495,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1466
1495
  // Helper to check if an agent name is internal/system (should be hidden from UI)
1467
1496
  // Convention: agent names starting with __ are internal (e.g., __spawner__, __DashboardBridge__)
1468
1497
  const isInternalAgent = (name) => {
1498
+ if (name === '__cli_sender__')
1499
+ return false;
1469
1500
  return name.startsWith('__');
1470
1501
  };
1502
+ // Display-name remapping for CLI sender (used across message and history endpoints)
1503
+ const remapAgentName = (name) => {
1504
+ if (name === '__cli_sender__')
1505
+ return 'CLI';
1506
+ return name;
1507
+ };
1471
1508
  const buildThreadSummaryMap = (rows) => {
1472
1509
  const summaries = new Map();
1473
1510
  for (const row of rows) {
@@ -1514,6 +1551,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1514
1551
  let attachments;
1515
1552
  let channel;
1516
1553
  let effectiveFrom = row.from;
1554
+ let effectiveTo = row.to;
1517
1555
  if (row.data && typeof row.data === 'object') {
1518
1556
  if ('attachments' in row.data) {
1519
1557
  attachments = row.data.attachments;
@@ -1527,9 +1565,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1527
1565
  effectiveFrom = row.data.senderName;
1528
1566
  }
1529
1567
  }
1568
+ effectiveFrom = remapAgentName(effectiveFrom);
1569
+ effectiveTo = remapAgentName(effectiveTo);
1530
1570
  return {
1531
1571
  from: effectiveFrom,
1532
- to: row.to,
1572
+ to: effectiveTo,
1533
1573
  content: row.body,
1534
1574
  timestamp: new Date(row.ts).toISOString(),
1535
1575
  id: row.id,
@@ -1775,9 +1815,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1775
1815
  // Ignore errors reading processing state - it's optional
1776
1816
  }
1777
1817
  }
1778
- // Mark spawned agents with isSpawned flag and team
1779
- if (spawner) {
1780
- const activeWorkers = spawner.getActiveWorkers();
1818
+ // Mark spawned agents with isSpawned flag, team, and model
1819
+ if (spawnReader) {
1820
+ const activeWorkers = spawnReader.getActiveWorkers();
1781
1821
  for (const worker of activeWorkers) {
1782
1822
  const agent = agentsMap.get(worker.name);
1783
1823
  if (agent) {
@@ -1785,6 +1825,13 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1785
1825
  if (worker.team) {
1786
1826
  agent.team = worker.team;
1787
1827
  }
1828
+ // Extract model from spawn command (e.g., "codex --model gpt-5.2-codex" → "gpt-5.2-codex")
1829
+ if (worker.cli) {
1830
+ const modelMatch = worker.cli.match(/--model\s+(\S+)/);
1831
+ if (modelMatch) {
1832
+ agent.model = modelMatch[1];
1833
+ }
1834
+ }
1788
1835
  }
1789
1836
  }
1790
1837
  }
@@ -2186,7 +2233,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2186
2233
  };
2187
2234
  // Helper to subscribe to an agent (async to handle spawn timing)
2188
2235
  const subscribeToAgent = async (agentName) => {
2189
- let isSpawned = spawner?.hasWorker(agentName) ?? false;
2236
+ let isSpawned = spawnReader?.hasWorker(agentName) ?? false;
2190
2237
  const isDaemon = isDaemonConnected(agentName);
2191
2238
  // Check if agent exists (either spawned or daemon-connected)
2192
2239
  if (!isSpawned && !isDaemon) {
@@ -2203,14 +2250,14 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2203
2250
  // poll to handle race condition between spawn API returning and
2204
2251
  // WebSocket connection. This is common for setup agents (__setup__*).
2205
2252
  // Longer timeout for CLI auth flows (Cursor, etc.) which can take time to initialize.
2206
- if (!isSpawned && isDaemon && spawner) {
2253
+ if (!isSpawned && isDaemon && spawnReader) {
2207
2254
  const isSetupAgent = agentName.startsWith('__setup__');
2208
2255
  const maxWaitMs = isSetupAgent ? 90000 : 5000; // 90s for setup agents (CLI auth can be slow), 5s otherwise
2209
2256
  const pollIntervalMs = 100;
2210
2257
  const startTime = Date.now();
2211
2258
  while (Date.now() - startTime < maxWaitMs) {
2212
2259
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
2213
- isSpawned = spawner.hasWorker(agentName);
2260
+ isSpawned = spawnReader.hasWorker(agentName);
2214
2261
  if (isSpawned) {
2215
2262
  console.log(`[dashboard] Agent ${agentName} appeared in spawner after ${Date.now() - startTime}ms`);
2216
2263
  break;
@@ -2228,9 +2275,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2228
2275
  }
2229
2276
  logSubscriptions.get(agentName).add(ws);
2230
2277
  debug(`[dashboard] Client subscribed to logs for: ${agentName} (spawned: ${isSpawned}, daemon: ${isDaemon})`);
2231
- if (isSpawned && spawner) {
2278
+ if (isSpawned && spawnReader) {
2232
2279
  // Send initial log history for spawned agents (5000 lines to match xterm scrollback capacity)
2233
- const lines = spawner.getWorkerOutput(agentName, 5000);
2280
+ const lines = spawnReader.getWorkerOutput(agentName, 5000);
2234
2281
  ws.send(JSON.stringify({
2235
2282
  type: 'history',
2236
2283
  agent: agentName,
@@ -2326,8 +2373,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2326
2373
  return;
2327
2374
  }
2328
2375
  // Check if this is a spawned agent (we can only send input to spawned agents)
2329
- if (spawner?.hasWorker(agentName)) {
2330
- const success = spawner.sendWorkerInput(agentName, msg.data);
2376
+ if (spawnReader?.hasWorker(agentName)) {
2377
+ const success = spawnReader.sendWorkerInput(agentName, msg.data);
2331
2378
  if (!success) {
2332
2379
  console.warn(`[dashboard] Failed to send input to agent ${agentName}`);
2333
2380
  }
@@ -2438,10 +2485,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2438
2485
  }
2439
2486
  });
2440
2487
  };
2441
- // Helper to broadcast channel messages to all presence clients
2442
- // This is used by fallback relay clients to forward messages to cloud-connected users
2488
+ // Helper to broadcast channel messages to all connected clients
2489
+ // Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
2443
2490
  const broadcastChannelMessage = (message) => {
2444
2491
  const payload = JSON.stringify(message);
2492
+ // Broadcast to main WebSocket clients (local mode)
2493
+ wss.clients.forEach((client) => {
2494
+ if (client.readyState === WebSocket.OPEN) {
2495
+ client.send(payload);
2496
+ }
2497
+ });
2498
+ // Also broadcast to presence WebSocket clients (cloud mode)
2445
2499
  wssPresence.clients.forEach((client) => {
2446
2500
  if (client.readyState === WebSocket.OPEN) {
2447
2501
  client.send(payload);
@@ -2829,8 +2883,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2829
2883
  try {
2830
2884
  const availableAgents = [];
2831
2885
  // Get spawned agents from spawner
2832
- if (spawner) {
2833
- const activeWorkers = spawner.getActiveWorkers();
2886
+ if (spawnReader) {
2887
+ const activeWorkers = spawnReader.getActiveWorkers();
2834
2888
  for (const worker of activeWorkers) {
2835
2889
  availableAgents.push({
2836
2890
  id: worker.name,
@@ -3234,6 +3288,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
3234
3288
  if (workspaceId && data?._workspaceId && data._workspaceId !== workspaceId) {
3235
3289
  return false;
3236
3290
  }
3291
+ // Filter out internal/system agents (e.g., __system__, __spawner__)
3292
+ if (isInternalAgent(m.from))
3293
+ return false;
3237
3294
  // Accept message if it has _isChannelMessage flag OR if it's addressed to a channel
3238
3295
  return Boolean(data?._isChannelMessage) || (m.to && m.to.startsWith('#'));
3239
3296
  });
@@ -3960,8 +4017,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
3960
4017
  try {
3961
4018
  const agents = [];
3962
4019
  // Get metrics from spawner's active workers
3963
- if (spawner) {
3964
- const activeWorkers = spawner.getActiveWorkers();
4020
+ if (spawnReader) {
4021
+ const activeWorkers = spawnReader.getActiveWorkers();
3965
4022
  for (const worker of activeWorkers) {
3966
4023
  // Get memory and CPU usage
3967
4024
  let rssBytes = 0;
@@ -4059,8 +4116,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4059
4116
  const totalCrashes24h = 0;
4060
4117
  let totalAlerts24h = 0;
4061
4118
  // Get spawned agent count
4062
- if (spawner) {
4063
- const workers = spawner.getActiveWorkers();
4119
+ if (spawnReader) {
4120
+ const workers = spawnReader.getActiveWorkers();
4064
4121
  agentCount = workers.length;
4065
4122
  // Check for high memory usage
4066
4123
  for (const worker of workers) {
@@ -4293,8 +4350,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4293
4350
  }
4294
4351
  const result = messages.map(m => ({
4295
4352
  id: m.id,
4296
- from: m.from,
4297
- to: m.to,
4353
+ from: remapAgentName(m.from),
4354
+ to: remapAgentName(m.to),
4298
4355
  content: m.body,
4299
4356
  timestamp: new Date(m.ts).toISOString(),
4300
4357
  thread: m.thread,
@@ -4329,8 +4386,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4329
4386
  // Skip messages from/to internal system agents (e.g., __spawner__)
4330
4387
  if (isInternalAgent(msg.from) || isInternalAgent(msg.to))
4331
4388
  continue;
4332
- // Create normalized key (sorted participants)
4333
- const participants = [msg.from, msg.to].sort();
4389
+ // Create normalized key (sorted participants, with display names)
4390
+ const participants = [remapAgentName(msg.from), remapAgentName(msg.to)].sort();
4334
4391
  const key = participants.join(':');
4335
4392
  const existing = conversationMap.get(key);
4336
4393
  if (existing) {
@@ -4438,19 +4495,19 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4438
4495
  * - raw: If 'true', return raw output instead of cleaned lines
4439
4496
  */
4440
4497
  app.get('/api/logs/:name', (req, res) => {
4441
- if (!spawner) {
4498
+ if (!spawnReader) {
4442
4499
  return res.status(503).json({ error: 'Spawner not enabled' });
4443
4500
  }
4444
4501
  const { name } = req.params;
4445
4502
  const limit = req.query.limit ? parseInt(req.query.limit, 10) : 500;
4446
4503
  const raw = req.query.raw === 'true';
4447
4504
  // Check if worker exists
4448
- if (!spawner.hasWorker(name)) {
4505
+ if (!spawnReader.hasWorker(name)) {
4449
4506
  return res.status(404).json({ error: `Agent ${name} not found` });
4450
4507
  }
4451
4508
  try {
4452
4509
  if (raw) {
4453
- const output = spawner.getWorkerRawOutput(name);
4510
+ const output = spawnReader.getWorkerRawOutput(name);
4454
4511
  res.json({
4455
4512
  name,
4456
4513
  raw: true,
@@ -4459,7 +4516,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4459
4516
  });
4460
4517
  }
4461
4518
  else {
4462
- const lines = spawner.getWorkerOutput(name, limit);
4519
+ const lines = spawnReader.getWorkerOutput(name, limit);
4463
4520
  res.json({
4464
4521
  name,
4465
4522
  raw: false,
@@ -4478,11 +4535,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4478
4535
  * GET /api/logs - List all agents with available logs
4479
4536
  */
4480
4537
  app.get('/api/logs', (req, res) => {
4481
- if (!spawner) {
4538
+ if (!spawnReader) {
4482
4539
  return res.status(503).json({ error: 'Spawner not enabled' });
4483
4540
  }
4484
4541
  try {
4485
- const workers = spawner.getActiveWorkers();
4542
+ const workers = spawnReader.getActiveWorkers();
4486
4543
  const agents = workers.map(w => ({
4487
4544
  name: w.name,
4488
4545
  cli: w.cli,
@@ -4513,7 +4570,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4513
4570
  * Body: { name: string, cli?: string, task?: string, team?: string, spawnerName?, cwd?, interactive?, shadowMode?, shadowAgent?, shadowOf?, shadowTriggers?, shadowSpeakOn? }
4514
4571
  */
4515
4572
  app.post('/api/spawn', async (req, res) => {
4516
- if (!spawner) {
4573
+ if (!spawner && !useExternalSpawnManager) {
4517
4574
  return res.status(503).json({
4518
4575
  success: false,
4519
4576
  error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
@@ -4527,23 +4584,56 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4527
4584
  });
4528
4585
  }
4529
4586
  try {
4530
- const request = {
4531
- name,
4532
- cli,
4533
- task,
4534
- team: team || undefined, // Optional team name
4535
- spawnerName: spawnerName || undefined, // For policy enforcement
4536
- cwd: cwd || undefined, // Working directory
4537
- interactive, // Disables auto-accept for auth setup flows
4538
- shadowMode,
4539
- shadowAgent,
4540
- shadowOf,
4541
- shadowTriggers,
4542
- shadowSpeakOn,
4543
- userId: typeof userId === 'string' ? userId : undefined,
4544
- includeWorkflowConventions: true, // Cloud opts into ACK/DONE workflow conventions
4545
- };
4546
- const result = await spawner.spawn(request);
4587
+ let result;
4588
+ if (useExternalSpawnManager) {
4589
+ // Route spawn through SDK → daemon socket → SpawnManager
4590
+ // This ensures relay-pty binary resolution works regardless of install method
4591
+ const client = await getRelayClient('Dashboard');
4592
+ if (!client) {
4593
+ return res.status(503).json({
4594
+ success: false,
4595
+ error: 'Not connected to relay daemon',
4596
+ });
4597
+ }
4598
+ // spawnerName/userId/includeWorkflowConventions need SDK >= 2.2.0 types
4599
+ // Cast removed once SDK is republished with these fields
4600
+ result = await client.spawn({
4601
+ name,
4602
+ cli,
4603
+ task,
4604
+ team: team || undefined,
4605
+ cwd: cwd || undefined,
4606
+ interactive,
4607
+ shadowMode,
4608
+ shadowAgent,
4609
+ shadowOf,
4610
+ shadowTriggers,
4611
+ shadowSpeakOn,
4612
+ spawnerName: spawnerName || undefined,
4613
+ userId: typeof userId === 'string' ? userId : undefined,
4614
+ includeWorkflowConventions: true,
4615
+ });
4616
+ }
4617
+ else {
4618
+ // Fall back to local AgentSpawner (standalone mode)
4619
+ const request = {
4620
+ name,
4621
+ cli,
4622
+ task,
4623
+ team: team || undefined,
4624
+ spawnerName: spawnerName || undefined,
4625
+ cwd: cwd || undefined,
4626
+ interactive,
4627
+ shadowMode,
4628
+ shadowAgent,
4629
+ shadowOf,
4630
+ shadowTriggers,
4631
+ shadowSpeakOn,
4632
+ userId: typeof userId === 'string' ? userId : undefined,
4633
+ includeWorkflowConventions: true,
4634
+ };
4635
+ result = await spawner.spawn(request);
4636
+ }
4547
4637
  if (result.success) {
4548
4638
  // Broadcast update to WebSocket clients
4549
4639
  broadcastData().catch(() => { });
@@ -4573,7 +4663,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4573
4663
  * Body: { cli?: string }
4574
4664
  */
4575
4665
  app.post('/api/spawn/architect', async (req, res) => {
4576
- if (!spawner) {
4666
+ if (!spawner && !useExternalSpawnManager) {
4577
4667
  return res.status(503).json({
4578
4668
  success: false,
4579
4669
  error: 'Spawner not enabled. Start dashboard with enableSpawner: true',
@@ -4581,7 +4671,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4581
4671
  }
4582
4672
  const { cli = 'claude' } = req.body;
4583
4673
  // Check if Architect already exists
4584
- const activeWorkers = spawner.getActiveWorkers();
4674
+ const activeWorkers = spawnReader?.getActiveWorkers() || [];
4585
4675
  if (activeWorkers.some(w => w.name.toLowerCase() === 'architect')) {
4586
4676
  return res.status(409).json({
4587
4677
  success: false,
@@ -4659,12 +4749,27 @@ Then output: \`->relay-file:all\`
4659
4749
 
4660
4750
  Start by greeting the project leads and asking for status updates.`;
4661
4751
  try {
4662
- const result = await spawner.spawn({
4663
- name: 'Architect',
4664
- cli,
4665
- task: architectPrompt,
4666
- includeWorkflowConventions: true, // Cloud opts into ACK/DONE workflow conventions
4667
- });
4752
+ let result;
4753
+ if (useExternalSpawnManager) {
4754
+ const client = await getRelayClient('Dashboard');
4755
+ if (!client) {
4756
+ return res.status(503).json({ success: false, error: 'Not connected to relay daemon' });
4757
+ }
4758
+ result = await client.spawn({
4759
+ name: 'Architect',
4760
+ cli,
4761
+ task: architectPrompt,
4762
+ includeWorkflowConventions: true,
4763
+ });
4764
+ }
4765
+ else {
4766
+ result = await spawner.spawn({
4767
+ name: 'Architect',
4768
+ cli,
4769
+ task: architectPrompt,
4770
+ includeWorkflowConventions: true,
4771
+ });
4772
+ }
4668
4773
  if (result.success) {
4669
4774
  broadcastData().catch(() => { });
4670
4775
  }
@@ -4694,8 +4799,8 @@ Start by greeting the project leads and asking for status updates.`;
4694
4799
  // Collect agents from all available sources
4695
4800
  const agentsByName = new Map();
4696
4801
  // Source 1: Spawner's active workers (authoritative for spawned agents)
4697
- if (spawner) {
4698
- for (const worker of spawner.getActiveWorkers()) {
4802
+ if (spawnReader) {
4803
+ for (const worker of spawnReader.getActiveWorkers()) {
4699
4804
  agentsByName.set(worker.name, {
4700
4805
  name: worker.name,
4701
4806
  cli: worker.cli,
@@ -4742,7 +4847,7 @@ Start by greeting the project leads and asking for status updates.`;
4742
4847
  agents,
4743
4848
  // Include source info for debugging
4744
4849
  sources: {
4745
- spawnerEnabled: !!spawner,
4850
+ spawnerEnabled: !!spawnReader,
4746
4851
  daemonAgentsFile: fs.existsSync(agentsPath),
4747
4852
  },
4748
4853
  });
@@ -4751,7 +4856,7 @@ Start by greeting the project leads and asking for status updates.`;
4751
4856
  * DELETE /api/spawned/:name - Release a spawned agent
4752
4857
  */
4753
4858
  app.delete('/api/spawned/:name', async (req, res) => {
4754
- if (!spawner) {
4859
+ if (!spawner && !useExternalSpawnManager) {
4755
4860
  return res.status(503).json({
4756
4861
  success: false,
4757
4862
  error: 'Spawner not enabled',
@@ -4759,7 +4864,19 @@ Start by greeting the project leads and asking for status updates.`;
4759
4864
  }
4760
4865
  const { name } = req.params;
4761
4866
  try {
4762
- const released = await spawner.release(name);
4867
+ let released;
4868
+ if (useExternalSpawnManager) {
4869
+ // Route release through SDK → daemon socket → SpawnManager
4870
+ const client = await getRelayClient('Dashboard');
4871
+ if (!client) {
4872
+ return res.status(503).json({ success: false, error: 'Not connected to relay daemon' });
4873
+ }
4874
+ const result = await client.release(name);
4875
+ released = result.success;
4876
+ }
4877
+ else {
4878
+ released = await spawner.release(name);
4879
+ }
4763
4880
  if (released) {
4764
4881
  broadcastData().catch(() => { });
4765
4882
  // Broadcast agent_released event to activity feed
@@ -4792,7 +4909,7 @@ Start by greeting the project leads and asking for status updates.`;
4792
4909
  * This is useful for breaking agents out of stuck loops without terminating them.
4793
4910
  */
4794
4911
  app.post('/api/agents/by-name/:name/interrupt', (req, res) => {
4795
- if (!spawner) {
4912
+ if (!spawnReader) {
4796
4913
  return res.status(503).json({
4797
4914
  success: false,
4798
4915
  error: 'Spawner not enabled',
@@ -4800,7 +4917,7 @@ Start by greeting the project leads and asking for status updates.`;
4800
4917
  }
4801
4918
  const { name } = req.params;
4802
4919
  // Check if agent exists
4803
- if (!spawner.hasWorker(name)) {
4920
+ if (!spawnReader.hasWorker(name)) {
4804
4921
  return res.status(404).json({
4805
4922
  success: false,
4806
4923
  error: `Agent ${name} not found or not spawned`,
@@ -4809,7 +4926,7 @@ Start by greeting the project leads and asking for status updates.`;
4809
4926
  try {
4810
4927
  // Send ESC ESC sequence to interrupt the agent
4811
4928
  // ESC = 0x1b in hexadecimal
4812
- const success = spawner.sendWorkerInput(name, '\x1b\x1b');
4929
+ const success = spawnReader.sendWorkerInput(name, '\x1b\x1b');
4813
4930
  if (success) {
4814
4931
  console.log(`[api] Sent interrupt (ESC ESC) to agent ${name}`);
4815
4932
  res.json({
@@ -5162,7 +5279,7 @@ Start by greeting the project leads and asking for status updates.`;
5162
5279
  */
5163
5280
  app.get('/api/fleet/servers', async (_req, res) => {
5164
5281
  const servers = [];
5165
- const localAgents = spawner?.getActiveWorkers() || [];
5282
+ const localAgents = spawnReader?.getActiveWorkers() || [];
5166
5283
  const agentStatuses = await loadAgentStatuses();
5167
5284
  let hasBridgeProjects = false;
5168
5285
  // Check for bridge connections first
@@ -5246,7 +5363,7 @@ Start by greeting the project leads and asking for status updates.`;
5246
5363
  * GET /api/fleet/stats - Get aggregate fleet statistics
5247
5364
  */
5248
5365
  app.get('/api/fleet/stats', async (_req, res) => {
5249
- const localAgents = spawner?.getActiveWorkers() || [];
5366
+ const localAgents = spawnReader?.getActiveWorkers() || [];
5250
5367
  const agentStatuses = await loadAgentStatuses();
5251
5368
  const totalAgents = localAgents.length;
5252
5369
  let onlineAgents = 0;
@@ -5551,8 +5668,9 @@ Start by greeting the project leads and asking for status updates.`;
5551
5668
  const listenCallback = async () => {
5552
5669
  console.log(`Dashboard running at http://${host || 'localhost'}:${availablePort} (build: cloud-channels-v2)`);
5553
5670
  console.log(`Monitoring: ${dataDir}`);
5554
- // Set the dashboard port on spawner so spawned agents can use the API for nested spawns
5555
- if (spawner) {
5671
+ // Set the dashboard port on local spawner so spawned agents can use the API for nested spawns
5672
+ // Not needed when using external SpawnManager (daemon handles this)
5673
+ if (spawner && !useExternalSpawnManager) {
5556
5674
  spawner.setDashboardPort(availablePort);
5557
5675
  }
5558
5676
  // Start health worker on separate thread for reliable health checks