agent-relay 1.3.2 → 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 (106) hide show
  1. package/README.md +23 -9
  2. package/dist/bridge/spawner.js +39 -75
  3. package/dist/cli/index.d.ts +8 -6
  4. package/dist/cli/index.js +251 -30
  5. package/dist/daemon/agent-manager.js +4 -0
  6. package/dist/daemon/connection.js +17 -9
  7. package/dist/daemon/router.js +2 -2
  8. package/dist/dashboard/out/404.html +1 -0
  9. package/dist/dashboard/out/_next/static/R-uQOUcOLINtsp6ACeZa9/_buildManifest.js +1 -0
  10. package/dist/dashboard/out/_next/static/R-uQOUcOLINtsp6ACeZa9/_ssgManifest.js +1 -0
  11. package/dist/dashboard/out/_next/static/chunks/116-de2a4ac06e5000dc.js +1 -0
  12. package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +2 -0
  13. package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  14. package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +9 -0
  15. package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +1 -0
  16. package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +1 -0
  17. package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +1 -0
  18. package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  19. package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +1 -0
  20. package/dist/dashboard/out/_next/static/chunks/919-87d604a5d76c1fbd.js +1 -0
  21. package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +1 -0
  22. package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8553743baca53a00.js +1 -0
  23. package/dist/dashboard/out/_next/static/chunks/app/app/page-7f64824ae7d06707.js +1 -0
  24. package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-3f559d393902aad2.js +1 -0
  25. package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-3538dfe0ffe984b8.js +1 -0
  26. package/dist/dashboard/out/_next/static/chunks/app/history/page-abb9ab2d329f56e9.js +1 -0
  27. package/dist/dashboard/out/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  28. package/dist/dashboard/out/_next/static/chunks/app/login/page-16d1715ddaa874ee.js +1 -0
  29. package/dist/dashboard/out/_next/static/chunks/app/metrics/page-f829604fb75a831a.js +1 -0
  30. package/dist/dashboard/out/_next/static/chunks/app/page-814efc4d77b4191d.js +1 -0
  31. package/dist/dashboard/out/_next/static/chunks/app/pricing/page-b08ed1c34d14434a.js +1 -0
  32. package/dist/dashboard/out/_next/static/chunks/app/providers/page-84322991d7244499.js +1 -0
  33. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-05606941a8e2be83.js +1 -0
  34. package/dist/dashboard/out/_next/static/chunks/app/signup/page-68d34f50baa8ab6b.js +1 -0
  35. package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  36. package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
  37. package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  38. package/dist/dashboard/out/_next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
  39. package/dist/dashboard/out/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +1 -0
  40. package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  41. package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  42. package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  43. package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  44. package/dist/dashboard/out/_next/static/css/44d2b52637b511bc.css +1 -0
  45. package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +1 -0
  46. package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
  47. package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
  48. package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
  49. package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
  50. package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
  51. package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +45 -0
  52. package/dist/dashboard/out/alt-logos/logo.svg +38 -0
  53. package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
  54. package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
  55. package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
  56. package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
  57. package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
  58. package/dist/dashboard/out/alt-logos/monogram-logo.svg +38 -0
  59. package/dist/dashboard/out/app/onboarding.html +1 -0
  60. package/dist/dashboard/out/app/onboarding.txt +7 -0
  61. package/dist/dashboard/out/app.html +1 -0
  62. package/dist/dashboard/out/app.txt +7 -0
  63. package/dist/dashboard/out/apple-icon.png +0 -0
  64. package/dist/dashboard/out/cloud/link.html +1 -0
  65. package/dist/dashboard/out/cloud/link.txt +7 -0
  66. package/dist/dashboard/out/connect-repos.html +1 -0
  67. package/dist/dashboard/out/connect-repos.txt +7 -0
  68. package/dist/dashboard/out/history.html +1 -0
  69. package/dist/dashboard/out/history.txt +7 -0
  70. package/dist/dashboard/out/index.html +1 -0
  71. package/dist/dashboard/out/index.txt +7 -0
  72. package/dist/dashboard/out/login.html +5 -0
  73. package/dist/dashboard/out/login.txt +7 -0
  74. package/dist/dashboard/out/metrics.html +1 -0
  75. package/dist/dashboard/out/metrics.txt +7 -0
  76. package/dist/dashboard/out/pricing.html +13 -0
  77. package/dist/dashboard/out/pricing.txt +7 -0
  78. package/dist/dashboard/out/providers/setup/claude.html +1 -0
  79. package/dist/dashboard/out/providers/setup/claude.txt +8 -0
  80. package/dist/dashboard/out/providers/setup/codex.html +1 -0
  81. package/dist/dashboard/out/providers/setup/codex.txt +8 -0
  82. package/dist/dashboard/out/providers.html +1 -0
  83. package/dist/dashboard/out/providers.txt +7 -0
  84. package/dist/dashboard/out/signup.html +6 -0
  85. package/dist/dashboard/out/signup.txt +7 -0
  86. package/dist/dashboard-server/metrics.d.ts +105 -0
  87. package/dist/dashboard-server/metrics.js +193 -0
  88. package/dist/dashboard-server/needs-attention.d.ts +24 -0
  89. package/dist/dashboard-server/needs-attention.js +78 -0
  90. package/dist/dashboard-server/server.d.ts +15 -0
  91. package/dist/dashboard-server/server.js +3992 -0
  92. package/dist/dashboard-server/start.d.ts +6 -0
  93. package/dist/dashboard-server/start.js +13 -0
  94. package/dist/dashboard-server/user-bridge.d.ts +103 -0
  95. package/dist/dashboard-server/user-bridge.js +189 -0
  96. package/dist/wrapper/base-wrapper.d.ts +4 -0
  97. package/dist/wrapper/base-wrapper.js +12 -4
  98. package/dist/wrapper/client.js +5 -0
  99. package/dist/wrapper/parser.js +2 -2
  100. package/dist/wrapper/pty-wrapper.d.ts +7 -1
  101. package/dist/wrapper/pty-wrapper.js +81 -5
  102. package/dist/wrapper/shared.d.ts +1 -1
  103. package/dist/wrapper/shared.js +1 -1
  104. package/dist/wrapper/tmux-wrapper.d.ts +8 -1
  105. package/dist/wrapper/tmux-wrapper.js +103 -28
  106. package/package.json +5 -3
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone dashboard starter for local development
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=start.d.ts.map
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone dashboard starter for local development
4
+ */
5
+ import { startDashboard } from './server.js';
6
+ import { getProjectPaths } from '../utils/project-namespace.js';
7
+ const port = parseInt(process.env.DASHBOARD_PORT || '3888', 10);
8
+ const paths = getProjectPaths();
9
+ console.log(`Starting dashboard for project: ${paths.projectRoot}`);
10
+ console.log(`Data dir: ${paths.dataDir}`);
11
+ console.log(`Database: ${paths.dbPath}`);
12
+ startDashboard(port, paths.dataDir, paths.teamDir, paths.dbPath).catch(console.error);
13
+ //# sourceMappingURL=start.js.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * User Bridge - Bridges dashboard WebSocket users to the relay daemon.
3
+ *
4
+ * This module allows human users connected via WebSocket to:
5
+ * - Register as "user" entities in the relay daemon
6
+ * - Join/leave channels
7
+ * - Send/receive messages through the relay daemon
8
+ * - Communicate with agents and other users
9
+ */
10
+ import type { WebSocket } from 'ws';
11
+ /**
12
+ * Relay client interface (subset of RelayClient for dependency injection)
13
+ */
14
+ export interface IRelayClient {
15
+ connect(): Promise<void>;
16
+ disconnect(): void;
17
+ state: string;
18
+ sendMessage(to: string, body: string, kind?: string, data?: unknown, thread?: string): boolean;
19
+ onMessage?: (from: string, payload: any, messageId: string, meta?: any, originalTo?: string) => void;
20
+ }
21
+ /**
22
+ * Factory function type for creating relay clients
23
+ */
24
+ export type RelayClientFactory = (options: {
25
+ socketPath: string;
26
+ agentName: string;
27
+ entityType: 'user';
28
+ displayName?: string;
29
+ avatarUrl?: string;
30
+ }) => Promise<IRelayClient>;
31
+ /**
32
+ * Options for creating a UserBridge
33
+ */
34
+ export interface UserBridgeOptions {
35
+ socketPath: string;
36
+ createRelayClient: RelayClientFactory;
37
+ }
38
+ /**
39
+ * Message options for sending
40
+ */
41
+ export interface SendMessageOptions {
42
+ thread?: string;
43
+ data?: Record<string, unknown>;
44
+ }
45
+ /**
46
+ * UserBridge manages the connection between dashboard WebSocket users
47
+ * and the relay daemon.
48
+ */
49
+ export declare class UserBridge {
50
+ private readonly socketPath;
51
+ private readonly createRelayClient;
52
+ private readonly users;
53
+ constructor(options: UserBridgeOptions);
54
+ /**
55
+ * Register a user with the relay daemon.
56
+ * Creates a relay client connection for the user.
57
+ */
58
+ registerUser(username: string, webSocket: WebSocket, options?: {
59
+ avatarUrl?: string;
60
+ displayName?: string;
61
+ }): Promise<void>;
62
+ /**
63
+ * Unregister a user and disconnect their relay client.
64
+ */
65
+ unregisterUser(username: string): void;
66
+ /**
67
+ * Check if a user is registered.
68
+ */
69
+ isUserRegistered(username: string): boolean;
70
+ /**
71
+ * Get list of all registered users.
72
+ */
73
+ getRegisteredUsers(): string[];
74
+ /**
75
+ * Join a channel.
76
+ */
77
+ joinChannel(username: string, channel: string): Promise<boolean>;
78
+ /**
79
+ * Leave a channel.
80
+ */
81
+ leaveChannel(username: string, channel: string): Promise<boolean>;
82
+ /**
83
+ * Get channels a user has joined.
84
+ */
85
+ getUserChannels(username: string): string[];
86
+ /**
87
+ * Send a message to a channel.
88
+ */
89
+ sendChannelMessage(username: string, channel: string, body: string, options?: SendMessageOptions): Promise<boolean>;
90
+ /**
91
+ * Send a direct message to another user or agent.
92
+ */
93
+ sendDirectMessage(fromUsername: string, toName: string, body: string, options?: SendMessageOptions): Promise<boolean>;
94
+ /**
95
+ * Handle incoming message from relay daemon.
96
+ */
97
+ private handleIncomingMessage;
98
+ /**
99
+ * Dispose of all user sessions.
100
+ */
101
+ dispose(): void;
102
+ }
103
+ //# sourceMappingURL=user-bridge.d.ts.map
@@ -0,0 +1,189 @@
1
+ /**
2
+ * User Bridge - Bridges dashboard WebSocket users to the relay daemon.
3
+ *
4
+ * This module allows human users connected via WebSocket to:
5
+ * - Register as "user" entities in the relay daemon
6
+ * - Join/leave channels
7
+ * - Send/receive messages through the relay daemon
8
+ * - Communicate with agents and other users
9
+ */
10
+ /**
11
+ * UserBridge manages the connection between dashboard WebSocket users
12
+ * and the relay daemon.
13
+ */
14
+ export class UserBridge {
15
+ socketPath;
16
+ createRelayClient;
17
+ users = new Map();
18
+ constructor(options) {
19
+ this.socketPath = options.socketPath;
20
+ this.createRelayClient = options.createRelayClient;
21
+ }
22
+ /**
23
+ * Register a user with the relay daemon.
24
+ * Creates a relay client connection for the user.
25
+ */
26
+ async registerUser(username, webSocket, options) {
27
+ // If user already registered, unregister first
28
+ if (this.users.has(username)) {
29
+ this.unregisterUser(username);
30
+ }
31
+ // Create relay client for this user
32
+ const relayClient = await this.createRelayClient({
33
+ socketPath: this.socketPath,
34
+ agentName: username,
35
+ entityType: 'user',
36
+ displayName: options?.displayName,
37
+ avatarUrl: options?.avatarUrl,
38
+ });
39
+ // Connect to daemon
40
+ await relayClient.connect();
41
+ // Set up message handler to forward messages to WebSocket
42
+ relayClient.onMessage = (from, payload, _messageId, _meta, _originalTo) => {
43
+ const body = typeof payload === 'object' && payload !== null && 'body' in payload
44
+ ? payload.body
45
+ : String(payload);
46
+ this.handleIncomingMessage(username, from, body, payload);
47
+ };
48
+ // Create session
49
+ const session = {
50
+ username,
51
+ relayClient,
52
+ webSocket,
53
+ channels: new Set(),
54
+ avatarUrl: options?.avatarUrl,
55
+ };
56
+ this.users.set(username, session);
57
+ // Set up WebSocket close handler
58
+ webSocket.on('close', () => {
59
+ this.unregisterUser(username);
60
+ });
61
+ console.log(`[user-bridge] User ${username} registered with relay daemon`);
62
+ }
63
+ /**
64
+ * Unregister a user and disconnect their relay client.
65
+ */
66
+ unregisterUser(username) {
67
+ const session = this.users.get(username);
68
+ if (!session)
69
+ return;
70
+ session.relayClient.disconnect();
71
+ this.users.delete(username);
72
+ console.log(`[user-bridge] User ${username} unregistered from relay daemon`);
73
+ }
74
+ /**
75
+ * Check if a user is registered.
76
+ */
77
+ isUserRegistered(username) {
78
+ return this.users.has(username);
79
+ }
80
+ /**
81
+ * Get list of all registered users.
82
+ */
83
+ getRegisteredUsers() {
84
+ return Array.from(this.users.keys());
85
+ }
86
+ /**
87
+ * Join a channel.
88
+ */
89
+ async joinChannel(username, channel) {
90
+ const session = this.users.get(username);
91
+ if (!session) {
92
+ console.warn(`[user-bridge] Cannot join channel - user ${username} not registered`);
93
+ return false;
94
+ }
95
+ // Send channel join via relay client
96
+ session.relayClient.sendMessage(channel, '', 'channel_join');
97
+ // Track membership
98
+ session.channels.add(channel);
99
+ console.log(`[user-bridge] User ${username} joined channel ${channel}`);
100
+ return true;
101
+ }
102
+ /**
103
+ * Leave a channel.
104
+ */
105
+ async leaveChannel(username, channel) {
106
+ const session = this.users.get(username);
107
+ if (!session) {
108
+ console.warn(`[user-bridge] Cannot leave channel - user ${username} not registered`);
109
+ return false;
110
+ }
111
+ // Send channel leave via relay client
112
+ session.relayClient.sendMessage(channel, '', 'channel_leave');
113
+ // Update membership
114
+ session.channels.delete(channel);
115
+ console.log(`[user-bridge] User ${username} left channel ${channel}`);
116
+ return true;
117
+ }
118
+ /**
119
+ * Get channels a user has joined.
120
+ */
121
+ getUserChannels(username) {
122
+ const session = this.users.get(username);
123
+ return session ? Array.from(session.channels) : [];
124
+ }
125
+ /**
126
+ * Send a message to a channel.
127
+ */
128
+ async sendChannelMessage(username, channel, body, options) {
129
+ const session = this.users.get(username);
130
+ if (!session) {
131
+ console.warn(`[user-bridge] Cannot send - user ${username} not registered`);
132
+ return false;
133
+ }
134
+ return session.relayClient.sendMessage(channel, body, 'message', options?.data, options?.thread);
135
+ }
136
+ /**
137
+ * Send a direct message to another user or agent.
138
+ */
139
+ async sendDirectMessage(fromUsername, toName, body, options) {
140
+ const session = this.users.get(fromUsername);
141
+ if (!session) {
142
+ console.warn(`[user-bridge] Cannot send DM - user ${fromUsername} not registered`);
143
+ return false;
144
+ }
145
+ return session.relayClient.sendMessage(toName, body, 'message', options?.data, options?.thread);
146
+ }
147
+ /**
148
+ * Handle incoming message from relay daemon.
149
+ */
150
+ handleIncomingMessage(username, from, body, envelope) {
151
+ const session = this.users.get(username);
152
+ if (!session)
153
+ return;
154
+ const ws = session.webSocket;
155
+ if (ws.readyState !== 1)
156
+ return; // Not OPEN
157
+ // Determine message type from envelope
158
+ const env = envelope;
159
+ if (env.type === 'CHANNEL_MESSAGE') {
160
+ // Channel message
161
+ ws.send(JSON.stringify({
162
+ type: 'channel_message',
163
+ channel: env.payload?.channel,
164
+ from,
165
+ body: env.payload?.body || body,
166
+ timestamp: new Date().toISOString(),
167
+ }));
168
+ }
169
+ else {
170
+ // Direct message (DELIVER)
171
+ ws.send(JSON.stringify({
172
+ type: 'direct_message',
173
+ from,
174
+ body: env.payload?.body || body,
175
+ timestamp: new Date().toISOString(),
176
+ }));
177
+ }
178
+ }
179
+ /**
180
+ * Dispose of all user sessions.
181
+ */
182
+ dispose() {
183
+ for (const [username] of this.users) {
184
+ this.unregisterUser(username);
185
+ }
186
+ console.log('[user-bridge] Disposed all user sessions');
187
+ }
188
+ }
189
+ //# sourceMappingURL=user-bridge.js.map
@@ -60,6 +60,10 @@ export interface BaseWrapperConfig {
60
60
  idleBeforeInjectMs?: number;
61
61
  /** Confidence threshold for idle detection (0-1, default: 0.7) */
62
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;
63
67
  }
