agent-relay 1.3.0 → 1.3.2

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 (240) hide show
  1. package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
  2. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
  3. package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
  4. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
  5. package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
  6. package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
  7. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
  8. package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
  9. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
  10. package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
  11. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
  12. package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
  13. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
  14. package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
  15. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
  16. package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
  17. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
  18. package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
  19. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
  20. package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
  21. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
  22. package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
  23. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
  24. package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
  25. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
  26. package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
  27. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
  28. package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
  29. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
  30. package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
  31. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
  32. package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
  33. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
  34. package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
  35. package/.trajectories/index.json +140 -1
  36. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  37. package/deploy/workspace/codex.config.toml +1 -1
  38. package/deploy/workspace/entrypoint.sh +20 -79
  39. package/deploy/workspace/gh-relay +156 -0
  40. package/deploy/workspace/git-credential-relay +5 -1
  41. package/dist/bridge/multi-project-client.js +13 -10
  42. package/dist/bridge/spawner.d.ts +2 -0
  43. package/dist/bridge/spawner.js +19 -1
  44. package/dist/bridge/types.d.ts +2 -0
  45. package/dist/cli/index.d.ts +1 -1
  46. package/dist/cli/index.js +115 -69
  47. package/dist/cloud/api/admin.js +16 -3
  48. package/dist/cloud/api/codex-auth-helper.js +28 -8
  49. package/dist/cloud/api/consensus.d.ts +13 -0
  50. package/dist/cloud/api/consensus.js +259 -0
  51. package/dist/cloud/api/daemons.js +205 -1
  52. package/dist/cloud/api/git.js +37 -7
  53. package/dist/cloud/api/onboarding.js +4 -1
  54. package/dist/cloud/api/provider-env.d.ts +5 -0
  55. package/dist/cloud/api/provider-env.js +27 -0
  56. package/dist/cloud/api/providers.js +2 -0
  57. package/dist/cloud/api/test-helpers.js +130 -0
  58. package/dist/cloud/api/workspaces.js +38 -3
  59. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  60. package/dist/cloud/db/bulk-ingest.js +268 -0
  61. package/dist/cloud/db/drizzle.d.ts +33 -0
  62. package/dist/cloud/db/drizzle.js +174 -2
  63. package/dist/cloud/db/index.d.ts +24 -5
  64. package/dist/cloud/db/index.js +19 -4
  65. package/dist/cloud/db/schema.d.ts +397 -3
  66. package/dist/cloud/db/schema.js +75 -1
  67. package/dist/cloud/provisioner/index.d.ts +8 -0
  68. package/dist/cloud/provisioner/index.js +256 -50
  69. package/dist/cloud/server.js +47 -3
  70. package/dist/cloud/services/index.d.ts +1 -0
  71. package/dist/cloud/services/index.js +2 -0
  72. package/dist/cloud/services/nango.d.ts +3 -4
  73. package/dist/cloud/services/nango.js +11 -33
  74. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  75. package/dist/cloud/services/workspace-keepalive.js +234 -0
  76. package/dist/config/relay-config.d.ts +23 -0
  77. package/dist/config/relay-config.js +23 -0
  78. package/dist/daemon/agent-manager.d.ts +20 -1
  79. package/dist/daemon/agent-manager.js +47 -0
  80. package/dist/daemon/agent-registry.js +4 -4
  81. package/dist/daemon/agent-signing.d.ts +158 -0
  82. package/dist/daemon/agent-signing.js +523 -0
  83. package/dist/daemon/api.js +18 -1
  84. package/dist/daemon/cli-auth.d.ts +4 -1
  85. package/dist/daemon/cli-auth.js +55 -11
  86. package/dist/daemon/cloud-sync.d.ts +47 -1
  87. package/dist/daemon/cloud-sync.js +152 -3
  88. package/dist/daemon/connection.d.ts +28 -0
  89. package/dist/daemon/connection.js +98 -15
  90. package/dist/daemon/consensus-integration.d.ts +167 -0
  91. package/dist/daemon/consensus-integration.js +371 -0
  92. package/dist/daemon/consensus.d.ts +271 -0
  93. package/dist/daemon/consensus.js +632 -0
  94. package/dist/daemon/delivery-tracker.d.ts +34 -0
  95. package/dist/daemon/delivery-tracker.js +104 -0
  96. package/dist/daemon/enhanced-features.d.ts +118 -0
  97. package/dist/daemon/enhanced-features.js +178 -0
  98. package/dist/daemon/index.d.ts +4 -0
  99. package/dist/daemon/index.js +5 -0
  100. package/dist/daemon/rate-limiter.d.ts +68 -0
  101. package/dist/daemon/rate-limiter.js +130 -0
  102. package/dist/daemon/router.d.ts +18 -11
  103. package/dist/daemon/router.js +55 -111
  104. package/dist/daemon/server.d.ts +13 -1
  105. package/dist/daemon/server.js +71 -9
  106. package/dist/daemon/sync-queue.d.ts +116 -0
  107. package/dist/daemon/sync-queue.js +361 -0
  108. package/dist/health-worker-manager.d.ts +62 -0
  109. package/dist/health-worker-manager.js +144 -0
  110. package/dist/health-worker.d.ts +9 -0
  111. package/dist/health-worker.js +79 -0
  112. package/dist/index.d.ts +2 -1
  113. package/dist/index.js +5 -1
  114. package/dist/memory/context-compaction.d.ts +156 -0
  115. package/dist/memory/context-compaction.js +453 -0
  116. package/dist/memory/index.d.ts +1 -0
  117. package/dist/memory/index.js +1 -0
  118. package/dist/protocol/channels.js +4 -4
  119. package/dist/protocol/framing.d.ts +72 -10
  120. package/dist/protocol/framing.js +194 -25
  121. package/dist/storage/adapter.d.ts +8 -1
  122. package/dist/storage/adapter.js +11 -0
  123. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  124. package/dist/storage/batched-sqlite-adapter.js +183 -0
  125. package/dist/storage/dead-letter-queue.d.ts +196 -0
  126. package/dist/storage/dead-letter-queue.js +427 -0
  127. package/dist/storage/dlq-adapter.d.ts +195 -0
  128. package/dist/storage/dlq-adapter.js +664 -0
  129. package/dist/trajectory/config.d.ts +32 -14
  130. package/dist/trajectory/config.js +38 -16
  131. package/dist/trajectory/integration.js +217 -64
  132. package/dist/utils/git-remote.d.ts +47 -0
  133. package/dist/utils/git-remote.js +125 -0
  134. package/dist/utils/id-generator.d.ts +35 -0
  135. package/dist/utils/id-generator.js +60 -0
  136. package/dist/utils/index.d.ts +1 -0
  137. package/dist/utils/index.js +1 -0
  138. package/dist/utils/precompiled-patterns.d.ts +110 -0
  139. package/dist/utils/precompiled-patterns.js +322 -0
  140. package/dist/wrapper/auth-detection.js +1 -1
  141. package/dist/wrapper/base-wrapper.d.ts +36 -0
  142. package/dist/wrapper/base-wrapper.js +48 -2
  143. package/dist/wrapper/client.d.ts +14 -4
  144. package/dist/wrapper/client.js +84 -31
  145. package/dist/wrapper/idle-detector.d.ts +102 -0
  146. package/dist/wrapper/idle-detector.js +279 -0
  147. package/dist/wrapper/parser.d.ts +4 -0
  148. package/dist/wrapper/parser.js +19 -1
  149. package/dist/wrapper/pty-wrapper.d.ts +7 -1
  150. package/dist/wrapper/pty-wrapper.js +51 -27
  151. package/dist/wrapper/tmux-wrapper.d.ts +12 -1
  152. package/dist/wrapper/tmux-wrapper.js +65 -17
  153. package/package.json +5 -5
  154. package/scripts/run-migrations.js +43 -0
  155. package/scripts/verify-schema.js +134 -0
  156. package/tests/benchmarks/protocol.bench.ts +310 -0
  157. package/dist/dashboard/out/404.html +0 -1
  158. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_buildManifest.js +0 -1
  159. package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +0 -1
  160. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  161. package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +0 -2
  162. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
  163. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
  164. package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +0 -1
  165. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +0 -1
  166. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +0 -1
  167. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +0 -1
  168. package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +0 -1
  169. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +0 -1
  170. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +0 -1
  171. package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +0 -1
  172. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +0 -1
  173. package/dist/dashboard/out/_next/static/chunks/app/history/page-8c8bed33beb2bf1c.js +0 -1
  174. package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
  175. package/dist/dashboard/out/_next/static/chunks/app/login/page-16f3b49e55b1e0ed.js +0 -1
  176. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +0 -1
  177. package/dist/dashboard/out/_next/static/chunks/app/page-4a5938c18a11a654.js +0 -1
  178. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +0 -1
  179. package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +0 -1
  180. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +0 -1
  181. package/dist/dashboard/out/_next/static/chunks/app/signup/page-547dd0ca55ecd0ba.js +0 -1
  182. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
  183. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
  184. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
  185. package/dist/dashboard/out/_next/static/chunks/main-2ee6beb2ae96d210.js +0 -1
  186. package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +0 -1
  187. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
  188. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
  189. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  190. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
  191. package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +0 -1
  192. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +0 -1
  193. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  194. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  195. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  196. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  197. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  198. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
  199. package/dist/dashboard/out/alt-logos/logo.svg +0 -38
  200. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  201. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  202. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  203. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  204. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  205. package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
  206. package/dist/dashboard/out/app/onboarding.html +0 -1
  207. package/dist/dashboard/out/app/onboarding.txt +0 -7
  208. package/dist/dashboard/out/app.html +0 -1
  209. package/dist/dashboard/out/app.txt +0 -7
  210. package/dist/dashboard/out/apple-icon.png +0 -0
  211. package/dist/dashboard/out/connect-repos.html +0 -1
  212. package/dist/dashboard/out/connect-repos.txt +0 -7
  213. package/dist/dashboard/out/history.html +0 -1
  214. package/dist/dashboard/out/history.txt +0 -7
  215. package/dist/dashboard/out/index.html +0 -1
  216. package/dist/dashboard/out/index.txt +0 -7
  217. package/dist/dashboard/out/login.html +0 -6
  218. package/dist/dashboard/out/login.txt +0 -7
  219. package/dist/dashboard/out/metrics.html +0 -1
  220. package/dist/dashboard/out/metrics.txt +0 -7
  221. package/dist/dashboard/out/pricing.html +0 -13
  222. package/dist/dashboard/out/pricing.txt +0 -7
  223. package/dist/dashboard/out/providers/setup/claude.html +0 -1
  224. package/dist/dashboard/out/providers/setup/claude.txt +0 -8
  225. package/dist/dashboard/out/providers/setup/codex.html +0 -1
  226. package/dist/dashboard/out/providers/setup/codex.txt +0 -8
  227. package/dist/dashboard/out/providers.html +0 -1
  228. package/dist/dashboard/out/providers.txt +0 -7
  229. package/dist/dashboard/out/signup.html +0 -6
  230. package/dist/dashboard/out/signup.txt +0 -7
  231. package/dist/dashboard-server/metrics.d.ts +0 -105
  232. package/dist/dashboard-server/metrics.js +0 -193
  233. package/dist/dashboard-server/needs-attention.d.ts +0 -24
  234. package/dist/dashboard-server/needs-attention.js +0 -78
  235. package/dist/dashboard-server/server.d.ts +0 -15
  236. package/dist/dashboard-server/server.js +0 -3776
  237. package/dist/dashboard-server/start.d.ts +0 -6
  238. package/dist/dashboard-server/start.js +0 -13
  239. package/dist/dashboard-server/user-bridge.d.ts +0 -103
  240. package/dist/dashboard-server/user-bridge.js +0 -189
