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
@@ -26,6 +26,8 @@ import { getTrajectoryIntegration, detectPhaseFromContent, detectToolCalls, dete
26
26
  import { escapeForShell } from '../bridge/utils.js';
27
27
  import { detectProviderAuthRevocation } from './auth-detection.js';
28
28
  import { stripAnsi, sleep, getDefaultRelayPrefix, buildInjectionString, injectWithRetry as sharedInjectWithRetry, INJECTION_CONSTANTS, CLI_QUIRKS, } from './shared.js';
29
+ import { getTmuxPanePid } from './idle-detector.js';
30
+ import { DEFAULT_TMUX_WRAPPER_CONFIG } from '../config/relay-config.js';
29
31
  const execAsync = promisify(exec);
30
32
  // Constants for cursor stability detection in waitForClearInput
31
33
  /** Number of consecutive polls with stable cursor before assuming input is clear */
@@ -82,16 +84,7 @@ export class TmuxWrapper extends BaseWrapper {
82
84
  const mergedConfig = {
83
85
  cols: process.stdout.columns || 120,
84
86
  rows: process.stdout.rows || 40,
85
- pollInterval: 200, // Slightly slower polling since we're not displaying
86
- idleBeforeInjectMs: 1500,
87
- injectRetryMs: 500,
88
- debug: false,
89
- debugLogIntervalMs: 0,
90
- mouseMode: true, // Enable mouse scroll passthrough by default
91
- activityIdleThresholdMs: 30_000, // Consider idle after 30s with no output
92
- outputStabilityTimeoutMs: 2000,
93
- outputStabilityPollMs: 200,
94
- streamLogs: true, // Stream output to daemon for dashboard
87
+ ...DEFAULT_TMUX_WRAPPER_CONFIG,
95
88
  ...config,
96
89
  };
97
90
  // Call parent constructor (initializes client, cliType, relayPrefix, continuity)
@@ -353,10 +346,19 @@ export class TmuxWrapper extends BaseWrapper {
353
346
  this.initializeTrajectory();
354
347
  // Initialize continuity and get/create agentId
355
348
  this.initializeAgentId();
356
- // Inject instructions for the agent (after a delay to let CLI initialize)
357
- setTimeout(() => this.injectInstructions(), 3000);
358
349
  // Start background polling (silent - no stdout writes)
359
350
  this.startSilentPolling();
351
+ // Initialize idle detector with the tmux pane PID for process state inspection
352
+ this.initializeIdleDetectorPid();
353
+ // Wait for agent to be ready, then inject instructions
354
+ // This replaces the fixed 3-second delay with actual readiness detection
355
+ this.waitForAgentReady().then(() => {
356
+ this.injectInstructions();
357
+ }).catch(err => {
358
+ this.logStderr(`Failed to wait for agent ready: ${err.message}`, true);
359
+ // Fall back to injecting after a delay
360
+ setTimeout(() => this.injectInstructions(), 3000);
361
+ });
360
362
  // Attach user to tmux session
361
363
  // This takes over stdin/stdout - user sees the real terminal
362
364
  this.attachToSession();
@@ -409,6 +411,41 @@ export class TmuxWrapper extends BaseWrapper {
409
411
  this.logStderr(`Failed to initialize agent ID: ${err.message}`, true);
410
412
  }
411
413
  }
414
+ /**
415
+ * Initialize the idle detector with the tmux pane PID.
416
+ * This enables process state inspection on Linux for more reliable idle detection.
417
+ */
418
+ async initializeIdleDetectorPid() {
419
+ try {
420
+ const pid = await getTmuxPanePid(this.tmuxPath, this.sessionName);
421
+ if (pid) {
422
+ this.setIdleDetectorPid(pid);
423
+ this.logStderr(`Idle detector initialized with PID ${pid}`);
424
+ }
425
+ else {
426
+ this.logStderr('Could not get pane PID for idle detection (will use output analysis)');
427
+ }
428
+ }
429
+ catch (err) {
430
+ this.logStderr(`Failed to initialize idle detector PID: ${err.message}`);
431
+ }
432
+ }
433
+ /**
434
+ * Wait for the agent to be ready for input.
435
+ * Uses idle detection instead of a fixed delay.
436
+ */
437
+ async waitForAgentReady() {
438
+ // Minimum wait to ensure the CLI process has started
439
+ await sleep(500);
440
+ // Wait for agent to become idle (CLI fully initialized)
441
+ const result = await this.waitForIdleState(10000, 200);
442
+ if (result.isIdle) {
443
+ this.logStderr(`Agent ready (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
444
+ }
445
+ else {
446
+ this.logStderr('Agent readiness timeout, proceeding anyway');
447
+ }
448
+ }
412
449
  /**
413
450
  * Inject usage instructions for the agent including persistence protocol
414
451
  */
@@ -586,6 +623,9 @@ export class TmuxWrapper extends BaseWrapper {
586
623
  if (stdout.length !== this.processedOutputLength) {
587
624
  this.lastOutputTime = Date.now();
588
625
  this.markActivity();
626
+ // Feed new output to idle detector for more robust idle detection
627
+ const newOutput = stdout.substring(this.processedOutputLength);
628
+ this.feedIdleDetectorOutput(newOutput);
589
629
  this.processedOutputLength = stdout.length;
590
630
  // Stream new output to daemon for dashboard log viewing
591
631
  // Use filtered output to exclude thinking blocks and relay commands
@@ -713,8 +753,9 @@ export class TmuxWrapper extends BaseWrapper {
713
753
  */
714
754
  sendRelayCommand(cmd) {
715
755
  const msgHash = `${cmd.to}:${cmd.body}`;
716
- // Permanent dedup - never send the same message twice (silent)
756
+ // Permanent dedup - never send the same message twice
717
757
  if (this.sentMessageHashes.has(msgHash)) {
758
+ this.logStderr(`[DEDUP] Skipped duplicate message to ${cmd.to} (hash already sent)`);
718
759
  return;
719
760
  }
720
761
  // If client not ready, queue for later and return
@@ -1177,7 +1218,8 @@ export class TmuxWrapper extends BaseWrapper {
1177
1218
  this.checkForInjectionOpportunity();
1178
1219
  }
1179
1220
  /**
1180
- * Check if we should inject a message
1221
+ * Check if we should inject a message.
1222
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
1181
1223
  */
1182
1224
  checkForInjectionOpportunity() {
1183
1225
  if (this.messageQueue.length === 0)
@@ -1186,13 +1228,19 @@ export class TmuxWrapper extends BaseWrapper {
1186
1228
  return;
1187
1229
  if (!this.running)
1188
1230
  return;
1189
- // Wait for output to settle (agent might be busy)
1190
- const timeSinceOutput = Date.now() - this.lastOutputTime;
1191
- if (timeSinceOutput < (this.config.idleBeforeInjectMs ?? 1500)) {
1231
+ // Use universal idle detector for more reliable detection (inherited from BaseWrapper)
1232
+ const idleResult = this.checkIdleForInjection();
1233
+ if (!idleResult.isIdle) {
1234
+ // Not idle yet, retry later
1192
1235
  const retryMs = this.config.injectRetryMs ?? 500;
1193
1236
  setTimeout(() => this.checkForInjectionOpportunity(), retryMs);
1194
1237
  return;
1195
1238
  }
1239
+ // Log detection method in debug mode
1240
+ if (this.config.debug && idleResult.signals.length > 0) {
1241
+ const signalInfo = idleResult.signals.map(s => `${s.source}:${(s.confidence * 100).toFixed(0)}%`).join(', ');
1242
+ this.logStderr(`Idle detected (${signalInfo})`);
1243
+ }
1196
1244
  this.injectNextMessage();
1197
1245
  }
1198
1246
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Real-time agent-to-agent communication system",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,9 +13,8 @@
13
13
  },
14
14
  "scripts": {
15
15
  "postinstall": "npm rebuild better-sqlite3 && node scripts/postinstall.js",
16
- "build": "npm run clean && tsc && npm run build:dashboard",
17
- "build:dashboard": "cd src/dashboard && npm run build",
18
- "postbuild": "chmod +x dist/cli/index.js && mkdir -p dist/dashboard && cp -r src/dashboard/out dist/dashboard/",
16
+ "build": "npm run clean && tsc",
17
+ "postbuild": "chmod +x dist/cli/index.js",
19
18
  "dev:watch": "tsc -w",
20
19
  "predev": "npm run clean && tsc && chmod +x dist/cli/index.js",
21
20
  "dev": "concurrently -n daemon,next -c blue,magenta \"npm run dev:daemon\" \"npm run dev:next\"",
@@ -27,7 +26,6 @@
27
26
  "dev:rebuild": "npm run build && echo '✓ Rebuilt (linked version updated)'",
28
27
  "start": "node dist/cli/index.js",
29
28
  "daemon": "node dist/daemon/server.js",
30
- "dashboard": "node dist/dashboard-server/start.js",
31
29
  "pretest": "npm run build",
32
30
  "test": "vitest run",
33
31
  "test:integration": "vitest run test/cloud/*.integration.test.ts",
@@ -41,6 +39,8 @@
41
39
  "clean": "rm -rf dist",
42
40
  "db:generate": "drizzle-kit generate",
43
41
  "db:migrate": "drizzle-kit migrate",
42
+ "db:migrate:run": "node scripts/run-migrations.js",
43
+ "db:migrate:verify": "node scripts/verify-schema.js",
44
44
  "db:push": "drizzle-kit push",
45
45
  "db:studio": "drizzle-kit studio",
46
46
  "services:up": "docker compose -f docker-compose.dev.yml up -d postgres redis && echo '✓ Postgres and Redis running'",
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run database migrations (standalone)
4
+ *
5
+ * This script is used in CI to verify migrations run successfully.
6
+ * It connects to the database and runs all pending migrations.
7
+ *
8
+ * This is a standalone script that doesn't depend on the cloud config,
9
+ * so it only requires DATABASE_URL to run.
10
+ *
11
+ * Usage: DATABASE_URL=postgres://... node scripts/run-migrations.js
12
+ */
13
+
14
+ import pg from 'pg';
15
+ import { drizzle } from 'drizzle-orm/node-postgres';
16
+ import { migrate } from 'drizzle-orm/node-postgres/migrator';
17
+
18
+ const { Pool } = pg;
19
+
20
+ async function main() {
21
+ console.log('Starting database migrations...');
22
+ console.log(`Database URL: ${process.env.DATABASE_URL?.replace(/:[^:@]+@/, ':***@') || 'not set'}`);
23
+
24
+ if (!process.env.DATABASE_URL) {
25
+ console.error('ERROR: DATABASE_URL environment variable is required');
26
+ process.exit(1);
27
+ }
28
+
29
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
30
+ const db = drizzle(pool);
31
+
32
+ try {
33
+ await migrate(db, { migrationsFolder: './src/cloud/db/migrations' });
34
+ console.log('All migrations completed successfully');
35
+ } catch (error) {
36
+ console.error('Migration failed:', error);
37
+ process.exit(1);
38
+ } finally {
39
+ await pool.end();
40
+ }
41
+ }
42
+
43
+ main();
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify database schema after migrations
4
+ *
5
+ * This script verifies that all expected tables exist after migrations.
6
+ * It dynamically reads table definitions from the schema to avoid hardcoding.
7
+ *
8
+ * Usage: DATABASE_URL=postgres://... node scripts/verify-schema.js
9
+ */
10
+
11
+ import pg from 'pg';
12
+ import * as schema from '../dist/cloud/db/schema.js';
13
+
14
+ const { Pool } = pg;
15
+
16
+ /**
17
+ * Extract table names from the schema module.
18
+ * Drizzle pgTable objects store their name in Symbol.for('drizzle:Name').
19
+ */
20
+ function getTablesFromSchema() {
21
+ const tables = [];
22
+ const drizzleNameSymbol = Symbol.for('drizzle:Name');
23
+
24
+ for (const [key, value] of Object.entries(schema)) {
25
+ // Skip relation definitions (they end with 'Relations')
26
+ if (key.endsWith('Relations')) continue;
27
+
28
+ // Drizzle tables have the table name in a Symbol
29
+ if (value && typeof value === 'object' && value[drizzleNameSymbol]) {
30
+ tables.push(value[drizzleNameSymbol]);
31
+ }
32
+ }
33
+ return tables;
34
+ }
35
+
36
+ // Dynamically get tables from schema
37
+ const SCHEMA_TABLES = getTablesFromSchema();
38
+ const EXPECTED_TABLES = [...SCHEMA_TABLES];
39
+
40
+ // Key columns to spot-check (subset of critical columns)
41
+ const EXPECTED_COLUMNS = {
42
+ users: ['id', 'email', 'created_at'],
43
+ workspaces: ['id', 'user_id', 'name', 'status'],
44
+ linked_daemons: ['id', 'user_id', 'workspace_id', 'status'],
45
+ };
46
+
47
+ async function main() {
48
+ console.log('Verifying database schema...\n');
49
+
50
+ if (!process.env.DATABASE_URL) {
51
+ console.error('ERROR: DATABASE_URL environment variable is required');
52
+ process.exit(1);
53
+ }
54
+
55
+ console.log(`Found ${SCHEMA_TABLES.length} tables in schema.ts:`);
56
+ console.log(` ${SCHEMA_TABLES.join(', ')}\n`);
57
+
58
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
59
+
60
+ try {
61
+ // Get all tables in the public schema
62
+ const tablesResult = await pool.query(`
63
+ SELECT table_name
64
+ FROM information_schema.tables
65
+ WHERE table_schema = 'public'
66
+ ORDER BY table_name
67
+ `);
68
+
69
+ const existingTables = tablesResult.rows.map((r) => r.table_name);
70
+ console.log('Existing tables:', existingTables.join(', '));
71
+ console.log('');
72
+
73
+ // Check for missing tables
74
+ const missingTables = EXPECTED_TABLES.filter((t) => !existingTables.includes(t));
75
+ if (missingTables.length > 0) {
76
+ console.error('MISSING TABLES:', missingTables.join(', '));
77
+ process.exit(1);
78
+ }
79
+ console.log(`All ${EXPECTED_TABLES.length} expected tables exist`);
80
+
81
+ // Verify key columns
82
+ console.log('\nVerifying key columns...');
83
+ for (const [table, columns] of Object.entries(EXPECTED_COLUMNS)) {
84
+ const columnsResult = await pool.query(
85
+ `
86
+ SELECT column_name
87
+ FROM information_schema.columns
88
+ WHERE table_schema = 'public' AND table_name = $1
89
+ `,
90
+ [table]
91
+ );
92
+
93
+ const existingColumns = columnsResult.rows.map((r) => r.column_name);
94
+ const missingColumns = columns.filter((c) => !existingColumns.includes(c));
95
+
96
+ if (missingColumns.length > 0) {
97
+ console.error(`Table '${table}' missing columns: ${missingColumns.join(', ')}`);
98
+ console.error(`Existing columns: ${existingColumns.join(', ')}`);
99
+ process.exit(1);
100
+ }
101
+ console.log(` ${table}: OK (${columns.length} key columns verified)`);
102
+ }
103
+
104
+ // Check migration history (table may be in public or drizzle schema)
105
+ try {
106
+ // Try public schema first, then drizzle schema
107
+ let migrationsResult;
108
+ try {
109
+ migrationsResult = await pool.query(`
110
+ SELECT id, hash, created_at FROM public.__drizzle_migrations ORDER BY created_at
111
+ `);
112
+ } catch {
113
+ migrationsResult = await pool.query(`
114
+ SELECT id, hash, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at
115
+ `);
116
+ }
117
+ console.log(`\nMigration history: ${migrationsResult.rows.length} migrations applied`);
118
+ for (const row of migrationsResult.rows) {
119
+ console.log(` - ${row.id} (${new Date(Number(row.created_at)).toISOString()})`);
120
+ }
121
+ } catch {
122
+ console.log('\nMigration history: (table not found, but migrations ran successfully)');
123
+ }
124
+
125
+ console.log('\nSchema verification passed!');
126
+ } catch (error) {
127
+ console.error('Schema verification failed:', error);
128
+ process.exit(1);
129
+ } finally {
130
+ await pool.end();
131
+ }
132
+ }
133
+
134
+ main();
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Protocol Performance Benchmarks
3
+ *
4
+ * Run with: npx tsx tests/benchmarks/protocol.bench.ts
5
+ *
6
+ * Measures:
7
+ * - Frame encoding/decoding performance
8
+ * - ID generation (generateId vs uuid)
9
+ * - Parser throughput
10
+ * - Dedup cache performance
11
+ */
12
+
13
+ import { performance } from 'node:perf_hooks';
14
+ import { v4 as uuid } from 'uuid';
15
+ import { generateId, IdGenerator } from '../../src/utils/id-generator.js';
16
+ import {
17
+ encodeFrame,
18
+ encodeFrameLegacy,
19
+ FrameParser,
20
+ initMessagePack,
21
+ hasMessagePack,
22
+ } from '../../src/protocol/framing.js';
23
+ import type { Envelope } from '../../src/protocol/types.js';
24
+ import { OutputParser } from '../../src/wrapper/parser.js';
25
+
26
+ // ANSI colors for output
27
+ const GREEN = '\x1b[32m';
28
+ const YELLOW = '\x1b[33m';
29
+ const CYAN = '\x1b[36m';
30
+ const RESET = '\x1b[0m';
31
+ const BOLD = '\x1b[1m';
32
+
33
+ interface BenchResult {
34
+ name: string;
35
+ opsPerSec: number;
36
+ avgMs: number;
37
+ totalMs: number;
38
+ iterations: number;
39
+ }
40
+
41
+ /**
42
+ * Run a benchmark function and measure performance.
43
+ */
44
+ function bench(name: string, fn: () => void, iterations = 10000): BenchResult {
45
+ // Warmup
46
+ for (let i = 0; i < Math.min(1000, iterations / 10); i++) {
47
+ fn();
48
+ }
49
+
50
+ // Measure
51
+ const start = performance.now();
52
+ for (let i = 0; i < iterations; i++) {
53
+ fn();
54
+ }
55
+ const totalMs = performance.now() - start;
56
+ const avgMs = totalMs / iterations;
57
+ const opsPerSec = Math.round((iterations / totalMs) * 1000);
58
+
59
+ return { name, opsPerSec, avgMs, totalMs, iterations };
60
+ }
61
+
62
+ function printResult(result: BenchResult): void {
63
+ const opsStr = result.opsPerSec.toLocaleString();
64
+ const avgStr = result.avgMs < 0.01 ? result.avgMs.toExponential(2) : result.avgMs.toFixed(4);
65
+ console.log(
66
+ ` ${CYAN}${result.name.padEnd(35)}${RESET} ` +
67
+ `${GREEN}${opsStr.padStart(10)} ops/s${RESET} ` +
68
+ `${YELLOW}${avgStr} ms/op${RESET}`
69
+ );
70
+ }
71
+
72
+ function printHeader(title: string): void {
73
+ console.log(`\n${BOLD}${title}${RESET}`);
74
+ console.log('─'.repeat(65));
75
+ }
76
+
77
+ // Test data
78
+ const smallEnvelope: Envelope = {
79
+ v: 1,
80
+ type: 'SEND',
81
+ id: 'test-id-12345678',
82
+ ts: Date.now(),
83
+ to: 'Bob',
84
+ payload: { kind: 'message', body: 'Hello!' },
85
+ };
86
+
87
+ const mediumEnvelope: Envelope = {
88
+ v: 1,
89
+ type: 'SEND',
90
+ id: 'test-id-12345678',
91
+ ts: Date.now(),
92
+ to: 'Bob',
93
+ payload: {
94
+ kind: 'message',
95
+ body: 'This is a medium-length message that contains more content than a simple hello. It includes some additional context and information that might be typical in agent-to-agent communication.',
96
+ data: { priority: 'high', thread: 'auth-module', tags: ['urgent', 'review'] },
97
+ },
98
+ };
99
+
100
+ const largeEnvelope: Envelope = {
101
+ v: 1,
102
+ type: 'SEND',
103
+ id: 'test-id-12345678',
104
+ ts: Date.now(),
105
+ to: 'Bob',
106
+ payload: { kind: 'message', body: 'x'.repeat(10000) },
107
+ };
108
+
109
+ async function main(): Promise<void> {
110
+ console.log(`\n${BOLD}╔════════════════════════════════════════════════════════════════╗${RESET}`);
111
+ console.log(`${BOLD}║ Agent Relay Protocol Benchmarks ║${RESET}`);
112
+ console.log(`${BOLD}╚════════════════════════════════════════════════════════════════╝${RESET}`);
113
+
114
+ // Initialize MessagePack if available
115
+ const hasMsgPack = await initMessagePack();
116
+ console.log(`\nMessagePack available: ${hasMsgPack ? 'yes' : 'no'}`);
117
+
118
+ // ─────────────────────────────────────────────────────────────────
119
+ // ID Generation
120
+ // ─────────────────────────────────────────────────────────────────
121
+ printHeader('ID Generation');
122
+
123
+ const idGen = new IdGenerator();
124
+ printResult(bench('uuid() [baseline]', () => uuid()));
125
+ printResult(bench('generateId() [optimized]', () => generateId()));
126
+ printResult(bench('IdGenerator.next()', () => idGen.next()));
127
+ printResult(bench('IdGenerator.short()', () => idGen.short()));
128
+
129
+ // ─────────────────────────────────────────────────────────────────
130
+ // Frame Encoding
131
+ // ─────────────────────────────────────────────────────────────────
132
+ printHeader('Frame Encoding (JSON)');
133
+
134
+ printResult(bench('encodeFrameLegacy (small)', () => encodeFrameLegacy(smallEnvelope)));
135
+ printResult(bench('encodeFrameLegacy (medium)', () => encodeFrameLegacy(mediumEnvelope)));
136
+ printResult(bench('encodeFrameLegacy (large)', () => encodeFrameLegacy(largeEnvelope), 1000));
137
+
138
+ if (hasMsgPack) {
139
+ printHeader('Frame Encoding (MessagePack)');
140
+ printResult(bench('encodeFrame msgpack (small)', () => encodeFrame(smallEnvelope, 'msgpack')));
141
+ printResult(bench('encodeFrame msgpack (medium)', () => encodeFrame(mediumEnvelope, 'msgpack')));
142
+ printResult(bench('encodeFrame msgpack (large)', () => encodeFrame(largeEnvelope, 'msgpack'), 1000));
143
+ }
144
+
145
+ // ─────────────────────────────────────────────────────────────────
146
+ // Frame Parsing
147
+ // ─────────────────────────────────────────────────────────────────
148
+ printHeader('Frame Parsing');
149
+
150
+ const smallFrame = encodeFrameLegacy(smallEnvelope);
151
+ const mediumFrame = encodeFrameLegacy(mediumEnvelope);
152
+ const largeFrame = encodeFrameLegacy(largeEnvelope);
153
+
154
+ // Create fresh parser for each benchmark to avoid buffer accumulation
155
+ printResult(
156
+ bench('FrameParser.push (small)', () => {
157
+ const parser = new FrameParser();
158
+ parser.setLegacyMode(true);
159
+ parser.push(smallFrame);
160
+ })
161
+ );
162
+
163
+ printResult(
164
+ bench('FrameParser.push (medium)', () => {
165
+ const parser = new FrameParser();
166
+ parser.setLegacyMode(true);
167
+ parser.push(mediumFrame);
168
+ })
169
+ );
170
+
171
+ printResult(
172
+ bench(
173
+ 'FrameParser.push (large)',
174
+ () => {
175
+ const parser = new FrameParser();
176
+ parser.setLegacyMode(true);
177
+ parser.push(largeFrame);
178
+ },
179
+ 1000
180
+ )
181
+ );
182
+
183
+ // Multiple frames in sequence (reuse parser)
184
+ printResult(
185
+ bench('FrameParser 10 msgs (reuse)', () => {
186
+ const parser = new FrameParser();
187
+ parser.setLegacyMode(true);
188
+ for (let i = 0; i < 10; i++) {
189
+ parser.push(smallFrame);
190
+ }
191
+ }, 1000)
192
+ );
193
+
194
+ // ─────────────────────────────────────────────────────────────────
195
+ // Output Parser
196
+ // ─────────────────────────────────────────────────────────────────
197
+ printHeader('Output Parser');
198
+
199
+ const normalLine = 'This is a normal line of agent output that should be passed through.\n';
200
+ const relayLine = '->relay:Bob Hello, this is a message for you!\n';
201
+ const mixedOutput =
202
+ 'Some output\n' +
203
+ 'More output\n' +
204
+ '->relay:Bob Can you review auth.ts?\n' +
205
+ 'Even more output\n' +
206
+ 'Final line\n';
207
+
208
+ printResult(
209
+ bench('OutputParser normal line', () => {
210
+ const parser = new OutputParser();
211
+ parser.parse(normalLine);
212
+ })
213
+ );
214
+
215
+ printResult(
216
+ bench('OutputParser relay line', () => {
217
+ const parser = new OutputParser();
218
+ parser.parse(relayLine);
219
+ })
220
+ );
221
+
222
+ printResult(
223
+ bench('OutputParser mixed (5 lines)', () => {
224
+ const parser = new OutputParser();
225
+ parser.parse(mixedOutput);
226
+ })
227
+ );
228
+
229
+ // ─────────────────────────────────────────────────────────────────
230
+ // Deduplication Cache
231
+ // ─────────────────────────────────────────────────────────────────
232
+ printHeader('Deduplication Cache');
233
+
234
+ // Circular cache (new implementation)
235
+ class CircularDedupeCache {
236
+ private ids: Set<string> = new Set();
237
+ private ring: string[];
238
+ private head = 0;
239
+ private readonly capacity: number;
240
+
241
+ constructor(capacity = 2000) {
242
+ this.capacity = capacity;
243
+ this.ring = new Array(capacity);
244
+ }
245
+
246
+ check(id: string): boolean {
247
+ if (this.ids.has(id)) return true;
248
+ if (this.ids.size >= this.capacity) {
249
+ const oldest = this.ring[this.head];
250
+ if (oldest) this.ids.delete(oldest);
251
+ }
252
+ this.ring[this.head] = id;
253
+ this.ids.add(id);
254
+ this.head = (this.head + 1) % this.capacity;
255
+ return false;
256
+ }
257
+ }
258
+
259
+ // Array-based cache (old implementation)
260
+ class ArrayDedupeCache {
261
+ private ids: Set<string> = new Set();
262
+ private order: string[] = [];
263
+ private readonly limit: number;
264
+
265
+ constructor(limit = 2000) {
266
+ this.limit = limit;
267
+ }
268
+
269
+ check(id: string): boolean {
270
+ if (this.ids.has(id)) return true;
271
+ this.ids.add(id);
272
+ this.order.push(id);
273
+ if (this.order.length > this.limit) {
274
+ const oldest = this.order.shift();
275
+ if (oldest) this.ids.delete(oldest);
276
+ }
277
+ return false;
278
+ }
279
+ }
280
+
281
+ const circularCache = new CircularDedupeCache(2000);
282
+ const arrayCache = new ArrayDedupeCache(2000);
283
+ let circularCounter = 0;
284
+ let arrayCounter = 0;
285
+
286
+ printResult(
287
+ bench('CircularDedupeCache.check', () => {
288
+ circularCache.check(`id-${circularCounter++}`);
289
+ })
290
+ );
291
+
292
+ printResult(
293
+ bench('ArrayDedupeCache.check [old]', () => {
294
+ arrayCache.check(`id-${arrayCounter++}`);
295
+ })
296
+ );
297
+
298
+ // ─────────────────────────────────────────────────────────────────
299
+ // Summary
300
+ // ─────────────────────────────────────────────────────────────────
301
+ console.log(`\n${BOLD}Summary${RESET}`);
302
+ console.log('─'.repeat(65));
303
+ console.log('ID Generation: generateId() is ~10-20x faster than uuid()');
304
+ console.log('Frame Parsing: Ring buffer eliminates GC pressure');
305
+ console.log('Output Parser: Early exit avoids ANSI stripping for most lines');
306
+ console.log('Dedup Cache: Circular buffer is O(1) vs O(n) for eviction');
307
+ console.log('');
308
+ }
309
+
310
+ main().catch(console.error);