@github/copilot-sdk 1.0.15 → 1.0.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/README.md CHANGED
@@ -37,6 +37,139 @@ verify all eight platforms.
37
37
  npm install @github/copilot-sdk
38
38
  ```
39
39
 
40
+ ## Runtime-supervised AHP host (experimental)
41
+
42
+ `startAhpHost()` exposes copilotd's complete Agent Host Protocol server through the
43
+ same runtime used by the SDK:
44
+
45
+ ```typescript
46
+ import { CopilotClient } from "@github/copilot-sdk";
47
+
48
+ await using client = new CopilotClient();
49
+ await client.start();
50
+ const host = await client.startAhpHost({
51
+ localServer: {},
52
+ onExit: (exit) => {
53
+ if (exit.error) console.error(exit.error);
54
+ },
55
+ });
56
+
57
+ // Connect an AHP client using host.url and host.token.
58
+ // Treat host.token as a secret; do not log it.
59
+
60
+ // Existing SDK sessions and AHP sessions share this runtime.
61
+ // Keep client alive while the listener is needed. Leaving this scope disposes
62
+ // client, and the runtime stops its listener without a separate host.dispose().
63
+ ```
64
+
65
+ The runtime hosts the complete AHP server in-process; the SDK does not launch a
66
+ second runtime or relay the host's traffic. The host has its own SDK connection
67
+ and belongs to the client connection that started it. Explicit disposal,
68
+ connection loss, and runtime shutdown stop the listener and its hosting task
69
+ without deleting underlying sessions. Reconnecting does not reclaim a host.
70
+ The optional `onExit` callback reports exits at most once. If the owner connection
71
+ is lost, it reports that loss rather than claiming that listener cleanup was
72
+ acknowledged.
73
+
74
+ `host.pid` is absent for in-process listeners. The optional field is retained
75
+ for separate host process IDs returned by legacy runtimes, never the runtime PID.
76
+ Use `dispose()` to stop the listener. `reason: "exited"` reports hosting-task
77
+ failure, not runtime process death, and `exitCode` is absent. Hosting no longer
78
+ provides process isolation from the runtime.
79
+
80
+ Select at least one transport explicitly: `localServer: {}` enables the local
81
+ listener, `githubEnvironment: { name: "My app", computeId: "stable-installation-id" }`
82
+ registers a Mission Control environment and enables remote WPS connections, and
83
+ both enables both transports. GitHub-only hosting opens no local listener;
84
+ `host.url` and `host.token` are undefined, while `host.environmentId` identifies
85
+ the environment. The application supplies a stable compute ID and configures
86
+ transports only at startup. Dispose and recreate the host to change them.
87
+
88
+ The runtime validates options inside `localServer` and applies their defaults:
89
+
90
+ - `hostname` defaults to `127.0.0.1`. Set it explicitly to request a non-loopback
91
+ listener, such as `hostname: "0.0.0.0"`, and restrict network access appropriately.
92
+ - `port` defaults to `0`, which selects an available port.
93
+ - `requireConnectionToken` defaults to `true`. The runtime generates a random
94
+ token unless you supply a nonempty `token`.
95
+ - Set `requireConnectionToken: false` to disable token authentication;
96
+ `host.token` is then undefined. A supplied `token` cannot be combined with
97
+ `requireConnectionToken: false`.
98
+
99
+ The listener follows the owning client's lifetime. Call `await host.dispose()`
100
+ only when you want to stop it earlier; `await using host` also supports a shorter
101
+ scope. Each disposal call forwards to the runtime, which owns idempotent cleanup.
102
+ The AHP transport remains owned by the host.
103
+
104
+ ### Application-owned sessions
105
+
106
+ Supply `createSession` to materialize fresh AHP sessions in your application:
107
+
108
+ ```typescript
109
+ import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk";
110
+
111
+ await using client = new CopilotClient();
112
+ await using host = await client.startAhpHost({
113
+ localServer: {},
114
+ createSession: ({ config, signal }) => {
115
+ signal.throwIfAborted();
116
+ return client.createSession({
117
+ ...config,
118
+ onPermissionRequest: approveAll,
119
+ systemMessage: { mode: "append", content: "Use the app's greeting tool." },
120
+ tools: [defineTool("greeting", {
121
+ description: "Get the application's greeting",
122
+ parameters: { type: "object", properties: {} },
123
+ handler: () => "Hello from the application!",
124
+ })],
125
+ });
126
+ },
127
+ onSessionReleased: async (originalSession) => {
128
+ // Optional: the app decides whether to disconnect, keep, or destroy it.
129
+ await originalSession.disconnect();
130
+ },
131
+ });
132
+ ```
133
+
134
+ Preserve the supplied `config`, including its fresh session identity, workspace,
135
+ and selected host settings. Add your prompt and tools where the host has not
136
+ explicitly selected those settings; conflicting settings fail rather than
137
+ silently advertising configuration that was not applied. Return a normal session
138
+ created by this same client. The creation factory does not adopt an arbitrary existing
139
+ session or expose unrelated application sessions in the AHP catalog.
140
+
141
+ Only the session ID returns through ordinary SDK RPC. The host attaches to **that same
142
+ resident session**, adding its own callback/tool registrations without replacing
143
+ the application's prompt, tool filters, hooks, or tools. Application tool functions
144
+ continue running in the app while their results stream through the existing AHP
145
+ projector. No second application connection or function serialization is involved.
146
+
147
+ The SDK retains the original returned object until participation ends and invokes
148
+ `onSessionReleased` at most once per handoff, including attach failure, hosting-task exit,
149
+ and owner disconnection. The SDK never automatically disconnects or destroys the
150
+ app object. The creation callback receives an abort signal; materialization
151
+ is bounded to 30 seconds and cancellation also releases objects returned late.
152
+ Graceful host disposal waits for AHP detach before reporting release. Omitting
153
+ `createSession` preserves copilotd-owned creation.
154
+
155
+ To restore durable application-owned sessions, also supply `resumeSession`.
156
+ It receives `{ sessionId, config, signal }` (`AhpSessionResumeRequest`).
157
+ Return the object from this client's `resumeSession(sessionId, { ...config,
158
+ onPermissionRequest, ... })`, restoring your tools, hooks, and handlers.
159
+ Alternatively, return a retained original session from this client when it
160
+ still matches the requested identity and workspace.
161
+ Only catalog entries marked as application-owned invoke this callback.
162
+ If the callback is missing, restoring such an entry fails instead of falling
163
+ back to host-owned creation.
164
+ Published resident sessions attach directly, without invoking it or replacing
165
+ their current registrations. Resumed sessions follow the same original-object
166
+ retention, cancellation, late-result release, and `onSessionReleased` rules.
167
+
168
+ The `copilotd-hosting` library runs inside the runtime provider.
169
+ Development integrations require a
170
+ source-built launcher and provider (`COPILOT_RUNTIME_PROVIDER_LIB`). Older
171
+ runtimes without these RPC operations cannot start a host.
172
+
40
173
  ## Run the Sample
41
174
 
42
175
  Try the interactive chat sample (from the repo root):
@@ -188,6 +321,9 @@ Start the CLI server and establish connection.
188
321
  ##### `stop(): Promise<Error[]>`
189
322
 
190
323
  Stop the server and close all sessions. Returns a list of any errors encountered during cleanup.
324
+ For an owned stdio runtime, closes stdin and waits up to 10 seconds for host cleanup
325
+ (including telemetry export) and process exit before falling back to termination.
326
+ This graceful-exit timeout is separate from the shutdown RPC and post-termination wait.
191
327
 
192
328
  ##### `forceStop(): Promise<void>`
193
329
 
@@ -22,7 +22,7 @@ __export(cliVersion_exports, {
22
22
  COPILOT_CLI_VERSION: () => COPILOT_CLI_VERSION
23
23
  });
24
24
  module.exports = __toCommonJS(cliVersion_exports);
25
- const COPILOT_CLI_VERSION = "1.0.89";
25
+ const COPILOT_CLI_VERSION = "1.0.90";
26
26
  const COPILOT_CLI_USE_NPM_PACKAGE = false;
27
27
  // Annotate the CommonJS export names for ESM import in node:
28
28
  0 && (module.exports = {
@@ -40,6 +40,7 @@ var import_node = require("vscode-jsonrpc/node.js");
40
40
  var import_rpc = require("./generated/rpc.js");
41
41
  var import_sdkProtocolVersion = require("./sdkProtocolVersion.js");
42
42
  var import_session = require("./session.js");
43
+ var import_host = require("./host.js");
43
44
  var import_runtimeArtifacts = require("./runtimeArtifacts.js");
44
45
  var import_cliVersion = require("./cliVersion.js");
45
46
  var import_sessionFsProvider = require("./sessionFsProvider.js");
@@ -51,6 +52,57 @@ var import_toolSet = require("./toolSet.js");
51
52
  var import_types = require("./types.js");
52
53
  const MIN_PROTOCOL_VERSION = 3;
53
54
  const RUNTIME_SHUTDOWN_TIMEOUT_MS = 1e4;
55
+ function createMessageConnection(reader, writer) {
56
+ let dispatch;
57
+ let finishDrain;
58
+ let drained;
59
+ let disposed = false;
60
+ const barrier = { jsonrpc: "2.0", method: "$/sdkDrain" };
61
+ const connection = (0, import_node.createMessageConnection)(
62
+ {
63
+ onError: reader.onError,
64
+ onClose: reader.onClose,
65
+ onPartialMessage: reader.onPartialMessage,
66
+ listen: (callback) => {
67
+ dispatch = callback;
68
+ return reader.listen(callback);
69
+ },
70
+ dispose: () => reader.dispose()
71
+ },
72
+ writer,
73
+ void 0,
74
+ {
75
+ messageStrategy: {
76
+ handleMessage: (message, next) => {
77
+ if (message === barrier) {
78
+ finishDrain?.();
79
+ } else {
80
+ next(message);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ );
86
+ connection.onDispose(() => {
87
+ disposed = true;
88
+ finishDrain?.();
89
+ });
90
+ return Object.assign(connection, {
91
+ drain: () => {
92
+ if (!drained) {
93
+ drained = new Promise((resolve2) => {
94
+ finishDrain = resolve2;
95
+ if (disposed || !dispatch) {
96
+ resolve2();
97
+ } else {
98
+ dispatch(barrier);
99
+ }
100
+ });
101
+ }
102
+ return drained;
103
+ }
104
+ });
105
+ }
54
106
  async function withTimeout(promise, timeoutMs, message) {
55
107
  let timeout;
56
108
  try {
@@ -237,6 +289,9 @@ class CopilotClient {
237
289
  /** Shared in-flight start; concurrent callers await it instead of spawning another CLI. */
238
290
  startPromise = null;
239
291
  sessions = /* @__PURE__ */ new Map();
292
+ hostExitCallbacks = /* @__PURE__ */ new Map();
293
+ hostSessionFactories = /* @__PURE__ */ new Map();
294
+ hostHandoffs = /* @__PURE__ */ new Map();
240
295
  stderrBuffer = "";
241
296
  // Captures CLI stderr for error messages
242
297
  /** Resolved connection mode chosen in the constructor. */
@@ -528,6 +583,11 @@ class CopilotClient {
528
583
  }
529
584
  setupClientGlobalHandlers() {
530
585
  const handlers = {};
586
+ handlers.host = {
587
+ exited: async (exit) => {
588
+ this.handleHostExit(exit);
589
+ }
590
+ };
531
591
  handlers.extensionLaunchProvider = this.extensionLaunchProvider;
532
592
  if (this.requestHandler) {
533
593
  this.requestAdapter = (0, import_copilotRequestHandler.createCopilotRequestAdapter)(this.requestHandler, () => {
@@ -681,7 +741,8 @@ class CopilotClient {
681
741
  * 1. Closes all active sessions (releases in-memory resources)
682
742
  * 2. Requests runtime shutdown for SDK-owned CLI processes
683
743
  * 3. Closes the JSON-RPC connection
684
- * 4. Terminates the CLI server process (if spawned by this client)
744
+ * 4. Signals EOF to an owned stdio process and waits for host cleanup, then
745
+ * terminates the process if it does not exit within the shutdown timeout
685
746
  *
686
747
  * Note: session data on disk is preserved, so sessions can be resumed later.
687
748
  * To permanently remove session data before stopping, call
@@ -799,7 +860,23 @@ class CopilotClient {
799
860
  }
800
861
  if (this.cliProcess && !this.isExternalServer) {
801
862
  const child = this.cliProcess;
802
- this.cliProcess = null;
863
+ if (this.connectionConfig.kind === "stdio" && child.stdin && child.exitCode == null && child.signalCode == null) {
864
+ const gracefulExitStart = Date.now();
865
+ try {
866
+ child.stdin.end();
867
+ const exited = await waitForChildExit(child, RUNTIME_SHUTDOWN_TIMEOUT_MS);
868
+ this.logDebugTiming(
869
+ exited ? "CopilotClient.stop graceful stdio exit complete" : "CopilotClient.stop graceful stdio exit timed out; terminating child",
870
+ gracefulExitStart
871
+ );
872
+ } catch (error) {
873
+ errors.push(
874
+ new Error(
875
+ `Failed to close CLI stdin: ${error instanceof Error ? error.message : String(error)}`
876
+ )
877
+ );
878
+ }
879
+ }
803
880
  try {
804
881
  if (child.exitCode == null && child.signalCode == null) {
805
882
  child.kill();
@@ -817,6 +894,10 @@ class CopilotClient {
817
894
  `Failed to kill CLI process: ${error instanceof Error ? error.message : String(error)}`
818
895
  )
819
896
  );
897
+ } finally {
898
+ if (this.cliProcess === child) {
899
+ this.cliProcess = null;
900
+ }
820
901
  }
821
902
  }
822
903
  if (this.ffiHost) {
@@ -840,6 +921,7 @@ class CopilotClient {
840
921
  this.runtimePort = null;
841
922
  this.stderrBuffer = "";
842
923
  this.processExitPromise = null;
924
+ this.disconnectHosts();
843
925
  return errors;
844
926
  }
845
927
  /**
@@ -884,6 +966,7 @@ class CopilotClient {
884
966
  */
885
967
  async forceStop() {
886
968
  this.forceStopping = true;
969
+ this.disconnectHosts();
887
970
  for (const session of this.sessions.values()) {
888
971
  session._markDisconnected();
889
972
  }
@@ -1183,6 +1266,7 @@ class CopilotClient {
1183
1266
  const response = await this.connection.sendRequest("session.create", {
1184
1267
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
1185
1268
  model: config.model,
1269
+ allowedModels: config.allowedModels,
1186
1270
  sessionId: localSessionId,
1187
1271
  clientName: config.clientName,
1188
1272
  reasoningEffort: config.reasoningEffort,
@@ -1313,6 +1397,11 @@ class CopilotClient {
1313
1397
  }
1314
1398
  throw e;
1315
1399
  }
1400
+ for (const entry of this.hostHandoffs.values()) {
1401
+ if (entry.requestedSessionId === session.sessionId) {
1402
+ entry.configs.set(session, config);
1403
+ }
1404
+ }
1316
1405
  return session;
1317
1406
  }
1318
1407
  /**
@@ -1419,6 +1508,7 @@ class CopilotClient {
1419
1508
  }
1420
1509
  try {
1421
1510
  const response = await this.connection.sendRequest("session.resume", {
1511
+ allowedModels: config.allowedModels,
1422
1512
  ...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
1423
1513
  sessionId,
1424
1514
  clientName: config.clientName,
@@ -1547,8 +1637,205 @@ class CopilotClient {
1547
1637
  }
1548
1638
  throw e;
1549
1639
  }
1640
+ for (const entry of this.hostHandoffs.values()) {
1641
+ if (entry.requestedSessionId === session.sessionId) {
1642
+ entry.configs.set(session, { ...config, sessionId });
1643
+ }
1644
+ }
1550
1645
  return session;
1551
1646
  }
1647
+ /**
1648
+ * Start a complete AHP listener hosted in-process by this runtime.
1649
+ *
1650
+ * This connection owns the host. Disposing it or disconnecting the client
1651
+ * stops the listener without deleting sessions. The host uses a separate
1652
+ * SDK connection to the same runtime, not another runtime process.
1653
+ * Only one AHP host may own the catalog in an effective Copilot home
1654
+ * at a time. Other SDK clients and sessions remain usable in that home.
1655
+ * Select localServer, githubEnvironment, or both. An empty localServer
1656
+ * selects 127.0.0.1 on an available port. `onExit` runs at most once; owner disconnection cannot
1657
+ * acknowledge listener cleanup over the disconnected transport.
1658
+ *
1659
+ * @experimental
1660
+ */
1661
+ async startAhpHost(options) {
1662
+ if (!options?.localServer && !options?.githubEnvironment) {
1663
+ throw new Error("At least one of localServer or githubEnvironment is required");
1664
+ }
1665
+ if (this.state !== "connected") {
1666
+ await this.start();
1667
+ }
1668
+ const rpc = this.rpc;
1669
+ const hostId = (0, import_node_crypto.randomUUID)();
1670
+ const {
1671
+ localServer,
1672
+ githubEnvironment,
1673
+ onExit,
1674
+ createSession,
1675
+ resumeSession,
1676
+ onSessionReleased
1677
+ } = options;
1678
+ if (createSession || resumeSession) {
1679
+ this.hostSessionFactories.set(hostId, {
1680
+ createSession,
1681
+ resumeSession,
1682
+ onSessionReleased
1683
+ });
1684
+ }
1685
+ if (onExit) {
1686
+ this.hostExitCallbacks.set(hostId, onExit);
1687
+ }
1688
+ try {
1689
+ const info = await rpc.host.start({
1690
+ hostId,
1691
+ localServer,
1692
+ githubEnvironment,
1693
+ sessionFactory: createSession ? true : void 0,
1694
+ resumeFactory: resumeSession ? true : void 0
1695
+ });
1696
+ return new import_host.AhpHost(
1697
+ info,
1698
+ async () => {
1699
+ await rpc.host.dispose({ hostId });
1700
+ },
1701
+ (sessionId) => rpc.host.publishSession({ hostId, sessionId })
1702
+ );
1703
+ } catch (error) {
1704
+ this.hostExitCallbacks.delete(hostId);
1705
+ this.releaseHostSessions(hostId);
1706
+ throw error;
1707
+ }
1708
+ }
1709
+ handleHostExit(exit) {
1710
+ this.releaseHostSessions(exit.hostId);
1711
+ const onExit = this.hostExitCallbacks.get(exit.hostId);
1712
+ if (onExit) {
1713
+ this.hostExitCallbacks.delete(exit.hostId);
1714
+ const reportError = (error) => {
1715
+ console.error("AHP host exit callback failed", { hostId: exit.hostId, error });
1716
+ };
1717
+ try {
1718
+ void Promise.resolve(onExit(exit)).catch(reportError);
1719
+ } catch (error) {
1720
+ reportError(error);
1721
+ }
1722
+ }
1723
+ }
1724
+ disconnectHosts() {
1725
+ for (const hostId of /* @__PURE__ */ new Set([
1726
+ ...this.hostExitCallbacks.keys(),
1727
+ ...this.hostSessionFactories.keys()
1728
+ ])) {
1729
+ this.handleHostExit({
1730
+ hostId,
1731
+ reason: "ownerDisconnected",
1732
+ error: "Owner connection closed; runtime cleanup cannot be acknowledged on this connection."
1733
+ });
1734
+ }
1735
+ }
1736
+ releaseHostSessions(hostId) {
1737
+ this.hostSessionFactories.delete(hostId);
1738
+ for (const [handoffId, entry] of this.hostHandoffs) {
1739
+ if (entry.hostId === hostId) this.releaseHostSession(handoffId);
1740
+ }
1741
+ }
1742
+ releaseHostSession(handoffId) {
1743
+ const entry = this.hostHandoffs.get(handoffId);
1744
+ if (!entry) return;
1745
+ this.hostHandoffs.delete(handoffId);
1746
+ entry.released = true;
1747
+ entry.configs.clear();
1748
+ entry.controller.abort();
1749
+ if (entry.session) this.notifyHostSessionReleased(entry);
1750
+ }
1751
+ notifyHostSessionReleased(entry) {
1752
+ const { session, onReleased } = entry;
1753
+ entry.session = void 0;
1754
+ if (session && onReleased) {
1755
+ try {
1756
+ void Promise.resolve(onReleased(session)).catch(() => {
1757
+ console.error("AHP session release callback failed");
1758
+ });
1759
+ } catch {
1760
+ console.error("AHP session release callback failed");
1761
+ }
1762
+ }
1763
+ }
1764
+ async createHostSession(params) {
1765
+ const options = this.hostSessionFactories.get(params.hostId);
1766
+ if (!(params.resume ? options?.resumeSession : options?.createSession) || this.hostHandoffs.has(params.handoffId)) {
1767
+ throw new Error("AHP session factory is unavailable or handoff already exists");
1768
+ }
1769
+ const entry = {
1770
+ hostId: params.hostId,
1771
+ requestedSessionId: params.config.sessionId,
1772
+ controller: new AbortController(),
1773
+ configs: /* @__PURE__ */ new Map(),
1774
+ released: false,
1775
+ onReleased: options.onSessionReleased
1776
+ };
1777
+ this.hostHandoffs.set(params.handoffId, entry);
1778
+ const { configDir, ...config } = params.config;
1779
+ if (configDir !== void 0) config.configDirectory = configDir;
1780
+ const expected = structuredClone(config);
1781
+ const cancelled = new Promise((_resolve, reject) => {
1782
+ entry.controller.signal.addEventListener(
1783
+ "abort",
1784
+ () => {
1785
+ reject(new Error("AHP session handoff ended"));
1786
+ },
1787
+ { once: true }
1788
+ );
1789
+ });
1790
+ const materialized = Promise.resolve().then(() => {
1791
+ if (params.resume) {
1792
+ const { sessionId, ...resumeConfig } = config;
1793
+ if (!sessionId) throw new Error("AHP resume requires sessionId");
1794
+ return options.resumeSession({
1795
+ sessionId,
1796
+ config: resumeConfig,
1797
+ signal: entry.controller.signal
1798
+ });
1799
+ }
1800
+ return options.createSession({
1801
+ config,
1802
+ signal: entry.controller.signal
1803
+ });
1804
+ }).then((session) => {
1805
+ entry.session = session;
1806
+ const actual = entry.configs.get(session);
1807
+ entry.configs.clear();
1808
+ if (entry.released) {
1809
+ this.notifyHostSessionReleased(entry);
1810
+ throw new Error("AHP session handoff ended");
1811
+ }
1812
+ if (this.sessions.get(session.sessionId) !== session || session.sessionId !== expected.sessionId) {
1813
+ throw new Error(
1814
+ "AHP callback must return the requested session from this client"
1815
+ );
1816
+ }
1817
+ const contains = (value, required) => {
1818
+ if (required && typeof required === "object" && !Array.isArray(required)) {
1819
+ return !!value && typeof value === "object" && Object.entries(required).every(
1820
+ ([key, child]) => contains(value[key], child)
1821
+ );
1822
+ }
1823
+ return JSON.stringify(value) === JSON.stringify(required);
1824
+ };
1825
+ if ((!params.resume || actual !== void 0) && !contains(actual, expected)) {
1826
+ throw new Error(
1827
+ "AHP callback must preserve the supplied session configuration"
1828
+ );
1829
+ }
1830
+ return { sessionId: session.sessionId };
1831
+ });
1832
+ try {
1833
+ return await Promise.race([materialized, cancelled]);
1834
+ } catch (error) {
1835
+ this.releaseHostSession(params.handoffId);
1836
+ throw error;
1837
+ }
1838
+ }
1552
1839
  /**
1553
1840
  * Sends a ping request to the server to verify connectivity.
1554
1841
  *
@@ -2088,8 +2375,13 @@ stderr: ${stderrOutput}`
2088
2375
  case "inprocess":