@@ -6,6 +6,8 @@ import { type Envelope, type SendEnvelope, type AckPayload, type ShadowConfig, t
6
6
  import type { ChannelJoinPayload, ChannelLeavePayload, ChannelMessagePayload } from '../protocol/channels.js';
7
7
  import type { StorageAdapter } from '../storage/adapter.js';
8
8
  import type { AgentRegistry } from './agent-registry.js';
9
+ import { type RateLimitConfig } from './rate-limiter.js';
10
+ import { type DeliveryReliabilityOptions } from './delivery-tracker.js';
9
11
  export interface RoutableConnection {
10
12
  id: string;
11
13
  agentName?: string;
@@ -32,14 +34,6 @@ export interface CrossMachineHandler {
32
34
  sendCrossMachineMessage(targetDaemonId: string, targetAgent: string, fromAgent: string, content: string, metadata?: Record<string, unknown>): Promise<boolean>;
33
35
  isRemoteAgent(agentName: string): RemoteAgentInfo | undefined;
34
36
  }
35
- export interface DeliveryReliabilityOptions {
36
- /** How long to wait for an ACK before retrying (ms) */
37
- ackTimeoutMs: number;
38
- /** Maximum attempts (initial send counts as attempt 1) */
39
- maxAttempts: number;
40
- /** How long to keep retrying before dropping (ms) */
41
- deliveryTtlMs: number;
42
- }
43
37
  /** Internal shadow relationship with resolved defaults */
44
38
  interface ShadowRelationship extends ShadowConfig {
45
39
  shadowAgent: string;
@@ -49,11 +43,10 @@ export declare class Router {
49
43
  private connections;
50
44
  private agents;
51
45
  private subscriptions;
52
- private pendingDeliveries;
53
46
  private processingAgents;
54
- private deliveryOptions;
55
47
  private registry?;
56
48
  private crossMachineHandler?;
49
+ private deliveryTracker;
57
50
  /** Shadow relationships: primaryAgent -> list of shadow configs */
58
51
  private shadowsByPrimary;
59
52
  /** Reverse lookup: shadowAgent -> primaryAgent (for cleanup) */
@@ -68,12 +61,16 @@ export declare class Router {
68
61
  private static readonly PROCESSING_TIMEOUT_MS;
69
62
  /** Callback when processing state changes (for real-time dashboard updates) */
70
63
  private onProcessingStateChange?;
64
+ /** Rate limiter for per-agent throttling */
65
+ private rateLimiter;
71
66
  constructor(options?: {
72
67
  storage?: StorageAdapter;
73
68
  delivery?: Partial<DeliveryReliabilityOptions>;
74
69
  registry?: AgentRegistry;
75
70
  onProcessingStateChange?: () => void;
76
71
  crossMachineHandler?: CrossMachineHandler;
72
+ /** Rate limit configuration. Set to null to disable rate limiting. */
73
+ rateLimit?: Partial<RateLimitConfig> | null;
77
74
  });
78
75
  /**
79
76
  * Set or update the cross-machine handler.
@@ -177,6 +174,17 @@ export declare class Router {
177
174
  */
178
175
  get connectionCount(): number;
179
176
  get pendingDeliveryCount(): number;
177
+ /**
178
+ * Get rate limiter statistics.
179
+ */
180
+ getRateLimiterStats(): {
181
+ agentCount: number;
182
+ config: RateLimitConfig;
183
+ };
184
+ /**
185
+ * Reset rate limit for a specific agent (admin operation).
186
+ */
187
+ resetRateLimit(agentName: string): void;
180
188
  /**
181
189
  * Get list of agents currently processing (thinking).
182
190
  * Returns an object with agent names as keys and processing info as values.
@@ -209,7 +217,6 @@ export declare class Router {
209
217
  * Track a delivery and schedule retries until ACKed or TTL/attempts exhausted.
210
218
  */
211
219
  private trackDelivery;
212
- private scheduleRetry;
213
220
  /**
214
221
  * Broadcast a system message to all connected agents.
215
222
  * Used for system notifications like agent death announcements.
@@ -2,24 +2,20 @@
2
2
  * Message router for the agent relay daemon.
3
3
  * Handles routing messages between agents, topic subscriptions, and broadcast.
4
4
  */
5
- import { v4 as uuid } from 'uuid';
5
+ import { generateId } from '../utils/id-generator.js';
6
6
  import { PROTOCOL_VERSION, } from '../protocol/types.js';
7
7
  import { routerLog } from '../utils/logger.js';
8
- const DEFAULT_DELIVERY_OPTIONS = {
9
- ackTimeoutMs: 5000,
10
- maxAttempts: 5,
11
- deliveryTtlMs: 60_000,
12
- };
8
+ import { RateLimiter, NoOpRateLimiter } from './rate-limiter.js';
9
+ import { DeliveryTracker, } from './delivery-tracker.js';
13
10
  export class Router {
14
11
  storage;
15
12
  connections = new Map(); // connectionId -> Connection
16
13
  agents = new Map(); // agentName -> Connection
17
14
  subscriptions = new Map(); // topic -> Set<agentName>
18
- pendingDeliveries = new Map(); // deliverId -> pending
19
15
  processingAgents = new Map(); // agentName -> processing state
20
- deliveryOptions;
21
16
  registry;
22
17
  crossMachineHandler;
18
+ deliveryTracker;
23
19
  /** Shadow relationships: primaryAgent -> list of shadow configs */
24
20
  shadowsByPrimary = new Map();
25
21
  /** Reverse lookup: shadowAgent -> primaryAgent (for cleanup) */
@@ -34,12 +30,22 @@ export class Router {
34
30
  static PROCESSING_TIMEOUT_MS = 30_000;
35
31
  /** Callback when processing state changes (for real-time dashboard updates) */
36
32
  onProcessingStateChange;
33
+ /** Rate limiter for per-agent throttling */
34
+ rateLimiter;
37
35
  constructor(options = {}) {
38
36
  this.storage = options.storage;
39
- this.deliveryOptions = { ...DEFAULT_DELIVERY_OPTIONS, ...options.delivery };
40
37
  this.registry = options.registry;
41
38
  this.onProcessingStateChange = options.onProcessingStateChange;
42
39
  this.crossMachineHandler = options.crossMachineHandler;
40
+ this.deliveryTracker = new DeliveryTracker({
41
+ storage: this.storage,
42
+ delivery: options.delivery,
43
+ getConnection: (id) => this.connections.get(id),
44
+ });
45
+ // Initialize rate limiter (null = disabled)
46
+ this.rateLimiter = options.rateLimit === null
47
+ ? new NoOpRateLimiter()
48
+ : new RateLimiter(options.rateLimit);
43
49
  }
44
50
  /**
45
51
  * Set or update the cross-machine handler.
@@ -239,7 +245,7 @@ export class Router {
239
245
  const triggerEnvelope = {
240
246
  v: PROTOCOL_VERSION,
241
247
  type: 'SEND',
242
- id: uuid(),
248
+ id: generateId(),
243
249
  ts: Date.now(),
244
250
  from: primaryAgent,
245
251
  to: shadow.shadowAgent,
@@ -287,6 +293,11 @@ export class Router {
287
293
  routerLog.warn('Dropping message - sender has no name');
288
294
  return;
289
295
  }
296
+ // Check rate limit
297
+ if (!this.rateLimiter.tryAcquire(senderName)) {
298
+ routerLog.warn(`Rate limited: ${senderName}`);
299
+ return;
300
+ }
290
301
  // Agent is responding - clear their processing state
291
302
  this.clearProcessing(senderName);
292
303
  this.registry?.recordSend(senderName);
@@ -435,22 +446,27 @@ export class Router {
435
446
  * Broadcast to all agents (optionally filtered by topic subscription).
436
447
  */
437
448
  broadcast(from, envelope, topic) {
449
+ // Build recipients list from both agents and users
438
450
  const recipients = topic
439
451
  ? this.subscriptions.get(topic) ?? new Set()
440
- : new Set(this.agents.keys());
441
- for (const agentName of recipients) {
442
- if (agentName === from)
452
+ : new Set([...this.agents.keys(), ...this.users.keys()]);
453
+ for (const recipientName of recipients) {
454
+ if (recipientName === from)
443
455
  continue; // Don't send to self
444
- const target = this.agents.get(agentName);
456
+ // Check both agents and users maps (consistent with sendDirect)
457
+ const target = this.agents.get(recipientName) ?? this.users.get(recipientName);
445
458
  if (target) {
446
- const deliver = this.createDeliverEnvelope(from, agentName, envelope, target);
459
+ const isUserTarget = target.entityType === 'user';
460
+ const deliver = this.createDeliverEnvelope(from, recipientName, envelope, target);
447
461
  const sent = target.send(deliver);
448
462
  this.persistDeliverEnvelope(deliver, true); // Mark as broadcast
449
463
  if (sent) {
450
464
  this.trackDelivery(target, deliver);
451
- this.registry?.recordReceive(agentName);
452
- // Mark recipient as processing
453
- this.setProcessing(agentName, deliver.id);
465
+ this.registry?.recordReceive(recipientName);
466
+ // Only mark AI agents as processing; humans don't need processing indicators
467
+ if (!isUserTarget) {
468
+ this.setProcessing(recipientName, deliver.id);
469
+ }
454
470
  }
455
471
  }
456
472
  }
@@ -464,7 +480,7 @@ export class Router {
464
480
  return {
465
481
  v: PROTOCOL_VERSION,
466
482
  type: 'DELIVER',
467
- id: uuid(),
483
+ id: generateId(),
468
484
  ts: Date.now(),
469
485
  from,
470
486
  to,
@@ -524,7 +540,19 @@ export class Router {
524
540
  return this.connections.size;
525
541
  }
526
542
  get pendingDeliveryCount() {
527
- return this.pendingDeliveries.size;
543
+ return this.deliveryTracker.pendingCount;
544
+ }
545
+ /**
546
+ * Get rate limiter statistics.
547
+ */
548
+ getRateLimiterStats() {
549
+ return this.rateLimiter.getStats();
550
+ }
551
+ /**
552
+ * Reset rate limit for a specific agent (admin operation).
553
+ */
554
+ resetRateLimit(agentName) {
555
+ this.rateLimiter.reset(agentName);
528
556
  }
529
557
  /**
530
558
  * Get list of agents currently processing (thinking).
@@ -580,103 +608,19 @@ export class Router {
580
608
  */
581
609
  handleAck(connection, envelope) {
582
610
  const ackId = envelope.payload.ack_id;
583
- const pending = this.pendingDeliveries.get(ackId);
584
- if (!pending)
585
- return;
586
- // Only accept ACKs from the same connection that received the deliver
587
- if (pending.connectionId !== connection.id)
588
- return;
589
- if (pending.timer) {
590
- clearTimeout(pending.timer);
591
- }
592
- this.pendingDeliveries.delete(ackId);
593
- const statusUpdate = this.storage?.updateMessageStatus?.(ackId, 'acked');
594
- if (statusUpdate instanceof Promise) {
595
- statusUpdate.catch(err => {
596
- routerLog.error('Failed to record ACK status', { error: String(err) });
597
- });
598
- }
599
- routerLog.debug(`ACK received for ${ackId}`);
611
+ this.deliveryTracker.handleAck(connection.id, ackId);
600
612
  }
601
613
  /**
602
614
  * Clear pending deliveries for a connection (e.g., on disconnect).
603
615
  */
604
616
  clearPendingForConnection(connectionId) {
605
- for (const [id, pending] of this.pendingDeliveries.entries()) {
606
- if (pending.connectionId === connectionId) {
607
- if (pending.timer)
608
- clearTimeout(pending.timer);
609
- this.pendingDeliveries.delete(id);
610
- }
611
- }
617
+ this.deliveryTracker.clearPendingForConnection(connectionId);
612
618
  }
613
619
  /**
614
620
  * Track a delivery and schedule retries until ACKed or TTL/attempts exhausted.
615
621
  */
616
622
  trackDelivery(target, deliver) {
617
- const pending = {
618
- envelope: deliver,
619
- connectionId: target.id,
620
- attempts: 1,
621
- firstSentAt: Date.now(),
622
- };
623
- pending.timer = this.scheduleRetry(deliver.id);
624
- this.pendingDeliveries.set(deliver.id, pending);
625
- }
626
- scheduleRetry(deliverId) {
627
- return setTimeout(() => {
628
- const pending = this.pendingDeliveries.get(deliverId);
629
- if (!pending)
630
- return;
631
- const now = Date.now();
632
- const elapsed = now - pending.firstSentAt;
633
- if (elapsed > this.deliveryOptions.deliveryTtlMs) {
634
- routerLog.warn(`Dropping ${deliverId} after TTL`, { ttlMs: this.deliveryOptions.deliveryTtlMs });
635
- this.pendingDeliveries.delete(deliverId);
636
- // Mark message as failed in storage
637
- const statusUpdate = this.storage?.updateMessageStatus?.(deliverId, 'failed');
638
- if (statusUpdate instanceof Promise) {
639
- statusUpdate.catch(err => {
640
- routerLog.error(`Failed to update status for ${deliverId}`, { error: String(err) });
641
- });
642
- }
643
- return;
644
- }
645
- if (pending.attempts >= this.deliveryOptions.maxAttempts) {
646
- routerLog.warn(`Dropping ${deliverId} after max attempts`, { maxAttempts: this.deliveryOptions.maxAttempts });
647
- this.pendingDeliveries.delete(deliverId);
648
- // Mark message as failed in storage
649
- const statusUpdate = this.storage?.updateMessageStatus?.(deliverId, 'failed');
650
- if (statusUpdate instanceof Promise) {
651
- statusUpdate.catch(err => {
652
- routerLog.error(`Failed to update status for ${deliverId}`, { error: String(err) });
653
- });
654
- }
655
- return;
656
- }
657
- const target = this.connections.get(pending.connectionId);
658
- if (!target) {
659
- routerLog.warn(`Dropping ${deliverId} - connection unavailable`);
660
- this.pendingDeliveries.delete(deliverId);
661
- // Mark message as failed in storage
662
- const statusUpdate = this.storage?.updateMessageStatus?.(deliverId, 'failed');
663
- if (statusUpdate instanceof Promise) {
664
- statusUpdate.catch(err => {
665
- routerLog.error(`Failed to update status for ${deliverId}`, { error: String(err) });
666
- });
667
- }
668
- return;
669
- }
670
- pending.attempts++;
671
- const sent = target.send(pending.envelope);
672
- if (!sent) {
673
- routerLog.warn(`Retry failed for ${deliverId}`, { attempt: pending.attempts });
674
- }
675
- else {
676
- routerLog.debug(`Retried ${deliverId}`, { attempt: pending.attempts });
677
- }
678
- pending.timer = this.scheduleRetry(deliverId);
679
- }, this.deliveryOptions.ackTimeoutMs);
623
+ this.deliveryTracker.track(target, deliver);
680
624
  }
681
625
  /**
682
626
  * Broadcast a system message to all connected agents.
@@ -686,7 +630,7 @@ export class Router {
686
630
  const envelope = {
687
631
  v: PROTOCOL_VERSION,
688
632
  type: 'SEND',
689
- id: uuid(),
633
+ id: generateId(),
690
634
  ts: Date.now(),
691
635
  from: '_system',
692
636
  to: '*',
@@ -776,7 +720,7 @@ export class Router {
776
720
  const joinNotification = {
777
721
  v: PROTOCOL_VERSION,
778
722
  type: 'CHANNEL_JOIN',
779
- id: uuid(),
723
+ id: generateId(),
780
724
  ts: Date.now(),
781
725
  from: memberName,
782
726
  payload: envelope.payload,
@@ -828,7 +772,7 @@ export class Router {
828
772
  const leaveNotification = {
829
773
  v: PROTOCOL_VERSION,
830
774
  type: 'CHANNEL_LEAVE',
831
- id: uuid(),
775
+ id: generateId(),
832
776
  ts: Date.now(),
833
777
  from: memberName,
834
778
  payload: envelope.payload,
@@ -871,7 +815,7 @@ export class Router {
871
815
  const deliverEnvelope = {
872
816
  v: PROTOCOL_VERSION,
873
817
  type: 'CHANNEL_MESSAGE',
874
- id: uuid(),
818
+ id: generateId(),
875
819
  ts: Date.now(),
876
820
  from: senderName,
877
821
  payload: envelope.payload,
@@ -5,6 +5,7 @@
5
5
  import { type ConnectionConfig } from './connection.js';
6
6
  import { type StorageAdapter, type StorageConfig } from '../storage/adapter.js';
7
7
  import { type RemoteAgent } from './cloud-sync.js';
8
+ import { ConsensusIntegration, type ConsensusIntegrationConfig } from './consensus-integration.js';
8
9
  export interface DaemonConfig extends ConnectionConfig {
9
10
  socketPath: string;
10
11
  pidFilePath: string;
@@ -16,8 +17,10 @@ export interface DaemonConfig extends ConnectionConfig {
16
17
  teamDir?: string;
17
18
  /** Enable cloud sync for cross-machine agent communication */
18
19
  cloudSync?: boolean;
19
- /** Cloud API URL (defaults to https://api.agent-relay.com) */
20
+ /** Cloud API URL (defaults to https://agent-relay.com) */
20
21
  cloudUrl?: string;
22
+ /** Consensus mechanism for multi-agent decisions (enabled by default, set to false to disable) */
23
+ consensus?: boolean | Partial<ConsensusIntegrationConfig>;
21
24
  }
22
25
  export declare const DEFAULT_SOCKET_PATH = "/tmp/agent-relay.sock";
23
26
  export declare const DEFAULT_DAEMON_CONFIG: DaemonConfig;
@@ -33,6 +36,7 @@ export declare class Daemon {
33
36
  private processingStateInterval?;
34
37
  private cloudSync?;
35
38
  private remoteAgents;
39
+ private consensus?;
36
40
  /** Callback for log output from agents (used by dashboard for streaming) */
37
41
  onLogOutput?: (agentName: string, data: string, timestamp: number) => void;
38
42
  /** Interval for writing processing state file (500ms for responsive UI) */
@@ -112,5 +116,13 @@ export declare class Daemon {
112
116
  * Check if daemon is running.
113
117
  */
114
118
  get isRunning(): boolean;
119
+ /**
120
+ * Check if consensus is enabled.
121
+ */
122
+ get consensusEnabled(): boolean;
123
+ /**
124
+ * Get the consensus integration (for API access).
125
+ */
126
+ getConsensus(): ConsensusIntegration | undefined;
115
127
  }
116
128
  //# sourceMappingURL=server.d.ts.map
@@ -14,7 +14,8 @@ import { getProjectPaths } from '../utils/project-namespace.js';
14
14
  import { AgentRegistry } from './agent-registry.js';
15
15
  import { daemonLog as log } from '../utils/logger.js';
16
16
  import { getCloudSync } from './cloud-sync.js';
17
- import { v4 as uuid } from 'uuid';
17
+ import { generateId } from '../utils/id-generator.js';
18
+ import { createConsensusIntegration, } from './consensus-integration.js';
18
19
  export const DEFAULT_SOCKET_PATH = '/tmp/agent-relay.sock';
19
20
  export const DEFAULT_DAEMON_CONFIG = {
20
21
  ...DEFAULT_CONFIG,
@@ -33,6 +34,7 @@ export class Daemon {
33
34
  processingStateInterval;
34
35
  cloudSync;
35
36
  remoteAgents = [];
37
+ consensus;
36
38
  /** Callback for log output from agents (used by dashboard for streaming) */
37
39
  onLogOutput;
38
40
  /** Interval for writing processing state file (500ms for responsive UI) */
@@ -115,6 +117,14 @@ export class Daemon {
115
117
  isRemoteAgent: this.isRemoteAgent.bind(this),
116
118
  },
117
119
  });
120
+ // Initialize consensus (enabled by default, can be disabled with consensus: false)
121
+ if (this.config.consensus !== false) {
122
+ const consensusConfig = typeof this.config.consensus === 'object'
123
+ ? this.config.consensus
124
+ : {};
125
+ this.consensus = createConsensusIntegration(this.router, consensusConfig);
126
+ log.info('Consensus mechanism enabled');
127
+ }
118
128
  this.storageInitialized = true;
119
129
  }
120
130
  /**
@@ -160,20 +170,41 @@ export class Daemon {
160
170
  * Initialize cloud sync service for cross-machine agent communication.
161
171
  */
162
172
  async initCloudSync() {
163
- // Check for cloud config file
173
+ // Check for cloud config file OR environment variables
164
174
  const dataDir = process.env.AGENT_RELAY_DATA_DIR ||
165
175
  path.join(os.homedir(), '.local', 'share', 'agent-relay');
166
176
  const configPath = path.join(dataDir, 'cloud-config.json');
167
- if (!fs.existsSync(configPath)) {
177
+ const hasConfigFile = fs.existsSync(configPath);
178
+ const hasEnvApiKey = !!process.env.AGENT_RELAY_API_KEY;
179
+ // Allow cloud sync if config file exists OR API key is set via env var
180
+ // This enables cloud-hosted workspaces (Fly.io) to sync messages without a config file
181
+ if (!hasConfigFile && !hasEnvApiKey) {
168
182
  log.info('Cloud sync disabled (not linked to cloud)');
169
183
  return;
170
184
  }
171
185
  try {
172
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
186
+ let apiKey;
187
+ let cloudUrl;
188
+ if (hasConfigFile) {
189
+ // Use config file (local daemons linked via CLI)
190
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
191
+ apiKey = config.apiKey;
192
+ cloudUrl = config.cloudUrl;
193
+ }
194
+ else {
195
+ // Use env vars (cloud-hosted workspaces like Fly.io)
196
+ apiKey = process.env.AGENT_RELAY_API_KEY;
197
+ // CLOUD_API_URL is set by Fly.io provisioner, AGENT_RELAY_CLOUD_URL is the standard
198
+ cloudUrl = process.env.AGENT_RELAY_CLOUD_URL || process.env.CLOUD_API_URL;
199
+ log.info('Using environment variables for cloud sync', { hasApiKey: !!apiKey, hasCloudUrl: !!cloudUrl });
200
+ }
201
+ // Get project root for workspace detection via git remote
202
+ const projectPaths = getProjectPaths();
173
203
  this.cloudSync = getCloudSync({
174
- apiKey: config.apiKey,
175
- cloudUrl: config.cloudUrl || this.config.cloudUrl,
204
+ apiKey,
205
+ cloudUrl: cloudUrl || this.config.cloudUrl,
176
206
  enabled: this.config.cloudSync !== false,
207
+ projectDirectory: projectPaths.projectRoot,
177
208
  });
178
209
  // Listen for remote agent updates
179
210
  this.cloudSync.on('remote-agents-updated', (agents) => {
@@ -191,6 +222,10 @@ export class Daemon {
191
222
  // Handle commands like credential updates, config changes, etc.
192
223
  });
193
224
  await this.cloudSync.start();
225
+ // Set storage adapter for message sync to cloud
226
+ if (this.storage) {
227
+ this.cloudSync.setStorage(this.storage);
228
+ }
194
229
  log.info('Cloud sync enabled');
195
230
  }
196
231
  catch (err) {
@@ -233,7 +268,7 @@ export class Daemon {
233
268
  const envelope = {
234
269
  v: 1,
235
270
  type: 'SEND',
236
- id: uuid(),
271
+ id: generateId(),
237
272
  ts: Date.now(),
238
273
  from: `${msg.from.daemonName}:${msg.from.agent}`,
239
274
  to: msg.to,
@@ -461,9 +496,24 @@ export class Daemon {
461
496
  */
462
497
  handleMessage(connection, envelope) {
463
498
  switch (envelope.type) {
464
- case 'SEND':
465
- this.router.route(connection, envelope);
499
+ case 'SEND': {
500
+ const sendEnvelope = envelope;
501
+ // Check for consensus commands (messages to _consensus)
502
+ if (this.consensus?.enabled && sendEnvelope.to === '_consensus') {
503
+ const from = connection.agentName ?? 'unknown';
504
+ const result = this.consensus.processIncomingMessage(from, sendEnvelope.payload.body);
505
+ if (result.isConsensusCommand) {
506
+ log.info(`Consensus ${result.type} from ${from}`, {
507
+ success: result.result?.success,
508
+ proposalId: result.result?.proposal?.id,
509
+ });
510
+ // Don't route consensus commands to the router
511
+ return;
512
+ }
513
+ }
514
+ this.router.route(connection, sendEnvelope);
466
515
  break;
516
+ }
467
517
  case 'SUBSCRIBE':
468
518
  if (connection.agentName && envelope.topic) {
469
519
  this.router.subscribe(connection.agentName, envelope.topic);
@@ -532,6 +582,18 @@ export class Daemon {
532
582
  get isRunning() {
533
583
  return this.running;
534
584
  }
585
+ /**
586
+ * Check if consensus is enabled.
587
+ */
588
+ get consensusEnabled() {
589
+ return this.consensus?.enabled ?? false;
590
+ }
591
+ /**
592
+ * Get the consensus integration (for API access).
593
+ */
594
+ getConsensus() {
595
+ return this.consensus;
596
+ }
535
597
  }
536
598
  // Run as standalone if executed directly
537
599
  const isMainModule = import.meta.url === `file://${process.argv[1]}`;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Optimized Cloud Sync Queue
3
+ *
4
+ * Handles batched, compressed, resilient message syncing to cloud.
5
+ * Features:
6
+ * - Adaptive batching (size/time/bytes triggers)
7
+ * - Gzip compression for payloads over threshold
8
+ * - Disk spillover for offline resilience
9
+ * - Retry with exponential backoff
10
+ * - Startup reconciliation
11
+ */
12
+ import type { StoredMessage } from '../storage/adapter.js';
13
+ export interface SyncQueueConfig {
14
+ /** Cloud API URL */
15
+ cloudUrl: string;
16
+ /** API key for authentication */
17
+ apiKey: string;
18
+ /** Maximum messages per batch (default: 100) */
19
+ batchSize: number;
20
+ /** Maximum time to wait before flush in ms (default: 200) */
21
+ batchDelayMs: number;
22
+ /** Maximum bytes in memory before flush (default: 512KB) */
23
+ maxBatchBytes: number;
24
+ /** Compress payloads larger than this (default: 1KB) */
25
+ compressionThreshold: number;
26
+ /** Directory for spill files (default: /tmp/agent-relay-sync) */
27
+ spillDir: string;
28
+ /** Maximum spill files to keep (default: 100) */
29
+ maxSpillFiles: number;
30
+ /** Maximum retry attempts (default: 3) */
31
+ maxRetries: number;
32
+ /** Base retry delay in ms (default: 1000) */
33
+ retryDelayMs: number;
34
+ /** Log sync operations (default: false) */
35
+ verbose: boolean;
36
+ }
37
+ export declare const DEFAULT_SYNC_QUEUE_CONFIG: SyncQueueConfig;
38
+ export interface SyncResult {
39
+ synced: number;
40
+ duplicates: number;
41
+ failed: number;
42
+ compressed: boolean;
43
+ bytesTransferred: number;
44
+ }
45
+ export interface SyncQueueStats {
46
+ queuedMessages: number;
47
+ queuedBytes: number;
48
+ totalSynced: number;
49
+ totalFailed: number;
50
+ totalCompressed: number;
51
+ totalBytesTransferred: number;
52
+ spilledFiles: number;
53
+ lastSyncAt?: number;
54
+ lastError?: string;
55
+ }
56
+ export declare class SyncQueue {
57
+ private config;
58
+ private queue;
59
+ private queueBytes;
60
+ private flushTimer?;
61
+ private flushing;
62
+ private flushPromise?;
63
+ private stats;
64
+ constructor(config: Partial<SyncQueueConfig>);
65
+ /**
66
+ * Queue a message for sync to cloud.
67
+ * May trigger an immediate flush if thresholds are exceeded.
68
+ */
69
+ enqueue(message: StoredMessage): Promise<void>;
70
+ /**
71
+ * Enqueue multiple messages at once.
72
+ */
73
+ enqueueBatch(messages: StoredMessage[]): Promise<void>;
74
+ /**
75
+ * Flush all queued messages to cloud.
76
+ */
77
+ flush(): Promise<SyncResult>;
78
+ /**
79
+ * Sync a batch of messages to cloud with retry and spillover.
80
+ */
81
+ private syncBatch;
82
+ /**
83
+ * Send messages to cloud API with optional compression.
84
+ */
85
+ private sendToCloud;
86
+ /**
87
+ * Spill failed batch to disk for later recovery.
88
+ */
89
+ private spillToDisk;
90
+ /**
91
+ * Recover and sync messages from spill files.
92
+ * Call this on startup to resume failed syncs.
93
+ */
94
+ recoverSpilledMessages(): Promise<{
95
+ recovered: number;
96
+ failed: number;
97
+ }>;
98
+ /**
99
+ * Cleanup old spill files beyond the limit.
100
+ */
101
+ private cleanupSpillFiles;
102
+ /**
103
+ * Get sync queue statistics.
104
+ */
105
+ getStats(): SyncQueueStats;
106
+ /**
107
+ * Reset statistics (for testing or periodic reporting).
108
+ */
109
+ resetStats(): void;
110
+ /**
111
+ * Gracefully close the queue, flushing any pending messages.
112
+ */
113
+ close(): Promise<void>;
114
+ private sleep;
115
+ }
116
+ //# sourceMappingURL=sync-queue.d.ts.map