64
68
  /**
65
69
  * Abstract base class for agent wrappers
@@ -63,8 +63,10 @@ export class BaseWrapper extends EventEmitter {
63
63
  workingDirectory: config.cwd,
64
64
  quiet: true,
65
65
  });
66
- // Initialize continuity manager
67
- 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
+ }
68
70
  // Initialize universal idle detector for robust injection timing
69
71
  this.idleDetector = new UniversalIdleDetector({
70
72
  minSilenceMs: config.idleBeforeInjectMs ?? DEFAULT_IDLE_BEFORE_INJECT_MS,
@@ -186,9 +188,15 @@ export class BaseWrapper extends EventEmitter {
186
188
  this.sentMessageHashes.delete(oldest);
187
189
  }
188
190
  // Only send if client ready
189
- 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})`);
190
193
  return;
191
- 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
+ }
192
200
  }
193
201
  // =========================================================================
194
202
  // Spawn/release handling
@@ -417,6 +417,7 @@ export class RelayClient {
417
417
  }
418
418
  }
419
419
  handleDeliver(envelope) {
420
+ console.log(`[relay-client:${this.config.agentName}] Received DELIVER from ${envelope.from}: "${envelope.payload.body?.substring(0, 40)}..."`);
420
421
  // Send ACK
421
422
  this.send({
422
423
  v: PROTOCOL_VERSION,
@@ -430,6 +431,7 @@ export class RelayClient {
430
431
  });
431
432
  const duplicate = this.markDelivered(envelope.id);
432
433
  if (duplicate) {
434
+ console.log(`[relay-client:${this.config.agentName}] Duplicate delivery, skipping`);
433
435
  return;
434
436
  }
435
437
  // Notify handler
@@ -437,6 +439,9 @@ export class RelayClient {
437
439
  if (this.onMessage && envelope.from) {
438
440
  this.onMessage(envelope.from, envelope.payload, envelope.id, envelope.payload_meta, envelope.delivery.originalTo);
439
441
  }
442
+ else {
443
+ console.log(`[relay-client:${this.config.agentName}] No onMessage handler or no from field`);
444
+ }
440
445
  }
441
446
  handlePing(envelope) {
442
447
  this.send({
@@ -811,12 +811,12 @@ export class OutputParser {
811
811
  shouldFilterFencedInline(target, body) {
812
812
  // Check for placeholder target names
813
813
  if (isPlaceholderTarget(target)) {
814
- console.error(`[parser] Filtered message - placeholder target: ${target}`);
814
+ // Silently filter placeholder targets (common in documentation)
815
815
  return true;
816
816
  }
817
817
  // Check for instructional body content
818
818
  if (isInstructionalText(body)) {
819
- console.error(`[parser] Filtered message to ${target} - instructional text detected. Body preview: ${body.substring(0, 100)}`);
819
+ // Silently filter instructional text (common in system prompts)
820
820
  return true;
821
821
  }
822
822
  return false;
@@ -230,9 +230,15 @@ export declare class PtyWrapper extends BaseWrapper {
230
230
  protected parseSpawnReleaseCommands(content: string): void;
231
231
  /**
232
232
  * Execute spawn via API or callback.
233
- * Overrides BaseWrapper to add PTY-specific logging and API path.
233
+ * After spawning, waits for the agent to come online and sends the task via relay.
234
234
  */
