@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.
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -100
- 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-9aee36b975a9556a.js +1 -0
- package/out/_next/static/css/2ee05ba949b3ac9f.css +1 -0
- package/out/about.html +2 -2
- 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 +2 -2
- 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 +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- 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 +2 -2
- 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 +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- 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 +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +1 -1
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- 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 +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- 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/css/ad96af0f7a47b705.css +0 -1
- /package/out/_next/static/{dZJGDdzFNEYs9W9huG7Nx → 6oI4iquYj1QbK8njLsK3s}/_buildManifest.js +0 -0
- /package/out/_next/static/{dZJGDdzFNEYs9W9huG7Nx → 6oI4iquYj1QbK8njLsK3s}/_ssgManifest.js +0 -0
package/dist/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
//
|
|
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) => {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
+
sendFileOrText404(res, path.join(dashboardDir, 'metrics.html'), uiMissingMessage);
|
|
793
822
|
});
|
|
794
823
|
app.get('/app', (req, res) => {
|
|
795
|
-
|
|
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
|
-
|
|
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 (
|
|
1166
|
-
const activeWorkers =
|
|
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:
|
|
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
|
|
1779
|
-
if (
|
|
1780
|
-
const activeWorkers =
|
|
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 =
|
|
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 &&
|
|
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 =
|
|
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 &&
|
|
2278
|
+
if (isSpawned && spawnReader) {
|
|
2232
2279
|
// Send initial log history for spawned agents (5000 lines to match xterm scrollback capacity)
|
|
2233
|
-
const lines =
|
|
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 (
|
|
2330
|
-
const success =
|
|
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
|
|
2442
|
-
//
|
|
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 (
|
|
2833
|
-
const activeWorkers =
|
|
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 (
|
|
3964
|
-
const activeWorkers =
|
|
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 (
|
|
4063
|
-
const workers =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
4538
|
+
if (!spawnReader) {
|
|
4482
4539
|
return res.status(503).json({ error: 'Spawner not enabled' });
|
|
4483
4540
|
}
|
|
4484
4541
|
try {
|
|
4485
|
-
const workers =
|
|
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
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
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 =
|
|
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
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
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 (
|
|
4698
|
-
for (const worker of
|
|
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: !!
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|