@agent-relay/dashboard-server 2.0.46 → 2.0.48

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 (54) hide show
  1. package/dist/server.js +147 -7
  2. package/dist/server.js.map +1 -1
  3. package/out/404.html +1 -1
  4. package/out/about.html +1 -1
  5. package/out/about.txt +1 -1
  6. package/out/app/onboarding.html +1 -1
  7. package/out/app/onboarding.txt +1 -1
  8. package/out/app.html +1 -1
  9. package/out/app.txt +1 -1
  10. package/out/blog.html +1 -1
  11. package/out/blog.txt +1 -1
  12. package/out/careers.html +1 -1
  13. package/out/careers.txt +1 -1
  14. package/out/changelog.html +1 -1
  15. package/out/changelog.txt +1 -1
  16. package/out/cloud/link.html +1 -1
  17. package/out/cloud/link.txt +1 -1
  18. package/out/complete-profile.html +1 -1
  19. package/out/complete-profile.txt +1 -1
  20. package/out/connect-repos.html +1 -1
  21. package/out/connect-repos.txt +1 -1
  22. package/out/contact.html +1 -1
  23. package/out/contact.txt +1 -1
  24. package/out/docs.html +1 -1
  25. package/out/docs.txt +1 -1
  26. package/out/history.html +1 -1
  27. package/out/history.txt +1 -1
  28. package/out/index.html +1 -1
  29. package/out/index.txt +1 -1
  30. package/out/login.html +1 -1
  31. package/out/login.txt +1 -1
  32. package/out/metrics.html +1 -1
  33. package/out/metrics.txt +1 -1
  34. package/out/pricing.html +1 -1
  35. package/out/pricing.txt +1 -1
  36. package/out/privacy.html +1 -1
  37. package/out/privacy.txt +1 -1
  38. package/out/providers/setup/claude.html +1 -1
  39. package/out/providers/setup/claude.txt +1 -1
  40. package/out/providers/setup/codex.html +1 -1
  41. package/out/providers/setup/codex.txt +1 -1
  42. package/out/providers/setup/cursor.html +1 -1
  43. package/out/providers/setup/cursor.txt +1 -1
  44. package/out/providers.html +1 -1
  45. package/out/providers.txt +1 -1
  46. package/out/security.html +1 -1
  47. package/out/security.txt +1 -1
  48. package/out/signup.html +1 -1
  49. package/out/signup.txt +1 -1
  50. package/out/terms.html +1 -1
  51. package/out/terms.txt +1 -1
  52. package/package.json +10 -10
  53. /package/out/_next/static/{kt1pBkOoE9ZJk69H9H3_X → HwEoE2BFhgZdESAQLY9dG}/_buildManifest.js +0 -0
  54. /package/out/_next/static/{kt1pBkOoE9ZJk69H9H3_X → HwEoE2BFhgZdESAQLY9dG}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -311,10 +311,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
311
311
  const disableStorage = process.env.RELAY_DISABLE_STORAGE === 'true';
312
312
  // Use createStorageAdapter to match daemon's storage type (JSONL by default)
313
313
  // This ensures dashboard reads from the same storage as daemon writes to
314
+ // Enable watchForChanges so JSONL adapter auto-reloads when daemon writes new messages
314
315
  const storagePath = dbPath ?? path.join(dataDir, 'messages.sqlite');
315
316
  const storage = disableStorage
316
317
  ? undefined
317
- : await createStorageAdapter(storagePath);
318
+ : await createStorageAdapter(storagePath, { watchForChanges: true });
318
319
  const defaultWorkspaceId = process.env.RELAY_WORKSPACE_ID ?? process.env.AGENT_RELAY_WORKSPACE_ID;
319
320
  const resolveWorkspaceId = (req) => {
320
321
  const fromQuery = req.query.workspaceId;
@@ -548,6 +549,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
548
549
  });
549
550
  // Track log subscriptions: agentName -> Set of WebSocket clients
550
551
  const logSubscriptions = new Map();
552
+ // Track file watchers for externally-spawned worker logs (module scope to avoid duplicates)
553
+ const fileWatchers = new Map();
554
+ const fileLastSize = new Map();
551
555
  // Track alive status for ping/pong keepalive on main dashboard connections
552
556
  // This prevents TCP/proxy timeouts from killing idle workspace connections
