connectonion 0.0.18 → 0.0.21

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.
@@ -1,109 +1,129 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.RemoteAgent = void 0;
37
- const address = __importStar(require("../address"));
38
4
  const endpoint_1 = require("./endpoint");
39
- /**
40
- * Proxy to a remote agent with streaming support.
41
- *
42
- * @example
43
- * ```typescript
44
- * const agent = connect("0x123abc");
45
- *
46
- * // Simple usage
47
- * const response = await agent.input("Search for Python docs");
48
- * console.log(response.text); // Agent's response
49
- * console.log(response.done); // true if complete
50
- *
51
- * // Access UI events for rendering
52
- * console.log(agent.ui); // Array of UI events
53
- * console.log(agent.status); // 'idle' | 'working' | 'waiting'
54
- * ```
55
- */
5
+ const auth_1 = require("./auth");
6
+ const chat_item_mapper_1 = require("./chat-item-mapper");
56
7
  class RemoteAgent {
57
- /** Alias for address (backwards compatibility) */
58
- get agentAddress() {
59
- return this.address;
60
- }
8
+ set onMessage(fn) { this._onMessage = fn; }
61
9
  constructor(agentAddress, options = {}) {
62
10
  this._endpointResolutionAttempted = false;
11
+ // Public reactive state
63
12
  this._status = 'idle';
64
13
  this._connectionState = 'disconnected';
65
14
  this._currentSession = null;
66
15
  this._chatItems = [];
67
- this._activeWs = null;
68
- this._pendingPrompt = null;
69
- this._pendingInputId = null;
70
- this._pendingSessionId = null;
16
+ this._error = null;
17
+ // Persistent WebSocket
18
+ this._ws = null;
19
+ this._authenticated = false;
20
+ // Promise resolution for current input() call
21
+ this._inputResolve = null;
22
+ this._inputReject = null;
23
+ this._inputTimer = null;
24
+ // Pending retry after onboard
25
+ this._pendingRetry = null;
26
+ // PING/PONG health check
71
27
  this._lastPingTime = 0;
72
- this._healthCheckInterval = null;
73
- // Reconnection state
74
- this._reconnectAttempts = 0;
75
- this._maxReconnectAttempts = 3;
76
- this._reconnectBaseDelay = 1000;
77
- this._shouldReconnect = false;
28
+ this._pingTimer = null;
29
+ // Callback + promise for ensureConnected
30
+ this._connectResolve = null;
31
+ this._connectReject = null;
32
+ this._onMessage = null;
78
33
  this.address = agentAddress;
79
- this._relayUrl = (0, endpoint_1.normalizeRelayBase)(options.relayUrl || 'wss://oo.openonion.ai');
34
+ this._relayUrl = (0, endpoint_1.normalizeRelayUrl)(options.relayUrl || 'wss://oo.openonion.ai');
80
35
  this._directUrl = options.directUrl?.replace(/\/$/, '');
81
- this._WS = options.wsCtor || (0, endpoint_1.defaultWebSocketCtor)();
82
- if (options.keys) {
36
+ this._WS = options.wsCtor || (0, endpoint_1.getWebSocketCtor)();
37
+ if (options.keys)
83
38
  this._keys = options.keys;
84
- }
85
- }
86
- // ==========================================================================
87
- // Public Properties
88
- // ==========================================================================
89
- get status() {
90
- return this._status;
91
39
  }
92
- get connectionState() {
93
- return this._connectionState;
94
- }
95
- get currentSession() {
96
- return this._currentSession;
40
+ // --- Public getters ---
41
+ get agentAddress() { return this.address; }
42
+ get status() { return this._status; }
43
+ get connectionState() { return this._connectionState; }
44
+ get currentSession() { return this._currentSession; }
45
+ get ui() { return this._chatItems; }
46
+ get mode() { return this._currentSession?.mode || 'safe'; }
47
+ get error() { return this._error || null; }
48
+ // --- Public API ---
49
+ async input(prompt, options) {
50
+ const timeoutMs = options?.timeoutMs ?? 600000;
51
+ this._addChatItem({ type: 'user', content: prompt, images: options?.images });
52
+ this._addChatItem({ type: 'thinking', id: '__optimistic__', status: 'running' });
53
+ this._status = 'working';
54
+ this._onMessage?.();
55
+ await this._ensureConnected();
56
+ const inputId = (0, endpoint_1.generateUUID)();
57
+ const isDirect = this._isDirect();
58
+ const msg = { type: 'INPUT', input_id: inputId, prompt };
59
+ if (options?.images?.length)
60
+ msg.images = options.images;
61
+ if (!isDirect)
62
+ msg.to = this.address;
63
+ this._ws.send(JSON.stringify(msg));
64
+ return new Promise((resolve, reject) => {
65
+ this._inputResolve = resolve;
66
+ this._inputReject = reject;
67
+ this._inputTimer = setTimeout(() => {
68
+ this._settleInput();
69
+ this._status = 'idle';
70
+ this._onMessage?.();
71
+ reject(new Error('Request timed out'));
72
+ }, timeoutMs);
73
+ });
97
74
  }
98
- get ui() {
99
- return this._chatItems;
75
+ async reconnect(sessionId) {
76
+ const sid = sessionId || this._currentSession?.session_id;
77
+ if (!sid)
78
+ throw new Error('No session to reconnect');
79
+ if (!this._currentSession)
80
+ this._currentSession = { session_id: sid };
81
+ this._status = 'working';
82
+ this._onMessage?.();
83
+ // Force new connection for reconnect
84
+ this._closeWs();
85
+ this._keys = (0, auth_1.ensureKeys)(this._keys);
86
+ await this._resolveEndpointOnce();
87
+ const { wsUrl, isDirect } = this._resolveWsUrl();
88
+ const ws = new this._WS(wsUrl);
89
+ this._ws = ws;
90
+ this._connectionState = 'reconnecting';
91
+ this._onMessage?.();
92
+ return new Promise((resolve, reject) => {
93
+ this._inputResolve = resolve;
94
+ this._inputReject = reject;
95
+ this._inputTimer = setTimeout(() => {
96
+ this._settleInput();
97
+ this._status = 'idle';
98
+ this._connectionState = 'disconnected';
99
+ this._onMessage?.();
100
+ reject(new Error('Reconnect timed out'));
101
+ }, 60000);
102
+ ws.onopen = () => {
103
+ this._connectionState = 'connected';
104
+ this._lastPingTime = Date.now();
105
+ this._startPingMonitor();
106
+ // Send CONNECT with session_id + session data
107
+ const payload = { timestamp: Math.floor(Date.now() / 1000) };
108
+ payload.to = this.address;
109
+ const signed = (0, auth_1.signPayload)(this._keys, payload);
110
+ const msg = { type: 'CONNECT', session_id: sid, ...signed };
111
+ if (!isDirect)
112
+ msg.to = this.address;
113
+ if (this._currentSession)
114
+ msg.session = { ...this._currentSession };
115
+ ws.send(JSON.stringify(msg));
116
+ };
117
+ ws.onmessage = (evt) => this._handleMessage(evt);
118
+ ws.onerror = () => this._handleConnectionLoss();
119
+ ws.onclose = () => this._handleConnectionLoss();
120
+ });
100
121
  }
101
- get mode() {
102
- return this._currentSession?.mode || 'safe';
122
+ send(message) {
123
+ if (!this._ws)
124
+ throw new Error('No active connection');
125
+ this._ws.send(JSON.stringify(message));
103
126
  }
104
- // ==========================================================================
105
- // Public Methods
106
- // ==========================================================================
107
127
  setMode(mode, options) {
108
128
  if (!this._currentSession) {
109
129
  this._currentSession = { mode };
@@ -115,708 +135,428 @@ class RemoteAgent {
115
135
  this._currentSession.ulw_turns = options?.turns || 100;
116
136
  this._currentSession.ulw_turns_used = 0;
117
137
  }
118
- if (this._activeWs) {
138
+ if (this._ws) {
119
139
  const msg = { type: 'mode_change', mode };
120
- if (mode === 'ulw' && options?.turns) {
140
+ if (mode === 'ulw' && options?.turns)
121
141
  msg.turns = options.turns;
122
- }
123
- this._activeWs.send(JSON.stringify(msg));
124
- }
125
- }
126
- setPrompt(prompt) {
127
- if (this._activeWs) {
128
- this._activeWs.send(JSON.stringify({ type: 'prompt_update', prompt }));
142
+ this._ws.send(JSON.stringify(msg));
129
143
  }
130
144
  }
131
145
  reset() {
132
- if (this._activeWs) {
133
- this._activeWs.close();
134
- this._activeWs = null;
135
- }
146
+ this._closeWs();
136
147
  this._currentSession = null;
137
148
  this._chatItems = [];
138
149
  this._status = 'idle';
139
150
  this._connectionState = 'disconnected';
140
- this._shouldReconnect = false;
141
- this._reconnectAttempts = 0;
142
- }
143
- resetConversation() {
144
- this.reset();
151
+ this._error = null;
152
+ this._settleInput();
153
+ this._pendingRetry = null;
145
154
  }
146
- async input(prompt, options) {
147
- const timeoutMs = options?.timeoutMs ?? 600000;
148
- const images = options?.images;
149
- return this._streamInput(prompt, timeoutMs, images);
150
- }
151
- async inputAsync(prompt, options) {
152
- return this.input(prompt, options);
153
- }
154
- respond(answer) {
155
- if (!this._activeWs) {
156
- throw new Error('No active connection to respond to');
157
- }
158
- const answerStr = Array.isArray(answer) ? answer.join(', ') : answer;
159
- this._activeWs.send(JSON.stringify({
160
- type: 'ASK_USER_RESPONSE',
161
- answer: answerStr,
162
- }));
163
- }
164
- respondToApproval(approved, scope = 'once', mode, feedback) {
165
- if (!this._activeWs) {
166
- throw new Error('No active connection to respond to');
167
- }
168
- this._activeWs.send(JSON.stringify({
169
- type: 'APPROVAL_RESPONSE',
170
- approved,
171
- scope,
172
- ...(mode && { mode }),
173
- ...(feedback && { feedback }),
174
- }));
175
- }
176
- submitOnboard(options) {
177
- if (!this._activeWs) {
178
- throw new Error('No active connection to submit onboard');
179
- }
180
- const payload = {
181
- timestamp: Math.floor(Date.now() / 1000),
182
- };
155
+ resetConversation() { this.reset(); }
156
+ signOnboard(options) {
157
+ const payload = { timestamp: Math.floor(Date.now() / 1000) };
183
158
  if (options.inviteCode)
184
159
  payload.invite_code = options.inviteCode;
185
160
  if (options.payment)
186
161
  payload.payment = options.payment;
187
- const signed = this._signPayload(payload);
188
- this._activeWs.send(JSON.stringify({
189
- type: 'ONBOARD_SUBMIT',
190
- ...signed,
191
- }));
192
- }
193
- respondToPlanReview(message) {
194
- if (!this._activeWs) {
195
- throw new Error('No active connection to respond to');
196
- }
197
- this._activeWs.send(JSON.stringify({
198
- type: 'PLAN_REVIEW_RESPONSE',
199
- message,
200
- }));
201
- }
202
- respondToUlwTurnsReached(action, options) {
203
- if (!this._activeWs) {
204
- throw new Error('No active connection to respond to ULW');
205
- }
206
- this._activeWs.send(JSON.stringify({
207
- type: 'ULW_RESPONSE',
208
- action,
209
- ...(action === 'continue' && options?.turns && { turns: options.turns }),
210
- ...(action === 'switch_mode' && options?.mode && { mode: options.mode }),
211
- }));
212
- }
213
- // ==========================================================================
214
- // Private Methods - Streaming
215
- // ==========================================================================
216
- _ensureKeys() {
217
- if (this._keys)
218
- return;
219
- const isBrowser = (0, endpoint_1.isBrowserEnv)();
220
- const existingKeys = isBrowser ? address.loadBrowser() : address.load();
221
- if (existingKeys) {
222
- this._keys = existingKeys;
223
- return;
224
- }
225
- if (isBrowser) {
226
- this._keys = address.generateBrowser();
227
- address.saveBrowser(this._keys);
228
- }
229
- else {
230
- this._keys = address.generate();
231
- }
232
- }
233
- _deriveWsUrl() {
234
- if (this._directUrl) {
235
- const base = this._directUrl.replace(/^https?:\/\//, '');
236
- const protocol = this._directUrl.startsWith('https') ? 'wss' : 'ws';
237
- return { wsUrl: `${protocol}://${base}/ws`, isDirect: true };
238
- }
239
- if (this._resolvedEndpoint) {
240
- return { wsUrl: this._resolvedEndpoint.wsUrl, isDirect: true };
162
+ return { type: 'ONBOARD_SUBMIT', ...(0, auth_1.signPayload)(this._keys, payload) };
163
+ }
164
+ async checkSessionStatus(sessionId) {
165
+ // If we have a live WS, send SESSION_STATUS over it (no new connection needed)
166
+ if (this._ws && this._authenticated) {
167
+ return new Promise((resolve) => {
168
+ const timeout = setTimeout(() => resolve('not_found'), 5000);
169
+ // Temporarily intercept the next SESSION_STATUS response
170
+ const origHandler = this._ws.onmessage;
171
+ this._ws.onmessage = (evt) => {
172
+ const raw = typeof evt.data === 'string' ? evt.data : String(evt.data);
173
+ const data = JSON.parse(raw);
174
+ if (data?.type === 'SESSION_STATUS') {
175
+ clearTimeout(timeout);
176
+ this._ws.onmessage = origHandler;
177
+ resolve(data.status || 'not_found');
178
+ }
179
+ else {
180
+ // Not our response — pass to normal handler
181
+ this._handleMessage(evt);
182
+ }
183
+ };
184
+ this._ws.send(JSON.stringify({
185
+ type: 'SESSION_STATUS',
186
+ session: { session_id: sessionId },
187
+ }));
188
+ });
241
189
  }
242
- return { wsUrl: `${this._relayUrl}/ws/input`, isDirect: false };
190
+ // No active connection open a short-lived WS just for the check
191
+ this._keys = (0, auth_1.ensureKeys)(this._keys);
192
+ await this._resolveEndpointOnce();
193
+ const { wsUrl, isDirect } = this._resolveWsUrl();
194
+ return new Promise((resolve) => {
195
+ const ws = new this._WS(wsUrl);
196
+ const timeout = setTimeout(() => { ws.close(); resolve('not_found'); }, 5000);
197
+ ws.onopen = () => {
198
+ ws.send(JSON.stringify({
199
+ type: 'SESSION_STATUS',
200
+ session: { session_id: sessionId },
201
+ ...(!isDirect && { to: this.address }),
202
+ }));
203
+ };
204
+ ws.onmessage = (evt) => {
205
+ const data = JSON.parse(typeof evt.data === 'string' ? evt.data : String(evt.data));
206
+ if (data?.type === 'SESSION_STATUS') {
207
+ clearTimeout(timeout);
208
+ ws.close();
209
+ resolve(data.status || 'not_found');
210
+ }
211
+ };
212
+ ws.onerror = () => { clearTimeout(timeout); ws.close(); resolve('not_found'); };
213
+ });
243
214
  }
244
- _startHealthCheck(ws, reject) {
245
- this._stopHealthCheck();
246
- this._healthCheckInterval = setInterval(() => {
247
- const timeSinceLastPing = Date.now() - this._lastPingTime;
248
- if (timeSinceLastPing > 60000) {
249
- this._stopHealthCheck();
250
- ws.close();
251
- reject(new Error('Connection health check failed: No PING received for 60 seconds'));
252
- }
253
- }, 10000);
215
+ async checkSession(sessionId) {
216
+ const sid = sessionId || this._currentSession?.session_id;
217
+ if (!sid)
218
+ return 'not_found';
219
+ await this._resolveEndpointOnce();
220
+ const httpUrl = this._directUrl || this._resolvedEndpoint?.httpUrl;
221
+ if (!httpUrl)
222
+ return 'not_found';
223
+ const res = await fetch(`${httpUrl}/sessions/${sid}`).catch(() => null);
224
+ if (!res || !res.ok)
225
+ return 'not_found';
226
+ const data = await res.json().catch(() => null);
227
+ return data?.status === 'running' ? 'running' : 'done';
254
228
  }
255
- _stopHealthCheck() {
256
- if (this._healthCheckInterval) {
257
- clearInterval(this._healthCheckInterval);
258
- this._healthCheckInterval = null;
259
- }
229
+ toString() {
230
+ const short = this.address.length > 12 ? this.address.slice(0, 12) + '...' : this.address;
231
+ return `RemoteAgent(${short})`;
260
232
  }
261
- _attemptReconnect(resolve, reject) {
262
- if (!this._currentSession?.session_id) {
263
- reject(new Error('No session to reconnect'));
264
- return;
265
- }
266
- if (this._reconnectAttempts >= this._maxReconnectAttempts) {
267
- this._reconnectAttempts = 0;
268
- this._shouldReconnect = false;
269
- this._connectionState = 'disconnected';
270
- reject(new Error('Max reconnection attempts reached'));
271
- return;
272
- }
273
- this._connectionState = 'reconnecting';
274
- this._reconnectAttempts++;
275
- const delay = Math.min(this._reconnectBaseDelay * Math.pow(2, this._reconnectAttempts - 1), 30000);
276
- console.log(`[ConnectOnion] Connection lost. Reconnecting in ${delay}ms (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
277
- setTimeout(() => {
278
- this._reconnect(resolve, reject);
279
- }, delay);
233
+ // --- Internal helpers (used by useAgentForHuman) ---
234
+ _addChatItem(event) {
235
+ const id = event.id || (0, endpoint_1.generateUUID)();
236
+ this._chatItems.push({ ...event, id });
280
237
  }
281
- _reconnect(resolve, reject) {
282
- const sessionId = this._currentSession?.session_id;
283
- if (!sessionId) {
284
- reject(new Error('No session to reconnect'));
285
- return;
286
- }
287
- const { wsUrl, isDirect } = this._deriveWsUrl();
288
- const ws = new this._WS(wsUrl);
289
- this._activeWs = ws;
290
- this._status = 'working';
291
- const state = {
292
- settled: false,
293
- timer: setTimeout(() => {
294
- if (!state.settled) {
295
- state.settled = true;
296
- this._status = 'idle';
297
- ws.close();
298
- this._attemptReconnect(resolve, reject);
299
- }
300
- }, 600000),
301
- };
302
- ws.onopen = () => {
303
- console.log('[ConnectOnion] Reconnected successfully');
304
- this._connectionState = 'connected';
305
- this._reconnectAttempts = 0;
306
- this._lastPingTime = Date.now();
307
- this._startHealthCheck(ws, reject);
308
- // Send full session for server-side merge (iteration count comparison)
309
- const payload = {
310
- prompt: '',
311
- timestamp: Math.floor(Date.now() / 1000),
312
- };
313
- if (!isDirect)
314
- payload.to = this.address;
315
- const signed = this._signPayload(payload);
316
- ws.send(JSON.stringify({
317
- type: 'INPUT',
318
- input_id: (0, endpoint_1.generateUUID)(),
319
- prompt: '',
320
- ...signed,
321
- ...(!isDirect && { to: this.address }),
322
- session: { ...this._currentSession, session_id: sessionId },
323
- }));
324
- };
325
- this._attachMessageHandlers(ws, (0, endpoint_1.generateUUID)(), isDirect, state, resolve, reject);
238
+ _clearPlaceholder() {
239
+ const idx = this._chatItems.findIndex(item => item.id === '__optimistic__');
240
+ if (idx !== -1)
241
+ this._chatItems.splice(idx, 1);
326
242
  }
327
- async _tryResolveEndpoint() {
328
- if (this._endpointResolutionAttempted || this._directUrl)
243
+ // --- Private: connection lifecycle ---
244
+ async _ensureConnected() {
245
+ if (this._ws && this._authenticated)
329
246
  return;
330
- this._endpointResolutionAttempted = true;
331
- if (!this.address.startsWith('0x') || this.address.length !== 66)
332
- return;
333
- const resolved = await (0, endpoint_1.resolveEndpoint)(this.address, this._relayUrl);
334
- if (resolved) {
335
- this._resolvedEndpoint = resolved;
336
- }
337
- }
338
- async _streamInput(prompt, timeoutMs, images) {
339
- this._ensureKeys();
340
- await this._tryResolveEndpoint();
341
- this._addChatItem({ type: 'user', content: prompt, images });
342
- this._addChatItem({ type: 'thinking', id: '__optimistic__', status: 'running' });
343
- this._status = 'working';
344
- const inputId = (0, endpoint_1.generateUUID)();
345
- const sessionId = this._currentSession?.session_id || (0, endpoint_1.generateUUID)();
346
- const { wsUrl, isDirect } = this._deriveWsUrl();
247
+ this._keys = (0, auth_1.ensureKeys)(this._keys);
248
+ await this._resolveEndpointOnce();
249
+ const { wsUrl, isDirect } = this._resolveWsUrl();
347
250
  const ws = new this._WS(wsUrl);
348
- this._activeWs = ws;
349
- this._shouldReconnect = true;
350
- return new Promise((resolve, reject) => {
351
- const state = {
352
- settled: false,
353
- timer: setTimeout(() => {
354
- if (!state.settled) {
355
- state.settled = true;
356
- this._status = 'idle';
357
- this._shouldReconnect = false;
358
- this._connectionState = 'disconnected';
359
- ws.close();
360
- reject(new Error('Connection timed out'));
361
- }
362
- }, timeoutMs),
363
- };
251
+ this._ws = ws;
252
+ // Wait for open
253
+ await new Promise((resolve, reject) => {
364
254
  ws.onopen = () => {
365
255
  this._connectionState = 'connected';
366
256
  this._lastPingTime = Date.now();
367
- this._startHealthCheck(ws, reject);
368
- const payload = {
369
- prompt,
370
- timestamp: Math.floor(Date.now() / 1000),
371
- };
372
- if (!isDirect) {
373
- payload.to = this.address;
374
- }
375
- const signed = this._signPayload(payload);
376
- const inputMessage = {
377
- type: 'INPUT',
378
- input_id: inputId,
379
- prompt,
380
- ...signed,
381
- };
382
- if (images && images.length > 0) {
383
- inputMessage.images = images;
384
- }
385
- if (!isDirect) {
386
- inputMessage.to = this.address;
387
- }
388
- if (this._currentSession) {
389
- inputMessage.session = { ...this._currentSession, session_id: sessionId };
390
- }
391
- else {
392
- inputMessage.session = { session_id: sessionId };
393
- }
394
- ws.send(JSON.stringify(inputMessage));
257
+ this._startPingMonitor();
258
+ resolve();
395
259
  };
396
- this._attachMessageHandlers(ws, inputId, isDirect, state, resolve, reject);
260
+ ws.onerror = (err) => reject(new Error(`WebSocket connection failed: ${String(err)}`));
397
261
  });
398
- }
399
- /**
400
- * Attach onmessage/onerror/onclose to a WebSocket.
401
- * Shared by _streamInput (initial connection) and _reconnect.
402
- */
403
- _attachMessageHandlers(ws, inputId, isDirect, state, resolve, reject) {
404
- ws.onmessage = (evt) => {
405
- if (state.settled)
406
- return;
407
- const raw = typeof evt.data === 'string' ? evt.data : String(evt.data);
408
- const data = JSON.parse(raw);
409
- if (data?.type === 'PING') {
410
- this._lastPingTime = Date.now();
411
- ws.send(JSON.stringify({ type: 'PONG' }));
412
- return;
413
- }
414
- if (data?.type === 'session_sync' && data.session) {
415
- this._currentSession = data.session;
416
- }
417
- if (data?.type === 'RECONNECTED') {
418
- console.log('[RemoteAgent] Reconnected to session:', data.session_id);
419
- }
420
- if (data?.type === 'SESSION_MERGED' && data.server_newer) {
421
- console.log('[RemoteAgent] Server had newer session, merged');
422
- }
423
- if (data?.type === 'mode_changed' && data.mode) {
424
- if (!this._currentSession) {
425
- this._currentSession = { mode: data.mode };
426
- }
427
- else {
428
- this._currentSession.mode = data.mode;
429
- }
430
- }
431
- if (data?.type === 'ulw_turns_reached') {
432
- this._status = 'waiting';
433
- if (this._currentSession) {
434
- this._currentSession.ulw_turns_used = data.turns_used;
435
- }
436
- this._addChatItem({
437
- type: 'ulw_turns_reached',
438
- turns_used: data.turns_used,
439
- max_turns: data.max_turns,
440
- });
441
- }
442
- if (data?.type === 'llm_call' || data?.type === 'llm_result' ||
443
- data?.type === 'tool_call' || data?.type === 'tool_result' ||
444
- data?.type === 'thinking' || data?.type === 'assistant' ||
445
- data?.type === 'intent' || data?.type === 'eval' || data?.type === 'compact' ||
446
- data?.type === 'tool_blocked') {
447
- this._handleStreamEvent(data);
448
- if (data.session) {
449
- this._currentSession = data.session;
262
+ // Wire up persistent message handler
263
+ ws.onmessage = (evt) => this._handleMessage(evt);
264
+ ws.onerror = () => this._handleConnectionLoss();
265
+ ws.onclose = () => this._handleConnectionLoss();
266
+ // Send CONNECT with session (conversation history)
267
+ const payload = { timestamp: Math.floor(Date.now() / 1000) };
268
+ payload.to = this.address;
269
+ const signed = (0, auth_1.signPayload)(this._keys, payload);
270
+ const connectMsg = { type: 'CONNECT', ...signed };
271
+ if (!isDirect)
272
+ connectMsg.to = this.address;
273
+ if (this._currentSession?.session_id)
274
+ connectMsg.session_id = this._currentSession.session_id;
275
+ if (this._currentSession)
276
+ connectMsg.session = { ...this._currentSession };
277
+ ws.send(JSON.stringify(connectMsg));
278
+ // Wait for CONNECTED response
279
+ const connected = await new Promise((resolve, reject) => {
280
+ this._connectResolve = resolve;
281
+ this._connectReject = reject;
282
+ setTimeout(() => {
283
+ if (this._connectResolve) {
284
+ this._connectResolve = null;
285
+ this._connectReject = null;
286
+ reject(new Error('Authentication timed out'));
450
287
  }
288
+ }, 30000);
289
+ });
290
+ this._authenticated = true;
291
+ // Update session from server (may include merged data)
292
+ const sid = connected.session_id;
293
+ if (sid) {
294
+ if (!this._currentSession) {
295
+ this._currentSession = { session_id: sid };
451
296
  }
452
- if (data?.type === 'ask_user') {
453
- this._status = 'waiting';
454
- this._addChatItem({ type: 'ask_user', text: data.text || '', options: data.options || [], multi_select: data.multi_select || false });
297
+ else {
298
+ this._currentSession.session_id = sid;
455
299
  }
456
- if (data?.type === 'approval_needed') {
457
- this._status = 'waiting';
458
- this._addChatItem({
459
- type: 'approval_needed',
460
- tool: data.tool,
461
- arguments: data.arguments,
462
- ...(data.description && { description: data.description }),
463
- ...(data.batch_remaining && { batch_remaining: data.batch_remaining }),
464
- });
300
+ }
301
+ if (connected.server_newer && connected.session) {
302
+ this._currentSession = connected.session;
303
+ }
304
+ if (connected.server_newer && connected.chat_items && Array.isArray(connected.chat_items)) {
305
+ const userItems = this._chatItems.filter(item => item.type === 'user');
306
+ const serverNonUserItems = connected.chat_items.filter(item => item.type !== 'user');
307
+ this._chatItems = [...userItems, ...serverNonUserItems];
308
+ this._onMessage?.();
309
+ }
310
+ }
311
+ _handleMessage(evt) {
312
+ const raw = typeof evt.data === 'string' ? evt.data : String(evt.data);
313
+ const data = JSON.parse(raw);
314
+ // PING/PONG keepalive
315
+ if (data?.type === 'PING') {
316
+ this._lastPingTime = Date.now();
317
+ this._ws?.send(JSON.stringify({ type: 'PONG' }));
318
+ return;
319
+ }
320
+ // CONNECTED — resolve ensureConnected() promise
321
+ if (data?.type === 'CONNECTED') {
322
+ if (this._connectResolve) {
323
+ const resolve = this._connectResolve;
324
+ this._connectResolve = null;
325
+ this._connectReject = null;
326
+ resolve(data);
327
+ this._onMessage?.();
328
+ return;
465
329
  }
466
- if (data?.type === 'plan_review') {
467
- this._status = 'waiting';
468
- this._addChatItem({
469
- type: 'plan_review',
470
- plan_content: data.plan_content,
471
- });
330
+ // CONNECTED during reconnect — update session and UI if server has newer data
331
+ if (data.server_newer && data.session) {
332
+ this._currentSession = data.session;
472
333
  }
473
- if (data?.type === 'ONBOARD_REQUIRED') {
474
- this._status = 'waiting';
475
- this._pendingPrompt = data.prompt || '';
476
- this._pendingInputId = inputId;
477
- this._pendingSessionId = this._currentSession?.session_id || null;
478
- this._addChatItem({
479
- type: 'onboard_required',
480
- methods: (data.methods || []),
481
- paymentAmount: data.payment_amount,
482
- });
334
+ if (data.server_newer && data.chat_items && Array.isArray(data.chat_items)) {
335
+ // Server has newer chat items (e.g., agent finished while client was away)
336
+ // Keep user items from client, take everything else from server
337
+ const userItems = this._chatItems.filter(item => item.type === 'user');
338
+ const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
339
+ this._chatItems = [...userItems, ...serverNonUserItems];
483
340
  }
484
- if (data?.type === 'ONBOARD_SUCCESS') {
485
- this._addChatItem({
486
- type: 'onboard_success',
487
- level: data.level,
488
- message: data.message,
489
- });
490
- if (this._pendingPrompt && this._activeWs) {
491
- this._status = 'working';
492
- const retryPrompt = this._pendingPrompt;
493
- const retryInputId = this._pendingInputId || (0, endpoint_1.generateUUID)();
494
- this._pendingPrompt = null;
495
- this._pendingInputId = null;
496
- const retryPayload = {
497
- prompt: retryPrompt,
498
- timestamp: Math.floor(Date.now() / 1000),
499
- };
500
- if (!isDirect)
501
- retryPayload.to = this.address;
502
- const retrySigned = this._signPayload(retryPayload);
503
- const retryMessage = {
504
- type: 'INPUT',
505
- input_id: retryInputId,
506
- prompt: retryPrompt,
507
- ...retrySigned,
508
- };
509
- if (!isDirect)
510
- retryMessage.to = this.address;
511
- if (this._currentSession) {
512
- retryMessage.session = { ...this._currentSession, session_id: this._pendingSessionId };
513
- }
514
- else {
515
- retryMessage.session = { session_id: this._pendingSessionId };
516
- }
517
- this._activeWs.send(JSON.stringify(retryMessage));
518
- this._pendingSessionId = null;
519
- }
341
+ const reconnectSid = data.session_id;
342
+ if (reconnectSid && this._currentSession) {
343
+ this._currentSession.session_id = reconnectSid;
520
344
  }
521
- const isOutputForUs = isDirect
522
- ? data?.type === 'OUTPUT'
523
- : data?.type === 'OUTPUT' && data?.input_id === inputId;
524
- if (isOutputForUs) {
525
- state.settled = true;
526
- clearTimeout(state.timer);
527
- this._stopHealthCheck();
528
- this._removeOptimisticThinking();
345
+ this._authenticated = true;
346
+ // If status is "connected" (idle), resolve immediately — session is alive, no events to wait for
347
+ if (data.status === 'connected' || data.status === 'new') {
529
348
  this._status = 'idle';
530
- this._shouldReconnect = false;
531
- this._connectionState = 'disconnected';
532
- this._reconnectAttempts = 0;
533
- if (data.session) {
534
- this._currentSession = data.session;
535
- }
536
- if (data.chat_items && Array.isArray(data.chat_items)) {
537
- const userItems = this._chatItems.filter(item => item.type === 'user');
538
- const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
539
- this._chatItems = [...userItems, ...serverNonUserItems];
540
- }
541
- if (data.server_newer) {
542
- console.log('[RemoteAgent] Session was merged with newer server state');
543
- }
544
- const result = data.result || '';
545
- if (result) {
546
- const lastAgent = this._chatItems.filter((e) => e.type === 'agent').pop();
547
- if (!lastAgent || lastAgent.content !== result) {
548
- this._addChatItem({ type: 'agent', content: result });
549
- }
550
- }
551
- this._activeWs = null;
552
- ws.close();
553
- resolve({ text: result, done: true });
554
- }
555
- if (data?.type === 'ERROR') {
556
- state.settled = true;
557
- clearTimeout(state.timer);
558
- this._stopHealthCheck();
559
- this._status = 'idle';
560
- this._shouldReconnect = false;
561
- this._connectionState = 'disconnected';
562
- this._activeWs = null;
563
- ws.close();
564
- reject(new Error(`Agent error: ${String(data.message || data.error || 'Unknown error')}`));
565
- }
566
- };
567
- ws.onerror = async (err) => {
568
- if (state.settled)
569
- return;
570
- this._stopHealthCheck();
571
- clearTimeout(state.timer);
572
- ws.close();
573
- if (isDirect && !this._directUrl) {
574
- this._resolvedEndpoint = undefined;
575
- this._endpointResolutionAttempted = false;
349
+ const resolve = this._inputResolve;
350
+ this._settleInput();
351
+ resolve?.({ text: '', done: true });
576
352
  }
577
- if (this._shouldReconnect && this._reconnectAttempts < this._maxReconnectAttempts) {
578
- this._attemptReconnect(resolve, reject);
353
+ // If status is "executing", events will stream in via _handleMessage — don't resolve yet
354
+ this._onMessage?.();
355
+ return;
356
+ }
357
+ // Session sync
358
+ if (data?.type === 'session_sync' && data.session) {
359
+ this._currentSession = data.session;
360
+ }
361
+ if (data?.type === 'RECONNECTED') {
362
+ // Server confirmed reconnect — events will follow
363
+ }
364
+ if (data?.type === 'SESSION_MERGED' && data.server_newer) {
365
+ // Server had newer session
366
+ }
367
+ if (data?.type === 'mode_changed' && data.mode) {
368
+ if (!this._currentSession) {
369
+ this._currentSession = { mode: data.mode };
579
370
  }
580
371
  else {
581
- state.settled = true;
582
- this._status = 'idle';
583
- this._shouldReconnect = false;
584
- this._connectionState = 'disconnected';
585
- reject(new Error(`WebSocket error: ${String(err)}`));
372
+ this._currentSession.mode = data.mode;
586
373
  }
587
- };
588
- ws.onclose = async () => {
589
- this._activeWs = null;
590
- this._stopHealthCheck();
591
- if (!state.settled) {
592
- clearTimeout(state.timer);
593
- if (isDirect && !this._directUrl) {
594
- this._resolvedEndpoint = undefined;
595
- this._endpointResolutionAttempted = false;
596
- }
597
- if (this._shouldReconnect && this._reconnectAttempts < this._maxReconnectAttempts) {
598
- this._attemptReconnect(resolve, reject);
599
- }
600
- else {
601
- state.settled = true;
602
- this._status = 'idle';
603
- this._shouldReconnect = false;
604
- this._connectionState = 'disconnected';
605
- reject(new Error('Connection closed before response'));
606
- }
607
- }
608
- };
609
- }
610
- // ==========================================================================
611
- // Private Methods - UI Helpers
612
- // ==========================================================================
613
- _removeOptimisticThinking() {
614
- const idx = this._chatItems.findIndex(item => item.id === '__optimistic__');
615
- if (idx !== -1) {
616
- this._chatItems.splice(idx, 1);
617
374
  }
618
- }
619
- _handleStreamEvent(event) {
620
- const eventType = event.type;
621
- this._removeOptimisticThinking();
622
- switch (eventType) {
623
- case 'tool_call': {
624
- const toolId = (event.tool_id || event.id);
625
- this._addChatItem({
626
- type: 'tool_call',
627
- id: toolId,
628
- name: event.name,
629
- args: event.args,
630
- status: 'running',
631
- });
632
- break;
633
- }
634
- case 'tool_result': {
635
- const toolId = (event.tool_id || event.id);
636
- const existing = this._chatItems.find((e) => e.type === 'tool_call' && e.id === toolId);
637
- if (existing) {
638
- existing.status = event.status === 'error' ? 'error' : 'done';
639
- existing.result = event.result;
640
- if (typeof event.timing_ms === 'number') {
641
- existing.timing_ms = event.timing_ms;
642
- }
643
- }
644
- break;
645
- }
646
- case 'llm_call': {
647
- const llmId = event.id;
648
- this._addChatItem({
649
- type: 'thinking',
650
- id: llmId,
651
- status: 'running',
652
- model: event.model,
653
- });
654
- break;
655
- }
656
- case 'llm_result': {
657
- const llmId = event.id;
658
- const existingThinking = this._chatItems.find((e) => e.type === 'thinking' && e.id === llmId);
659
- if (existingThinking) {
660
- existingThinking.status = event.status === 'error' ? 'error' : 'done';
661
- if (typeof event.duration_ms === 'number') {
662
- existingThinking.duration_ms = event.duration_ms;
663
- }
664
- if (event.model) {
665
- existingThinking.model = event.model;
666
- }
667
- if (event.usage) {
668
- existingThinking.usage = event.usage;
669
- }
670
- if (typeof event.context_percent === 'number') {
671
- existingThinking.context_percent = event.context_percent;
672
- }
673
- }
674
- break;
675
- }
676
- case 'thinking': {
677
- this._addChatItem({
678
- type: 'thinking',
679
- id: event.id != null ? String(event.id) : undefined,
680
- status: 'done',
681
- content: event.content,
682
- kind: event.kind,
683
- });
684
- break;
375
+ // ULW turns reached
376
+ if (data?.type === 'ulw_turns_reached') {
377
+ this._status = 'waiting';
378
+ if (this._currentSession) {
379
+ this._currentSession.ulw_turns_used = data.turns_used;
685
380
  }
686
- case 'assistant': {
687
- if (event.content) {
688
- this._addChatItem({
689
- type: 'agent',
690
- id: event.id != null ? String(event.id) : undefined,
691
- content: event.content,
692
- });
693
- }
694
- break;
381
+ this._addChatItem({
382
+ type: 'ulw_turns_reached',
383
+ turns_used: data.turns_used,
384
+ max_turns: data.max_turns,
385
+ });
386
+ }
387
+ // Stream events → ChatItem mapping
388
+ if (data?.type === 'llm_call' || data?.type === 'llm_result' ||
389
+ data?.type === 'tool_call' || data?.type === 'tool_result' ||
390
+ data?.type === 'thinking' || data?.type === 'assistant' ||
391
+ data?.type === 'intent' || data?.type === 'eval' || data?.type === 'compact' ||
392
+ data?.type === 'tool_blocked') {
393
+ this._clearPlaceholder();
394
+ (0, chat_item_mapper_1.mapEventToChatItem)(this._chatItems, data, (item) => this._addChatItem(item));
395
+ if (data.session) {
396
+ this._currentSession = data.session;
695
397
  }
696
- case 'agent_image': {
697
- const imageData = event.image;
698
- if (imageData) {
699
- const lastAgent = this._chatItems.filter((e) => e.type === 'agent').pop();
700
- if (lastAgent) {
701
- if (!lastAgent.images) {
702
- lastAgent.images = [];
703
- }
704
- lastAgent.images.push(imageData);
705
- }
706
- else {
707
- this._addChatItem({
708
- type: 'agent',
709
- id: event.id != null ? String(event.id) : undefined,
710
- content: '',
711
- images: [imageData],
712
- });
713
- }
714
- }
715
- break;
398
+ }
399
+ // Interactive events
400
+ if (data?.type === 'ask_user') {
401
+ this._status = 'waiting';
402
+ this._addChatItem({ type: 'ask_user', text: data.text || '', options: data.options || [], multi_select: data.multi_select || false });
403
+ }
404
+ if (data?.type === 'approval_needed') {
405
+ this._status = 'waiting';
406
+ this._addChatItem({
407
+ type: 'approval_needed',
408
+ tool: data.tool,
409
+ arguments: data.arguments,
410
+ ...(data.description && { description: data.description }),
411
+ ...(data.batch_remaining && { batch_remaining: data.batch_remaining }),
412
+ });
413
+ }
414
+ if (data?.type === 'plan_review') {
415
+ this._status = 'waiting';
416
+ this._addChatItem({ type: 'plan_review', plan_content: data.plan_content });
417
+ }
418
+ // Onboard flow
419
+ if (data?.type === 'ONBOARD_REQUIRED') {
420
+ this._status = 'waiting';
421
+ this._pendingRetry = { prompt: data.prompt || '', inputId: (0, endpoint_1.generateUUID)() };
422
+ this._addChatItem({
423
+ type: 'onboard_required',
424
+ methods: (data.methods || []),
425
+ paymentAmount: data.payment_amount,
426
+ });
427
+ }
428
+ if (data?.type === 'ONBOARD_SUCCESS') {
429
+ this._addChatItem({
430
+ type: 'onboard_success',
431
+ level: data.level,
432
+ message: data.message,
433
+ });
434
+ if (this._pendingRetry && this._ws) {
435
+ this._status = 'working';
436
+ const retry = this._pendingRetry;
437
+ this._pendingRetry = null;
438
+ const isDirect = this._isDirect();
439
+ const msg = { type: 'INPUT', input_id: retry.inputId, prompt: retry.prompt };
440
+ if (!isDirect)
441
+ msg.to = this.address;
442
+ this._ws.send(JSON.stringify(msg));
716
443
  }
717
- case 'intent': {
718
- const intentId = event.id;
719
- const status = event.status;
720
- if (status === 'analyzing') {
721
- this._addChatItem({
722
- type: 'intent',
723
- id: intentId,
724
- status: 'analyzing',
725
- });
726
- }
727
- else if (status === 'understood') {
728
- const existing = this._chatItems.find((e) => e.type === 'intent' && e.id === intentId);
729
- if (existing) {
730
- existing.status = 'understood';
731
- existing.ack = event.ack;
732
- existing.is_build = event.is_build;
733
- }
734
- }
735
- break;
444
+ }
445
+ // OUTPUT resolve input() promise
446
+ if (data?.type === 'OUTPUT') {
447
+ this._clearPlaceholder();
448
+ this._status = 'idle';
449
+ if (data.session) {
450
+ this._currentSession = data.session;
736
451
  }
737
- case 'eval': {
738
- const evalId = event.id;
739
- const status = event.status;
740
- if (status === 'evaluating') {
741
- this._addChatItem({
742
- type: 'eval',
743
- id: evalId,
744
- status: 'evaluating',
745
- expected: event.expected,
746
- eval_path: event.eval_path,
747
- });
748
- }
749
- else if (status === 'done') {
750
- const existing = this._chatItems.find((e) => e.type === 'eval' && e.id === evalId);
751
- if (existing) {
752
- existing.status = 'done';
753
- existing.passed = event.passed;
754
- existing.summary = event.summary;
755
- existing.expected = event.expected;
756
- existing.eval_path = event.eval_path;
757
- }
758
- }
759
- break;
452
+ if (data.server_newer && data.chat_items && Array.isArray(data.chat_items)) {
453
+ const userItems = this._chatItems.filter(item => item.type === 'user');
454
+ const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
455
+ this._chatItems = [...userItems, ...serverNonUserItems];
760
456
  }
761
- case 'compact': {
762
- const compactId = event.id;
763
- const status = event.status;
764
- if (status === 'compacting') {
765
- this._addChatItem({
766
- type: 'compact',
767
- id: compactId,
768
- status: 'compacting',
769
- context_percent: event.context_percent,
770
- });
771
- }
772
- else {
773
- const existing = this._chatItems.find((e) => e.type === 'compact' && e.id === compactId);
774
- if (existing) {
775
- existing.status = status;
776
- existing.context_before = event.context_before;
777
- existing.context_after = event.context_after;
778
- existing.message = event.message;
779
- existing.error = event.error;
780
- }
457
+ const result = data.result || '';
458
+ if (result) {
459
+ const lastAgent = this._chatItems.filter((e) => e.type === 'agent').pop();
460
+ if (!lastAgent || lastAgent.content !== result) {
461
+ this._addChatItem({ type: 'agent', content: result });
781
462
  }
782
- break;
783
463
  }
784
- case 'tool_blocked': {
785
- this._addChatItem({
786
- type: 'tool_blocked',
787
- tool: event.tool,
788
- reason: event.reason,
789
- message: event.message,
790
- command: event.command,
791
- });
792
- break;
464
+ // Don't close WS — keep it for next input()
465
+ const resolve = this._inputResolve;
466
+ this._settleInput();
467
+ resolve?.({ text: result, done: true });
468
+ }
469
+ // ERROR — reject input() promise
470
+ if (data?.type === 'ERROR') {
471
+ this._status = 'idle';
472
+ this._connectionState = 'disconnected';
473
+ this._closeWs();
474
+ const reject = this._inputReject;
475
+ this._settleInput();
476
+ reject?.(new Error(`Agent error: ${String(data.message || data.error || 'Unknown error')}`));
477
+ }
478
+ this._onMessage?.();
479
+ }
480
+ _handleConnectionLoss() {
481
+ this._ws = null;
482
+ this._authenticated = false;
483
+ this._stopPingMonitor();
484
+ // Reject pending connect
485
+ if (this._connectReject) {
486
+ const reject = this._connectReject;
487
+ this._connectResolve = null;
488
+ this._connectReject = null;
489
+ reject(new Error('Connection lost during authentication'));
490
+ return;
491
+ }
492
+ // Reject pending input only if there is one
493
+ if (this._inputReject) {
494
+ this._status = 'idle';
495
+ this._connectionState = 'disconnected';
496
+ const reject = this._inputReject;
497
+ this._settleInput();
498
+ reject(new Error('Connection closed before response'));
499
+ this._onMessage?.();
500
+ }
501
+ }
502
+ _settleInput() {
503
+ if (this._inputTimer) {
504
+ clearTimeout(this._inputTimer);
505
+ this._inputTimer = null;
506
+ }
507
+ this._inputResolve = null;
508
+ this._inputReject = null;
509
+ }
510
+ _closeWs() {
511
+ this._stopPingMonitor();
512
+ if (this._ws) {
513
+ // Prevent close handler from firing during intentional close
514
+ this._ws.onerror = null;
515
+ this._ws.onclose = null;
516
+ this._ws.onmessage = null;
517
+ this._ws.close();
518
+ this._ws = null;
519
+ }
520
+ this._authenticated = false;
521
+ this._connectionState = 'disconnected';
522
+ }
523
+ _startPingMonitor() {
524
+ this._stopPingMonitor();
525
+ this._pingTimer = setInterval(() => {
526
+ if (Date.now() - this._lastPingTime > 60000) {
527
+ this._stopPingMonitor();
528
+ this._ws?.close();
793
529
  }
530
+ }, 10000);
531
+ }
532
+ _stopPingMonitor() {
533
+ if (this._pingTimer) {
534
+ clearInterval(this._pingTimer);
535
+ this._pingTimer = null;
794
536
  }
795
537
  }
796
- _addChatItem(event) {
797
- const id = event.id || (0, endpoint_1.generateUUID)();
798
- this._chatItems.push({ ...event, id });
538
+ _isDirect() {
539
+ return !!this._directUrl || !!this._resolvedEndpoint;
799
540
  }
800
- // ==========================================================================
801
- // Private Methods - Signing
802
- // ==========================================================================
803
- _signPayload(payload) {
804
- if (!this._keys) {
805
- return { prompt: payload.prompt };
541
+ _resolveWsUrl() {
542
+ if (this._directUrl) {
543
+ const base = this._directUrl.replace(/^https?:\/\//, '');
544
+ const protocol = this._directUrl.startsWith('https') ? 'wss' : 'ws';
545
+ return { wsUrl: `${protocol}://${base}/ws`, isDirect: true };
806
546
  }
807
- const canonicalMessage = (0, endpoint_1.canonicalJSON)(payload);
808
- const signer = (0, endpoint_1.isBrowserEnv)() ? address.signBrowser : address.sign;
809
- const signature = signer(this._keys, canonicalMessage);
810
- return {
811
- payload,
812
- from: this._keys.address,
813
- signature,
814
- timestamp: payload.timestamp,
815
- };
547
+ if (this._resolvedEndpoint)
548
+ return { wsUrl: this._resolvedEndpoint.wsUrl, isDirect: true };
549
+ return { wsUrl: `${this._relayUrl}/ws/input`, isDirect: false };
816
550
  }
817
- toString() {
818
- const short = this.address.length > 12 ? this.address.slice(0, 12) + '...' : this.address;
819
- return `RemoteAgent(${short})`;
551
+ async _resolveEndpointOnce() {
552
+ if (this._endpointResolutionAttempted || this._directUrl)
553
+ return;
554
+ this._endpointResolutionAttempted = true;
555
+ if (!this.address.startsWith('0x') || this.address.length !== 66)
556
+ return;
557
+ const resolved = await (0, endpoint_1.resolveEndpoint)(this.address, this._relayUrl);
558
+ if (resolved)
559
+ this._resolvedEndpoint = resolved;
820
560
  }
821
561
  }
822
562
  exports.RemoteAgent = RemoteAgent;