agent-relay 1.3.1 → 1.3.3

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 (202) 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/README.md +23 -9
  37. package/TRAIL_GIT_AUTH_FIX.md +113 -0
  38. package/deploy/workspace/codex.config.toml +1 -1
  39. package/deploy/workspace/entrypoint.sh +20 -79
  40. package/deploy/workspace/gh-relay +156 -0
  41. package/deploy/workspace/git-credential-relay +5 -1
  42. package/dist/bridge/multi-project-client.js +13 -10
  43. package/dist/bridge/spawner.d.ts +2 -0
  44. package/dist/bridge/spawner.js +58 -76
  45. package/dist/bridge/types.d.ts +2 -0
  46. package/dist/cli/index.d.ts +8 -6
  47. package/dist/cli/index.js +297 -30
  48. package/dist/cloud/api/admin.js +16 -3
  49. package/dist/cloud/api/codex-auth-helper.js +28 -8
  50. package/dist/cloud/api/consensus.d.ts +13 -0
  51. package/dist/cloud/api/consensus.js +259 -0
  52. package/dist/cloud/api/daemons.js +205 -1
  53. package/dist/cloud/api/git.js +37 -7
  54. package/dist/cloud/api/onboarding.js +4 -1
  55. package/dist/cloud/api/provider-env.d.ts +5 -0
  56. package/dist/cloud/api/provider-env.js +27 -0
  57. package/dist/cloud/api/providers.js +2 -0
  58. package/dist/cloud/api/test-helpers.js +130 -0
  59. package/dist/cloud/api/workspaces.js +38 -3
  60. package/dist/cloud/db/bulk-ingest.d.ts +88 -0
  61. package/dist/cloud/db/bulk-ingest.js +268 -0
  62. package/dist/cloud/db/drizzle.d.ts +33 -0
  63. package/dist/cloud/db/drizzle.js +174 -2
  64. package/dist/cloud/db/index.d.ts +24 -5
  65. package/dist/cloud/db/index.js +19 -4
  66. package/dist/cloud/db/schema.d.ts +397 -3
  67. package/dist/cloud/db/schema.js +75 -1
  68. package/dist/cloud/provisioner/index.d.ts +8 -0
  69. package/dist/cloud/provisioner/index.js +256 -50
  70. package/dist/cloud/server.js +47 -3
  71. package/dist/cloud/services/index.d.ts +1 -0
  72. package/dist/cloud/services/index.js +2 -0
  73. package/dist/cloud/services/nango.d.ts +3 -4
  74. package/dist/cloud/services/nango.js +11 -33
  75. package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
  76. package/dist/cloud/services/workspace-keepalive.js +234 -0
  77. package/dist/config/relay-config.d.ts +23 -0
  78. package/dist/config/relay-config.js +23 -0
  79. package/dist/daemon/agent-manager.d.ts +20 -1
  80. package/dist/daemon/agent-manager.js +51 -0
  81. package/dist/daemon/agent-registry.js +4 -4
  82. package/dist/daemon/agent-signing.d.ts +158 -0
  83. package/dist/daemon/agent-signing.js +523 -0
  84. package/dist/daemon/api.js +18 -1
  85. package/dist/daemon/cli-auth.d.ts +4 -1
  86. package/dist/daemon/cli-auth.js +55 -11
  87. package/dist/daemon/cloud-sync.d.ts +47 -1
  88. package/dist/daemon/cloud-sync.js +152 -3
  89. package/dist/daemon/connection.d.ts +28 -0
  90. package/dist/daemon/connection.js +113 -22
  91. package/dist/daemon/consensus-integration.d.ts +167 -0
  92. package/dist/daemon/consensus-integration.js +371 -0
  93. package/dist/daemon/consensus.d.ts +271 -0
  94. package/dist/daemon/consensus.js +632 -0
  95. package/dist/daemon/delivery-tracker.d.ts +34 -0
  96. package/dist/daemon/delivery-tracker.js +104 -0
  97. package/dist/daemon/enhanced-features.d.ts +118 -0
  98. package/dist/daemon/enhanced-features.js +178 -0
  99. package/dist/daemon/index.d.ts +4 -0
  100. package/dist/daemon/index.js +5 -0
  101. package/dist/daemon/rate-limiter.d.ts +68 -0
  102. package/dist/daemon/rate-limiter.js +130 -0
  103. package/dist/daemon/router.d.ts +18 -11
  104. package/dist/daemon/router.js +57 -113
  105. package/dist/daemon/server.d.ts +13 -1
  106. package/dist/daemon/server.js +71 -9
  107. package/dist/daemon/sync-queue.d.ts +116 -0
  108. package/dist/daemon/sync-queue.js +361 -0
  109. package/dist/dashboard/out/404.html +1 -1
  110. package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
  111. package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  112. package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
  113. package/dist/dashboard/out/_next/static/chunks/app/app/{page-c617745b81344f4f.js → page-7f64824ae7d06707.js} +1 -1
  114. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
  115. package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
  116. package/dist/dashboard/out/_next/static/chunks/app/{page-dc786c183425c2ac.js → page-814efc4d77b4191d.js} +1 -1
  117. package/dist/dashboard/out/_next/static/chunks/{main-2ee6beb2ae96d210.js → main-5a40a5ae29646e1b.js} +1 -1
  118. package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
  119. package/dist/dashboard/out/app/onboarding.html +1 -1
  120. package/dist/dashboard/out/app/onboarding.txt +1 -1
  121. package/dist/dashboard/out/app.html +1 -1
  122. package/dist/dashboard/out/app.txt +2 -2
  123. package/dist/dashboard/out/cloud/link.html +1 -0
  124. package/dist/dashboard/out/cloud/link.txt +7 -0
  125. package/dist/dashboard/out/connect-repos.html +1 -1
  126. package/dist/dashboard/out/connect-repos.txt +1 -1
  127. package/dist/dashboard/out/history.html +1 -1
  128. package/dist/dashboard/out/history.txt +2 -2
  129. package/dist/dashboard/out/index.html +1 -1
  130. package/dist/dashboard/out/index.txt +2 -2
  131. package/dist/dashboard/out/login.html +2 -3
  132. package/dist/dashboard/out/login.txt +2 -2
  133. package/dist/dashboard/out/metrics.html +1 -1
  134. package/dist/dashboard/out/metrics.txt +2 -2
  135. package/dist/dashboard/out/pricing.html +2 -2
  136. package/dist/dashboard/out/pricing.txt +1 -1
  137. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  138. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  139. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  140. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  141. package/dist/dashboard/out/providers.html +1 -1
  142. package/dist/dashboard/out/providers.txt +1 -1
  143. package/dist/dashboard/out/signup.html +2 -2
  144. package/dist/dashboard/out/signup.txt +1 -1
  145. package/dist/dashboard-server/server.js +244 -28
  146. package/dist/health-worker-manager.d.ts +62 -0
  147. package/dist/health-worker-manager.js +144 -0
  148. package/dist/health-worker.d.ts +9 -0
  149. package/dist/health-worker.js +79 -0
  150. package/dist/index.d.ts +2 -1
  151. package/dist/index.js +5 -1
  152. package/dist/memory/context-compaction.d.ts +156 -0
  153. package/dist/memory/context-compaction.js +453 -0
  154. package/dist/memory/index.d.ts +1 -0
  155. package/dist/memory/index.js +1 -0
  156. package/dist/protocol/channels.js +4 -4
  157. package/dist/protocol/framing.d.ts +72 -10
  158. package/dist/protocol/framing.js +194 -25
  159. package/dist/storage/adapter.d.ts +8 -1
  160. package/dist/storage/adapter.js +11 -0
  161. package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
  162. package/dist/storage/batched-sqlite-adapter.js +183 -0
  163. package/dist/storage/dead-letter-queue.d.ts +196 -0
  164. package/dist/storage/dead-letter-queue.js +427 -0
  165. package/dist/storage/dlq-adapter.d.ts +195 -0
  166. package/dist/storage/dlq-adapter.js +664 -0
  167. package/dist/trajectory/config.d.ts +32 -14
  168. package/dist/trajectory/config.js +38 -16
  169. package/dist/trajectory/integration.js +217 -64
  170. package/dist/utils/git-remote.d.ts +47 -0
  171. package/dist/utils/git-remote.js +125 -0
  172. package/dist/utils/id-generator.d.ts +35 -0
  173. package/dist/utils/id-generator.js +60 -0
  174. package/dist/utils/index.d.ts +1 -0
  175. package/dist/utils/index.js +1 -0
  176. package/dist/utils/precompiled-patterns.d.ts +110 -0
  177. package/dist/utils/precompiled-patterns.js +322 -0
  178. package/dist/wrapper/auth-detection.js +1 -1
  179. package/dist/wrapper/base-wrapper.d.ts +40 -0
  180. package/dist/wrapper/base-wrapper.js +60 -6
  181. package/dist/wrapper/client.d.ts +14 -4
  182. package/dist/wrapper/client.js +89 -31
  183. package/dist/wrapper/idle-detector.d.ts +102 -0
  184. package/dist/wrapper/idle-detector.js +279 -0
  185. package/dist/wrapper/parser.d.ts +4 -0
  186. package/dist/wrapper/parser.js +19 -1
  187. package/dist/wrapper/pty-wrapper.d.ts +14 -2
  188. package/dist/wrapper/pty-wrapper.js +132 -32
  189. package/dist/wrapper/shared.d.ts +1 -1
  190. package/dist/wrapper/shared.js +1 -1
  191. package/dist/wrapper/tmux-wrapper.d.ts +20 -2
  192. package/dist/wrapper/tmux-wrapper.js +163 -40
  193. package/package.json +3 -1
  194. package/scripts/run-migrations.js +43 -0
  195. package/scripts/verify-schema.js +134 -0
  196. package/tests/benchmarks/protocol.bench.ts +310 -0
  197. package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
  198. package/dist/dashboard/out/_next/static/chunks/899-fc02ed79e3de4302.js +0 -1
  199. package/dist/dashboard/out/_next/static/chunks/app/login/page-c22d080201cbd9fb.js +0 -1
  200. package/dist/dashboard/out/_next/static/css/48a8fbe3e659080e.css +0 -1
  201. /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_buildManifest.js +0 -0
  202. /package/dist/dashboard/out/_next/static/{sDcbGRTYLcpPvyTs_rsNb → R-uQOUcOLINtsp6ACeZa9}/_ssgManifest.js +0 -0
