@love-moon/conductor-cli 0.2.38 → 0.2.39

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.
@@ -89,6 +89,13 @@ export function shouldRunReconnectRecovery({
89
89
  return !runner.shouldSuppressReconnectRecovery();
90
90
  }
91
91
 
92
+ export function shouldFireReportTaskStatus({ launchedByDaemon = false, phase } = {}) {
93
+ if (phase === "final") {
94
+ return true;
95
+ }
96
+ return !launchedByDaemon;
97
+ }
98
+
92
99
  // Load allow_cli_list from config file (no defaults - must be configured)
93
100
  function loadFireConfigYaml(configFilePath) {
94
101
  const home = os.homedir();
@@ -577,6 +584,39 @@ export class FireWatchdog {
577
584
  }
578
585
  }
579
586
 
587
+ export function createPendingRemoteInterruptQueue() {
588
+ const pending = [];
589
+
590
+ return {
591
+ enqueue(event) {
592
+ return new Promise((resolve) => {
593
+ pending.push({ event, resolve });
594
+ });
595
+ },
596
+
597
+ async flushWith(dispatch) {
598
+ while (pending.length > 0) {
599
+ const next = pending.shift();
600
+ if (!next) {
601
+ continue;
602
+ }
603
+ try {
604
+ next.resolve(await dispatch(next.event));
605
+ } catch {
606
+ next.resolve(false);
607
+ }
608
+ }
609
+ },
610
+
611
+ rejectAll() {
612
+ while (pending.length > 0) {
613
+ const next = pending.shift();
614
+ next?.resolve(false);
615
+ }
616
+ },
617
+ };
618
+ }
619
+
580
620
  async function main() {
581
621
  syncPwdEnvWithProcessCwdForDaemonLaunch();
582
622
  const cliArgs = await parseCliArgs();
@@ -637,6 +677,7 @@ async function main() {
637
677
  let reconnectRunner = null;
638
678
  let reconnectTaskId = null;
639
679
  let pendingRemoteStopEvent = null;
680
+ const pendingRemoteInterruptQueue = createPendingRemoteInterruptQueue();
640
681
  let conductor = null;
641
682
  let reconnectResumeInFlight = false;
642
683
  let fireShuttingDown = false;
@@ -676,7 +717,7 @@ async function main() {
676
717
  source: "conductor-fire",
677
718
  metadata: { reconnect: true },
678
719
  });
679
- if (!launchedByDaemon) {
720
+ if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "reconnect_running" })) {
680
721
  await conductor.sendTaskStatus(reconnectTaskId, {
681
722
  status: "RUNNING",
682
723
  summary: "conductor fire reconnected",
@@ -706,6 +747,21 @@ async function main() {
706
747
  pendingRemoteStopEvent = event;
707
748
  };
708
749
 
750
+ const handleInterruptTurnCommand = async (event) => {
751
+ fireWatchdog.onInbound();
752
+ if (!event || typeof event !== "object") {
753
+ return false;
754
+ }
755
+ const taskId = typeof event.taskId === "string" ? event.taskId : "";
756
+ if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
757
+ return false;
758
+ }
759
+ if (reconnectRunner && typeof reconnectRunner.requestInterruptFromRemote === "function") {
760
+ return await reconnectRunner.requestInterruptFromRemote(event);
761
+ }
762
+ return await pendingRemoteInterruptQueue.enqueue(event);
763
+ };
764
+
709
765
  if (cliArgs.configFile) {
710
766
  env.CONDUCTOR_CONFIG = cliArgs.configFile;
711
767
  }
@@ -756,6 +812,7 @@ async function main() {
756
812
  fireWatchdog.onPong(event);
757
813
  },
758
814
  onStopTask: handleStopTaskCommand,
815
+ onInterruptTurn: handleInterruptTurnCommand,
759
816
  });
760
817
 
