agent-relay 2.0.4 → 2.0.6

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 (131) hide show
  1. package/dist/dashboard/out/404.html +1 -1
  2. package/dist/dashboard/out/app/onboarding.html +1 -1
  3. package/dist/dashboard/out/app/onboarding.txt +1 -1
  4. package/dist/dashboard/out/app.html +1 -1
  5. package/dist/dashboard/out/app.txt +1 -1
  6. package/dist/dashboard/out/cloud/link.html +1 -1
  7. package/dist/dashboard/out/cloud/link.txt +1 -1
  8. package/dist/dashboard/out/connect-repos.html +1 -1
  9. package/dist/dashboard/out/connect-repos.txt +1 -1
  10. package/dist/dashboard/out/history.html +1 -1
  11. package/dist/dashboard/out/history.txt +1 -1
  12. package/dist/dashboard/out/index.html +1 -1
  13. package/dist/dashboard/out/index.txt +1 -1
  14. package/dist/dashboard/out/login.html +1 -1
  15. package/dist/dashboard/out/login.txt +1 -1
  16. package/dist/dashboard/out/metrics.html +1 -1
  17. package/dist/dashboard/out/metrics.txt +1 -1
  18. package/dist/dashboard/out/pricing.html +1 -1
  19. package/dist/dashboard/out/pricing.txt +1 -1
  20. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  21. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  22. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  23. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  24. package/dist/dashboard/out/providers/setup/cursor.html +1 -1
  25. package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
  26. package/dist/dashboard/out/providers.html +1 -1
  27. package/dist/dashboard/out/providers.txt +1 -1
  28. package/dist/dashboard/out/signup.html +1 -1
  29. package/dist/dashboard/out/signup.txt +1 -1
  30. package/dist/src/cli/index.js +61 -3
  31. package/package.json +14 -12
  32. package/packages/api-types/package.json +1 -1
  33. package/packages/bridge/dist/spawner.d.ts +2 -0
  34. package/packages/bridge/dist/spawner.js +24 -6
  35. package/packages/bridge/dist/types.d.ts +2 -0
  36. package/packages/bridge/package.json +7 -7
  37. package/packages/cloud/package.json +6 -6
  38. package/packages/config/package.json +2 -2
  39. package/packages/continuity/package.json +1 -1
  40. package/packages/daemon/dist/router.d.ts +2 -1
  41. package/packages/daemon/dist/router.js +142 -52
  42. package/packages/daemon/dist/server.d.ts +3 -0
  43. package/packages/daemon/dist/server.js +31 -1
  44. package/packages/daemon/dist/spawn-manager.d.ts +5 -0
  45. package/packages/daemon/dist/spawn-manager.js +44 -1
  46. package/packages/daemon/package.json +12 -11
  47. package/packages/dashboard/dist/server.js +2 -0
  48. package/packages/dashboard/package.json +12 -12
  49. package/packages/dashboard/ui-dist/404.html +1 -1
  50. package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
  51. package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
  52. package/packages/dashboard/ui-dist/app.html +1 -1
  53. package/packages/dashboard/ui-dist/app.txt +1 -1
  54. package/packages/dashboard/ui-dist/cloud/link.html +1 -1
  55. package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
  56. package/packages/dashboard/ui-dist/connect-repos.html +1 -1
  57. package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
  58. package/packages/dashboard/ui-dist/history.html +1 -1
  59. package/packages/dashboard/ui-dist/history.txt +1 -1
  60. package/packages/dashboard/ui-dist/index.html +1 -1
  61. package/packages/dashboard/ui-dist/index.txt +1 -1
  62. package/packages/dashboard/ui-dist/login.html +1 -1
  63. package/packages/dashboard/ui-dist/login.txt +1 -1
  64. package/packages/dashboard/ui-dist/metrics.html +1 -1
  65. package/packages/dashboard/ui-dist/metrics.txt +1 -1
  66. package/packages/dashboard/ui-dist/pricing.html +1 -1
  67. package/packages/dashboard/ui-dist/pricing.txt +1 -1
  68. package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
  69. package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
  70. package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
  71. package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
  72. package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
  73. package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
  74. package/packages/dashboard/ui-dist/providers.html +1 -1
  75. package/packages/dashboard/ui-dist/providers.txt +1 -1
  76. package/packages/dashboard/ui-dist/signup.html +1 -1
  77. package/packages/dashboard/ui-dist/signup.txt +1 -1
  78. package/packages/dashboard-server/dist/server.js +2 -0
  79. package/packages/dashboard-server/dist/user-bridge.d.ts +7 -3
  80. package/packages/dashboard-server/dist/user-bridge.js +48 -30
  81. package/packages/dashboard-server/package.json +12 -12
  82. package/packages/hooks/package.json +4 -4
  83. package/packages/mcp/README.md +19 -135
  84. package/packages/mcp/dist/client.js +67 -27
  85. package/packages/mcp/dist/cloud.js +1 -18
  86. package/packages/mcp/dist/prompts/protocol.d.ts +1 -1
  87. package/packages/mcp/dist/prompts/protocol.js +6 -14
  88. package/packages/mcp/package.json +2 -1
  89. package/packages/memory/package.json +2 -2
  90. package/packages/policy/package.json +2 -2
  91. package/packages/protocol/dist/types.d.ts +2 -0
  92. package/packages/protocol/package.json +1 -1
  93. package/packages/resiliency/package.json +1 -1
  94. package/packages/sdk/README.md +43 -160
  95. package/packages/sdk/dist/client.d.ts +3 -99
  96. package/packages/sdk/dist/client.js +6 -113
  97. package/packages/sdk/dist/index.d.ts +0 -1
  98. package/packages/sdk/dist/index.js +0 -2
  99. package/packages/sdk/dist/standalone.js +0 -1
  100. package/packages/sdk/package.json +2 -2
  101. package/packages/spawner/package.json +1 -1
  102. package/packages/state/package.json +1 -1
  103. package/packages/storage/package.json +2 -2
  104. package/packages/telemetry/dist/client.d.ts +19 -0
  105. package/packages/telemetry/dist/client.js +126 -0
  106. package/packages/telemetry/dist/config.d.ts +29 -0
  107. package/packages/telemetry/dist/config.js +88 -0
  108. package/packages/telemetry/dist/events.d.ts +106 -0
  109. package/packages/telemetry/dist/events.js +10 -0
  110. package/packages/telemetry/dist/index.d.ts +8 -0
  111. package/packages/telemetry/dist/index.js +7 -0
  112. package/packages/telemetry/dist/machine-id.d.ts +12 -0
  113. package/packages/telemetry/dist/machine-id.js +58 -0
  114. package/packages/telemetry/dist/posthog-config.d.ts +16 -0
  115. package/packages/telemetry/dist/posthog-config.js +33 -0
  116. package/packages/telemetry/package.json +41 -0
  117. package/packages/trajectory/package.json +2 -2
  118. package/packages/user-directory/package.json +2 -2
  119. package/packages/utils/package.json +1 -1
  120. package/packages/wrapper/dist/relay-pty-orchestrator.js +38 -29
  121. package/packages/wrapper/package.json +6 -6
  122. package/packages/sdk/dist/discovery.d.ts +0 -29
  123. package/packages/sdk/dist/discovery.js +0 -126
  124. /package/dist/dashboard/out/_next/static/{72btMIJ64BCAB4UgVkpaq → lIJs7zSKBaI58kpqegulQ}/_buildManifest.js +0 -0
  125. /package/dist/dashboard/out/_next/static/{72btMIJ64BCAB4UgVkpaq → lIJs7zSKBaI58kpqegulQ}/_ssgManifest.js +0 -0
  126. /package/packages/dashboard/ui-dist/_next/static/{0AsOfRemPXJmtynCKT-rx → KIxE0Ds_zdGuDJDQu7_sb}/_buildManifest.js +0 -0
  127. /package/packages/dashboard/ui-dist/_next/static/{0AsOfRemPXJmtynCKT-rx → KIxE0Ds_zdGuDJDQu7_sb}/_ssgManifest.js +0 -0
  128. /package/packages/dashboard/ui-dist/_next/static/{72btMIJ64BCAB4UgVkpaq → SoK46dEi3IsNBVWXD9x0L}/_buildManifest.js +0 -0
  129. /package/packages/dashboard/ui-dist/_next/static/{72btMIJ64BCAB4UgVkpaq → SoK46dEi3IsNBVWXD9x0L}/_ssgManifest.js +0 -0
  130. /package/packages/dashboard/ui-dist/_next/static/{clUN2n0bz9HCjKI0qlOxU → lIJs7zSKBaI58kpqegulQ}/_buildManifest.js +0 -0
  131. /package/packages/dashboard/ui-dist/_next/static/{clUN2n0bz9HCjKI0qlOxU → lIJs7zSKBaI58kpqegulQ}/_ssgManifest.js +0 -0
