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
@@ -20,6 +20,7 @@ import type { ParsedCommand, ParsedSummary } from './parser.js';
20
20
  import type { SendPayload, SendMeta, SpeakOnTrigger } from '../protocol/types.js';
21
21
  import { type QueuedMessage, type InjectionMetrics, type CliType } from './shared.js';
22
22
  import { type ContinuityManager } from '../continuity/index.js';
23
+ import { UniversalIdleDetector } from './idle-detector.js';
23
24
  /**
24
25
  * Base configuration shared by all wrapper types
25
26
  */
@@ -55,6 +56,14 @@ export interface BaseWrapperConfig {
55
56
  /** Shadow configuration */
56
57
  shadowOf?: string;
57
58
  shadowSpeakOn?: SpeakOnTrigger[];
59
+ /** Milliseconds of idle time before injection is allowed (default: 1500) */
60
+ idleBeforeInjectMs?: number;
61
+ /** Confidence threshold for idle detection (0-1, default: 0.7) */
62
+ idleConfidenceThreshold?: number;
63
+ /** Skip initial instruction injection (when using --append-system-prompt) */
64
+ skipInstructions?: boolean;
65
+ /** Skip continuity loading (for spawned agents that don't need session recovery) */
66
+ skipContinuity?: boolean;
58
67
  }
59
68
  /**
60
69
  * Abstract base class for agent wrappers
@@ -86,6 +95,7 @@ export declare abstract class BaseWrapper extends EventEmitter {
86
95
  completedTasks?: string[];
87
96
  };
88
97
  protected lastSummaryRawContent: string;
98
+ protected idleDetector: UniversalIdleDetector;
89
99
  constructor(config: BaseWrapperConfig);
90
100
  /** Start the agent process */
91
101
  abstract start(): Promise<void>;
@@ -102,6 +112,36 @@ export declare abstract class BaseWrapper extends EventEmitter {
102
112
  successRate: number;
103
113
  };
104
114
  get pendingMessageCount(): number;
115
+ /**
116
+ * Set the PID for process state inspection (Linux only).
117
+ * Call this after the agent process is started.
118
+ */
119
+ protected setIdleDetectorPid(pid: number): void;
120
+ /**
121
+ * Feed output to the idle detector.
122
+ * Call this whenever new output is received from the agent.
123
+ */
124
+ protected feedIdleDetectorOutput(output: string): void;
125
+ /**
126
+ * Check if the agent is idle and ready for injection.
127
+ * Returns idle state with confidence signals.
128
+ */
129
+ protected checkIdleForInjection(): {
130
+ isIdle: boolean;
131
+ confidence: number;
132
+ signals: Array<{
133
+ source: string;
134
+ confidence: number;
135
+ }>;
136
+ };
137
+ /**
138
+ * Wait for the agent to become idle.
139
+ * Returns when idle or after timeout.
140
+ */
141
+ protected waitForIdleState(timeoutMs?: number, pollMs?: number): Promise<{
142
+ isIdle: boolean;
143
+ confidence: number;
144
+ }>;
105
145
  /**
106
146
  * Handle incoming message from relay
107
147
  */
@@ -18,7 +18,9 @@ import { EventEmitter } from 'node:events';
18
18
  import { RelayClient } from './client.js';
19
19
  import { isPlaceholderTarget } from './parser.js';
20
20
  import { getDefaultRelayPrefix, detectCliType, createInjectionMetrics, } from './shared.js';
21
+ import { DEFAULT_IDLE_BEFORE_INJECT_MS, DEFAULT_IDLE_CONFIDENCE_THRESHOLD, } from '../config/relay-config.js';
21
22
  import { getContinuityManager, parseContinuityCommand, hasContinuityCommand, } from '../continuity/index.js';
23
+ import { UniversalIdleDetector } from './idle-detector.js';
22
24
  /**
23
25
  * Abstract base class for agent wrappers
24
26
  */