761
818
  const taskContext = await ensureTaskContext(conductor, {
@@ -853,6 +910,7 @@ async function main() {
853
910
  await runner.requestStopFromRemote(pendingRemoteStopEvent);
854
911
  pendingRemoteStopEvent = null;
855
912
  }
913
+ await pendingRemoteInterruptQueue.flushWith((event) => runner.requestInterruptFromRemote(event));
856
914
 
857
915
  const signals = new AbortController();
858
916
  let shutdownSignal = null;
@@ -887,7 +945,7 @@ async function main() {
887
945
  process.on("SIGINT", onSigint);
888
946
  process.on("SIGTERM", onSigterm);
889
947
 
890
- if (!launchedByDaemon) {
948
+ if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "running" })) {
891
949
  try {
892
950
  await conductor.sendTaskStatus(taskContext.taskId, {
893
951
  status: "RUNNING",
@@ -911,7 +969,7 @@ async function main() {
911
969
  } finally {
912
970
  process.off("SIGINT", onSigint);
913
971
  process.off("SIGTERM", onSigterm);
914
- if (!launchedByDaemon) {
972
+ if (shouldFireReportTaskStatus({ launchedByDaemon, phase: "final" })) {
915
973
  const remoteStopReason = typeof runner.getRemoteStopReason === "function" ? runner.getRemoteStopReason() : null;
916
974
  const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
917
975
  // When the task was deleted by the user, the DB record is already gone —
@@ -934,10 +992,10 @@ async function main() {
934
992
  status: "KILLED",
935
993
  summary: remoteStopSummary,
936
994
  }
937
- : {
938
- status: "COMPLETED",
939
- summary: "conductor fire exited",
940
- };
995
+ : {
996
+ status: "COMPLETED",
997
+ summary: "conductor fire exited",
998
+ };
941
999
  if (!taskDeletedByUser) {
942
1000
  try {
943
1001
  const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
@@ -966,6 +1024,7 @@ async function main() {
966
1024
  }
967
1025
  }
968
1026
  } finally {
1027
+ pendingRemoteInterruptQueue.rejectAll();
969
1028
  fireShuttingDown = true;
970
1029
  fireWatchdog.stop();
971
1030
  if (backendSession && typeof backendSession.close === "function") {
@@ -1731,6 +1790,9 @@ export class BridgeRunner {
1731
1790
  os.hostname();
1732
1791
  this.needsReconnectRecovery = false;
1733
1792
  this.remoteStopInfo = null;
1793
+ this.remoteInterruptsByReplyTo = new Map();
1794
+ this.pendingInterruptRetryTimers = new Map();
1795
+ this.activeTurnReplyTo = "";
1734
1796
  this.sessionAnnouncementSent = false;
1735
1797
  this.boundSessionId = "";
1736
1798
  this.errorLoop = null;
@@ -1990,6 +2052,200 @@ export class BridgeRunner {
1990
2052
  }
1991
2053
  }
1992
2054
 
2055
+ normalizeReplyTarget(replyTo) {
2056
+ return typeof replyTo === "string" ? replyTo.trim() : "";
2057
+ }
2058
+
2059
+ isTurnInterruptedError(error) {
2060
+ const reason = typeof error?.reason === "string" ? error.reason.trim().toLowerCase() : "";
2061
+ if (reason === "turn_interrupted" || reason === "turn_cancelled") {
2062
+ return true;
2063
+ }
2064
+ const turnStatus = typeof error?.turnStatus === "string" ? error.turnStatus.trim().toLowerCase() : "";
2065
+ if (
2066
+ turnStatus === "interrupted" ||
2067
+ turnStatus === "cancelled" ||
2068
+ turnStatus === "canceled" ||
2069
+ turnStatus === "aborted"
2070
+ ) {
2071
+ return true;
2072
+ }
2073
+ const name = typeof error?.name === "string" ? error.name.trim().toLowerCase() : "";
2074
+ if (name === "aborterror") {
2075
+ return true;
2076
+ }
2077
+ const message = String(error?.message || error || "").toLowerCase();
2078
+ return (
2079
+ message.includes(" interrupted") ||
2080
+ message.includes("interrupt ") ||
2081
+ message.includes("turn interrupted") ||
2082
+ message.includes("cancelled") ||
2083
+ message.includes("canceled") ||
2084
+ message.includes("aborted")
2085
+ );
2086
+ }
2087
+
2088
+ async requestInterruptFromRemote(event = {}) {
2089
+ const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
2090
+ if (taskId && taskId !== this.taskId) {
2091
+ return false;
2092
+ }
2093
+ const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
2094
+ const reason = typeof event.reason === "string" ? event.reason.trim() : "";
2095
+ const targetReplyTo = this.normalizeReplyTarget(event.targetReplyTo);
2096
+ if (!targetReplyTo) {
2097
+ return false;
2098
+ }
2099
+ if (this.processedMessageIds.has(targetReplyTo)) {
2100
+ this.copilotLog(`ignore late interrupt_turn for processed replyTo=${targetReplyTo}`);
2101
+ return false;
2102
+ }
2103
+
2104
+ const existing = this.remoteInterruptsByReplyTo.get(targetReplyTo) || {};
2105
+ const interruptInfo = {
2106
+ requestId: requestId || existing.requestId || null,
2107
+ reason: reason || existing.reason || "user_interrupt",
2108
+ issued: Boolean(existing.issued),
2109
+ };
2110
+ this.remoteInterruptsByReplyTo.set(targetReplyTo, interruptInfo);
2111
+ log(
2112
+ `Received interrupt_turn for ${this.taskId} replyTo=${targetReplyTo}${
2113
+ interruptInfo.reason ? ` (${interruptInfo.reason})` : ""
2114
+ }`,
2115
+ );
2116
+ return await this.issueInterruptForReplyTarget(targetReplyTo);
2117
+ }
2118
+
2119
+ async issueInterruptForReplyTarget(replyTo) {
2120
+ const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
2121
+ if (!normalizedReplyTo) {
2122
+ return false;
2123
+ }
2124
+ const interruptInfo = this.remoteInterruptsByReplyTo.get(normalizedReplyTo);
2125
+ if (!interruptInfo) {
2126
+ return false;
2127
+ }
2128
+ if (interruptInfo.issued) {
2129
+ return true;
2130
+ }
2131
+ const supportsTurnInterrupt = typeof this.backendSession?.interruptCurrentTurn === "function";
2132
+ const isActiveTarget = this.runningTurn && normalizedReplyTo === this.activeTurnReplyTo;
2133
+ const isInFlightTarget = this.inFlightMessageIds.has(normalizedReplyTo);
2134
+
2135
+ if (!isActiveTarget && isInFlightTarget) {
2136
+ this.copilotLog(`interrupt arrived after replyTo=${normalizedReplyTo} stopped being interruptible`);
2137
+ return false;
2138
+ }
2139
+
2140
+ if (!isActiveTarget) {
2141
+ if (!supportsTurnInterrupt) {
2142
+ log(`Backend session for ${this.taskId} does not support turn interruption`);
2143
+ return false;
2144
+ }
2145
+ this.copilotLog(`queued interrupt request for future replyTo=${normalizedReplyTo}`);
2146
+ return true;
2147
+ }
2148
+
2149
+ if (!supportsTurnInterrupt) {
2150
+ log(`Backend session for ${this.taskId} does not support turn interruption`);
2151
+ return false;
2152
+ }
2153
+ try {
2154
+ const interrupted = await this.backendSession.interruptCurrentTurn();
2155
+ if (interrupted === false) {
2156
+ interruptInfo.issued = false;
2157
+ this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
2158
+ if (
2159
+ this.runningTurn &&
2160
+ this.activeTurnReplyTo === normalizedReplyTo &&
2161
+ this.inFlightMessageIds.has(normalizedReplyTo)
2162
+ ) {
2163
+ this.copilotLog(`backend interrupt not ready replyTo=${normalizedReplyTo}; retrying`);
2164
+ this.scheduleInterruptRetryForReplyTarget(normalizedReplyTo);
2165
+ return true;
2166
+ }
2167
+ return false;
2168
+ }
2169
+ interruptInfo.issued = true;
2170
+ this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
2171
+ this.copilotLog(`requested backend interrupt replyTo=${normalizedReplyTo}`);
2172
+ return true;
2173
+ } catch (error) {
2174
+ interruptInfo.issued = false;
2175
+ this.remoteInterruptsByReplyTo.set(normalizedReplyTo, interruptInfo);
2176
+ log(`Failed to interrupt replyTo=${normalizedReplyTo} for ${this.taskId}: ${error?.message || error}`);
2177
+ return false;
2178
+ }
2179
+ }
2180
+
2181
+ scheduleInterruptRetryForReplyTarget(replyTo) {
2182
+ const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
2183
+ if (!normalizedReplyTo || this.pendingInterruptRetryTimers.has(normalizedReplyTo)) {
2184
+ return;
2185
+ }
2186
+
2187
+ const timer = setTimeout(() => {
2188
+ this.pendingInterruptRetryTimers.delete(normalizedReplyTo);
2189
+ const interruptInfo = this.remoteInterruptsByReplyTo.get(normalizedReplyTo);
2190
+ if (
2191
+ !interruptInfo ||
2192
+ interruptInfo.issued ||
2193
+ this.processedMessageIds.has(normalizedReplyTo) ||
2194
+ !this.runningTurn ||
2195
+ this.activeTurnReplyTo !== normalizedReplyTo ||
2196
+ !this.inFlightMessageIds.has(normalizedReplyTo)
2197
+ ) {
2198
+ return;
2199
+ }
2200
+ void this.issueInterruptForReplyTarget(normalizedReplyTo);
2201
+ }, 50);
2202
+ if (typeof timer.unref === "function") {
2203
+ timer.unref();
2204
+ }
2205
+ this.pendingInterruptRetryTimers.set(normalizedReplyTo, timer);
2206
+ }
2207
+
2208
+ clearInterruptRetryForReplyTarget(replyTo) {
2209
+ const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
2210
+ const timer = this.pendingInterruptRetryTimers.get(normalizedReplyTo);
2211
+ if (!timer) {
2212
+ return;
2213
+ }
2214
+ clearTimeout(timer);
2215
+ this.pendingInterruptRetryTimers.delete(normalizedReplyTo);
2216
+ }
2217
+
2218
+ async handleInterruptedTurn(replyTo, interruptInfo) {
2219
+ const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
2220
+ this.clearInterruptRetryForReplyTarget(normalizedReplyTo);
2221
+ this.copilotLog(`turn interrupted replyTo=${normalizedReplyTo || "latest"}`);
2222
+ await this.reportRuntimeStatus(
2223
+ {
2224
+ phase: "interrupted",
2225
+ reply_in_progress: false,
2226
+ status_done_line: "Conversation interrupted",
2227
+ },
2228
+ normalizedReplyTo,
2229
+ );
2230
+ try {
2231
+ await this.conductor.sendMessage(this.taskId, "Conversation interrupted", {
2232
+ backend: this.backendName,
2233
+ reply_to: normalizedReplyTo || undefined,
2234
+ interrupted: true,
2235
+ interruption_request_id: interruptInfo?.requestId || undefined,
2236
+ reason: interruptInfo?.reason || undefined,
2237
+ cli_args: this.cliArgs,
2238
+ });
2239
+ } catch (error) {
2240
+ log(`Failed to send interrupt confirmation for ${this.taskId}: ${error?.message || error}`);
2241
+ }
2242
+ if (normalizedReplyTo) {
2243
+ this.processedMessageIds.add(normalizedReplyTo);
2244
+ this.remoteInterruptsByReplyTo.delete(normalizedReplyTo);
2245
+ }
2246
+ this.resetErrorLoop();
2247
+ }
2248
+
1993
2249
  async recoverAfterReconnect() {
1994
2250
  if (!this.needsReconnectRecovery) {
1995
2251
  return;
@@ -2502,6 +2758,7 @@ export class BridgeRunner {
2502
2758
  }
2503
2759
  this.lastRuntimeStatusSignature = null;
2504
2760
  this.runningTurn = true;
2761
+ this.activeTurnReplyTo = this.normalizeReplyTarget(replyTo);
2505
2762
  const turnStartedAt = Date.now();
2506
2763
  let turnWatchdog = null;
2507
2764
  if (this.isCopilotBackend) {
@@ -2540,12 +2797,15 @@ export class BridgeRunner {
2540
2797
  );
2541
2798
  }
2542
2799
 
2543
- const result = await this.backendSession.runTurn(content, {
2800
+ const turnPromise = this.backendSession.runTurn(content, {
2544
2801
  useInitialImages,
2545
2802
  onProgress: (payload) => {
2546
2803
  void this.reportRuntimeStatus(payload, replyTo);
2547
2804
  },
2548
2805
  });
2806
+ await this.issueInterruptForReplyTarget(replyTo);
2807
+ const result = await turnPromise;
2808
+ this.activeTurnReplyTo = "";
2549
2809
  this.copilotLog(
2550
2810
  `runTurn completed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} answerLen=${String(
2551
2811
  result.text || "",
@@ -2582,6 +2842,10 @@ export class BridgeRunner {
2582
2842
  });
2583
2843
  }
2584
2844
  await this.syncBackendSessionBinding();
2845
+ if (replyTo) {
2846
+ this.clearInterruptRetryForReplyTarget(replyTo);
2847
+ this.remoteInterruptsByReplyTo.delete(replyTo);
2848
+ }
2585
2849
  if (replyTo) {
2586
2850
  this.processedMessageIds.add(replyTo);
2587
2851
  }
@@ -2600,6 +2864,7 @@ export class BridgeRunner {
2600
2864
  this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
2601
2865
  }
2602
2866
  } catch (error) {
2867
+ this.activeTurnReplyTo = "";
2603
2868
  const errorMessage = error instanceof Error ? error.message : String(error);
2604
2869
  if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
2605
2870
  this.copilotLog(
@@ -2607,6 +2872,11 @@ export class BridgeRunner {
2607
2872
  );
2608
2873
  return;
2609
2874
  }
2875
+ const interruptInfo = replyTo ? this.remoteInterruptsByReplyTo.get(replyTo) : null;
2876
+ if (interruptInfo && this.isTurnInterruptedError(error)) {
2877
+ await this.handleInterruptedTurn(replyTo, interruptInfo);
2878
+ return;
2879
+ }
2610
2880
  if (await this.settleCodexCheckpointUnavailableAfterStream(replyTo, errorMessage)) {
2611
2881
  return;
2612
2882
  }
@@ -2663,7 +2933,9 @@ export class BridgeRunner {
2663
2933
  }
2664
2934
  if (replyTo) {
2665
2935
  this.inFlightMessageIds.delete(replyTo);
2936
+ this.clearInterruptRetryForReplyTarget(replyTo);
2666
2937
  }
2938
+ this.activeTurnReplyTo = "";
2667
2939
  this.copilotLog(
2668
2940
  `turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
2669
2941
  );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.38",
4
- "gitCommitId": "6bf5515",
3
+ "version": "0.2.39",
4
+ "gitCommitId": "30204c8",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -18,9 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@love-moon/ai-bridge": "0.1.4",
21
- "@love-moon/ai-manager": "0.2.38",
22
- "@love-moon/ai-sdk": "0.2.38",
23
- "@love-moon/conductor-sdk": "0.2.38",
21
+ "@love-moon/ai-manager": "0.2.39",
22
+ "@love-moon/ai-sdk": "0.2.39",
23
+ "@love-moon/conductor-sdk": "0.2.39",
24
24
  "chrome-launcher": "^1.2.1",
25
25
  "chrome-remote-interface": "^0.33.0",
26
26
  "dotenv": "^16.4.5",
package/src/daemon.js CHANGED
@@ -1589,15 +1589,27 @@ export function startDaemon(config = {}, deps = {}) {
1589
1589
  }
1590
1590
  }
1591
1591
 
1592
- watchdogTimer = setInterval(() => {
1592
+ const runMaintenanceTick = async () => {
1593
1593
  void runDaemonWatchdog();
1594
- // Auto-update checks (internally throttled)
1595
- void checkForUpdate().catch(() => {});
1596
- void tryAutoUpdate().catch(() => {});
1594
+ try {
1595
+ await checkForUpdate();
1596
+ } catch {
1597
+ // ignore non-critical version check failures
1598
+ }
1599
+ try {
1600
+ await tryAutoUpdate();
1601
+ } catch {
1602
+ // ignore non-critical auto-update failures
1603
+ }
1604
+ };
1605
+
1606
+ watchdogTimer = setInterval(() => {
1607
+ void runMaintenanceTick();
1597
1608
  }, DAEMON_WATCHDOG_INTERVAL_MS);
1598
1609
  if (typeof watchdogTimer?.unref === "function") {
1599
1610
  watchdogTimer.unref();
1600
1611
  }
1612
+ void runMaintenanceTick();
1601
1613
  })();
1602
1614
 
1603
1615
  function markBackendHttpSuccess(at = Date.now()) {
@@ -3824,6 +3836,10 @@ export function startDaemon(config = {}, deps = {}) {
3824
3836
  return true;
3825
3837
  }
3826
3838
 
3839
+ function shouldDaemonReportFireChildTerminalStatus(record) {
3840
+ return !Boolean(record?.managedByFireBridge);
3841
+ }
3842
+
3827
3843
  function handleStopTask(payload) {
3828
3844
  const taskId = payload?.task_id;
3829
3845
  if (!taskId) return;
@@ -4326,6 +4342,7 @@ export function startDaemon(config = {}, deps = {}) {
4326
4342
  projectId,
4327
4343
  logPath,
4328
4344
  stopForceKillTimer: null,
4345
+ managedByFireBridge: true,
4329
4346
  });
4330
4347
 
4331
4348
  client
@@ -4395,7 +4412,7 @@ export function startDaemon(config = {}, deps = {}) {
4395
4412
  ? "completed"
4396
4413
  : `exited with code ${code}`;
4397
4414
 
4398
- if (!suppressExitStatusReport) {
4415
+ if (!suppressExitStatusReport && shouldDaemonReportFireChildTerminalStatus(active)) {
4399
4416
  client
4400
4417
  .sendJson({
4401
4418
  type: "task_status_update",
@@ -4750,6 +4767,7 @@ export function startDaemon(config = {}, deps = {}) {
4750
4767
  projectId: normalizedProjectId,
4751
4768
  logPath,
4752
4769
  stopForceKillTimer: null,
4770
+ managedByFireBridge: true,
4753
4771
  });
4754
4772
 
4755
4773
  client
@@ -4812,7 +4830,7 @@ export function startDaemon(config = {}, deps = {}) {
4812
4830
  ? "completed"
4813
4831
  : `exited with code ${code}`;
4814
4832
 
4815
- if (!suppressExitStatusReport) {
4833
+ if (!suppressExitStatusReport && shouldDaemonReportFireChildTerminalStatus(active)) {
4816
4834
  client
4817
4835
  .sendJson({
4818
4836
  type: "task_status_update",
@@ -4852,6 +4870,9 @@ export function startDaemon(config = {}, deps = {}) {
4852
4870
  await Promise.allSettled(
4853
4871
  activeEntries.map(async ([taskId, record]) => {
4854
4872
  suppressedExitStatusReports.add(taskId);
4873
+ if (!shouldDaemonReportFireChildTerminalStatus(record)) {
4874
+ return;
4875
+ }
4855
4876
  try {
4856
4877
  await withTimeout(
4857
4878
  client.sendJson({
@@ -28,7 +28,7 @@ function appendProviderModulePaths(parts, value) {
28
28
  if (!raw) {
29
29
  return;
30
30
  }
31
- for (const item of raw.split(process.platform === "win32" ? ";" : ":")) {
31
+ for (const item of splitProviderModulePathString(raw)) {
32
32
  const normalized = item.trim();
33
33
  if (normalized) {
34
34
  parts.push(normalized);
@@ -36,6 +36,49 @@ function appendProviderModulePaths(parts, value) {
36
36
  }
37
37
  }
38
38
 
39
+ function looksLikeProviderModulePath(value) {
40
+ const normalized = String(value || "").trim();
41
+ if (!normalized) {
42
+ return false;
43
+ }
44
+ return (
45
+ normalized.startsWith("/") ||
46
+ normalized.startsWith("./") ||
47
+ normalized.startsWith("../") ||
48
+ normalized.startsWith("~/") ||
49
+ normalized.startsWith("file:") ||
50
+ normalized.includes("/") ||
51
+ normalized.includes("\\") ||
52
+ /\.[cm]?[jt]sx?$/i.test(normalized) ||
53
+ /^[A-Za-z]:[\\/]/.test(normalized)
54
+ );
55
+ }
56
+
57
+ function splitProviderModulePathString(raw) {
58
+ const normalized = String(raw || "").trim();
59
+ if (!normalized) {
60
+ return [];
61
+ }
62
+
63
+ const platformParts = normalized
64
+ .split(path.delimiter)
65
+ .map((item) => item.trim())
66
+ .filter(Boolean);
67
+ if (platformParts.length > 1 || !normalized.includes(",")) {
68
+ return platformParts;
69
+ }
70
+
71
+ const commaParts = normalized
72
+ .split(",")
73
+ .map((item) => item.trim())
74
+ .filter(Boolean);
75
+ if (commaParts.length > 1 && commaParts.every(looksLikeProviderModulePath)) {
76
+ return commaParts;
77
+ }
78
+
79
+ return platformParts;
80
+ }
81
+
39
82
  function listProviderModulePaths(providerPathEnv) {
40
83
  const parts = [];
41
84
  appendProviderModulePaths(parts, providerPathEnv);