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
@@ -184,6 +184,11 @@ export class PtyWrapper extends BaseWrapper {
184
184
  }
185
185
  this.running = true;
186
186
  this.sessionStartTime = Date.now();
187
+ // Set PID for idle detector (enables process state inspection on Linux)
188
+ if (this.ptyProcess.pid) {
189
+ this.setIdleDetectorPid(this.ptyProcess.pid);
190
+ console.log(`[pty:${this.config.name}] Idle detector initialized with PID ${this.ptyProcess.pid}`);
191
+ }
187
192
  // Skip hooks and continuity in interactive mode - user handles all prompts directly
188
193
  if (!this.config.interactive) {
189
194
  // Dispatch session start hook (handles trajectory initialization)
@@ -208,18 +213,40 @@ export class PtyWrapper extends BaseWrapper {
208
213
  this.config.onExit?.(exitCode);
209
214
  this.client.destroy();
210
215
  });
211
- // Inject initial instructions after a delay, then mark ready for messages
212
- // Skip in interactive mode - user handles all prompts directly
213
- setTimeout(() => {
216
+ // Wait for agent to be idle before injecting instructions
217
+ // This replaces the fixed 2-second delay with actual readiness detection
218
+ this.waitForAgentReady().then(() => {
214
219
  if (!this.config.interactive) {
215
220
  this.injectInstructions();
216
221
  }
217
222
  this.readyForMessages = true;
223
+ console.log(`[pty:${this.config.name}] Agent ready for messages (queueLen=${this.messageQueue.length}, interactive=${this.config.interactive})`);
218
224
  // Process any messages that arrived while waiting (skip in interactive mode)
219
225
  if (!this.config.interactive) {
220
226
  this.processMessageQueue();
221
227
  }
222
- }, 2000);
228
+ }).catch(err => {
229
+ console.error(`[pty:${this.config.name}] Failed to wait for agent ready:`, err);
230
+ // Fall back to marking ready anyway to avoid blocking forever
231
+ this.readyForMessages = true;
232
+ console.log(`[pty:${this.config.name}] Agent ready for messages (fallback, queueLen=${this.messageQueue.length})`);
233
+ });
234
+ }
235
+ /**
236
+ * Wait for the agent to be ready for input.
237
+ * Uses idle detection instead of a fixed delay.
238
+ */
239
+ async waitForAgentReady() {
240
+ // Minimum wait to ensure the CLI process has started
241
+ await sleep(500);
242
+ // Wait for agent to become idle (CLI fully initialized)
243
+ const result = await this.waitForIdleState(10000, 200);
244
+ if (result.isIdle) {
245
+ console.log(`[pty:${this.config.name}] Agent ready (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
246
+ }
247
+ else {
248
+ console.warn(`[pty:${this.config.name}] Agent readiness timeout, proceeding anyway`);
249
+ }
223
250
  }
224
251
  // Note: initializeAgentId() and getAgentId() are inherited from BaseWrapper
225
252
  /**
@@ -322,6 +349,8 @@ export class PtyWrapper extends BaseWrapper {
322
349
  handleOutput(data) {
323
350
  // Track output timing for stability checks
324
351
  this.lastOutputTime = Date.now();
352
+ // Feed output to idle detector for robust idle detection
353
+ this.feedIdleDetectorOutput(data);
325
354
  // Append to raw buffer
326
355
  this.rawBuffer += data;
327
356
  // Write to log file if available
@@ -581,6 +610,14 @@ export class PtyWrapper extends BaseWrapper {
581
610
  // 500 chars is enough to capture most relay message headers
582
611
  const lookbackStart = Math.max(0, this.lastParsedLength - 500);
583
612
  const contentToParse = cleanContent.substring(lookbackStart);
613
+ // Debug: Check if content contains relay pattern
614
+ if (contentToParse.includes('->relay:')) {
615
+ const relayLines = contentToParse.split('\n').filter(l => l.includes('->relay:'));
616
+ console.log(`[pty:${this.config.name}] [RELAY-DEBUG] Found ${relayLines.length} lines with ->relay: pattern`);
617
+ relayLines.slice(0, 3).forEach((line, i) => {
618
+ console.log(`[pty:${this.config.name}] [RELAY-DEBUG] Line ${i}: "${line.substring(0, 80)}..."`);
619
+ });
620
+ }
584
621
  // First, try to find fenced multi-line messages: ->relay:Target <<<\n...\n>>>
585
622
  this.parseFencedMessages(contentToParse);
586
623
  // Then parse single-line messages
@@ -610,6 +647,7 @@ export class PtyWrapper extends BaseWrapper {
610
647
  }
611
648
  // Skip placeholder targets (documentation examples like "AgentName", "Lead", etc.)
612
649
  if (isPlaceholderTarget(target)) {
650
+ console.error(`[pty-wrapper] Filtered fenced message - placeholder target: ${target}`);
613
651
  continue;
614
652
  }
615
653
  // Find the closing >>>
@@ -630,6 +668,7 @@ export class PtyWrapper extends BaseWrapper {
630
668
  }
631
669
  // Skip placeholder targets after parsing cross-project syntax
632
670
  if (isPlaceholderTarget(to)) {
671
+ console.error(`[pty-wrapper] Filtered fenced message - placeholder target after cross-project parse: ${to}`);
633
672
  continue;
634
673
  }
635
674
  this.sendRelayCommand({
@@ -676,8 +715,10 @@ export class PtyWrapper extends BaseWrapper {
676
715
  if (!body)
677
716
  continue;
678
717
  // Skip placeholder targets (documentation examples)
679
- if (isPlaceholderTarget(target))
718
+ if (isPlaceholderTarget(target)) {
719
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target: ${target}`);
680
720
  continue;
721
+ }
681
722
  // Parse target for cross-project syntax
682
723
  const colonIdx = target.indexOf(':');
683
724
  let to = target;
@@ -687,8 +728,10 @@ export class PtyWrapper extends BaseWrapper {
687
728
  to = target.substring(colonIdx + 1);
688
729
  }
689
730
  // Skip placeholder targets after parsing cross-project syntax
690
- if (isPlaceholderTarget(to))
731
+ if (isPlaceholderTarget(to)) {
732
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target after cross-project parse: ${to}`);
691
733
  continue;
734
+ }
692
735
  this.sendRelayCommand({
693
736
  to,
694
737
  kind: 'message',
@@ -702,8 +745,10 @@ export class PtyWrapper extends BaseWrapper {
702
745
  if (!body)
703
746
  continue;
704
747
  // Skip placeholder targets (documentation examples)
705
- if (isPlaceholderTarget(target))
748
+ if (isPlaceholderTarget(target)) {
749
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target: ${target}`);
706
750
  continue;
751
+ }
707
752
  // Parse target for cross-project syntax
