@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.
Files changed (65) hide show
  1. package/dist/server/client/daemon-client.d.ts +41 -4
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +355 -84
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-manager.d.ts +10 -0
  6. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  7. package/dist/server/server/agent/agent-manager.js +261 -18
  8. package/dist/server/server/agent/agent-manager.js.map +1 -1
  9. package/dist/server/server/agent/agent-projections.d.ts +5 -0
  10. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  11. package/dist/server/server/agent/agent-projections.js +24 -0
  12. package/dist/server/server/agent/agent-projections.js.map +1 -1
  13. package/dist/server/server/agent/agent-sdk-types.d.ts +11 -0
  14. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  15. package/dist/server/server/agent/agent-storage.d.ts +15 -5
  16. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-storage.js +2 -0
  18. package/dist/server/server/agent/agent-storage.js.map +1 -1
  19. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.d.ts.map +1 -1
  20. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js +2 -0
  21. package/dist/server/server/agent/providers/claude/tool-call-detail-parser.js.map +1 -1
  22. package/dist/server/server/agent/providers/claude/tool-call-mapper.d.ts.map +1 -1
  23. package/dist/server/server/agent/providers/claude/tool-call-mapper.js +2 -0
  24. package/dist/server/server/agent/providers/claude/tool-call-mapper.js.map +1 -1
  25. package/dist/server/server/agent/providers/claude-agent.d.ts +7 -1
  26. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  27. package/dist/server/server/agent/providers/claude-agent.js +1470 -237
  28. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  29. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  30. package/dist/server/server/agent/providers/codex-app-server-agent.js +19 -4
  31. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  32. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +40 -0
  33. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  34. package/dist/server/server/agent/providers/tool-call-detail-primitives.js +1 -0
  35. package/dist/server/server/agent/providers/tool-call-detail-primitives.js.map +1 -1
  36. package/dist/server/server/client-message-id.d.ts +3 -0
  37. package/dist/server/server/client-message-id.d.ts.map +1 -0
  38. package/dist/server/server/client-message-id.js +12 -0
  39. package/dist/server/server/client-message-id.js.map +1 -0
  40. package/dist/server/server/persisted-config.d.ts +8 -8
  41. package/dist/server/server/persistence-hooks.js +1 -1
  42. package/dist/server/server/persistence-hooks.js.map +1 -1
  43. package/dist/server/server/relay-transport.d.ts.map +1 -1
  44. package/dist/server/server/relay-transport.js +27 -28
  45. package/dist/server/server/relay-transport.js.map +1 -1
  46. package/dist/server/server/session.d.ts +4 -2
  47. package/dist/server/server/session.d.ts.map +1 -1
  48. package/dist/server/server/session.js +122 -31
  49. package/dist/server/server/session.js.map +1 -1
  50. package/dist/server/server/websocket-server.d.ts +8 -4
  51. package/dist/server/server/websocket-server.d.ts.map +1 -1
  52. package/dist/server/server/websocket-server.js +272 -75
  53. package/dist/server/server/websocket-server.js.map +1 -1
  54. package/dist/server/shared/daemon-endpoints.d.ts +9 -1
  55. package/dist/server/shared/daemon-endpoints.d.ts.map +1 -1
  56. package/dist/server/shared/daemon-endpoints.js +18 -3
  57. package/dist/server/shared/daemon-endpoints.js.map +1 -1
  58. package/dist/server/shared/messages.d.ts +2065 -313
  59. package/dist/server/shared/messages.d.ts.map +1 -1
  60. package/dist/server/shared/messages.js +40 -1
  61. package/dist/server/shared/messages.js.map +1 -1
  62. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  63. package/dist/server/shared/tool-call-display.js +4 -0
  64. package/dist/server/shared/tool-call-display.js.map +1 -1
  65. 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, safeRandomId, } from './daemon-client-transport.js';
7
+ import { createRelayE2eeTransportFactory, createWebSocketTransportFactory, decodeMessageData, defaultWebSocketFactory, describeTransportClose, describeTransportError, encodeUtf8String, } from './daemon-client-transport.js';
8
8
  const consoleLogger = {
9
- debug: (obj, msg) => console.debug(msg, obj),
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.relayClientId = null;
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
- // Relay requires a clientId so the daemon can create an independent
70
- // socket + E2EE channel per connected client. Generate one per DaemonClient
71
- // instance (stable across reconnects in this tab/app session).
72
- if (isRelayClientWebSocketUrl(this.config.url)) {
73
- try {
74
- const parsed = new URL(this.config.url);
75
- if (!parsed.searchParams.get('clientId')) {
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
- // If we reconnect while the previous socket is still open (common in browsers
118
- // where `onerror` may fire before `onclose`), we can end up with multiple
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 transport = transportFactory({ url: this.config.url, headers });
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.reconnectAttempt = 0;
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.updateConnectionState({
174
- status: 'disconnected',
175
- ...(reason ? { reason } : {}),
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.updateConnectionState({ status: 'disconnected', reason });
206
- this.scheduleReconnect(reason);
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.updateConnectionState({ status: 'disconnected', reason });
218
- this.scheduleReconnect(reason);
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(message);
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.updateConnectionState({
259
- status: 'disconnected',
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
- return payload.agent;
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 startedAt = Date.now();
1598
- while (Date.now() - startedAt < timeout) {
1599
- const snapshot = await this.fetchAgent(agentId).catch(() => null);
1600
- if (snapshot && predicate(snapshot)) {
1601
- return snapshot;
1602
- }
1603
- await new Promise((resolve) => setTimeout(resolve, 250));
1604
- }
1605
- throw new Error(`Timed out waiting for agent ${agentId}`);
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?.message?.type ?? 'unknown';
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(reason) {
2169
+ scheduleReconnect(input) {
1910
2170
  if (this.reconnectTimeout) {
1911
2171
  clearTimeout(this.reconnectTimeout);
1912
2172
  this.reconnectTimeout = null;
1913
2173
  }
1914
- if (!this.shouldReconnect || this.config.reconnect?.enabled === false) {
1915
- this.rejectConnect(new Error(reason ?? 'Transport disconnected before connect'));
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) {