553
557
  const mainClientAlive = new WeakMap();
@@ -1742,6 +1746,23 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1742
1746
  }
1743
1747
  }
1744
1748
  }
1749
+ // Also check workers.json for externally-spawned workers (e.g., from agentswarm)
1750
+ // These workers have log files but weren't spawned by the dashboard's spawner
1751
+ const workersJsonPath = path.join(teamDir, 'workers.json');
1752
+ if (fs.existsSync(workersJsonPath)) {
1753
+ try {
1754
+ const workersData = JSON.parse(fs.readFileSync(workersJsonPath, 'utf-8'));
1755
+ for (const worker of workersData.workers || []) {
1756
+ const agent = agentsMap.get(worker.name);
1757
+ if (agent && !agent.isSpawned && worker.logFile && fs.existsSync(worker.logFile)) {
1758
+ agent.isSpawned = true; // Mark as spawned so log button appears
1759
+ }
1760
+ }
1761
+ }
1762
+ catch (err) {
1763
+ // Ignore errors reading workers.json
1764
+ }
1765
+ }
1745
1766
  // Set team from teams.json for agents that don't have a team yet
1746
1767
  // This ensures agents defined in teams.json are associated with their team
1747
1768
  // even if they weren't spawned via auto-spawn
@@ -2038,6 +2059,89 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2038
2059
  return false;
2039
2060
  }
2040
2061
  };
2062
+ const getExternalWorkerInfo = (agentName) => {
2063
+ const workersPath = path.join(teamDir, 'workers.json');
2064
+ if (!fs.existsSync(workersPath))
2065
+ return null;
2066
+ try {
2067
+ const data = JSON.parse(fs.readFileSync(workersPath, 'utf-8'));
2068
+ const worker = data.workers?.find((w) => w.name === agentName);
2069
+ return worker ?? null;
2070
+ }
2071
+ catch {
2072
+ return null;
2073
+ }
2074
+ };
2075
+ // Helper to read logs from a log file (for externally-spawned workers)
2076
+ const readLogsFromFile = (logFile, limit = 5000) => {
2077
+ if (!fs.existsSync(logFile))
2078
+ return [];
2079
+ try {
2080
+ const content = fs.readFileSync(logFile, 'utf-8');
2081
+ const lines = content.split('\n');
2082
+ // Return last `limit` lines
2083
+ return lines.slice(-limit);
2084
+ }
2085
+ catch {
2086
+ return [];
2087
+ }
2088
+ };
2089
+ // Helper to start watching a log file for live updates
2090
+ const watchLogFile = (agentName, logFile) => {
2091
+ if (fileWatchers.has(agentName))
2092
+ return; // Already watching
2093
+ if (!fs.existsSync(logFile))
2094
+ return;
2095
+ try {
2096
+ // Track current file size for incremental reads
2097
+ const stats = fs.statSync(logFile);
2098
+ fileLastSize.set(agentName, stats.size);
2099
+ const watcher = fs.watch(logFile, (eventType) => {
2100
+ if (eventType !== 'change')
2101
+ return;
2102
+ const clients = logSubscriptions.get(agentName);
2103
+ if (!clients || clients.size === 0) {
2104
+ // No subscribers, stop watching
2105
+ watcher.close();
2106
+ fileWatchers.delete(agentName);
2107
+ fileLastSize.delete(agentName);
2108
+ return;
2109
+ }
2110
+ try {
2111
+ const newStats = fs.statSync(logFile);
2112
+ const lastSize = fileLastSize.get(agentName) || 0;
2113
+ if (newStats.size > lastSize) {
2114
+ // Read only the new content
2115
+ const fd = fs.openSync(logFile, 'r');
2116
+ const buffer = Buffer.alloc(newStats.size - lastSize);
2117
+ fs.readSync(fd, buffer, 0, buffer.length, lastSize);
2118
+ fs.closeSync(fd);
2119
+ const newContent = buffer.toString('utf-8');
2120
+ fileLastSize.set(agentName, newStats.size);
2121
+ // Broadcast to subscribed clients
2122
+ const payload = JSON.stringify({
2123
+ type: 'output',
2124
+ agent: agentName,
2125
+ data: newContent,
2126
+ timestamp: new Date().toISOString(),
2127
+ });
2128
+ for (const client of clients) {
2129
+ if (client.readyState === WebSocket.OPEN) {
2130
+ client.send(payload);
2131
+ }
2132
+ }
2133
+ }
2134
+ }
2135
+ catch (err) {
2136
+ console.error(`[dashboard] Error reading log file updates for ${agentName}:`, err);
2137
+ }
2138
+ });
2139
+ fileWatchers.set(agentName, watcher);
2140
+ }
2141
+ catch (err) {
2142
+ console.error(`[dashboard] Failed to watch log file for ${agentName}:`, err);
2143
+ }
2144
+ };
2041
2145
  // Helper to subscribe to an agent (async to handle spawn timing)
