@github/copilot-sdk 1.0.13-preview.6 → 1.0.14-preview.0

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
@@ -267,11 +267,26 @@ Send a message to the session. Returns immediately after the message is queued;
267
267
  **Options:**
268
268
 
269
269
  - `prompt: string` - The message/prompt to send
270
+ - `source?: MessageSource` - `"user"`, `"system"`, or `` `agent-${string}` `` provenance; omitted by default
270
271
  - `attachments?: Array<{type, path, displayName}>` - File attachments
271
272
  - `mode?: "enqueue" | "immediate"` - Delivery mode
272
273
 
273
274
  Returns the message ID.
274
275
 
276
+ Use `source: "system"` for automated messages from your application:
277
+
278
+ ```typescript
279
+ await session.send({ prompt: "Context updated", source: "system" });
280
+ ```
281
+
282
+ For a message from another agent, use its trusted sender ID:
283
+
284
+ ```typescript
285
+ await session.send({ prompt: "Review complete", source: "agent-reviewer-id" });
286
+ ```
287
+
288
+ Source is independent of delivery mode. Leaving it unset preserves the existing human-message payload; it does not set billing flags or use the notification API.
289
+
275
290
  ##### `sendAndWait(options: MessageOptions, timeout?: number): Promise<AssistantMessageEvent | undefined>`
276
291
 
277
292
  Send a message and wait until the session becomes idle.
@@ -279,6 +294,7 @@ Send a message and wait until the session becomes idle.
279
294
  **Options:**
280
295
 
281
296
  - `prompt: string` - The message/prompt to send
297
+ - `source?: MessageSource` - Same optional provenance as `send`
282
298
  - `attachments?: Array<{type, path, displayName}>` - File attachments
283
299
  - `mode?: "enqueue" | "immediate"` - Delivery mode
284
300
  - `timeout?: number` - Optional timeout in milliseconds
