@adhdev/session-host-core 1.0.82-rc.25 → 1.0.82-rc.27

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/index.d.mts CHANGED
@@ -478,7 +478,7 @@ declare function createCliChildPriorityPolicy(deps?: PriorityDependencies): <T e
478
478
  * registering listeners/doing other work. There is a spawn-to-setPriority race:
479
479
  * a child can run (and fork) before this call. Later descendants inherit nice
480
480
  * on POSIX; early descendants are not retroactively changed. Windows creation
481
- * and ConPTY inheritance require verification on the MainPC.
481
+ * and ConPTY inheritance require verification on the DesktopA.
482
482
  * Optional darwin-bg is applied asynchronously by taskpolicy after nice; the
483
483
  * extra helper startup adds a further race before the background class takes
484
484
  * effect. It also throttles I/O, so nice alone remains the default.
@@ -662,6 +662,9 @@ declare function createLineParser<TEnvelope = SessionHostWireEnvelope>(onEnvelop
662
662
  interface SessionHostClientOptions {
663
663
  endpoint?: SessionHostEndpoint;
664
664
  appName?: string;
665
+ requestTimeoutMs?: number;
666
+ /** Join the shared client registry (default true). Disable for short-lived probes. */
667
+ registerAsShared?: boolean;
665
668
  }
666
669
  /**
667
670
  * Why an established session-host connection went away.
@@ -670,6 +673,8 @@ interface SessionHostClientOptions {
670
673
  * - `closed` — socket fully closed ('close') without a preceding error/end
671
674
  */
672
675
  type SessionHostDisconnectReason = 'error' | 'ended' | 'closed';
676
+ declare function disconnectSessionHostClients(endpoint?: SessionHostEndpoint, error?: Error): void;
677
+ declare function findConnectedSessionHostClient(endpoint?: SessionHostEndpoint): SessionHostClient | null;
673
678
  interface SessionHostDisconnectInfo {
674
679
  reason: SessionHostDisconnectReason;
675
680
  endpointPath: string;
@@ -690,12 +695,19 @@ interface SessionHostDisconnectInfo {
690
695
  declare class SessionHostClient {
691
696
  readonly endpoint: SessionHostEndpoint;
692
697
  private socket;
698
+ private connectPromise;
699
+ private requestTimeoutMs;
693
700
  private requestWaiters;
694
701
  private eventListeners;
695
702
  private disconnectListeners;
696
703
  /** Guards against emitting both an 'error'-path and a 'close'-path disconnect for one socket. */
697
704
  private disconnectedSockets;
705
+ /** Session subscriptions established via create_session/attach_session, replayed automatically on reconnect. */
706
+ private attachedSessions;
698
707
  constructor(options?: SessionHostClientOptions);
708
+ static disconnectClients(endpoint?: SessionHostEndpoint, error?: Error): void;
709
+ static findConnectedClient(endpoint?: SessionHostEndpoint): SessionHostClient | null;
710
+ isConnected(): boolean;
699
711
  /**
700
712
  * Fires when an established connection to the session host goes away.
701
713
  *
@@ -710,9 +722,13 @@ declare class SessionHostClient {
710
722
  */
711
723
  onDisconnect(listener: (info: SessionHostDisconnectInfo) => void): () => void;
712
724
  private handleDisconnect;
713
- connect(): Promise<void>;
725
+ private sendRawRequestOnSocket;
726
+ connect(timeoutMs?: number): Promise<void>;
714
727
  onEvent(listener: (event: SessionHostEvent) => void): () => void;
715
- request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>>;
728
+ request<T = unknown>(request: SessionHostRequest, options?: {
729
+ timeoutMs?: number;
730
+ }): Promise<SessionHostResponse<T>>;
731
+ disconnect(error?: Error): void;
716
732
  close(): Promise<void>;
717
733
  }
718
734
  declare function createResponseEnvelope(requestId: string, response: SessionHostResponse): SessionHostResponseEnvelope;
@@ -871,4 +887,4 @@ interface SessionHostControlTransport {
871
887
  */
872
888
  declare function createSessionHostControlPlane(transport: SessionHostControlTransport): SessionHostControlPlane;
873
889
 
874
- export { type AcquireWritePayload, type AttachSessionPayload, CLI_CHILD_NICE_ENV, CLI_CHILD_NICE_INCREMENT, CLI_CHILD_SCHEDULING_ENV, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type HostUpgradeLeasePayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSnapshot, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createCliChildPriorityPolicy, createLineParser, createResponseEnvelope, createSessionHostControlPlane, ensureNodePtySpawnHelperPermissions, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, hiddenSpawn as hiddenSpawnProcess, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, replayLaunchEnv, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, spawnCliChild, writeEnvelope };
890
+ export { type AcquireWritePayload, type AttachSessionPayload, CLI_CHILD_NICE_ENV, CLI_CHILD_NICE_INCREMENT, CLI_CHILD_SCHEDULING_ENV, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type HostUpgradeLeasePayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSnapshot, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createCliChildPriorityPolicy, createLineParser, createResponseEnvelope, createSessionHostControlPlane, disconnectSessionHostClients, ensureNodePtySpawnHelperPermissions, findConnectedSessionHostClient, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, hiddenSpawn as hiddenSpawnProcess, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, replayLaunchEnv, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, spawnCliChild, writeEnvelope };
package/dist/index.d.ts CHANGED
@@ -478,7 +478,7 @@ declare function createCliChildPriorityPolicy(deps?: PriorityDependencies): <T e
478
478
  * registering listeners/doing other work. There is a spawn-to-setPriority race:
479
479
  * a child can run (and fork) before this call. Later descendants inherit nice
480
480
  * on POSIX; early descendants are not retroactively changed. Windows creation
481
- * and ConPTY inheritance require verification on the MainPC.
481
+ * and ConPTY inheritance require verification on the DesktopA.
482
482
  * Optional darwin-bg is applied asynchronously by taskpolicy after nice; the
483
483
  * extra helper startup adds a further race before the background class takes
484
484
  * effect. It also throttles I/O, so nice alone remains the default.
@@ -662,6 +662,9 @@ declare function createLineParser<TEnvelope = SessionHostWireEnvelope>(onEnvelop
662
662
  interface SessionHostClientOptions {
663
663
  endpoint?: SessionHostEndpoint;
664
664
  appName?: string;
665
+ requestTimeoutMs?: number;
666
+ /** Join the shared client registry (default true). Disable for short-lived probes. */
667
+ registerAsShared?: boolean;
665
668
  }
666
669
  /**
667
670
  * Why an established session-host connection went away.
@@ -670,6 +673,8 @@ interface SessionHostClientOptions {
670
673
  * - `closed` — socket fully closed ('close') without a preceding error/end
671
674
  */
672
675
  type SessionHostDisconnectReason = 'error' | 'ended' | 'closed';
676
+ declare function disconnectSessionHostClients(endpoint?: SessionHostEndpoint, error?: Error): void;
677
+ declare function findConnectedSessionHostClient(endpoint?: SessionHostEndpoint): SessionHostClient | null;
673
678
  interface SessionHostDisconnectInfo {
674
679
  reason: SessionHostDisconnectReason;
675
680
  endpointPath: string;
@@ -690,12 +695,19 @@ interface SessionHostDisconnectInfo {
690
695
  declare class SessionHostClient {
691
696
  readonly endpoint: SessionHostEndpoint;
692
697
  private socket;
698
+ private connectPromise;
699
+ private requestTimeoutMs;
693
700
  private requestWaiters;
694
701
  private eventListeners;
695
702
  private disconnectListeners;
696
703
  /** Guards against emitting both an 'error'-path and a 'close'-path disconnect for one socket. */
697
704
  private disconnectedSockets;
705
+ /** Session subscriptions established via create_session/attach_session, replayed automatically on reconnect. */
706
+ private attachedSessions;
698
707
  constructor(options?: SessionHostClientOptions);
708
+ static disconnectClients(endpoint?: SessionHostEndpoint, error?: Error): void;
709
+ static findConnectedClient(endpoint?: SessionHostEndpoint): SessionHostClient | null;
710
+ isConnected(): boolean;
699
711
  /**
700
712
  * Fires when an established connection to the session host goes away.
701
713
  *
@@ -710,9 +722,13 @@ declare class SessionHostClient {
710
722
  */
711
723
  onDisconnect(listener: (info: SessionHostDisconnectInfo) => void): () => void;
712
724
  private handleDisconnect;
713
- connect(): Promise<void>;
725
+ private sendRawRequestOnSocket;
726
+ connect(timeoutMs?: number): Promise<void>;
714
727
  onEvent(listener: (event: SessionHostEvent) => void): () => void;
715
- request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>>;
728
+ request<T = unknown>(request: SessionHostRequest, options?: {
729
+ timeoutMs?: number;
730
+ }): Promise<SessionHostResponse<T>>;
731
+ disconnect(error?: Error): void;
716
732
  close(): Promise<void>;
717
733
  }
718
734
  declare function createResponseEnvelope(requestId: string, response: SessionHostResponse): SessionHostResponseEnvelope;
@@ -871,4 +887,4 @@ interface SessionHostControlTransport {
871
887
  */
872
888
  declare function createSessionHostControlPlane(transport: SessionHostControlTransport): SessionHostControlPlane;
873
889
 
874
- export { type AcquireWritePayload, type AttachSessionPayload, CLI_CHILD_NICE_ENV, CLI_CHILD_NICE_INCREMENT, CLI_CHILD_SCHEDULING_ENV, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type HostUpgradeLeasePayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSnapshot, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createCliChildPriorityPolicy, createLineParser, createResponseEnvelope, createSessionHostControlPlane, ensureNodePtySpawnHelperPermissions, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, hiddenSpawn as hiddenSpawnProcess, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, replayLaunchEnv, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, spawnCliChild, writeEnvelope };
890
+ export { type AcquireWritePayload, type AttachSessionPayload, CLI_CHILD_NICE_ENV, CLI_CHILD_NICE_INCREMENT, CLI_CHILD_SCHEDULING_ENV, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type HostUpgradeLeasePayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSnapshot, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createCliChildPriorityPolicy, createLineParser, createResponseEnvelope, createSessionHostControlPlane, disconnectSessionHostClients, ensureNodePtySpawnHelperPermissions, findConnectedSessionHostClient, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, hiddenSpawn as hiddenSpawnProcess, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, replayLaunchEnv, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, spawnCliChild, writeEnvelope };
package/dist/index.js CHANGED
@@ -50,7 +50,9 @@ __export(index_exports, {
50
50
  createLineParser: () => createLineParser,
51
51
  createResponseEnvelope: () => createResponseEnvelope,
52
52
  createSessionHostControlPlane: () => createSessionHostControlPlane,
53
+ disconnectSessionHostClients: () => disconnectSessionHostClients,
53
54
  ensureNodePtySpawnHelperPermissions: () => ensureNodePtySpawnHelperPermissions,
55
+ findConnectedSessionHostClient: () => findConnectedSessionHostClient,
54
56
  formatRuntimeOwner: () => formatRuntimeOwner,
55
57
  getDefaultSessionHostEndpoint: () => getDefaultSessionHostEndpoint,
56
58
  getSessionHostRecoveryLabel: () => getSessionHostRecoveryLabel,
@@ -804,16 +806,59 @@ function createLineParser(onEnvelope) {
804
806
  return parser;
805
807
  }
806
808
  var establishedSockets = /* @__PURE__ */ new WeakSet();
809
+ var activeClientRefs = /* @__PURE__ */ new Set();
810
+ function disconnectSessionHostClients(endpoint, error) {
811
+ for (const ref of activeClientRefs) {
812
+ const client = ref.deref();
813
+ if (!client) {
814
+ activeClientRefs.delete(ref);
815
+ continue;
816
+ }
817
+ if (!endpoint || client.endpoint.path === endpoint.path) {
818
+ client.disconnect(error);
819
+ }
820
+ }
821
+ }
822
+ function findConnectedSessionHostClient(endpoint) {
823
+ for (const ref of activeClientRefs) {
824
+ const client = ref.deref();
825
+ if (!client) {
826
+ activeClientRefs.delete(ref);
827
+ continue;
828
+ }
829
+ if ((!endpoint || client.endpoint.path === endpoint.path) && client.isConnected()) {
830
+ return client;
831
+ }
832
+ }
833
+ return null;
834
+ }
807
835
  var SessionHostClient = class {
808
836
  endpoint;
809
837
  socket = null;
838
+ connectPromise = null;
839
+ requestTimeoutMs = 3e4;
810
840
  requestWaiters = /* @__PURE__ */ new Map();
811
841
  eventListeners = /* @__PURE__ */ new Set();
812
842
  disconnectListeners = /* @__PURE__ */ new Set();
813
843
  /** Guards against emitting both an 'error'-path and a 'close'-path disconnect for one socket. */
814
844
  disconnectedSockets = /* @__PURE__ */ new WeakSet();
845
+ /** Session subscriptions established via create_session/attach_session, replayed automatically on reconnect. */
846
+ attachedSessions = /* @__PURE__ */ new Map();
815
847
  constructor(options = {}) {
816
848
  this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
849
+ if (typeof options.requestTimeoutMs === "number" && options.requestTimeoutMs > 0) {
850
+ this.requestTimeoutMs = options.requestTimeoutMs;
851
+ }
852
+ if (options.registerAsShared !== false) activeClientRefs.add(new WeakRef(this));
853
+ }
854
+ static disconnectClients(endpoint, error) {
855
+ disconnectSessionHostClients(endpoint, error);
856
+ }
857
+ static findConnectedClient(endpoint) {
858
+ return findConnectedSessionHostClient(endpoint);
859
+ }
860
+ isConnected() {
861
+ return Boolean(this.socket && !this.socket.destroyed && establishedSockets.has(this.socket));
817
862
  }
818
863
  /**
819
864
  * Fires when an established connection to the session host goes away.
@@ -856,6 +901,7 @@ var SessionHostClient = class {
856
901
  this.requestWaiters.clear();
857
902
  if (this.socket === socket) {
858
903
  this.socket = null;
904
+ this.connectPromise = null;
859
905
  }
860
906
  try {
861
907
  socket.destroy();
@@ -875,8 +921,47 @@ var SessionHostClient = class {
875
921
  }
876
922
  }
877
923
  }
878
- async connect() {
879
- if (this.socket && !this.socket.destroyed) return;
924
+ sendRawRequestOnSocket(socket, request, timeoutMs = this.requestTimeoutMs) {
925
+ const requestId = generateUUID();
926
+ const envelope = {
927
+ kind: "request",
928
+ requestId,
929
+ request
930
+ };
931
+ const timeoutLabel = timeoutMs % 1e3 === 0 ? `${timeoutMs / 1e3}s` : `${timeoutMs}ms`;
932
+ return new Promise((resolve3, reject) => {
933
+ const timeout = setTimeout(() => {
934
+ const timeoutError = Object.assign(new Error(`Session host request timed out after ${timeoutLabel} (${request.type})`), { code: "SESSION_HOST_REQUEST_TIMEOUT", requestType: request.type });
935
+ if (this.socket === socket) {
936
+ this.disconnect(timeoutError);
937
+ } else {
938
+ this.requestWaiters.delete(requestId);
939
+ this.disconnectedSockets.add(socket);
940
+ try {
941
+ socket.destroy();
942
+ } catch {
943
+ }
944
+ reject(timeoutError);
945
+ }
946
+ }, timeoutMs);
947
+ this.requestWaiters.set(requestId, {
948
+ resolve: (value) => {
949
+ clearTimeout(timeout);
950
+ resolve3(value);
951
+ },
952
+ reject: (error) => {
953
+ clearTimeout(timeout);
954
+ reject(error);
955
+ }
956
+ });
957
+ socket.write(serializeEnvelope(envelope));
958
+ });
959
+ }
960
+ async connect(timeoutMs = this.requestTimeoutMs) {
961
+ if (this.socket && !this.socket.destroyed) {
962
+ if (this.connectPromise) await this.connectPromise;
963
+ return;
964
+ }
880
965
  if (this.socket) {
881
966
  try {
882
967
  this.socket.destroy();
@@ -884,6 +969,7 @@ var SessionHostClient = class {
884
969
  }
885
970
  this.socket = null;
886
971
  }
972
+ this.connectPromise = null;
887
973
  const socket = net.createConnection(this.endpoint.path);
888
974
  this.socket = socket;
889
975
  const parser = createLineParser((envelope) => {
@@ -917,13 +1003,54 @@ var SessionHostClient = class {
917
1003
  socket.on("end", () => {
918
1004
  this.handleDisconnect(socket, "ended", void 0, flushParser().length);
919
1005
  });
920
- await new Promise((resolve3, reject) => {
921
- socket.once("connect", () => {
922
- establishedSockets.add(socket);
923
- resolve3();
1006
+ const connectTask = (async () => {
1007
+ await new Promise((resolve3, reject) => {
1008
+ const cleanup = () => {
1009
+ clearTimeout(timer);
1010
+ socket.off("connect", connected);
1011
+ socket.off("error", failed);
1012
+ socket.off("close", closed);
1013
+ };
1014
+ const connected = () => {
1015
+ cleanup();
1016
+ establishedSockets.add(socket);
1017
+ resolve3();
1018
+ };
1019
+ const failed = (error) => {
1020
+ cleanup();
1021
+ reject(error);
1022
+ };
1023
+ const closed = () => failed(new Error("Session host connection closed before connect"));
1024
+ const timer = setTimeout(() => {
1025
+ const error = Object.assign(new Error(`Session host connect timed out after ${timeoutMs}ms`), { code: "SESSION_HOST_CONNECT_TIMEOUT" });
1026
+ failed(error);
1027
+ if (this.socket === socket) this.disconnect(error);
1028
+ }, timeoutMs);
1029
+ socket.once("connect", connected);
1030
+ socket.once("error", failed);
1031
+ socket.once("close", closed);
924
1032
  });
925
- socket.once("error", reject);
926
- });
1033
+ if (this.attachedSessions.size > 0 && this.socket === socket && !socket.destroyed) {
1034
+ for (const [sessionId, sub] of [...this.attachedSessions.entries()]) {
1035
+ if (this.socket !== socket || socket.destroyed) break;
1036
+ const replayResponse = await this.sendRawRequestOnSocket(socket, {
1037
+ type: "attach_session",
1038
+ payload: sub
1039
+ }).catch(() => null);
1040
+ if (replayResponse && !replayResponse.success) {
1041
+ this.attachedSessions.delete(sessionId);
1042
+ }
1043
+ }
1044
+ }
1045
+ })();
1046
+ this.connectPromise = connectTask;
1047
+ try {
1048
+ await connectTask;
1049
+ } finally {
1050
+ if (this.connectPromise === connectTask) {
1051
+ this.connectPromise = null;
1052
+ }
1053
+ }
927
1054
  }
928
1055
  onEvent(listener) {
929
1056
  this.eventListeners.add(listener);
@@ -931,35 +1058,73 @@ var SessionHostClient = class {
931
1058
  this.eventListeners.delete(listener);
932
1059
  };
933
1060
  }
934
- async request(request) {
935
- await this.connect();
936
- if (!this.socket) throw new Error("Session host socket unavailable");
937
- const requestId = generateUUID();
938
- const envelope = {
939
- kind: "request",
940
- requestId,
941
- request
942
- };
943
- const response = await new Promise((resolve3, reject) => {
944
- const timeout = setTimeout(() => {
945
- this.requestWaiters.delete(requestId);
946
- reject(new Error(`Session host request timed out after 30s (${request.type})`));
947
- }, 3e4);
948
- this.requestWaiters.set(requestId, {
949
- resolve: (value) => {
950
- clearTimeout(timeout);
951
- resolve3(value);
952
- },
953
- reject: (error) => {
954
- clearTimeout(timeout);
955
- reject(error);
1061
+ async request(request, options = {}) {
1062
+ const timeoutMs = Math.max(1, options.timeoutMs ?? this.requestTimeoutMs);
1063
+ const deadline = Date.now() + timeoutMs;
1064
+ const alreadyConnected = this.isConnected() && !this.connectPromise;
1065
+ if (options.timeoutMs !== void 0) {
1066
+ let timer;
1067
+ try {
1068
+ await Promise.race([
1069
+ this.connect(timeoutMs),
1070
+ new Promise((_resolve, reject) => {
1071
+ timer = setTimeout(() => {
1072
+ const error = Object.assign(new Error(`Session host connect timed out after ${timeoutMs}ms`), { code: "SESSION_HOST_CONNECT_TIMEOUT" });
1073
+ reject(error);
1074
+ this.disconnect(error);
1075
+ }, timeoutMs);
1076
+ })
1077
+ ]);
1078
+ } finally {
1079
+ if (timer) clearTimeout(timer);
1080
+ }
1081
+ } else {
1082
+ await this.connect(timeoutMs);
1083
+ }
1084
+ const requestSocket = this.socket;
1085
+ if (!requestSocket) throw new Error("Session host socket unavailable");
1086
+ const response = await this.sendRawRequestOnSocket(requestSocket, request, options.timeoutMs === void 0 || alreadyConnected ? timeoutMs : Math.max(1, deadline - Date.now()));
1087
+ if (response.success) {
1088
+ if (request.type === "attach_session") {
1089
+ this.attachedSessions.set(request.payload.sessionId, {
1090
+ sessionId: request.payload.sessionId,
1091
+ clientId: request.payload.clientId,
1092
+ clientType: request.payload.clientType,
1093
+ readOnly: request.payload.readOnly === true
1094
+ });
1095
+ } else if (request.type === "create_session") {
1096
+ const createdId = response.result?.sessionId || request.payload.sessionId;
1097
+ const clientId = typeof request.payload.clientId === "string" ? request.payload.clientId.trim() : "";
1098
+ if (createdId && clientId) {
1099
+ this.attachedSessions.set(createdId, {
1100
+ sessionId: createdId,
1101
+ clientId,
1102
+ clientType: request.payload.clientType || "daemon",
1103
+ readOnly: false
1104
+ });
956
1105
  }
957
- });
958
- this.socket?.write(serializeEnvelope(envelope));
959
- });
1106
+ } else if (request.type === "detach_session" || request.type === "delete_session") {
1107
+ this.attachedSessions.delete(request.payload.sessionId);
1108
+ }
1109
+ }
960
1110
  return response;
961
1111
  }
1112
+ disconnect(error) {
1113
+ const socket = this.socket;
1114
+ if (socket) {
1115
+ this.handleDisconnect(socket, error ? "error" : "closed", error, 0);
1116
+ return;
1117
+ }
1118
+ this.connectPromise = null;
1119
+ const failure = error || new Error(`Session host client disconnected (${this.endpoint.path})`);
1120
+ for (const waiter of this.requestWaiters.values()) {
1121
+ waiter.reject(failure);
1122
+ }
1123
+ this.requestWaiters.clear();
1124
+ }
962
1125
  async close() {
1126
+ this.attachedSessions.clear();
1127
+ this.connectPromise = null;
963
1128
  if (!this.socket) return;
964
1129
  const socket = this.socket;
965
1130
  this.socket = null;
@@ -1177,7 +1342,9 @@ function createSessionHostControlPlane(transport) {
1177
1342
  createLineParser,
1178
1343
  createResponseEnvelope,
1179
1344
  createSessionHostControlPlane,
1345
+ disconnectSessionHostClients,
1180
1346
  ensureNodePtySpawnHelperPermissions,
1347
+ findConnectedSessionHostClient,
1181
1348
  formatRuntimeOwner,
1182
1349
  getDefaultSessionHostEndpoint,
1183
1350
  getSessionHostRecoveryLabel,