agent-relay 2.0.12 → 2.0.14
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.
- package/bin/relay-pty-darwin-arm64 +0 -0
- package/bin/relay-pty-darwin-x64 +0 -0
- package/bin/relay-pty-linux-x64 +0 -0
- package/deploy/workspace/codex.config.toml +5 -0
- package/deploy/workspace/entrypoint.sh +10 -2
- package/dist/dashboard/out/404.html +1 -1
- package/dist/dashboard/out/app/onboarding.html +1 -1
- package/dist/dashboard/out/app/onboarding.txt +1 -1
- package/dist/dashboard/out/app.html +1 -1
- package/dist/dashboard/out/app.txt +1 -1
- package/dist/dashboard/out/cloud/link.html +1 -1
- package/dist/dashboard/out/cloud/link.txt +1 -1
- package/dist/dashboard/out/connect-repos.html +1 -1
- package/dist/dashboard/out/connect-repos.txt +1 -1
- package/dist/dashboard/out/history.html +1 -1
- package/dist/dashboard/out/history.txt +1 -1
- package/dist/dashboard/out/index.html +1 -1
- package/dist/dashboard/out/index.txt +1 -1
- package/dist/dashboard/out/login.html +1 -1
- package/dist/dashboard/out/login.txt +1 -1
- package/dist/dashboard/out/metrics.html +1 -1
- package/dist/dashboard/out/metrics.txt +1 -1
- package/dist/dashboard/out/pricing.html +1 -1
- package/dist/dashboard/out/pricing.txt +1 -1
- package/dist/dashboard/out/providers/setup/claude.html +1 -1
- package/dist/dashboard/out/providers/setup/claude.txt +1 -1
- package/dist/dashboard/out/providers/setup/codex.html +1 -1
- package/dist/dashboard/out/providers/setup/codex.txt +1 -1
- package/dist/dashboard/out/providers/setup/cursor.html +1 -1
- package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
- package/dist/dashboard/out/providers.html +1 -1
- package/dist/dashboard/out/providers.txt +1 -1
- package/dist/dashboard/out/signup.html +1 -1
- package/dist/dashboard/out/signup.txt +1 -1
- package/dist/src/cli/index.js +131 -21
- package/package.json +20 -19
- package/packages/api-types/package.json +1 -1
- package/packages/bridge/dist/index.d.ts +1 -1
- package/packages/bridge/dist/index.js +1 -1
- package/packages/bridge/dist/spawner.d.ts +18 -0
- package/packages/bridge/dist/spawner.js +144 -39
- package/packages/bridge/package.json +8 -7
- package/packages/cloud/package.json +6 -6
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +1 -1
- package/packages/daemon/dist/connection.js +5 -1
- package/packages/daemon/dist/relay-ledger.d.ts +3 -1
- package/packages/daemon/dist/relay-ledger.js +8 -2
- package/packages/daemon/dist/router.js +13 -0
- package/packages/daemon/dist/server.d.ts +7 -0
- package/packages/daemon/dist/server.js +338 -4
- package/packages/daemon/package.json +12 -12
- package/packages/dashboard/dist/server.js +29 -5
- package/packages/dashboard/package.json +13 -12
- package/packages/dashboard/ui-dist/404.html +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
- package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
- package/packages/dashboard/ui-dist/app.html +1 -1
- package/packages/dashboard/ui-dist/app.txt +1 -1
- package/packages/dashboard/ui-dist/cloud/link.html +1 -1
- package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
- package/packages/dashboard/ui-dist/connect-repos.html +1 -1
- package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
- package/packages/dashboard/ui-dist/history.html +1 -1
- package/packages/dashboard/ui-dist/history.txt +1 -1
- package/packages/dashboard/ui-dist/index.html +1 -1
- package/packages/dashboard/ui-dist/index.txt +1 -1
- package/packages/dashboard/ui-dist/login.html +1 -1
- package/packages/dashboard/ui-dist/login.txt +1 -1
- package/packages/dashboard/ui-dist/metrics.html +1 -1
- package/packages/dashboard/ui-dist/metrics.txt +1 -1
- package/packages/dashboard/ui-dist/pricing.html +1 -1
- package/packages/dashboard/ui-dist/pricing.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
- package/packages/dashboard/ui-dist/providers.html +1 -1
- package/packages/dashboard/ui-dist/providers.txt +1 -1
- package/packages/dashboard/ui-dist/signup.html +1 -1
- package/packages/dashboard/ui-dist/signup.txt +1 -1
- package/packages/dashboard-server/dist/server.js +29 -5
- package/packages/dashboard-server/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/README.md +24 -3
- package/packages/mcp/dist/bin.js +13 -5
- package/packages/mcp/dist/client.d.ts +54 -1
- package/packages/mcp/dist/client.js +132 -18
- package/packages/mcp/dist/cloud.d.ts +12 -0
- package/packages/mcp/dist/cloud.js +125 -1
- package/packages/mcp/dist/file-transport.d.ts +97 -0
- package/packages/mcp/dist/file-transport.js +197 -0
- package/packages/mcp/dist/hybrid-client.d.ts +28 -0
- package/packages/mcp/dist/hybrid-client.js +159 -0
- package/packages/mcp/dist/index.d.ts +4 -2
- package/packages/mcp/dist/index.js +6 -2
- package/packages/mcp/dist/install.d.ts +23 -1
- package/packages/mcp/dist/install.js +229 -31
- package/packages/mcp/dist/server.js +7 -1
- package/packages/mcp/dist/simple.d.ts +1 -1
- package/packages/mcp/dist/tools/index.d.ts +1 -0
- package/packages/mcp/dist/tools/index.js +1 -0
- package/packages/mcp/dist/tools/relay-continuity.d.ts +35 -0
- package/packages/mcp/dist/tools/relay-continuity.js +101 -0
- package/packages/mcp/dist/tools/relay-health.d.ts +1 -4
- package/packages/mcp/dist/tools/relay-health.js +7 -15
- package/packages/mcp/dist/tools/relay-logs.js +4 -2
- package/packages/mcp/dist/tools/relay-metrics.d.ts +1 -4
- package/packages/mcp/dist/tools/relay-metrics.js +4 -15
- package/packages/mcp/dist/tools/relay-send.d.ts +2 -2
- package/packages/mcp/package.json +3 -2
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/dist/relay-pty-schemas.d.ts +14 -0
- package/packages/protocol/dist/types.d.ts +152 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/dist/client.js +7 -0
- package/packages/sdk/package.json +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/dist/logger.js +3 -1
- package/packages/utils/package.json +1 -1
- package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +28 -1
- package/packages/wrapper/dist/relay-pty-orchestrator.js +358 -43
- package/packages/wrapper/package.json +6 -6
- package/scripts/demos/README.md +79 -0
- package/scripts/demos/server-capacity.sh +69 -0
- package/scripts/demos/sprint-planning.sh +73 -0
- /package/dist/dashboard/out/_next/static/{h1U3qU5XIfQSy46M_SDsz → RgEj_9Y-mWbLaxggzni-X}/_buildManifest.js +0 -0
- /package/dist/dashboard/out/_next/static/{h1U3qU5XIfQSy46M_SDsz → RgEj_9Y-mWbLaxggzni-X}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{4WryIM4xHT22BbJ46YITr → RgEj_9Y-mWbLaxggzni-X}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{4WryIM4xHT22BbJ46YITr → RgEj_9Y-mWbLaxggzni-X}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{dS0EgrS-iG-_pkUVhBypz → UkLmDJOkaPWU2PaNQnkx5}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{dS0EgrS-iG-_pkUVhBypz → UkLmDJOkaPWU2PaNQnkx5}/_ssgManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{h1U3qU5XIfQSy46M_SDsz → bv9xidgU2pXi7xxPoCAK-}/_buildManifest.js +0 -0
- /package/packages/dashboard/ui-dist/_next/static/{h1U3qU5XIfQSy46M_SDsz → bv9xidgU2pXi7xxPoCAK-}/_ssgManifest.js +0 -0
|
@@ -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 } 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)
|
|
@@ -239,22 +263,82 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
239
263
|
if (!existsSync(linkParent)) {
|
|
240
264
|
mkdirSync(linkParent, { recursive: true });
|
|
241
265
|
}
|
|
242
|
-
if (
|
|
266
|
+
// Remove existing path if it exists (file, symlink, or directory)
|
|
267
|
+
// Use lstatSync instead of existsSync to detect broken symlinks
|
|
268
|
+
// (existsSync returns false for broken symlinks, but the symlink itself still exists)
|
|
269
|
+
let pathExists = false;
|
|
270
|
+
try {
|
|
271
|
+
lstatSync(linkPath);
|
|
272
|
+
pathExists = true;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// Path doesn't exist at all - proceed to create symlink
|
|
276
|
+
}
|
|
277
|
+
if (pathExists) {
|
|
243
278
|
try {
|
|
244
279
|
const stats = lstatSync(linkPath);
|
|
245
|
-
if (stats.isSymbolicLink()
|
|
280
|
+
if (stats.isSymbolicLink()) {
|
|
281
|
+
// Handle both valid and broken symlinks
|
|
282
|
+
try {
|
|
283
|
+
const currentTarget = readlinkSync(linkPath);
|
|
284
|
+
if (currentTarget === targetPath) {
|
|
285
|
+
// Symlink already points to correct target, no need to recreate
|
|
286
|
+
this.log(` Symlink already exists and is correct: ${linkPath} -> ${targetPath}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Broken symlink (target doesn't exist) - remove it
|
|
292
|
+
this.log(` Removing broken symlink: ${linkPath}`);
|
|
293
|
+
}
|
|
294
|
+
unlinkSync(linkPath);
|
|
295
|
+
}
|
|
296
|
+
else if (stats.isFile()) {
|
|
246
297
|
unlinkSync(linkPath);
|
|
247
298
|
}
|
|
248
299
|
else if (stats.isDirectory()) {
|
|
300
|
+
// Force remove directory - this is critical for fixing existing directories
|
|
249
301
|
rmSync(linkPath, { recursive: true, force: true });
|
|
302
|
+
// Verify removal succeeded using lstatSync to catch broken symlinks
|
|
303
|
+
try {
|
|
304
|
+
lstatSync(linkPath);
|
|
305
|
+
throw new Error(`Failed to remove existing directory: ${linkPath}`);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (err.code !== 'ENOENT') {
|
|
309
|
+
throw err; // Re-throw if it's not a "doesn't exist" error
|
|
310
|
+
}
|
|
311
|
+
// Path successfully removed
|
|
312
|
+
}
|
|
250
313
|
}
|
|
251
314
|
}
|
|
252
|
-
catch {
|
|
253
|
-
//
|
|
315
|
+
catch (err) {
|
|
316
|
+
// Log cleanup errors instead of silently ignoring them
|
|
317
|
+
this.logError(` Failed to clean up existing path ${linkPath}: ${err.message}`);
|
|
318
|
+
throw err; // Re-throw to prevent symlink creation on failed cleanup
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Create the symlink
|
|
322
|
+
try {
|
|
323
|
+
symlinkSync(targetPath, linkPath);
|
|
324
|
+
// Verify symlink was created correctly
|
|
325
|
+
if (!existsSync(linkPath)) {
|
|
326
|
+
throw new Error(`Symlink creation failed: ${linkPath}`);
|
|
327
|
+
}
|
|
328
|
+
const verifyStats = lstatSync(linkPath);
|
|
329
|
+
if (!verifyStats.isSymbolicLink()) {
|
|
330
|
+
throw new Error(`Created path is not a symlink: ${linkPath}`);
|
|
254
331
|
}
|
|
332
|
+
const verifyTarget = readlinkSync(linkPath);
|
|
333
|
+
if (verifyTarget !== targetPath) {
|
|
334
|
+
throw new Error(`Symlink points to wrong target: expected ${targetPath}, got ${verifyTarget}`);
|
|
335
|
+
}
|
|
336
|
+
this.log(` Created symlink: ${linkPath} -> ${targetPath}`);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
this.logError(` Failed to create symlink ${linkPath} -> ${targetPath}: ${err.message}`);
|
|
340
|
+
throw err;
|
|
255
341
|
}
|
|
256
|
-
symlinkSync(targetPath, linkPath);
|
|
257
|
-
this.log(` Created symlink: ${linkPath} -> ${targetPath}`);
|
|
258
342
|
};
|
|
259
343
|
// In workspace mode, create symlinks so agents can use canonical path
|
|
260
344
|
if (this._workspaceId) {
|
|
@@ -276,6 +360,25 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
276
360
|
catch (err) {
|
|
277
361
|
this.logError(` Failed to set up outbox: ${err.message}`);
|
|
278
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
|
+
}
|
|
279
382
|
// Find relay-pty binary
|
|
280
383
|
const binaryPath = this.findRelayPtyBinary();
|
|
281
384
|
if (!binaryPath) {
|
|
@@ -318,6 +421,11 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
318
421
|
this.stopQueueMonitor();
|
|
319
422
|
this.stopProtocolMonitor();
|
|
320
423
|
this.stopPeriodicReminder();
|
|
424
|
+
// Clear socket reconnect timer
|
|
425
|
+
if (this.socketReconnectTimer) {
|
|
426
|
+
clearTimeout(this.socketReconnectTimer);
|
|
427
|
+
this.socketReconnectTimer = undefined;
|
|
428
|
+
}
|
|
321
429
|
// Unregister from memory monitor
|
|
322
430
|
this.memoryMonitor.unregister(this.config.name);
|
|
323
431
|
if (this.memoryAlertHandler) {
|
|
@@ -432,6 +540,7 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
432
540
|
...process.env,
|
|
433
541
|
...this.config.env,
|
|
434
542
|
AGENT_RELAY_NAME: this.config.name,
|
|
543
|
+
RELAY_AGENT_NAME: this.config.name, // MCP server uses this env var
|
|
435
544
|
AGENT_RELAY_OUTBOX: this._canonicalOutboxPath, // Agents use this for outbox path
|
|
436
545
|
TERM: 'xterm-256color',
|
|
437
546
|
},
|
|
@@ -847,6 +956,15 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
847
956
|
*/
|
|
848
957
|
attemptSocketConnection(timeout) {
|
|
849
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
|
+
}
|
|
850
968
|
const timer = setTimeout(() => {
|
|
851
969
|
reject(new Error('Socket connection timeout'));
|
|
852
970
|
}, timeout);
|
|
@@ -860,9 +978,18 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
860
978
|
this.socketConnected = false;
|
|
861
979
|
reject(err);
|
|
862
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
|
+
});
|
|
863
986
|
this.socket.on('close', () => {
|
|
864
987
|
this.socketConnected = false;
|
|
865
988
|
this.log(` Socket closed`);
|
|
989
|
+
// Auto-reconnect if not intentionally stopped
|
|
990
|
+
if (this.running && !this.isGracefulStop) {
|
|
991
|
+
this.scheduleSocketReconnect();
|
|
992
|
+
}
|
|
866
993
|
});
|
|
867
994
|
// Handle incoming data (responses)
|
|
868
995
|
let buffer = '';
|
|
@@ -895,6 +1022,55 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
895
1022
|
}
|
|
896
1023
|
this.pendingInjections.clear();
|
|
897
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
|
+
}
|
|
898
1074
|
/**
|
|
899
1075
|
* Send a request to the socket and optionally wait for response
|
|
900
1076
|
*/
|
|
@@ -942,6 +1118,12 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
942
1118
|
case 'shutdown_ack':
|
|
943
1119
|
this.log(` Shutdown acknowledged`);
|
|
944
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;
|
|
945
1127
|
}
|
|
946
1128
|
}
|
|
947
1129
|
catch (err) {
|
|
@@ -1024,7 +1206,6 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1024
1206
|
this.log(` Message ${pending.shortId} NOT found in output after delivery`);
|
|
1025
1207
|
// Check if we should retry
|
|
1026
1208
|
if (pending.retryCount < INJECTION_CONSTANTS.MAX_RETRIES - 1) {
|
|
1027
|
-
this.log(` Retrying injection (attempt ${pending.retryCount + 2}/${INJECTION_CONSTANTS.MAX_RETRIES})`);
|
|
1028
1209
|
clearTimeout(pending.timeout);
|
|
1029
1210
|
this.pendingInjections.delete(response.id);
|
|
1030
1211
|
// Wait before retry with backoff
|
|
@@ -1044,37 +1225,54 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1044
1225
|
pending.resolve(true);
|
|
1045
1226
|
return;
|
|
1046
1227
|
}
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
this.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
+
}
|
|
1078
1276
|
}
|
|
1079
1277
|
else {
|
|
1080
1278
|
// Max retries exceeded
|
|
@@ -1107,6 +1305,77 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1107
1305
|
}
|
|
1108
1306
|
// queued/injecting are intermediate states - wait for final status
|
|
1109
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
|
+
}
|
|
1110
1379
|
/**
|
|
1111
1380
|
* Handle backpressure notification
|
|
1112
1381
|
*/
|
|
@@ -1180,12 +1449,42 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1180
1449
|
* Process queued messages
|
|
1181
1450
|
*/
|
|
1182
1451
|
async processMessageQueue() {
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
+
}
|
|
1185
1473
|
}
|
|
1186
1474
|
if (this.messageQueue.length === 0) {
|
|
1187
1475
|
return;
|
|
1188
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
|
+
}
|
|
1189
1488
|
// Check if agent is in editor mode - delay injection if so
|
|
1190
1489
|
const idleResult = this.idleDetector.checkIdle();
|
|
1191
1490
|
if (idleResult.inEditorMode) {
|
|
@@ -1244,6 +1543,18 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1244
1543
|
this.log(` Queue length after add: ${this.messageQueue.length}`);
|
|
1245
1544
|
this.processMessageQueue();
|
|
1246
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
|
+
}
|
|
1247
1558
|
// =========================================================================
|
|
1248
1559
|
// Queue monitor - Detect and process stuck messages
|
|
1249
1560
|
// =========================================================================
|
|
@@ -1841,6 +2152,10 @@ Then output: \`->relay-file:spawn\`
|
|
|
1841
2152
|
*/
|
|
1842
2153
|
async kill() {
|
|
1843
2154
|
this.isGracefulStop = true; // Mark as intentional to prevent crash broadcast
|
|
2155
|
+
if (this.socketReconnectTimer) {
|
|
2156
|
+
clearTimeout(this.socketReconnectTimer);
|
|
2157
|
+
this.socketReconnectTimer = undefined;
|
|
2158
|
+
}
|
|
1844
2159
|
if (this.relayPtyProcess && !this.relayPtyProcess.killed) {
|
|
1845
2160
|
this.relayPtyProcess.kill('SIGKILL');
|
|
1846
2161
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/wrapper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
4
4
|
"description": "CLI agent wrappers for Agent Relay - tmux, pty integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"clean": "rm -rf dist"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@agent-relay/api-types": "2.0.
|
|
34
|
-
"@agent-relay/protocol": "2.0.
|
|
35
|
-
"@agent-relay/config": "2.0.
|
|
36
|
-
"@agent-relay/continuity": "2.0.
|
|
37
|
-
"@agent-relay/resiliency": "2.0.
|
|
33
|
+
"@agent-relay/api-types": "2.0.14",
|
|
34
|
+
"@agent-relay/protocol": "2.0.14",
|
|
35
|
+
"@agent-relay/config": "2.0.14",
|
|
36
|
+
"@agent-relay/continuity": "2.0.14",
|
|
37
|
+
"@agent-relay/resiliency": "2.0.14"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"typescript": "^5.9.3",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Multi-Agent Negotiation Demos
|
|
2
|
+
|
|
3
|
+
Two demos showcasing agents with competing priorities negotiating limited resources using the full agent-relay system.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Build agent-relay and link globally
|
|
9
|
+
cd /path/to/relay
|
|
10
|
+
npm run build
|
|
11
|
+
npm link
|
|
12
|
+
|
|
13
|
+
# Verify
|
|
14
|
+
agent-relay --version
|
|
15
|
+
|
|
16
|
+
# Ensure claude CLI is authenticated
|
|
17
|
+
claude --version
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## The Demos
|
|
21
|
+
|
|
22
|
+
### 1. Server Capacity (Emergency)
|
|
23
|
+
|
|
24
|
+
**Scenario**: Black Friday traffic spike. 3 services compete for 10 emergency server slots.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
./scripts/demos/server-capacity.sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### 2. Sprint Planning
|
|
33
|
+
|
|
34
|
+
**Scenario**: 50 story points available, 85 requested. Product Lead facilitates.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
./scripts/demos/sprint-planning.sh
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Running a Demo
|
|
43
|
+
|
|
44
|
+
### Step 1: Start Relay Daemon
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
agent-relay up --dashboard
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Open http://localhost:3888 to watch the conversation.
|
|
51
|
+
|
|
52
|
+
### Step 2: Run Setup Script
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
./scripts/demos/server-capacity.sh
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This creates a prompt file in `/tmp/agent-relay-demos/`.
|
|
59
|
+
|
|
60
|
+
### Step 3: Start Agents
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Terminal 2
|
|
64
|
+
agent-relay -n WebAPI claude
|
|
65
|
+
# Say: Read /tmp/agent-relay-demos/server-capacity.md - you are WebAPI. Join #incident channel.
|
|
66
|
+
|
|
67
|
+
# Terminal 3
|
|
68
|
+
agent-relay -n BatchJobs claude
|
|
69
|
+
# Say: Read /tmp/agent-relay-demos/server-capacity.md - you are BatchJobs. Join #incident channel.
|
|
70
|
+
|
|
71
|
+
# Terminal 4
|
|
72
|
+
agent-relay -n Analytics claude
|
|
73
|
+
# Say: Read /tmp/agent-relay-demos/server-capacity.md - you are Analytics. Join #incident channel.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What You'll See
|
|
77
|
+
|
|
78
|
+
- **Dashboard**: Agents connect, messages flow in real-time
|
|
79
|
+
- **Terminals**: Agents negotiate, propose allocations, vote on outcomes
|