@@ -319,6 +335,32 @@ const unsubscribe = session.on((event) => {
319
335
  unsubscribe();
320
336
  ```
321
337
 
338
+ ##### `setModel(model: string, options?): Promise<void>`
339
+
340
+ Change the model for this session. The new model takes effect for the next message; conversation history is preserved.
341
+
342
+ **Options:**
343
+
344
+ - `reasoningEffort?: string` - Reasoning effort level
345
+ - `autoTier?: AutoTier | null` - Auto routing preference to stage together with selecting `auto`. Pass `null` to return to the provider's default Auto routing; omit it to leave the current preference unchanged.
346
+
347
+ ##### `setAutoTier(autoTier: AutoTier | null): Promise<ModelSwitchAutoTierResult>`
348
+
349
+ Change the Auto routing preference without changing the selected model. Pass `null` to return to the provider's default Auto routing.
350
+
351
+ The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives.
352
+
353
+ Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure, and read the authoritative state at any time with `session.rpc.model.getCurrent()`.
354
+
355
+ ```typescript
356
+ const result = await session.setAutoTier("intelligence");
357
+ if (result.status === "pending") {
358
+ // Accepted, but not yet in effect.
359
+ }
360
+ ```
361
+
362
+ See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules.
363
+
322
364
  ##### `abort(): Promise<void>`
323
365
 
324
366
  Abort the currently processing message in this session.
@@ -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.83-3";
25
+ const COPILOT_CLI_VERSION = "1.0.84-5";
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 = {
@@ -236,10 +236,13 @@ class CopilotClient {
236
236
  ffiHost = null;
237
237
  connection = null;
238
238
  messageWriter = null;
239
+ connectionClosed = false;
239
240
  socket = null;
240
241
  runtimePort = null;
241
242
  actualHost = "localhost";
242
243
  state = "disconnected";
244
+ /** Shared in-flight start; concurrent callers await it instead of spawning another CLI. */
245
+ startPromise = null;
243
246
  sessions = /* @__PURE__ */ new Map();
244
247
  stderrBuffer = "";
245
248
  // Captures CLI stderr for error messages
@@ -617,7 +620,19 @@ class CopilotClient {
617
620
  if (this.state === "connected") {
618
621
  return;
619
622
  }
623
+ if (this.startPromise) {
624
+ return this.startPromise;
625
+ }
626
+ this.startPromise = this.doStart();
627
+ try {
628
+ await this.startPromise;
629
+ } finally {
630
+ this.startPromise = null;
631
+ }
632
+ }
633
+ async doStart() {
620
634
  this.forceStopping = false;
635
+ this.connectionClosed = false;
621
636
  this.processTransportError = null;
622
637
  this.state = "connecting";
623
638
  try {
@@ -716,7 +731,7 @@ class CopilotClient {
716
731
  }
717
732
  this.sessions.clear();
718
733
  this.githubTokenProviders.clear();
719
- if (this.connection && (this.cliProcess || this.ffiHost) && !this.isExternalServer) {
734
+ if (this.connection && !this.connectionClosed && (this.cliProcess || this.ffiHost) && !this.isExternalServer) {
720
735
  const runtimeShutdownStart = Date.now();
721
736
  const shutdownPromise = this.rpc.runtime.shutdown();
722
737
  void shutdownPromise.catch(() => void 0);
@@ -805,7 +820,7 @@ class CopilotClient {
805
820
  const host = this.ffiHost;
806
821
  this.ffiHost = null;
807
822
  try {
808
- host.dispose();
823
+ await host.dispose();
809
824
  } catch (error) {
810
825
  errors.push(
811
826
  new Error(
@@ -900,11 +915,12 @@ class CopilotClient {
900
915
  this.cliProcess = null;
901
916
  }
902
917
  if (this.ffiHost) {
918
+ const host = this.ffiHost;
919
+ this.ffiHost = null;
903
920
  try {
904
- this.ffiHost.dispose();
921
+ await host.dispose();
905
922
  } catch {
906
923
  }
907
- this.ffiHost = null;
908
924
  }
909
925
  if (this.cliStartTimeout) {
910
926
  clearTimeout(this.cliStartTimeout);
@@ -1221,6 +1237,7 @@ class CopilotClient {
1221
1237
  ...this.onGitHubTelemetry != null ? { enableGitHubTelemetryForwarding: true } : {},
1222
1238
  mcpServers: toWireMcpServers(config.mcpServers),
1223
1239
  mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
1240
+ authClientIdMetadataUrl: config.authClientIdMetadataUrl,
1224
1241
  envValueMode: "direct",
1225
1242
  customAgents: toWireCustomAgents(config.customAgents),
1226
1243
  customAgentsLocalOnly: config.customAgentsLocalOnly,
@@ -1281,6 +1298,7 @@ class CopilotClient {
1281
1298
  await this.updateSessionOptionsForMode(session, config);
1282
1299
  this.commitGitHubTokenProvider(returnedSessionId, gitHubTokenProviderRegistrationId);
1283
1300
  } catch (e) {
1301
+ session?._markDisconnected();
1284
1302
  if (registeredId !== void 0) {
1285
1303
  this.sessions.delete(registeredId);
1286
1304
  }
@@ -1465,6 +1483,7 @@ class CopilotClient {
1465
1483
  ...this.onGitHubTelemetry != null ? { enableGitHubTelemetryForwarding: true } : {},
1466
1484
  mcpServers: toWireMcpServers(config.mcpServers),
1467
1485
  mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
1486
+ authClientIdMetadataUrl: config.authClientIdMetadataUrl,
1468
1487
  envValueMode: "direct",
1469
1488
  customAgents: toWireCustomAgents(config.customAgents),
1470
1489
  customAgentsLocalOnly: config.customAgentsLocalOnly,
@@ -1513,6 +1532,7 @@ class CopilotClient {
1513
1532
  await this.updateSessionOptionsForMode(session, config);
1514
1533
  this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId);
1515
1534
  } catch (e) {
1535
+ session._markDisconnected();
1516
1536
  this.sessions.delete(sessionId);
1517
1537
  if (gitHubTokenProviderRegistrationId !== void 0) {
1518
1538
  this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId);
@@ -1628,7 +1648,10 @@ class CopilotClient {
1628
1648
  const raceAgainstExit = (p) => this.processExitPromise ? Promise.race([p, this.processExitPromise]) : p;
1629
1649
  let serverVersion;
1630
1650
  try {
1631
- const connectParams = { token: this.effectiveConnectionToken };
1651
+ const connectParams = {
1652
+ token: this.effectiveConnectionToken,
1653
+ supportedTaskKinds: ["agent", "client", "shell"]
1654
+ };
1632
1655
  if (this.onGitHubTelemetry != null) {
1633
1656
  connectParams.enableGitHubTelemetryForwarding = true;
1634
1657
  }
@@ -2259,12 +2282,24 @@ stderr: ${stderrOutput}` : ""}`
2259
2282
  return await this.handleHooksInvoke(params);
2260
2283
  }
2261
2284
  );
2262
- this.connection.onClose(() => {
2285
+ const connection = this.connection;
2286
+ const markDisconnected = () => {
2287
+ if (this.connection !== connection) {
2288
+ return;
2289
+ }
2290
+ this.connectionClosed = true;
2263
2291
  this.state = "disconnected";
2292
+ for (const session of this.sessions.values()) {
2293
+ session._markDisconnected();
2294
+ }
2295
+ this.sessions.clear();
2264
2296
  this.githubTokenProviders.clear();
2265
- });
2266
- this.connection.onError((_error) => {
2267
- this.state = "disconnected";
2297
+ };
2298
+ this.connection.onClose(markDisconnected);
2299
+ this.connection.onError(() => {
2300
+ if (this.connection === connection) {
2301
+ this.state = "disconnected";
2302
+ }
2268
2303
  });
2269
2304
  }
2270
2305
  handleSessionEventNotification(notification) {
@@ -37,6 +37,7 @@ var import_node_path = require("node:path");
37
37
  var import_node_stream = require("node:stream");
38
38
  const SYMBOL_PREFIX = "copilot_runtime_";
39
39
  const KEEP_ALIVE_INTERVAL_MS = 1 << 30;
40
+ const CLEANUP_RETRY_INTERVAL_MS = 100;
40
41
  let loadedLibraryPath;
41
42
  let loadedLibrary;
42
43
  function loadLibrary(libraryPath) {
@@ -129,12 +130,16 @@ class FfiRuntimeHost {
129
130
  cliEntrypoint;
130
131
  environment;
131
132
  args;
133
+ static quarantinedHosts = /* @__PURE__ */ new Set();
132
134
  lib;
133
135
  serverId = 0;
134
136
  connectionId = 0;
135
137
  disposed = false;
138
+ starting = false;
136
139
  outboundCallback;
137
140
  keepAliveTimer;
141
+ cleanupRetryTimer;
142
+ cleanupInProgress = false;
138
143
  /** The stream JSON-RPC reads server→client frames from. */
139
144
  receiveStream;
140
145
  /** The stream JSON-RPC writes client→server frames to. */
@@ -156,49 +161,65 @@ class FfiRuntimeHost {
156
161
  }
157
162
  /** Starts the in-process Rust runtime and opens the FFI JSON-RPC connection. */
158
163
  async start() {
164
+ if (this.disposed) {
165
+ throw new Error("The in-process runtime host is disposed.");
166
+ }
167
+ this.starting = true;
159
168
  const argvJson = buildArgvJson(this.cliEntrypoint, this.args);
160
169
  const envJson = buildEnvJson(this.environment);
161
- this.serverId = await new Promise((resolvePromise, rejectPromise) => {
162
- this.lib.hostStart.async(
163
- argvJson,
164
- argvJson.length,
165
- envJson,
166
- envJson ? envJson.length : 0,
167
- (error, result) => {
168
- if (error) {
169
- rejectPromise(error);
170
- } else {
171
- resolvePromise(result);
170
+ try {
171
+ this.serverId = await new Promise((resolvePromise, rejectPromise) => {
172
+ this.lib.hostStart.async(
173
+ argvJson,
174
+ argvJson.length,
175
+ envJson,
176
+ envJson ? envJson.length : 0,
177
+ (error, result) => {
178
+ if (error) {
179
+ rejectPromise(error);
180
+ } else {
181
+ resolvePromise(result);
182
+ }
172
183
  }
173
- }
184
+ );
185
+ });
186
+ if (!this.serverId) {
187
+ throw new Error(
188
+ `copilot_runtime_host_start failed (library '${this.libraryPath}').`
189
+ );
190
+ }
191
+ if (this.disposed) {
192
+ throw new Error("The in-process runtime host was disposed during startup.");
193
+ }
194
+ this.outboundCallback = import_koffi.default.register(
195
+ (_userData, bytesPtr, bytesLen) => this.feedInbound(bytesPtr, bytesLen),
196
+ this.lib.outboundCallbackType
174
197
  );
175
- });
176
- if (!this.serverId) {
177
- throw new Error(`copilot_runtime_host_start failed (library '${this.libraryPath}').`);
178
- }
179
- this.outboundCallback = import_koffi.default.register(
180
- (_userData, bytesPtr, bytesLen) => this.feedInbound(bytesPtr, bytesLen),
181
- this.lib.outboundCallbackType
182
- );
183
- this.connectionId = this.lib.connectionOpen(
184
- this.serverId,
185
- this.outboundCallback,
186
- null,
187
- null,
188
- 0,
189
- null,
190
- 0,
191
- null,
192
- 0
193
- );
194
- if (!this.connectionId) {
195
- this.unregisterCallback();
196
- this.lib.hostShutdown(this.serverId);
197
- this.serverId = 0;
198
- throw new Error("copilot_runtime_connection_open failed.");
198
+ this.connectionId = this.lib.connectionOpen(
199
+ this.serverId,
200
+ this.outboundCallback,
201
+ null,
202
+ null,
203
+ 0,
204
+ null,
205
+ 0,
206
+ null,
207
+ 0
208
+ );
209
+ if (!this.connectionId) {
210
+ this.unregisterCallback();
211
+ this.lib.hostShutdown(this.serverId);
212
+ this.serverId = 0;
213
+ throw new Error("copilot_runtime_connection_open failed.");
214
+ }
215
+ this.keepAliveTimer = setInterval(() => {
216
+ }, KEEP_ALIVE_INTERVAL_MS);
217
+ } finally {
218
+ this.starting = false;
219
+ if (this.disposed) {
220
+ void this.tryFinalizeCleanup();
221
+ }
199
222
  }
200
- this.keepAliveTimer = setInterval(() => {
201
- }, KEEP_ALIVE_INTERVAL_MS);
202
223
  }
203
224
  writeFrame(frame) {
204
225
  if (this.disposed || !this.connectionId) {
@@ -237,41 +258,105 @@ class FfiRuntimeHost {
237
258
  }
238
259
  unregisterCallback() {
239
260
  if (this.outboundCallback === void 0) {
240
- return;
261
+ return true;
241
262
  }
242
263
  const callback = this.outboundCallback;
243
- this.outboundCallback = void 0;
244
264
  try {
245
265
  import_koffi.default.unregister(callback);
246
- } catch {
266
+ this.outboundCallback = void 0;
267
+ return true;
268
+ } catch (error) {
269
+ console.error(
270
+ `Failed to unregister in-process FFI callback: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
271
+ );
272
+ FfiRuntimeHost.quarantinedHosts.add(this);
273
+ return false;
247
274
  }
