@love-moon/conductor-cli 0.2.36 → 0.2.37

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.
@@ -48,19 +48,19 @@ function resolveLauncherConfig() {
48
48
 
49
49
  if (inheritedLauncherScript && inheritedSubcommand === "daemon" && inheritedSubcommandArgs) {
50
50
  return {
51
- restartLauncherScript: inheritedLauncherScript,
52
- restartLauncherArgs: ["daemon", ...stripNohupArgs(inheritedSubcommandArgs)],
53
- versionCheckScript: inheritedLauncherScript,
54
- versionCheckArgs: ["--version"],
51
+ RESTART_LAUNCHER_SCRIPT: inheritedLauncherScript,
52
+ RESTART_LAUNCHER_ARGS: ["daemon", ...stripNohupArgs(inheritedSubcommandArgs)],
53
+ VERSION_CHECK_SCRIPT: inheritedLauncherScript,
54
+ VERSION_CHECK_ARGS: ["--version"],
55
55
  };
56
56
  }
57
57
 
58
58
  const daemonScript = path.resolve(process.argv[1]);
59
59
  return {
60
- restartLauncherScript: daemonScript,
61
- restartLauncherArgs: argv,
62
- versionCheckScript: inheritedLauncherScript,
63
- versionCheckArgs: ["--version"],
60
+ RESTART_LAUNCHER_SCRIPT: daemonScript,
61
+ RESTART_LAUNCHER_ARGS: argv,
62
+ VERSION_CHECK_SCRIPT: inheritedLauncherScript,
63
+ VERSION_CHECK_ARGS: ["--version"],
64
64
  };
65
65
  }