708
753
  const colonIdx = target.indexOf(':');
709
754
  let to = target;
@@ -713,8 +758,10 @@ export class PtyWrapper extends BaseWrapper {
713
758
  to = target.substring(colonIdx + 1);
714
759
  }
715
760
  // Skip placeholder targets after parsing cross-project syntax
716
- if (isPlaceholderTarget(to))
761
+ if (isPlaceholderTarget(to)) {
762
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target after cross-project parse: ${to}`);
717
763
  continue;
764
+ }
718
765
  this.sendRelayCommand({
719
766
  to,
720
767
  kind: 'message',
@@ -948,22 +995,24 @@ export class PtyWrapper extends BaseWrapper {
948
995
  }
949
996
  /**
950
997
  * Execute spawn via API or callback.
951
- * Overrides BaseWrapper to add PTY-specific logging and API path.
998
+ * After spawning, waits for the agent to come online and sends the task via relay.
952
999
  */
953
1000
  async executeSpawn(name, cli, task) {
954
1001
  console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] executeSpawn called: name=${name}, cli=${cli}, task="${task.substring(0, 50)}..."`);
955
1002
  console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] dashboardPort=${this.config.dashboardPort}, hasOnSpawn=${!!this.config.onSpawn}`);
1003
+ let spawned = false;
956
1004
  if (this.config.dashboardPort) {
957
1005
  // Use dashboard API for spawning (works from spawned agents)
958
1006
  try {
959
1007
  const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawn`, {
960
1008
  method: 'POST',
961
1009
  headers: { 'Content-Type': 'application/json' },
962
- body: JSON.stringify({ name, cli, task }),
1010
+ body: JSON.stringify({ name, cli }), // No task - we send it after agent is online
963
1011
  });