248
275
  }
249
- /** Closes the FFI connection, shuts down the native host, and releases resources. */
250
- dispose() {
251
- if (this.disposed) {
276
+ scheduleCleanupRetry() {
277
+ if (this.cleanupRetryTimer !== void 0) {
252
278
  return;
253
279
  }
254
- this.disposed = true;
255
- if (this.keepAliveTimer !== void 0) {
256
- clearInterval(this.keepAliveTimer);
257
- this.keepAliveTimer = void 0;
280
+ this.cleanupRetryTimer = setTimeout(() => {
281
+ this.cleanupRetryTimer = void 0;
282
+ void this.tryFinalizeCleanup();
283
+ }, CLEANUP_RETRY_INTERVAL_MS);
284
+ }
285
+ async tryFinalizeCleanup() {
286
+ if (this.cleanupInProgress) {
287
+ this.scheduleCleanupRetry();
288
+ return;
258
289
  }
290
+ this.cleanupInProgress = true;
259
291
  try {
260
292
  if (this.connectionId) {
261
- this.lib.connectionClose(this.connectionId);
293
+ let closed = false;
294
+ try {
295
+ closed = await new Promise((resolvePromise, rejectPromise) => {
296
+ this.lib.connectionClose.async(
297
+ this.connectionId,
298
+ (error, result) => {
299
+ if (error) {
300
+ rejectPromise(error);
301
+ } else {
302
+ resolvePromise(result);
303
+ }
304
+ }
305
+ );
306
+ });
307
+ } catch (error) {
308
+ console.error(
309
+ `Failed to close in-process FFI connection: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
310
+ );
311
+ FfiRuntimeHost.quarantinedHosts.add(this);
312
+ if (this.keepAliveTimer !== void 0) {
313
+ clearInterval(this.keepAliveTimer);
314
+ this.keepAliveTimer = void 0;
315
+ }
316
+ return;
317
+ }
318
+ if (!closed) {
319
+ this.scheduleCleanupRetry();
320
+ return;
321
+ }
262
322
  this.connectionId = 0;
263
323
  }
264
- } catch {
265
- }
266
- try {
324
+ const callbackUnregistered = this.unregisterCallback();
325
+ if (this.keepAliveTimer !== void 0) {
326
+ clearInterval(this.keepAliveTimer);
327
+ this.keepAliveTimer = void 0;
328
+ }
267
329
  if (this.serverId) {
268
- this.lib.hostShutdown(this.serverId);
330
+ try {
331
+ if (!this.lib.hostShutdown(this.serverId)) {
332
+ console.error(
333
+ `In-process FFI host shutdown did not recognize server ${this.serverId}.`
334
+ );
335
+ }
336
+ } catch (error) {
337
+ console.error(
338
+ `Failed to shut down in-process FFI host: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
339
+ );
340
+ }
269
341
  this.serverId = 0;
270
342
  }
271
- } catch {
343
+ if (callbackUnregistered) {
344
+ FfiRuntimeHost.quarantinedHosts.delete(this);
345
+ }
346
+ } finally {
347
+ this.cleanupInProgress = false;
348
+ }
349
+ }
350
+ /** Awaits the initial cleanup attempt; a non-quiescent close is retried in the background. */
351
+ async dispose() {
352
+ if (this.disposed) {
353
+ return;
272
354
  }
355
+ this.disposed = true;
273
356
  this.receiveStream.end();
274
- this.unregisterCallback();
357
+ if (!this.starting) {
358
+ await this.tryFinalizeCleanup();
359
+ }
275
360
  }
276
361
  }
277
362
  // Annotate the CommonJS export names for ESM import in node: