@agent-relay/dashboard-server 2.0.47 → 2.0.49

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 (56) hide show
  1. package/dist/server.js +148 -25
  2. package/dist/server.js.map +1 -1
  3. package/dist/start.js +33 -2
  4. package/dist/start.js.map +1 -1
  5. package/out/404.html +1 -1
  6. package/out/about.html +1 -1
  7. package/out/about.txt +1 -1
  8. package/out/app/onboarding.html +1 -1
  9. package/out/app/onboarding.txt +1 -1
  10. package/out/app.html +1 -1
  11. package/out/app.txt +1 -1
  12. package/out/blog.html +1 -1
  13. package/out/blog.txt +1 -1
  14. package/out/careers.html +1 -1
  15. package/out/careers.txt +1 -1
  16. package/out/changelog.html +1 -1
  17. package/out/changelog.txt +1 -1
  18. package/out/cloud/link.html +1 -1
  19. package/out/cloud/link.txt +1 -1
  20. package/out/complete-profile.html +1 -1
  21. package/out/complete-profile.txt +1 -1
  22. package/out/connect-repos.html +1 -1
  23. package/out/connect-repos.txt +1 -1
  24. package/out/contact.html +1 -1
  25. package/out/contact.txt +1 -1
  26. package/out/docs.html +1 -1
  27. package/out/docs.txt +1 -1
  28. package/out/history.html +1 -1
  29. package/out/history.txt +1 -1
  30. package/out/index.html +1 -1
  31. package/out/index.txt +1 -1
  32. package/out/login.html +1 -1
  33. package/out/login.txt +1 -1
  34. package/out/metrics.html +1 -1
  35. package/out/metrics.txt +1 -1
  36. package/out/pricing.html +1 -1
  37. package/out/pricing.txt +1 -1
  38. package/out/privacy.html +1 -1
  39. package/out/privacy.txt +1 -1
  40. package/out/providers/setup/claude.html +1 -1
  41. package/out/providers/setup/claude.txt +1 -1
  42. package/out/providers/setup/codex.html +1 -1
  43. package/out/providers/setup/codex.txt +1 -1
  44. package/out/providers/setup/cursor.html +1 -1
  45. package/out/providers/setup/cursor.txt +1 -1
  46. package/out/providers.html +1 -1
  47. package/out/providers.txt +1 -1
  48. package/out/security.html +1 -1
  49. package/out/security.txt +1 -1
  50. package/out/signup.html +1 -1
  51. package/out/signup.txt +1 -1
  52. package/out/terms.html +1 -1
  53. package/out/terms.txt +1 -1
  54. package/package.json +10 -10
  55. /package/out/_next/static/{8HZf56LQSyhTavUG83J66 → SfyoLSw2xdXgcKQ_dT07o}/_buildManifest.js +0 -0
  56. /package/out/_next/static/{8HZf56LQSyhTavUG83J66 → SfyoLSw2xdXgcKQ_dT07o}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -549,6 +549,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
549
549
  });
550
550
  // Track log subscriptions: agentName -> Set of WebSocket clients
551
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();
552
555
  // Track alive status for ping/pong keepalive on main dashboard connections
553
556
  // This prevents TCP/proxy timeouts from killing idle workspace connections
554
557
  const mainClientAlive = new WeakMap();
@@ -1191,6 +1194,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1191
1194
  }
1192
1195
  const hasMessageData = Object.keys(messageData).length > 0;
1193
1196
  // Send to all targets (single agent, team members, or broadcast)
1197
+ // Note: We do NOT persist outgoing messages here because the relay daemon
1198
+ // already persists them when delivered (in router.persistDeliverEnvelope).
1199
+ // Persisting here would cause duplicate messages in storage.
1194
1200
  let allSent = true;
1195
1201
  for (const target of targets) {
1196
1202
  const sent = relayClient.sendMessage(target, message, 'message', hasMessageData ? messageData : undefined, thread);
@@ -1198,25 +1204,6 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1198
1204
  allSent = false;
1199
1205
  console.error(`[dashboard] Failed to send message to ${target}`);
1200
1206
  }
1201
- else if (storage) {
1202
- // Persist outgoing message to storage so it survives page refresh
1203
- const messageId = `out-${crypto.randomUUID()}`;
1204
- storage.saveMessage({
1205
- id: messageId,
1206
- ts: Date.now(),
1207
- from: senderName || 'Dashboard',
1208
- to: target,
1209
- topic: thread,
1210
- kind: 'message',
1211
- body: message,
1212
- data: hasMessageData ? messageData : undefined,
1213
- status: 'read', // Outgoing messages are already "read" by sender
1214
- is_urgent: false,
1215
- is_broadcast: isBroadcast,
1216
- }).catch((err) => {
1217
- console.error('[dashboard] Failed to persist outgoing message', err);
1218
- });
1219
- }
1220
1207
  }
