@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.
- package/bin/conductor-daemon.js +8 -8
- package/package.json +5 -5
- package/src/daemon.js +136 -26
package/bin/conductor-daemon.js
CHANGED
|
@@ -48,19 +48,19 @@ function resolveLauncherConfig() {
|
|
|
48
48
|
|
|
49
49
|
if (inheritedLauncherScript && inheritedSubcommand === "daemon" && inheritedSubcommandArgs) {
|
|
50
50
|
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
4
|
-
"gitCommitId": "
|
|
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.
|
|
22
|
-
"@love-moon/ai-sdk": "0.2.
|
|
23
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
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
|
|
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(
|
|
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(`[
|
|
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(`[
|
|
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 (
|
|
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
|
-
|
|
1972
|
-
await shutdownDaemon("auto-update");
|
|
1981
|
+
await shutdownDaemon(reason);
|
|
1973
1982
|
|
|
1974
|
-
|
|
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(`[
|
|
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]
|
|
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()) {
|