@agent-relay/dashboard-server 2.0.69 → 2.0.71

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 (59) hide show
  1. package/dist/server.js +47 -39
  2. package/dist/server.js.map +1 -1
  3. package/out/404.html +1 -1
  4. package/out/_next/static/chunks/{285-271fc707e03bb4c5.js → 285-52fb0aee5b6b90a6.js} +1 -1
  5. package/out/about.html +1 -1
  6. package/out/about.txt +1 -1
  7. package/out/app/onboarding.html +1 -1
  8. package/out/app/onboarding.txt +1 -1
  9. package/out/app.html +1 -1
  10. package/out/app.txt +2 -2
  11. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
  12. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  13. package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
  14. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  15. package/out/blog.html +1 -1
  16. package/out/blog.txt +1 -1
  17. package/out/careers.html +1 -1
  18. package/out/careers.txt +1 -1
  19. package/out/changelog.html +1 -1
  20. package/out/changelog.txt +1 -1
  21. package/out/cloud/link.html +1 -1
  22. package/out/cloud/link.txt +1 -1
  23. package/out/complete-profile.html +1 -1
  24. package/out/complete-profile.txt +1 -1
  25. package/out/connect-repos.html +1 -1
  26. package/out/connect-repos.txt +1 -1
  27. package/out/contact.html +1 -1
  28. package/out/contact.txt +1 -1
  29. package/out/docs.html +1 -1
  30. package/out/docs.txt +1 -1
  31. package/out/history.html +1 -1
  32. package/out/history.txt +1 -1
  33. package/out/index.html +1 -1
  34. package/out/index.txt +2 -2
  35. package/out/login.html +1 -1
  36. package/out/login.txt +1 -1
  37. package/out/metrics.html +1 -1
  38. package/out/metrics.txt +1 -1
  39. package/out/pricing.html +1 -1
  40. package/out/pricing.txt +1 -1
  41. package/out/privacy.html +1 -1
  42. package/out/privacy.txt +1 -1
  43. package/out/providers/setup/claude.html +1 -1
  44. package/out/providers/setup/claude.txt +1 -1
  45. package/out/providers/setup/codex.html +1 -1
  46. package/out/providers/setup/codex.txt +1 -1
  47. package/out/providers/setup/cursor.html +1 -1
  48. package/out/providers/setup/cursor.txt +1 -1
  49. package/out/providers.html +1 -1
  50. package/out/providers.txt +1 -1
  51. package/out/security.html +1 -1
  52. package/out/security.txt +1 -1
  53. package/out/signup.html +1 -1
  54. package/out/signup.txt +1 -1
  55. package/out/terms.html +1 -1
  56. package/out/terms.txt +1 -1
  57. package/package.json +1 -1
  58. /package/out/_next/static/{ksklx6dOwf9cjVY6ez8ZH → 4b3iV9SX2_ON0klS0h-1g}/_buildManifest.js +0 -0
  59. /package/out/_next/static/{ksklx6dOwf9cjVY6ez8ZH → 4b3iV9SX2_ON0klS0h-1g}/_ssgManifest.js +0 -0
package/dist/server.js CHANGED
@@ -989,6 +989,8 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
989
989
  };
990
990
  // Set up direct message handler to forward messages to presence WebSocket
991
991
  // This enables agents to send replies that appear in the dashboard UI
