@agent-relay/wrapper 2.0.13 → 2.0.15
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.
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* @see docs/RUST_WRAPPER_DESIGN.md for protocol details
|
|
17
17
|
*/
|
|
18
18
|
import { BaseWrapper, type BaseWrapperConfig } from './base-wrapper.js';
|
|
19
|
-
import type { SendPayload, SendMeta } from '@agent-relay/protocol/types';
|
|
19
|
+
import type { SendPayload, SendMeta, Envelope } from '@agent-relay/protocol/types';
|
|
20
|
+
import type { ChannelMessagePayload } from '@agent-relay/protocol/channels';
|
|
20
21
|
interface StatusResponse {
|
|
21
22
|
type: 'status';
|
|
22
23
|
agent_idle: boolean;
|
|
@@ -92,6 +93,7 @@ export declare class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
92
93
|
private lastParsedLength;
|
|
93
94
|
private isInteractive;
|
|
94
95
|
private pendingInjections;
|
|
96
|
+
private pendingSendEnter;
|
|
95
97
|
private backpressureActive;
|
|
96
98
|
private readyForMessages;
|
|
97
99
|
private throttle;
|
|
@@ -116,10 +118,12 @@ export declare class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
116
118
|
constructor(config: RelayPtyOrchestratorConfig);
|
|
117
119
|
/**
|
|
118
120
|
* Debug log - only outputs when debug is enabled
|
|
121
|
+
* Writes to log file to avoid polluting TUI output
|
|
119
122
|
*/
|
|
120
123
|
private log;
|
|
121
124
|
/**
|
|
122
125
|
* Error log - always outputs (errors are important)
|
|
126
|
+
* Writes to log file to avoid polluting TUI output
|
|
123
127
|
*/
|
|
124
128
|
private logError;
|
|
125
129
|
/**
|
|
@@ -210,6 +214,14 @@ export declare class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
210
214
|
* Disconnect from socket
|
|
211
215
|
*/
|
|
212
216
|
private disconnectSocket;
|
|
217
|
+
/** Timer for socket reconnection */
|
|
218
|
+
private socketReconnectTimer?;
|
|
219
|
+
/** Current reconnection attempt count */
|
|
220
|
+
private socketReconnectAttempt;
|
|
221
|
+
/**
|
|
222
|
+
* Schedule a socket reconnection attempt with exponential backoff
|
|
223
|
+
*/
|
|
224
|
+
private scheduleSocketReconnect;
|
|
213
225
|
/**
|
|
214
226
|
* Send a request to the socket and optionally wait for response
|
|
215
227
|
*/
|
|
@@ -224,6 +236,15 @@ export declare class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
224
236
|
* If verification fails, retries up to MAX_RETRIES times.
|
|
225
237
|
*/
|
|
226
238
|
private handleInjectResult;
|
|
239
|
+
/**
|
|
240
|
+
* Handle SendEnter result (stuck input recovery)
|
|
241
|
+
* Called when relay-pty responds to a SendEnter request
|
|
242
|
+
*/
|
|
243
|
+
private handleSendEnterResult;
|
|
244
|
+
/**
|
|
245
|
+
* Do a full retry with message content (used when SendEnter fails or for subsequent retries)
|
|
246
|
+
*/
|
|
247
|
+
private doFullRetry;
|
|
227
248
|
/**
|
|
228
249
|
* Handle backpressure notification
|
|
229
250
|
*/
|
|
@@ -240,6 +261,12 @@ export declare class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
240
261
|
* Override handleIncomingMessage to trigger queue processing
|
|
241
262
|
*/
|
|
242
263
|
protected handleIncomingMessage(from: string, payload: SendPayload, messageId: string, meta?: SendMeta, originalTo?: string): void;
|
|
264
|
+
/**
|
|
265
|
+
* Override handleIncomingChannelMessage to trigger queue processing.
|
|
266
|
+
* Without this override, channel messages would be queued but processMessageQueue()
|
|
267
|
+
* would never be called, causing messages to get stuck until the queue monitor runs.
|
|
268
|
+
*/
|
|
269
|
+
protected handleIncomingChannelMessage(from: string, channel: string, body: string, envelope: Envelope<ChannelMessagePayload>): void;
|
|
243
270
|
/**
|
|
244
271
|
* Start the queue monitor to periodically check for stuck messages.
|
|
245
272
|
* This ensures messages don't get orphaned in the queue when the agent is idle.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay-pty-orchestrator.d.ts","sourceRoot":"","sources":["../src/relay-pty-orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAeH,OAAO,EAAE,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"relay-pty-orchestrator.d.ts","sourceRoot":"","sources":["../src/relay-pty-orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ import { spawn } from 'node:child_process';
|
|
|
19
19
|
import { createConnection } from 'node:net';
|
|
20
20
|
import { createHash } from 'node:crypto';
|
|
21
21
|
import { join, dirname } from 'node:path';
|
|
22
|
-
import { existsSync, unlinkSync, mkdirSync, symlinkSync, lstatSync, rmSync, watch, readdirSync, readlinkSync } from 'node:fs';
|
|
22
|
+
import { existsSync, unlinkSync, mkdirSync, symlinkSync, lstatSync, rmSync, watch, readdirSync, readlinkSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
23
23
|
import { getProjectPaths } from '@agent-relay/config/project-namespace';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
// Get the directory where this module is located
|
|
@@ -63,6 +63,8 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
63
63
|
isInteractive = false;
|
|
64
64
|
// Injection state
|
|
65
65
|
pendingInjections = new Map();
|
|
66
|
+
// Pending SendEnter requests (for stuck input recovery)
|
|
67
|
+
pendingSendEnter = new Map();
|
|
66
68
|
backpressureActive = false;
|
|
67
69
|
readyForMessages = false;
|
|
68
70
|
// Adaptive throttle for message queue - adjusts delay based on success/failure
|
|
@@ -168,17 +170,39 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
168
170
|
}
|
|
169
171
|
/**
|
|
170
172
|
* Debug log - only outputs when debug is enabled
|
|
173
|
+
* Writes to log file to avoid polluting TUI output
|
|
171
174
|
*/
|
|
172
175
|
log(message) {
|
|
173
176
|
if (this.config.debug) {
|
|
174
|
-
|
|
177
|
+
const logLine = `${new Date().toISOString()} [relay-pty-orchestrator:${this.config.name}] ${message}\n`;
|
|
178
|
+
try {
|
|
179
|
+
const logDir = dirname(this._logPath);
|
|
180
|
+
if (!existsSync(logDir)) {
|
|
181
|
+
mkdirSync(logDir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
appendFileSync(this._logPath, logLine);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Fallback to stderr if file write fails (only during init before _logPath is set)
|
|
187
|
+
}
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
/**
|
|
178
191
|
* Error log - always outputs (errors are important)
|
|
192
|
+
* Writes to log file to avoid polluting TUI output
|
|
179
193
|
*/
|
|
180
194
|
logError(message) {
|
|
181
|
-
|
|
195
|
+
const logLine = `${new Date().toISOString()} [relay-pty-orchestrator:${this.config.name}] ERROR: ${message}\n`;
|
|
196
|
+
try {
|
|
197
|
+
const logDir = dirname(this._logPath);
|
|
198
|
+
if (!existsSync(logDir)) {
|
|
199
|
+
mkdirSync(logDir, { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
appendFileSync(this._logPath, logLine);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Fallback to stderr if file write fails (only during init before _logPath is set)
|
|
205
|
+
}
|
|
182
206
|
}
|
|
183
207
|
/**
|
|
184
208
|
* Get the outbox path for this agent (for documentation purposes)
|
|
@@ -336,6 +360,25 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
336
360
|
catch (err) {
|
|
337
361
|
this.logError(` Failed to set up outbox: ${err.message}`);
|
|
338
362
|
}
|
|
363
|
+
// Write MCP identity file so MCP servers can discover their agent name
|
|
364
|
+
// This is needed because Claude Code may not pass through env vars to MCP server processes
|
|
365
|
+
try {
|
|
366
|
+
const projectPaths = getProjectPaths(this.config.cwd);
|
|
367
|
+
const identityDir = join(projectPaths.dataDir);
|
|
368
|
+
if (!existsSync(identityDir)) {
|
|
369
|
+
mkdirSync(identityDir, { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
// Write a per-process identity file (using PPID so MCP server finds parent's identity)
|
|
372
|
+
const identityPath = join(identityDir, `mcp-identity-${process.pid}`);
|
|
373
|
+
writeFileSync(identityPath, this.config.name, 'utf-8');
|
|
374
|
+
this.log(` Wrote MCP identity file: ${identityPath}`);
|
|
375
|
+
// Also write a simple identity file (for single-agent scenarios)
|
|
376
|
+
const simpleIdentityPath = join(identityDir, 'mcp-identity');
|
|
377
|
+
writeFileSync(simpleIdentityPath, this.config.name, 'utf-8');
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
this.logError(` Failed to write MCP identity file: ${err.message}`);
|
|
381
|
+
}
|
|
339
382
|
// Find relay-pty binary
|
|
340
383
|
const binaryPath = this.findRelayPtyBinary();
|
|
341
384
|
if (!binaryPath) {
|
|
@@ -378,6 +421,11 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
378
421
|
this.stopQueueMonitor();
|
|
379
422
|
this.stopProtocolMonitor();
|
|
380
423
|
this.stopPeriodicReminder();
|
|
424
|
+
// Clear socket reconnect timer
|
|
425
|
+
if (this.socketReconnectTimer) {
|
|
426
|
+
clearTimeout(this.socketReconnectTimer);
|
|
427
|
+
this.socketReconnectTimer = undefined;
|
|
428
|
+
}
|
|
381
429
|
// Unregister from memory monitor
|
|
382
430
|
this.memoryMonitor.unregister(this.config.name);
|
|
383
431
|
if (this.memoryAlertHandler) {
|
|
@@ -492,6 +540,7 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
492
540
|
...process.env,
|
|
493
541
|
...this.config.env,
|
|
494
542
|
AGENT_RELAY_NAME: this.config.name,
|
|
543
|
+
RELAY_AGENT_NAME: this.config.name, // MCP server uses this env var
|
|
495
544
|
AGENT_RELAY_OUTBOX: this._canonicalOutboxPath, // Agents use this for outbox path
|
|
496
545
|
TERM: 'xterm-256color',
|
|
497
546
|
},
|
|
@@ -907,6 +956,15 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
907
956
|
*/
|
|
908
957
|
attemptSocketConnection(timeout) {
|
|
909
958
|
return new Promise((resolve, reject) => {
|
|
959
|
+
// Clean up any existing socket before creating new one
|
|
960
|
+
// This prevents orphaned sockets with stale event handlers
|
|
961
|
+
if (this.socket) {
|
|
962
|
+
// Remove all listeners to prevent the old socket's 'close' event
|
|
963
|
+
// from triggering another reconnect cycle
|
|
964
|
+
this.socket.removeAllListeners();
|
|
965
|
+
this.socket.destroy();
|
|
966
|
+
this.socket = undefined;
|
|
967
|
+
}
|
|
910
968
|
const timer = setTimeout(() => {
|
|
911
969
|
reject(new Error('Socket connection timeout'));
|
|
912
970
|
}, timeout);
|
|
@@ -920,9 +978,18 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
920
978
|
this.socketConnected = false;
|
|
921
979
|
reject(err);
|
|
922
980
|
});
|
|
981
|
+
// Handle 'end' event - server closed its write side (half-close)
|
|
982
|
+
this.socket.on('end', () => {
|
|
983
|
+
this.socketConnected = false;
|
|
984
|
+
this.log(` Socket received end (server closed write side)`);
|
|
985
|
+
});
|
|
923
986
|
this.socket.on('close', () => {
|
|
924
987
|
this.socketConnected = false;
|
|
925
988
|
this.log(` Socket closed`);
|
|
989
|
+
// Auto-reconnect if not intentionally stopped
|
|
990
|
+
if (this.running && !this.isGracefulStop) {
|
|
991
|
+
this.scheduleSocketReconnect();
|
|
992
|
+
}
|
|
926
993
|
});
|
|
927
994
|
// Handle incoming data (responses)
|
|
928
995
|
let buffer = '';
|
|
@@ -955,6 +1022,55 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
955
1022
|
}
|
|
956
1023
|
this.pendingInjections.clear();
|
|
957
1024
|
}
|
|
1025
|
+
/** Timer for socket reconnection */
|
|
1026
|
+
socketReconnectTimer;
|
|
1027
|
+
/** Current reconnection attempt count */
|
|
1028
|
+
socketReconnectAttempt = 0;
|
|
1029
|
+
/**
|
|
1030
|
+
* Schedule a socket reconnection attempt with exponential backoff
|
|
1031
|
+
*/
|
|
1032
|
+
scheduleSocketReconnect() {
|
|
1033
|
+
const maxAttempts = this.config.socketReconnectAttempts ?? 3;
|
|
1034
|
+
// Clear any existing timer
|
|
1035
|
+
if (this.socketReconnectTimer) {
|
|
1036
|
+
clearTimeout(this.socketReconnectTimer);
|
|
1037
|
+
this.socketReconnectTimer = undefined;
|
|
1038
|
+
}
|
|
1039
|
+
if (this.socketReconnectAttempt >= maxAttempts) {
|
|
1040
|
+
this.logError(` Socket reconnect failed after ${maxAttempts} attempts`);
|
|
1041
|
+
// Reset counter for future reconnects (processMessageQueue can trigger new cycle)
|
|
1042
|
+
this.socketReconnectAttempt = 0;
|
|
1043
|
+
// Note: socketReconnectTimer is already undefined, allowing processMessageQueue
|
|
1044
|
+
// to trigger a new reconnection cycle when new messages arrive
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
this.socketReconnectAttempt++;
|
|
1048
|
+
const delay = Math.min(1000 * Math.pow(2, this.socketReconnectAttempt - 1), 10000); // Max 10s
|
|
1049
|
+
this.log(` Scheduling socket reconnect in ${delay}ms (attempt ${this.socketReconnectAttempt}/${maxAttempts})`);
|
|
1050
|
+
this.socketReconnectTimer = setTimeout(async () => {
|
|
1051
|
+
// Clear timer reference now that callback is executing
|
|
1052
|
+
this.socketReconnectTimer = undefined;
|
|
1053
|
+
if (!this.running || this.isGracefulStop) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
try {
|
|
1057
|
+
const timeout = this.config.socketConnectTimeoutMs ?? 5000;
|
|
1058
|
+
await this.attemptSocketConnection(timeout);
|
|
1059
|
+
this.log(` Socket reconnected successfully`);
|
|
1060
|
+
this.socketReconnectAttempt = 0; // Reset on success
|
|
1061
|
+
// Process any queued messages that were waiting
|
|
1062
|
+
if (this.messageQueue.length > 0 && !this.isInjecting) {
|
|
1063
|
+
this.log(` Processing ${this.messageQueue.length} queued messages after reconnect`);
|
|
1064
|
+
this.processMessageQueue();
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
catch (err) {
|
|
1068
|
+
this.logError(` Socket reconnect attempt ${this.socketReconnectAttempt} failed: ${err.message}`);
|
|
1069
|
+
// Schedule another attempt
|
|
1070
|
+
this.scheduleSocketReconnect();
|
|
1071
|
+
}
|
|
1072
|
+
}, delay);
|
|
1073
|
+
}
|
|
958
1074
|
/**
|
|
959
1075
|
* Send a request to the socket and optionally wait for response
|
|
960
1076
|
*/
|
|
@@ -1002,6 +1118,12 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1002
1118
|
case 'shutdown_ack':
|
|
1003
1119
|
this.log(` Shutdown acknowledged`);
|
|
1004
1120
|
break;
|
|
1121
|
+
case 'send_enter_result':
|
|
1122
|
+
// Handle SendEnter result (stuck input recovery)
|
|
1123
|
+
this.handleSendEnterResult(response).catch((err) => {
|
|
1124
|
+
this.logError(` Error handling send_enter result: ${err.message}`);
|
|
1125
|
+
});
|
|
1126
|
+
break;
|
|
1005
1127
|
}
|
|
1006
1128
|
}
|
|
1007
1129
|
catch (err) {
|
|
@@ -1084,7 +1206,6 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1084
1206
|
this.log(` Message ${pending.shortId} NOT found in output after delivery`);
|
|
1085
1207
|
// Check if we should retry
|
|
1086
1208
|
if (pending.retryCount < INJECTION_CONSTANTS.MAX_RETRIES - 1) {
|
|
1087
|
-
this.log(` Retrying injection (attempt ${pending.retryCount + 2}/${INJECTION_CONSTANTS.MAX_RETRIES})`);
|
|
1088
1209
|
clearTimeout(pending.timeout);
|
|
1089
1210
|
this.pendingInjections.delete(response.id);
|
|
1090
1211
|
// Wait before retry with backoff
|
|
@@ -1104,37 +1225,54 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1104
1225
|
pending.resolve(true);
|
|
1105
1226
|
return;
|
|
1106
1227
|
}
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
this.
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1228
|
+
// On first retry attempt (retryCount === 0), try SendEnter first
|
|
1229
|
+
// This handles the case where message content was written but Enter wasn't processed
|
|
1230
|
+
if (pending.retryCount === 0) {
|
|
1231
|
+
this.log(` Trying SendEnter first for ${pending.shortId} (stuck input recovery)`);
|
|
1232
|
+
// Send just the Enter key
|
|
1233
|
+
const sendEnterRequest = {
|
|
1234
|
+
type: 'send_enter',
|
|
1235
|
+
id: response.id,
|
|
1236
|
+
};
|
|
1237
|
+
// Track this SendEnter request for verification
|
|
1238
|
+
const sendEnterTimeout = setTimeout(() => {
|
|
1239
|
+
this.logError(` SendEnter timeout for ${pending.shortId}`);
|
|
1240
|
+
this.pendingSendEnter.delete(response.id);
|
|
1241
|
+
// Fall back to full retry after SendEnter timeout
|
|
1242
|
+
this.doFullRetry(response.id, pending);
|
|
1243
|
+
}, 5000); // 5 second timeout for SendEnter
|
|
1244
|
+
this.pendingSendEnter.set(response.id, {
|
|
1245
|
+
resolve: (verified) => {
|
|
1246
|
+
if (verified) {
|
|
1247
|
+
// SendEnter worked!
|
|
1248
|
+
this.injectionMetrics.successWithRetry++;
|
|
1249
|
+
this.injectionMetrics.total++;
|
|
1250
|
+
pending.resolve(true);
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
// SendEnter didn't work, do full retry
|
|
1254
|
+
this.doFullRetry(response.id, pending);
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
timeout: sendEnterTimeout,
|
|
1258
|
+
from: pending.from,
|
|
1259
|
+
shortId: pending.shortId,
|
|
1260
|
+
retryCount: pending.retryCount,
|
|
1261
|
+
originalBody: pending.originalBody,
|
|
1262
|
+
originalResolve: pending.resolve,
|
|
1263
|
+
});
|
|
1264
|
+
this.sendSocketRequest(sendEnterRequest).catch((err) => {
|
|
1265
|
+
this.logError(` SendEnter request failed: ${err.message}`);
|
|
1266
|
+
clearTimeout(sendEnterTimeout);
|
|
1267
|
+
this.pendingSendEnter.delete(response.id);
|
|
1268
|
+
// Fall back to full retry
|
|
1269
|
+
this.doFullRetry(response.id, pending);
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
// On subsequent retries (retryCount > 0), do full retry directly
|
|
1274
|
+
this.doFullRetry(response.id, pending);
|
|
1275
|
+
}
|
|
1138
1276
|
}
|
|
1139
1277
|
else {
|
|
1140
1278
|
// Max retries exceeded
|
|
@@ -1167,6 +1305,77 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1167
1305
|
}
|
|
1168
1306
|
// queued/injecting are intermediate states - wait for final status
|
|
1169
1307
|
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Handle SendEnter result (stuck input recovery)
|
|
1310
|
+
* Called when relay-pty responds to a SendEnter request
|
|
1311
|
+
*/
|
|
1312
|
+
async handleSendEnterResult(response) {
|
|
1313
|
+
this.log(` handleSendEnterResult: id=${response.id.substring(0, 8)} success=${response.success}`);
|
|
1314
|
+
const pendingEnter = this.pendingSendEnter.get(response.id);
|
|
1315
|
+
if (!pendingEnter) {
|
|
1316
|
+
this.log(` No pending SendEnter found for ${response.id.substring(0, 8)}`);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
clearTimeout(pendingEnter.timeout);
|
|
1320
|
+
this.pendingSendEnter.delete(response.id);
|
|
1321
|
+
if (!response.success) {
|
|
1322
|
+
this.log(` SendEnter failed for ${pendingEnter.shortId}, will try full retry`);
|
|
1323
|
+
pendingEnter.resolve(false);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
// SendEnter succeeded - wait and verify
|
|
1327
|
+
this.log(` SendEnter sent for ${pendingEnter.shortId}, waiting to verify...`);
|
|
1328
|
+
await sleep(150); // Give time for Enter to be processed
|
|
1329
|
+
// Verify the message appeared in output
|
|
1330
|
+
const verified = await verifyInjection(pendingEnter.shortId, pendingEnter.from, async () => this.getCleanOutput());
|
|
1331
|
+
if (verified) {
|
|
1332
|
+
this.log(` Message ${pendingEnter.shortId} verified after SendEnter ✓`);
|
|
1333
|
+
pendingEnter.resolve(true);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
this.log(` Message ${pendingEnter.shortId} still not verified after SendEnter, will try full retry`);
|
|
1337
|
+
pendingEnter.resolve(false);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Do a full retry with message content (used when SendEnter fails or for subsequent retries)
|
|
1342
|
+
*/
|
|
1343
|
+
doFullRetry(messageId, pending) {
|
|
1344
|
+
this.log(` Doing full retry for ${pending.shortId} (attempt ${pending.retryCount + 2}/${INJECTION_CONSTANTS.MAX_RETRIES})`);
|
|
1345
|
+
// Re-inject by sending another socket request
|
|
1346
|
+
// Prepend [RETRY] to help agent notice this is a retry
|
|
1347
|
+
const retryBody = pending.originalBody.startsWith('[RETRY]')
|
|
1348
|
+
? pending.originalBody
|
|
1349
|
+
: `[RETRY] ${pending.originalBody}`;
|
|
1350
|
+
const retryRequest = {
|
|
1351
|
+
type: 'inject',
|
|
1352
|
+
id: messageId,
|
|
1353
|
+
from: pending.from,
|
|
1354
|
+
body: retryBody,
|
|
1355
|
+
priority: 1, // Higher priority for retries
|
|
1356
|
+
};
|
|
1357
|
+
// Create new pending entry with incremented retry count
|
|
1358
|
+
const newTimeout = setTimeout(() => {
|
|
1359
|
+
this.logError(` Retry timeout for ${pending.shortId}`);
|
|
1360
|
+
this.pendingInjections.delete(messageId);
|
|
1361
|
+
pending.resolve(false);
|
|
1362
|
+
}, 30000);
|
|
1363
|
+
this.pendingInjections.set(messageId, {
|
|
1364
|
+
resolve: pending.resolve,
|
|
1365
|
+
reject: pending.reject,
|
|
1366
|
+
timeout: newTimeout,
|
|
1367
|
+
from: pending.from,
|
|
1368
|
+
shortId: pending.shortId,
|
|
1369
|
+
retryCount: pending.retryCount + 1,
|
|
1370
|
+
originalBody: retryBody,
|
|
1371
|
+
});
|
|
1372
|
+
this.sendSocketRequest(retryRequest).catch((err) => {
|
|
1373
|
+
this.logError(` Full retry request failed: ${err.message}`);
|
|
1374
|
+
clearTimeout(newTimeout);
|
|
1375
|
+
this.pendingInjections.delete(messageId);
|
|
1376
|
+
pending.resolve(false);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1170
1379
|
/**
|
|
1171
1380
|
* Handle backpressure notification
|
|
1172
1381
|
*/
|
|
@@ -1240,12 +1449,42 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1240
1449
|
* Process queued messages
|
|
1241
1450
|
*/
|
|
1242
1451
|
async processMessageQueue() {
|
|
1243
|
-
|
|
1244
|
-
|
|
1452
|
+
// Debug: Log blocking conditions when queue has messages
|
|
1453
|
+
if (this.messageQueue.length > 0) {
|
|
1454
|
+
if (!this.readyForMessages) {
|
|
1455
|
+
this.log(` Queue blocked: readyForMessages=false (queue=${this.messageQueue.length})`);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (this.backpressureActive) {
|
|
1459
|
+
this.log(` Queue blocked: backpressure active (queue=${this.messageQueue.length})`);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (this.isInjecting) {
|
|
1463
|
+
// Already injecting - the finally block will process next message
|
|
1464
|
+
// But add a safety timeout in case injection gets stuck
|
|
1465
|
+
const elapsed = this.injectionStartTime > 0 ? Date.now() - this.injectionStartTime : 0;
|
|
1466
|
+
if (elapsed > 35000) {
|
|
1467
|
+
this.logError(` Injection stuck for ${elapsed}ms, forcing reset`);
|
|
1468
|
+
this.isInjecting = false;
|
|
1469
|
+
this.injectionStartTime = 0;
|
|
1470
|
+
}
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1245
1473
|
}
|
|
1246
1474
|
if (this.messageQueue.length === 0) {
|
|
1247
1475
|
return;
|
|
1248
1476
|
}
|
|
1477
|
+
// Proactively reconnect socket if disconnected and we have messages to send
|
|
1478
|
+
if (!this.socketConnected && !this.socketReconnectTimer) {
|
|
1479
|
+
this.log(` Socket disconnected, triggering reconnect before processing queue`);
|
|
1480
|
+
this.scheduleSocketReconnect();
|
|
1481
|
+
return; // Wait for reconnection to complete
|
|
1482
|
+
}
|
|
1483
|
+
if (!this.socketConnected) {
|
|
1484
|
+
// Reconnection in progress, wait for it
|
|
1485
|
+
this.log(` Queue waiting: socket reconnecting (queue=${this.messageQueue.length})`);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1249
1488
|
// Check if agent is in editor mode - delay injection if so
|
|
1250
1489
|
const idleResult = this.idleDetector.checkIdle();
|
|
1251
1490
|
if (idleResult.inEditorMode) {
|
|
@@ -1304,6 +1543,18 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1304
1543
|
this.log(` Queue length after add: ${this.messageQueue.length}`);
|
|
1305
1544
|
this.processMessageQueue();
|
|
1306
1545
|
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Override handleIncomingChannelMessage to trigger queue processing.
|
|
1548
|
+
* Without this override, channel messages would be queued but processMessageQueue()
|
|
1549
|
+
* would never be called, causing messages to get stuck until the queue monitor runs.
|
|
1550
|
+
*/
|
|
1551
|
+
handleIncomingChannelMessage(from, channel, body, envelope) {
|
|
1552
|
+
this.log(` === CHANNEL MESSAGE RECEIVED: ${envelope.id.substring(0, 8)} from ${from} on ${channel} ===`);
|
|
1553
|
+
this.log(` Body preview: ${body?.substring(0, 100) ?? '(no body)'}...`);
|
|
1554
|
+
super.handleIncomingChannelMessage(from, channel, body, envelope);
|
|
1555
|
+
this.log(` Queue length after add: ${this.messageQueue.length}`);
|
|
1556
|
+
this.processMessageQueue();
|
|
1557
|
+
}
|
|
1307
1558
|
// =========================================================================
|
|
1308
1559
|
// Queue monitor - Detect and process stuck messages
|
|
1309
1560
|
// =========================================================================
|
|
@@ -1901,6 +2152,10 @@ Then output: \`->relay-file:spawn\`
|
|
|
1901
2152
|
*/
|
|
1902
2153
|
async kill() {
|
|
1903
2154
|
this.isGracefulStop = true; // Mark as intentional to prevent crash broadcast
|
|
2155
|
+
if (this.socketReconnectTimer) {
|
|
2156
|
+
clearTimeout(this.socketReconnectTimer);
|
|
2157
|
+
this.socketReconnectTimer = undefined;
|
|
2158
|
+
}
|
|
1904
2159
|
if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
|
|
1905
2160
|
this.relayPtyProcess.kill('SIGKILL');
|
|
1906
2161
|
}
|