@github/copilot-sdk 1.0.29 → 1.0.30-preview.1

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/client.js CHANGED
@@ -1,11 +1,14 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { spawn } from "node:child_process";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { accessSync, constants, existsSync, statSync } from "node:fs";
4
5
  import { isIPv6, Socket } from "node:net";
5
6
  import { dirname, isAbsolute, join, resolve, win32 } from "node:path";
6
7
  import {
8
+ CancellationTokenSource,
7
9
  createMessageConnection as createRpcMessageConnection,
8
10
  ErrorCodes,
11
+ Message,
9
12
  ResponseError,
10
13
  StreamMessageReader,
11
14
  StreamMessageWriter
@@ -16,6 +19,7 @@ import {
16
19
  registerClientGlobalApiHandlers,
17
20
  registerClientSessionApiHandlers
18
21
  } from "./generated/rpc.js";
22
+ import { CopilotDecisions } from "./decisions.js";
19
23
  import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
20
24
  import { CopilotSession } from "./session.js";
21
25
  import {
@@ -36,6 +40,20 @@ import { defaultJoinSessionPermissionHandler } from "./types.js";
36
40
  const MIN_PROTOCOL_VERSION = 3;
37
41
  const RUNTIME_SHUTDOWN_TIMEOUT_MS = 1e4;
38
42
  const CLOUD_SESSION_CLEANUP_TIMEOUT_MS = 1e4;
43
+ const AUTO_MODE_RECOVERY_CLEANUP_TIMEOUT_MS = 1e4;
44
+ function sessionIdsOverlap(left, right) {
45
+ if (left === right) return true;
46
+ if (!isUuidSessionId(left) || !isUuidSessionId(right)) return false;
47
+ const normalizedLeft = left.toLowerCase();
48
+ const normalizedRight = right.toLowerCase();
49
+ return normalizedLeft.startsWith(normalizedRight) || normalizedRight.startsWith(normalizedLeft);
50
+ }
51
+ function isAbbreviatedUuidPrefix(sessionId) {
52
+ return /^[0-9a-f]{7}[0-9a-f-]{0,28}$/i.test(sessionId);
53
+ }
54
+ function isUuidSessionId(sessionId) {
55
+ return isAbbreviatedUuidPrefix(sessionId) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
56
+ }
39
57
  const messageEncodeErrors = /* @__PURE__ */ new WeakSet();
40
58
  const jsonMessageEncoder = {
41
59
  name: "application/json",
@@ -49,11 +67,12 @@ const jsonMessageEncoder = {
49
67
  }
50
68
  }
51
69
  };
52
- function createMessageConnection(reader, writer) {
70
+ function createMessageConnection(reader, writer, sessionInitializationContext) {
53
71
  let dispatch;
54
72
  let finishDrain;
55
73
  let drained;
56
74
  let disposed = false;
75
+ const callbacks = /* @__PURE__ */ new Map();
57
76
  const barrier = { jsonrpc: "2.0", method: "$/sdkDrain" };
58
77
  const connection = createRpcMessageConnection(
59
78
  {
@@ -66,7 +85,22 @@ function createMessageConnection(reader, writer) {
66
85
  },
67
86
  dispose: () => reader.dispose()
68
87
  },
69
- writer,
88
+ {
89
+ onError: writer.onError,
90
+ onClose: writer.onClose,
91
+ write: (message) => {
92
+ if (Message.isResponse(message)) {
93
+ const context = callbacks.get(message.id);
94
+ if (context) {
95
+ context.active = false;
96
+ callbacks.delete(message.id);
97
+ }
98
+ }
99
+ return writer.write(message);
100
+ },
101
+ end: () => writer.end(),
102
+ dispose: () => writer.dispose()
103
+ },
70
104
  void 0,
71
105
  {
72
106
  messageStrategy: {
@@ -74,7 +108,14 @@ function createMessageConnection(reader, writer) {
74
108
  if (message === barrier) {
75
109
  finishDrain?.();
76
110
  } else {
77
- next(message);
111
+ const request = Message.isRequest(message) ? message : void 0;
112
+ const params = request?.params;
113
+ const sessionId = params && typeof params === "object" && "sessionId" in params && typeof params.sessionId === "string" ? params.sessionId : void 0;
114
+ const context = sessionId === void 0 ? void 0 : { sessionId, active: true };
115
+ if (request && context) {
116
+ callbacks.set(request.id, context);
117
+ }
118
+ sessionInitializationContext.run(context, next, message);
78
119
  }
79
120
  }
80
121
  }
@@ -82,6 +123,10 @@ function createMessageConnection(reader, writer) {
82
123
  );
83
124
  connection.onDispose(() => {
84
125
  disposed = true;
126
+ for (const context of callbacks.values()) {
127
+ context.active = false;
128
+ }
129
+ callbacks.clear();
85
130
  finishDrain?.();
86
131
  });
87
132
  return Object.assign(connection, {
@@ -144,8 +189,8 @@ async function waitForChildExit(child, timeoutMs) {
144
189
  }
145
190
  });
146
191
  }
147
- async function acquireKerberosToken(connection, callback, request, token) {
148
- const ended = () => token.isCancellationRequested ? new ResponseError(-32800, "Kerberos auth request cancelled") : new Error("Kerberos auth connection closed");
192
+ async function runCancellableCallback(connection, label, token, invoke) {
193
+ const ended = () => token.isCancellationRequested ? new ResponseError(-32800, `${label} request cancelled`) : new Error(`${label} connection closed`);
149
194
  if (token.isCancellationRequested) {
150
195
  throw ended();
151
196
  }
@@ -160,14 +205,47 @@ async function acquireKerberosToken(connection, callback, request, token) {
160
205
  connection.onDispose(abort)
161
206
  ];
162
207
  try {
163
- const lookup = Promise.resolve().then(() => callback(request, controller.signal));
164
- return { token: await Promise.race([lookup, abandoned]) };
208
+ const lookup = Promise.resolve().then(() => {
209
+ if (controller.signal.aborted) {
210
+ throw ended();
211
+ }
212
+ return invoke(controller.signal);
213
+ });
214
+ return await Promise.race([lookup, abandoned]);
165
215
  } finally {
166
216
  for (const subscription of subscriptions) {
167
217
  subscription.dispose();
168
218
  }
169
219
  }
170
220
  }
221
+ function snapshotNetworkConfig(network) {
222
+ const { scope, resolveProxy, authenticateNegotiate } = network;
223
+ return {
224
+ scope,
225
+ resolveProxy: resolveProxy && ((request, signal) => resolveProxy.call(network, request, signal)),
226
+ authenticateNegotiate: authenticateNegotiate && ((request, signal) => authenticateNegotiate.call(network, request, signal))
227
+ };
228
+ }
229
+ async function resolveClientProxy(connection, callback, request, token) {
230
+ return {
231
+ proxy: await runCancellableCallback(
232
+ connection,
233
+ "Proxy resolution",
234
+ token,
235
+ (signal) => callback(request, signal)
236
+ )
237
+ };
238
+ }
239
+ async function authenticateClientNegotiate(connection, callback, request, token) {
240
+ return {
241
+ token: await runCancellableCallback(
242
+ connection,
243
+ "Negotiate authentication",
244
+ token,
245
+ (signal) => callback(request, signal)
246
+ )
247
+ };
248
+ }
171
249
  const DEFAULT_PROVIDER_NAME = "default";
172
250
  function extractBearerTokenProviders(provider, providers) {
173
251
  const callbacks = /* @__PURE__ */ new Map();
@@ -206,6 +284,18 @@ function toWireMcpServers(mcpServers) {
206
284
  })
207
285
  );
208
286
  }
287
+ function requestedMemoryStore(memory) {
288
+ if (!memory?.storeMemory) return void 0;
289
+ if (memory.enabled !== true) {
290
+ throw new Error("memory.storeMemory requires memory.enabled to be true.");
291
+ }
292
+ return memory.storeMemory;
293
+ }
294
+ function toWireMemoryConfiguration(memory) {
295
+ if (!memory) return memory;
296
+ const { storeMemory: _storeMemory, ...wire } = memory;
297
+ return wire;
298
+ }
209
299
  function toWireCustomAgents(agents) {
210
300
  if (!agents) return void 0;
211
301
  return agents.map((agent) => {
@@ -308,6 +398,7 @@ class CopilotClient {
308
398
  ffiHost = null;
309
399
  connection = null;
310
400
  requestAdapter = null;
401
+ pendingMemoryStores = /* @__PURE__ */ new Set();
311
402
  messageWriter = null;
312
403
  connectionClosed = false;
313
404
  connectionEpoch = 0;
@@ -317,7 +408,16 @@ class CopilotClient {
317
408
  state = "disconnected";
318
409
  /** Shared in-flight start; concurrent callers await it instead of spawning another CLI. */
319
410
  startPromise = null;
411
+ shutdownCanceledStartup = null;
412
+ stopPromise = null;
413
+ forceStopPromise = null;
320
414
  sessions = /* @__PURE__ */ new Map();
415
+ pendingSessionResumes = /* @__PURE__ */ new Map();
416
+ sessionInitializations = /* @__PURE__ */ new Map();
417
+ failedAutoModeRecoveryCleanups = /* @__PURE__ */ new WeakMap();
418
+ sessionInitializationContext = new AsyncLocalStorage();
419
+ sessionInitializationLifetime = new AbortController();
420
+ sessionInitializationGeneration = 0;
321
421
  hostExitCallbacks = /* @__PURE__ */ new Map();
322
422
  hostSessionFactories = /* @__PURE__ */ new Map();
323
423
  hostAuthenticators = /* @__PURE__ */ new Map();
@@ -346,6 +446,7 @@ class CopilotClient {
346
446
  typedLifecycleHandlers = /* @__PURE__ */ new Map();
347
447
  _rpc = null;
348
448
  _internalRpc = null;
449
+ _decisions;
349
450
  processExitPromise = null;
350
451
  // Rejects when CLI process exits
351
452
  processTransportError = null;
@@ -354,7 +455,7 @@ class CopilotClient {
354
455
  sessionFsConfig = null;
355
456
  requestHandler = null;
356
457
  extensionLaunchProvider;
357
- kerberosAuth;
458
+ network;
358
459
  installationConfirmationHandler;
359
460
  builtinPluginDirectories = [];
360
461
  onGitHubTelemetry;
@@ -373,6 +474,16 @@ class CopilotClient {
373
474
  }
374
475
  return this._rpc;
375
476
  }
477
+ /**
478
+ * One-off yes/no, pick-one and rating questions answered by Copilot decision
479
+ * models. Requires the runtime's `DECISIONS_API` feature flag.
480
+ *
481
+ * @experimental
482
+ */
483
+ get decisions() {
484
+ this._decisions ??= new CopilotDecisions(() => this.rpc);
485
+ return this._decisions;
486
+ }
376
487
  /**
377
488
  * Internal RPC surface (e.g. handshake helpers). Not part of the public API.
378
489
  * @internal
@@ -517,7 +628,7 @@ class CopilotClient {
517
628
  this.sessionFsConfig = options.sessionFs ?? null;
518
629
  this.requestHandler = options.requestHandler ?? null;
519
630
  this.extensionLaunchProvider = options.extensionLaunchProvider;
520
- this.kerberosAuth = options.kerberosAuth;
631
+ this.network = options.network ? snapshotNetworkConfig(options.network) : void 0;
521
632
  this.installationConfirmationHandler = options.installationConfirmationHandler;
522
633
  this.onGitHubTelemetry = options.onGitHubTelemetry;
523
634
  this.setupClientGlobalHandlers();
@@ -560,10 +671,15 @@ class CopilotClient {
560
671
  connectionExtraArgs = [];
561
672
  /**
562
673
  * Parse CLI URL into host and port
563
- * Supports formats: "host:port", "[ipv6]:port", "http://host:port", "https://host:port", or just "port"
674
+ * Supports raw TCP addresses: "host:port", "[ipv6]:port", "http://host:port", or just "port"
564
675
  */
565
676
  parseCliUrl(url) {
566
- const cleanUrl = url.replace(/^https?:\/\//, "");
677
+ if (/^https:\/\//i.test(url)) {
678
+ throw new Error(
679
+ "HTTPS runtime connections are not supported: this transport uses plaintext TCP, not TLS"
680
+ );
681
+ }
682
+ const cleanUrl = url.replace(/^http:\/\//, "");
567
683
  if (/^\d+$/.test(cleanUrl)) {
568
684
  return { host: "localhost", port: parseInt(cleanUrl, 10) };
569
685
  }
@@ -735,20 +851,38 @@ class CopilotClient {
735
851
  * ```
736
852
  */
737
853
  async start() {
854
+ if (this.forceStopPromise || this.stopPromise && !this.forceStopping) {
855
+ throw new Error("Client disconnected during session initialization");
856
+ }
857
+ const startup = this.startPromise;
858
+ if (startup && startup === this.shutdownCanceledStartup) {
859
+ const generation = this.sessionInitializationGeneration;
860
+ await Promise.allSettled([startup, this.stopPromise]);
861
+ if (generation !== this.sessionInitializationGeneration) {
862
+ throw new Error("Client disconnected during session initialization");
863
+ }
864
+ return this.start();
865
+ }
866
+ if (startup && !this.stopPromise) {
867
+ return startup;
868
+ }
869
+ if (this.stopPromise || this.connection && this.sessionInitializationLifetime.signal.aborted) {
870
+ throw new Error("Client disconnected during session initialization");
871
+ }
738
872
  if (this.state === "connected") {
739
873
  return;
740
874
  }
741
- if (this.startPromise) {
742
- return this.startPromise;
743
- }
744
875
  this.startPromise = this.doStart();
745
876
  try {
746
877
  await this.startPromise;
747
878
  } finally {
748
879
  this.startPromise = null;
880
+ this.shutdownCanceledStartup = null;
749
881
  }
750
882
  }
751
883
  async doStart() {
884
+ this.sessionInitializationLifetime = new AbortController();
885
+ const signal = this.sessionInitializationLifetime.signal;
752
886
  this.forceStopping = false;
753
887
  this.connectionEpoch++;
754
888
  this.connectionClosed = false;
@@ -760,24 +894,25 @@ class CopilotClient {
760
894
  } else if (!this.isExternalServer) {
761
895
  await this.startCLIServer();
762
896
  }
897
+ signal.throwIfAborted();
763
898
  await this.connectToServer();
899
+ signal.throwIfAborted();
764
900
  await this.verifyProtocolVersion();
765
- if (this.kerberosAuth) {
766
- const { mode, scope } = this.kerberosAuth;
767
- await this.rpc.kerberosAuth.setProvider(scope ? { mode, scope } : { mode });
901
+ if (this.network?.resolveProxy || this.network?.authenticateNegotiate) {
902
+ const { scope, resolveProxy, authenticateNegotiate } = this.network;
903
+ await this.rpc.network.setProvider({
904
+ ...scope ? { scope } : {},
905
+ ...resolveProxy ? { resolveProxy: {} } : {},
906
+ ...authenticateNegotiate ? { authenticateNegotiate: {} } : {}
907
+ });
768
908
  }
769
909
  if (this.extensionLaunchProvider) {
770
910
  await this.rpc.registerExtensionLaunchProvider();
771
911
  }
772
912
  if (this.builtinPluginDirectories.length > 0) {
773
- try {
774
- await this.connection.sendRequest("plugins.builtin.set", {
775
- paths: this.builtinPluginDirectories
776
- });
777
- } catch (error) {
778
- await this.forceStop();
779
- throw error;
780
- }
913
+ await this.connection.sendRequest("plugins.builtin.set", {
914
+ paths: this.builtinPluginDirectories
915
+ });
781
916
  }
782
917
  if (this.sessionFsConfig) {
783
918
  await this.connection.sendRequest("sessionFs.setProvider", {
@@ -793,10 +928,14 @@ class CopilotClient {
793
928
  if (this.connectionClosed) {
794
929
  throw new Error("CLI server connection closed during startup");
795
930
  }
931
+ signal.throwIfAborted();
796
932
  this.state = "connected";
797
933
  } catch (error) {
798
934
  const startupError = this.processTransportError ?? error;
799
- await this.forceStop();
935
+ if (!signal.aborted) {
936
+ this.invalidateSessionInitializations();
937
+ }
938
+ await this.doForceStop();
800
939
  this.state = "error";
801
940
  throw startupError;
802
941
  }
@@ -832,6 +971,21 @@ class CopilotClient {
832
971
  * ```
833
972
  */
834
973
  async stop() {
974
+ if (this.stopPromise) {
975
+ return this.stopPromise;
976
+ }
977
+ if (!this.sessionInitializationLifetime.signal.aborted) {
978
+ this.shutdownCanceledStartup = this.startPromise;
979
+ }
980
+ this.invalidateSessionInitializations();
981
+ this.stopPromise = this.doStop(this.startPromise);
982
+ try {
983
+ return await this.stopPromise;
984
+ } finally {
985
+ this.stopPromise = null;
986
+ }
987
+ }
988
+ async doStop(startup) {
835
989
  const errors = [];
836
990
  const activeSessions = [...this.sessions.values()];
837
991
  if (this.connectionConfig.kind === "inprocess") {
@@ -885,12 +1039,13 @@ class CopilotClient {
885
1039
  }
886
1040
  }
887
1041
  clearTimeout(detachDeadlineTimer);
888
- for (const session of activeSessions) {
1042
+ for (const session of /* @__PURE__ */ new Set([...activeSessions, ...this.sessions.values()])) {
889
1043
  session._markDisconnected();
890
1044
  }
891
1045
  this.sessions.clear();
892
1046
  this.githubTokenProviders.clear();
893
1047
  this.requestAdapter?.cancelPending();
1048
+ this.abortPendingMemoryStores();
894
1049
  if (this.connection && !this.connectionClosed && (this.cliProcess || this.ffiHost) && !this.isExternalServer) {
895
1050
  const runtimeShutdownStart = Date.now();
896
1051
  const shutdownPromise = this.rpc.runtime.shutdown();
@@ -1026,6 +1181,9 @@ class CopilotClient {
1026
1181
  clearTimeout(this.cliStartTimeout);
1027
1182
  this.cliStartTimeout = null;
1028
1183
  }
1184
+ if (startup) {
1185
+ await Promise.allSettled([startup]);
1186
+ }
1029
1187
  this.state = "disconnected";
1030
1188
  this.runtimePort = null;
1031
1189
  this.stderrBuffer = "";
@@ -1057,7 +1215,8 @@ class CopilotClient {
1057
1215
  * - Sends SIGKILL to the CLI process (if spawned by this client)
1058
1216
  * - Waits for the spawned CLI process to exit
1059
1217
  *
1060
- * @returns A promise that resolves when the force stop is complete
1218
+ * @returns Resolves after disconnection and spawned-process exit; later starts
1219
+ * wait for pending startup cleanup.
1061
1220
  * @throws If the spawned CLI process cannot be terminated
1062
1221
  *
1063
1222
  * @example
@@ -1076,6 +1235,21 @@ class CopilotClient {
1076
1235
  * ```
1077
1236
  */
1078
1237
  async forceStop() {
1238
+ if (this.forceStopPromise) {
1239
+ return this.forceStopPromise;
1240
+ }
1241
+ if (!this.sessionInitializationLifetime.signal.aborted) {
1242
+ this.shutdownCanceledStartup = this.startPromise;
1243
+ }
1244
+ this.invalidateSessionInitializations();
1245
+ this.forceStopPromise = this.doForceStop();
1246
+ try {
1247
+ await this.forceStopPromise;
1248
+ } finally {
1249
+ this.forceStopPromise = null;
1250
+ }
1251
+ }
1252
+ async doForceStop() {
1079
1253
  this.forceStopping = true;
1080
1254
  this.disconnectHosts();
1081
1255
  for (const session of this.sessions.values()) {
@@ -1084,6 +1258,7 @@ class CopilotClient {
1084
1258
  this.sessions.clear();
1085
1259
  this.githubTokenProviders.clear();
1086
1260
  this.requestAdapter?.cancelPending();
1261
+ this.abortPendingMemoryStores();
1087
1262
  if (this.messageWriter) {
1088
1263
  this.messageWriter.suppressWriteErrors = true;
1089
1264
  }
@@ -1295,15 +1470,112 @@ class CopilotClient {
1295
1470
  }
1296
1471
  }
1297
1472
  async createSession(config) {
1473
+ if (config.autoModeRecovery && typeof config.onAutoModeSwitchRequest !== "function") {
1474
+ throw new Error("autoModeRecovery requires onAutoModeSwitchRequest");
1475
+ }
1298
1476
  if (config.gitHubToken !== void 0 && config.gitHubTokenProvider !== void 0) {
1299
1477
  throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive");
1300
1478
  }
1301
1479
  if (config.cloud != null && config.skillProvider) {
1302
1480
  throw new Error("Skill providers are not supported for cloud sessions.");
1303
1481
  }
1304
- if (!this.connection || this.startPromise) {
1305
- await this.start();
1482
+ const storeMemory = requestedMemoryStore(config.memory);
1483
+ if (config.cloud != null && storeMemory) {
1484
+ throw new Error("Memory stores are not supported for cloud sessions.");
1485
+ }
1486
+ const signal = await this.getSessionInitializationSignal();
1487
+ const sessionId = config.sessionId ?? (config.cloud != null ? void 0 : randomUUID());
1488
+ if (sessionId === void 0) {
1489
+ return this.createSessionInternal(config, signal);
1490
+ }
1491
+ const release = await this.acquireSessionInitialization(sessionId, signal);
1492
+ try {
1493
+ signal.throwIfAborted();
1494
+ return await this.createSessionInternal({ ...config, sessionId }, signal);
1495
+ } finally {
1496
+ release();
1497
+ }
1498
+ }
1499
+ invalidateSessionInitializations() {
1500
+ this.sessionInitializationGeneration++;
1501
+ this.sessionInitializationLifetime.abort(
1502
+ new Error("Client disconnected during session initialization")
1503
+ );
1504
+ }
1505
+ async getSessionInitializationSignal() {
1506
+ const generation = this.sessionInitializationGeneration;
1507
+ const starting = !this.connection || this.startPromise ? this.start() : void 0;
1508
+ await starting;
1509
+ if (generation !== this.sessionInitializationGeneration) {
1510
+ throw new Error("Client disconnected during session initialization");
1511
+ }
1512
+ const signal = this.sessionInitializationLifetime.signal;
1513
+ signal.throwIfAborted();
1514
+ return signal;
1515
+ }
1516
+ async runInitializationCallback(sessionId, callback) {
1517
+ const context = sessionId === void 0 ? void 0 : { sessionId, active: true };
1518
+ try {
1519
+ return await this.sessionInitializationContext.run(context, callback);
1520
+ } finally {
1521
+ if (context) context.active = false;
1522
+ }
1523
+ }
1524
+ async acquireSessionInitialization(sessionId, signal) {
1525
+ signal.throwIfAborted();
1526
+ const predecessors = [...this.sessionInitializations].filter(([id]) => sessionIdsOverlap(id, sessionId)).map(([, pending]) => pending);
1527
+ const previous = predecessors.length ? Promise.all(predecessors).then(() => {
1528
+ }) : void 0;
1529
+ const context = this.sessionInitializationContext.getStore();
1530
+ if (previous && context?.active && sessionIdsOverlap(context.sessionId, sessionId)) {
1531
+ throw new Error(
1532
+ `Cannot re-enter session initialization for ${sessionId} from its callback`
1533
+ );
1306
1534
  }
1535
+ let finish;
1536
+ const current = new Promise((resolve2) => {
1537
+ finish = resolve2;
1538
+ });
1539
+ const tail = previous ? previous.then(() => current) : current;
1540
+ this.sessionInitializations.set(sessionId, tail);
1541
+ void tail.then(() => {
1542
+ if (this.sessionInitializations.get(sessionId) === tail) {
1543
+ this.sessionInitializations.delete(sessionId);
1544
+ }
1545
+ });
1546
+ try {
1547
+ if (previous) {
1548
+ let cancel;
1549
+ try {
1550
+ await new Promise((resolve2, reject) => {
1551
+ cancel = () => reject(signal.reason);
1552
+ signal.addEventListener("abort", cancel, { once: true });
1553
+ void previous.then(resolve2);
1554
+ });
1555
+ } finally {
1556
+ signal.removeEventListener("abort", cancel);
1557
+ }
1558
+ }
1559
+ signal.throwIfAborted();
1560
+ const cleanupError = this.connection ? [...this.failedAutoModeRecoveryCleanups.get(this.connection) ?? []].find(
1561
+ ([id]) => sessionIdsOverlap(id, sessionId)
1562
+ )?.[1] : void 0;
1563
+ if (cleanupError) {
1564
+ throw new Error(
1565
+ `Cannot initialize session ${sessionId} after failed Auto recovery cleanup; stop and restart the client before retrying`,
1566
+ { cause: cleanupError }
1567
+ );
1568
+ }
1569
+ return finish;
1570
+ } catch (error) {
1571
+ finish();
1572
+ throw error;
1573
+ }
1574
+ }
1575
+ async createSessionInternal(config, signal) {
1576
+ signal.throwIfAborted();
1577
+ const connection = this.connection;
1578
+ const storeMemory = requestedMemoryStore(config.memory);
1307
1579
  const modeDefaults = this.configDefaultsForMode();
1308
1580
  config = { ...modeDefaults, ...config };
1309
1581
  config.customAgentsLocalOnly ??= modeDefaults.customAgentsLocalOnly;
@@ -1324,20 +1596,13 @@ class CopilotClient {
1324
1596
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
1325
1597
  config.systemMessage
1326
1598
  );
1599
+ let replacedSession;
1327
1600
  const initializeSession = (sessionId) => {
1328
- const s = new CopilotSession(
1329
- sessionId,
1330
- this.connection,
1331
- void 0,
1332
- this.onGetTraceContext,
1333
- {
1334
- mcpAuthHandler: config.onMcpAuthRequest,
1335
- managedSettingsEnabled: config.enableManagedSettings === true || config.managedSettings !== void 0,
1336
- onDisconnected: gitHubTokenProviderRegistrationId === void 0 ? void 0 : () => this.githubTokenProviders.delete(
1337
- gitHubTokenProviderRegistrationId
1338
- )
1339
- }
1340
- );
1601
+ const s = new CopilotSession(sessionId, connection, void 0, this.onGetTraceContext, {
1602
+ mcpAuthHandler: config.onMcpAuthRequest,
1603
+ managedSettingsEnabled: config.enableManagedSettings === true || config.managedSettings !== void 0,
1604
+ onDisconnected: gitHubTokenProviderRegistrationId === void 0 ? void 0 : () => this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId)
1605
+ });
1341
1606
  s.registerTools(config.tools);
1342
1607
  s.registerCanvases(config.canvases);
1343
1608
  s.registerCommands(config.commands);
@@ -1363,6 +1628,7 @@ class CopilotClient {
1363
1628
  if (config.skillProvider) {
1364
1629
  s.registerSkillProvider(config.skillProvider);
1365
1630
  }
1631
+ s._registerMemoryStore(storeMemory);
1366
1632
  if (transformCallbacks) {
1367
1633
  s.registerTransformCallbacks(transformCallbacks);
1368
1634
  }
@@ -1370,12 +1636,15 @@ class CopilotClient {
1370
1636
  s.on(config.onEvent);
1371
1637
  }
1372
1638
  this.setupSessionFs(s, config);
1639
+ signal.throwIfAborted();
1640
+ replacedSession = this.sessions.get(sessionId);
1373
1641
  this.sessions.set(sessionId, s);
1374
1642
  return s;
1375
1643
  };
1376
1644
  let session;
1377
1645
  let registeredId;
1378
1646
  let uninitializedCloudSessionId;
1647
+ let releaseCloudInitialization;
1379
1648
  if (localSessionId !== void 0) {
1380
1649
  try {
1381
1650
  session = initializeSession(localSessionId);
@@ -1388,14 +1657,20 @@ class CopilotClient {
1388
1657
  }
1389
1658
  }
1390
1659
  try {
1391
- const response = await this.connection.sendRequest("session.create", {
1392
- ...await getTraceContext(this.onGetTraceContext),
1660
+ const traceContext = await this.runInitializationCallback(
1661
+ localSessionId,
1662
+ () => getTraceContext(this.onGetTraceContext)
1663
+ );
1664
+ signal.throwIfAborted();
1665
+ const response = await connection.sendRequest("session.create", {
1666
+ ...traceContext,
1393
1667
  model: config.model,
1394
1668
  allowedModels: config.allowedModels,
1395
1669
  sessionId: localSessionId,
1396
1670
  name: config.name,
1397
1671
  clientName: config.clientName,
1398
1672
  reasoningEffort: config.reasoningEffort,
1673
+ allowMultipleToolCalls: config.allowMultipleToolCalls,
1399
1674
  reasoningSummary: config.reasoningSummary,
1400
1675
  isExperimentalMode: this.experimentalModeForMode(config.enableExperimentalMode),
1401
1676
  contextTier: config.contextTier,
@@ -1446,6 +1721,7 @@ class CopilotClient {
1446
1721
  ...config.githubMcpToolConfig != null ? { githubMcpToolConfig: config.githubMcpToolConfig } : {},
1447
1722
  requestExitPlanMode: !!config.onExitPlanModeRequest,
1448
1723
  requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
1724
+ ...config.autoModeRecovery ? { requestAutoModeRecovery: true } : {},
1449
1725
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
1450
1726
  workingDirectory: config.workingDirectory,
1451
1727
  trustWorkingDirectory: config.trustWorkingDirectory,
@@ -1476,6 +1752,7 @@ class CopilotClient {
1476
1752
  enableSkills: config.enableSkills,
1477
1753
  skillDirectories: config.skillDirectories,
1478
1754
  ...config.skillProvider ? { hasSkillProvider: true } : {},
1755
+ ...storeMemory ? { hasMemoryStore: true } : {},
1479
1756
  agentDirectories: config.agentDirectories,
1480
1757
  hookDirectories: config.hookDirectories,
1481
1758
  pluginDirectories: config.pluginDirectories,
@@ -1484,7 +1761,7 @@ class CopilotClient {
1484
1761
  disabledMcpServers: config.disabledMcpServers,
1485
1762
  sandboxConfig: config.sandboxConfig,
1486
1763
  infiniteSessions: config.infiniteSessions,
1487
- memory: config.memory,
1764
+ memory: toWireMemoryConfiguration(config.memory),
1488
1765
  gitHubToken: config.gitHubToken,
1489
1766
  gitHubTokenProviderRegistrationId,
1490
1767
  cloud: config.cloud,
@@ -1494,12 +1771,14 @@ class CopilotClient {
1494
1771
  excludedManagedSettings: config.excludedManagedSettings,
1495
1772
  enforceManagedModelDefaults: config.enforceManagedModelDefaults,
1496
1773
  interactiveUser: config.interactiveUser,
1774
+ permissionMode: config.permissionMode,
1497
1775
  managedSettings: config.managedSettings
1498
1776
  });
1499
1777
  const {
1500
1778
  sessionId: returnedSessionId,
1501
1779
  workspacePath,
1502
- capabilities
1780
+ capabilities,
1781
+ permissionModeResult
1503
1782
  } = response;
1504
1783
  if (!returnedSessionId) {
1505
1784
  throw new Error("session.create response did not include a sessionId");
@@ -1511,25 +1790,47 @@ class CopilotClient {
1511
1790
  }
1512
1791
  if (session === void 0) {
1513
1792
  uninitializedCloudSessionId = returnedSessionId;
1793
+ }
1794
+ signal.throwIfAborted();
1795
+ if (session === void 0) {
1796
+ releaseCloudInitialization = await this.acquireSessionInitialization(
1797
+ returnedSessionId,
1798
+ signal
1799
+ );
1800
+ signal.throwIfAborted();
1514
1801
  session = initializeSession(returnedSessionId);
1515
- uninitializedCloudSessionId = void 0;
1516
1802
  registeredId = returnedSessionId;
1517
1803
  }
1518
1804
  this.assignGitHubTokenProvider(gitHubTokenProviderRegistrationId, returnedSessionId);
1805
+ session.setCapabilities(capabilities);
1806
+ if (config.autoModeRecovery) {
1807
+ await this.requireAutoModeRecovery(session, connection);
1808
+ }
1519
1809
  if (config.onMcpAuthRequest) {
1520
- await this.connection.sendRequest("session.eventLog.registerInterest", {
1810
+ await connection.sendRequest("session.eventLog.registerInterest", {
1521
1811
  sessionId: returnedSessionId,
1522
1812
  eventType: "mcp.oauth_required"
1523
1813
  });
1524
1814
  }
1525
1815
  session["_workspacePath"] = workspacePath;
1526
- session.setCapabilities(capabilities);
1816
+ session.setPermissionModeResult(permissionModeResult);
1527
1817
  await this.updateSessionOptionsForMode(session, config);
1818
+ signal.throwIfAborted();
1819
+ uninitializedCloudSessionId = void 0;
1528
1820
  this.commitGitHubTokenProvider(returnedSessionId, gitHubTokenProviderRegistrationId);
1529
1821
  } catch (e) {
1530
1822
  session?._markDisconnected();
1531
- if (registeredId !== void 0) {
1532
- this.sessions.delete(registeredId);
1823
+ if (registeredId !== void 0 && session !== void 0) {
1824
+ const current = this.sessions.get(registeredId);
1825
+ if (current === session) {
1826
+ if (replacedSession && !replacedSession._isDisconnected()) {
1827
+ this.sessions.set(registeredId, replacedSession);
1828
+ } else {
1829
+ this.sessions.delete(registeredId);
1830
+ }
1831
+ } else if (current === void 0) {
1832
+ replacedSession?._markDisconnected();
1833
+ }
1533
1834
  }
1534
1835
  if (gitHubTokenProviderRegistrationId !== void 0) {
1535
1836
  this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId);
@@ -1537,18 +1838,33 @@ class CopilotClient {
1537
1838
  if (uninitializedCloudSessionId !== void 0) {
1538
1839
  try {
1539
1840
  await withTimeout(
1540
- this.deleteSession(uninitializedCloudSessionId),
1841
+ this.requestSessionDeletion(connection, uninitializedCloudSessionId),
1541
1842
  CLOUD_SESSION_CLEANUP_TIMEOUT_MS,
1542
1843
  `session.delete timed out after ${CLOUD_SESSION_CLEANUP_TIMEOUT_MS}ms`
1543
1844
  );
1544
1845
  } catch (cleanupError) {
1545
- throw new AggregateError(
1846
+ const failure = new AggregateError(
1546
1847
  [e, cleanupError],
1547
- "Failed to initialize and delete cloud session"
1848
+ `Failed to initialize and delete cloud session ${uninitializedCloudSessionId}`
1548
1849
  );
1850
+ if (config.autoModeRecovery) {
1851
+ this.recordFailedAutoModeRecoveryCleanup(
1852
+ connection,
1853
+ uninitializedCloudSessionId,
1854
+ failure
1855
+ );
1856
+ }
1857
+ throw failure;
1549
1858
  }
1550
1859
  }
1551
1860
  throw e;
1861
+ } finally {
1862
+ releaseCloudInitialization?.();
1863
+ }
1864
+ if (session._isDisconnected()) {
1865
+ this.forgetSession(session);
1866
+ } else {
1867
+ session._setReleaseFromClient(this.forgetSession.bind(this, session));
1552
1868
  }
1553
1869
  for (const entry of this.hostHandoffs.values()) {
1554
1870
  if (entry.requestedSessionId === session.sessionId) {
@@ -1589,16 +1905,52 @@ class CopilotClient {
1589
1905
  return this.resumeSessionInternal(sessionId, config, contributions, extensionOptions);
1590
1906
  }
1591
1907
  async resumeSessionInternal(sessionId, config, contributions = {}, extensionOptions) {
1592
- const { workflows } = contributions;
1908
+ if (config.autoModeRecovery && typeof config.onAutoModeSwitchRequest !== "function") {
1909
+ throw new Error("autoModeRecovery requires onAutoModeSwitchRequest");
1910
+ }
1593
1911
  if (config.gitHubToken !== void 0 && config.gitHubTokenProvider !== void 0) {
1594
1912
  throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive");
1595
1913
  }
1596
- if (!this.connection || this.startPromise) {
1597
- await this.start();
1914
+ requestedMemoryStore(config.memory);
1915
+ const signal = await this.getSessionInitializationSignal();
1916
+ const release = await this.acquireSessionInitialization(sessionId, signal);
1917
+ try {
1918
+ signal.throwIfAborted();
1919
+ return await this.resumeSessionWithInitialization(
1920
+ sessionId,
1921
+ config,
1922
+ contributions,
1923
+ signal,
1924
+ extensionOptions
1925
+ );
1926
+ } finally {
1927
+ release();
1928
+ }
1929
+ }
1930
+ async resumeSessionWithInitialization(sessionId, config, contributions, signal, extensionOptions) {
1931
+ const { workflows } = contributions;
1932
+ const storeMemory = requestedMemoryStore(config.memory);
1933
+ signal.throwIfAborted();
1934
+ const connection = this.connection;
1935
+ const resolveAliases = config.autoModeRecovery === true && !this.sessionFsConfig;
1936
+ if (resolveAliases && isAbbreviatedUuidPrefix(sessionId)) {
1937
+ const resolved = await connection.sendRequest(
1938
+ "sessions.findByPrefix",
1939
+ { prefix: sessionId, configDir: config.configDirectory }
1940
+ );
1941
+ const canonicalId = resolved?.sessionId;
1942
+ if (typeof canonicalId !== "string" || canonicalId.length === 0) {
1943
+ throw new Error(`No unique session matches prefix ${sessionId}`);
1944
+ }
1945
+ if (!canonicalId.toLowerCase().startsWith(sessionId.toLowerCase())) {
1946
+ throw new Error(`Runtime returned an unrelated session ID for prefix ${sessionId}`);
1947
+ }
1948
+ sessionId = canonicalId;
1949
+ signal.throwIfAborted();
1598
1950
  }
1599
1951
  const session = new CopilotSession(
1600
1952
  sessionId,
1601
- this.connection,
1953
+ connection,
1602
1954
  void 0,
1603
1955
  this.onGetTraceContext,
1604
1956
  {
@@ -1637,6 +1989,7 @@ class CopilotClient {
1637
1989
  if (config.skillProvider) {
1638
1990
  session.registerSkillProvider(config.skillProvider);
1639
1991
  }
1992
+ session._registerMemoryStore(storeMemory);
1640
1993
  const modeDefaults = this.configDefaultsForMode();
1641
1994
  config = { ...modeDefaults, ...config };
1642
1995
  config.customAgentsLocalOnly ??= modeDefaults.customAgentsLocalOnly;
@@ -1652,8 +2005,12 @@ class CopilotClient {
1652
2005
  }
1653
2006
  const toolFilterOptions = this.resolveToolFilterOptions(config);
1654
2007
  this.setupSessionFs(session, config);
1655
- const replacedSession = this.sessions.get(sessionId);
1656
- this.sessions.set(sessionId, session);
2008
+ signal.throwIfAborted();
2009
+ const requestedSessionId = sessionId;
2010
+ let replacedRequestedSession;
2011
+ let replacedSession;
2012
+ let releaseCanonicalInitialization;
2013
+ let keepResumedSessionOnRecoveryRejection = false;
1657
2014
  const gitHubTokenProviderRegistrationId = this.registerGitHubTokenProvider(
1658
2015
  config.gitHubTokenProvider,
1659
2016
  sessionId
@@ -1664,14 +2021,29 @@ class CopilotClient {
1664
2021
  );
1665
2022
  }
1666
2023
  try {
1667
- const response = await this.connection.sendRequest("session.resume", {
2024
+ const traceContext = await this.runInitializationCallback(
2025
+ sessionId,
2026
+ () => getTraceContext(this.onGetTraceContext)
2027
+ );
2028
+ signal.throwIfAborted();
2029
+ replacedRequestedSession = this.sessions.get(sessionId);
2030
+ replacedSession = replacedRequestedSession;
2031
+ this.sessions.set(sessionId, session);
2032
+ this.pendingSessionResumes.set(requestedSessionId, {
2033
+ session,
2034
+ connection,
2035
+ gitHubTokenProviderRegistrationId,
2036
+ resolveAliases
2037
+ });
2038
+ const response = await connection.sendRequest("session.resume", {
1668
2039
  allowedModels: config.allowedModels,
1669
- ...await getTraceContext(this.onGetTraceContext),
2040
+ ...traceContext,
1670
2041
  sessionId,
1671
2042
  allowTranscriptRecovery: config.allowTranscriptRecovery,
1672
2043
  clientName: config.clientName,
1673
2044
  model: config.model,
1674
2045
  reasoningEffort: config.reasoningEffort,
2046
+ allowMultipleToolCalls: config.allowMultipleToolCalls,
1675
2047
  reasoningSummary: config.reasoningSummary,
1676
2048
  isExperimentalMode: this.experimentalModeForMode(config.enableExperimentalMode),
1677
2049
  contextTier: config.contextTier,
@@ -1723,6 +2095,7 @@ class CopilotClient {
1723
2095
  ...config.githubMcpToolConfig != null ? { githubMcpToolConfig: config.githubMcpToolConfig } : {},
1724
2096
  requestExitPlanMode: !!config.onExitPlanModeRequest,
1725
2097
  requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
2098
+ ...config.autoModeRecovery ? { requestAutoModeRecovery: true } : {},
1726
2099
  hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
1727
2100
  workingDirectory: config.workingDirectory,
1728
2101
  additionalDirectories: config.additionalDirectories,
@@ -1751,6 +2124,7 @@ class CopilotClient {
1751
2124
  agent: config.agent,
1752
2125
  skillDirectories: config.skillDirectories,
1753
2126
  ...config.skillProvider ? { hasSkillProvider: true } : {},
2127
+ ...storeMemory ? { hasMemoryStore: true } : {},
1754
2128
  agentDirectories: config.agentDirectories,
1755
2129
  hookDirectories: config.hookDirectories,
1756
2130
  pluginDirectories: config.pluginDirectories,
@@ -1759,7 +2133,7 @@ class CopilotClient {
1759
2133
  disabledMcpServers: config.disabledMcpServers,
1760
2134
  sandboxConfig: config.sandboxConfig,
1761
2135
  infiniteSessions: config.infiniteSessions,
1762
- memory: config.memory,
2136
+ memory: toWireMemoryConfiguration(config.memory),
1763
2137
  disableResume: config.suppressResumeEvent,
1764
2138
  continuePendingWork: config.continuePendingWork,
1765
2139
  gitHubToken: config.gitHubToken,
@@ -1771,11 +2145,34 @@ class CopilotClient {
1771
2145
  excludedManagedSettings: config.excludedManagedSettings,
1772
2146
  enforceManagedModelDefaults: config.enforceManagedModelDefaults,
1773
2147
  interactiveUser: config.interactiveUser,
2148
+ permissionMode: config.permissionMode,
1774
2149
  managedSettings: config.managedSettings,
1775
2150
  ...extensionOptions?.requestedEnvironmentVariables ? {
1776
2151
  requestedEnvironmentVariables: extensionOptions.requestedEnvironmentVariables
1777
2152
  } : {}
1778
2153
  });
2154
+ const { sessionId: returnedSessionId } = response;
2155
+ if (returnedSessionId && returnedSessionId !== sessionId) {
2156
+ if (!sessionIdsOverlap(requestedSessionId, returnedSessionId)) {
2157
+ releaseCanonicalInitialization = await this.acquireSessionInitialization(
2158
+ returnedSessionId,
2159
+ signal
2160
+ );
2161
+ }
2162
+ replacedSession = this.sessions.get(returnedSessionId);
2163
+ if (this.sessions.get(requestedSessionId) === session) {
2164
+ if (replacedRequestedSession) {
2165
+ this.sessions.set(requestedSessionId, replacedRequestedSession);
2166
+ } else {
2167
+ this.sessions.delete(requestedSessionId);
2168
+ }
2169
+ }
2170
+ session._setSessionId(returnedSessionId);
2171
+ sessionId = returnedSessionId;
2172
+ this.sessions.set(sessionId, session);
2173
+ this.assignGitHubTokenProvider(gitHubTokenProviderRegistrationId, sessionId);
2174
+ }
2175
+ signal.throwIfAborted();
1779
2176
  if (extensionOptions?.requestedEnvironmentVariables) {
1780
2177
  const requested = new Set(extensionOptions.requestedEnvironmentVariables);
1781
2178
  const { grantedEnvironmentVariables } = response;
@@ -1785,32 +2182,67 @@ class CopilotClient {
1785
2182
  }
1786
2183
  }
1787
2184
  }
1788
- const { workspacePath, capabilities, openCanvases, transcriptRecovery } = response;
2185
+ const {
2186
+ workspacePath,
2187
+ capabilities,
2188
+ openCanvases,
2189
+ transcriptRecovery,
2190
+ permissionModeResult
2191
+ } = response;
1789
2192
  session["_workspacePath"] = workspacePath;
1790
2193
  session.setCapabilities(capabilities);
2194
+ if (config.autoModeRecovery) {
2195
+ keepResumedSessionOnRecoveryRejection = replacedSession !== void 0 && session.capabilities.ui?.autoModeRecovery !== true;
2196
+ await this.requireAutoModeRecovery(
2197
+ session,
2198
+ connection,
2199
+ replacedSession === void 0
2200
+ );
2201
+ }
1791
2202
  session.setOpenCanvases(openCanvases ?? []);
1792
2203
  session.setTranscriptRecovery(transcriptRecovery);
2204
+ session.setPermissionModeResult(permissionModeResult);
1793
2205
  if (config.onMcpAuthRequest) {
1794
- await this.connection.sendRequest("session.eventLog.registerInterest", {
2206
+ await connection.sendRequest("session.eventLog.registerInterest", {
1795
2207
  sessionId,
1796
2208
  eventType: "mcp.oauth_required"
1797
2209
  });
1798
2210
  }
1799
2211
  await this.updateSessionOptionsForMode(session, config);
2212
+ signal.throwIfAborted();
1800
2213
  this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId);
1801
2214
  } catch (e) {
1802
- session._markDisconnected();
1803
- if (this.sessions.get(sessionId) === session) {
1804
- if (replacedSession) {
2215
+ if (!keepResumedSessionOnRecoveryRejection) {
2216
+ session._markDisconnected();
2217
+ }
2218
+ const current = this.sessions.get(sessionId);
2219
+ if (current === session) {
2220
+ if (replacedSession && !replacedSession._isDisconnected() && !keepResumedSessionOnRecoveryRejection) {
1805
2221
  this.sessions.set(sessionId, replacedSession);
1806
- } else {
2222
+ } else if (!keepResumedSessionOnRecoveryRejection) {
1807
2223
  this.sessions.delete(sessionId);
1808
2224
  }
2225
+ } else if (current === void 0) {
2226
+ replacedSession?._markDisconnected();
1809
2227
  }
1810
2228
  if (gitHubTokenProviderRegistrationId !== void 0) {
1811
- this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId);
2229
+ if (keepResumedSessionOnRecoveryRejection) {
2230
+ this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId);
2231
+ } else {
2232
+ this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId);
2233
+ }
1812
2234
  }
1813
2235
  throw e;
2236
+ } finally {
2237
+ if (this.pendingSessionResumes.get(requestedSessionId)?.session === session) {
2238
+ this.pendingSessionResumes.delete(requestedSessionId);
2239
+ }
2240
+ releaseCanonicalInitialization?.();
2241
+ }
2242
+ if (session._isDisconnected()) {
2243
+ this.forgetSession(session);
2244
+ } else {
2245
+ session._setReleaseFromClient(this.forgetSession.bind(this, session));
1814
2246
  }
1815
2247
  for (const entry of this.hostHandoffs.values()) {
1816
2248
  if (entry.requestedSessionId === session.sessionId) {
@@ -1819,6 +2251,47 @@ class CopilotClient {
1819
2251
  }
1820
2252
  return session;
1821
2253
  }
2254
+ async requireAutoModeRecovery(session, connection, detachOnFailure = true) {
2255
+ if (session.capabilities.ui?.autoModeRecovery === true) {
2256
+ return;
2257
+ }
2258
+ const error = new Error("The runtime does not support autoModeRecovery");
2259
+ if (!detachOnFailure) {
2260
+ throw error;
2261
+ }
2262
+ const cancellation = new CancellationTokenSource();
2263
+ try {
2264
+ const response = await withTimeout(
2265
+ connection.sendRequest(
2266
+ "session.detach",
2267
+ { sessionId: session.sessionId },
2268
+ cancellation.token
2269
+ ),
2270
+ AUTO_MODE_RECOVERY_CLEANUP_TIMEOUT_MS,
2271
+ `session.detach timed out after ${AUTO_MODE_RECOVERY_CLEANUP_TIMEOUT_MS}ms`
2272
+ );
2273
+ if (!response.success) {
2274
+ throw new Error(`Failed to detach session: ${response.error || "Unknown error"}`);
2275
+ }
2276
+ } catch (cleanupError) {
2277
+ const failure = new AggregateError([error, cleanupError], error.message);
2278
+ this.recordFailedAutoModeRecoveryCleanup(connection, session.sessionId, failure);
2279
+ throw failure;
2280
+ } finally {
2281
+ cancellation.cancel();
2282
+ cancellation.dispose();
2283
+ session._markDisconnected();
2284
+ }
2285
+ throw error;
2286
+ }
2287
+ recordFailedAutoModeRecoveryCleanup(connection, sessionId, failure) {
2288
+ let failed = this.failedAutoModeRecoveryCleanups.get(connection);
2289
+ if (!failed) {
2290
+ failed = /* @__PURE__ */ new Map();
2291
+ this.failedAutoModeRecoveryCleanups.set(connection, failed);
2292
+ }
2293
+ failed.set(sessionId, failure);
2294
+ }
1822
2295
  /**
1823
2296
  * Start a complete AHP listener hosted in-process by this runtime.
1824
2297
  *
@@ -1930,6 +2403,12 @@ class CopilotClient {
1930
2403
  }
1931
2404
  }
1932
2405
  }
2406
+ abortPendingMemoryStores() {
2407
+ for (const controller of this.pendingMemoryStores) {
2408
+ controller.abort();
2409
+ }
2410
+ this.pendingMemoryStores.clear();
2411
+ }
1933
2412
  disconnectHosts() {
1934
2413
  for (const hostId of /* @__PURE__ */ new Set([
1935
2414
  ...this.hostExitCallbacks.keys(),
@@ -2261,16 +2740,39 @@ class CopilotClient {
2261
2740
  if (!this.connection) {
2262
2741
  throw new Error("Client not connected");
2263
2742
  }
2264
- const response = await this.connection.sendRequest("session.delete", {
2265
- sessionId
2266
- });
2743
+ const signal = await this.getSessionInitializationSignal();
2744
+ const release = await this.acquireSessionInitialization(sessionId, signal);
2745
+ try {
2746
+ signal.throwIfAborted();
2747
+ const connection = this.connection;
2748
+ const session = this.sessions.get(sessionId);
2749
+ await this.requestSessionDeletion(connection, sessionId);
2750
+ signal.throwIfAborted();
2751
+ if (this.sessions.get(sessionId) === session) {
2752
+ this.sessions.delete(sessionId);
2753
+ session?._markDisconnected();
2754
+ }
2755
+ } finally {
2756
+ release();
2757
+ }
2758
+ }
2759
+ async requestSessionDeletion(connection, sessionId) {
2760
+ const response = await connection.sendRequest(
2761
+ "session.delete",
2762
+ {
2763
+ sessionId
2764
+ }
2765
+ );
2267
2766
  const { success, error } = response;
2268
2767
  if (!success) {
2269
2768
  throw new Error(`Failed to delete session ${sessionId}: ${error || "Unknown error"}`);
2270
2769
  }
2271
- const session = this.sessions.get(sessionId);
2272
- this.sessions.delete(sessionId);
2273
- session?._runOnDisconnected();
2770
+ }
2771
+ /** Stops tracking a disconnected session unless a later create or resume replaced it. */
2772
+ forgetSession(session) {
2773
+ if (this.sessions.get(session.sessionId) === session) {
2774
+ this.sessions.delete(session.sessionId);
2775
+ }
2274
2776
  }
2275
2777
  /**
2276
2778
  * List all available sessions.
@@ -2727,7 +3229,8 @@ stderr: ${this.stderrBuffer.trim().slice(-8192) || "(empty)"}`
2727
3229
  this.messageWriter = new TeardownResilientStreamMessageWriter(this.ffiHost.sendStream);
2728
3230
  this.connection = createMessageConnection(
2729
3231
  new StreamMessageReader(this.ffiHost.receiveStream),
2730
- this.messageWriter
3232
+ this.messageWriter,
3233
+ this.sessionInitializationContext
2731
3234
  );
2732
3235
  this.attachConnectionHandlers();
2733
3236
  this.connection.listen();
@@ -2787,7 +3290,8 @@ stderr: ${stderrOutput}` : ""}`
2787
3290
  this.messageWriter = new TeardownResilientStreamMessageWriter(this.cliProcess.stdin);
2788
3291
  this.connection = createMessageConnection(
2789
3292
  new StreamMessageReader(this.cliProcess.stdout),
2790
- this.messageWriter
3293
+ this.messageWriter,
3294
+ this.sessionInitializationContext
2791
3295
  );
2792
3296
  this.attachConnectionHandlers();
2793
3297
  this.connection.listen();
@@ -2802,7 +3306,8 @@ stderr: ${stderrOutput}` : ""}`
2802
3306
  this.messageWriter = new TeardownResilientStreamMessageWriter(process.stdout);
2803
3307
  this.connection = createMessageConnection(
2804
3308
  new StreamMessageReader(process.stdin),
2805
- this.messageWriter
3309
+ this.messageWriter,
3310
+ this.sessionInitializationContext
2806
3311
  );
2807
3312
  this.attachConnectionHandlers();
2808
3313
  this.connection.listen();
@@ -2825,7 +3330,8 @@ stderr: ${stderrOutput}` : ""}`
2825
3330
  this.messageWriter = new TeardownResilientStreamMessageWriter(this.socket);
2826
3331
  this.connection = createMessageConnection(
2827
3332
  new StreamMessageReader(this.socket),
2828
- this.messageWriter
3333
+ this.messageWriter,
3334
+ this.sessionInitializationContext
2829
3335
  );
2830
3336
  this.attachConnectionHandlers();
2831
3337
  this.connection.listen();
@@ -2863,9 +3369,8 @@ stderr: ${stderrOutput}` : ""}`
2863
3369
  "systemMessage.transform",
2864
3370
  async (params) => await this.handleSystemMessageTransform(params)
2865
3371
  );
2866
- const sessions = this.sessions;
2867
3372
  registerClientSessionApiHandlers(this.connection, (sessionId) => {
2868
- const session = sessions.get(sessionId);
3373
+ const session = this.resolveSessionCallback(sessionId);
2869
3374
  if (!session) throw new Error(`No session found for sessionId: ${sessionId}`);
2870
3375
  return session.clientSessionApis;
2871
3376
  });
@@ -2883,12 +3388,20 @@ stderr: ${stderrOutput}` : ""}`
2883
3388
  (params, token) => confirmation.confirm(params, token)
2884
3389
  );
2885
3390
  }
2886
- if (this.kerberosAuth) {
2887
- const { callback } = this.kerberosAuth;
3391
+ const resolveProxy = this.network?.resolveProxy;
3392
+ if (resolveProxy) {
3393
+ const connection2 = this.connection;
3394
+ this.connection.onRequest(
3395
+ "network.resolveProxy",
3396
+ (params, token) => resolveClientProxy(connection2, resolveProxy, params, token)
3397
+ );
3398
+ }
3399
+ const authenticateNegotiate = this.network?.authenticateNegotiate;
3400
+ if (authenticateNegotiate) {
2888
3401
  const connection2 = this.connection;
2889
3402
  this.connection.onRequest(
2890
- "kerberosAuth.acquire",
2891
- (params, token) => acquireKerberosToken(connection2, callback, params, token)
3403
+ "network.authenticateNegotiate",
3404
+ (params, token) => authenticateClientNegotiate(connection2, authenticateNegotiate, params, token)
2892
3405
  );
2893
3406
  }
2894
3407
  this.connection.onRequest(
@@ -2959,11 +3472,29 @@ stderr: ${stderrOutput}` : ""}`
2959
3472
  this.connection.onRequest(
2960
3473
  "sessionManagement.create",
2961
3474
  async (params, token) => {
2962
- const session = this.sessions.get(params.sessionId);
3475
+ const session = this.resolveSessionCallback(params.sessionId);
2963
3476
  if (!session) throw new Error("Session creation owner is unavailable");
2964
3477
  return await session._handleSessionManagementCreate(params, token);
2965
3478
  }
2966
3479
  );
3480
+ this.connection.onRequest(
3481
+ "memoryStore.store",
3482
+ async (params, token) => {
3483
+ const session = this.sessions.get(params?.sessionId);
3484
+ if (!session) throw new Error("Memory store owner is unavailable");
3485
+ const controller = new AbortController();
3486
+ this.pendingMemoryStores.add(controller);
3487
+ const subscription = token.onCancellationRequested(() => controller.abort());
3488
+ if (token.isCancellationRequested) controller.abort();
3489
+ try {
3490
+ await session._handleMemoryStore(params, controller.signal);
3491
+ } finally {
3492
+ this.pendingMemoryStores.delete(controller);
3493
+ subscription.dispose();
3494
+ }
3495
+ return null;
3496
+ }
3497
+ );
2967
3498
  this.connection.onRequest(
2968
3499
  "skillProvider.read",
2969
3500
  async (params, token) => {
@@ -2995,6 +3526,7 @@ stderr: ${stderrOutput}` : ""}`
2995
3526
  this.sessions.clear();
2996
3527
  this.githubTokenProviders.clear();
2997
3528
  this.requestAdapter?.cancelPending();
3529
+ this.abortPendingMemoryStores();
2998
3530
  connection.dispose();
2999
3531
  this.socket?.destroy();
3000
3532
  this.connection = null;
@@ -3014,6 +3546,7 @@ stderr: ${stderrOutput}` : ""}`
3014
3546
  }
3015
3547
  if (disconnecting) return;
3016
3548
  disconnecting = true;
3549
+ this.invalidateSessionInitializations();
3017
3550
  this.connectionClosed = true;
3018
3551
  if (messageWriter) messageWriter.suppressWriteErrors = true;
3019
3552
  void connection.drain().then(markDisconnected);
@@ -3045,11 +3578,34 @@ stderr: ${stderrOutput}` : ""}`
3045
3578
  }
3046
3579
  });
3047
3580
  }
3581
+ resolveSessionCallback(sessionId) {
3582
+ let selected;
3583
+ let selectedPrefixLength = -1;
3584
+ for (const [prefix, pending] of this.pendingSessionResumes) {
3585
+ if (pending.connection !== this.connection || !pending.resolveAliases && prefix !== sessionId || prefix.length <= selectedPrefixLength || !(sessionIdsOverlap(prefix, sessionId) && sessionId.length >= prefix.length))
3586
+ continue;
3587
+ const knownOwners = new Set(
3588
+ [...this.sessions.values()].filter(
3589
+ (owner) => owner !== pending.session && sessionIdsOverlap(prefix, owner.sessionId) && owner.sessionId.length >= prefix.length
3590
+ )
3591
+ );
3592
+ if (knownOwners.size > 1) continue;
3593
+ if (pending.session.sessionId !== sessionId) {
3594
+ pending.session._setSessionId(sessionId);
3595
+ }
3596
+ this.assignGitHubTokenProvider(pending.gitHubTokenProviderRegistrationId, sessionId);
3597
+ selected = pending.session;
3598
+ selectedPrefixLength = prefix.length;
3599
+ }
3600
+ return selected ?? this.sessions.get(sessionId);
3601
+ }
3048
3602
  handleSessionEventNotification(notification) {
3049
3603
  if (typeof notification !== "object" || !notification || !("sessionId" in notification) || typeof notification.sessionId !== "string" || !("event" in notification)) {
3050
3604
  return;
3051
3605
  }
3052
- const session = this.sessions.get(notification.sessionId);
3606
+ const session = this.resolveSessionCallback(
3607
+ notification.sessionId
3608
+ );
3053
3609
  const event = notification.event;
3054
3610
  if (session) {
3055
3611
  session._dispatchEvent(event);
@@ -3093,7 +3649,7 @@ stderr: ${stderrOutput}` : ""}`
3093
3649
  if (!params || typeof params.sessionId !== "string" || typeof params.question !== "string") {
3094
3650
  throw new Error("Invalid user input request payload");
3095
3651
  }
3096
- const session = this.sessions.get(params.sessionId);
3652
+ const session = this.resolveSessionCallback(params.sessionId);
3097
3653
  if (!session) {
3098
3654
  throw new Error(`Session not found: ${params.sessionId}`);
3099
3655
  }
@@ -3109,7 +3665,7 @@ stderr: ${stderrOutput}` : ""}`
3109
3665
  if (!params || typeof params.sessionId !== "string" || typeof params.summary !== "string" || !Array.isArray(params.actions) || typeof params.recommendedAction !== "string") {
3110
3666
  throw new Error("Invalid exit plan mode request payload");
3111
3667
  }
3112
- const session = this.sessions.get(params.sessionId);
3668
+ const session = this.resolveSessionCallback(params.sessionId);
3113
3669
  if (!session) {
3114
3670
  throw new Error(`Session not found: ${params.sessionId}`);
3115
3671
  }
@@ -3125,13 +3681,14 @@ stderr: ${stderrOutput}` : ""}`
3125
3681
  if (!params || typeof params.sessionId !== "string") {
3126
3682
  throw new Error("Invalid auto mode switch request payload");
3127
3683
  }
3128
- const session = this.sessions.get(params.sessionId);
3684
+ const session = this.resolveSessionCallback(params.sessionId);
3129
3685
  if (!session) {
3130
3686
  throw new Error(`Session not found: ${params.sessionId}`);
3131
3687
  }
3132
3688
  const response = await session._handleAutoModeSwitchRequest({
3133
3689
  errorCode: params.errorCode,
3134
- retryAfterSeconds: params.retryAfterSeconds
3690
+ retryAfterSeconds: params.retryAfterSeconds,
3691
+ ...params.autoModeRecovery === void 0 ? {} : { autoModeRecovery: params.autoModeRecovery }
3135
3692
  });
3136
3693
  return { response };
3137
3694
  }
@@ -3139,7 +3696,7 @@ stderr: ${stderrOutput}` : ""}`
3139
3696
  if (!params || typeof params.sessionId !== "string") {
3140
3697
  throw new Error("Invalid skillProvider payload");
3141
3698
  }
3142
- const session = this.sessions.get(params.sessionId);
3699
+ const session = this.resolveSessionCallback(params.sessionId);
3143
3700
  if (!session) {
3144
3701
  throw new Error(`Session not found: ${params.sessionId}`);
3145
3702
  }
@@ -3149,7 +3706,7 @@ stderr: ${stderrOutput}` : ""}`
3149
3706
  if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
3150
3707
  throw new Error("Invalid hooks invoke payload");
3151
3708
  }
3152
- const session = this.sessions.get(params.sessionId);
3709
+ const session = this.resolveSessionCallback(params.sessionId);
3153
3710
  if (!session) {
3154
3711
  throw new Error(`Session not found: ${params.sessionId}`);
3155
3712
  }
@@ -3160,7 +3717,7 @@ stderr: ${stderrOutput}` : ""}`
3160
3717
  if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
3161
3718
  throw new Error("Invalid systemMessage.transform payload");
3162
3719
  }
3163
- const session = this.sessions.get(params.sessionId);
3720
+ const session = this.resolveSessionCallback(params.sessionId);
3164
3721
  if (!session) {
3165
3722
  throw new Error(`Session not found: ${params.sessionId}`);
3166
3723
  }