2042
2146
  const subscribeToAgent = async (agentName) => {
2043
2147
  let isSpawned = spawner?.hasWorker(agentName) ?? false;
@@ -2092,12 +2196,27 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2092
2196
  }));
2093
2197
  }
2094
2198
  else {
2095
- // For daemon-connected agents, explain that PTY output isn't available
2096
- ws.send(JSON.stringify({
2097
- type: 'history',
2098
- agent: agentName,
2099
- lines: [`[${agentName} is a daemon-connected agent - PTY output not available. Showing relay messages only.]`],
2100
- }));
2199
+ // Check if this is an externally-spawned worker with a log file
2200
+ const externalWorker = getExternalWorkerInfo(agentName);
2201
+ if (externalWorker?.logFile && fs.existsSync(externalWorker.logFile)) {
2202
+ // Read logs from the external worker's log file
2203
+ const lines = readLogsFromFile(externalWorker.logFile, 5000);
2204
+ ws.send(JSON.stringify({
2205
+ type: 'history',
2206
+ agent: agentName,
2207
+ lines,
2208
+ }));
2209
+ // Start watching the log file for live updates
2210
+ watchLogFile(agentName, externalWorker.logFile);
2211
+ }
2212
+ else {
2213
+ // For daemon-connected agents without log files, explain that PTY output isn't available
2214
+ ws.send(JSON.stringify({
2215
+ type: 'history',
2216
+ agent: agentName,
2217
+ lines: [`[${agentName} is a daemon-connected agent - PTY output not available. Showing relay messages only.]`],
2218
+ }));
2219
+ }
2101
2220
  }
2102
2221
  ws.send(JSON.stringify({
2103
2222
  type: 'subscribed',
@@ -2128,6 +2247,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2128
2247
  const agentName = msg.unsubscribe;
2129
2248
  clientSubscriptions.delete(agentName);
2130
2249
  logSubscriptions.get(agentName)?.delete(ws);
2250
+ // Clean up file watcher if no more subscribers
2251
+ const remainingClients = logSubscriptions.get(agentName);
2252
+ if (!remainingClients || remainingClients.size === 0) {
2253
+ const watcher = fileWatchers.get(agentName);
2254
+ if (watcher) {
2255
+ watcher.close();
2256
+ fileWatchers.delete(agentName);
2257
+ fileLastSize.delete(agentName);
2258
+ }
2259
+ }
2131
2260
  console.log(`[dashboard] Client unsubscribed from logs for: ${agentName}`);
2132
2261
  ws.send(JSON.stringify({
2133
2262
  type: 'unsubscribed',
@@ -2173,6 +2302,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2173
2302
  // Clean up subscriptions on disconnect
2174
2303
  for (const agentName of clientSubscriptions) {
2175
2304
  logSubscriptions.get(agentName)?.delete(ws);
2305
+ // Clean up file watchers if no more subscribers
2306
+ const remainingClients = logSubscriptions.get(agentName);
2307
+ if (!remainingClients || remainingClients.size === 0) {
2308
+ const watcher = fileWatchers.get(agentName);
2309
+ if (watcher) {
2310
+ watcher.close();
2311
+ fileWatchers.delete(agentName);
2312
+ fileLastSize.delete(agentName);
2313
+ console.log(`[dashboard] Stopped watching log file for: ${agentName}`);
2314
+ }
2315
+ }
2176
2316
  }
2177
2317
  const reasonStr = reason?.toString() || 'no reason';
2178
2318
  console.log(`[dashboard] Logs WebSocket client disconnected (code: ${code}, reason: ${reasonStr})`);