2089
2376
  return this.connectViaFfi();
2090
2377
  case "tcp":
2091
- case "uri":
2092
2378
  return this.connectViaTcp();
2379
+ case "uri": {
2380
+ const { host, port } = this.parseCliUrl(this.connectionConfig.url);
2381
+ this.actualHost = host;
2382
+ this.runtimePort = port;
2383
+ return this.connectViaTcp();
2384
+ }
2093
2385
  }
2094
2386
  }
2095
2387
  /** Starts the in-process FFI runtime with SDK-managed typed options. */
@@ -2148,7 +2440,7 @@ stderr: ${stderrOutput}`
2148
2440
  throw new Error("In-process FFI runtime host not started");
2149
2441
  }
2150
2442
  this.messageWriter = new TeardownResilientStreamMessageWriter(this.ffiHost.sendStream);
2151
- this.connection = (0, import_node.createMessageConnection)(
2443
+ this.connection = createMessageConnection(
2152
2444
  new import_node.StreamMessageReader(this.ffiHost.receiveStream),
2153
2445
  this.messageWriter
2154
2446
  );
@@ -2202,7 +2494,7 @@ stderr: ${stderrOutput}` : ""}`
2202
2494
  }
2203
2495
  });
2204
2496
  this.messageWriter = new TeardownResilientStreamMessageWriter(this.cliProcess.stdin);
2205
- this.connection = (0, import_node.createMessageConnection)(
2497
+ this.connection = createMessageConnection(
2206
2498
  new import_node.StreamMessageReader(this.cliProcess.stdout),
2207
2499
  this.messageWriter
2208
2500
  );
@@ -2217,7 +2509,7 @@ stderr: ${stderrOutput}` : ""}`
2217
2509
  throw new Error("CLI child process was unexpectedly started in parent process mode");
2218
2510
  }
2219
2511
  this.messageWriter = new TeardownResilientStreamMessageWriter(process.stdout);
2220
- this.connection = (0, import_node.createMessageConnection)(
2512
+ this.connection = createMessageConnection(
2221
2513
  new import_node.StreamMessageReader(process.stdin),
2222
2514
  this.messageWriter
2223
2515
  );
@@ -2240,7 +2532,7 @@ stderr: ${stderrOutput}` : ""}`
2240
2532
  this.socket.connect(this.runtimePort, this.actualHost, () => {
2241
2533
  clearTimeout(connectionTimeout);
2242
2534
  this.messageWriter = new TeardownResilientStreamMessageWriter(this.socket);
2243
- this.connection = (0, import_node.createMessageConnection)(
2535
+ this.connection = createMessageConnection(
2244
2536
  new import_node.StreamMessageReader(this.socket),
2245
2537
  this.messageWriter
2246
2538
  );
@@ -2297,6 +2589,18 @@ stderr: ${stderrOutput}` : ""}`
2297
2589
  (params, token) => confirmation.confirm(params, token)
2298
2590
  );
2299
2591
  }
2592
+ this.connection.onRequest(
2593
+ "host.materializeSession",
2594
+ (params) => this.createHostSession(params)
2595
+ );
2596
+ this.connection.onNotification(
2597
+ "host.sessionReleased",
2598
+ (params) => {
2599
+ if (this.hostHandoffs.get(params.handoffId)?.hostId === params.hostId) {
2600
+ this.releaseHostSession(params.handoffId);
2601
+ }
2602
+ }
2603
+ );
2300
2604
  this.connection.onRequest(
2301
2605
  "hooks.invoke",
2302
2606
  async (params) => {
@@ -2304,20 +2608,27 @@ stderr: ${stderrOutput}` : ""}`
2304
2608
  }
2305
2609
  );
2306
2610
  const connection = this.connection;
2611
+ const messageWriter = this.messageWriter;
2307
2612
  const markDisconnected = () => {
2308
2613
  if (this.connection !== connection) {
2614
+ connection.dispose();
2309
2615
  return;
2310
2616
  }
2311
2617
  this.connectionClosed = true;
2312
2618
  this.state = "disconnected";
2619
+ this.disconnectHosts();
2313
2620
  for (const session of this.sessions.values()) {
2314
2621
  session._markDisconnected();
2315
2622
  }
2316
2623
  this.sessions.clear();
2317
2624
  this.githubTokenProviders.clear();
2318
2625
  this.requestAdapter?.cancelPending();
2626
+ connection.dispose();
2319
2627
  };
2320
- this.connection.onClose(markDisconnected);
2628
+ this.connection.onClose(() => {
2629
+ if (messageWriter) messageWriter.suppressWriteErrors = true;
2630
+ void connection.drain().then(markDisconnected);
2631
+ });
2321
2632
  this.connection.onError(() => {
2322
2633
  if (this.connection === connection) {
2323
2634
  this.state = "disconnected";