235
235
  protected executeSpawn(name: string, cli: string, task: string): Promise<void>;
236
+ /**
237
+ * Wait for a spawned agent to come online, then send the task via relay.
238
+ * Uses the wrapper's own relay client so the message comes "from" this agent,
239
+ * not from the dashboard's relay client.
240
+ */
241
+ private waitAndSendTask;
236
242
  /**
237
243
  * Execute release via API or callback.
238
244
  * Overrides BaseWrapper to add PTY-specific logging and API path.
@@ -220,6 +220,7 @@ export class PtyWrapper extends BaseWrapper {
220
220
  this.injectInstructions();
221
221
  }
222
222
  this.readyForMessages = true;
223
+ console.log(`[pty:${this.config.name}] Agent ready for messages (queueLen=${this.messageQueue.length}, interactive=${this.config.interactive})`);
223
224
  // Process any messages that arrived while waiting (skip in interactive mode)
224
225
  if (!this.config.interactive) {
225
226
  this.processMessageQueue();
@@ -228,6 +229,7 @@ export class PtyWrapper extends BaseWrapper {
228
229
  console.error(`[pty:${this.config.name}] Failed to wait for agent ready:`, err);
229
230
  // Fall back to marking ready anyway to avoid blocking forever
230
231
  this.readyForMessages = true;
232
+ console.log(`[pty:${this.config.name}] Agent ready for messages (fallback, queueLen=${this.messageQueue.length})`);
231
233
  });
232
234
  }
233
235
  /**
@@ -608,6 +610,14 @@ export class PtyWrapper extends BaseWrapper {
608
610
  // 500 chars is enough to capture most relay message headers
609
611
  const lookbackStart = Math.max(0, this.lastParsedLength - 500);
610
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
+ }
611
621
  // First, try to find fenced multi-line messages: ->relay:Target <<<\n...\n>>>
612
622
  this.parseFencedMessages(contentToParse);
613
623
  // Then parse single-line messages
@@ -985,22 +995,24 @@ export class PtyWrapper extends BaseWrapper {
985
995
  }
986
996
  /**
987
997
  * Execute spawn via API or callback.
988
- * 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.
989
999
  */
