@adhdev/session-host-core 1.0.82-rc.26 → 1.0.82-rc.29

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/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export type {
2
+ MeshWriterAssignment,
3
+ MeshWriterStopTarget,
4
+ StopMeshWriterPayload,
2
5
  HostUpgradeLeasePayload,
3
6
  AcquireWritePayload,
4
7
  AttachSessionPayload,
@@ -77,6 +80,7 @@ export {
77
80
  createLineParser,
78
81
  createResponseEnvelope,
79
82
  disconnectSessionHostClients,
83
+ findConnectedSessionHostClient,
80
84
  getDefaultSessionHostEndpoint,
81
85
  writeEnvelope,
82
86
  } from './ipc.js';
package/src/ipc.ts CHANGED
@@ -124,6 +124,8 @@ export interface SessionHostClientOptions {
124
124
  endpoint?: SessionHostEndpoint;
125
125
  appName?: string;
126
126
  requestTimeoutMs?: number;
127
+ /** Join the shared client registry (default true). Disable for short-lived probes. */
128
+ registerAsShared?: boolean;
127
129
  }
128
130
 
129
131
  /**
@@ -155,6 +157,20 @@ export function disconnectSessionHostClients(endpoint?: SessionHostEndpoint, err
155
157
  }
156
158
  }
157
159
 
160
+ export function findConnectedSessionHostClient(endpoint?: SessionHostEndpoint): SessionHostClient | null {
161
+ for (const ref of activeClientRefs) {
162
+ const client = ref.deref();
163
+ if (!client) {
164
+ activeClientRefs.delete(ref);
165
+ continue;
166
+ }
167
+ if ((!endpoint || client.endpoint.path === endpoint.path) && client.isConnected()) {
168
+ return client;
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
158
174
  export interface SessionHostDisconnectInfo {
159
175
  reason: SessionHostDisconnectReason;
160
176
  endpointPath: string;
@@ -192,13 +208,21 @@ export class SessionHostClient {
192
208
  if (typeof options.requestTimeoutMs === 'number' && options.requestTimeoutMs > 0) {
193
209
  this.requestTimeoutMs = options.requestTimeoutMs;
194
210
  }
195
- activeClientRefs.add(new WeakRef(this));
211
+ if (options.registerAsShared !== false) activeClientRefs.add(new WeakRef(this));
196
212
  }
197
213
 
198
214
  static disconnectClients(endpoint?: SessionHostEndpoint, error?: Error): void {
199
215
  disconnectSessionHostClients(endpoint, error);
200
216
  }
201
217
 
218
+ static findConnectedClient(endpoint?: SessionHostEndpoint): SessionHostClient | null {
219
+ return findConnectedSessionHostClient(endpoint);
220
+ }
221
+
222
+ isConnected(): boolean {
223
+ return Boolean(this.socket && !this.socket.destroyed && establishedSockets.has(this.socket));
224
+ }
225
+
202
226
  /**
203
227
  * Fires when an established connection to the session host goes away.
204
228
  *
@@ -278,6 +302,7 @@ export class SessionHostClient {
278
302
  private sendRawRequestOnSocket<T = unknown>(
279
303
  socket: net.Socket,
280
304
  request: SessionHostRequest,
305
+ timeoutMs = this.requestTimeoutMs,
281
306
  ): Promise<SessionHostResponse<T>> {
282
307
  const requestId = generateUUID();
283
308
  const envelope: SessionHostRequestEnvelope = {
@@ -285,11 +310,25 @@ export class SessionHostClient {
285
310
  requestId,
286
311
  request,
287
312
  };
288
- const timeoutMs = this.requestTimeoutMs > 0 ? this.requestTimeoutMs : 30_000;
289
313
  const timeoutLabel = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}s` : `${timeoutMs}ms`;
314
+ const startedAt = Date.now();
315
+ const startedMonotonicAt = performance.now();
290
316
  return new Promise<SessionHostResponse<T>>((resolve, reject) => {
291
317
  const timeout = setTimeout(() => {
292
- const timeoutError = new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`);
318
+ const actualElapsedMs = Math.max(0, Math.round(performance.now() - startedMonotonicAt));
319
+ const wallElapsedMs = Math.max(0, Date.now() - startedAt);
320
+ const observerLagMs = Math.max(0, actualElapsedMs - timeoutMs);
321
+ const timeoutError = Object.assign(
322
+ new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`),
323
+ {
324
+ code: 'SESSION_HOST_REQUEST_TIMEOUT',
325
+ requestType: request.type,
326
+ scheduledTimeoutMs: timeoutMs,
327
+ actualElapsedMs,
328
+ wallElapsedMs,
329
+ observerLagMs,
330
+ },
331
+ );
293
332
  if (this.socket === socket) {
294
333
  this.disconnect(timeoutError);
295
334
  } else {
@@ -307,7 +346,7 @@ export class SessionHostClient {
307
346
  });
308
347
  }
309
348
 
310
- async connect(): Promise<void> {
349
+ async connect(timeoutMs = this.requestTimeoutMs): Promise<void> {
311
350
  if (this.socket && !this.socket.destroyed) {
312
351
  if (this.connectPromise) await this.connectPromise;
313
352
  return;
@@ -381,11 +420,37 @@ export class SessionHostClient {
381
420
 
382
421
  const connectTask = (async () => {
383
422
  await new Promise<void>((resolve, reject) => {
384
- socket.once('connect', () => {
385
- establishedSockets.add(socket);
386
- resolve();
387
- });
388
- socket.once('error', reject);
423
+ const cleanup = () => {
424
+ clearTimeout(timer);
425
+ socket.off('connect', connected);
426
+ socket.off('error', failed);
427
+ socket.off('close', closed);
428
+ };
429
+ const connected = () => { cleanup(); establishedSockets.add(socket); resolve(); };
430
+ const failed = (error: Error) => { cleanup(); reject(error); };
431
+ const closed = () => failed(new Error('Session host connection closed before connect'));
432
+ const connectStartedAt = Date.now();
433
+ const connectStartedMonotonicAt = performance.now();
434
+ const timer = setTimeout(() => {
435
+ const actualElapsedMs = Math.max(0, Math.round(performance.now() - connectStartedMonotonicAt));
436
+ const wallElapsedMs = Math.max(0, Date.now() - connectStartedAt);
437
+ const observerLagMs = Math.max(0, actualElapsedMs - timeoutMs);
438
+ const error = Object.assign(
439
+ new Error(`Session host connect timed out after ${timeoutMs}ms`),
440
+ {
441
+ code: 'SESSION_HOST_CONNECT_TIMEOUT',
442
+ scheduledTimeoutMs: timeoutMs,
443
+ actualElapsedMs,
444
+ wallElapsedMs,
445
+ observerLagMs,
446
+ },
447
+ );
448
+ failed(error);
449
+ if (this.socket === socket) this.disconnect(error);
450
+ }, timeoutMs);
451
+ socket.once('connect', connected);
452
+ socket.once('error', failed);
453
+ socket.once('close', closed);
389
454
  });
390
455
  if (this.attachedSessions.size > 0 && this.socket === socket && !socket.destroyed) {
391
456
  for (const [sessionId, sub] of [...this.attachedSessions.entries()]) {
@@ -418,12 +483,47 @@ export class SessionHostClient {
418
483
  };
419
484
  }
420
485
 
421
- async request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>> {
422
- await this.connect();
486
+ async request<T = unknown>(request: SessionHostRequest, options: { timeoutMs?: number } = {}): Promise<SessionHostResponse<T>> {
487
+ const timeoutMs = Math.max(1, options.timeoutMs ?? this.requestTimeoutMs);
488
+ const deadline = Date.now() + timeoutMs;
489
+ const alreadyConnected = this.isConnected() && !this.connectPromise;
490
+ // A probe must also bound a connect/re-attach already in flight on this
491
+ // client; that task may have started with the normal 30s timeout.
492
+ if (options.timeoutMs !== undefined) {
493
+ let timer: ReturnType<typeof setTimeout> | undefined;
494
+ const raceStartedAt = Date.now();
495
+ const raceStartedMonotonicAt = performance.now();
496
+ try {
497
+ await Promise.race([
498
+ this.connect(timeoutMs),
499
+ new Promise<never>((_resolve, reject) => {
500
+ timer = setTimeout(() => {
501
+ const actualElapsedMs = Math.max(0, Math.round(performance.now() - raceStartedMonotonicAt));
502
+ const wallElapsedMs = Math.max(0, Date.now() - raceStartedAt);
503
+ const observerLagMs = Math.max(0, actualElapsedMs - timeoutMs);
504
+ const error = Object.assign(
505
+ new Error(`Session host connect timed out after ${timeoutMs}ms`),
506
+ {
507
+ code: 'SESSION_HOST_CONNECT_TIMEOUT',
508
+ scheduledTimeoutMs: timeoutMs,
509
+ actualElapsedMs,
510
+ wallElapsedMs,
511
+ observerLagMs,
512
+ },
513
+ );
514
+ reject(error);
515
+ this.disconnect(error);
516
+ }, timeoutMs);
517
+ }),
518
+ ]);
519
+ } finally { if (timer) clearTimeout(timer); }
520
+ } else {
521
+ await this.connect(timeoutMs);
522
+ }
423
523
  const requestSocket = this.socket;
424
524
  if (!requestSocket) throw new Error('Session host socket unavailable');
425
525
 
426
- const response = await this.sendRawRequestOnSocket<T>(requestSocket, request);
526
+ const response = await this.sendRawRequestOnSocket<T>(requestSocket, request, options.timeoutMs === undefined || alreadyConnected ? timeoutMs : Math.max(1, deadline - Date.now()));
427
527
  if (response.success) {
428
528
  if (request.type === 'attach_session') {
429
529
  this.attachedSessions.set(request.payload.sessionId, {
package/src/types.ts CHANGED
@@ -157,6 +157,15 @@ export interface ResizeSessionPayload {
157
157
  export interface StopSessionPayload {
158
158
  sessionId: string;
159
159
  }
160
+ export interface MeshWriterAssignment {
161
+ meshId: string; nodeId: string; taskId: string; attemptId: string;
162
+ generation: number; dispatchNonce: number; ownerDaemonId: string;
163
+ }
164
+ export interface MeshWriterStopTarget {
165
+ runtimeKey: string; startedAt: number; osPid: number; providerType: string;
166
+ assignment: MeshWriterAssignment;
167
+ }
168
+ export interface StopMeshWriterPayload extends MeshWriterStopTarget { sessionId: string; clientId: string }
160
169
 
161
170
  export interface DeleteSessionPayload {
162
171
  sessionId: string;
@@ -317,6 +326,7 @@ export type SessionHostRequest =
317
326
  | { type: 'send_input'; payload: SendInputPayload }
318
327
  | { type: 'resize_session'; payload: ResizeSessionPayload }
319
328
  | { type: 'stop_session'; payload: StopSessionPayload }
329
+ | { type: 'stop_mesh_writer'; payload: StopMeshWriterPayload }
320
330
  | { type: 'delete_session'; payload: DeleteSessionPayload }
321
331
  | { type: 'resume_session'; payload: ResumeSessionPayload }
322
332
  | { type: 'acquire_write'; payload: AcquireWritePayload }
@@ -344,6 +354,7 @@ export const SESSION_HOST_SUPPORTED_REQUEST_TYPES: readonly SessionHostRequestTy
344
354
  'send_input',
345
355
  'resize_session',
346
356
  'stop_session',
357
+ 'stop_mesh_writer',
347
358
  'delete_session',
348
359
  'resume_session',
349
360
  'acquire_write',