1221
1208
  if (allSent) {
1222
1209
  // Broadcast updated data to all connected clients so they see the sent message
@@ -1743,6 +1730,23 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1743
1730
  }
1744
1731
  }
1745
1732
  }
1733
+ // Also check workers.json for externally-spawned workers (e.g., from agentswarm)
1734
+ // These workers have log files but weren't spawned by the dashboard's spawner
1735
+ const workersJsonPath = path.join(teamDir, 'workers.json');
1736
+ if (fs.existsSync(workersJsonPath)) {
1737
+ try {
1738
+ const workersData = JSON.parse(fs.readFileSync(workersJsonPath, 'utf-8'));
1739
+ for (const worker of workersData.workers || []) {
1740
+ const agent = agentsMap.get(worker.name);
1741
+ if (agent && !agent.isSpawned && worker.logFile && fs.existsSync(worker.logFile)) {
1742
+ agent.isSpawned = true; // Mark as spawned so log button appears
1743
+ }
1744
+ }
1745
+ }
1746
+ catch (err) {
1747
+ // Ignore errors reading workers.json
1748
+ }
1749
+ }
1746
1750
  // Set team from teams.json for agents that don't have a team yet
1747
1751
  // This ensures agents defined in teams.json are associated with their team
1748
1752
  // even if they weren't spawned via auto-spawn
@@ -2039,6 +2043,89 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2039
2043
  return false;
2040
2044
  }
2041
2045
  };
2046
+ const getExternalWorkerInfo = (agentName) => {
2047
+ const workersPath = path.join(teamDir, 'workers.json');
2048
+ if (!fs.existsSync(workersPath))
2049
+ return null;
2050
+ try {
2051
+ const data = JSON.parse(fs.readFileSync(workersPath, 'utf-8'));
2052
+ const worker = data.workers?.find((w) => w.name === agentName);
2053
+ return worker ?? null;
2054
+ }
2055
+ catch {
2056
+ return null;
2057
+ }
2058
+ };
2059
+ // Helper to read logs from a log file (for externally-spawned workers)
2060
+ const readLogsFromFile = (logFile, limit = 5000) => {
2061
+ if (!fs.existsSync(logFile))
2062
+ return [];
2063
+ try {
2064
+ const content = fs.readFileSync(logFile, 'utf-8');
2065
+ const lines = content.split('\n');
2066
+ // Return last `limit` lines
2067
+ return lines.slice(-limit);
2068
+ }
2069
+ catch {
2070
+ return [];
2071
+ }
2072
+ };
2073
+ // Helper to start watching a log file for live updates
2074
+ const watchLogFile = (agentName, logFile) => {
2075
+ if (fileWatchers.has(agentName))
2076
+ return; // Already watching
2077
+ if (!fs.existsSync(logFile))
2078
+ return;
2079
+ try {
2080
+ // Track current file size for incremental reads
2081
+ const stats = fs.statSync(logFile);
2082
+ fileLastSize.set(agentName, stats.size);
2083
+ const watcher = fs.watch(logFile, (eventType) => {
2084
+ if (eventType !== 'change')
2085
+ return;
2086
+ const clients = logSubscriptions.get(agentName);
2087
+ if (!clients || clients.size === 0) {
2088
+ // No subscribers, stop watching
2089
+ watcher.close();
2090
+ fileWatchers.delete(agentName);
2091
+ fileLastSize.delete(agentName);
2092
+ return;
2093
+ }
2094
+ try {
2095
+ const newStats = fs.statSync(logFile);
2096
+ const lastSize = fileLastSize.get(agentName) || 0;
2097
+ if (newStats.size > lastSize) {
2098
+ // Read only the new content
2099
+ const fd = fs.openSync(logFile, 'r');
2100
+ const buffer = Buffer.alloc(newStats.size - lastSize);
2101
+ fs.readSync(fd, buffer, 0, buffer.length, lastSize);
2102
+ fs.closeSync(fd);
2103
+ const newContent = buffer.toString('utf-8');
2104
+ fileLastSize.set(agentName, newStats.size);
2105
+ // Broadcast to subscribed clients
2106
+ const payload = JSON.stringify({
2107
+ type: 'output',
2108
+ agent: agentName,
2109
+ data: newContent,
2110
+ timestamp: new Date().toISOString(),
2111
+ });
2112
+ for (const client of clients) {
2113
+ if (client.readyState === WebSocket.OPEN) {
2114
+ client.send(payload);
2115
+ }
2116
+ }
2117
+ }
2118
+ }
2119
+ catch (err) {
2120
+ console.error(`[dashboard] Error reading log file updates for ${agentName}:`, err);
2121
+ }
2122
+ });
2123
+ fileWatchers.set(agentName, watcher);
2124
+ }
2125
+ catch (err) {
2126
+ console.error(`[dashboard] Failed to watch log file for ${agentName}:`, err);
2127
+ }
2128
+ };
2042
2129
  // Helper to subscribe to an agent (async to handle spawn timing)