66
66
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.36",
4
- "gitCommitId": "54d9de4",
3
+ "version": "0.2.37",
4
+ "gitCommitId": "c656a7d",
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.36",
22
- "@love-moon/ai-sdk": "0.2.36",
23
- "@love-moon/conductor-sdk": "0.2.36",
21
+ "@love-moon/ai-manager": "0.2.37",
22
+ "@love-moon/ai-sdk": "0.2.37",
23
+ "@love-moon/conductor-sdk": "0.2.37",
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
@@ -691,6 +691,17 @@ export function startDaemon(config = {}, deps = {}) {
691
691
  autoUpdateSupportedInstall &&
692
692
  (process.env.CONDUCTOR_AUTO_UPDATE !== "false") &&
693
693
  (userConfig.auto_update !== false);
694
+ // Auto-update respawn was historically broken in prod (camelCase/UPPER_SNAKE
695
+ // config-key mismatch between conductor-daemon.js and daemon.js), so no fleet
696
+ // has actually restarted itself via this path. The key mismatch is now fixed,
697
+ // which means auto-update would start respawning daemons globally. To avoid a
698
+ // silent activation, keep respawn gated behind an explicit opt-in until it
699
+ // has been validated on a canary.
700
+ const AUTO_UPDATE_RESPAWN_ENABLED =
701
+ config.AUTO_UPDATE_RESPAWN === true ||
702
+ config.AUTO_UPDATE_RESPAWN === "true" ||
703
+ process.env.CONDUCTOR_AUTO_UPDATE_RESPAWN === "true" ||
704
+ userConfig.auto_update_respawn === true;
694
705
  const UPDATE_WINDOW = parseUpdateWindowFn(
695
706
  process.env.CONDUCTOR_UPDATE_WINDOW || userConfig.update_window || "02:00-04:00"
696
707
  );
@@ -1450,7 +1461,7 @@ export function startDaemon(config = {}, deps = {}) {
1450
1461
  "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
1451
1462
  "x-conductor-version": cliVersion,
1452
1463
  };
1453
- const advertisedCapabilities = ["project_path_validation"];
1464
+ const advertisedCapabilities = ["project_path_validation", "restart_daemon"];
1454
1465
  if (ptyTaskCapabilityEnabled) {
1455
1466
  advertisedCapabilities.push("pty_task", "terminal_snapshot");
1456
1467
  }
@@ -1897,7 +1908,7 @@ export function startDaemon(config = {}, deps = {}) {
1897
1908
  return newPkg.version || null;
1898
1909
  }
1899
1910
 
1900
- async function performAutoUpdate(targetVersion) {
1911
+ async function installCliVersion(targetVersion, tag) {
1901
1912
  const pm = detectPackageManagerFn({
1902
1913
  launcherPath: restartLauncherScript || versionCheckScript,
1903
1914
  packageRoot: installedPackageRoot,
@@ -1905,7 +1916,7 @@ export function startDaemon(config = {}, deps = {}) {
1905
1916
  const pkgSpec = `${PACKAGE_NAME}@${targetVersion}`;
1906
1917
 
1907
1918
  if (pm === "pnpm") {
1908
- log("[auto-update] Preparing pnpm native dependency allowlist for node-pty");
1919
+ log(`[${tag}] Preparing pnpm native dependency allowlist for node-pty`);
1909
1920
  await ensurePnpmOnlyBuiltDependencies({
1910
1921
  runCommand: runBufferedCommand,
1911
1922
  dependencies: ["node-pty"],
@@ -1913,9 +1924,7 @@ export function startDaemon(config = {}, deps = {}) {
1913
1924
  });
1914
1925
  }
1915
1926
 
1916
- log(`[auto-update] Installing ${pkgSpec} via ${pm}...`);
1917
-
1918
- // Step 1: install
1927
+ log(`[${tag}] Installing ${pkgSpec} via ${pm}...`);
1919
1928
  const result = await runInstallCommand(pm, pkgSpec);
1920
1929
  if (!result.success) {
1921
1930
  throw new Error(
@@ -1923,13 +1932,6 @@ export function startDaemon(config = {}, deps = {}) {
1923
1932
  );
1924
1933
  }
1925
1934
 
1926
- // Step 2: re-check active tasks — a task may have arrived during the install
1927
- if (hasActiveTasks()) {
1928
- log("[auto-update] Active tasks appeared during install; aborting restart (will retry later)");
1929
- return;
1930
- }
1931
-
1932
- // Step 3: verify installed version using the globally resolved CLI entry point.
1933
1935
  try {
1934
1936
  const installedVersion = await readInstalledCliVersion();
1935
1937
  if (installedVersion !== targetVersion) {
@@ -1941,7 +1943,6 @@ export function startDaemon(config = {}, deps = {}) {
1941
1943
  throw new Error(`Version verification failed: ${verifyErr?.message || verifyErr}`);
1942
1944
  }
1943
1945
 
1944
- // Step 4: repair and verify native dependencies before shutting down the healthy daemon.
1945
1946
  try {
1946
1947
  await repairAndVerifyGlobalNodePty({
1947
1948
  packageManager: pm,
@@ -1953,26 +1954,33 @@ export function startDaemon(config = {}, deps = {}) {
1953
1954
  throw new Error(`Native dependency verification failed: ${verifyErr?.message || verifyErr}`);
1954
1955
  }
1955
1956
 
1956
- log(`[auto-update] Verified ${targetVersion} and node-pty. Restarting daemon...`);
1957
+ log(`[${tag}] Installed and verified ${targetVersion} (node-pty OK)`);
1958
+ }
1959
+
1960
+ async function restartDaemonProcess(reason, { allowForegroundRespawn = false } = {}) {
1961
+ const shouldRespawn = isBackgroundProcess || allowForegroundRespawn;
1962
+ if (shouldRespawn && !restartLauncherScript) {
1963
+ throw new Error("Missing daemon restart launcher script");
1964
+ }
1957
1965
 
1958
1966
  let logFd = null;
1959
- if (isBackgroundProcess) {
1960
- if (!restartLauncherScript) {
1961
- throw new Error("Missing daemon restart launcher script");
1962
- }
1967
+ if (shouldRespawn) {
1963
1968
  try {
1964
1969
  mkdirSyncFn(DAEMON_LOG_DIR, { recursive: true });
1965
1970
  } catch {
1966
1971
  /* ignore */
1967
1972
  }
1968
1973
  logFd = fs.openSync(DAEMON_LOG_PATH, "a");
1974
+ if (!isBackgroundProcess) {
1975
+ log(
1976
+ `[${reason}] Foreground daemon will be respawned in background. Logs: ${DAEMON_LOG_PATH}`
1977
+ );
1978
+ }
1969
1979
  }
1970
1980
 
1971
- // Step 5: graceful shutdown
1972
- await shutdownDaemon("auto-update");
1981
+ await shutdownDaemon(reason);
1973
1982
 
1974
- // Step 6: re-spawn (only in background/nohup mode)
1975
- if (isBackgroundProcess) {
1983
+ if (shouldRespawn) {
1976
1984
  const handoffToken = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1977
1985
  const handoffExpiresAt = Date.now() + 15_000;
1978
1986
  try {
@@ -1992,7 +2000,7 @@ export function startDaemon(config = {}, deps = {}) {
1992
2000
  },
1993
2001
  });
1994
2002
  child.unref();
1995
- log(`[auto-update] New daemon spawned (PID ${child.pid})`);
2003
+ log(`[${reason}] New daemon spawned (PID ${child.pid})`);
1996
2004
  } catch (error) {
1997
2005
  cleanupLock();
1998
2006
  exitFn(1);
@@ -2003,11 +2011,108 @@ export function startDaemon(config = {}, deps = {}) {
2003
2011
  }
2004
2012
  }
2005
2013
  } else {
2014
+ log(`[${reason}] Foreground mode — please restart the daemon manually.`);
2015
+ }
2016
+ exitFn(0);
2017
+ }
2018
+
2019
+ async function performAutoUpdate(targetVersion) {
2020
+ await installCliVersion(targetVersion, "auto-update");
2021
+
2022
+ if (!AUTO_UPDATE_RESPAWN_ENABLED) {
2006
2023
  log(
2007
- `[auto-update] Updated to ${targetVersion}. Foreground mode please restart the daemon.`
2024
+ `[auto-update] Installed ${targetVersion}. Respawn is gated off (set CONDUCTOR_AUTO_UPDATE_RESPAWN=true to enable); new version will take effect on next manual restart.`
2008
2025
  );
2026
+ return;
2027
+ }
2028
+
2029
+ // Re-check active tasks — a task may have arrived during the install
2030
+ if (hasActiveTasks()) {
2031
+ log("[auto-update] Active tasks appeared during install; aborting restart (will retry later)");
2032
+ return;
2033
+ }
2034
+
2035
+ log(`[auto-update] Restarting daemon after upgrade to ${targetVersion}...`);
2036
+ await restartDaemonProcess("auto-update");
2037
+ }
2038
+
2039
+ async function handleRestartDaemon(payload) {
2040
+ const requestId = payload?.request_id ? String(payload.request_id) : "";
2041
+ const targetVersionRaw = payload?.target_version
2042
+ ? String(payload.target_version).trim()
2043
+ : "latest";
2044
+
2045
+ if (daemonShuttingDown) {
2046
+ log(`[restart_daemon] Ignored (${requestId}): daemon already shutting down`);
2047
+ sendAgentCommandAck({
2048
+ requestId,
2049
+ eventType: "restart_daemon",
2050
+ accepted: false,
2051
+ }).catch(() => {});
2052
+ return;
2053
+ }
2054
+ if (autoUpdateInProgress) {
2055
+ log(`[restart_daemon] Ignored (${requestId}): restart already in progress`);
2056
+ sendAgentCommandAck({
2057
+ requestId,
2058
+ eventType: "restart_daemon",
2059
+ accepted: false,
2060
+ }).catch(() => {});
2061
+ return;
2062
+ }
2063
+
2064
+ autoUpdateInProgress = true;
2065
+ try {
2066
+ log(
2067
+ `[restart_daemon] Received (request_id=${requestId}, target=${targetVersionRaw}, current=${cliVersion})`
2068
+ );
2069
+ // Ack receipt before blocking work so the server learns this daemon
2070
+ // accepted the command even if install/shutdown takes several seconds.
2071
+ await sendAgentCommandAck({
2072
+ requestId,
2073
+ eventType: "restart_daemon",
2074
+ accepted: true,
2075
+ }).catch(() => {});
2076
+
2077
+ let resolvedTarget = null;
2078
+ if (targetVersionRaw === "latest") {
2079
+ try {
2080
+ const latest = await fetchLatestVersionFn();
2081
+ if (latest && SEMVER_RE.test(latest) && isNewerVersionFn(latest, cliVersion)) {
2082
+ resolvedTarget = latest;
2083
+ } else if (latest) {
2084
+ log(`[restart_daemon] Already on latest (${cliVersion}); plain restart`);
2085
+ }
2086
+ } catch (err) {
2087
+ logError(`[restart_daemon] Failed to fetch latest version: ${err?.message || err}`);
2088
+ }
2089
+ } else if (SEMVER_RE.test(targetVersionRaw) && targetVersionRaw !== cliVersion) {
2090
+ resolvedTarget = targetVersionRaw;
2091
+ }
2092
+
2093
+ if (resolvedTarget) {
2094
+ try {
2095
+ await installCliVersion(resolvedTarget, "restart_daemon");
2096
+ } catch (err) {
2097
+ logError(
2098
+ `[restart_daemon] Install failed, falling back to plain restart: ${err?.message || err}`
2099
+ );
2100
+ }
2101
+ }
2102
+
2103
+ try {
2104
+ await restartDaemonProcess("restart_daemon", { allowForegroundRespawn: true });
2105
+ } catch (err) {
2106
+ logError(`[restart_daemon] Restart failed after shutdown; exiting: ${err?.message || err}`);
2107
+ cleanupLock();
2108
+ exitFn(1);
2109
+ }
2110
+ } finally {
2111
+ // Clear in case restartDaemonProcess never actually exited (e.g. in tests
2112
+ // where exitFn is mocked). In real runtime exitFn is process.exit, so
2113
+ // this line is unreachable on both success and failure paths.
2114
+ autoUpdateInProgress = false;
2009
2115
  }
2010
- exitFn(0);
2011
2116
  }
2012
2117
 
2013
2118
  const getActiveTaskIds = () => [
@@ -3439,6 +3544,11 @@ export function startDaemon(config = {}, deps = {}) {
3439
3544
  logError(`Unhandled ai_manager_request failure: ${error?.message || error}`);
3440
3545
  });
3441
3546
  }
3547
+ if (event.type === "restart_daemon") {
3548
+ void handleRestartDaemon(event.payload).catch((error) => {
3549
+ logError(`Unhandled restart_daemon failure: ${error?.message || error}`);
3550
+ });
3551
+ }
3442
3552
  }
3443
3553
 
3444
3554
  function markWatchdogHealthy(signal, at = Date.now()) {