@getpaseo/server 0.1.14 → 0.1.16
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/server/client/daemon-client.d.ts +41 -4
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +355 -84
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +10 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +261 -18
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts +5 -0
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +24 -0
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +11 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +15 -5
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +2 -0
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +2 -0
- package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js +2 -0
- package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts +7 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +1470 -237
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/codex-app-server-agent.js +19 -4
- package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +40 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js +1 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
- package/dist/server/server/client-message-id.d.ts +3 -0
- package/dist/server/server/client-message-id.d.ts.map +1 -0
- package/dist/server/server/client-message-id.js +12 -0
- package/dist/server/server/client-message-id.js.map +1 -0
- package/dist/server/server/persisted-config.d.ts +8 -8
- package/dist/server/server/persistence-hooks.js +1 -1
- package/dist/server/server/persistence-hooks.js.map +1 -1
- package/dist/server/server/relay-transport.d.ts.map +1 -1
- package/dist/server/server/relay-transport.js +27 -28
- package/dist/server/server/relay-transport.js.map +1 -1
- package/dist/server/server/session.d.ts +4 -2
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +122 -31
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/websocket-server.d.ts +8 -4
- package/dist/server/server/websocket-server.d.ts.map +1 -1
- package/dist/server/server/websocket-server.js +272 -75
- package/dist/server/server/websocket-server.js.map +1 -1
- package/dist/server/shared/daemon-endpoints.d.ts +9 -1
- package/dist/server/shared/daemon-endpoints.d.ts.map +1 -1
- package/dist/server/shared/daemon-endpoints.js +18 -3
- package/dist/server/shared/daemon-endpoints.js.map +1 -1
- package/dist/server/shared/messages.d.ts +2065 -313
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +40 -1
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/tool-call-display.d.ts.map +1 -1
- package/dist/server/shared/tool-call-display.js +4 -0
- package/dist/server/shared/tool-call-display.js.map +1 -1
- package/package.json +3 -3
|
@@ -4,13 +4,19 @@ import { isRelayClientWebSocketUrl } from '../shared/daemon-endpoints.js';
|
|
|
4
4
|
import { asUint8Array, decodeBinaryMuxFrame, encodeBinaryMuxFrame, BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from '../shared/binary-mux.js';
|
|
5
5
|
import { encodeTerminalKeyInput } from '../shared/terminal-key-input.js';
|
|
6
6
|
import { TerminalStreamManager, } from './daemon-client-terminal-stream-manager.js';
|
|
7
|
-
import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, encodeUtf8String,
|
|
7
|
+
import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, encodeUtf8String, } from './daemon-client-transport.js';
|
|
8
8
|
const consoleLogger = {
|
|
9
|
-
debug: (
|
|
9
|
+
debug: () => { },
|
|
10
10
|
info: (obj, msg) => console.info(msg, obj),
|
|
11
11
|
warn: (obj, msg) => console.warn(msg, obj),
|
|
12
12
|
error: (obj, msg) => console.error(msg, obj),
|
|
13
13
|
};
|
|
14
|
+
function getNowMs() {
|
|
15
|
+
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
|
16
|
+
return performance.now();
|
|
17
|
+
}
|
|
18
|
+
return Date.now();
|
|
19
|
+
}
|
|
14
20
|
class DaemonRpcError extends Error {
|
|
15
21
|
constructor(params) {
|
|
16
22
|
super(params.error);
|
|
@@ -22,6 +28,7 @@ class DaemonRpcError extends Error {
|
|
|
22
28
|
}
|
|
23
29
|
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500;
|
|
24
30
|
const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000;
|
|
31
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
|
|
25
32
|
/** Default timeout for waiting for connection before sending queued messages */
|
|
26
33
|
const DEFAULT_SEND_QUEUE_TIMEOUT_MS = 10000;
|
|
27
34
|
const DEFAULT_DICTATION_FINISH_ACCEPT_TIMEOUT_MS = 15000;
|
|
@@ -30,6 +37,42 @@ const DEFAULT_DICTATION_FINISH_TIMEOUT_GRACE_MS = 5000;
|
|
|
30
37
|
function isWaiterTimeoutError(error) {
|
|
31
38
|
return error instanceof Error && error.message.startsWith('Timeout waiting for message');
|
|
32
39
|
}
|
|
40
|
+
function normalizeClientId(value) {
|
|
41
|
+
if (typeof value !== 'string') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
46
|
+
}
|
|
47
|
+
function hashForLog(value) {
|
|
48
|
+
let hash = 0;
|
|
49
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
50
|
+
hash = (hash * 31 + value.charCodeAt(index)) | 0;
|
|
51
|
+
}
|
|
52
|
+
return `h_${Math.abs(hash).toString(16)}`;
|
|
53
|
+
}
|
|
54
|
+
function toReasonCode(reason) {
|
|
55
|
+
if (!reason) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const normalized = reason.toLowerCase();
|
|
59
|
+
if (normalized.includes('timed out')) {
|
|
60
|
+
return 'connect_timeout';
|
|
61
|
+
}
|
|
62
|
+
if (normalized.includes('disposed')) {
|
|
63
|
+
return 'disposed';
|
|
64
|
+
}
|
|
65
|
+
if (normalized.includes('client closed')) {
|
|
66
|
+
return 'client_closed';
|
|
67
|
+
}
|
|
68
|
+
if (normalized.includes('transport')) {
|
|
69
|
+
return 'transport_error';
|
|
70
|
+
}
|
|
71
|
+
if (normalized.includes('failed to connect')) {
|
|
72
|
+
return 'connect_failed';
|
|
73
|
+
}
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
33
76
|
export class DaemonClient {
|
|
34
77
|
constructor(config) {
|
|
35
78
|
this.config = config;
|
|
@@ -42,6 +85,7 @@ export class DaemonClient {
|
|
|
42
85
|
this.checkoutStatusInFlight = new Map();
|
|
43
86
|
this.connectionListeners = new Set();
|
|
44
87
|
this.reconnectTimeout = null;
|
|
88
|
+
this.connectTimeout = null;
|
|
45
89
|
this.pendingGenericTransportErrorTimeout = null;
|
|
46
90
|
this.reconnectAttempt = 0;
|
|
47
91
|
this.shouldReconnect = true;
|
|
@@ -53,8 +97,28 @@ export class DaemonClient {
|
|
|
53
97
|
this.checkoutDiffSubscriptions = new Map();
|
|
54
98
|
this.terminalDirectorySubscriptions = new Set();
|
|
55
99
|
this.pendingSendQueue = [];
|
|
56
|
-
this.
|
|
100
|
+
this.lastWelcomeMessage = null;
|
|
57
101
|
this.logger = config.logger ?? consoleLogger;
|
|
102
|
+
this.logConnectionPath = isRelayClientWebSocketUrl(this.config.url) ? 'relay' : 'direct';
|
|
103
|
+
let parsedUrlForLog = null;
|
|
104
|
+
try {
|
|
105
|
+
parsedUrlForLog = new URL(this.config.url);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
parsedUrlForLog = null;
|
|
109
|
+
}
|
|
110
|
+
const parsedServerIdForLog = normalizeClientId(parsedUrlForLog?.searchParams.get('serverId'));
|
|
111
|
+
this.logServerId = parsedServerIdForLog ?? parsedUrlForLog?.host ?? null;
|
|
112
|
+
const resolvedClientId = normalizeClientId(this.config.clientId);
|
|
113
|
+
if (!resolvedClientId) {
|
|
114
|
+
throw new Error('Daemon client requires a non-empty clientId');
|
|
115
|
+
}
|
|
116
|
+
this.config.clientId = resolvedClientId;
|
|
117
|
+
this.logClientIdHash = hashForLog(resolvedClientId);
|
|
118
|
+
this.logGeneration =
|
|
119
|
+
typeof this.config.runtimeGeneration === 'number' && Number.isFinite(this.config.runtimeGeneration)
|
|
120
|
+
? this.config.runtimeGeneration
|
|
121
|
+
: null;
|
|
58
122
|
this.terminalStreams = new TerminalStreamManager({
|
|
59
123
|
sendAck: (ack) => {
|
|
60
124
|
this.sendBinaryFrame({
|
|
@@ -66,27 +130,22 @@ export class DaemonClient {
|
|
|
66
130
|
});
|
|
67
131
|
},
|
|
68
132
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this.relayClientId = `clt_${safeRandomId()}`;
|
|
77
|
-
parsed.searchParams.set('clientId', this.relayClientId);
|
|
78
|
-
this.config.url = parsed.toString();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// ignore - invalid URL will be handled on connect
|
|
83
|
-
}
|
|
133
|
+
}
|
|
134
|
+
emitDiagnosticsEvent(event) {
|
|
135
|
+
try {
|
|
136
|
+
this.config.onDiagnosticsEvent?.(event);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Diagnostics hooks must never break daemon message handling.
|
|
84
140
|
}
|
|
85
141
|
}
|
|
86
142
|
// ============================================================================
|
|
87
143
|
// Connection
|
|
88
144
|
// ============================================================================
|
|
89
145
|
async connect() {
|
|
146
|
+
if (this.connectionState.status === 'disposed') {
|
|
147
|
+
throw new Error('Daemon client is disposed');
|
|
148
|
+
}
|
|
90
149
|
if (this.connectionState.status === 'connected') {
|
|
91
150
|
return;
|
|
92
151
|
}
|
|
@@ -102,6 +161,10 @@ export class DaemonClient {
|
|
|
102
161
|
return this.connectPromise;
|
|
103
162
|
}
|
|
104
163
|
attemptConnect() {
|
|
164
|
+
if (this.connectionState.status === 'disposed') {
|
|
165
|
+
this.rejectConnect(new Error('Daemon client is disposed'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
105
168
|
if (!this.shouldReconnect) {
|
|
106
169
|
this.rejectConnect(new Error('Daemon client is closed'));
|
|
107
170
|
return;
|
|
@@ -114,10 +177,8 @@ export class DaemonClient {
|
|
|
114
177
|
headers['Authorization'] = this.config.authHeader;
|
|
115
178
|
}
|
|
116
179
|
try {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// concurrent relay sockets. Cloudflare then closes the old one with
|
|
120
|
-
// "Replaced by new connection", causing a disconnect loop.
|
|
180
|
+
// Reconnect can overlap with browser close/error delivery ordering.
|
|
181
|
+
// Always dispose previous transport before constructing the next one.
|
|
121
182
|
this.disposeTransport();
|
|
122
183
|
const baseTransportFactory = this.config.transportFactory ??
|
|
123
184
|
createWebSocketTransportFactory(this.config.webSocketFactory ?? defaultWebSocketFactory);
|
|
@@ -134,12 +195,28 @@ export class DaemonClient {
|
|
|
134
195
|
logger: this.logger,
|
|
135
196
|
});
|
|
136
197
|
}
|
|
137
|
-
const
|
|
198
|
+
const transportUrl = this.resolveTransportUrlForAttempt();
|
|
199
|
+
const transport = transportFactory({ url: transportUrl, headers });
|
|
138
200
|
this.transport = transport;
|
|
201
|
+
this.lastWelcomeMessage = null;
|
|
139
202
|
this.updateConnectionState({
|
|
140
203
|
status: 'connecting',
|
|
141
204
|
attempt: this.reconnectAttempt,
|
|
142
|
-
});
|
|
205
|
+
}, { event: 'CONNECT_REQUEST' });
|
|
206
|
+
this.resetConnectTimeout();
|
|
207
|
+
const timeoutMs = Math.max(1, this.config.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
|
|
208
|
+
this.connectTimeout = setTimeout(() => {
|
|
209
|
+
if (this.connectionState.status !== 'connecting') {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.lastErrorValue = 'Connection timed out';
|
|
213
|
+
this.disposeTransport(1001, 'Connection timed out');
|
|
214
|
+
this.scheduleReconnect({
|
|
215
|
+
reason: 'Connection timed out',
|
|
216
|
+
event: 'CONNECT_TIMEOUT',
|
|
217
|
+
reasonCode: 'connect_timeout',
|
|
218
|
+
});
|
|
219
|
+
}, timeoutMs);
|
|
143
220
|
this.transportCleanup = [
|
|
144
221
|
transport.onOpen(() => {
|
|
145
222
|
if (this.pendingGenericTransportErrorTimeout) {
|
|
@@ -147,48 +224,26 @@ export class DaemonClient {
|
|
|
147
224
|
this.pendingGenericTransportErrorTimeout = null;
|
|
148
225
|
}
|
|
149
226
|
this.lastErrorValue = null;
|
|
150
|
-
this.
|
|
151
|
-
this.updateConnectionState({ status: 'connected' });
|
|
152
|
-
this.resubscribeCheckoutDiffSubscriptions();
|
|
153
|
-
this.resubscribeTerminalDirectorySubscriptions();
|
|
154
|
-
this.flushPendingSendQueue();
|
|
155
|
-
this.resolveConnect();
|
|
227
|
+
this.sendHelloMessage();
|
|
156
228
|
}),
|
|
157
229
|
transport.onClose((event) => {
|
|
230
|
+
this.resetConnectTimeout();
|
|
158
231
|
if (this.pendingGenericTransportErrorTimeout) {
|
|
159
232
|
clearTimeout(this.pendingGenericTransportErrorTimeout);
|
|
160
233
|
this.pendingGenericTransportErrorTimeout = null;
|
|
161
234
|
}
|
|
162
|
-
const closeRecord = event;
|
|
163
|
-
const closeCode = closeRecord && typeof closeRecord === 'object' && typeof closeRecord.code === 'number'
|
|
164
|
-
? closeRecord.code
|
|
165
|
-
: null;
|
|
166
|
-
const closeReason = closeRecord && typeof closeRecord === 'object' && typeof closeRecord.reason === 'string'
|
|
167
|
-
? closeRecord.reason
|
|
168
|
-
: null;
|
|
169
235
|
const reason = describeTransportClose(event);
|
|
170
236
|
if (reason) {
|
|
171
237
|
this.lastErrorValue = reason;
|
|
172
238
|
}
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
|
|
239
|
+
this.scheduleReconnect({
|
|
240
|
+
reason,
|
|
241
|
+
event: 'TRANSPORT_CLOSE',
|
|
242
|
+
reasonCode: 'transport_closed',
|
|
176
243
|
});
|
|
177
|
-
// When connecting over the relay, only one client connection is allowed at a time.
|
|
178
|
-
// If another device/tab takes over, we should not auto-reconnect and "fight" the new
|
|
179
|
-
// connection (which causes flapping where both sides repeatedly replace each other).
|
|
180
|
-
if (isRelayClientWebSocketUrl(this.config.url) &&
|
|
181
|
-
closeCode === 1008 &&
|
|
182
|
-
(closeReason ?? reason) === 'Replaced by new connection') {
|
|
183
|
-
this.shouldReconnect = false;
|
|
184
|
-
this.clearWaiters(new Error(reason ?? 'Replaced by new connection'));
|
|
185
|
-
this.rejectPendingSendQueue(new Error(reason ?? 'Replaced by new connection'));
|
|
186
|
-
this.rejectConnect(new Error(reason ?? 'Replaced by new connection'));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
this.scheduleReconnect(reason);
|
|
190
244
|
}),
|
|
191
245
|
transport.onError((event) => {
|
|
246
|
+
this.resetConnectTimeout();
|
|
192
247
|
const reason = describeTransportError(event);
|
|
193
248
|
const isGeneric = reason === 'Transport error';
|
|
194
249
|
// Browser WebSocket.onerror often provides no useful details and is followed
|
|
@@ -202,8 +257,11 @@ export class DaemonClient {
|
|
|
202
257
|
if (this.connectionState.status === 'connected' ||
|
|
203
258
|
this.connectionState.status === 'connecting') {
|
|
204
259
|
this.lastErrorValue = reason;
|
|
205
|
-
this.
|
|
206
|
-
|
|
260
|
+
this.scheduleReconnect({
|
|
261
|
+
reason,
|
|
262
|
+
event: 'TRANSPORT_ERROR',
|
|
263
|
+
reasonCode: 'transport_error',
|
|
264
|
+
});
|
|
207
265
|
}
|
|
208
266
|
}, 250);
|
|
209
267
|
}
|
|
@@ -214,16 +272,24 @@ export class DaemonClient {
|
|
|
214
272
|
this.pendingGenericTransportErrorTimeout = null;
|
|
215
273
|
}
|
|
216
274
|
this.lastErrorValue = reason;
|
|
217
|
-
this.
|
|
218
|
-
|
|
275
|
+
this.scheduleReconnect({
|
|
276
|
+
reason,
|
|
277
|
+
event: 'TRANSPORT_ERROR',
|
|
278
|
+
reasonCode: 'transport_error',
|
|
279
|
+
});
|
|
219
280
|
}),
|
|
220
281
|
transport.onMessage((data) => this.handleTransportMessage(data)),
|
|
221
282
|
];
|
|
222
283
|
}
|
|
223
284
|
catch (error) {
|
|
285
|
+
this.resetConnectTimeout();
|
|
224
286
|
const message = error instanceof Error ? error.message : 'Failed to connect';
|
|
225
287
|
this.lastErrorValue = message;
|
|
226
|
-
this.scheduleReconnect(
|
|
288
|
+
this.scheduleReconnect({
|
|
289
|
+
reason: message,
|
|
290
|
+
event: 'CONNECT_FAILED',
|
|
291
|
+
reasonCode: 'connect_failed',
|
|
292
|
+
});
|
|
227
293
|
this.rejectConnect(error instanceof Error ? error : new Error(message));
|
|
228
294
|
}
|
|
229
295
|
}
|
|
@@ -244,6 +310,9 @@ export class DaemonClient {
|
|
|
244
310
|
this.connectReject = null;
|
|
245
311
|
}
|
|
246
312
|
async close() {
|
|
313
|
+
if (this.connectionState.status === 'disposed') {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
247
316
|
this.shouldReconnect = false;
|
|
248
317
|
this.connectPromise = null;
|
|
249
318
|
this.connectResolve = null;
|
|
@@ -252,15 +321,18 @@ export class DaemonClient {
|
|
|
252
321
|
clearTimeout(this.reconnectTimeout);
|
|
253
322
|
this.reconnectTimeout = null;
|
|
254
323
|
}
|
|
324
|
+
this.resetConnectTimeout();
|
|
255
325
|
this.disposeTransport(1000, 'Client closed');
|
|
256
326
|
this.clearWaiters(new Error('Daemon client closed'));
|
|
327
|
+
this.rejectPendingSendQueue(new Error('Daemon client closed'));
|
|
257
328
|
this.terminalStreams.clearAll();
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
reason: 'client_closed',
|
|
261
|
-
});
|
|
329
|
+
this.lastWelcomeMessage = null;
|
|
330
|
+
this.updateConnectionState({ status: 'disposed' }, { event: 'DISPOSE', reason: 'Client closed', reasonCode: 'disposed' });
|
|
262
331
|
}
|
|
263
332
|
ensureConnected() {
|
|
333
|
+
if (this.connectionState.status === 'disposed') {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
264
336
|
if (!this.shouldReconnect) {
|
|
265
337
|
this.shouldReconnect = true;
|
|
266
338
|
}
|
|
@@ -614,7 +686,10 @@ export class DaemonClient {
|
|
|
614
686
|
if (payload.error) {
|
|
615
687
|
throw new Error(payload.error);
|
|
616
688
|
}
|
|
617
|
-
|
|
689
|
+
if (!payload.agent) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
return { agent: payload.agent, project: payload.project ?? null };
|
|
618
693
|
}
|
|
619
694
|
resubscribeCheckoutDiffSubscriptions() {
|
|
620
695
|
if (this.checkoutDiffSubscriptions.size === 0) {
|
|
@@ -653,6 +728,7 @@ export class DaemonClient {
|
|
|
653
728
|
requestId,
|
|
654
729
|
config,
|
|
655
730
|
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
|
731
|
+
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
|
656
732
|
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
|
657
733
|
...(options.images && options.images.length > 0 ? { images: options.images } : {}),
|
|
658
734
|
...(options.git ? { git: options.git } : {}),
|
|
@@ -1594,15 +1670,91 @@ export class DaemonClient {
|
|
|
1594
1670
|
// Waiting / Streaming Helpers
|
|
1595
1671
|
// ============================================================================
|
|
1596
1672
|
async waitForAgentUpsert(agentId, predicate, timeout = 60000) {
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1673
|
+
const initialResult = await this.fetchAgent(agentId).catch(() => null);
|
|
1674
|
+
if (initialResult && predicate(initialResult.agent)) {
|
|
1675
|
+
return initialResult.agent;
|
|
1676
|
+
}
|
|
1677
|
+
const deadline = Date.now() + timeout;
|
|
1678
|
+
return await new Promise((resolve, reject) => {
|
|
1679
|
+
let settled = false;
|
|
1680
|
+
let pollInFlight = false;
|
|
1681
|
+
let pollTimer = null;
|
|
1682
|
+
let timeoutTimer = null;
|
|
1683
|
+
let unsubscribe = null;
|
|
1684
|
+
const finish = (result) => {
|
|
1685
|
+
if (settled) {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
settled = true;
|
|
1689
|
+
if (timeoutTimer) {
|
|
1690
|
+
clearTimeout(timeoutTimer);
|
|
1691
|
+
timeoutTimer = null;
|
|
1692
|
+
}
|
|
1693
|
+
if (pollTimer) {
|
|
1694
|
+
clearInterval(pollTimer);
|
|
1695
|
+
pollTimer = null;
|
|
1696
|
+
}
|
|
1697
|
+
if (unsubscribe) {
|
|
1698
|
+
unsubscribe();
|
|
1699
|
+
unsubscribe = null;
|
|
1700
|
+
}
|
|
1701
|
+
if (result.kind === 'ok') {
|
|
1702
|
+
resolve(result.snapshot);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
reject(result.error);
|
|
1706
|
+
};
|
|
1707
|
+
const maybeResolve = (snapshot) => {
|
|
1708
|
+
if (!snapshot) {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
if (!predicate(snapshot)) {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
finish({ kind: 'ok', snapshot });
|
|
1715
|
+
return true;
|
|
1716
|
+
};
|
|
1717
|
+
const poll = async () => {
|
|
1718
|
+
if (settled || pollInFlight) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
pollInFlight = true;
|
|
1722
|
+
try {
|
|
1723
|
+
const result = await this.fetchAgent(agentId).catch(() => null);
|
|
1724
|
+
maybeResolve(result?.agent ?? null);
|
|
1725
|
+
}
|
|
1726
|
+
finally {
|
|
1727
|
+
pollInFlight = false;
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
unsubscribe = this.on('agent_update', (message) => {
|
|
1731
|
+
if (settled) {
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (message.type !== 'agent_update') {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (message.payload.kind !== 'upsert') {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const snapshot = message.payload.agent;
|
|
1741
|
+
if (snapshot.id !== agentId) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
maybeResolve(snapshot);
|
|
1745
|
+
});
|
|
1746
|
+
const remaining = Math.max(1, deadline - Date.now());
|
|
1747
|
+
timeoutTimer = setTimeout(() => {
|
|
1748
|
+
finish({
|
|
1749
|
+
kind: 'error',
|
|
1750
|
+
error: new Error(`Timed out waiting for agent ${agentId}`),
|
|
1751
|
+
});
|
|
1752
|
+
}, remaining);
|
|
1753
|
+
pollTimer = setInterval(() => {
|
|
1754
|
+
void poll();
|
|
1755
|
+
}, 250);
|
|
1756
|
+
void poll();
|
|
1757
|
+
});
|
|
1606
1758
|
}
|
|
1607
1759
|
async waitForFinish(agentId, timeout = 60000) {
|
|
1608
1760
|
const requestId = this.createRequestId();
|
|
@@ -1822,6 +1974,39 @@ export class DaemonClient {
|
|
|
1822
1974
|
createRequestId(requestId) {
|
|
1823
1975
|
return requestId ?? crypto.randomUUID();
|
|
1824
1976
|
}
|
|
1977
|
+
getLastWelcomeMessage() {
|
|
1978
|
+
return this.lastWelcomeMessage;
|
|
1979
|
+
}
|
|
1980
|
+
resolveTransportUrlForAttempt() {
|
|
1981
|
+
return this.config.url;
|
|
1982
|
+
}
|
|
1983
|
+
sendHelloMessage() {
|
|
1984
|
+
if (!this.transport) {
|
|
1985
|
+
this.scheduleReconnect({
|
|
1986
|
+
reason: 'Transport unavailable before hello',
|
|
1987
|
+
event: 'HELLO_TRANSPORT_MISSING',
|
|
1988
|
+
reasonCode: 'transport_error',
|
|
1989
|
+
});
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
try {
|
|
1993
|
+
this.transport.send(JSON.stringify({
|
|
1994
|
+
type: 'hello',
|
|
1995
|
+
clientId: this.config.clientId,
|
|
1996
|
+
clientType: this.config.clientType ?? 'cli',
|
|
1997
|
+
protocolVersion: 1,
|
|
1998
|
+
}));
|
|
1999
|
+
}
|
|
2000
|
+
catch (error) {
|
|
2001
|
+
const message = error instanceof Error ? error.message : 'Failed to send hello message';
|
|
2002
|
+
this.lastErrorValue = message;
|
|
2003
|
+
this.scheduleReconnect({
|
|
2004
|
+
reason: message,
|
|
2005
|
+
event: 'HELLO_SEND_FAILED',
|
|
2006
|
+
reasonCode: 'transport_error',
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1825
2010
|
disposeTransport(code = 1001, reason = 'Reconnecting') {
|
|
1826
2011
|
this.cleanupTransport();
|
|
1827
2012
|
if (this.transport) {
|
|
@@ -1835,6 +2020,7 @@ export class DaemonClient {
|
|
|
1835
2020
|
}
|
|
1836
2021
|
}
|
|
1837
2022
|
cleanupTransport() {
|
|
2023
|
+
this.resetConnectTimeout();
|
|
1838
2024
|
if (this.pendingGenericTransportErrorTimeout) {
|
|
1839
2025
|
clearTimeout(this.pendingGenericTransportErrorTimeout);
|
|
1840
2026
|
this.pendingGenericTransportErrorTimeout = null;
|
|
@@ -1849,12 +2035,26 @@ export class DaemonClient {
|
|
|
1849
2035
|
}
|
|
1850
2036
|
this.transportCleanup = [];
|
|
1851
2037
|
}
|
|
2038
|
+
resetConnectTimeout() {
|
|
2039
|
+
if (!this.connectTimeout) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
clearTimeout(this.connectTimeout);
|
|
2043
|
+
this.connectTimeout = null;
|
|
2044
|
+
}
|
|
1852
2045
|
handleTransportMessage(data) {
|
|
2046
|
+
const startedAtMs = getNowMs();
|
|
1853
2047
|
const rawData = data && typeof data === 'object' && 'data' in data ? data.data : data;
|
|
1854
2048
|
const rawBytes = asUint8Array(rawData);
|
|
1855
2049
|
if (rawBytes) {
|
|
1856
2050
|
const frame = decodeBinaryMuxFrame(rawBytes);
|
|
1857
2051
|
if (frame) {
|
|
2052
|
+
this.emitDiagnosticsEvent({
|
|
2053
|
+
type: 'transport_binary_frame',
|
|
2054
|
+
channel: frame.channel,
|
|
2055
|
+
messageType: frame.messageType,
|
|
2056
|
+
payloadBytes: frame.payload?.byteLength ?? 0,
|
|
2057
|
+
});
|
|
1858
2058
|
this.handleBinaryFrame(frame);
|
|
1859
2059
|
return;
|
|
1860
2060
|
}
|
|
@@ -1864,21 +2064,64 @@ export class DaemonClient {
|
|
|
1864
2064
|
return;
|
|
1865
2065
|
}
|
|
1866
2066
|
let parsedJson;
|
|
2067
|
+
let parseMs = 0;
|
|
1867
2068
|
try {
|
|
2069
|
+
const parseStartedAtMs = getNowMs();
|
|
1868
2070
|
parsedJson = JSON.parse(payload);
|
|
2071
|
+
parseMs = getNowMs() - parseStartedAtMs;
|
|
1869
2072
|
}
|
|
1870
2073
|
catch {
|
|
2074
|
+
this.emitDiagnosticsEvent({
|
|
2075
|
+
type: 'transport_message_timing',
|
|
2076
|
+
messageType: 'unknown',
|
|
2077
|
+
payloadBytes: payload.length,
|
|
2078
|
+
parseMs: 0,
|
|
2079
|
+
validateMs: 0,
|
|
2080
|
+
totalMs: getNowMs() - startedAtMs,
|
|
2081
|
+
outcome: 'parse_error',
|
|
2082
|
+
});
|
|
1871
2083
|
return;
|
|
1872
2084
|
}
|
|
2085
|
+
const validateStartedAtMs = getNowMs();
|
|
1873
2086
|
const parsed = WSOutboundMessageSchema.safeParse(parsedJson);
|
|
2087
|
+
const validateMs = getNowMs() - validateStartedAtMs;
|
|
1874
2088
|
if (!parsed.success) {
|
|
1875
|
-
const msgType = parsedJson?.
|
|
2089
|
+
const msgType = parsedJson?.type ?? 'unknown';
|
|
2090
|
+
this.emitDiagnosticsEvent({
|
|
2091
|
+
type: 'transport_message_timing',
|
|
2092
|
+
messageType: msgType,
|
|
2093
|
+
payloadBytes: payload.length,
|
|
2094
|
+
parseMs,
|
|
2095
|
+
validateMs,
|
|
2096
|
+
totalMs: getNowMs() - startedAtMs,
|
|
2097
|
+
outcome: 'validation_error',
|
|
2098
|
+
});
|
|
1876
2099
|
this.logger.warn({ msgType, error: parsed.error.message }, 'Message validation failed');
|
|
1877
2100
|
return;
|
|
1878
2101
|
}
|
|
2102
|
+
this.emitDiagnosticsEvent({
|
|
2103
|
+
type: 'transport_message_timing',
|
|
2104
|
+
messageType: parsed.data.type,
|
|
2105
|
+
payloadBytes: payload.length,
|
|
2106
|
+
parseMs,
|
|
2107
|
+
validateMs,
|
|
2108
|
+
totalMs: getNowMs() - startedAtMs,
|
|
2109
|
+
outcome: 'ok',
|
|
2110
|
+
});
|
|
1879
2111
|
if (parsed.data.type === 'pong') {
|
|
1880
2112
|
return;
|
|
1881
2113
|
}
|
|
2114
|
+
if (parsed.data.type === 'welcome') {
|
|
2115
|
+
this.lastWelcomeMessage = parsed.data;
|
|
2116
|
+
this.resetConnectTimeout();
|
|
2117
|
+
this.reconnectAttempt = 0;
|
|
2118
|
+
this.updateConnectionState({ status: 'connected' }, { event: 'HELLO_WELCOME' });
|
|
2119
|
+
this.resubscribeCheckoutDiffSubscriptions();
|
|
2120
|
+
this.resubscribeTerminalDirectorySubscriptions();
|
|
2121
|
+
this.flushPendingSendQueue();
|
|
2122
|
+
this.resolveConnect();
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
1882
2125
|
this.handleSessionMessage(parsed.data.message);
|
|
1883
2126
|
}
|
|
1884
2127
|
handleBinaryFrame(frame) {
|
|
@@ -1895,8 +2138,25 @@ export class DaemonClient {
|
|
|
1895
2138
|
return;
|
|
1896
2139
|
}
|
|
1897
2140
|
}
|
|
1898
|
-
updateConnectionState(next) {
|
|
2141
|
+
updateConnectionState(next, metadata) {
|
|
2142
|
+
const previous = this.connectionState;
|
|
1899
2143
|
this.connectionState = next;
|
|
2144
|
+
const reasonFromNext = next.status === 'disconnected' && typeof next.reason === 'string'
|
|
2145
|
+
? next.reason
|
|
2146
|
+
: null;
|
|
2147
|
+
const reason = metadata?.reason ?? reasonFromNext;
|
|
2148
|
+
const reasonCode = metadata?.reasonCode ?? toReasonCode(reason);
|
|
2149
|
+
this.logger.debug({
|
|
2150
|
+
serverId: this.logServerId,
|
|
2151
|
+
clientIdHash: this.logClientIdHash,
|
|
2152
|
+
from: previous.status,
|
|
2153
|
+
to: next.status,
|
|
2154
|
+
event: metadata?.event ?? 'STATE_UPDATE',
|
|
2155
|
+
connectionPath: this.logConnectionPath,
|
|
2156
|
+
generation: this.logGeneration,
|
|
2157
|
+
reasonCode,
|
|
2158
|
+
reason,
|
|
2159
|
+
}, 'DaemonClientTransition');
|
|
1900
2160
|
for (const listener of this.connectionListeners) {
|
|
1901
2161
|
try {
|
|
1902
2162
|
listener(next);
|
|
@@ -1906,20 +2166,13 @@ export class DaemonClient {
|
|
|
1906
2166
|
}
|
|
1907
2167
|
}
|
|
1908
2168
|
}
|
|
1909
|
-
scheduleReconnect(
|
|
2169
|
+
scheduleReconnect(input) {
|
|
1910
2170
|
if (this.reconnectTimeout) {
|
|
1911
2171
|
clearTimeout(this.reconnectTimeout);
|
|
1912
2172
|
this.reconnectTimeout = null;
|
|
1913
2173
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
const attempt = this.reconnectAttempt;
|
|
1919
|
-
const baseDelay = this.config.reconnect?.baseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
1920
|
-
const maxDelay = this.config.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
1921
|
-
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
1922
|
-
this.reconnectAttempt = attempt + 1;
|
|
2174
|
+
const wasDisposed = this.connectionState.status === 'disposed';
|
|
2175
|
+
const reason = input?.reason;
|
|
1923
2176
|
if (typeof reason === 'string' && reason.trim().length > 0) {
|
|
1924
2177
|
this.lastErrorValue = reason.trim();
|
|
1925
2178
|
}
|
|
@@ -1928,10 +2181,28 @@ export class DaemonClient {
|
|
|
1928
2181
|
this.clearWaiters(new Error(reason ?? 'Connection lost'));
|
|
1929
2182
|
this.rejectPendingSendQueue(new Error(reason ?? 'Connection lost'));
|
|
1930
2183
|
this.terminalStreams.clearAll();
|
|
2184
|
+
this.lastWelcomeMessage = null;
|
|
2185
|
+
if (wasDisposed) {
|
|
2186
|
+
this.rejectConnect(new Error(reason ?? 'Daemon client is disposed'));
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
1931
2189
|
this.updateConnectionState({
|
|
1932
2190
|
status: 'disconnected',
|
|
1933
2191
|
...(reason ? { reason } : {}),
|
|
2192
|
+
}, {
|
|
2193
|
+
event: input?.event ?? 'TRANSPORT_CLOSE',
|
|
2194
|
+
...(reason ? { reason } : {}),
|
|
2195
|
+
...(input?.reasonCode ? { reasonCode: input.reasonCode } : {}),
|
|
1934
2196
|
});
|
|
2197
|
+
if (!this.shouldReconnect || this.config.reconnect?.enabled === false) {
|
|
2198
|
+
this.rejectConnect(new Error(reason ?? 'Transport disconnected before connect'));
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const attempt = this.reconnectAttempt;
|
|
2202
|
+
const baseDelay = this.config.reconnect?.baseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
2203
|
+
const maxDelay = this.config.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
2204
|
+
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
2205
|
+
this.reconnectAttempt = attempt + 1;
|
|
1935
2206
|
this.reconnectTimeout = setTimeout(() => {
|
|
1936
2207
|
this.reconnectTimeout = null;
|
|
1937
2208
|
if (!this.shouldReconnect) {
|