990
1000
  async executeSpawn(name, cli, task) {
991
1001
  console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] executeSpawn called: name=${name}, cli=${cli}, task="${task.substring(0, 50)}..."`);
992
1002
  console.log(`[pty:${this.config.name}] [SPAWN-DEBUG] dashboardPort=${this.config.dashboardPort}, hasOnSpawn=${!!this.config.onSpawn}`);
1003
+ let spawned = false;
993
1004
  if (this.config.dashboardPort) {
994
1005
  // Use dashboard API for spawning (works from spawned agents)
995
1006
  try {
996
1007
  const response = await fetch(`http://localhost:${this.config.dashboardPort}/api/spawn`, {
997
1008
  method: 'POST',
998
1009
  headers: { 'Content-Type': 'application/json' },
999
- body: JSON.stringify({ name, cli, task }),
1010
+ body: JSON.stringify({ name, cli }), // No task - we send it after agent is online
1000
1011
  });
1001
1012
  const result = await response.json();
1002
1013
  if (result.success) {
1003
1014
  console.log(`[pty:${this.config.name}] Spawned ${name} via API`);
1015
+ spawned = true;
1004
1016
  }
1005
1017
  else {
1006
1018
  console.error(`[pty:${this.config.name}] Spawn failed: ${result.error}`);
@@ -1014,11 +1026,57 @@ export class PtyWrapper extends BaseWrapper {
1014
1026
  // Fall back to callback
1015
1027
  try {
1016
1028
  await this.config.onSpawn(name, cli, task);
1029
+ spawned = true;
1017
1030
  }
1018
1031
  catch (err) {
1019
1032
  console.error(`[pty:${this.config.name}] Spawn failed: ${err.message}`);
1020
1033
  }
1021
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`);
1022
1080
  }
1023
1081
  /**
1024
1082
  * Execute release via API or callback.
@@ -1058,6 +1116,8 @@ export class PtyWrapper extends BaseWrapper {
1058
1116
  * Extends BaseWrapper to add PTY-specific behavior.
1059
1117
  */
1060
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})`);
1061
1121
  // Call base class to handle deduplication and queuing
1062
1122
  super.handleIncomingMessage(from, payload, messageId, meta, originalTo);
1063
1123
  // PTY-specific: Process the message queue immediately
@@ -1114,6 +1174,8 @@ export class PtyWrapper extends BaseWrapper {
1114
1174
  this.isInjecting = false;
1115
1175
  return;
1116
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})`);
1117
1179
  try {
1118
1180
  // Wait for output to stabilize before injecting
1119
1181
  await this.waitForOutputStable();
@@ -1152,9 +1214,21 @@ export class PtyWrapper extends BaseWrapper {
1152
1214
  if (!this.ptyProcess || !this.running) {
1153
1215
  throw new Error('PTY process not running');
1154
1216
  }
1155
- // Write message to PTY, then send Enter separately after a small delay
1156
- this.ptyProcess.write(inj);
1157
- 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)
1158
1232
  this.ptyProcess.write('\r');
1159
1233
  },
1160
1234
  log: (message) => console.log(`[pty:${this.config.name}] ${message}`),
@@ -1199,6 +1273,8 @@ export class PtyWrapper extends BaseWrapper {
1199
1273
  injectInstructions() {
1200
1274
  if (!this.running)
1201
1275
  return;
1276
+ if (this.config.skipInstructions)
1277
+ return;
1202
1278
  // Guard: Only inject once per session
1203
1279
  if (this.instructionsInjected) {
1204
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) */
@@ -232,9 +232,16 @@ export declare class TmuxWrapper extends BaseWrapper {
232
232
  */
233
233
  private parseSessionEndAndClose;
234
234
  /**
235
- * 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.
236
237
  */
237
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;
238
245
  /**
239
246
  * Execute release via API (if dashboardPort set) or callback
240
247
  */