992
+ // Note: the relay daemon already persists messages authoritatively, so we
993
+ // only need to broadcast the real-time event here (no storage.saveMessage).
992
994
  client.onMessage = (from, payload, messageId) => {
993
995
  const body = typeof payload === 'object' && payload !== null && 'body' in payload
994
996
  ? payload.body
@@ -1000,28 +1002,9 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1000
1002
  // Determine entity type: user if they have presence state, agent otherwise
1001
1003
  const fromEntityType = senderPresence ? 'user' : 'agent';
1002
1004
  const timestamp = new Date().toISOString();
1003
- // Persist the message to storage so it survives page refresh
1004
- if (storage) {
1005
- storage.saveMessage({
1006
- id: messageId || `dm-${crypto.randomUUID()}`,
1007
- ts: Date.now(),
1008
- from,
1009
- to: senderName,
1010
- topic: undefined,
1011
- kind: 'message',
1012
- body,
1013
- data: {
1014
- fromAvatarUrl,
1015
- fromEntityType,
1016
- },
1017
- status: 'unread',
1018
- is_urgent: false,
1019
- is_broadcast: false,
1020
- }).catch((err) => {
1021
- console.error('[dashboard] Failed to persist direct message', err);
1022
- });
1023
- }
1024
- // Broadcast to presence WebSocket clients so cloud/dashboard can display the message
1005
+ // Broadcast real-time event so the dashboard UI updates immediately.
1006
+ // Pass id (= messageId) so the client has a stable identifier and
1007
+ // doesn't need to fabricate one from Date.now().
1025
1008
  broadcastDirectMessage({
1026
1009
  type: 'direct_message',
1027
1010
  targetUser: senderName,
@@ -1029,6 +1012,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1029
1012
  fromAvatarUrl,
1030
1013
  fromEntityType,
1031
1014
  body,
1015
+ id: messageId,
1032
1016
  messageId,
1033
1017
  timestamp,
1034
1018
  });
@@ -1756,15 +1740,17 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1756
1740
  existing.avatarUrl = user.avatarUrl;
1757
1741
  }
1758
1742
  else {
1759
- const now = new Date().toISOString();
1743
+ // Use stable timestamps from the user/file data, not new Date(),
1744
+ // so getAllData() produces deterministic output for dedup comparison
1745
+ const stableTimestamp = user.lastSeen || user.connectedAt || new Date(remoteData.updatedAt).toISOString();
1760
1746
  agentsMap.set(user.name, {
1761
1747
  name: user.name,
1762
1748
  role: 'User',
1763
1749
  cli: 'dashboard',
1764
1750
  messageCount: 0,
1765
1751
  status: 'online',
1766
- lastSeen: now,
1767
- lastActive: now,
1752
+ lastSeen: stableTimestamp,
1753
+ lastActive: stableTimestamp,
1768
1754
  needsAttention: false,
1769
1755
  avatarUrl: user.avatarUrl,
1770
1756
  });
@@ -1954,18 +1940,21 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1954
1940
  return true;
1955
1941
  });
1956
1942
  // Separate AI agents from human users
1943
+ // Sort by name for deterministic JSON serialization (enables dedup comparison)
1957
1944
  const filteredAgents = validEntries
1958
1945
  .filter(agent => agent.cli !== 'dashboard')
1959
1946
  .map(agent => ({
1960
1947
  ...agent,
1961
1948
  isHuman: false,
1962
- }));
1949
+ }))
1950
+ .sort((a, b) => a.name.localeCompare(b.name));
1963
1951
  const humanUsers = validEntries
1964
1952
  .filter(agent => agent.cli === 'dashboard')
1965
1953
  .map(agent => ({
1966
1954
  ...agent,
1967
1955
  isHuman: true,
1968
- }));
1956
+ }))
1957
+ .sort((a, b) => a.name.localeCompare(b.name));
1969
1958
  return {
1970
1959
  agents: filteredAgents,
1971
1960
  users: humanUsers,
@@ -1978,6 +1967,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1978
1967
  // Track clients that are still initializing (haven't received first data yet)
1979
1968
  // This prevents race conditions where broadcastData sends before initial data is sent
1980
1969
  const initializingClients = new WeakSet();
1970
+ let lastBroadcastPayload = '';
1981
1971
  const broadcastData = async () => {
1982
1972
  try {
1983
1973
  const data = await getAllData();
@@ -1987,6 +1977,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
1987
1977
  console.warn('[dashboard] Skipping broadcast - empty payload');
1988
1978
  return;
1989
1979
  }
1980
+ // Skip broadcast if data hasn't changed since last send
1981
+ if (rawPayload === lastBroadcastPayload) {
1982
+ return;
1983
+ }
1984
+ lastBroadcastPayload = rawPayload;
1990
1985
  // Push into buffer and wrap with sequence ID for replay support
1991
1986
  const seq = mainMessageBuffer.push('data', rawPayload);
1992
1987
  const payload = JSON.stringify({ seq, ...data });
@@ -2066,6 +2061,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2066
2061
  }
2067
2062
  return { projects: [], messages: [], connected: false };
2068
2063
  };