2043
2130
  const subscribeToAgent = async (agentName) => {
2044
2131
  let isSpawned = spawner?.hasWorker(agentName) ?? false;
@@ -2093,12 +2180,27 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2093
2180
  }));
2094
2181
  }
2095
2182
  else {
2096
- // For daemon-connected agents, explain that PTY output isn't available
2097
- ws.send(JSON.stringify({
2098
- type: 'history',
2099
- agent: agentName,
2100
- lines: [`[${agentName} is a daemon-connected agent - PTY output not available. Showing relay messages only.]`],
2101
- }));
2183
+ // Check if this is an externally-spawned worker with a log file
2184
+ const externalWorker = getExternalWorkerInfo(agentName);
2185
+ if (externalWorker?.logFile && fs.existsSync(externalWorker.logFile)) {
2186
+ // Read logs from the external worker's log file
2187
+ const lines = readLogsFromFile(externalWorker.logFile, 5000);
2188
+ ws.send(JSON.stringify({
2189
+ type: 'history',
2190
+ agent: agentName,
2191
+ lines,
2192
+ }));
2193
+ // Start watching the log file for live updates
2194
+ watchLogFile(agentName, externalWorker.logFile);
2195
+ }
2196
+ else {
2197
+ // For daemon-connected agents without log files, explain that PTY output isn't available
2198
+ ws.send(JSON.stringify({
2199
+ type: 'history',
2200
+ agent: agentName,
2201
+ lines: [`[${agentName} is a daemon-connected agent - PTY output not available. Showing relay messages only.]`],
2202
+ }));
2203
+ }
2102
2204
  }
2103
2205
  ws.send(JSON.stringify({
2104
2206
  type: 'subscribed',
@@ -2129,6 +2231,16 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2129
2231
  const agentName = msg.unsubscribe;
2130
2232
  clientSubscriptions.delete(agentName);
2131
2233
  logSubscriptions.get(agentName)?.delete(ws);
2234
+ // Clean up file watcher if no more subscribers
2235
+ const remainingClients = logSubscriptions.get(agentName);
2236
+ if (!remainingClients || remainingClients.size === 0) {
2237
+ const watcher = fileWatchers.get(agentName);
2238
+ if (watcher) {
2239
+ watcher.close();
2240
+ fileWatchers.delete(agentName);
2241
+ fileLastSize.delete(agentName);
2242
+ }
2243
+ }
2132
2244
  console.log(`[dashboard] Client unsubscribed from logs for: ${agentName}`);
2133
2245
  ws.send(JSON.stringify({
2134
2246
  type: 'unsubscribed',
@@ -2174,6 +2286,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2174
2286
  // Clean up subscriptions on disconnect
2175
2287
  for (const agentName of clientSubscriptions) {
2176
2288
  logSubscriptions.get(agentName)?.delete(ws);
2289
+ // Clean up file watchers if no more subscribers
2290
+ const remainingClients = logSubscriptions.get(agentName);
2291
+ if (!remainingClients || remainingClients.size === 0) {
2292
+ const watcher = fileWatchers.get(agentName);
2293
+ if (watcher) {
2294
+ watcher.close();
2295
+ fileWatchers.delete(agentName);
2296
+ fileLastSize.delete(agentName);
2297
+ console.log(`[dashboard] Stopped watching log file for: ${agentName}`);
2298
+ }
2299
+ }
2177
2300
  }
2178
2301
  const reasonStr = reason?.toString() || 'no reason';
2179
2302
  console.log(`[dashboard] Logs WebSocket client disconnected (code: ${code}, reason: ${reasonStr})`);