@love-moon/conductor-cli 0.2.39 → 0.2.40

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.
@@ -37,6 +37,11 @@ const DEFAULT_CLIs = {
37
37
  execArgs: "",
38
38
  description: "OpenCode CLI (Conductor runs opencode serve with permission=allow)"
39
39
  },
40
+ copilot: {
41
+ command: "copilot",
42
+ execArgs: "",
43
+ description: "GitHub Copilot (built in via SDK)"
44
+ },
40
45
  };
41
46
 
42
47
  const backendUrl =
@@ -63,6 +68,13 @@ function colorize(text, color) {
63
68
  return `${COLORS[color] || ""}${text}${COLORS.reset}`;
64
69
  }
65
70
 
71
+ function isBuiltInCopilotAvailable() {
72
+ return Boolean(
73
+ packageJson?.dependencies?.["@github/copilot-sdk"] ||
74
+ packageJson?.optionalDependencies?.["@github/copilot-sdk"],
75
+ );
76
+ }
77
+
66
78
  function buildConfigEntryLines(cli, info, { commented = false } = {}) {
67
79
  const fullCommand = info.execArgs
68
80
  ? `${info.command} ${info.execArgs}`
@@ -278,6 +290,10 @@ function detectInstalledCLIs() {
278
290
  if (!RUNTIME_SUPPORTED_BACKENDS.includes(key)) {
279
291
  continue;
280
292
  }
293
+ if (key === "copilot" && isBuiltInCopilotAvailable()) {
294
+ detected.push(key);
295
+ continue;
296
+ }
281
297
  if (isCommandAvailable(info.command)) {
282
298
  detected.push(key);
283
299
  }
@@ -29,7 +29,9 @@ import {
29
29
  import {
30
30
  filterRuntimeSupportedAllowCliList,
31
31
  isBuiltInRuntimeBackend,
32
+ isCommandOptionalBuiltInRuntimeBackend,
32
33
  listAdvertisedBackends,
34
+ parseCommandParts,
33
35
  resolveConfiguredRuntimeBackend,
34
36
  normalizeRuntimeBackendAlias,
35
37
  normalizeRuntimeBackendName,
@@ -69,9 +71,20 @@ const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1
69
71
  "",
70
72
  );
71
73
 
72
- export function buildConductorConnectHeaders(version = pkgJson.version) {
74
+ export function buildConductorConnectHeaders(
75
+ version = pkgJson.version,
76
+ options = {},
77
+ ) {
78
+ const backends = Array.isArray(options.backends)
79
+ ? options.backends.map((entry) => String(entry || "").trim()).filter(Boolean)
80
+ : [];
81
+ const capabilities = Array.isArray(options.capabilities)
82
+ ? options.capabilities.map((entry) => String(entry || "").trim()).filter(Boolean)
83
+ : [];
73
84
  return {
74
85
  "x-conductor-version": version,
86
+ ...(backends.length > 0 ? { "x-conductor-backends": backends.join(",") } : {}),
87
+ ...(capabilities.length > 0 ? { "x-conductor-capabilities": capabilities.join(",") } : {}),
75
88
  };
76
89
  }
77
90
 
@@ -155,70 +168,8 @@ export function resolveConfiguredPrePrompt({ configFilePath, backend, sessionBac
155
168
  return undefined;
156
169
  }
157
170
 
158
- function parseCommandParts(commandLine) {
159
- const input = String(commandLine || "").trim();
160
- if (!input) {
161
- return [];
162
- }
163
-
164
- const parts = [];
165
- let current = "";
166
- let quote = "";
167
- let escaping = false;
168
- let tokenStarted = false;
169
-
170
- for (const char of input) {
171
- if (escaping) {
172
- current += char;
173
- tokenStarted = true;
174
- escaping = false;
175
- continue;
176
- }
177
-
178
- if (char === "\\") {
179
- escaping = true;
180
- tokenStarted = true;
181
- continue;
182
- }
183
-
184
- if (quote) {
185
- if (char === quote) {
186
- quote = "";
187
- } else {
188
- current += char;
189
- }
190
- tokenStarted = true;
191
- continue;
192
- }
193
-
194
- if (char === "'" || char === "\"") {
195
- quote = char;
196
- tokenStarted = true;
197
- continue;
198
- }
199
-
200
- if (/\s/.test(char)) {
201
- if (tokenStarted) {
202
- parts.push(current);
203
- current = "";
204
- tokenStarted = false;
205
- }
206
- continue;
207
- }
208
-
209
- current += char;
210
- tokenStarted = true;
211
- }
212
-
213
- if (tokenStarted) {
214
- parts.push(current);
215
- }
216
-
217
- return parts;
218
- }
219
-
220
171
  function extractModelOptionFromCommandLine(commandLine) {
221
- const parts = parseCommandParts(commandLine);
172
+ const { parts } = parseCommandParts(commandLine);
222
173
  for (let index = 0; index < parts.length; index += 1) {
223
174
  const token = String(parts[index] || "").trim();
224
175
  if (!token) {
@@ -664,7 +615,8 @@ async function main() {
664
615
  let resumeContext = null;
665
616
  if (cliArgs.resumeSessionId) {
666
617
  const bootstrap = await bootstrapResumeContextForFire({
667
- backend: cliArgs.sessionBackend || cliArgs.backend,
618
+ backend: cliArgs.backend,
619
+ sessionBackend: cliArgs.sessionBackend,
668
620
  configFile: cliArgs.configFile,
669
621
  resumeSessionId: cliArgs.resumeSessionId,
670
622
  });
@@ -678,6 +630,7 @@ async function main() {
678
630
  let reconnectTaskId = null;
679
631
  let pendingRemoteStopEvent = null;
680
632
  const pendingRemoteInterruptQueue = createPendingRemoteInterruptQueue();
633
+ const completedRefreshSessionRequests = new Map();
681
634
  let conductor = null;
682
635
  let reconnectResumeInFlight = false;
683
636
  let fireShuttingDown = false;
@@ -762,6 +715,41 @@ async function main() {
762
715
  return await pendingRemoteInterruptQueue.enqueue(event);
763
716
  };
764
717
 
718
+ const rememberCompletedRefreshSessionRequest = (requestId, accepted) => {
719
+ if (!requestId) {
720
+ return accepted;
721
+ }
722
+ completedRefreshSessionRequests.set(requestId, accepted);
723
+ while (completedRefreshSessionRequests.size > 20) {
724
+ const oldest = completedRefreshSessionRequests.keys().next();
725
+ if (oldest.done) {
726
+ break;
727
+ }
728
+ completedRefreshSessionRequests.delete(oldest.value);
729
+ }
730
+ return accepted;
731
+ };
732
+
733
+ const handleRefreshSessionCommand = async (event) => {
734
+ fireWatchdog.onInbound();
735
+ if (!event || typeof event !== "object") {
736
+ return false;
737
+ }
738
+ const taskId = typeof event.taskId === "string" ? event.taskId : "";
739
+ const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
740
+ if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
741
+ return false;
742
+ }
743
+ if (requestId && completedRefreshSessionRequests.has(requestId)) {
744
+ return completedRefreshSessionRequests.get(requestId) === true;
745
+ }
746
+ if (reconnectRunner && typeof reconnectRunner.requestRefreshSessionFromRemote === "function") {
747
+ const accepted = await reconnectRunner.requestRefreshSessionFromRemote(event);
748
+ return rememberCompletedRefreshSessionRequest(requestId, accepted !== false);
749
+ }
750
+ return rememberCompletedRefreshSessionRequest(requestId, false);
751
+ };
752
+
765
753
  if (cliArgs.configFile) {
766
754
  env.CONDUCTOR_CONFIG = cliArgs.configFile;
767
755
  }
@@ -796,7 +784,10 @@ async function main() {
796
784
  conductor = await ConductorClient.connect({
797
785
  projectPath: runtimeProjectPath,
798
786
  extraEnv: env,
799
- extraHeaders: buildConductorConnectHeaders(),
787
+ extraHeaders: buildConductorConnectHeaders(pkgJson.version, {
788
+ backends: [cliArgs.backend],
789
+ capabilities: ["refresh_session_inplace"],
790
+ }),
800
791
  configFile: cliArgs.configFile,
801
792
  onConnected: (event) => {
802
793
  fireWatchdog.onConnected(event);
@@ -813,6 +804,7 @@ async function main() {
813
804
  },
814
805
  onStopTask: handleStopTaskCommand,
815
806
  onInterruptTurn: handleInterruptTurnCommand,
807
+ onRefreshSession: handleRefreshSessionCommand,
816
808
  });
817
809
 
818
810
  const taskContext = await ensureTaskContext(conductor, {
@@ -852,7 +844,10 @@ async function main() {
852
844
  }
853
845
  }
854
846
 
855
- const resolvedResumeSessionId = cliArgs.resumeSessionId;
847
+ let nextResumeSessionId = cliArgs.resumeSessionId;
848
+ let nextInitialPrompt =
849
+ taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "";
850
+ let pendingRefreshSessionRequest = null;
856
851
 
857
852
  const sessionCommandLine = resolveAiSessionCommandLine(
858
853
  cliArgs.backend,
@@ -867,19 +862,6 @@ async function main() {
867
862
  env: process.env,
868
863
  });
869
864
 
870
- backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
871
- initialImages: cliArgs.initialImages,
872
- cwd: runtimeProjectPath,
873
- resumeSessionId: resolvedResumeSessionId,
874
- configFile: cliArgs.configFile,
875
- ...(cliArgs.sessionOptions || {}),
876
- ...(sessionCommandLine ? { commandLine: sessionCommandLine } : {}),
877
- logger: { log },
878
- ...(resolvedPrePrompt ? { prePrompt: resolvedPrePrompt } : {}),
879
- sessionStoreKey: taskContext.taskId ? `task-${taskContext.taskId}` : undefined,
880
- resumePersistedSession: Boolean(!resolvedResumeSessionId && taskContext.taskId),
881
- });
882
-
883
865
  log(`Using backend: ${cliArgs.backend}`);
884
866
 
885
867
  try {
@@ -892,26 +874,6 @@ async function main() {
892
874
  log(`Failed to report agent resume: ${error?.message || error}`);
893
875
  }
894
876
 
895
- const runner = new BridgeRunner({
896
- backendSession,
897
- conductor,
898
- taskId: taskContext.taskId,
899
- pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
900
- initialPrompt: taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "",
901
- initialPromptDelivery: taskContext.initialPromptDelivery || "none",
902
- includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
903
- cliArgs: cliArgs.rawBackendArgs,
904
- backendName: cliArgs.backend,
905
- resumeSessionId: resolvedResumeSessionId,
906
- daemonName: resolvedDaemonName,
907
- });
908
- reconnectRunner = runner;
909
- if (pendingRemoteStopEvent) {
910
- await runner.requestStopFromRemote(pendingRemoteStopEvent);
911
- pendingRemoteStopEvent = null;
912
- }
913
- await pendingRemoteInterruptQueue.flushWith((event) => runner.requestInterruptFromRemote(event));
914
-
915
877
  const signals = new AbortController();
916
878
  let shutdownSignal = null;
917
879
  let backendShutdownRequested = false;
@@ -955,68 +917,148 @@ async function main() {
955
917
  }
956
918
  }
957
919
 
958
- let runnerError = null;
959
920
  try {
960
- if (!resolvedResumeSessionId && String(cliArgs.sessionBackend || cliArgs.backend).trim().toLowerCase() === "codex") {
961
- await withFreshSessionBootstrapLock(cliArgs.sessionBackend || cliArgs.backend, runtimeProjectPath, async () => {
962
- await runner.announceBackendSession();
921
+ while (true) {
922
+ const currentRefreshSessionRequest = pendingRefreshSessionRequest;
923
+ let runnerError = null;
924
+
925
+ backendSession = createAiSession(cliArgs.sessionBackend || cliArgs.backend, {
926
+ initialImages: cliArgs.initialImages,
927
+ cwd: runtimeProjectPath,
928
+ resumeSessionId: nextResumeSessionId,
929
+ configFile: cliArgs.configFile,
930
+ ...(cliArgs.sessionOptions || {}),
931
+ ...(sessionCommandLine ? { commandLine: sessionCommandLine } : {}),
932
+ logger: { log },
933
+ ...(resolvedPrePrompt ? { prePrompt: resolvedPrePrompt } : {}),
934
+ sessionStoreKey: taskContext.taskId ? `task-${taskContext.taskId}` : undefined,
935
+ resumePersistedSession: Boolean(!nextResumeSessionId && taskContext.taskId),
963
936
  });
964
- }
965
- await runner.start(signals.signal);
966
- } catch (error) {
967
- runnerError = error;
968
- throw error;
969
- } finally {
970
- process.off("SIGINT", onSigint);
971
- process.off("SIGTERM", onSigterm);
972
- if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "final" })) {
973
- const remoteStopReason = typeof runner.getRemoteStopReason === "function" ? runner.getRemoteStopReason() : null;
974
- const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
975
- // When the task was deleted by the user, the DB record is already gone —
976
- // attempting to send a final status update would fail with 500 and the
977
- // SDK durable outbox would retry forever, preventing the process from
978
- // exiting.
979
- const taskDeletedByUser = remoteStopReason === "deleted_by_user";
980
- const finalStatus = shutdownSignal
981
- ? {
982
- status: "KILLED",
983
- summary: `terminated by ${shutdownSignal}`,
984
- }
985
- : runnerError
937
+
938
+ const runner = new BridgeRunner({
939
+ backendSession,
940
+ conductor,
941
+ taskId: taskContext.taskId,
942
+ pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
943
+ initialPrompt: nextInitialPrompt,
944
+ initialPromptDelivery: taskContext.initialPromptDelivery || "none",
945
+ includeInitialImages: Boolean(nextInitialPrompt && cliArgs.initialImages.length),
946
+ cliArgs: cliArgs.rawBackendArgs,
947
+ backendName: cliArgs.backend,
948
+ resumeSessionId: nextResumeSessionId,
949
+ daemonName: resolvedDaemonName,
950
+ });
951
+ reconnectRunner = runner;
952
+ if (pendingRemoteStopEvent) {
953
+ await runner.requestStopFromRemote(pendingRemoteStopEvent);
954
+ pendingRemoteStopEvent = null;
955
+ }
956
+ await pendingRemoteInterruptQueue.flushWith((event) => runner.requestInterruptFromRemote(event));
957
+
958
+ try {
959
+ if (currentRefreshSessionRequest) {
960
+ await runner.announceBackendSession();
961
+ currentRefreshSessionRequest.resolve(true);
962
+ pendingRefreshSessionRequest = null;
963
+ } else if (
964
+ !nextResumeSessionId &&
965
+ String(cliArgs.sessionBackend || cliArgs.backend).trim().toLowerCase() === "codex"
966
+ ) {
967
+ await withFreshSessionBootstrapLock(
968
+ cliArgs.sessionBackend || cliArgs.backend,
969
+ runtimeProjectPath,
970
+ async () => {
971
+ await runner.announceBackendSession();
972
+ },
973
+ );
974
+ }
975
+ await runner.start(signals.signal);
976
+ } catch (error) {
977
+ runnerError = error;
978
+ if (currentRefreshSessionRequest) {
979
+ currentRefreshSessionRequest.resolve(false);
980
+ pendingRefreshSessionRequest = null;
981
+ }
982
+ }
983
+
984
+ const refreshSessionRequest =
985
+ typeof runner.getRefreshSessionRequest === "function"
986
+ ? runner.getRefreshSessionRequest()
987
+ : null;
988
+ if (!runnerError && !shutdownSignal && refreshSessionRequest) {
989
+ const refreshedSessionId =
990
+ refreshSessionRequest.sessionId ||
991
+ (typeof runner.boundSessionId === "string" ? runner.boundSessionId.trim() : "") ||
992
+ nextResumeSessionId;
993
+ if (refreshedSessionId) {
994
+ nextResumeSessionId = refreshedSessionId;
995
+ nextInitialPrompt = "";
996
+ pendingRefreshSessionRequest = refreshSessionRequest;
997
+ continue;
998
+ }
999
+ runnerError = new Error("refresh_session requires a bound session id");
1000
+ }
1001
+ if (refreshSessionRequest && refreshSessionRequest !== pendingRefreshSessionRequest) {
1002
+ refreshSessionRequest.resolve(false);
1003
+ }
1004
+
1005
+ if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "final" })) {
1006
+ const remoteStopReason = typeof runner.getRemoteStopReason === "function" ? runner.getRemoteStopReason() : null;
1007
+ const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
1008
+ // When the task was deleted by the user, the DB record is already gone —
1009
+ // attempting to send a final status update would fail with 500 and the
1010
+ // SDK durable outbox would retry forever, preventing the process from
1011
+ // exiting.
1012
+ const taskDeletedByUser = remoteStopReason === "deleted_by_user";
1013
+ const finalStatus = shutdownSignal
986
1014
  ? {
987
1015
  status: "KILLED",
988
- summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
1016
+ summary: `terminated by ${shutdownSignal}`,
989
1017
  }
990
- : remoteStopSummary
1018
+ : runnerError
991
1019
  ? {
992
1020
  status: "KILLED",
993
- summary: remoteStopSummary,
1021
+ summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
994
1022
  }
995
- : {
996
- status: "COMPLETED",
997
- summary: "conductor fire exited",
998
- };
999
- if (!taskDeletedByUser) {
1000
- try {
1001
- const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
1002
- if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
1003
- await conductor.flushPendingUpstreamEvents({
1004
- timeoutMs: 5_000,
1005
- retryIntervalMs: 250,
1006
- });
1023
+ : remoteStopSummary
1024
+ ? {
1025
+ status: "KILLED",
1026
+ summary: remoteStopSummary,
1027
+ }
1028
+ : {
1029
+ status: "COMPLETED",
1030
+ summary: "conductor fire exited",
1031
+ };
1032
+ if (!taskDeletedByUser) {
1033
+ try {
1034
+ const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
1035
+ if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
1036
+ await conductor.flushPendingUpstreamEvents({
1037
+ timeoutMs: 5_000,
1038
+ retryIntervalMs: 250,
1039
+ });
1040
+ }
1041
+ } catch (error) {
1042
+ log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
1043
+ }
1044
+ } else {
1045
+ log(`Skipping final status report: task was deleted by user`);
1046
+ // Also clear any pending durable outbox retries (e.g. task_stop_ack)
1047
+ // that would keep failing against the deleted task.
1048
+ if (typeof conductor.clearDurableOutboxTimer === "function") {
1049
+ conductor.clearDurableOutboxTimer();
1007
1050
  }
1008
- } catch (error) {
1009
- log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
1010
- }
1011
- } else {
1012
- log(`Skipping final status report: task was deleted by user`);
1013
- // Also clear any pending durable outbox retries (e.g. task_stop_ack)
1014
- // that would keep failing against the deleted task.
1015
- if (typeof conductor.clearDurableOutboxTimer === "function") {
1016
- conductor.clearDurableOutboxTimer();
1017
1051
  }
1018
1052
  }
1053
+
1054
+ if (runnerError) {
1055
+ throw runnerError;
1056
+ }
1057
+ break;
1019
1058
  }
1059
+ } finally {
1060
+ process.off("SIGINT", onSigint);
1061
+ process.off("SIGTERM", onSigterm);
1020
1062
  if (shutdownSignal === "SIGINT") {
1021
1063
  process.exitCode = 130;
1022
1064
  } else if (shutdownSignal === "SIGTERM") {
@@ -1242,6 +1284,12 @@ Environment:
1242
1284
  const requestedBackend = conductorArgs.backend
1243
1285
  ? normalizeRuntimeBackendName(conductorArgs.backend)
1244
1286
  : supportedBackends[0] || externalBackends[0];
1287
+ if ((conductorArgs.listBackends || listBackendsWithoutSeparator) && discoveryError) {
1288
+ throw discoveryError;
1289
+ }
1290
+ if (!conductorArgs.backend && discoveryError && isCommandOptionalBuiltInRuntimeBackend(requestedBackend)) {
1291
+ throw discoveryError;
1292
+ }
1245
1293
  const configuredBackend = await resolveConfiguredRuntimeBackend(requestedBackend, allowCliList, {
1246
1294
  configFilePath: configFileFromArgs,
1247
1295
  });
@@ -1266,7 +1314,9 @@ Environment:
1266
1314
  const isAllowedExternalBackend =
1267
1315
  !isBuiltInRuntimeBackend(sessionBackend) &&
1268
1316
  advertisedExternalBackends.has(sessionBackend);
1269
- if (backend && shouldRequireBackend && !hasConfiguredEntry && !isAllowedExternalBackend) {
1317
+ const isCommandOptionalBuiltInBackend =
1318
+ isCommandOptionalBuiltInRuntimeBackend(sessionBackend || backend);
1319
+ if (backend && shouldRequireBackend && !hasConfiguredEntry && !isAllowedExternalBackend && !isCommandOptionalBuiltInBackend) {
1270
1320
  throw new Error(
1271
1321
  `Unsupported backend "${backend}". Supported backends: ${[...runtimeSupportedBackends].join(", ") || "none configured"}.`,
1272
1322
  );
@@ -1618,6 +1668,7 @@ export async function resolveResumeContext(backend, sessionId, options = {}) {
1618
1668
 
1619
1669
  export async function bootstrapResumeContextForFire({
1620
1670
  backend,
1671
+ sessionBackend,
1621
1672
  configFile,
1622
1673
  resumeSessionId,
1623
1674
  env = process.env,
@@ -1641,7 +1692,8 @@ export async function bootstrapResumeContextForFire({
1641
1692
  return { resumeContext, runtimeProjectPath };
1642
1693
  }
1643
1694
 
1644
- resumeContext = await resolveResumeContextFn(backend, resumeSessionId, {
1695
+ const resumeLookupBackend = backend || sessionBackend;
1696
+ resumeContext = await resolveResumeContextFn(resumeLookupBackend, resumeSessionId, {
1645
1697
  configFilePath: configFile,
1646
1698
  });
1647
1699
  const sessionLocation = resumeContext.sessionPath ? ` at ${resumeContext.sessionPath}` : "";
@@ -1790,6 +1842,7 @@ export class BridgeRunner {
1790
1842
  os.hostname();
1791
1843
  this.needsReconnectRecovery = false;
1792
1844
  this.remoteStopInfo = null;
1845
+ this.refreshSessionRequest = null;
1793
1846
  this.remoteInterruptsByReplyTo = new Map();
1794
1847
  this.pendingInterruptRetryTimers = new Map();
1795
1848
  this.activeTurnReplyTo = "";
@@ -2009,7 +2062,11 @@ export class BridgeRunner {
2009
2062
  }
2010
2063
 
2011
2064
  shouldSuppressReconnectRecovery() {
2012
- return this.stopped || Boolean(this.remoteStopInfo);
2065
+ return this.stopped || Boolean(this.remoteStopInfo) || Boolean(this.refreshSessionRequest);
2066
+ }
2067
+
2068
+ getRefreshSessionRequest() {
2069
+ return this.refreshSessionRequest;
2013
2070
  }
2014
2071
 
2015
2072
  getRemoteStopReason() {
@@ -2052,6 +2109,54 @@ export class BridgeRunner {
2052
2109
  }
2053
2110
  }
2054
2111
 
2112
+ async requestRefreshSessionFromRemote(event = {}) {
2113
+ const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
2114
+ if (taskId && taskId !== this.taskId) {
2115
+ return false;
2116
+ }
2117
+ const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
2118
+ const sessionId = typeof event.sessionId === "string" ? event.sessionId.trim() : "";
2119
+ const sessionFilePath =
2120
+ typeof event.sessionFilePath === "string" && event.sessionFilePath.trim()
2121
+ ? event.sessionFilePath.trim()
2122
+ : "";
2123
+ if (!sessionId) {
2124
+ return false;
2125
+ }
2126
+ if (this.refreshSessionRequest) {
2127
+ if (
2128
+ requestId &&
2129
+ this.refreshSessionRequest.requestId &&
2130
+ this.refreshSessionRequest.requestId === requestId
2131
+ ) {
2132
+ return await this.refreshSessionRequest.promise;
2133
+ }
2134
+ return false;
2135
+ }
2136
+
2137
+ let resolveRefresh;
2138
+ const promise = new Promise((resolve) => {
2139
+ resolveRefresh = resolve;
2140
+ });
2141
+ this.refreshSessionRequest = {
2142
+ requestId: requestId || null,
2143
+ sessionId,
2144
+ sessionFilePath: sessionFilePath || null,
2145
+ promise,
2146
+ resolve: resolveRefresh,
2147
+ };
2148
+ log(`Received refresh_session for ${this.taskId}; rebuilding backend session in-process`);
2149
+ this.stopped = true;
2150
+ if (typeof this.backendSession?.close === "function") {
2151
+ try {
2152
+ await this.backendSession.close();
2153
+ } catch (error) {
2154
+ log(`Failed to close backend session for refresh ${this.taskId}: ${error?.message || error}`);
2155
+ }
2156
+ }
2157
+ return await promise;
2158
+ }
2159
+
2055
2160
  normalizeReplyTarget(replyTo) {
2056
2161
  return typeof replyTo === "string" ? replyTo.trim() : "";
2057
2162
  }