@@ -24,7 +24,7 @@ export class Router {
24
24
  primaryByShadow = new Map();
25
25
  /** Channel membership: channel -> Set of member names */
26
26
  channels = new Map();
27
- /** User entities (human users, not agents) */
27
+ /** User entities (human users, not agents) - supports multiple connections per user (multi-tab) */
28
28
  users = new Map();
29
29
  /** Reverse lookup: member name -> Set of channels they're in */
30
30
  memberChannels = new Map();
@@ -158,14 +158,13 @@ export class Router {
158
158
  if (connection.agentName) {
159
159
  const isUser = connection.entityType === 'user';
160
160
  if (isUser) {
161
- // Handle existing user connection with same name (disconnect old)
162
- const existingUser = this.users.get(connection.agentName);
163
- if (existingUser && existingUser.id !== connection.id) {
164
- existingUser.close();
165
- this.connections.delete(existingUser.id);
161
+ // Users can have multiple connections (e.g., multiple browser tabs)
162
+ // Track ALL connections per user so messages reach all tabs
163
+ if (!this.users.has(connection.agentName)) {
164
+ this.users.set(connection.agentName, new Set());
166
165
  }
167
- this.users.set(connection.agentName, connection);
168
- routerLog.info(`User registered: ${connection.agentName}`);
166
+ this.users.get(connection.agentName).add(connection);
167
+ routerLog.info(`User registered: ${connection.agentName} (${this.users.get(connection.agentName).size} connections)`);
169
168
  }
170
169
  else {
171
170
  // Handle existing agent connection with same name (disconnect old)
@@ -202,10 +201,17 @@ export class Router {
202
201
  const isUser = connection.entityType === 'user';
203
202
  let wasCurrentConnection = false;
204
203
  if (isUser) {
205
- const currentUser = this.users.get(connection.agentName);
206
- if (currentUser?.id === connection.id) {
207
- this.users.delete(connection.agentName);
208
- wasCurrentConnection = true;
204
+ const userConnections = this.users.get(connection.agentName);
205
+ if (userConnections) {
206
+ userConnections.delete(connection);
207
+ if (userConnections.size === 0) {
208
+ this.users.delete(connection.agentName);
209
+ wasCurrentConnection = true;
210
+ routerLog.info(`User fully unregistered: ${connection.agentName} (all connections closed)`);
211
+ }
212
+ else {
213
+ routerLog.debug(`User connection closed: ${connection.agentName} (${userConnections.size} connections remaining)`);
214
+ }
209
215
  }
210
216
  }
211
217
  else {
@@ -496,10 +502,12 @@ export class Router {
496
502
  * This prevents silent message drops during brief disconnections or spawn timing issues.
497
503
  */
498
504
  sendDirect(from, to, envelope) {
499
- const target = this.agents.get(to) ?? this.users.get(to);
500
- const isUserTarget = target?.entityType === 'user';
501
- // If agent not found locally, check if it's on a remote machine
502
- if (!target) {
505
+ // Check for agent first, then user connections
506
+ const agentTarget = this.agents.get(to);
507
+ const userConnections = this.users.get(to);
508
+ const hasTarget = agentTarget || (userConnections && userConnections.size > 0);
509
+ // If target not found locally, check remote
510
+ if (!hasTarget) {
503
511
  const remoteAgent = this.crossMachineHandler?.isRemoteAgent(to);
504
512
  if (remoteAgent) {
505
513
  routerLog.info(`Routing to remote agent: ${to}`, { daemonName: remoteAgent.daemonName });
@@ -533,6 +541,33 @@ export class Router {
533
541
  routerLog.warn(`Target "${to}" not found and unknown`, { availableAgents: Array.from(this.agents.keys()), spawningAgents: Array.from(this.spawningAgents.keys()) });
534
542
  return false;
535
543
  }
544
+ // For user targets, send to ALL connections (multi-tab support)
545
+ if (userConnections && userConnections.size > 0) {
546
+ let anySent = false;
547
+ for (const userConn of userConnections) {
548
+ const deliver = this.createDeliverEnvelope(from, to, envelope, userConn);
549
+ const sent = userConn.send(deliver);
550
+ if (sent)
551
+ anySent = true;
552
+ routerLog.debug(`Delivered ${from} -> ${to} (user connection ${userConn.id})`, {
553
+ success: sent,
554
+ preview: envelope.payload.body?.substring(0, 40),
555
+ });
556
+ // Persist only once (for the first connection)
557
+ if (userConn === [...userConnections][0]) {
558
+ this.persistDeliverEnvelope(deliver);
559
+ }
560
+ if (sent) {
561
+ this.trackDelivery(userConn, deliver);
562
+ }
563
+ }
564
+ if (anySent) {
565
+ this.registry?.recordReceive(to);
566
+ }
567
+ return anySent;
568
+ }
569
+ // For agent targets, send to single connection
570
+ const target = agentTarget;
536
571
  const deliver = this.createDeliverEnvelope(from, to, envelope, target);
537
572
  const sent = target.send(deliver);
538
573
  routerLog.debug(`Delivered ${from} -> ${to}`, { success: sent, preview: envelope.payload.body?.substring(0, 40) });
@@ -540,10 +575,7 @@ export class Router {
540
575
  if (sent) {
541
576
  this.trackDelivery(target, deliver);
542
577
  this.registry?.recordReceive(to);
543
- // Only mark AI agents as processing; humans don't need processing indicators
544
- if (!isUserTarget) {
545
- this.setProcessing(to, deliver.id);
546
- }
578
+ this.setProcessing(to, deliver.id);
547
579
  }
548
580
  return sent;
549
581
  }
@@ -606,19 +638,33 @@ export class Router {
606
638
  for (const recipientName of recipients) {
607
639
  if (recipientName === from)
608
640
  continue; // Don't send to self
609
- // Check both agents and users maps (consistent with sendDirect)
610
- const target = this.agents.get(recipientName) ?? this.users.get(recipientName);
611
- if (target) {
612
- const isUserTarget = target.entityType === 'user';
613
- const deliver = this.createDeliverEnvelope(from, recipientName, envelope, target);
614
- const sent = target.send(deliver);
641
+ // Check agents first
642
+ const agentTarget = this.agents.get(recipientName);
643
+ if (agentTarget) {
644
+ const deliver = this.createDeliverEnvelope(from, recipientName, envelope, agentTarget);
645
+ const sent = agentTarget.send(deliver);
615
646
  this.persistDeliverEnvelope(deliver, true); // Mark as broadcast
616
647
  if (sent) {
617
- this.trackDelivery(target, deliver);
648
+ this.trackDelivery(agentTarget, deliver);
618
649
  this.registry?.recordReceive(recipientName);
619
- // Only mark AI agents as processing; humans don't need processing indicators
620
- if (!isUserTarget) {
621
- this.setProcessing(recipientName, deliver.id);
650
+ this.setProcessing(recipientName, deliver.id);
651
+ }
652
+ continue;
653
+ }
654
+ // Check user connections (send to all connections for multi-tab)
655
+ const userConnections = this.users.get(recipientName);
656
+ if (userConnections && userConnections.size > 0) {
657
+ let persisted = false;
658
+ for (const userConn of userConnections) {
659
+ const deliver = this.createDeliverEnvelope(from, recipientName, envelope, userConn);
660
+ const sent = userConn.send(deliver);
661
+ if (!persisted) {
662
+ this.persistDeliverEnvelope(deliver, true); // Mark as broadcast, persist once
663
+ persisted = true;
664
+ }
665
+ if (sent) {
666
+ this.trackDelivery(userConn, deliver);
667
+ this.registry?.recordReceive(recipientName);
622
668
  }
623
669
  }
624
670
  }
@@ -629,16 +675,24 @@ export class Router {
629
675
  * Used for system notifications that only humans should see.
630
676
  */
631
677
  broadcastToUsers(from, envelope) {
632
- for (const [userName, target] of this.users) {
678
+ for (const [userName, userConnections] of this.users) {
633
679
  if (userName === from)
634
680
  continue; // Don't send to self
635
- const deliver = this.createDeliverEnvelope(from, userName, envelope, target);
636
- const sent = target.send(deliver);
637
- this.persistDeliverEnvelope(deliver, true); // Mark as broadcast
638
- if (sent) {
639
- this.trackDelivery(target, deliver);
640
- this.registry?.recordReceive(userName);
641
- routerLog.debug(`Broadcast to user ${userName}`);
681
+ if (userConnections.size === 0)
682
+ continue;
683
+ let persisted = false;
684
+ for (const userConn of userConnections) {
685
+ const deliver = this.createDeliverEnvelope(from, userName, envelope, userConn);
686
+ const sent = userConn.send(deliver);
687
+ if (!persisted) {
688
+ this.persistDeliverEnvelope(deliver, true); // Mark as broadcast, persist once
689
+ persisted = true;
690
+ }
691
+ if (sent) {
692
+ this.trackDelivery(userConn, deliver);
693
+ this.registry?.recordReceive(userName);
694
+ routerLog.debug(`Broadcast to user ${userName} (connection ${userConn.id})`);
695
+ }
642
696
  }
643
697
  }
644
698
  }
@@ -1104,8 +1158,9 @@ export class Router {
1104
1158
  if (this.namesMatch(memberName, senderName)) {
1105
1159
  continue;
1106
1160
  }
1107
- const memberConn = this.getConnectionByName(memberName);
1108
- if (memberConn) {
1161
+ // Check for agent connection first
1162
+ const agentConn = this.agents.get(memberName);
1163
+ if (agentConn) {
1109
1164
  const deliverEnvelope = {
1110
1165
  v: PROTOCOL_VERSION,
1111
1166
  type: 'CHANNEL_MESSAGE',
@@ -1114,20 +1169,48 @@ export class Router {
1114
1169
  from: senderName,
1115
1170
  payload: envelope.payload,
1116
1171
  };
1117
- const sent = memberConn.send(deliverEnvelope);
1172
+ const sent = agentConn.send(deliverEnvelope);
1118
1173
  if (sent) {
1119
1174
  deliveredCount++;
1120
- routerLog.info(`Delivered to ${memberName} (${memberConn.entityType || 'agent'})`);
1175
+ routerLog.info(`Delivered to ${memberName} (agent)`);
1121
1176
  }
1122
1177
  else {
1123
1178
  routerLog.warn(`Failed to send to ${memberName}`);
1124
1179
  undeliveredMembers.push(memberName);
1125
1180
  }
1181
+ continue;
1126
1182
  }
1127
- else {
1128
- routerLog.warn(`Member ${memberName} is registered in channel but NOT connected to daemon - message not delivered`);
1129
- undeliveredMembers.push(memberName);
1183
+ // Check for user connections (send to ALL for multi-tab support)
1184
+ const userConnections = this.users.get(memberName);
1185
+ if (userConnections && userConnections.size > 0) {
1186
+ let anyDelivered = false;
1187
+ for (const userConn of userConnections) {
1188
+ const deliverEnvelope = {
1189
+ v: PROTOCOL_VERSION,
1190
+ type: 'CHANNEL_MESSAGE',
1191
+ id: generateId(),
1192
+ ts: Date.now(),
1193
+ from: senderName,
1194
+ payload: envelope.payload,
1195
+ };
1196
+ const sent = userConn.send(deliverEnvelope);
1197
+ if (sent) {
1198
+ anyDelivered = true;
1199
+ routerLog.info(`Delivered to ${memberName} (user connection ${userConn.id})`);
1200
+ }
1201
+ }
1202
+ if (anyDelivered) {
1203
+ deliveredCount++;
1204
+ }
1205
+ else {
1206
+ routerLog.warn(`Failed to send to any connection for user ${memberName}`);
1207
+ undeliveredMembers.push(memberName);
1208
+ }
1209
+ continue;
1130
1210
  }
1211
+ // Member not connected
1212
+ routerLog.warn(`Member ${memberName} is registered in channel but NOT connected to daemon - message not delivered`);
1213
+ undeliveredMembers.push(memberName);
1131
1214
  }
1132
1215
  // Persist channel message
1133
1216
  this.persistChannelMessage(envelope, senderName);
@@ -1246,21 +1329,28 @@ export class Router {
1246
1329
  /**
1247
1330
  * Get a connection by name (checks both agents and users).
1248
1331
  * Uses case-insensitive lookup to handle mismatched casing.
1332
+ * For users with multiple connections, returns the first connection.
1249
1333
  */
1250
1334
  getConnectionByName(name) {
1251
- // Try exact match first
1252
- const exact = this.agents.get(name) ?? this.users.get(name);
1253
- if (exact)
1254
- return exact;
1335
+ // Try exact match for agent first
1336
+ const agentExact = this.agents.get(name);
1337
+ if (agentExact)
1338
+ return agentExact;
1339
+ // Try exact match for user (get first connection from set)
1340
+ const userConnections = this.users.get(name);
1341
+ if (userConnections && userConnections.size > 0) {
1342
+ return [...userConnections][0];
1343
+ }
1255
1344
  // Fall back to case-insensitive search
1256
1345
  const lowerName = name.toLowerCase();
1257
1346
  for (const [key, conn] of this.agents) {
1258
1347
  if (key.toLowerCase() === lowerName)
1259
1348
  return conn;
1260
1349
  }
1261
- for (const [key, conn] of this.users) {
1262
- if (key.toLowerCase() === lowerName)
1263
- return conn;
1350
+ for (const [key, conns] of this.users) {
1351
+ if (key.toLowerCase() === lowerName && conns.size > 0) {
1352
+ return [...conns][0];
1353
+ }
1264
1354
  }
1265
1355
  return undefined;
1266
1356
  }
@@ -44,6 +44,9 @@ export declare class Daemon {
44
44
  private consensus?;
45
45
  private cloudSyncDebounceTimer?;
46
46
  private spawnManager?;
47
+ /** Telemetry tracking */
48
+ private startTime?;
49
+ private agentSpawnCount;
47
50
  /** Callback for log output from agents (used by dashboard for streaming) */
48
51
  onLogOutput?: (agentName: string, data: string, timestamp: number) => void;
49
52
  /** Interval for writing processing state file (500ms for responsive UI) */
@@ -18,6 +18,7 @@ import { daemonLog as log } from '@agent-relay/utils/logger';
18
18
  import { getCloudSync } from './cloud-sync.js';
19
19
  import { generateId } from '@agent-relay/wrapper';
20
20
  import { createConsensusIntegration, } from './consensus-integration.js';
21
+ import { initTelemetry, track, shutdown as shutdownTelemetry, } from '@agent-relay/telemetry';
21
22
  export const DEFAULT_SOCKET_PATH = '/tmp/agent-relay.sock';
22
23
  export const DEFAULT_DAEMON_CONFIG = {
23
24
  ...DEFAULT_CONFIG,
@@ -41,6 +42,9 @@ export class Daemon {
41
42
  consensus;
42
43
  cloudSyncDebounceTimer;
43
44
  spawnManager;
45
+ /** Telemetry tracking */
46
+ startTime;
47
+ agentSpawnCount = 0;
44
48
  /** Callback for log output from agents (used by dashboard for streaming) */
45
49
  onLogOutput;
46
50
  /** Interval for writing processing state file (500ms for responsive UI) */
@@ -69,6 +73,10 @@ export class Daemon {
69
73
  projectRoot,
70
74
  socketPath: this.config.socketPath,
71
75
  ...spawnConfig,
76
+ // Track spawn count for telemetry
77
+ onAgentSpawn: () => {
78
+ this.agentSpawnCount++;
79
+ },
72
80
  });
73
81
  }
74
82
  // Storage is initialized lazily in start() to support async createStorageAdapter
@@ -213,6 +221,10 @@ export class Daemon {
213
221
  async start() {
214
222
  if (this.running)
215
223
  return;
224
+ // Initialize telemetry (don't show notice - CLI handles that)
225
+ initTelemetry({ showNotice: false });
226
+ this.startTime = Date.now();
227
+ this.agentSpawnCount = 0;
216
228
  // Initialize storage
217
229
  await this.initStorage();
218
230
  // Restore channel memberships from persisted storage (cloud DB or SQLite)
@@ -227,11 +239,17 @@ export class Daemon {
227
239
  }
228
240
  fs.unlinkSync(this.config.socketPath);
229
241
  }
230
- // Ensure directory exists
242
+ // Ensure socket directory exists
231
243
  const socketDir = path.dirname(this.config.socketPath);
232
244
  if (!fs.existsSync(socketDir)) {
233
245
  fs.mkdirSync(socketDir, { recursive: true });
234
246
  }
247
+ // Ensure team directory exists for state files (agents.json, processing-state.json, etc.)
248
+ // Always check and create, even if same as socketDir, to handle edge cases
249
+ const teamDir = this.config.teamDir ?? socketDir;
250
+ if (!fs.existsSync(teamDir)) {
251
+ fs.mkdirSync(teamDir, { recursive: true });
252
+ }
235
253
  // Set up inbox symlink for workspace namespacing
236
254
  // Daemon delivers to legacy path (/tmp/relay-inbox), symlink points to workspace path
237
255
  // This allows agents to use simple instructions while maintaining workspace isolation
@@ -314,6 +332,8 @@ export class Daemon {
314
332
  this.processingStateInterval = setInterval(() => {
315
333
  this.writeProcessingStateFile();
316
334
  }, Daemon.PROCESSING_STATE_INTERVAL_MS);
335
+ // Track daemon start
336
+ track('daemon_start', {});
317
337
  log.info('Listening', { socketPath: this.config.socketPath });
318
338
  resolve();
319
339
  });
@@ -570,6 +590,16 @@ export class Daemon {
570
590
  async stop() {
571
591
  if (!this.running)
572
592
  return;
593
+ // Track daemon stop
594
+ const uptimeSeconds = this.startTime
595
+ ? Math.floor((Date.now() - this.startTime) / 1000)
596
+ : 0;
597
+ track('daemon_stop', {
598
+ uptime_seconds: uptimeSeconds,
599
+ agent_spawn_count: this.agentSpawnCount,
600
+ });
601
+ // Shutdown telemetry (flush pending events)
602
+ await shutdownTelemetry();
573
603
  // Stop cloud sync
574
604
  if (this.cloudSync) {
575
605
  this.cloudSync.stop();
@@ -18,6 +18,8 @@ export interface SpawnManagerConfig {
18
18
  policyFetcher?: CloudPolicyFetcher;
19
19
  /** Callback when an agent dies unexpectedly */
20
20
  onAgentDeath?: OnAgentDeathCallback;
21
+ /** Callback when an agent is spawned (for telemetry tracking) */
22
+ onAgentSpawn?: () => void;
21
23
  }
22
24
  /**
23
25
  * SpawnManager handles agent lifecycle via protocol messages.
@@ -25,6 +27,9 @@ export interface SpawnManagerConfig {
25
27
  */
26
28
  export declare class SpawnManager {
27
29
  private spawner;
30
+ private spawnTimes;
31
+ private agentClis;
32
+ private onAgentSpawn?;
28
33
  constructor(config: SpawnManagerConfig);
29
34
  /**
30
35
  * Handle a SPAWN message from a connection.
@@ -6,14 +6,22 @@
6
6
  import { generateId } from '@agent-relay/wrapper';
7
7
  import { PROTOCOL_VERSION, } from '@agent-relay/protocol/types';
8
8
  import { AgentSpawner } from '@agent-relay/bridge';
9
+ import { track } from '@agent-relay/telemetry';
9
10
  /**
10
11
  * SpawnManager handles agent lifecycle via protocol messages.
11
12
  * The daemon creates this instance and forwards SPAWN/RELEASE messages to it.
12
13
  */
13
14
  export class SpawnManager {
14
15
  spawner;
16
+ spawnTimes = new Map();
17
+ agentClis = new Map();
18
+ onAgentSpawn;
15
19
  constructor(config) {
16
- this.spawner = new AgentSpawner(config.projectRoot);
20
+ this.spawner = new AgentSpawner({
21
+ projectRoot: config.projectRoot,
22
+ socketPath: config.socketPath,
23
+ });
24
+ this.onAgentSpawn = config.onAgentSpawn;
17
25
  if (config.cloudPersistence) {
18
26
  this.spawner.setCloudPersistence(config.cloudPersistence);
19
27
  }
@@ -45,6 +53,7 @@ export class SpawnManager {
45
53
  shadowOf: payload.shadowOf,
46
54
  shadowSpeakOn: payload.shadowSpeakOn,
47
55
  userId: payload.userId,
56
+ includeWorkflowConventions: payload.includeWorkflowConventions,
48
57
  });
49
58
  this.sendResult(connection, 'SPAWN_RESULT', envelope.id, {
50
59
  replyTo: envelope.id,
@@ -54,6 +63,21 @@ export class SpawnManager {
54
63
  error: result.error,
55
64
  policyDecision: result.policyDecision,
56
65
  });
66
+ // Track successful spawn
67
+ if (result.success) {
68
+ this.spawnTimes.set(payload.name, Date.now());
69
+ this.agentClis.set(payload.name, payload.cli);
70
+ // Determine spawn source
71
+ const spawnSource = spawnerName ? 'agent' : 'protocol';
72
+ track('agent_spawn', {
73
+ cli: payload.cli,
74
+ spawn_source: spawnSource,
75
+ has_task: !!payload.task,
76
+ is_shadow: !!payload.shadowOf,
77
+ });
78
+ // Notify daemon to increment spawn count
79
+ this.onAgentSpawn?.();
80
+ }
57
81
  console.log(`[spawn-manager] SPAWN ${result.success ? 'succeeded' : 'failed'}: ${payload.name}`, {
58
82
  pid: result.pid,
59
83
  error: result.error,
@@ -86,6 +110,25 @@ export class SpawnManager {
86
110
  name: payload.name,
87
111
  error: success ? undefined : `Worker ${payload.name} not found`,
88
112
  });
113
+ // Track successful release
114
+ if (success) {
115
+ const spawnTime = this.spawnTimes.get(payload.name);
116
+ const cli = this.agentClis.get(payload.name) || 'unknown';
117
+ const lifetimeSeconds = spawnTime
118
+ ? Math.floor((Date.now() - spawnTime) / 1000)
119
+ : 0;
120
+ // Determine release source
121
+ const releaseSource = requester ? 'agent' : 'protocol';
122
+ track('agent_release', {
123
+ cli,
124
+ release_reason: 'explicit',
125
+ lifetime_seconds: lifetimeSeconds,
126
+ release_source: releaseSource,
127
+ });
128
+ // Clean up tracking data
129
+ this.spawnTimes.delete(payload.name);
130
+ this.agentClis.delete(payload.name);
131
+ }
89
132
  console.log(`[spawn-manager] RELEASE ${success ? 'succeeded' : 'failed'}: ${payload.name}`);
90
133
  }
91
134
  catch (err) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/daemon",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Relay daemon server - agent coordination and message routing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,16 +22,17 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/protocol": "2.0.4",
26
- "@agent-relay/config": "2.0.4",
27
- "@agent-relay/storage": "2.0.4",
28
- "@agent-relay/bridge": "2.0.4",
29
- "@agent-relay/utils": "2.0.4",
30
- "@agent-relay/policy": "2.0.4",
31
- "@agent-relay/memory": "2.0.4",
32
- "@agent-relay/resiliency": "2.0.4",
33
- "@agent-relay/user-directory": "2.0.4",
34
- "@agent-relay/wrapper": "2.0.4",
25
+ "@agent-relay/protocol": "2.0.6",
26
+ "@agent-relay/config": "2.0.6",
27
+ "@agent-relay/storage": "2.0.6",
28
+ "@agent-relay/bridge": "2.0.6",
29
+ "@agent-relay/utils": "2.0.6",
30
+ "@agent-relay/policy": "2.0.6",
31
+ "@agent-relay/memory": "2.0.6",
32
+ "@agent-relay/resiliency": "2.0.6",
33
+ "@agent-relay/user-directory": "2.0.6",
34
+ "@agent-relay/wrapper": "2.0.6",
35
+ "@agent-relay/telemetry": "2.0.6",
35
36
  "ws": "^8.18.3",
36
37
  "better-sqlite3": "^12.6.2",
37
38
  "pg": "^8.16.3",
@@ -4131,6 +4131,7 @@ export async function startDashboard(portOrOptions, dataDirArg, teamDirArg, dbPa
4131
4131
  shadowTriggers,
4132
4132
  shadowSpeakOn,
4133
4133
  userId: typeof userId === 'string' ? userId : undefined,
4134
+ includeWorkflowConventions: true, // Cloud opts into ACK/DONE workflow conventions
4134
4135
  };
4135
4136
  const result = await spawner.spawn(request);
4136
4137
  if (result.success) {
@@ -4252,6 +4253,7 @@ Start by greeting the project leads and asking for status updates.`;
4252
4253
  name: 'Architect',
4253
4254
  cli,
4254
4255
  task: architectPrompt,
4256
+ includeWorkflowConventions: true, // Cloud opts into ACK/DONE workflow conventions
4255
4257
  });
4256
4258
  if (result.success) {
4257
4259
  broadcastData().catch(() => { });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/dashboard",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Web dashboard for Agent Relay - optional package for visual agent coordination",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,17 +25,17 @@
25
25
  "test:watch": "vitest"
26
26
  },
27
27
  "dependencies": {
28
- "@agent-relay/protocol": "2.0.4",
29
- "@agent-relay/config": "2.0.4",
30
- "@agent-relay/storage": "2.0.4",
31
- "@agent-relay/bridge": "2.0.4",
32
- "@agent-relay/utils": "2.0.4",
33
- "@agent-relay/resiliency": "2.0.4",
34
- "@agent-relay/trajectory": "2.0.4",
35
- "@agent-relay/cloud": "2.0.4",
36
- "@agent-relay/daemon": "2.0.4",
37
- "@agent-relay/user-directory": "2.0.4",
38
- "@agent-relay/wrapper": "2.0.4",
28
+ "@agent-relay/protocol": "2.0.6",
29
+ "@agent-relay/config": "2.0.6",
30
+ "@agent-relay/storage": "2.0.6",
31
+ "@agent-relay/bridge": "2.0.6",
32
+ "@agent-relay/utils": "2.0.6",
33
+ "@agent-relay/resiliency": "2.0.6",
34
+ "@agent-relay/trajectory": "2.0.6",
35
+ "@agent-relay/cloud": "2.0.6",
36
+ "@agent-relay/daemon": "2.0.6",
37
+ "@agent-relay/user-directory": "2.0.6",
38
+ "@agent-relay/wrapper": "2.0.6",
39
39
  "express": "^5.2.1",
40
40
  "ws": "^8.18.3"
41
41
  },
@@ -1 +1 @@
1
- <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/99c2552394077586.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js"/><script src="/_next/static/chunks/fd9d1056-609918ca7b6280bb.js" async=""></script><script src="/_next/static/chunks/117-c8afed19e821a35d.js" async=""></script><script src="/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Agent Relay Dashboard</title><meta name="description" content="Fleet control dashboard for Agent Relay"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/css/99c2552394077586.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"2:I[2846,[],\"\"]\n4:I[4707,[],\"\"]\n5:I[6423,[],\"\"]\nb:I[1060,[],\"\"]\n6:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\n7:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\n8:{\"display\":\"inline-block\"}\n9:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\nc:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L2\",null,{\"buildId\":\"72btMIJ64BCAB4UgVkpaq\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"_not-found\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L3\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/99c2552394077586.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$6\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$7\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$8\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$9\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"Agent Relay Dashboard\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Fleet control dashboard for Agent Relay\"}]]\n3:null\n"])</script></body></html>
1
+ <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/99c2552394077586.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js"/><script src="/_next/static/chunks/fd9d1056-609918ca7b6280bb.js" async=""></script><script src="/_next/static/chunks/117-c8afed19e821a35d.js" async=""></script><script src="/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Agent Relay Dashboard</title><meta name="description" content="Fleet control dashboard for Agent Relay"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><script src="/_next/static/chunks/webpack-1cdd8ed57114d5e1.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/css/99c2552394077586.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"2:I[2846,[],\"\"]\n4:I[4707,[],\"\"]\n5:I[6423,[],\"\"]\nb:I[1060,[],\"\"]\n6:{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"}\n7:{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"}\n8:{\"display\":\"inline-block\"}\n9:{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0}\nc:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L2\",null,{\"buildId\":\"lIJs7zSKBaI58kpqegulQ\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"_not-found\"],\"initialTree\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{},[[\"$L3\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null],null],null]},[null,[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"/_not-found\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\"}]],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/99c2552394077586.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L4\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L5\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":\"$6\",\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":\"$7\",\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":\"$8\",\"children\":[\"$\",\"h2\",null,{\"style\":\"$9\",\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],\"$La\"],\"globalErrorComponent\":\"$b\",\"missingSlots\":\"$Wc\"}]\n"])</script><script>self.__next_f.push([1,"a:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"Agent Relay Dashboard\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Fleet control dashboard for Agent Relay\"}]]\n3:null\n"])</script></body></html>