964
1012
  const result = await response.json();
965
1013
  if (result.success) {
966
1014
  console.log(`[pty:${this.config.name}] Spawned ${name} via API`);
1015
+ spawned = true;
967
1016
  }
968
1017
  else {
969
1018
  console.error(`[pty:${this.config.name}] Spawn failed: ${result.error}`);
@@ -977,11 +1026,57 @@ export class PtyWrapper extends BaseWrapper {
977
1026
  // Fall back to callback
978
1027
  try {
979
1028
  await this.config.onSpawn(name, cli, task);
1029
+ spawned = true;
980
1030
  }
981
1031
  catch (err) {
982
1032
  console.error(`[pty:${this.config.name}] Spawn failed: ${err.message}`);
983
1033
  }
984
1034
  }
1035
+ // If spawn succeeded and we have a task, wait for agent to come online and send it
1036
+ if (spawned && task && task.trim() && this.config.dashboardPort) {
1037
+ await this.waitAndSendTask(name, task);
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Wait for a spawned agent to come online, then send the task via relay.
1042
+ * Uses the wrapper's own relay client so the message comes "from" this agent,
1043
+ * not from the dashboard's relay client.
1044
+ */
1045
+ async waitAndSendTask(agentName, task) {
1046
+ const maxWaitMs = 30000;
1047
+ const pollIntervalMs = 500;
1048
+ const startTime = Date.now();
1049
+ console.log(`[pty:${this.config.name}] Waiting for ${agentName} to come online...`);
1050
+ // Poll for agent to be online using dedicated status endpoint
1051
+ while (Date.now() - startTime < maxWaitMs) {
1052
+ try {
1053
+ const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/agents/${encodeURIComponent(agentName)}/online`);
1054
+ const data = await response.json();
1055
+ if (data.online) {
1056
+ console.log(`[pty:${this.config.name}] ${agentName} is online, sending task...`);
1057
+ // Send task directly via our relay client (not dashboard API)
1058
+ // This ensures the message comes "from" this agent, not from _DashboardUI
1059
+ if (this.client.state === 'READY') {
1060
+ const sent = this.client.sendMessage(agentName, task, 'message');
1061
+ if (sent) {
1062
+ console.log(`[pty:${this.config.name}] Task sent to ${agentName}`);
1063
+ }
1064
+ else {
1065
+ console.error(`[pty:${this.config.name}] Failed to send task to ${agentName}: sendMessage returned false`);
1066
+ }
1067
+ }
1068
+ else {
1069
+ console.error(`[pty:${this.config.name}] Failed to send task to ${agentName}: relay client not ready (state: ${this.client.state})`);
1070
+ }
1071
+ return;
1072
+ }
1073
+ }
1074
+ catch (err) {
1075
+ // Ignore poll errors, keep trying
1076
+ }
1077
+ await sleep(pollIntervalMs);
1078
+ }
1079
+ console.error(`[pty:${this.config.name}] Timeout waiting for ${agentName} to come online`);
985
1080
  }
986
1081
  /**
987
1082
  * Execute release via API or callback.
@@ -1021,6 +1116,8 @@ export class PtyWrapper extends BaseWrapper {
1021
1116
  * Extends BaseWrapper to add PTY-specific behavior.
1022
1117
  */
1023
1118
  handleIncomingMessage(from, payload, messageId, meta, originalTo) {
1119
+ const bodyPreview = payload.body.substring(0, 50).replace(/\n/g, '\\n');
1120
+ console.log(`[pty:${this.config.name}] Message received from ${from}: "${bodyPreview}..." (readyForMessages=${this.readyForMessages}, queueLen=${this.messageQueue.length})`);
1024
1121
  // Call base class to handle deduplication and queuing
1025
1122
  super.handleIncomingMessage(from, payload, messageId, meta, originalTo);
1026
1123
  // PTY-specific: Process the message queue immediately
@@ -1032,27 +1129,14 @@ export class PtyWrapper extends BaseWrapper {
1032
1129
  }
1033
1130
  /**
1034
1131
  * Wait for output to stabilize before injection.
1035
- * Returns true if output has been stable for the required duration.
1132
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
1133
+ * Returns true if agent is idle and ready for input.
1036
1134
  */
1037
1135
  async waitForOutputStable() {
1038
- const startTime = Date.now();
1039
- let stablePolls = 0;
1040
- let lastBufferLength = this.rawBuffer.length;
1041
- while (Date.now() - startTime < INJECTION_CONSTANTS.STABILITY_TIMEOUT_MS) {
1042
- await sleep(INJECTION_CONSTANTS.STABILITY_POLL_MS);
1043
- const timeSinceOutput = Date.now() - this.lastOutputTime;
1044
- const bufferUnchanged = this.rawBuffer.length === lastBufferLength;
1045
- // Consider stable if no output for at least one poll interval
1046
- if (timeSinceOutput >= INJECTION_CONSTANTS.STABILITY_POLL_MS && bufferUnchanged) {
1047
- stablePolls++;
1048
- if (stablePolls >= INJECTION_CONSTANTS.REQUIRED_STABLE_POLLS) {
1049
- return true;
1050
- }
1051
- }
1052
- else {
1053
- stablePolls = 0;
1054
- lastBufferLength = this.rawBuffer.length;
1055
- }
1136
+ const result = await this.waitForIdleState(INJECTION_CONSTANTS.STABILITY_TIMEOUT_MS, INJECTION_CONSTANTS.STABILITY_POLL_MS);
1137
+ if (result.isIdle) {
1138
+ console.log(`[pty:${this.config.name}] Idle detected (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
1139
+ return true;
1056
1140
  }
1057
1141
  // Timeout - return true anyway to avoid blocking forever
1058
1142
  console.warn(`[pty:${this.config.name}] Stability timeout, proceeding with injection`);
@@ -1090,6 +1174,8 @@ export class PtyWrapper extends BaseWrapper {
1090
1174
  this.isInjecting = false;
1091
1175
  return;
1092
1176
  }
1177
+ const bodyPreview = msg.body.substring(0, 50).replace(/\n/g, '\\n');
1178
+ console.log(`[pty:${this.config.name}] Processing message from ${msg.from}: "${bodyPreview}..." (remaining=${this.messageQueue.length})`);
1093
1179
  try {
1094
1180
  // Wait for output to stabilize before injecting
1095
1181
  await this.waitForOutputStable();
@@ -1128,9 +1214,21 @@ export class PtyWrapper extends BaseWrapper {
1128
1214
  if (!this.ptyProcess || !this.running) {
1129
1215
  throw new Error('PTY process not running');
1130
1216
  }
1131
- // Write message to PTY, then send Enter separately after a small delay
1132
- this.ptyProcess.write(inj);
1133
- await sleep(INJECTION_CONSTANTS.ENTER_DELAY_MS);
1217
+ // Use bracketed paste mode for CLIs that support it (claude, codex, gemini)
1218
+ // This prevents interleaving with CLI output and ensures clean input
1219
+ const useBracketedPaste = this.cliType === 'claude' || this.cliType === 'codex' || this.cliType === 'gemini';
1220
+ if (useBracketedPaste) {
1221
+ // Bracketed paste: \x1b[200~ starts paste, \x1b[201~ ends paste
1222
+ this.ptyProcess.write('\x1b[200~' + inj + '\x1b[201~');
1223
+ }
1224
+ else {
1225
+ this.ptyProcess.write(inj);
1226
+ }
1227
+ // Wait longer for CLI to process the pasted content before sending Enter.
1228
+ // The standard 50ms delay is too short for CLIs like Claude that need time
1229
+ // to process bracketed paste content before accepting Enter.
1230
+ await sleep(200);
1231
+ // Send Enter key - use \r for PTY (carriage return)
1134
1232
  this.ptyProcess.write('\r');
1135
1233
  },
1136
1234
  log: (message) => console.log(`[pty:${this.config.name}] ${message}`),
@@ -1175,6 +1273,8 @@ export class PtyWrapper extends BaseWrapper {
1175
1273
  injectInstructions() {
1176
1274
  if (!this.running)
1177
1275
  return;
1276
+ if (this.config.skipInstructions)
1277
+ return;
1178
1278
  // Guard: Only inject once per session
1179
1279
  if (this.instructionsInjected) {
1180
1280
  console.log(`[pty:${this.config.name}] Init instructions already injected, skipping`);
@@ -53,7 +53,7 @@ export declare const INJECTION_CONSTANTS: {
53
53
  /** Timeout for injection verification (ms) */
54
54
  readonly VERIFICATION_TIMEOUT_MS: 2000;
55
55
  /** Delay between message and Enter key (ms) */
56
- readonly ENTER_DELAY_MS: 50;
56
+ readonly ENTER_DELAY_MS: 100;
57
57
  /** Backoff multiplier for retries (ms per attempt) */
58
58
  readonly RETRY_BACKOFF_MS: 300;
59
59
  /** Delay between processing queued messages (ms) */
@@ -19,7 +19,7 @@ export const INJECTION_CONSTANTS = {
19
19
  /** Timeout for injection verification (ms) */
20
20
  VERIFICATION_TIMEOUT_MS: 2000,
21
21
  /** Delay between message and Enter key (ms) */
22
- ENTER_DELAY_MS: 50,
22
+ ENTER_DELAY_MS: 100,
23
23
  /** Backoff multiplier for retries (ms per attempt) */
24
24
  RETRY_BACKOFF_MS: 300,
25
25
  /** Delay between processing queued messages (ms) */
@@ -127,6 +127,16 @@ export declare class TmuxWrapper extends BaseWrapper {
127
127
  * Initialize agent ID for continuity/resume functionality (uses logStderr for tmux)
128
128
  */
129
129
  protected initializeAgentId(): Promise<void>;
130
+ /**
131
+ * Initialize the idle detector with the tmux pane PID.
132
+ * This enables process state inspection on Linux for more reliable idle detection.
133
+ */
134
+ private initializeIdleDetectorPid;
135
+ /**
136
+ * Wait for the agent to be ready for input.
137
+ * Uses idle detection instead of a fixed delay.
138
+ */
139
+ private waitForAgentReady;
130
140
  /**
131
141
  * Inject usage instructions for the agent including persistence protocol
132
142
  */
@@ -222,9 +232,16 @@ export declare class TmuxWrapper extends BaseWrapper {
222
232
  */
223
233
  private parseSessionEndAndClose;
224
234
  /**
225
- * Execute spawn via API (if dashboardPort set) or callback
235
+ * Execute spawn via API (if dashboardPort set) or callback.
236
+ * After spawning, waits for the agent to come online and sends the task via relay.
226
237
  */
227
238
  protected executeSpawn(name: string, cli: string, task: string): Promise<void>;
239
+ /**
240
+ * Wait for a spawned agent to come online, then send the task via relay.
241
+ * Uses the wrapper's own relay client so the message comes "from" this agent,
242
+ * not from the dashboard's relay client.
243
+ */
244
+ private waitAndSendTask;
228
245
  /**
229
246
  * Execute release via API (if dashboardPort set) or callback
230
247
  */
@@ -246,7 +263,8 @@ export declare class TmuxWrapper extends BaseWrapper {
246
263
  */
247
264
  protected handleIncomingMessage(from: string, payload: SendPayload, messageId: string, meta?: SendMeta, originalTo?: string): void;
248
265
  /**
249
- * Check if we should inject a message
266
+ * Check if we should inject a message.
267
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
250
268
  */
251
269
  private checkForInjectionOpportunity;
252
270
  /**