@astroanywhere/agent 0.1.0
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/LICENSE +76 -0
- package/README.md +178 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +401 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +9 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +16 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +19 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/setup.d.ts +20 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +585 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/start.d.ts +16 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +638 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +63 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +5 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +85 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/execution/direct-strategy.d.ts +18 -0
- package/dist/execution/direct-strategy.d.ts.map +1 -0
- package/dist/execution/direct-strategy.js +156 -0
- package/dist/execution/direct-strategy.js.map +1 -0
- package/dist/execution/docker-strategy.d.ts +26 -0
- package/dist/execution/docker-strategy.d.ts.map +1 -0
- package/dist/execution/docker-strategy.js +222 -0
- package/dist/execution/docker-strategy.js.map +1 -0
- package/dist/execution/index.d.ts +14 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +13 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/kubernetes-exec-strategy.d.ts +23 -0
- package/dist/execution/kubernetes-exec-strategy.d.ts.map +1 -0
- package/dist/execution/kubernetes-exec-strategy.js +232 -0
- package/dist/execution/kubernetes-exec-strategy.js.map +1 -0
- package/dist/execution/registry.d.ts +41 -0
- package/dist/execution/registry.d.ts.map +1 -0
- package/dist/execution/registry.js +84 -0
- package/dist/execution/registry.js.map +1 -0
- package/dist/execution/slurm-strategy.d.ts +22 -0
- package/dist/execution/slurm-strategy.d.ts.map +1 -0
- package/dist/execution/slurm-strategy.js +219 -0
- package/dist/execution/slurm-strategy.js.map +1 -0
- package/dist/execution/types.d.ts +72 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/execution/types.js +10 -0
- package/dist/execution/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +35 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +126 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config.d.ts +174 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +399 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/copy-worktree.d.ts +73 -0
- package/dist/lib/copy-worktree.d.ts.map +1 -0
- package/dist/lib/copy-worktree.js +374 -0
- package/dist/lib/copy-worktree.js.map +1 -0
- package/dist/lib/git-pr.d.ts +63 -0
- package/dist/lib/git-pr.d.ts.map +1 -0
- package/dist/lib/git-pr.js +224 -0
- package/dist/lib/git-pr.js.map +1 -0
- package/dist/lib/hardware-id.d.ts +25 -0
- package/dist/lib/hardware-id.d.ts.map +1 -0
- package/dist/lib/hardware-id.js +186 -0
- package/dist/lib/hardware-id.js.map +1 -0
- package/dist/lib/hpc-context.d.ts +35 -0
- package/dist/lib/hpc-context.d.ts.map +1 -0
- package/dist/lib/hpc-context.js +167 -0
- package/dist/lib/hpc-context.js.map +1 -0
- package/dist/lib/prompt-templates.d.ts +195 -0
- package/dist/lib/prompt-templates.d.ts.map +1 -0
- package/dist/lib/prompt-templates.js +353 -0
- package/dist/lib/prompt-templates.js.map +1 -0
- package/dist/lib/providers.d.ts +27 -0
- package/dist/lib/providers.d.ts.map +1 -0
- package/dist/lib/providers.js +372 -0
- package/dist/lib/providers.js.map +1 -0
- package/dist/lib/repo-context.d.ts +18 -0
- package/dist/lib/repo-context.d.ts.map +1 -0
- package/dist/lib/repo-context.js +61 -0
- package/dist/lib/repo-context.js.map +1 -0
- package/dist/lib/repo-utils.d.ts +35 -0
- package/dist/lib/repo-utils.d.ts.map +1 -0
- package/dist/lib/repo-utils.js +222 -0
- package/dist/lib/repo-utils.js.map +1 -0
- package/dist/lib/resources.d.ts +17 -0
- package/dist/lib/resources.d.ts.map +1 -0
- package/dist/lib/resources.js +227 -0
- package/dist/lib/resources.js.map +1 -0
- package/dist/lib/slurm-detect.d.ts +15 -0
- package/dist/lib/slurm-detect.d.ts.map +1 -0
- package/dist/lib/slurm-detect.js +148 -0
- package/dist/lib/slurm-detect.js.map +1 -0
- package/dist/lib/slurm-executor.d.ts +70 -0
- package/dist/lib/slurm-executor.d.ts.map +1 -0
- package/dist/lib/slurm-executor.js +402 -0
- package/dist/lib/slurm-executor.js.map +1 -0
- package/dist/lib/slurm-job-monitor.d.ts +52 -0
- package/dist/lib/slurm-job-monitor.d.ts.map +1 -0
- package/dist/lib/slurm-job-monitor.js +212 -0
- package/dist/lib/slurm-job-monitor.js.map +1 -0
- package/dist/lib/ssh-discovery.d.ts +17 -0
- package/dist/lib/ssh-discovery.d.ts.map +1 -0
- package/dist/lib/ssh-discovery.js +287 -0
- package/dist/lib/ssh-discovery.js.map +1 -0
- package/dist/lib/ssh-installer.d.ts +69 -0
- package/dist/lib/ssh-installer.d.ts.map +1 -0
- package/dist/lib/ssh-installer.js +230 -0
- package/dist/lib/ssh-installer.js.map +1 -0
- package/dist/lib/streaming-prompt.d.ts +48 -0
- package/dist/lib/streaming-prompt.d.ts.map +1 -0
- package/dist/lib/streaming-prompt.js +91 -0
- package/dist/lib/streaming-prompt.js.map +1 -0
- package/dist/lib/task-executor.d.ts +114 -0
- package/dist/lib/task-executor.d.ts.map +1 -0
- package/dist/lib/task-executor.js +753 -0
- package/dist/lib/task-executor.js.map +1 -0
- package/dist/lib/websocket-client.d.ts +200 -0
- package/dist/lib/websocket-client.d.ts.map +1 -0
- package/dist/lib/websocket-client.js +781 -0
- package/dist/lib/websocket-client.js.map +1 -0
- package/dist/lib/workdir-safety.d.ts +63 -0
- package/dist/lib/workdir-safety.d.ts.map +1 -0
- package/dist/lib/workdir-safety.js +247 -0
- package/dist/lib/workdir-safety.js.map +1 -0
- package/dist/lib/worktree-include.d.ts +14 -0
- package/dist/lib/worktree-include.d.ts.map +1 -0
- package/dist/lib/worktree-include.js +68 -0
- package/dist/lib/worktree-include.js.map +1 -0
- package/dist/lib/worktree-setup.d.ts +18 -0
- package/dist/lib/worktree-setup.d.ts.map +1 -0
- package/dist/lib/worktree-setup.js +60 -0
- package/dist/lib/worktree-setup.js.map +1 -0
- package/dist/lib/worktree.d.ts +37 -0
- package/dist/lib/worktree.d.ts.map +1 -0
- package/dist/lib/worktree.js +411 -0
- package/dist/lib/worktree.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +45 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +153 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session-bridge.d.ts +87 -0
- package/dist/mcp/session-bridge.d.ts.map +1 -0
- package/dist/mcp/session-bridge.js +317 -0
- package/dist/mcp/session-bridge.js.map +1 -0
- package/dist/mcp/tools.d.ts +70 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +234 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +197 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +16 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/base-adapter.d.ts +56 -0
- package/dist/providers/base-adapter.d.ts.map +1 -0
- package/dist/providers/base-adapter.js +5 -0
- package/dist/providers/base-adapter.js.map +1 -0
- package/dist/providers/claude-code-adapter.d.ts +27 -0
- package/dist/providers/claude-code-adapter.d.ts.map +1 -0
- package/dist/providers/claude-code-adapter.js +298 -0
- package/dist/providers/claude-code-adapter.js.map +1 -0
- package/dist/providers/claude-sdk-adapter.d.ts +60 -0
- package/dist/providers/claude-sdk-adapter.d.ts.map +1 -0
- package/dist/providers/claude-sdk-adapter.js +632 -0
- package/dist/providers/claude-sdk-adapter.js.map +1 -0
- package/dist/providers/codex-adapter.d.ts +21 -0
- package/dist/providers/codex-adapter.d.ts.map +1 -0
- package/dist/providers/codex-adapter.js +197 -0
- package/dist/providers/codex-adapter.js.map +1 -0
- package/dist/providers/index.d.ts +26 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +58 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/slurm-adapter.d.ts +26 -0
- package/dist/providers/slurm-adapter.d.ts.map +1 -0
- package/dist/providers/slurm-adapter.js +146 -0
- package/dist/providers/slurm-adapter.js.map +1 -0
- package/dist/types.d.ts +592 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client with automatic reconnection and heartbeat
|
|
3
|
+
*/
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import jwt from 'jsonwebtoken';
|
|
6
|
+
import { getMachineResources } from './resources.js';
|
|
7
|
+
import { config as configManager } from './config.js';
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
relayUrl: 'wss://relay.astro.dev',
|
|
10
|
+
maxConcurrentTasks: 4,
|
|
11
|
+
heartbeatInterval: 30000, // 30 seconds
|
|
12
|
+
reconnectMaxRetries: -1, // Infinite retries
|
|
13
|
+
reconnectBaseDelay: 1000, // 1 second
|
|
14
|
+
reconnectMaxDelay: 60000, // 1 minute
|
|
15
|
+
taskTimeout: 3600000, // 1 hour
|
|
16
|
+
logLevel: 'info',
|
|
17
|
+
};
|
|
18
|
+
export class WebSocketClient {
|
|
19
|
+
ws = null;
|
|
20
|
+
config;
|
|
21
|
+
runnerId;
|
|
22
|
+
machineId;
|
|
23
|
+
providers;
|
|
24
|
+
executionStrategies;
|
|
25
|
+
version;
|
|
26
|
+
wsToken;
|
|
27
|
+
reconnectAttempts = 0;
|
|
28
|
+
reconnectTimeout = null;
|
|
29
|
+
heartbeatInterval = null;
|
|
30
|
+
isConnecting = false;
|
|
31
|
+
shouldReconnect = true;
|
|
32
|
+
activeTasks = new Set();
|
|
33
|
+
pendingApprovals = new Map();
|
|
34
|
+
onEvent;
|
|
35
|
+
onTaskDispatch;
|
|
36
|
+
onTaskCancel;
|
|
37
|
+
onTaskSteer;
|
|
38
|
+
onTaskSafetyDecision;
|
|
39
|
+
onFileList;
|
|
40
|
+
onRepoSetup;
|
|
41
|
+
onSlashCommands;
|
|
42
|
+
onRepoDetect;
|
|
43
|
+
onGitInit;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.runnerId = options.runnerId;
|
|
46
|
+
this.machineId = options.machineId;
|
|
47
|
+
this.providers = options.providers;
|
|
48
|
+
this.executionStrategies = options.executionStrategies;
|
|
49
|
+
this.version = options.version ?? '0.1.0';
|
|
50
|
+
this.wsToken = options.wsToken;
|
|
51
|
+
this.config = { ...DEFAULT_CONFIG, ...options.config };
|
|
52
|
+
this.onEvent = options.onEvent;
|
|
53
|
+
this.onTaskDispatch = options.onTaskDispatch;
|
|
54
|
+
this.onTaskCancel = options.onTaskCancel;
|
|
55
|
+
this.onTaskSteer = options.onTaskSteer;
|
|
56
|
+
this.onTaskSafetyDecision = options.onTaskSafetyDecision;
|
|
57
|
+
this.onFileList = options.onFileList;
|
|
58
|
+
this.onRepoSetup = options.onRepoSetup;
|
|
59
|
+
this.onSlashCommands = options.onSlashCommands;
|
|
60
|
+
this.onRepoDetect = options.onRepoDetect;
|
|
61
|
+
this.onGitInit = options.onGitInit;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if access token is expired or expiring soon
|
|
65
|
+
*/
|
|
66
|
+
isTokenExpiring(token, bufferSeconds = 5 * 60) {
|
|
67
|
+
try {
|
|
68
|
+
const decoded = jwt.decode(token);
|
|
69
|
+
if (!decoded || !decoded.exp) {
|
|
70
|
+
return true; // Assume expired if we can't decode
|
|
71
|
+
}
|
|
72
|
+
const now = Math.floor(Date.now() / 1000);
|
|
73
|
+
return decoded.exp < now + bufferSeconds;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return true; // Assume expired on error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Refresh the access token and WebSocket token using the refresh token
|
|
81
|
+
*/
|
|
82
|
+
async refreshAccessToken() {
|
|
83
|
+
const refreshToken = configManager.getRefreshToken();
|
|
84
|
+
if (!refreshToken) {
|
|
85
|
+
throw new Error('No refresh token available. Please run setup again to re-authenticate.');
|
|
86
|
+
}
|
|
87
|
+
const apiUrl = configManager.getApiUrl();
|
|
88
|
+
const response = await fetch(`${apiUrl}/api/device/refresh`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
refreshToken,
|
|
93
|
+
grantType: 'refresh_token',
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
98
|
+
throw new Error(`Token refresh failed: ${error.error || response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
// Update stored tokens
|
|
102
|
+
configManager.setAccessToken(data.accessToken);
|
|
103
|
+
if (data.refreshToken) {
|
|
104
|
+
configManager.setRefreshToken(data.refreshToken);
|
|
105
|
+
}
|
|
106
|
+
if (data.wsToken) {
|
|
107
|
+
configManager.setWsToken(data.wsToken);
|
|
108
|
+
}
|
|
109
|
+
// Return wsToken for WebSocket connections (signed with RELAY_JWT_SECRET)
|
|
110
|
+
// Falls back to accessToken if server didn't return a wsToken
|
|
111
|
+
return data.wsToken || data.accessToken;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Ensure we have a valid WebSocket token before connecting.
|
|
115
|
+
* Returns null in dev mode when no token is configured (allows unauthenticated connections).
|
|
116
|
+
*/
|
|
117
|
+
async ensureValidToken() {
|
|
118
|
+
let token = this.wsToken || configManager.getWsToken();
|
|
119
|
+
// In dev mode (non-wss relay), skip auth entirely
|
|
120
|
+
if (this.config.relayUrl.startsWith('ws://')) {
|
|
121
|
+
if (!token) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Even with a stale token, dev mode doesn't need auth
|
|
125
|
+
if (this.isTokenExpiring(token)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!token) {
|
|
130
|
+
throw new Error('No access token configured. Please run setup to authenticate.');
|
|
131
|
+
}
|
|
132
|
+
// Check if token is expired or expiring soon
|
|
133
|
+
if (this.isTokenExpiring(token)) {
|
|
134
|
+
console.log('[ws-client] Access token expired or expiring soon, refreshing...');
|
|
135
|
+
try {
|
|
136
|
+
token = await this.refreshAccessToken();
|
|
137
|
+
this.wsToken = token; // Update instance token
|
|
138
|
+
console.log('[ws-client] ✅ Access token refreshed successfully');
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error('[ws-client] ❌ Failed to refresh token:', error instanceof Error ? error.message : String(error));
|
|
142
|
+
throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Token refresh failed'}. Please run setup again.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return token;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Connect to the relay server
|
|
149
|
+
*/
|
|
150
|
+
async connect() {
|
|
151
|
+
if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.isConnecting = true;
|
|
155
|
+
this.shouldReconnect = true;
|
|
156
|
+
// Ensure we have a valid token before connecting (null = dev mode, no auth)
|
|
157
|
+
let token;
|
|
158
|
+
try {
|
|
159
|
+
token = await this.ensureValidToken();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.isConnecting = false;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
try {
|
|
167
|
+
const headers = {
|
|
168
|
+
'X-Runner-Id': this.runnerId,
|
|
169
|
+
'X-Machine-Id': this.machineId,
|
|
170
|
+
};
|
|
171
|
+
if (token) {
|
|
172
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
173
|
+
}
|
|
174
|
+
this.ws = new WebSocket(this.config.relayUrl, { headers });
|
|
175
|
+
this.ws.on('open', async () => {
|
|
176
|
+
this.isConnecting = false;
|
|
177
|
+
this.reconnectAttempts = 0;
|
|
178
|
+
await this.handleOpen();
|
|
179
|
+
resolve();
|
|
180
|
+
});
|
|
181
|
+
this.ws.on('message', (data) => {
|
|
182
|
+
this.handleMessage(data);
|
|
183
|
+
});
|
|
184
|
+
const thisSocket = this.ws;
|
|
185
|
+
this.ws.on('close', (code, reason) => {
|
|
186
|
+
// Only handle close if this is still the active socket.
|
|
187
|
+
// When the server closes a stale connection after re-registration,
|
|
188
|
+
// the old socket's close handler fires — ignore it to prevent a reconnect loop.
|
|
189
|
+
if (this.ws !== thisSocket) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.handleClose(code, reason.toString());
|
|
193
|
+
});
|
|
194
|
+
this.ws.on('error', (error) => {
|
|
195
|
+
this.isConnecting = false;
|
|
196
|
+
if (this.reconnectAttempts === 0) {
|
|
197
|
+
reject(error);
|
|
198
|
+
}
|
|
199
|
+
this.emitEvent({ type: 'error', error: error });
|
|
200
|
+
});
|
|
201
|
+
this.ws.on('pong', () => {
|
|
202
|
+
// Server responded to ping, connection is alive
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
this.isConnecting = false;
|
|
207
|
+
reject(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Disconnect from the relay server
|
|
213
|
+
*/
|
|
214
|
+
disconnect() {
|
|
215
|
+
this.shouldReconnect = false;
|
|
216
|
+
this.cleanup();
|
|
217
|
+
if (this.ws) {
|
|
218
|
+
this.ws.close(1000, 'Client disconnect');
|
|
219
|
+
this.ws = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Send a task status update
|
|
224
|
+
*/
|
|
225
|
+
sendTaskStatus(taskId, status, progress, message) {
|
|
226
|
+
const msg = {
|
|
227
|
+
type: 'task_status',
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
payload: { taskId, status, progress, message },
|
|
230
|
+
};
|
|
231
|
+
this.send(msg);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Send task output (stdout/stderr)
|
|
235
|
+
*/
|
|
236
|
+
sendTaskOutput(taskId, stream, data, sequence) {
|
|
237
|
+
const msg = {
|
|
238
|
+
type: 'task_output',
|
|
239
|
+
timestamp: new Date().toISOString(),
|
|
240
|
+
payload: { taskId, stream, data, sequence },
|
|
241
|
+
};
|
|
242
|
+
this.send(msg);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Send task result
|
|
246
|
+
*/
|
|
247
|
+
sendTaskResult(result) {
|
|
248
|
+
const msg = {
|
|
249
|
+
type: 'task_result',
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
payload: result,
|
|
252
|
+
};
|
|
253
|
+
this.send(msg);
|
|
254
|
+
// Remove from active tasks
|
|
255
|
+
this.activeTasks.delete(result.taskId);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Send tool trace
|
|
259
|
+
*/
|
|
260
|
+
sendToolTrace(taskId, toolName, toolInput, toolResult, success) {
|
|
261
|
+
const msg = {
|
|
262
|
+
type: 'task_tool_trace',
|
|
263
|
+
timestamp: new Date().toISOString(),
|
|
264
|
+
payload: { taskId, toolName, toolInput, toolResult, success },
|
|
265
|
+
};
|
|
266
|
+
this.send(msg);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Send structured text (bypasses stdout throttle on relay)
|
|
270
|
+
*/
|
|
271
|
+
sendTaskText(taskId, text, sequence) {
|
|
272
|
+
const msg = {
|
|
273
|
+
type: 'task_text',
|
|
274
|
+
timestamp: new Date().toISOString(),
|
|
275
|
+
payload: { taskId, text, sequence },
|
|
276
|
+
};
|
|
277
|
+
this.send(msg);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Send structured tool use event
|
|
281
|
+
*/
|
|
282
|
+
sendTaskToolUse(taskId, toolName, toolInput) {
|
|
283
|
+
const msg = {
|
|
284
|
+
type: 'task_tool_use',
|
|
285
|
+
timestamp: new Date().toISOString(),
|
|
286
|
+
payload: { taskId, toolName, toolInput },
|
|
287
|
+
};
|
|
288
|
+
this.send(msg);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Send structured tool result event
|
|
292
|
+
*/
|
|
293
|
+
sendTaskToolResult(taskId, toolName, result, success) {
|
|
294
|
+
const msg = {
|
|
295
|
+
type: 'task_tool_result',
|
|
296
|
+
timestamp: new Date().toISOString(),
|
|
297
|
+
payload: { taskId, toolName, result, success },
|
|
298
|
+
};
|
|
299
|
+
this.send(msg);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Send structured file change event
|
|
303
|
+
*/
|
|
304
|
+
sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff) {
|
|
305
|
+
const msg = {
|
|
306
|
+
type: 'task_file_change',
|
|
307
|
+
timestamp: new Date().toISOString(),
|
|
308
|
+
payload: { taskId, path, action, linesAdded, linesRemoved, diff },
|
|
309
|
+
};
|
|
310
|
+
this.send(msg);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Send structured session init event
|
|
314
|
+
*/
|
|
315
|
+
sendTaskSessionInit(taskId, sessionId, model) {
|
|
316
|
+
const msg = {
|
|
317
|
+
type: 'task_session_init',
|
|
318
|
+
timestamp: new Date().toISOString(),
|
|
319
|
+
payload: { taskId, sessionId, model },
|
|
320
|
+
};
|
|
321
|
+
this.send(msg);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Send steer acknowledgment
|
|
325
|
+
*/
|
|
326
|
+
sendSteerAck(taskId, accepted, message, interrupted) {
|
|
327
|
+
const msg = {
|
|
328
|
+
type: 'task_steer_ack',
|
|
329
|
+
timestamp: new Date().toISOString(),
|
|
330
|
+
payload: { taskId, accepted, message },
|
|
331
|
+
};
|
|
332
|
+
if (interrupted) {
|
|
333
|
+
msg.payload.interrupted = true;
|
|
334
|
+
}
|
|
335
|
+
this.send(msg);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Send approval request and wait for response (following Cyrus pattern)
|
|
339
|
+
* Returns a promise that resolves when the user responds or rejects on timeout
|
|
340
|
+
*/
|
|
341
|
+
sendApprovalRequest(taskId, question, options) {
|
|
342
|
+
const requestId = `${taskId}-${Date.now()}`;
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
// Store the promise handlers
|
|
345
|
+
this.pendingApprovals.set(requestId, { resolve, reject });
|
|
346
|
+
// Send the approval request message
|
|
347
|
+
const msg = {
|
|
348
|
+
type: 'task_approval_request',
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
payload: { taskId, requestId, question, options },
|
|
351
|
+
};
|
|
352
|
+
this.send(msg);
|
|
353
|
+
console.log(`[ws-client] Sent approval request ${requestId} for task ${taskId}`);
|
|
354
|
+
// Set timeout (60 seconds for user to respond)
|
|
355
|
+
setTimeout(() => {
|
|
356
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
357
|
+
if (pending) {
|
|
358
|
+
this.pendingApprovals.delete(requestId);
|
|
359
|
+
pending.resolve({ answered: false, message: 'Approval request timed out (60s)' });
|
|
360
|
+
}
|
|
361
|
+
}, 60000);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Send safety prompt to user
|
|
366
|
+
*/
|
|
367
|
+
sendSafetyPrompt(taskId, safetyTier, warning, options) {
|
|
368
|
+
const msg = {
|
|
369
|
+
type: 'task_safety_prompt',
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
payload: {
|
|
372
|
+
taskId,
|
|
373
|
+
safetyTier,
|
|
374
|
+
warning,
|
|
375
|
+
blockReason: undefined,
|
|
376
|
+
options,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
this.send(msg);
|
|
380
|
+
console.log(`[ws-client] Sent safety prompt for task ${taskId} (tier: ${safetyTier})`);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Send resource update
|
|
384
|
+
*/
|
|
385
|
+
async sendResourceUpdate() {
|
|
386
|
+
const resources = await getMachineResources();
|
|
387
|
+
const msg = {
|
|
388
|
+
type: 'resource_update',
|
|
389
|
+
timestamp: new Date().toISOString(),
|
|
390
|
+
payload: { runnerId: this.runnerId, resources },
|
|
391
|
+
};
|
|
392
|
+
this.send(msg);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Check if connected
|
|
396
|
+
*/
|
|
397
|
+
isConnected() {
|
|
398
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get current configuration
|
|
402
|
+
*/
|
|
403
|
+
getConfig() {
|
|
404
|
+
return { ...this.config };
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get active task count
|
|
408
|
+
*/
|
|
409
|
+
getActiveTaskCount() {
|
|
410
|
+
return this.activeTasks.size;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Add task to active set
|
|
414
|
+
*/
|
|
415
|
+
addActiveTask(taskId) {
|
|
416
|
+
this.activeTasks.add(taskId);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Remove task from active set
|
|
420
|
+
*/
|
|
421
|
+
removeActiveTask(taskId) {
|
|
422
|
+
this.activeTasks.delete(taskId);
|
|
423
|
+
}
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// Private Methods
|
|
426
|
+
// ============================================================================
|
|
427
|
+
async handleOpen() {
|
|
428
|
+
// Send registration message
|
|
429
|
+
const resources = await getMachineResources();
|
|
430
|
+
// Allow config to override the display name (e.g., SSH alias like "nebius-2")
|
|
431
|
+
const machineName = configManager.getMachineName();
|
|
432
|
+
const registerMsg = {
|
|
433
|
+
type: 'register',
|
|
434
|
+
timestamp: new Date().toISOString(),
|
|
435
|
+
payload: {
|
|
436
|
+
runnerId: this.runnerId,
|
|
437
|
+
machineId: this.machineId,
|
|
438
|
+
...(machineName ? { name: machineName } : {}),
|
|
439
|
+
providers: this.providers,
|
|
440
|
+
executionStrategies: this.executionStrategies,
|
|
441
|
+
resources,
|
|
442
|
+
version: this.version,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
this.send(registerMsg);
|
|
446
|
+
// Start heartbeat
|
|
447
|
+
this.startHeartbeat();
|
|
448
|
+
this.emitEvent({ type: 'connected' });
|
|
449
|
+
}
|
|
450
|
+
handleMessage(data) {
|
|
451
|
+
try {
|
|
452
|
+
const raw = JSON.parse(data.toString());
|
|
453
|
+
// Handle task.steer (dot notation from relay) by normalizing to task_steer
|
|
454
|
+
if (raw.type === 'task.steer') {
|
|
455
|
+
// Relay sends: { type: 'task.steer', taskId, message, action, interrupt }
|
|
456
|
+
// Normalize to agent-runner format: { type: 'task_steer', payload: { taskId, message, action, interrupt } }
|
|
457
|
+
const steerMsg = {
|
|
458
|
+
type: 'task_steer',
|
|
459
|
+
timestamp: raw.timestamp ?? new Date().toISOString(),
|
|
460
|
+
payload: {
|
|
461
|
+
taskId: raw.taskId,
|
|
462
|
+
message: raw.message,
|
|
463
|
+
action: raw.action,
|
|
464
|
+
interrupt: raw.interrupt,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
this.handleTaskSteer(steerMsg);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// Handle file_list.request (dot notation from relay)
|
|
471
|
+
if (raw.type === 'file_list.request') {
|
|
472
|
+
const fileListMsg = {
|
|
473
|
+
type: 'file_list_request',
|
|
474
|
+
timestamp: raw.timestamp ?? new Date().toISOString(),
|
|
475
|
+
payload: {
|
|
476
|
+
path: raw.path,
|
|
477
|
+
correlationId: raw.correlationId,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
this.handleFileListRequest(fileListMsg);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Handle slash_commands.request (dot notation from relay)
|
|
484
|
+
if (raw.type === 'slash_commands.request') {
|
|
485
|
+
const slashMsg = {
|
|
486
|
+
type: 'slash_commands_request',
|
|
487
|
+
timestamp: raw.timestamp ?? new Date().toISOString(),
|
|
488
|
+
payload: {
|
|
489
|
+
correlationId: raw.correlationId,
|
|
490
|
+
workingDirectory: raw.workingDirectory,
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
this.handleSlashCommandsRequest(slashMsg);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Handle repo_setup.request (dot notation from relay)
|
|
497
|
+
if (raw.type === 'repo_setup.request') {
|
|
498
|
+
const repoMsg = {
|
|
499
|
+
type: 'repo_setup_request',
|
|
500
|
+
timestamp: raw.timestamp ?? new Date().toISOString(),
|
|
501
|
+
payload: raw.payload,
|
|
502
|
+
};
|
|
503
|
+
this.handleRepoSetupRequest(repoMsg);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// Handle repo_detect.request (dot notation from relay)
|
|
507
|
+
if (raw.type === 'repo_detect.request') {
|
|
508
|
+
const detectMsg = {
|
|
509
|
+
type: 'repo_detect_request',
|
|
510
|
+
timestamp: raw.timestamp ?? new Date().toISOString(),
|
|
511
|
+
payload: raw.payload,
|
|
512
|
+
};
|
|
513
|
+
this.handleRepoDetectRequest(detectMsg);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// Handle git_init.request (dot notation from relay)
|
|
517
|
+
if (raw.type === 'git_init.request') {
|
|
518
|
+
const gitInitMsg = {
|
|
519
|
+
type: 'git_init_request',
|
|
520
|
+
timestamp: raw.timestamp ?? new Date().toISOString(),
|
|
521
|
+
payload: raw.payload,
|
|
522
|
+
};
|
|
523
|
+
this.handleGitInitRequest(gitInitMsg);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const message = raw;
|
|
527
|
+
this.routeMessage(message);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
this.emitEvent({ type: 'error', error: error });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
routeMessage(message) {
|
|
534
|
+
switch (message.type) {
|
|
535
|
+
case 'registered':
|
|
536
|
+
this.handleRegistered(message);
|
|
537
|
+
break;
|
|
538
|
+
case 'heartbeat_ack':
|
|
539
|
+
// Heartbeat acknowledged, nothing to do
|
|
540
|
+
break;
|
|
541
|
+
case 'task_dispatch':
|
|
542
|
+
this.handleTaskDispatch(message);
|
|
543
|
+
break;
|
|
544
|
+
case 'task_cancel':
|
|
545
|
+
this.handleTaskCancel(message);
|
|
546
|
+
break;
|
|
547
|
+
case 'task_steer':
|
|
548
|
+
this.handleTaskSteer(message);
|
|
549
|
+
break;
|
|
550
|
+
case 'task_approval_response':
|
|
551
|
+
this.handleApprovalResponse(message);
|
|
552
|
+
break;
|
|
553
|
+
case 'task_safety_decision':
|
|
554
|
+
this.handleSafetyDecision(message);
|
|
555
|
+
break;
|
|
556
|
+
case 'config_update':
|
|
557
|
+
this.handleConfigUpdate(message);
|
|
558
|
+
break;
|
|
559
|
+
case 'file_list_request':
|
|
560
|
+
this.handleFileListRequest(message);
|
|
561
|
+
break;
|
|
562
|
+
case 'repo_setup_request':
|
|
563
|
+
this.handleRepoSetupRequest(message);
|
|
564
|
+
break;
|
|
565
|
+
case 'slash_commands_request':
|
|
566
|
+
this.handleSlashCommandsRequest(message);
|
|
567
|
+
break;
|
|
568
|
+
case 'repo_detect_request':
|
|
569
|
+
this.handleRepoDetectRequest(message);
|
|
570
|
+
break;
|
|
571
|
+
case 'git_init_request':
|
|
572
|
+
this.handleGitInitRequest(message);
|
|
573
|
+
break;
|
|
574
|
+
case 'error':
|
|
575
|
+
this.handleError(message);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
handleRegistered(message) {
|
|
580
|
+
// Apply server-provided configuration
|
|
581
|
+
if (message.payload.config) {
|
|
582
|
+
this.config = { ...this.config, ...message.payload.config };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
handleTaskDispatch(message) {
|
|
586
|
+
const task = message.payload;
|
|
587
|
+
// Check if we can accept more tasks
|
|
588
|
+
if (this.activeTasks.size >= this.config.maxConcurrentTasks) {
|
|
589
|
+
this.sendTaskStatus(task.id, 'queued', 0, 'Waiting for available slot');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.activeTasks.add(task.id);
|
|
593
|
+
this.emitEvent({ type: 'task_received', task });
|
|
594
|
+
this.onTaskDispatch?.(task);
|
|
595
|
+
}
|
|
596
|
+
handleTaskCancel(message) {
|
|
597
|
+
const { taskId } = message.payload;
|
|
598
|
+
this.activeTasks.delete(taskId);
|
|
599
|
+
this.emitEvent({ type: 'task_cancelled', taskId });
|
|
600
|
+
this.onTaskCancel?.(taskId);
|
|
601
|
+
}
|
|
602
|
+
handleTaskSteer(message) {
|
|
603
|
+
const { taskId, message: steerMessage, action, interrupt } = message.payload;
|
|
604
|
+
this.onTaskSteer?.(taskId, steerMessage, action, interrupt);
|
|
605
|
+
}
|
|
606
|
+
handleSafetyDecision(message) {
|
|
607
|
+
const { taskId, decision } = message.payload;
|
|
608
|
+
console.log(`[ws-client] Received safety decision for ${taskId}: ${decision}`);
|
|
609
|
+
this.onTaskSafetyDecision?.(taskId, decision);
|
|
610
|
+
}
|
|
611
|
+
handleApprovalResponse(message) {
|
|
612
|
+
const { requestId, answered, answer, message: responseMessage } = message.payload;
|
|
613
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
614
|
+
if (pending) {
|
|
615
|
+
console.log(`[ws-client] Received approval response for ${requestId}: answered=${answered}, answer=${answer}`);
|
|
616
|
+
this.pendingApprovals.delete(requestId);
|
|
617
|
+
pending.resolve({ answered, answer, message: responseMessage });
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
console.warn(`[ws-client] Received approval response for unknown request ${requestId}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
handleConfigUpdate(message) {
|
|
624
|
+
this.config = { ...this.config, ...message.payload };
|
|
625
|
+
// Restart heartbeat with new interval if changed
|
|
626
|
+
if (message.payload.heartbeatInterval) {
|
|
627
|
+
this.stopHeartbeat();
|
|
628
|
+
this.startHeartbeat();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
handleError(message) {
|
|
632
|
+
const code = message.payload?.code ?? 'UNKNOWN';
|
|
633
|
+
const msg = message.payload?.message ?? 'Unknown error';
|
|
634
|
+
const error = new Error(`${code}: ${msg}`);
|
|
635
|
+
this.emitEvent({ type: 'error', error });
|
|
636
|
+
}
|
|
637
|
+
handleFileListRequest(message) {
|
|
638
|
+
const { path, correlationId } = message.payload;
|
|
639
|
+
this.onFileList?.(path, correlationId);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Send file list response
|
|
643
|
+
*/
|
|
644
|
+
sendFileListResponse(correlationId, files) {
|
|
645
|
+
const msg = {
|
|
646
|
+
type: 'file_list_response',
|
|
647
|
+
timestamp: new Date().toISOString(),
|
|
648
|
+
payload: { correlationId, files },
|
|
649
|
+
};
|
|
650
|
+
this.send(msg);
|
|
651
|
+
}
|
|
652
|
+
handleRepoSetupRequest(message) {
|
|
653
|
+
this.onRepoSetup?.(message.payload);
|
|
654
|
+
}
|
|
655
|
+
handleSlashCommandsRequest(message) {
|
|
656
|
+
const { correlationId, workingDirectory } = message.payload;
|
|
657
|
+
this.onSlashCommands?.(correlationId, workingDirectory);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Send slash commands response
|
|
661
|
+
*/
|
|
662
|
+
sendSlashCommandsResponse(correlationId, commands) {
|
|
663
|
+
const msg = {
|
|
664
|
+
type: 'slash_commands_response',
|
|
665
|
+
timestamp: new Date().toISOString(),
|
|
666
|
+
payload: { correlationId, commands },
|
|
667
|
+
};
|
|
668
|
+
this.send(msg);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Send repo setup response
|
|
672
|
+
*/
|
|
673
|
+
sendRepoSetupResponse(correlationId, result) {
|
|
674
|
+
const msg = {
|
|
675
|
+
type: 'repo_setup_response',
|
|
676
|
+
timestamp: new Date().toISOString(),
|
|
677
|
+
payload: { correlationId, ...result },
|
|
678
|
+
};
|
|
679
|
+
this.send(msg);
|
|
680
|
+
}
|
|
681
|
+
handleRepoDetectRequest(message) {
|
|
682
|
+
this.onRepoDetect?.(message.payload);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Send repo detect response
|
|
686
|
+
*/
|
|
687
|
+
sendRepoDetectResponse(correlationId, result) {
|
|
688
|
+
const msg = {
|
|
689
|
+
type: 'repo_detect_response',
|
|
690
|
+
timestamp: new Date().toISOString(),
|
|
691
|
+
payload: { correlationId, ...result },
|
|
692
|
+
};
|
|
693
|
+
this.send(msg);
|
|
694
|
+
}
|
|
695
|
+
handleGitInitRequest(message) {
|
|
696
|
+
this.onGitInit?.(message.payload);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Send git init response
|
|
700
|
+
*/
|
|
701
|
+
sendGitInitResponse(correlationId, result) {
|
|
702
|
+
const msg = {
|
|
703
|
+
type: 'git_init_response',
|
|
704
|
+
timestamp: new Date().toISOString(),
|
|
705
|
+
payload: { correlationId, ...result },
|
|
706
|
+
};
|
|
707
|
+
this.send(msg);
|
|
708
|
+
}
|
|
709
|
+
handleClose(code, reason) {
|
|
710
|
+
this.cleanup();
|
|
711
|
+
this.emitEvent({ type: 'disconnected', reason: `${code}: ${reason}` });
|
|
712
|
+
// Custom close code 4001 means this connection was replaced by another
|
|
713
|
+
// process with the same machineId — do not reconnect.
|
|
714
|
+
if (code === 4001) {
|
|
715
|
+
console.error('[ws-client] Connection replaced by another process with the same machineId. Not reconnecting.');
|
|
716
|
+
this.shouldReconnect = false;
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (this.shouldReconnect) {
|
|
720
|
+
this.scheduleReconnect();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
send(message) {
|
|
724
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
725
|
+
this.ws.send(JSON.stringify(message));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
startHeartbeat() {
|
|
729
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
730
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
731
|
+
// Send application-level heartbeat
|
|
732
|
+
const resources = await getMachineResources();
|
|
733
|
+
const heartbeat = {
|
|
734
|
+
type: 'heartbeat',
|
|
735
|
+
timestamp: new Date().toISOString(),
|
|
736
|
+
payload: {
|
|
737
|
+
runnerId: this.runnerId,
|
|
738
|
+
activeTasks: Array.from(this.activeTasks),
|
|
739
|
+
resources,
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
this.send(heartbeat);
|
|
743
|
+
// Also send WebSocket ping for connection health
|
|
744
|
+
this.ws.ping();
|
|
745
|
+
}
|
|
746
|
+
}, this.config.heartbeatInterval);
|
|
747
|
+
}
|
|
748
|
+
stopHeartbeat() {
|
|
749
|
+
if (this.heartbeatInterval) {
|
|
750
|
+
clearInterval(this.heartbeatInterval);
|
|
751
|
+
this.heartbeatInterval = null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
scheduleReconnect() {
|
|
755
|
+
const maxRetries = this.config.reconnectMaxRetries;
|
|
756
|
+
if (maxRetries >= 0 && this.reconnectAttempts >= maxRetries) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
this.reconnectAttempts++;
|
|
760
|
+
// Exponential backoff with jitter
|
|
761
|
+
const delay = Math.min(this.config.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1) +
|
|
762
|
+
Math.random() * 1000, this.config.reconnectMaxDelay);
|
|
763
|
+
this.emitEvent({ type: 'reconnecting', attempt: this.reconnectAttempts });
|
|
764
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
765
|
+
this.connect().catch(() => {
|
|
766
|
+
// Error already handled in connect()
|
|
767
|
+
});
|
|
768
|
+
}, delay);
|
|
769
|
+
}
|
|
770
|
+
cleanup() {
|
|
771
|
+
this.stopHeartbeat();
|
|
772
|
+
if (this.reconnectTimeout) {
|
|
773
|
+
clearTimeout(this.reconnectTimeout);
|
|
774
|
+
this.reconnectTimeout = null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
emitEvent(event) {
|
|
778
|
+
this.onEvent?.(event);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
//# sourceMappingURL=websocket-client.js.map
|