@love-moon/conductor-cli 0.2.31 → 0.2.32

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.
@@ -66,6 +66,20 @@ export function buildConductorConnectHeaders(version = pkgJson.version) {
66
66
  };
67
67
  }
68
68
 
69
+ export function shouldRunReconnectRecovery({
70
+ isReconnect,
71
+ fireShuttingDown = false,
72
+ runner = null,
73
+ } = {}) {
74
+ if (!isReconnect || fireShuttingDown) {
75
+ return false;
76
+ }
77
+ if (!runner || typeof runner.shouldSuppressReconnectRecovery !== "function") {
78
+ return true;
79
+ }
80
+ return !runner.shouldSuppressReconnectRecovery();
81
+ }
82
+
69
83
  // Load allow_cli_list from config file (no defaults - must be configured)
70
84
  async function loadAllowCliList(configFilePath) {
71
85
  const home = os.homedir();
@@ -485,7 +499,13 @@ async function main() {
485
499
  fireWatchdog.start();
486
500
 
487
501
  const scheduleReconnectRecovery = ({ isReconnect }) => {
488
- if (!isReconnect) {
502
+ if (
503
+ !shouldRunReconnectRecovery({
504
+ isReconnect,
505
+ fireShuttingDown,
506
+ runner: reconnectRunner,
507
+ })
508
+ ) {
489
509
  return;
490
510
  }
491
511
  log("Conductor connection restored");
@@ -742,7 +762,13 @@ async function main() {
742
762
  summary: "conductor fire exited",
743
763
  };
744
764
  try {
745
- await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
765
+ const statusResult = await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
766
+ if (statusResult?.pending && typeof conductor.flushPendingUpstreamEvents === "function") {
767
+ await conductor.flushPendingUpstreamEvents({
768
+ timeoutMs: 5_000,
769
+ retryIntervalMs: 250,
770
+ });
771
+ }
746
772
  } catch (error) {
747
773
  log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
748
774
  }
@@ -1644,6 +1670,10 @@ export class BridgeRunner {
1644
1670
  this.needsReconnectRecovery = true;
1645
1671
  }
1646
1672
 
1673
+ shouldSuppressReconnectRecovery() {
1674
+ return this.stopped || Boolean(this.remoteStopInfo);
1675
+ }
1676
+
1647
1677
  getRemoteStopSummary() {
1648
1678
  if (!this.remoteStopInfo) {
1649
1679
  return null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.31",
4
- "gitCommitId": "7e0bd83",
3
+ "version": "0.2.32",
4
+ "gitCommitId": "c749d4b",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@love-moon/ai-bridge": "0.1.4",
21
- "@love-moon/ai-sdk": "0.2.31",
22
- "@love-moon/conductor-sdk": "0.2.31",
21
+ "@love-moon/ai-sdk": "0.2.32",
22
+ "@love-moon/conductor-sdk": "0.2.32",
23
23
  "chrome-launcher": "^1.2.1",
24
24
  "chrome-remote-interface": "^0.33.0",
25
25
  "dotenv": "^16.4.5",
package/src/daemon.js CHANGED
@@ -59,6 +59,7 @@ const DEFAULT_TERMINAL_ROWS = 40;
59
59
  const DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES = 2 * 1024 * 1024;
60
60
  const DEFAULT_TERMINAL_RESUME_SNAPSHOT_MAX_BYTES = 128 * 1024;
61
61
  const DEFAULT_RTC_MODULE_CANDIDATES = ["@roamhq/wrtc", "wrtc"];
62
+ const BLOCKING_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
62
63
  let nodePtySpawnPromise = null;
63
64
 
64
65
  function resolveNodePtySpawnExport(mod) {
@@ -117,6 +118,20 @@ function logError(message) {
117
118
  appendDaemonLog(line);
118
119
  }
119
120
 
121
+ function sleepSync(ms) {
122
+ if (!Number.isFinite(ms) || ms <= 0) {
123
+ return;
124
+ }
125
+ try {
126
+ Atomics.wait(BLOCKING_SLEEP_BUFFER, 0, 0, ms);
127
+ } catch {
128
+ const deadline = Date.now() + ms;
129
+ while (Date.now() < deadline) {
130
+ // best-effort fallback for runtimes that disallow Atomics.wait
131
+ }
132
+ }
133
+ }
134
+
120
135
  function getUserConfig(configFilePath) {
121
136
  try {
122
137
  const home = os.homedir();
@@ -560,6 +575,18 @@ export function startDaemon(config = {}, deps = {}) {
560
575
  process.env.CONDUCTOR_STOP_FORCE_KILL_TIMEOUT_MS,
561
576
  5000,
562
577
  );
578
+ const DAEMON_FORCE_STOP_GRACE_MS = parsePositiveInt(
579
+ process.env.CONDUCTOR_DAEMON_FORCE_STOP_GRACE_MS,
580
+ 15_000,
581
+ );
582
+ const DAEMON_FORCE_STOP_POLL_INTERVAL_MS = parsePositiveInt(
583
+ process.env.CONDUCTOR_DAEMON_FORCE_STOP_POLL_INTERVAL_MS,
584
+ 100,
585
+ );
586
+ const DAEMON_FORCE_KILL_WAIT_MS = parsePositiveInt(
587
+ process.env.CONDUCTOR_DAEMON_FORCE_KILL_WAIT_MS,
588
+ 2_000,
589
+ );
563
590
  const SHUTDOWN_STATUS_REPORT_TIMEOUT_MS = parsePositiveInt(
564
591
  process.env.CONDUCTOR_SHUTDOWN_STATUS_REPORT_TIMEOUT_MS,
565
592
  1000,
@@ -648,6 +675,20 @@ export function startDaemon(config = {}, deps = {}) {
648
675
  return true;
649
676
  };
650
677
 
678
+ const waitForProcessExitSync = (pid, timeoutMs) => {
679
+ const deadline = Date.now() + timeoutMs;
680
+ while (true) {
681
+ if (!isProcessAlive(pid)) {
682
+ return true;
683
+ }
684
+ const remainingMs = deadline - Date.now();
685
+ if (remainingMs <= 0) {
686
+ return false;
687
+ }
688
+ sleepSync(Math.min(DAEMON_FORCE_STOP_POLL_INTERVAL_MS, remainingMs));
689
+ }
690
+ };
691
+
651
692
  try {
652
693
  mkdirSyncFn(WORKSPACE_ROOT, { recursive: true });
653
694
  } catch (err) {
@@ -670,17 +711,35 @@ export function startDaemon(config = {}, deps = {}) {
670
711
  if (alive) {
671
712
  if (config.FORCE) {
672
713
  log(`Force enabled: stopping existing daemon PID ${pid}`);
714
+ let alreadyExited = false;
673
715
  try {
674
716
  killFn(pid, "SIGTERM");
675
717
  } catch (killErr) {
676
- if (!killErr || killErr.code !== "ESRCH") {
718
+ if (killErr?.code === "ESRCH") {
719
+ alreadyExited = true;
720
+ } else {
677
721
  logError(`Failed to stop existing daemon PID ${pid}: ${killErr.message}`);
678
722
  return exitAndReturn(1);
679
723
  }
680
724
  }
681
725
  try {
682
- if (isProcessAlive(pid)) {
683
- logError(`Existing daemon PID ${pid} is still running; please stop it manually.`);
726
+ let exited = alreadyExited || waitForProcessExitSync(pid, DAEMON_FORCE_STOP_GRACE_MS);
727
+ if (!exited) {
728
+ log(
729
+ `Existing daemon PID ${pid} did not exit within ${DAEMON_FORCE_STOP_GRACE_MS}ms; sending SIGKILL`,
730
+ );
731
+ try {
732
+ killFn(pid, "SIGKILL");
733
+ } catch (killErr) {
734
+ if (killErr?.code !== "ESRCH") {
735
+ logError(`Failed to force kill existing daemon PID ${pid}: ${killErr.message}`);
736
+ return exitAndReturn(1);
737
+ }
738
+ }
739
+ exited = waitForProcessExitSync(pid, DAEMON_FORCE_KILL_WAIT_MS);
740
+ }
741
+ if (!exited) {
742
+ logError(`Existing daemon PID ${pid} is still running after force restart; please stop it manually.`);
684
743
  return exitAndReturn(1);
685
744
  }
686
745
  } catch (checkErr) {
@@ -688,7 +747,9 @@ export function startDaemon(config = {}, deps = {}) {
688
747
  return exitAndReturn(1);
689
748
  }
690
749
  log("Removing lock file after force stop");
691
- unlinkSyncFn(LOCK_FILE);
750
+ if (existsSyncFn(LOCK_FILE)) {
751
+ unlinkSyncFn(LOCK_FILE);
752
+ }
692
753
  } else {
693
754
  logError(`Daemon already running with PID ${pid}`);
694
755
  return exitAndReturn(1);
@@ -126,6 +126,14 @@ function registerExternalAlias(catalog, alias, backend, sourcePath) {
126
126
  catalog.aliasToBackend.set(alias, backend);
127
127
  }
128
128
 
129
+ function formatExternalProviderLoadError(modulePath, error) {
130
+ const message = error?.message || String(error);
131
+ return [
132
+ `Failed to load external AI SDK provider module ${modulePath}: ${message}`,
133
+ "Help: if this provider comes from a local repo or workspace, did you forget to run pnpm install?",
134
+ ].join(" ");
135
+ }
136
+
129
137
  async function loadExternalRuntimeCatalog(providerPathEnv) {
130
138
  const catalog = createEmptyExternalCatalog();
131
139
  for (const modulePath of listProviderModulePaths(providerPathEnv)) {
@@ -133,7 +141,7 @@ async function loadExternalRuntimeCatalog(providerPathEnv) {
133
141
  try {
134
142
  importedModule = await importExternalProviderModule(modulePath);
135
143
  } catch (error) {
136
- throw new Error(`Failed to load external AI SDK provider module ${modulePath}: ${error?.message || error}`);
144
+ throw new Error(formatExternalProviderLoadError(modulePath, error));
137
145
  }
138
146
  const providers = Array.isArray(importedModule?.providers) ? importedModule.providers : [];
139
147
  if (providers.length === 0) {