@@ -45,6 +47,8 @@ export class BaseWrapper extends EventEmitter {
45
47
  sessionEndProcessed = false;
46
48
  sessionEndData;
47
49
  lastSummaryRawContent = '';
50
+ // Universal idle detection (shared across all wrapper types)
51
+ idleDetector;
48
52
  constructor(config) {
49
53
  super();
50
54
  this.config = config;
@@ -59,8 +63,15 @@ export class BaseWrapper extends EventEmitter {
59
63
  workingDirectory: config.cwd,
60
64
  quiet: true,
61
65
  });
62
- // Initialize continuity manager
63
- this.continuity = getContinuityManager({ defaultCli: this.cliType });
66
+ // Initialize continuity manager (skip for spawned agents that don't need session recovery)
67
+ if (!config.skipContinuity) {
68
+ this.continuity = getContinuityManager({ defaultCli: this.cliType });
69
+ }
70
+ // Initialize universal idle detector for robust injection timing
71
+ this.idleDetector = new UniversalIdleDetector({
72
+ minSilenceMs: config.idleBeforeInjectMs ?? DEFAULT_IDLE_BEFORE_INJECT_MS,
73
+ confidenceThreshold: config.idleConfidenceThreshold ?? DEFAULT_IDLE_CONFIDENCE_THRESHOLD,
74
+ });
64
75
  // Set up message handler
65
76
  this.client.onMessage = (from, payload, messageId, meta, originalTo) => {
66
77
  this.handleIncomingMessage(from, payload, messageId, meta, originalTo);
@@ -93,6 +104,39 @@ export class BaseWrapper extends EventEmitter {
93
104
  return this.messageQueue.length;
94
105
  }
95
106
  // =========================================================================
107
+ // Idle detection (shared across all wrapper types)
108
+ // =========================================================================
109
+ /**
110
+ * Set the PID for process state inspection (Linux only).
111
+ * Call this after the agent process is started.
112
+ */
113
+ setIdleDetectorPid(pid) {
114
+ this.idleDetector.setPid(pid);
115
+ }
116
+ /**
117
+ * Feed output to the idle detector.
118
+ * Call this whenever new output is received from the agent.
119
+ */
120
+ feedIdleDetectorOutput(output) {
121
+ this.idleDetector.onOutput(output);
122
+ }
123
+ /**
124
+ * Check if the agent is idle and ready for injection.
125
+ * Returns idle state with confidence signals.
126
+ */
127
+ checkIdleForInjection() {
128
+ return this.idleDetector.checkIdle({
129
+ minSilenceMs: this.config.idleBeforeInjectMs ?? 1500,
130
+ });
131
+ }
132
+ /**
133
+ * Wait for the agent to become idle.
134
+ * Returns when idle or after timeout.
135
+ */
136
+ async waitForIdleState(timeoutMs = 30000, pollMs = 200) {
137
+ return this.idleDetector.waitForIdle(timeoutMs, pollMs);
138
+ }
139
+ // =========================================================================
96
140
  // Message handling
97
141
  // =========================================================================
98
142
  /**
@@ -126,12 +170,16 @@ export class BaseWrapper extends EventEmitter {
126
170
  */
127
171
  sendRelayCommand(cmd) {
128
172
  // Validate target
129
- if (isPlaceholderTarget(cmd.to))
173
+ if (isPlaceholderTarget(cmd.to)) {
174
+ console.error(`[base-wrapper] Skipped message - placeholder target: ${cmd.to}`);
130
175
  return;
176
+ }
131
177
  // Create hash for deduplication (use first 100 chars of body)
132
178
  const hash = `${cmd.to}:${cmd.body.substring(0, 100)}`;
133
- if (this.sentMessageHashes.has(hash))
179
+ if (this.sentMessageHashes.has(hash)) {
180
+ console.error(`[base-wrapper] Skipped duplicate message to ${cmd.to}`);
134
181
  return;
182
+ }
135
183
  this.sentMessageHashes.add(hash);
136
184
  // Limit hash set size
137
185
  if (this.sentMessageHashes.size > 500) {
@@ -140,9 +188,15 @@ export class BaseWrapper extends EventEmitter {
140
188
  this.sentMessageHashes.delete(oldest);
141
189
  }
142
190
  // Only send if client ready
143
- if (this.client.state !== 'READY')
191
+ if (this.client.state !== 'READY') {
192
+ console.error(`[base-wrapper] Skipped message to ${cmd.to} - client not ready (state: ${this.client.state})`);
144
193
  return;
145
- this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread);
194
+ }
195
+ console.log(`[base-wrapper] Sending message to ${cmd.to}: "${cmd.body.substring(0, 50)}..."`);
196
+ const sent = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread);
197
+ if (!sent) {
198
+ console.error(`[base-wrapper] Failed to send message to ${cmd.to} - sendMessage returned false`);
199
+ }
146
200
  }
147
201
  // =========================================================================
148
202
  // Spawn/release handling
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Relay Client
3
3
  * Connects to the daemon and handles message sending/receiving.
4
+ *
5
+ * Optimizations:
6
+ * - Monotonic ID generation (faster than UUID)
7
+ * - Write coalescing (batch socket writes)
8
+ * - Circular dedup cache (O(1) eviction)
4
9
  */
5
10
  import { type SendPayload, type SendMeta, type PayloadKind, type SpeakOnTrigger, type EntityType } from '../protocol/types.js';
6
11
  export type ClientState = 'DISCONNECTED' | 'CONNECTING' | 'HANDSHAKING' | 'READY' | 'BACKOFF';
@@ -41,9 +46,9 @@ export declare class RelayClient {
41
46
  private reconnectDelay;
42
47
  private reconnectTimer?;
43
48
  private _destroyed;
44
- private deliveredIds;
45
- private deliveredOrder;
46
- private readonly deliveredCacheLimit;
49
+ private dedupeCache;
50
+ private writeQueue;
51
+ private writeScheduled;
47
52
  /**
48
53
  * Handler for incoming messages.
49
54
  * @param from - The sender agent name
@@ -123,6 +128,10 @@ export declare class RelayClient {
123
128
  private setState;
124
129
  private sendHello;
125
130
  private send;
131
+ /**
132
+ * Flush all queued writes in a single syscall.
133
+ */
134
+ private flushWrites;
126
135
  private handleData;
127
136
  private processFrame;
128
137
  private handleWelcome;
@@ -133,7 +142,8 @@ export declare class RelayClient {
133
142
  private handleError;
134
143
  private scheduleReconnect;
135
144
  /**
136
- * Track delivered message IDs to provide deterministic deduplication when messages are replayed.
145
+ * Check if message was already delivered (deduplication).
146
+ * Uses circular buffer for O(1) eviction.
137
147
  * @returns true if the message has already been seen.
138
148
  */
139
149
  private markDelivered;
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Relay Client
3
3
  * Connects to the daemon and handles message sending/receiving.
4
+ *
5
+ * Optimizations:
6
+ * - Monotonic ID generation (faster than UUID)
7
+ * - Write coalescing (batch socket writes)
8
+ * - Circular dedup cache (O(1) eviction)
4
9
  */
5
10
  import net from 'node:net';
6
- import { v4 as uuid } from 'uuid';
11
+ import { generateId } from '../utils/id-generator.js';
7
12
  import { PROTOCOL_VERSION, } from '../protocol/types.js';
8
- import { encodeFrame, FrameParser } from '../protocol/framing.js';
13
+ import { encodeFrameLegacy, FrameParser } from '../protocol/framing.js';
9
14
  import { DEFAULT_SOCKET_PATH } from '../daemon/server.js';
10
15
  const DEFAULT_CLIENT_CONFIG = {
11
16
  socketPath: DEFAULT_SOCKET_PATH,
@@ -17,6 +22,40 @@ const DEFAULT_CLIENT_CONFIG = {
17
22
  reconnectDelayMs: 100,
18
23
  reconnectMaxDelayMs: 30000,
19
24
  };
25
+ /**
26
+ * Circular buffer for O(1) deduplication with bounded memory.
27
+ */
28
+ class CircularDedupeCache {
29
+ ids = new Set();
30
+ ring;
31
+ head = 0;
32
+ capacity;
33
+ constructor(capacity = 2000) {
34
+ this.capacity = capacity;
35
+ this.ring = new Array(capacity);
36
+ }
37
+ /** Returns true if duplicate (already seen) */
38
+ check(id) {
39
+ if (this.ids.has(id))
40
+ return true;
41
+ // Evict oldest if at capacity
42
+ if (this.ids.size >= this.capacity) {
43
+ const oldest = this.ring[this.head];
44
+ if (oldest)
45
+ this.ids.delete(oldest);
46
+ }
47
+ // Add new ID
48
+ this.ring[this.head] = id;
49
+ this.ids.add(id);
50
+ this.head = (this.head + 1) % this.capacity;
51
+ return false;
52
+ }
53
+ clear() {
54
+ this.ids.clear();
55
+ this.ring = new Array(this.capacity);
56
+ this.head = 0;
57
+ }
58
+ }
20
59
  export class RelayClient {
21
60
  config;
22
61
  socket;
@@ -28,9 +67,11 @@ export class RelayClient {
28
67
  reconnectDelay;
29
68
  reconnectTimer;
30
69
  _destroyed = false;
31
- deliveredIds = new Set();
32
- deliveredOrder = [];
33
- deliveredCacheLimit = 2000;
70
+ // Circular dedup cache (O(1) eviction vs O(n) array shift)
71
+ dedupeCache = new CircularDedupeCache(2000);
72
+ // Write coalescing: batch multiple writes into single syscall
73
+ writeQueue = [];
74
+ writeScheduled = false;
34
75
  // Event handlers
35
76
  /**
36
77
  * Handler for incoming messages.
@@ -46,6 +87,7 @@ export class RelayClient {
46
87
  constructor(config = {}) {
47
88
  this.config = { ...DEFAULT_CLIENT_CONFIG, ...config };
48
89
  this.parser = new FrameParser();
90
+ this.parser.setLegacyMode(true); // Use 4-byte header for backwards compatibility
49
91
  this.reconnectDelay = this.config.reconnectDelayMs;
50
92
  }
51
93
  get state() {
@@ -124,7 +166,7 @@ export class RelayClient {
124
166
  this.send({
125
167
  v: PROTOCOL_VERSION,
126
168
  type: 'BYE',
127
- id: uuid(),
169
+ id: generateId(),
128
170
  ts: Date.now(),
129
171
  payload: {},
130
172
  });
@@ -156,7 +198,7 @@ export class RelayClient {
156
198
  const envelope = {
157
199
  v: PROTOCOL_VERSION,
158
200
  type: 'SEND',
159
- id: uuid(),
201
+ id: generateId(),
160
202
  ts: Date.now(),
161
203
  to,
162
204
  payload: {
@@ -184,7 +226,7 @@ export class RelayClient {
184
226
  return this.send({
185
227
  v: PROTOCOL_VERSION,
186
228
  type: 'SUBSCRIBE',
187
- id: uuid(),
229
+ id: generateId(),
188
230
  ts: Date.now(),
189
231
  topic,
190
232
  payload: {},
@@ -199,7 +241,7 @@ export class RelayClient {
199
241
  return this.send({
200
242
  v: PROTOCOL_VERSION,
201
243
  type: 'UNSUBSCRIBE',
202
- id: uuid(),
244
+ id: generateId(),
203
245
  ts: Date.now(),
204
246
  topic,
205
247
  payload: {},
@@ -217,7 +259,7 @@ export class RelayClient {
217
259
  return this.send({
218
260
  v: PROTOCOL_VERSION,
219
261
  type: 'SHADOW_BIND',
220
- id: uuid(),
262
+ id: generateId(),
221
263
  ts: Date.now(),
222
264
  payload: {
223
265
  primaryAgent,
@@ -237,7 +279,7 @@ export class RelayClient {
237
279
  return this.send({
238
280
  v: PROTOCOL_VERSION,
239
281
  type: 'SHADOW_UNBIND',
240
- id: uuid(),
282
+ id: generateId(),
241
283
  ts: Date.now(),
242
284
  payload: {
243
285
  primaryAgent,
@@ -257,7 +299,7 @@ export class RelayClient {
257
299
  const envelope = {
258
300
  v: PROTOCOL_VERSION,
259
301
  type: 'LOG',
260
- id: uuid(),
302
+ id: generateId(),
261
303
  ts: Date.now(),
262
304
  payload: {
263
305
  data,
@@ -276,7 +318,7 @@ export class RelayClient {
276
318
  const hello = {
277
319
  v: PROTOCOL_VERSION,
278
320
  type: 'HELLO',
279
- id: uuid(),
321
+ id: generateId(),
280
322
  ts: Date.now(),
281
323
  payload: {
282
324
  agent: this.config.agentName,
@@ -303,8 +345,13 @@ export class RelayClient {
303
345
  if (!this.socket)
304
346
  return false;
305
347
  try {
306
- const frame = encodeFrame(envelope);
307
- this.socket.write(frame);
348
+ const frame = encodeFrameLegacy(envelope);
349
+ this.writeQueue.push(frame);
350
+ // Coalesce writes: schedule flush on next tick if not already scheduled
351
+ if (!this.writeScheduled) {
352
+ this.writeScheduled = true;
353
+ setImmediate(() => this.flushWrites());
354
+ }
308
355
  return true;
309
356
  }
310
357
  catch (err) {
@@ -312,6 +359,23 @@ export class RelayClient {
312
359
  return false;
313
360
  }
314
361
  }
362
+ /**
363
+ * Flush all queued writes in a single syscall.
364
+ */
365
+ flushWrites() {
366
+ this.writeScheduled = false;
367
+ if (this.writeQueue.length === 0 || !this.socket)
368
+ return;
369
+ if (this.writeQueue.length === 1) {
370
+ // Single frame - write directly (no concat needed)
371
+ this.socket.write(this.writeQueue[0]);
372
+ }
373
+ else {
374
+ // Multiple frames - batch into single write
375
+ this.socket.write(Buffer.concat(this.writeQueue));
376
+ }
377
+ this.writeQueue = [];
378
+ }
315
379
  handleData(data) {
316
380
  try {
317
381
  const frames = this.parser.push(data);
@@ -353,11 +417,12 @@ export class RelayClient {
353
417
  }
354
418
  }
355
419
  handleDeliver(envelope) {
420
+ console.log(`[relay-client:${this.config.agentName}] Received DELIVER from ${envelope.from}: "${envelope.payload.body?.substring(0, 40)}..."`);
356
421
  // Send ACK
357
422
  this.send({
358
423
  v: PROTOCOL_VERSION,
359
424
  type: 'ACK',
360
- id: uuid(),
425
+ id: generateId(),
361
426
  ts: Date.now(),
362
427
  payload: {
363
428
  ack_id: envelope.id,
@@ -366,6 +431,7 @@ export class RelayClient {
366
431
  });
367
432
  const duplicate = this.markDelivered(envelope.id);
368
433
  if (duplicate) {
434
+ console.log(`[relay-client:${this.config.agentName}] Duplicate delivery, skipping`);
369
435
  return;
370
436
  }
371
437
  // Notify handler
@@ -373,12 +439,15 @@ export class RelayClient {
373
439
  if (this.onMessage && envelope.from) {
374
440
  this.onMessage(envelope.from, envelope.payload, envelope.id, envelope.payload_meta, envelope.delivery.originalTo);
375
441
  }
442
+ else {
443
+ console.log(`[relay-client:${this.config.agentName}] No onMessage handler or no from field`);
444
+ }
376
445
  }
377
446
  handlePing(envelope) {
378
447
  this.send({
379
448
  v: PROTOCOL_VERSION,
380
449
  type: 'PONG',
381
- id: uuid(),
450
+ id: generateId(),
382
451
  ts: Date.now(),
383
452
  payload: envelope.payload ?? {},
384
453
  });
@@ -435,23 +504,12 @@ export class RelayClient {
435
504
  }, delay);
436
505
  }
437
506
  /**
438
- * Track delivered message IDs to provide deterministic deduplication when messages are replayed.
507
+ * Check if message was already delivered (deduplication).
508
+ * Uses circular buffer for O(1) eviction.
439
509
  * @returns true if the message has already been seen.
440
510
  */
441
511
  markDelivered(id) {
442
- if (this.deliveredIds.has(id)) {
443
- return true;
444
- }
445
- this.deliveredIds.add(id);
446
- this.deliveredOrder.push(id);
447
- // Simple FIFO eviction to keep memory bounded
448
- if (this.deliveredOrder.length > this.deliveredCacheLimit) {
449
- const oldest = this.deliveredOrder.shift();
450
- if (oldest) {
451
- this.deliveredIds.delete(oldest);
452
- }
453
- }
454
- return false;
512
+ return this.dedupeCache.check(id);
455
513
  }
456
514
  }
457
515
  //# sourceMappingURL=client.js.map
@@ -0,0 +1,102 @@
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
+ export interface IdleSignal {
12
+ source: 'process_state' | 'output_silence' | 'natural_ending';
13
+ confidence: number;
14
+ timestamp: number;
15
+ details?: string;
16
+ }
17
+ export interface IdleResult {
18
+ isIdle: boolean;
19
+ confidence: number;
20
+ signals: IdleSignal[];
21
+ }
22
+ export interface IdleDetectorConfig {
23
+ /** Minimum silence duration to consider for idle (ms) */
24
+ minSilenceMs?: number;
25
+ /** Output buffer size limit */
26
+ bufferLimit?: number;
27
+ /** Confidence threshold for idle detection (0-1) */
28
+ confidenceThreshold?: number;
29
+ }
30
+ /**
31
+ * Universal idle detector for any CLI-based agent.
32
+ */
33
+ export declare class UniversalIdleDetector {
34
+ private lastOutputTime;
35
+ private outputBuffer;
36
+ private pid;
37
+ private config;
38
+ constructor(config?: IdleDetectorConfig);
39
+ /**
40
+ * Set the PID of the agent process to monitor.
41
+ * Required for Linux process state inspection.
42
+ */
43
+ setPid(pid: number): void;
44
+ /**
45
+ * Get the current PID being monitored.
46
+ */
47
+ getPid(): number | null;
48
+ /**
49
+ * Process output chunk from the agent.
50
+ * Call this for every output received from the agent process.
51
+ */
52
+ onOutput(chunk: string): void;
53
+ /**
54
+ * Check if the agent process is blocked on read (waiting for input).
55
+ * This is the most reliable signal - the OS knows when a process is waiting.
56
+ *
57
+ * Linux-only; returns null on other platforms.
58
+ */
59
+ private isProcessWaitingForInput;
60
+ /**
61
+ * Get milliseconds since last output.
62
+ */
63
+ private getOutputSilenceMs;
64
+ /**
65
+ * Check if the last output ends "naturally" (complete thought vs mid-sentence).
66
+ * Helps distinguish between pauses in output and waiting for input.
67
+ */
68
+ private hasNaturalEnding;
69
+ /**
70
+ * Determine if the agent is idle and ready for input.
71
+ * Combines multiple signals for reliability across all CLI types.
72
+ */
73
+ checkIdle(options?: {
74
+ minSilenceMs?: number;
75
+ }): IdleResult;
76
+ /**
77
+ * Wait for idle state with timeout.
78
+ * Returns the idle result when achieved or after timeout.
79
+ */
80
+ waitForIdle(timeoutMs?: number, pollMs?: number): Promise<IdleResult>;
81
+ /**
82
+ * Reset state (call when agent starts new response).
83
+ */
84
+ reset(): void;
85
+ /**
86
+ * Get time since last output in milliseconds.
87
+ */
88
+ getTimeSinceLastOutput(): number;
89
+ }
90
+ /**
91
+ * Get the PID of a process running in a tmux pane.
92
+ * Uses tmux list-panes with format specifier.
93
+ */
94
+ export declare function getTmuxPanePid(tmuxPath: string, sessionName: string): Promise<number | null>;
95
+ /**
96
+ * Create an idle detector configured for the current platform.
97
+ * Logs a warning on non-Linux platforms where process state inspection isn't available.
98
+ */
99
+ export declare function createIdleDetector(config?: IdleDetectorConfig, options?: {
100
+ quiet?: boolean;
101
+ }): UniversalIdleDetector;
102
+ //# sourceMappingURL=idle-detector.d.ts.map