@agent-relay/wrapper 2.0.18 → 2.0.20
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/dist/base-wrapper.d.ts.map +1 -1
- package/dist/base-wrapper.js +27 -10
- package/dist/base-wrapper.js.map +1 -1
- package/dist/idle-detector.d.ts +4 -0
- package/dist/idle-detector.d.ts.map +1 -1
- package/dist/idle-detector.js +21 -8
- package/dist/idle-detector.js.map +1 -1
- package/dist/relay-pty-orchestrator.d.ts +5 -0
- package/dist/relay-pty-orchestrator.d.ts.map +1 -1
- package/dist/relay-pty-orchestrator.js +60 -5
- package/dist/relay-pty-orchestrator.js.map +1 -1
- package/dist/tmux-wrapper.d.ts.map +1 -1
- package/dist/tmux-wrapper.js +16 -0
- package/dist/tmux-wrapper.js.map +1 -1
- package/package.json +7 -7
- package/src/base-wrapper.test.ts +49 -0
- package/src/base-wrapper.ts +25 -10
- package/src/idle-detector.test.ts +28 -0
- package/src/idle-detector.ts +22 -8
- package/src/relay-pty-orchestrator.test.ts +4 -0
- package/src/relay-pty-orchestrator.ts +69 -7
- package/src/tmux-wrapper.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/wrapper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
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.20",
|
|
34
|
+
"@agent-relay/protocol": "2.0.20",
|
|
35
|
+
"@agent-relay/config": "2.0.20",
|
|
36
|
+
"@agent-relay/continuity": "2.0.20",
|
|
37
|
+
"@agent-relay/resiliency": "2.0.20"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"typescript": "^5.9.3",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"url": "git+https://github.com/AgentWorkforce/relay.git",
|
|
60
60
|
"directory": "packages/wrapper"
|
|
61
61
|
},
|
|
62
|
-
"license": "
|
|
62
|
+
"license": "Apache-2.0"
|
|
63
63
|
}
|
package/src/base-wrapper.test.ts
CHANGED
|
@@ -17,11 +17,16 @@ vi.mock('./client.js', () => ({
|
|
|
17
17
|
name,
|
|
18
18
|
state: 'READY' as string,
|
|
19
19
|
sentMessages: [] as Array<{ to: string; body: string; kind: string; meta?: unknown }>,
|
|
20
|
+
sentChannelMessages: [] as Array<{ channel: string; body: string; options?: unknown }>,
|
|
20
21
|
onMessage: null as ((from: string, payload: any, messageId: string, meta?: any, originalTo?: string) => void) | null,
|
|
21
22
|
sendMessage: vi.fn().mockImplementation(function(this: any, to: string, body: string, kind: string, meta?: unknown) {
|
|
22
23
|
this.sentMessages.push({ to, body, kind, meta });
|
|
23
24
|
return true;
|
|
24
25
|
}),
|
|
26
|
+
sendChannelMessage: vi.fn().mockImplementation(function(this: any, channel: string, body: string, options?: unknown) {
|
|
27
|
+
this.sentChannelMessages.push({ channel, body, options });
|
|
28
|
+
return true;
|
|
29
|
+
}),
|
|
25
30
|
destroy: vi.fn(),
|
|
26
31
|
})),
|
|
27
32
|
}));
|
|
@@ -462,6 +467,50 @@ describe('BaseWrapper', () => {
|
|
|
462
467
|
|
|
463
468
|
expect(wrapper.testClient.sentMessages).toHaveLength(0);
|
|
464
469
|
});
|
|
470
|
+
|
|
471
|
+
it('does not add hash when client not ready (allows retry)', () => {
|
|
472
|
+
// First attempt - client not ready
|
|
473
|
+
wrapper.testClient.state = 'CONNECTING';
|
|
474
|
+
wrapper.testSendRelayCommand({ to: 'ReceiverAgent', body: 'Hello' });
|
|
475
|
+
expect(wrapper.testClient.sentMessages).toHaveLength(0);
|
|
476
|
+
expect(wrapper.testSentMessageHashes.size).toBe(0);
|
|
477
|
+
|
|
478
|
+
// Client becomes ready - retry should work
|
|
479
|
+
wrapper.testClient.state = 'READY';
|
|
480
|
+
wrapper.testSendRelayCommand({ to: 'ReceiverAgent', body: 'Hello' });
|
|
481
|
+
expect(wrapper.testClient.sentMessages).toHaveLength(1);
|
|
482
|
+
expect(wrapper.testSentMessageHashes.size).toBe(1);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('uses sendChannelMessage for channel targets (starting with #)', () => {
|
|
486
|
+
wrapper.testSendRelayCommand({
|
|
487
|
+
to: '#general',
|
|
488
|
+
body: 'Hello channel!',
|
|
489
|
+
thread: 'thread-1',
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Should use channel message, not regular message
|
|
493
|
+
expect(wrapper.testClient.sentChannelMessages).toHaveLength(1);
|
|
494
|
+
expect(wrapper.testClient.sentMessages).toHaveLength(0);
|
|
495
|
+
expect(wrapper.testClient.sentChannelMessages[0].channel).toBe('#general');
|
|
496
|
+
expect(wrapper.testClient.sentChannelMessages[0].body).toBe('Hello channel!');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('sends to different channels correctly', () => {
|
|
500
|
+
wrapper.testSendRelayCommand({ to: '#general', body: 'Hello general!' });
|
|
501
|
+
wrapper.testSendRelayCommand({ to: '#team', body: 'Hello team!' });
|
|
502
|
+
|
|
503
|
+
expect(wrapper.testClient.sentChannelMessages).toHaveLength(2);
|
|
504
|
+
expect(wrapper.testClient.sentMessages).toHaveLength(0);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('uses sendMessage for non-channel targets', () => {
|
|
508
|
+
wrapper.testSendRelayCommand({ to: 'Bob', body: 'Direct message' });
|
|
509
|
+
wrapper.testSendRelayCommand({ to: '*', body: 'Broadcast message' });
|
|
510
|
+
|
|
511
|
+
expect(wrapper.testClient.sentMessages).toHaveLength(2);
|
|
512
|
+
expect(wrapper.testClient.sentChannelMessages).toHaveLength(0);
|
|
513
|
+
});
|
|
465
514
|
});
|
|
466
515
|
|
|
467
516
|
describe('joinContinuationLines', () => {
|
package/src/base-wrapper.ts
CHANGED
|
@@ -456,15 +456,8 @@ export abstract class BaseWrapper extends EventEmitter {
|
|
|
456
456
|
console.error(`[base-wrapper] Skipped duplicate message to ${cmd.to}`);
|
|
457
457
|
return;
|
|
458
458
|
}
|
|
459
|
-
this.sentMessageHashes.add(hash);
|
|
460
459
|
|
|
461
|
-
//
|
|
462
|
-
if (this.sentMessageHashes.size > 500) {
|
|
463
|
-
const oldest = this.sentMessageHashes.values().next().value;
|
|
464
|
-
if (oldest) this.sentMessageHashes.delete(oldest);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Only send if client ready
|
|
460
|
+
// Only send if client ready - check BEFORE adding hash to avoid blocking retries
|
|
468
461
|
if (this.client.state !== 'READY') {
|
|
469
462
|
console.error(`[base-wrapper] Client not ready (state=${this.client.state}), dropping message to ${cmd.to}`);
|
|
470
463
|
return;
|
|
@@ -481,14 +474,29 @@ export abstract class BaseWrapper extends EventEmitter {
|
|
|
481
474
|
};
|
|
482
475
|
}
|
|
483
476
|
|
|
477
|
+
// Helper to mark message as sent (only after successful transmission)
|
|
478
|
+
const markSent = () => {
|
|
479
|
+
this.sentMessageHashes.add(hash);
|
|
480
|
+
// Limit hash set size
|
|
481
|
+
if (this.sentMessageHashes.size > 500) {
|
|
482
|
+
const oldest = this.sentMessageHashes.values().next().value;
|
|
483
|
+
if (oldest) this.sentMessageHashes.delete(oldest);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
484
487
|
// Check if target is a channel (starts with #)
|
|
485
488
|
if (cmd.to.startsWith('#')) {
|
|
486
489
|
// Use CHANNEL_MESSAGE protocol for channel targets
|
|
487
490
|
console.error(`[base-wrapper] Sending CHANNEL_MESSAGE to ${cmd.to}`);
|
|
488
|
-
this.client.sendChannelMessage(cmd.to, cmd.body, {
|
|
491
|
+
const success = this.client.sendChannelMessage(cmd.to, cmd.body, {
|
|
489
492
|
thread: cmd.thread,
|
|
490
493
|
data: cmd.data,
|
|
491
494
|
});
|
|
495
|
+
if (success) {
|
|
496
|
+
markSent();
|
|
497
|
+
} else {
|
|
498
|
+
console.error(`[base-wrapper] sendChannelMessage failed for ${cmd.to}`);
|
|
499
|
+
}
|
|
492
500
|
} else {
|
|
493
501
|
// Use SEND protocol for direct messages and broadcasts
|
|
494
502
|
if (cmd.sync?.blocking) {
|
|
@@ -497,11 +505,18 @@ export abstract class BaseWrapper extends EventEmitter {
|
|
|
497
505
|
kind: cmd.kind,
|
|
498
506
|
data: cmd.data,
|
|
499
507
|
thread: cmd.thread,
|
|
508
|
+
}).then(() => {
|
|
509
|
+
markSent();
|
|
500
510
|
}).catch((err) => {
|
|
501
511
|
console.error(`[base-wrapper] sendAndWait failed for ${cmd.to}: ${err.message}`);
|
|
502
512
|
});
|
|
503
513
|
} else {
|
|
504
|
-
this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread, sendMeta);
|
|
514
|
+
const success = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread, sendMeta);
|
|
515
|
+
if (success) {
|
|
516
|
+
markSent();
|
|
517
|
+
} else {
|
|
518
|
+
console.error(`[base-wrapper] sendMessage failed for ${cmd.to}`);
|
|
519
|
+
}
|
|
505
520
|
}
|
|
506
521
|
}
|
|
507
522
|
}
|
|
@@ -386,5 +386,33 @@ describe('UniversalIdleDetector', () => {
|
|
|
386
386
|
const result = detector.checkIdle();
|
|
387
387
|
expect(result.inEditorMode).toBe(false);
|
|
388
388
|
});
|
|
389
|
+
|
|
390
|
+
it('returns false for Claude CLI status bar (not actual vim)', () => {
|
|
391
|
+
// Claude CLI shows "-- INSERT --" followed by permission indicators
|
|
392
|
+
// This is NOT vim editor mode - it's Claude's vim keybindings mode
|
|
393
|
+
detector.onOutput('> Try "how do I log an error?"\n-- INSERT -- ⏵⏵ bypass permissions on (shift+tab to cycle)');
|
|
394
|
+
expect(detector.isInEditorMode()).toBe(false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('returns false for Claude CLI NORMAL mode status bar', () => {
|
|
398
|
+
detector.onOutput('> Try "how do I log an error?"\n-- NORMAL -- ⏵⏵ bypass permissions on');
|
|
399
|
+
expect(detector.isInEditorMode()).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('returns false for Claude CLI with ▶ symbol', () => {
|
|
403
|
+
detector.onOutput('Some prompt\n-- INSERT -- ▶ strict mode');
|
|
404
|
+
expect(detector.isInEditorMode()).toBe(false);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('still detects actual vim INSERT mode at end of line', () => {
|
|
408
|
+
// Vim shows "-- INSERT --" alone at the end of the line
|
|
409
|
+
detector.onOutput('~\n~\n-- INSERT --\n');
|
|
410
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('still detects vim INSERT mode with trailing whitespace', () => {
|
|
414
|
+
detector.onOutput('~\n-- INSERT -- ');
|
|
415
|
+
expect(detector.isInEditorMode()).toBe(true);
|
|
416
|
+
});
|
|
389
417
|
});
|
|
390
418
|
});
|
package/src/idle-detector.ts
CHANGED
|
@@ -142,20 +142,34 @@ export class UniversalIdleDetector {
|
|
|
142
142
|
/**
|
|
143
143
|
* Check if the agent is in an editor mode (vim INSERT, REPLACE, etc.).
|
|
144
144
|
* When in editor mode, message injection should be delayed.
|
|
145
|
+
*
|
|
146
|
+
* Note: Claude CLI shows "-- INSERT --" in its status bar (for vim keybindings mode)
|
|
147
|
+
* but this is NOT the same as being in vim. We exclude Claude CLI's status bar
|
|
148
|
+
* by checking for the "⏵" symbol that follows its mode indicator.
|
|
145
149
|
*/
|
|
146
150
|
isInEditorMode(): boolean {
|
|
147
151
|
// Check the last portion of output for editor mode indicators
|
|
148
152
|
const lastOutput = this.outputBuffer.slice(-500);
|
|
149
153
|
|
|
150
|
-
//
|
|
154
|
+
// Claude CLI status bar pattern - this is NOT vim editor mode
|
|
155
|
+
// Example: "-- INSERT -- ⏵⏵ bypass permissions on (shift+tab to cycle)"
|
|
156
|
+
// Also match: "-- NORMAL --", "-- VISUAL --" followed by Claude's UI elements
|
|
157
|
+
const claudeCliStatusBar = /-- (?:INSERT|NORMAL|VISUAL|REPLACE) --\s*[⏵⏴►▶]/;
|
|
158
|
+
if (claudeCliStatusBar.test(lastOutput)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Vim/Neovim mode indicators (standalone, not part of Claude CLI status)
|
|
163
|
+
// These patterns require the mode indicator to be at the end of a line
|
|
164
|
+
// or followed only by whitespace, which matches vim's actual display
|
|
151
165
|
const editorModePatterns = [
|
|
152
|
-
/-- INSERT
|
|
153
|
-
/-- REPLACE
|
|
154
|
-
/-- VISUAL
|
|
155
|
-
/-- VISUAL LINE
|
|
156
|
-
/-- VISUAL BLOCK
|
|
157
|
-
/-- SELECT
|
|
158
|
-
/-- TERMINAL
|
|
166
|
+
/-- INSERT --\s*$/m,
|
|
167
|
+
/-- REPLACE --\s*$/m,
|
|
168
|
+
/-- VISUAL --\s*$/m,
|
|
169
|
+
/-- VISUAL LINE --\s*$/m,
|
|
170
|
+
/-- VISUAL BLOCK --\s*$/m,
|
|
171
|
+
/-- SELECT --\s*$/m,
|
|
172
|
+
/-- TERMINAL --\s*$/m,
|
|
159
173
|
// Emacs indicators
|
|
160
174
|
/\*\*\* Emacs/,
|
|
161
175
|
/M-x/,
|
|
@@ -135,6 +135,10 @@ describe('RelayPtyOrchestrator', () => {
|
|
|
135
135
|
// Mock waitUntilCliReady to resolve immediately in tests
|
|
136
136
|
// This avoids waiting for CLI readiness checks which require complex simulation
|
|
137
137
|
vi.spyOn(RelayPtyOrchestrator.prototype, 'waitUntilCliReady').mockResolvedValue(true);
|
|
138
|
+
|
|
139
|
+
// Mock isProcessAlive to return true - the real implementation uses process.kill(pid, 0)
|
|
140
|
+
// which fails for mock PIDs that don't correspond to real processes
|
|
141
|
+
vi.spyOn(RelayPtyOrchestrator.prototype as any, 'isProcessAlive').mockReturnValue(true);
|
|
138
142
|
});
|
|
139
143
|
|
|
140
144
|
afterEach(async () => {
|
|
@@ -251,6 +251,9 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
251
251
|
// Track if agent is being gracefully stopped (vs crashed)
|
|
252
252
|
private isGracefulStop = false;
|
|
253
253
|
|
|
254
|
+
// Track early process exit for better error messages
|
|
255
|
+
private earlyExitInfo?: { code: number | null; signal: NodeJS.Signals | null; stderr: string };
|
|
256
|
+
|
|
254
257
|
// Memory/CPU monitoring
|
|
255
258
|
private memoryMonitor: AgentMemoryMonitor;
|
|
256
259
|
private memoryAlertHandler: ((alert: MemoryAlert) => void) | null = null;
|
|
@@ -574,7 +577,15 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
574
577
|
|
|
575
578
|
this.log(` Using binary: ${binaryPath}`);
|
|
576
579
|
|
|
577
|
-
//
|
|
580
|
+
// Spawn relay-pty process FIRST (before connecting to daemon)
|
|
581
|
+
// This ensures the CLI is actually running before we register with the daemon
|
|
582
|
+
await this.spawnRelayPty(binaryPath);
|
|
583
|
+
|
|
584
|
+
// Wait for socket to become available and connect
|
|
585
|
+
await this.connectToSocket();
|
|
586
|
+
|
|
587
|
+
// Connect to relay daemon AFTER CLI is spawned
|
|
588
|
+
// This prevents the spawner from seeing us as "registered" before the CLI runs
|
|
578
589
|
try {
|
|
579
590
|
await this.client.connect();
|
|
580
591
|
this.log(` Relay daemon connected`);
|
|
@@ -582,12 +593,6 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
582
593
|
this.logError(` Relay connect failed: ${err.message}`);
|
|
583
594
|
}
|
|
584
595
|
|
|
585
|
-
// Spawn relay-pty process
|
|
586
|
-
await this.spawnRelayPty(binaryPath);
|
|
587
|
-
|
|
588
|
-
// Wait for socket to become available and connect
|
|
589
|
-
await this.connectToSocket();
|
|
590
|
-
|
|
591
596
|
this.running = true;
|
|
592
597
|
// DON'T set readyForMessages yet - wait for CLI to be ready first
|
|
593
598
|
// This prevents messages from being injected during CLI startup
|
|
@@ -754,6 +759,9 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
754
759
|
|
|
755
760
|
this.log(` Spawning: ${binaryPath} ${args.join(' ')}`);
|
|
756
761
|
|
|
762
|
+
// Reset early exit info from any previous spawn attempt
|
|
763
|
+
this.earlyExitInfo = undefined;
|
|
764
|
+
|
|
757
765
|
// For interactive mode, let Rust directly inherit stdin/stdout from the terminal
|
|
758
766
|
// This is more robust than manual forwarding through pipes
|
|
759
767
|
// We still pipe stderr to capture JSON parsed commands
|
|
@@ -783,10 +791,15 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
783
791
|
});
|
|
784
792
|
}
|
|
785
793
|
|
|
794
|
+
// Capture stderr for early exit diagnosis
|
|
795
|
+
let stderrBuffer = '';
|
|
796
|
+
|
|
786
797
|
// Handle stderr (relay-pty logs and JSON output) - always needed
|
|
798
|
+
// Also captures to buffer for error diagnostics if process dies early
|
|
787
799
|
if (proc.stderr) {
|
|
788
800
|
proc.stderr.on('data', (data: Buffer) => {
|
|
789
801
|
const text = data.toString();
|
|
802
|
+
stderrBuffer += text;
|
|
790
803
|
this.handleStderr(text);
|
|
791
804
|
});
|
|
792
805
|
}
|
|
@@ -795,6 +808,12 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
795
808
|
proc.on('exit', (code, signal) => {
|
|
796
809
|
const exitCode = code ?? (signal === 'SIGKILL' ? 137 : 1);
|
|
797
810
|
this.log(` Process exited: code=${exitCode} signal=${signal}`);
|
|
811
|
+
|
|
812
|
+
// Capture early exit info for better error messages if socket not yet connected
|
|
813
|
+
if (!this.socketConnected) {
|
|
814
|
+
this.earlyExitInfo = { code, signal, stderr: stderrBuffer };
|
|
815
|
+
}
|
|
816
|
+
|
|
798
817
|
this.running = false;
|
|
799
818
|
|
|
800
819
|
// Get crash context before unregistering from memory monitor
|
|
@@ -1213,6 +1232,22 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1213
1232
|
// Socket communication
|
|
1214
1233
|
// =========================================================================
|
|
1215
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Check if the relay-pty process is still alive
|
|
1237
|
+
*/
|
|
1238
|
+
private isProcessAlive(): boolean {
|
|
1239
|
+
if (!this.relayPtyProcess || this.relayPtyProcess.exitCode !== null) {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
// Signal 0 checks if process exists without killing it
|
|
1244
|
+
process.kill(this.relayPtyProcess.pid!, 0);
|
|
1245
|
+
return true;
|
|
1246
|
+
} catch {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1216
1251
|
/**
|
|
1217
1252
|
* Connect to the relay-pty socket
|
|
1218
1253
|
*/
|
|
@@ -1221,6 +1256,21 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1221
1256
|
const maxAttempts = this.config.socketReconnectAttempts ?? 3;
|
|
1222
1257
|
|
|
1223
1258
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1259
|
+
// Check if relay-pty process died before attempting connection
|
|
1260
|
+
if (!this.isProcessAlive()) {
|
|
1261
|
+
const exitInfo = this.earlyExitInfo;
|
|
1262
|
+
if (exitInfo) {
|
|
1263
|
+
const exitReason = exitInfo.signal
|
|
1264
|
+
? `signal ${exitInfo.signal}`
|
|
1265
|
+
: `code ${exitInfo.code ?? 'unknown'}`;
|
|
1266
|
+
const stderrHint = exitInfo.stderr
|
|
1267
|
+
? `\n stderr: ${exitInfo.stderr.trim().slice(0, 500)}`
|
|
1268
|
+
: '';
|
|
1269
|
+
throw new Error(`relay-pty process died early (${exitReason}).${stderrHint}`);
|
|
1270
|
+
}
|
|
1271
|
+
throw new Error('relay-pty process died before socket could be created');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1224
1274
|
try {
|
|
1225
1275
|
await this.attemptSocketConnection(timeout);
|
|
1226
1276
|
this.log(` Socket connected`);
|
|
@@ -1233,6 +1283,18 @@ export class RelayPtyOrchestrator extends BaseWrapper {
|
|
|
1233
1283
|
}
|
|
1234
1284
|
}
|
|
1235
1285
|
|
|
1286
|
+
// Final check for process death after all attempts
|
|
1287
|
+
if (!this.isProcessAlive() && this.earlyExitInfo) {
|
|
1288
|
+
const exitInfo = this.earlyExitInfo;
|
|
1289
|
+
const exitReason = exitInfo.signal
|
|
1290
|
+
? `signal ${exitInfo.signal}`
|
|
1291
|
+
: `code ${exitInfo.code ?? 'unknown'}`;
|
|
1292
|
+
const stderrHint = exitInfo.stderr
|
|
1293
|
+
? `\n stderr: ${exitInfo.stderr.trim().slice(0, 500)}`
|
|
1294
|
+
: '';
|
|
1295
|
+
throw new Error(`relay-pty process died during socket connection (${exitReason}).${stderrHint}`);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1236
1298
|
throw new Error(`Failed to connect to socket after ${maxAttempts} attempts`);
|
|
1237
1299
|
}
|
|
1238
1300
|
|
package/src/tmux-wrapper.ts
CHANGED
|
@@ -969,6 +969,23 @@ export class TmuxWrapper extends BaseWrapper {
|
|
|
969
969
|
};
|
|
970
970
|
}
|
|
971
971
|
|
|
972
|
+
// Check if target is a channel (starts with #)
|
|
973
|
+
if (cmd.to.startsWith('#')) {
|
|
974
|
+
// Use CHANNEL_MESSAGE protocol for channel targets
|
|
975
|
+
this.logStderr(`→ [channel] ${cmd.to}: ${cmd.body.substring(0, Math.min(RELAY_LOG_TRUNCATE_LENGTH, cmd.body.length))}...`);
|
|
976
|
+
const success = this.client.sendChannelMessage(cmd.to, cmd.body, {
|
|
977
|
+
thread: cmd.thread,
|
|
978
|
+
data: cmd.data,
|
|
979
|
+
});
|
|
980
|
+
if (success) {
|
|
981
|
+
this.sentMessageHashes.add(msgHash);
|
|
982
|
+
this.queuedMessageHashes.delete(msgHash);
|
|
983
|
+
this.trajectory?.message('sent', this.config.name, cmd.to, cmd.body);
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Use SEND protocol for direct messages and broadcasts
|
|
972
989
|
if (cmd.sync?.blocking) {
|
|
973
990
|
this.client.sendAndWait(cmd.to, cmd.body, {
|
|
974
991
|
timeoutMs: cmd.sync.timeoutMs,
|