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
@@ -0,0 +1,279 @@
1
+ /**
2
+ * UniversalIdleDetector - Detect when an agent is waiting for input
3
+ *
4
+ * Works across all CLI tools (Claude, Codex, Gemini, Aider, etc.) by combining:
5
+ * 1. Process state inspection via /proc/{pid}/stat (Linux, 95% confidence)
6
+ * 2. Output silence analysis (cross-platform, 60-80% confidence)
7
+ * 3. Natural ending detection (heuristic, 60% confidence)
8
+ *
9
+ * The hybrid approach ensures reliable idle detection regardless of CLI type.
10
+ */
11
+ import fs from 'node:fs';
12
+ const DEFAULT_CONFIG = {
13
+ minSilenceMs: 500,
14
+ bufferLimit: 10000,
15
+ confidenceThreshold: 0.7,
16
+ };
17
+ /**
18
+ * Universal idle detector for any CLI-based agent.
19
+ */
20
+ export class UniversalIdleDetector {
21
+ lastOutputTime = 0;
22
+ outputBuffer = '';
23
+ pid = null;
24
+ config;
25
+ constructor(config = {}) {
26
+ this.config = { ...DEFAULT_CONFIG, ...config };
27
+ // Initialize lastOutputTime to now to avoid immediate false positives
28
+ this.lastOutputTime = Date.now();
29
+ }
30
+ /**
31
+ * Set the PID of the agent process to monitor.
32
+ * Required for Linux process state inspection.
33
+ */
34
+ setPid(pid) {
35
+ this.pid = pid;
36
+ }
37
+ /**
38
+ * Get the current PID being monitored.
39
+ */
40
+ getPid() {
41
+ return this.pid;
42
+ }
43
+ /**
44
+ * Process output chunk from the agent.
45
+ * Call this for every output received from the agent process.
46
+ */
47
+ onOutput(chunk) {
48
+ this.lastOutputTime = Date.now();
49
+ this.outputBuffer += chunk;
50
+ // Keep buffer bounded
51
+ if (this.outputBuffer.length > this.config.bufferLimit) {
52
+ this.outputBuffer = this.outputBuffer.slice(-Math.floor(this.config.bufferLimit / 2));
53
+ }
54
+ }
55
+ /**
56
+ * Check if the agent process is blocked on read (waiting for input).
57
+ * This is the most reliable signal - the OS knows when a process is waiting.
58
+ *
59
+ * Linux-only; returns null on other platforms.
60
+ */
61
+ isProcessWaitingForInput() {
62
+ if (process.platform !== 'linux' || !this.pid) {
63
+ return null; // Can't determine on non-Linux
64
+ }
65
+ try {
66
+ // Check process state from /proc/{pid}/stat
67
+ // State codes: R=running, S=sleeping, D=disk sleep, Z=zombie, T=stopped
68
+ const statPath = `/proc/${this.pid}/stat`;
69
+ const stat = fs.readFileSync(statPath, 'utf-8');
70
+ const fields = stat.split(' ');
71
+ const state = fields[2]; // Third field is state
72
+ // S (sleeping) often means waiting for I/O
73
+ if (state !== 'S') {
74
+ return { waiting: false }; // Running or other state = not waiting
75
+ }
76
+ // More precise: check what the process is blocked on
77
+ const wchanPath = `/proc/${this.pid}/wchan`;
78
+ const wchan = fs.readFileSync(wchanPath, 'utf-8').trim();
79
+ // Common wait channels for terminal input
80
+ const inputWaitChannels = [
81
+ 'wait_woken',
82
+ 'poll_schedule_timeout',
83
+ 'do_select',
84
+ 'n_tty_read',
85
+ 'unix_stream_read_generic',
86
+ 'pipe_read',
87
+ 'ep_poll', // epoll wait
88
+ 'futex_wait_queue', // sometimes seen
89
+ ];
90
+ const isWaiting = inputWaitChannels.some(ch => wchan.includes(ch));
91
+ return { waiting: isWaiting, wchan };
92
+ }
93
+ catch {
94
+ return null; // Process may have exited or permission denied
95
+ }
96
+ }
97
+ /**
98
+ * Get milliseconds since last output.
99
+ */
100
+ getOutputSilenceMs() {
101
+ return Date.now() - this.lastOutputTime;
102
+ }
103
+ /**
104
+ * Check if the last output ends "naturally" (complete thought vs mid-sentence).
105
+ * Helps distinguish between pauses in output and waiting for input.
106
+ */
107
+ hasNaturalEnding() {
108
+ const lastChars = this.outputBuffer.slice(-100).trim();
109
+ if (lastChars.length === 0)
110
+ return true;
111
+ // Positive signals: output ended cleanly
112
+ const naturalEndings = [
113
+ /[.!?]\s*$/, // Sentence ended
114
+ /```\s*$/, // Code block closed
115
+ /\n\n$/, // Paragraph break
116
+ />\s*$/, // Prompt character
117
+ /\$\s*$/, // Shell prompt
118
+ />>>\s*$/, // Python/Aider prompt
119
+ /❯\s*$/, // Fancy prompts
120
+ /λ\s*$/, // Lambda prompts
121
+ /→\s*$/, // Arrow prompts
122
+ ];
123
+ // Negative signals: output mid-thought
124
+ const midThought = [
125
+ /[,;:]\s*$/, // Comma, semicolon = more coming
126
+ /\w$/, // Ended mid-word (no trailing space)
127
+ /[-–—]\s*$/, // Dash = continuation
128
+ /\(\s*$/, // Open paren
129
+ /\[\s*$/, // Open bracket
130
+ /\{\s*$/, // Open brace
131
+ /\\\s*$/, // Line continuation
132
+ ];
133
+ for (const pattern of midThought) {
134
+ if (pattern.test(lastChars)) {
135
+ return false;
136
+ }
137
+ }
138
+ for (const pattern of naturalEndings) {
139
+ if (pattern.test(lastChars)) {
140
+ return true;
141
+ }
142
+ }
143
+ // Default: assume natural if no negative signals and some silence
144
+ return true;
145
+ }
146
+ /**
147
+ * Determine if the agent is idle and ready for input.
148
+ * Combines multiple signals for reliability across all CLI types.
149
+ */
150
+ checkIdle(options = {}) {
151
+ const minSilence = options.minSilenceMs ?? this.config.minSilenceMs;
152
+ const signals = [];
153
+ // Signal 1: Process state (most reliable on Linux)
154
+ const processState = this.isProcessWaitingForInput();
155
+ if (processState !== null) {
156
+ if (processState.waiting) {
157
+ signals.push({
158
+ source: 'process_state',
159
+ confidence: 0.95, // Very high - OS-level truth
160
+ timestamp: Date.now(),
161
+ details: processState.wchan,
162
+ });
163
+ }
164
+ else {
165
+ // Process is actively running - definitely not idle
166
+ return {
167
+ isIdle: false,
168
+ confidence: 0.95,
169
+ signals: [{
170
+ source: 'process_state',
171
+ confidence: 0.95,
172
+ timestamp: Date.now(),
173
+ details: 'process running',
174
+ }],
175
+ };
176
+ }
177
+ }
178
+ // processState === null means we can't determine (non-Linux)
179
+ // Signal 2: Output silence
180
+ const silenceMs = this.getOutputSilenceMs();
181
+ if (silenceMs > minSilence) {
182
+ // Confidence scales with silence duration (up to 0.8)
183
+ // 500ms = 0.13, 1000ms = 0.27, 2000ms = 0.53, 3000ms = 0.8
184
+ const silenceConfidence = Math.min(silenceMs / 3000, 0.8);
185
+ signals.push({
186
+ source: 'output_silence',
187
+ confidence: silenceConfidence,
188
+ timestamp: Date.now(),
189
+ details: `${silenceMs}ms`,
190
+ });
191
+ }
192
+ // Signal 3: Natural ending (only if some silence)
193
+ if (silenceMs > 200 && this.hasNaturalEnding()) {
194
+ signals.push({
195
+ source: 'natural_ending',
196
+ confidence: 0.6,
197
+ timestamp: Date.now(),
198
+ });
199
+ }
200
+ // No signals = not idle
201
+ if (signals.length === 0) {
202
+ return { isIdle: false, confidence: 0, signals: [] };
203
+ }
204
+ // Combine signals
205
+ // Use max confidence, boosted if multiple signals agree
206
+ const maxConfidence = Math.max(...signals.map(s => s.confidence));
207
+ const boost = signals.length > 1 ? 0.1 : 0;
208
+ const combinedConfidence = Math.min(maxConfidence + boost, 1.0);
209
+ return {
210
+ isIdle: combinedConfidence >= this.config.confidenceThreshold,
211
+ confidence: combinedConfidence,
212
+ signals,
213
+ };
214
+ }
215
+ /**
216
+ * Wait for idle state with timeout.
217
+ * Returns the idle result when achieved or after timeout.
218
+ */
219
+ async waitForIdle(timeoutMs = 30000, pollMs = 200) {
220
+ const startTime = Date.now();
221
+ while (Date.now() - startTime < timeoutMs) {
222
+ const result = this.checkIdle();
223
+ if (result.isIdle) {
224
+ return result;
225
+ }
226
+ await new Promise(r => setTimeout(r, pollMs));
227
+ }
228
+ // Timeout - return current state
229
+ return this.checkIdle();
230
+ }
231
+ /**
232
+ * Reset state (call when agent starts new response).
233
+ */
234
+ reset() {
235
+ this.outputBuffer = '';
236
+ this.lastOutputTime = Date.now();
237
+ }
238
+ /**
239
+ * Get time since last output in milliseconds.
240
+ */
241
+ getTimeSinceLastOutput() {
242
+ return this.getOutputSilenceMs();
243
+ }
244
+ }
245
+ /**
246
+ * Get the PID of a process running in a tmux pane.
247
+ * Uses tmux list-panes with format specifier.
248
+ */
249
+ export async function getTmuxPanePid(tmuxPath, sessionName) {
250
+ const { exec } = await import('node:child_process');
251
+ const { promisify } = await import('node:util');
252
+ const execAsync = promisify(exec);
253
+ try {
254
+ // Get the PID of the command running in the pane
255
+ const { stdout } = await execAsync(`"${tmuxPath}" list-panes -t ${sessionName} -F "#{pane_pid}" 2>/dev/null`);
256
+ const pid = parseInt(stdout.trim(), 10);
257
+ return isNaN(pid) ? null : pid;
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ }
263
+ /**
264
+ * Create an idle detector configured for the current platform.
265
+ * Logs a warning on non-Linux platforms where process state inspection isn't available.
266
+ */
267
+ export function createIdleDetector(config = {}, options = {}) {
268
+ const detector = new UniversalIdleDetector(config);
269
+ if (!options.quiet) {
270
+ if (process.platform === 'darwin') {
271
+ console.warn('[idle-detector] macOS: using output analysis only (less reliable)');
272
+ }
273
+ else if (process.platform === 'win32') {
274
+ console.warn('[idle-detector] Windows: using output analysis only (less reliable)');
275
+ }
276
+ }
277
+ return detector;
278
+ }
279
+ //# sourceMappingURL=idle-detector.js.map
@@ -104,6 +104,10 @@ export declare class OutputParser {
104
104
  *
105
105
  * IMPORTANT: We strip ANSI codes for pattern matching, but preserve
106
106
  * the original line for output to maintain terminal rendering.
107
+ *
108
+ * OPTIMIZATION: Early exit for lines that can't possibly be relay commands.
109
+ * Most lines don't contain relay patterns, so we avoid expensive regex/ANSI
110
+ * stripping for the common case.
107
111
  */
108
112
  private processLine;
109
113
  /**
@@ -640,9 +640,23 @@ export class OutputParser {
640
640
  *
641
641
  * IMPORTANT: We strip ANSI codes for pattern matching, but preserve
642
642
  * the original line for output to maintain terminal rendering.
643
+ *
644
+ * OPTIMIZATION: Early exit for lines that can't possibly be relay commands.
645
+ * Most lines don't contain relay patterns, so we avoid expensive regex/ANSI
646
+ * stripping for the common case.
643
647
  */
644
648
  processLine(line) {
645
- // Strip ANSI codes for pattern matching
649
+ // FAST PATH: Quick string check before any expensive operations
650
+ // Most lines don't contain relay commands, so early exit is a big win
651
+ // Check for prefix bases (without the colon to handle custom prefixes like '>>')
652
+ const relayBase = this.options.prefix.replace(/:$/, '');
653
+ const thinkingBase = this.options.thinkingPrefix.replace(/:$/, '');
654
+ const hasRelayPattern = line.includes(relayBase) || line.includes(thinkingBase);
655
+ const hasBlockPattern = line.includes('[[') || line.includes('```');
656
+ if (!hasRelayPattern && !hasBlockPattern) {
657
+ return { command: null, output: line };
658
+ }
659
+ // Strip ANSI codes for pattern matching (only when potentially needed)
646
660
  const stripped = stripAnsi(line);
647
661
  // Handle code fences
648
662
  if (CODE_FENCE.test(stripped)) {
@@ -672,11 +686,13 @@ export class OutputParser {
672
686
  const [raw, target, threadProject, threadId, body] = relayMatch;
673
687
  // Skip instructional/example text (common in system prompts)
674
688
  if (isInstructionalText(body)) {
689
+ console.error(`[parser] Filtered inline message to ${target} - instructional text. Body: ${body.substring(0, 100)}`);
675
690
  return { command: null, output: line };
676
691
  }
677
692
  const { to, project } = parseTarget(target);
678
693
  // Skip placeholder target names (common in documentation/examples)
679
694
  if (isPlaceholderTarget(to)) {
695
+ console.error(`[parser] Filtered inline message - placeholder target: ${to}`);
680
696
  return { command: null, output: line };
681
697
  }
682
698
  return {
@@ -795,10 +811,12 @@ export class OutputParser {
795
811
  shouldFilterFencedInline(target, body) {
796
812
  // Check for placeholder target names
797
813
  if (isPlaceholderTarget(target)) {
814
+ console.error(`[parser] Filtered message - placeholder target: ${target}`);
798
815
  return true;
799
816
  }
800
817
  // Check for instructional body content
801
818
  if (isInstructionalText(body)) {
819
+ console.error(`[parser] Filtered message to ${target} - instructional text detected. Body preview: ${body.substring(0, 100)}`);
802
820
  return true;
803
821
  }
804
822
  return false;
@@ -112,6 +112,11 @@ export declare class PtyWrapper extends BaseWrapper {
112
112
  * Start the agent process
113
113
  */
114
114
  start(): Promise<void>;
115
+ /**
116
+ * Wait for the agent to be ready for input.
117
+ * Uses idle detection instead of a fixed delay.
118
+ */
119
+ private waitForAgentReady;
115
120
  /**
116
121
  * Inject continuity context from previous session.
117
122
  * Called after agent ID initialization to restore state from ledger.
@@ -240,7 +245,8 @@ export declare class PtyWrapper extends BaseWrapper {
240
245
  protected handleIncomingMessage(from: string, payload: SendPayload, messageId: string, meta?: SendMeta, originalTo?: string): void;
241
246
  /**
242
247
  * Wait for output to stabilize before injection.
243
- * Returns true if output has been stable for the required duration.
248
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
249
+ * Returns true if agent is idle and ready for input.
244
250
  */
245
251
  private waitForOutputStable;
246
252
  /**
@@ -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,9 +213,9 @@ 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
  }
@@ -219,7 +224,27 @@ export class PtyWrapper extends BaseWrapper {
219
224
  if (!this.config.interactive) {
220
225
  this.processMessageQueue();
221
226
  }
222
- }, 2000);
227
+ }).catch(err => {
228
+ console.error(`[pty:${this.config.name}] Failed to wait for agent ready:`, err);
229
+ // Fall back to marking ready anyway to avoid blocking forever
230
+ this.readyForMessages = true;
231
+ });
232
+ }
233
+ /**
234
+ * Wait for the agent to be ready for input.
235
+ * Uses idle detection instead of a fixed delay.
236
+ */
237
+ async waitForAgentReady() {
238
+ // Minimum wait to ensure the CLI process has started
239
+ await sleep(500);
240
+ // Wait for agent to become idle (CLI fully initialized)
241
+ const result = await this.waitForIdleState(10000, 200);
242
+ if (result.isIdle) {
243
+ console.log(`[pty:${this.config.name}] Agent ready (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
244
+ }
245
+ else {
246
+ console.warn(`[pty:${this.config.name}] Agent readiness timeout, proceeding anyway`);
247
+ }
223
248
  }
224
249
  // Note: initializeAgentId() and getAgentId() are inherited from BaseWrapper
225
250
  /**
@@ -322,6 +347,8 @@ export class PtyWrapper extends BaseWrapper {
322
347
  handleOutput(data) {
323
348
  // Track output timing for stability checks
324
349
  this.lastOutputTime = Date.now();
350
+ // Feed output to idle detector for robust idle detection
351
+ this.feedIdleDetectorOutput(data);
325
352
  // Append to raw buffer
326
353
  this.rawBuffer += data;
327
354
  // Write to log file if available
@@ -610,6 +637,7 @@ export class PtyWrapper extends BaseWrapper {
610
637
  }
611
638
  // Skip placeholder targets (documentation examples like "AgentName", "Lead", etc.)
612
639
  if (isPlaceholderTarget(target)) {
640
+ console.error(`[pty-wrapper] Filtered fenced message - placeholder target: ${target}`);
613
641
  continue;
614
642
  }
615
643
  // Find the closing >>>
@@ -630,6 +658,7 @@ export class PtyWrapper extends BaseWrapper {
630
658
  }
631
659
  // Skip placeholder targets after parsing cross-project syntax
632
660
  if (isPlaceholderTarget(to)) {
661
+ console.error(`[pty-wrapper] Filtered fenced message - placeholder target after cross-project parse: ${to}`);
633
662
  continue;
634
663
  }
635
664
  this.sendRelayCommand({
@@ -676,8 +705,10 @@ export class PtyWrapper extends BaseWrapper {
676
705
  if (!body)
677
706
  continue;
678
707
  // Skip placeholder targets (documentation examples)
679
- if (isPlaceholderTarget(target))
708
+ if (isPlaceholderTarget(target)) {
709
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target: ${target}`);
680
710
  continue;
711
+ }
681
712
  // Parse target for cross-project syntax
682
713
  const colonIdx = target.indexOf(':');
683
714
  let to = target;
@@ -687,8 +718,10 @@ export class PtyWrapper extends BaseWrapper {
687
718
  to = target.substring(colonIdx + 1);
688
719
  }
689
720
  // Skip placeholder targets after parsing cross-project syntax
690
- if (isPlaceholderTarget(to))
721
+ if (isPlaceholderTarget(to)) {
722
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target after cross-project parse: ${to}`);
691
723
  continue;
724
+ }
692
725
  this.sendRelayCommand({
693
726
  to,
694
727
  kind: 'message',
@@ -702,8 +735,10 @@ export class PtyWrapper extends BaseWrapper {
702
735
  if (!body)
703
736
  continue;
704
737
  // Skip placeholder targets (documentation examples)
705
- if (isPlaceholderTarget(target))
738
+ if (isPlaceholderTarget(target)) {
739
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target: ${target}`);
706
740
  continue;
741
+ }
707
742
  // Parse target for cross-project syntax
708
743
  const colonIdx = target.indexOf(':');
709
744
  let to = target;
@@ -713,8 +748,10 @@ export class PtyWrapper extends BaseWrapper {
713
748
  to = target.substring(colonIdx + 1);
714
749
  }
715
750
  // Skip placeholder targets after parsing cross-project syntax
716
- if (isPlaceholderTarget(to))
751
+ if (isPlaceholderTarget(to)) {
752
+ console.error(`[pty-wrapper] Filtered single-line message - placeholder target after cross-project parse: ${to}`);
717
753
  continue;
754
+ }
718
755
  this.sendRelayCommand({
719
756
  to,
720
757
  kind: 'message',
@@ -1032,27 +1069,14 @@ export class PtyWrapper extends BaseWrapper {
1032
1069
  }
1033
1070
  /**
1034
1071
  * Wait for output to stabilize before injection.
1035
- * Returns true if output has been stable for the required duration.
1072
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
1073
+ * Returns true if agent is idle and ready for input.
1036
1074
  */
1037
1075
  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
- }
1076
+ const result = await this.waitForIdleState(INJECTION_CONSTANTS.STABILITY_TIMEOUT_MS, INJECTION_CONSTANTS.STABILITY_POLL_MS);
1077
+ if (result.isIdle) {
1078
+ console.log(`[pty:${this.config.name}] Idle detected (confidence: ${(result.confidence * 100).toFixed(0)}%)`);
1079
+ return true;
1056
1080
  }
1057
1081
  // Timeout - return true anyway to avoid blocking forever
1058
1082
  console.warn(`[pty:${this.config.name}] Stability timeout, proceeding with injection`);
@@ -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
  */
@@ -246,7 +256,8 @@ export declare class TmuxWrapper extends BaseWrapper {
246
256
  */
247
257
  protected handleIncomingMessage(from: string, payload: SendPayload, messageId: string, meta?: SendMeta, originalTo?: string): void;
248
258
  /**
249
- * Check if we should inject a message
259
+ * Check if we should inject a message.
260
+ * Uses UniversalIdleDetector (from BaseWrapper) for robust cross-CLI idle detection.
250
261
  */
251
262
  private checkForInjectionOpportunity;
252
263
  /**