@@ -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)
@@ -225,11 +218,18 @@ export class TmuxWrapper extends BaseWrapper {
225
218
  if (!this.config.args || this.config.args.length === 0) {
226
219
  return this.config.command;
227
220
  }
228
- // Quote any argument that contains spaces, quotes, or special chars
221
+ // Quote any argument that contains spaces, quotes, or shell special chars
222
+ // Must handle: spaces, quotes, $, <, >, |, &, ;, (, ), `, etc.
229
223
  const quotedArgs = this.config.args.map(arg => {
230
- if (arg.includes(' ') || arg.includes('"') || arg.includes("'") || arg.includes('$')) {
231
- // Use double quotes and escape internal quotes
232
- return `"${arg.replace(/"/g, '\\"')}"`;
224
+ if (/[\s"'$<>|&;()`,!\\]/.test(arg)) {
225
+ // Use double quotes and escape internal quotes and special chars
226
+ const escaped = arg
227
+ .replace(/\\/g, '\\\\')
228
+ .replace(/"/g, '\\"')
229
+ .replace(/\$/g, '\\$')
230
+ .replace(/`/g, '\\`')
231
+ .replace(/!/g, '\\!');
232
+ return `"${escaped}"`;
233
233
  }
234
234
  return arg;
235
235
  });
@@ -337,9 +337,13 @@ export class TmuxWrapper extends BaseWrapper {
337
337
  // Wait for shell to be ready (look for prompt)
338
338
  await this.waitForShellReady();
339
339
  // Send the command to run
340
+ this.logStderr('Sending command to tmux...');
340
341
  await this.sendKeysLiteral(fullCommand);
341
- await sleep(100);
342
+ await sleep(300); // Give shell time to process the command literal
343
+ this.logStderr('Sending Enter...');
342
344
  await this.sendKeys('Enter');
345
+ await sleep(500); // Ensure Enter is processed and command starts before we continue
346
+ this.logStderr('Command sent');
343
347
  }
344
348
  catch (err) {
345
349
  throw new Error(`Failed to create tmux session: ${err.message}`);
@@ -353,10 +357,19 @@ export class TmuxWrapper extends BaseWrapper {
353
357
  this.initializeTrajectory();
354
358
  // Initialize continuity and get/create agentId
355
359
  this.initializeAgentId();
356
- // Inject instructions for the agent (after a delay to let CLI initialize)
357
- setTimeout(() => this.injectInstructions(), 3000);
358
360
  // Start background polling (silent - no stdout writes)
359
361
  this.startSilentPolling();
362
+ // Initialize idle detector with the tmux pane PID for process state inspection
363
+ this.initializeIdleDetectorPid();
364
+ // Wait for agent to be ready, then inject instructions
365
+ // This replaces the fixed 3-second delay with actual readiness detection
366
+ this.waitForAgentReady().then(() => {
367
+ this.injectInstructions();
368
+ }).catch(err => {
369
+ this.logStderr(`Failed to wait for agent ready: ${err.message}`, true);
370
+ // Fall back to injecting after a delay
371
+ setTimeout(() => this.injectInstructions(), 3000);
372
+ });
360
373
  // Attach user to tmux session
361
374
  // This takes over stdin/stdout - user sees the real terminal
362
375
  this.attachToSession();
@@ -409,12 +422,49 @@ export class TmuxWrapper extends BaseWrapper {
409
422
  this.logStderr(`Failed to initialize agent ID: ${err.message}`, true);
410
423
  }
411
424
  }
425
+ /**
426
+ * Initialize the idle detector with the tmux pane PID.
427
+ * This enables process state inspection on Linux for more reliable idle detection.
428
+ */
429
+ async initializeIdleDetectorPid() {
430
+ try {
431
+ const pid = await getTmuxPanePid(this.tmuxPath, this.sessionName);
432
+ if (pid) {
433
+ this.setIdleDetectorPid(pid);
434
+ this.logStderr(`Idle detector initialized with PID ${pid}`);
435
+ }
436
+ else {
437
+ this.logStderr('Could not get pane PID for idle detection (will use output analysis)');
438
+ }
439
+ }
440
+ catch (err) {
441
+ this.logStderr(`Failed to initialize idle detector PID: ${err.message}`);
442
+ }
443
+ }
444
+ /**
445
+ * Wait for the agent to be ready for input.
446
+ * Uses idle detection instead of a fixed delay.
447
+ */
448
+ async waitForAgentReady() {
449
+ // Minimum wait to ensure the CLI process has started
450
+ await sleep(500);
451
+ // Wait for agent to become idle (CLI fully initialized)
452
+ const result = await this.waitForIdleState(10000, 200);
453
+ if (result.isIdle) {
454
+ this.logStderr(`Agent ready (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
455
+ }
456
+ else {
457
+ this.logStderr('Agent readiness timeout, proceeding anyway');
458
+ }
459
+ }
412
460
  /**
413
461
  * Inject usage instructions for the agent including persistence protocol
414
462
  */
415
463
  async injectInstructions() {
416
464
  if (!this.running)
417
465
  return;
466
+ if (this.config.skipInstructions)
467
+ return;
418
468
  // Use escaped prefix (\->relay:) in examples to prevent parser from treating them as real commands
419
469
  const escapedPrefix = '\\' + this.relayPrefix;
420
470
  // Build instructions including relay and trail
@@ -586,6 +636,9 @@ export class TmuxWrapper extends BaseWrapper {
586
636
  if (stdout.length !== this.processedOutputLength) {
587
637
  this.lastOutputTime = Date.now();
588
638
  this.markActivity();
639
+ // Feed new output to idle detector for more robust idle detection
640
+ const newOutput = stdout.substring(this.processedOutputLength);
641
+ this.feedIdleDetectorOutput(newOutput);
589
642
  this.processedOutputLength = stdout.length;
590
643
  // Stream new output to daemon for dashboard log viewing
591
644
  // Use filtered output to exclude thinking blocks and relay commands
@@ -713,8 +766,9 @@ export class TmuxWrapper extends BaseWrapper {
713
766
  */
714
767
  sendRelayCommand(cmd) {
715
768
  const msgHash = `${cmd.to}:${cmd.body}`;
716
- // Permanent dedup - never send the same message twice (silent)
769
+ // Permanent dedup - never send the same message twice
717
770
  if (this.sentMessageHashes.has(msgHash)) {
771
+ this.logStderr(`[DEDUP] Skipped duplicate message to ${cmd.to} (hash already sent)`);
718
772
  return;
719
773
  }
720
774
  // If client not ready, queue for later and return
@@ -950,20 +1004,23 @@ export class TmuxWrapper extends BaseWrapper {
950
1004
  });
951
1005
  }
952
1006
  /**
953
- * Execute spawn via API (if dashboardPort set) or callback
1007
+ * Execute spawn via API (if dashboardPort set) or callback.
1008
+ * After spawning, waits for the agent to come online and sends the task via relay.
954
1009
  */
955
1010
  async executeSpawn(name, cli, task) {
1011
+ let spawned = false;
956
1012
  if (this.config.dashboardPort) {
957
1013
  // Use dashboard API for spawning (works from any context, no terminal required)
958
1014
  try {
959
1015
  const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawn`, {
960
1016
  method: 'POST',
961
1017
  headers: { 'Content-Type': 'application/json' },
962
- body: JSON.stringify({ name, cli, task }),
1018
+ body: JSON.stringify({ name, cli }), // No task - we send it after agent is online
963
1019
  });
964
1020
  const result = await response.json();
965
1021
  if (result.success) {
966
1022
  this.logStderr(`Spawned ${name} via API`);
1023
+ spawned = true;
967
1024
  }
968
1025
  else {
969
1026
  this.logStderr(`Spawn failed: ${result.error}`, true);
@@ -977,11 +1034,57 @@ export class TmuxWrapper extends BaseWrapper {
977
1034
  // Fall back to callback
978
1035
  try {
979
1036
  await this.config.onSpawn(name, cli, task);
1037
+ spawned = true;
980
1038
  }
981
1039
  catch (err) {
982
1040
  this.logStderr(`Spawn failed: ${err.message}`, true);
983
1041
  }
984
1042
  }
1043
+ // If spawn succeeded and we have a task, wait for agent to come online and send it
1044
+ if (spawned && task && task.trim() && this.config.dashboardPort) {
1045
+ await this.waitAndSendTask(name, task);
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Wait for a spawned agent to come online, then send the task via relay.
1050
+ * Uses the wrapper's own relay client so the message comes "from" this agent,
1051
+ * not from the dashboard's relay client.
1052
+ */
1053
+ async waitAndSendTask(agentName, task) {
1054
+ const maxWaitMs = 30000;
1055
+ const pollIntervalMs = 500;
1056
+ const startTime = Date.now();
1057
+ this.logStderr(`Waiting for ${agentName} to come online...`);
1058
+ // Poll for agent to be online using dedicated status endpoint
1059
+ while (Date.now() - startTime < maxWaitMs) {
1060
+ try {
1061
+ const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/agents/${encodeURIComponent(agentName)}/online`);
1062
+ const data = await response.json();
1063
+ if (data.online) {
1064
+ this.logStderr(`${agentName} is online, sending task...`);
1065
+ // Send task directly via our relay client (not dashboard API)
1066
+ // This ensures the message comes "from" this agent, not from _DashboardUI
1067
+ if (this.client.state === 'READY') {
1068
+ const sent = this.client.sendMessage(agentName, task, 'message');
1069
+ if (sent) {
1070
+ this.logStderr(`Task sent to ${agentName}`);
1071
+ }
1072
+ else {
1073
+ this.logStderr(`Failed to send task to ${agentName}: sendMessage returned false`, true);
1074
+ }
1075
+ }
1076
+ else {
1077
+ this.logStderr(`Failed to send task to ${agentName}: relay client not ready (state: ${this.client.state})`, true);
1078
+ }
1079
+ return;
1080
+ }
1081
+ }
1082
+ catch (err) {
1083
+ // Ignore poll errors, keep trying
1084
+ }
1085
+ await sleep(pollIntervalMs);
1086
+ }
1087
+ this.logStderr(`Timeout waiting for ${agentName} to come online`, true);
985
1088
  }
986
1089
  /**
987
1090
  * Execute release via API (if dashboardPort set) or callback
@@ -1028,6 +1131,10 @@ export class TmuxWrapper extends BaseWrapper {
1028
1131
  // Only process if we have API or callbacks configured
1029
1132
  const canSpawn = this.config.dashboardPort || this.config.onSpawn;
1030
1133
  const canRelease = this.config.dashboardPort || this.config.onRelease;
1134
+ // Debug: Log spawn capability status
1135
+ if (content.includes('->relay:spawn')) {
1136
+ this.logStderr(`[spawn-debug] canSpawn=${!!canSpawn} dashboardPort=${this.config.dashboardPort} hasOnSpawn=${!!this.config.onSpawn}`);
1137
+ }
1031
1138
  if (!canSpawn && !canRelease)
1032
1139
  return;
1033
1140
  const lines = content.split('\n');
@@ -1038,6 +1145,12 @@ export class TmuxWrapper extends BaseWrapper {
1038
1145
  let trimmed = line.trim();
1039
1146
  // Strip common line prefixes (bullets, prompts) before checking for commands
1040
1147
  trimmed = trimmed.replace(linePrefixPattern, '');
1148
+ // Fix for over-stripping: the linePrefixPattern includes - and > characters,
1149
+ // which can accidentally strip the -> from ->relay:spawn, leaving just relay:spawn.
1150
+ // If we detect this happened, restore the -> prefix.
1151
+ if (/^(relay|thinking|continuity):/.test(trimmed)) {
1152
+ trimmed = '->' + trimmed;
1153
+ }
1041
1154
  // If we're in fenced spawn mode, accumulate lines until we see >>>
1042
1155
  if (this.pendingFencedSpawn) {
1043
1156
  // Check for fence close (>>> at end of line or on its own line)
@@ -1177,7 +1290,8 @@ export class TmuxWrapper extends BaseWrapper {
1177
1290
  this.checkForInjectionOpportunity();
1178
1291
  }
1179
1292
  /**
1180
- * Check if we should inject a message
1293
+ * Check if we should inject a message.
1294
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
1181
1295
  */
1182
1296
  checkForInjectionOpportunity() {
1183
1297
  if (this.messageQueue.length === 0)
@@ -1186,9 +1300,10 @@ export class TmuxWrapper extends BaseWrapper {
1186
1300
  return;
1187
1301
  if (!this.running)
1188
1302
  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)) {
1303
+ // Use universal idle detector for more reliable detection (inherited from BaseWrapper)
1304
+ const idleResult = this.checkIdleForInjection();
1305
+ if (!idleResult.isIdle) {
1306
+ // Not idle yet, retry later
1192
1307
  const retryMs = this.config.injectRetryMs ?? 500;
1193
1308
  setTimeout(() => this.checkForInjectionOpportunity(), retryMs);
1194
1309
  return;
@@ -1204,19 +1319,16 @@ export class TmuxWrapper extends BaseWrapper {
1204
1319
  if (!msg)
1205
1320
  return;
1206
1321
  this.isInjecting = true;
1207
- this.logStderr(`Injecting message from ${msg.from} (cli: ${this.cliType})`);
1208
1322
  try {
1209
1323
  const shortId = msg.messageId.substring(0, 8);
1210
1324
  // Wait for input to be clear before injecting
1211
1325
  // If input is not clear (human typing), re-queue and try later - never clear forcefully!
1212
- // Fix for agent-relay-j9z: forceful clearing destroys human input in progress
1213
1326
  const waitTimeoutMs = this.config.inputWaitTimeoutMs ?? 5000;
1214
1327
  const waitPollMs = this.config.inputWaitPollMs ?? 200;
1215
1328
  const inputClear = await this.waitForClearInput(waitTimeoutMs, waitPollMs);
1216
1329
  if (!inputClear) {
1217
1330
  // Input still has text after timeout - DON'T clear forcefully, re-queue instead
1218
- // This preserves any human input in progress
1219
- this.logStderr('Input not clear after waiting, re-queuing injection to preserve human input');
1331
+ this.logStderr('Input not clear, re-queuing injection');
1220
1332
  this.messageQueue.unshift(msg);
1221
1333
  this.isInjecting = false;
1222
1334
  setTimeout(() => this.checkForInjectionOpportunity(), this.config.injectRetryMs ?? 1000);
@@ -1268,7 +1380,9 @@ export class TmuxWrapper extends BaseWrapper {
1268
1380
  }
1269
1381
  },
1270
1382
  performInjection: async (inj) => {
1271
- await this.pasteLiteral(inj);
1383
+ // Use send-keys -l (literal) instead of paste-buffer
1384
+ // paste-buffer causes issues where Claude shows "[Pasted text]" but content doesn't appear
1385
+ await this.sendKeysLiteral(inj);
1272
1386
  await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
1273
1387
  await this.sendKeys('Enter');
1274
1388
  },
@@ -1319,7 +1433,15 @@ export class TmuxWrapper extends BaseWrapper {
1319
1433
  * Send special keys to tmux
1320
1434
  */
1321
1435
  async sendKeys(keys) {
1322
- await execAsync(`"${this.tmuxPath}" send-keys -t ${this.sessionName} ${keys}`);
1436
+ const cmd = `"${this.tmuxPath}" send-keys -t ${this.sessionName} ${keys}`;
1437
+ try {
1438
+ await execAsync(cmd);
1439
+ this.logStderr(`[sendKeys] Sent: ${keys}`);
1440
+ }
1441
+ catch (err) {
1442
+ this.logStderr(`[sendKeys] Failed to send ${keys}: ${err.message}`, true);
1443
+ throw err;
1444
+ }
1323
1445
  }
1324
1446
  /**
1325
1447
  * Send literal text to tmux
@@ -1334,7 +1456,14 @@ export class TmuxWrapper extends BaseWrapper {
1334
1456
  .replace(/\$/g, '\\$')
1335
1457
  .replace(/`/g, '\\`')
1336
1458
  .replace(/!/g, '\\!');
1337
- await execAsync(`"${this.tmuxPath}" send-keys -t ${this.sessionName} -l "${escaped}"`);
1459
+ try {
1460
+ await execAsync(`"${this.tmuxPath}" send-keys -t ${this.sessionName} -l "${escaped}"`);
1461
+ this.logStderr(`[sendKeysLiteral] Sent ${text.length} chars`);
1462
+ }
1463
+ catch (err) {
1464
+ this.logStderr(`[sendKeysLiteral] Failed: ${err.message}`, true);
1465
+ throw err;
1466
+ }
1338
1467
  }
1339
1468
  /**
1340
1469
  * Paste text using tmux buffer with optional bracketed paste to avoid interleaving with ongoing output.
@@ -1350,15 +1479,9 @@ export class TmuxWrapper extends BaseWrapper {
1350
1479
  .replace(/`/g, '\\`')
1351
1480
  .replace(/!/g, '\\!');
1352
1481
  // Set tmux buffer then paste
1353
- // Skip bracketed paste (-p) for CLIs that don't handle it properly (droid, other)
1354
- await execAsync(`"${this.tmuxPath}" set-buffer -- "${escaped}"`);
1355
- const useBracketedPaste = this.cliType === 'claude' || this.cliType === 'codex' || this.cliType === 'gemini' || this.cliType === 'opencode';
1356
- if (useBracketedPaste) {
1357
- await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName} -p`);
1358
- }
1359
- else {
1360
- await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName}`);
1361
- }
1482
+ const setBufferCmd = `"${this.tmuxPath}" set-buffer -- "${escaped}"`;
1483
+ await execAsync(setBufferCmd);
1484
+ await execAsync(`"${this.tmuxPath}" paste-buffer -t ${this.sessionName}`);
1362
1485
  }
1363
1486
  /**
1364
1487
  * Reset session-specific state for wrapper reuse.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Real-time agent-to-agent communication system",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,6 +41,8 @@
41
41
  "clean": "rm -rf dist",
42
42
  "db:generate": "drizzle-kit generate",
43
43
  "db:migrate": "drizzle-kit migrate",
44
+ "db:migrate:run": "node scripts/run-migrations.js",
45
+ "db:migrate:verify": "node scripts/verify-schema.js",
44
46
  "db:push": "drizzle-kit push",
45
47
  "db:studio": "drizzle-kit studio",
46
48
  "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();