2064
+ let lastBridgeBroadcastPayload = '';
2069
2065
  const broadcastBridgeData = async () => {
2070
2066
  try {
2071
2067
  const data = await getBridgeData();
@@ -2075,6 +2071,11 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2075
2071
  console.warn('[dashboard] Skipping bridge broadcast - empty payload');
2076
2072
  return;
2077
2073
  }
2074
+ // Skip broadcast if data hasn't changed since last send
2075
+ if (payload === lastBridgeBroadcastPayload) {
2076
+ return;
2077
+ }
2078
+ lastBridgeBroadcastPayload = payload;
2078
2079
  wssBridge.clients.forEach(client => {
2079
2080
  if (client.readyState === WebSocket.OPEN) {
2080
2081
  try {
@@ -2633,7 +2634,6 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2633
2634
  };
2634
2635
  // Helper to broadcast direct messages to all connected clients
2635
2636
  // This enables agent replies to appear in the dashboard UI
2636
- // Broadcasts to both main wss (local mode) and wssPresence (cloud mode)
2637
2637
  const broadcastDirectMessage = (message) => {
2638
2638
  // Push into buffer and wrap with sequence ID for replay support
2639
2639
  const rawPayload = JSON.stringify(message);
@@ -2647,15 +2647,20 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
2647
2647
  client.send(payload);
2648
2648
  }
2649
2649
  });
2650
- // Also broadcast to presence WebSocket clients (cloud mode)
2651
- const presenceClients = Array.from(wssPresence.clients).filter(c => c.readyState === WebSocket.OPEN);
2652
- if (presenceClients.length > 0) {
2653
- debug(`[dashboard] Broadcasting direct_message to ${presenceClients.length} presence clients`);
2654
- wssPresence.clients.forEach((client) => {
2655
- if (client.readyState === WebSocket.OPEN) {
2656
- client.send(payload);
2657
- }
2658
- });
2650
+ // Only broadcast to presence WS if UserBridge does NOT have a session for
2651
+ // the target user. When UserBridge is active it already delivers the DM
2652
+ // directly to the user's WebSocket(s), so broadcasting here would duplicate.
2653
+ const targetHandledByBridge = userBridge?.isUserRegistered(message.targetUser) ?? false;
2654
+ if (!targetHandledByBridge) {
2655
+ const presenceClients = Array.from(wssPresence.clients).filter(c => c.readyState === WebSocket.OPEN);
2656
+ if (presenceClients.length > 0) {
2657
+ debug(`[dashboard] Broadcasting direct_message to ${presenceClients.length} presence clients (no bridge session)`);
2658
+ wssPresence.clients.forEach((client) => {
2659
+ if (client.readyState === WebSocket.OPEN) {
2660
+ client.send(payload);
2661
+ }
2662
+ });
2663
+ }
2659
2664
  }
2660
2665
  };
2661
2666
  // Helper to get online users list (without ws references)
@@ -5816,12 +5821,15 @@ Start by greeting the project leads and asking for status updates.`;
5816
5821
  }
5817
5822
  return {};
5818
5823
  }
5819
- // Watch for changes
5824
+ // Watch for changes - poll as a safety net for DB-backed storage mode.
5825
+ // Real-time updates are already handled by explicit broadcastData() calls
5826
+ // at every data mutation point (message send, spawn, release, cwd update, etc.).
5827
+ // This interval only catches external/indirect changes (presence, DB edits).
5820
5828
  if (storage) {
5821
5829
  setInterval(() => {
5822
5830
  broadcastData().catch((err) => console.error('Broadcast failed', err));
5823
5831
  broadcastBridgeData().catch((err) => console.error('Bridge broadcast failed', err));
5824
- }, 1000);
5832
+ }, 5000);
5825
5833
  }
5826
5834
  else {
5827
5835
  let fsWait = null;