@love-moon/conductor-cli 0.2.35 → 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 +6 -4
- package/src/ai-manager-handlers.js +158 -0
- package/src/daemon.js +144 -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,8 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@love-moon/ai-bridge": "0.1.4",
|
|
21
|
-
"@love-moon/ai-
|
|
22
|
-
"@love-moon/
|
|
21
|
+
"@love-moon/ai-manager": "0.2.37",
|
|
22
|
+
"@love-moon/ai-sdk": "0.2.37",
|
|
23
|
+
"@love-moon/conductor-sdk": "0.2.37",
|
|
23
24
|
"chrome-launcher": "^1.2.1",
|
|
24
25
|
"chrome-remote-interface": "^0.33.0",
|
|
25
26
|
"dotenv": "^16.4.5",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
],
|
|
40
41
|
"overrides": {
|
|
41
42
|
"@love-moon/ai-sdk": "file:../modules/ai-sdk",
|
|
43
|
+
"@love-moon/ai-manager": "file:../modules/ai-manager",
|
|
42
44
|
"@love-moon/conductor-sdk": "file:../modules/conductor-sdk"
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Daemon-side glue between the realtime WebSocket and the @love-moon/ai-manager module.
|
|
2
|
+
// The web backend sends `ai_manager_request`; we dispatch by `action`, run it, and
|
|
3
|
+
// reply with `ai_manager_response` carrying the same `request_id`.
|
|
4
|
+
|
|
5
|
+
import { AiManager } from "@love-moon/ai-manager";
|
|
6
|
+
|
|
7
|
+
const VALID_ACTIONS = new Set(["status", "quota", "list_accounts", "switch_account"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {string} [opts.configPath] Path to ~/.conductor/config.yaml. Defaults to ai-manager's default.
|
|
12
|
+
*/
|
|
13
|
+
export function createAiManagerHandlers(opts = {}) {
|
|
14
|
+
const manager = new AiManager(opts.configPath ? { configPath: opts.configPath } : undefined);
|
|
15
|
+
|
|
16
|
+
async function status() {
|
|
17
|
+
// Probe install first; only check network for tools that are actually
|
|
18
|
+
// present so we don't pay an outbound HTTP timeout for a CLI the user
|
|
19
|
+
// never installed.
|
|
20
|
+
const [install, current] = await Promise.all([
|
|
21
|
+
manager.checkInstallAll(),
|
|
22
|
+
manager.getCurrentCodexAccount().catch(() => null),
|
|
23
|
+
]);
|
|
24
|
+
const network = {};
|
|
25
|
+
const tools = ["codex", "claude", "kimi"];
|
|
26
|
+
await Promise.all(
|
|
27
|
+
tools.map(async (tool) => {
|
|
28
|
+
if (install[tool]?.installed) {
|
|
29
|
+
network[tool] = await manager.checkNetwork(tool);
|
|
30
|
+
} else {
|
|
31
|
+
network[tool] = {
|
|
32
|
+
reachable: false,
|
|
33
|
+
endpoint: "",
|
|
34
|
+
error: "not installed",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
return { install, network, currentCodexAccount: current };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function quota(args = {}) {
|
|
43
|
+
const tools = pickToolFilter(args);
|
|
44
|
+
const out = {};
|
|
45
|
+
if (tools.has("codex")) {
|
|
46
|
+
try {
|
|
47
|
+
out.codex = await manager.getCodexQuota({
|
|
48
|
+
forceRefresh: Boolean(args.forceRefresh),
|
|
49
|
+
});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
out.codex = { tool: "codex", error: errMsg(err), source: "unknown" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (tools.has("claude")) {
|
|
55
|
+
try {
|
|
56
|
+
out.claude = await manager.getClaudeQuota({
|
|
57
|
+
forceRefresh: Boolean(args.forceRefresh),
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
out.claude = { tool: "claude", error: errMsg(err), source: "unknown" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (tools.has("kimi")) {
|
|
64
|
+
try {
|
|
65
|
+
out.kimi = await manager.getKimiQuota({
|
|
66
|
+
forceRefresh: Boolean(args.forceRefresh),
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
out.kimi = { tool: "kimi", error: errMsg(err), source: "unknown" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function listAccounts() {
|
|
76
|
+
return { accounts: await manager.listCodexAccounts() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function switchAccount(args = {}) {
|
|
80
|
+
if (!args.name || typeof args.name !== "string") {
|
|
81
|
+
throw new Error("switch_account requires a `name` string");
|
|
82
|
+
}
|
|
83
|
+
return await manager.switchCodexAccount(args.name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run a single action and return a `result` object, never throwing.
|
|
88
|
+
* @param {{action:string,args?:object}} payload
|
|
89
|
+
*/
|
|
90
|
+
async function dispatch(payload) {
|
|
91
|
+
const action = payload?.action;
|
|
92
|
+
if (!VALID_ACTIONS.has(action)) {
|
|
93
|
+
return { error: `unknown action: ${action}` };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
switch (action) {
|
|
97
|
+
case "status":
|
|
98
|
+
return { result: await status() };
|
|
99
|
+
case "quota":
|
|
100
|
+
return { result: await quota(payload?.args ?? {}) };
|
|
101
|
+
case "list_accounts":
|
|
102
|
+
return { result: await listAccounts() };
|
|
103
|
+
case "switch_account":
|
|
104
|
+
return { result: await switchAccount(payload?.args ?? {}) };
|
|
105
|
+
default:
|
|
106
|
+
return { error: `unhandled action: ${action}` };
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return { error: errMsg(err) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { dispatch, manager };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wire a handler against an event payload from the web backend, sending the
|
|
118
|
+
* response back through `client.sendJson` once the action completes.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} client - conductor websocket client (must have sendJson)
|
|
121
|
+
* @param {ReturnType<typeof createAiManagerHandlers>} handlers
|
|
122
|
+
* @param {object} payload - event.payload with shape { request_id, action, args }
|
|
123
|
+
*/
|
|
124
|
+
export async function handleAiManagerRequest(client, handlers, payload) {
|
|
125
|
+
const requestId = payload?.request_id ? String(payload.request_id) : "";
|
|
126
|
+
const action = payload?.action ? String(payload.action) : "";
|
|
127
|
+
if (!requestId) {
|
|
128
|
+
// Without a request_id we cannot route the response anywhere; drop and log upstream.
|
|
129
|
+
return { error: "missing request_id" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const out = await handlers.dispatch({ action, args: payload?.args });
|
|
133
|
+
await client
|
|
134
|
+
.sendJson({
|
|
135
|
+
type: "ai_manager_response",
|
|
136
|
+
payload: {
|
|
137
|
+
request_id: requestId,
|
|
138
|
+
action,
|
|
139
|
+
result: out.result,
|
|
140
|
+
error: out.error,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {});
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function pickToolFilter(args) {
|
|
148
|
+
const t = args?.tool;
|
|
149
|
+
if (t === "codex") return new Set(["codex"]);
|
|
150
|
+
if (t === "claude") return new Set(["claude"]);
|
|
151
|
+
if (t === "kimi") return new Set(["kimi"]);
|
|
152
|
+
return new Set(["codex", "claude", "kimi"]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function errMsg(err) {
|
|
156
|
+
if (err instanceof Error) return err.message;
|
|
157
|
+
return String(err);
|
|
158
|
+
}
|
package/src/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ProjectContext,
|
|
17
17
|
} from "@love-moon/conductor-sdk";
|
|
18
18
|
import { DaemonLogCollector } from "./log-collector.js";
|
|
19
|
+
import { createAiManagerHandlers, handleAiManagerRequest } from "./ai-manager-handlers.js";
|
|
19
20
|
import { resolveResumeContext } from "./fire/resume.js";
|
|
20
21
|
import {
|
|
21
22
|
filterRuntimeSupportedAllowCliList,
|
|
@@ -690,6 +691,17 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
690
691
|
autoUpdateSupportedInstall &&
|
|
691
692
|
(process.env.CONDUCTOR_AUTO_UPDATE !== "false") &&
|
|
692
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;
|
|
693
705
|
const UPDATE_WINDOW = parseUpdateWindowFn(
|
|
694
706
|
process.env.CONDUCTOR_UPDATE_WINDOW || userConfig.update_window || "02:00-04:00"
|
|
695
707
|
);
|
|
@@ -1449,13 +1461,15 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1449
1461
|
"x-conductor-backends": SUPPORTED_BACKENDS.join(","),
|
|
1450
1462
|
"x-conductor-version": cliVersion,
|
|
1451
1463
|
};
|
|
1452
|
-
const advertisedCapabilities = ["project_path_validation"];
|
|
1464
|
+
const advertisedCapabilities = ["project_path_validation", "restart_daemon"];
|
|
1453
1465
|
if (ptyTaskCapabilityEnabled) {
|
|
1454
1466
|
advertisedCapabilities.push("pty_task", "terminal_snapshot");
|
|
1455
1467
|
}
|
|
1456
1468
|
if (advertisedCapabilities.length > 0) {
|
|
1457
1469
|
extraHeaders["x-conductor-capabilities"] = advertisedCapabilities.join(",");
|
|
1458
1470
|
}
|
|
1471
|
+
const aiManagerHandlers = createAiManagerHandlers({ configPath: config.CONFIG_FILE });
|
|
1472
|
+
|
|
1459
1473
|
const client = createWebSocketClient(sdkConfig, {
|
|
1460
1474
|
extraHeaders,
|
|
1461
1475
|
onConnected: ({ isReconnect, connectedAt } = { isReconnect: false, connectedAt: Date.now() }) => {
|
|
@@ -1894,7 +1908,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1894
1908
|
return newPkg.version || null;
|
|
1895
1909
|
}
|
|
1896
1910
|
|
|
1897
|
-
async function
|
|
1911
|
+
async function installCliVersion(targetVersion, tag) {
|
|
1898
1912
|
const pm = detectPackageManagerFn({
|
|
1899
1913
|
launcherPath: restartLauncherScript || versionCheckScript,
|
|
1900
1914
|
packageRoot: installedPackageRoot,
|
|
@@ -1902,7 +1916,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1902
1916
|
const pkgSpec = `${PACKAGE_NAME}@${targetVersion}`;
|
|
1903
1917
|
|
|
1904
1918
|
if (pm === "pnpm") {
|
|
1905
|
-
log(
|
|
1919
|
+
log(`[${tag}] Preparing pnpm native dependency allowlist for node-pty`);
|
|
1906
1920
|
await ensurePnpmOnlyBuiltDependencies({
|
|
1907
1921
|
runCommand: runBufferedCommand,
|
|
1908
1922
|
dependencies: ["node-pty"],
|
|
@@ -1910,9 +1924,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1910
1924
|
});
|
|
1911
1925
|
}
|
|
1912
1926
|
|
|
1913
|
-
log(`[
|
|
1914
|
-
|
|
1915
|
-
// Step 1: install
|
|
1927
|
+
log(`[${tag}] Installing ${pkgSpec} via ${pm}...`);
|
|
1916
1928
|
const result = await runInstallCommand(pm, pkgSpec);
|
|
1917
1929
|
if (!result.success) {
|
|
1918
1930
|
throw new Error(
|
|
@@ -1920,13 +1932,6 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1920
1932
|
);
|
|
1921
1933
|
}
|
|
1922
1934
|
|
|
1923
|
-
// Step 2: re-check active tasks — a task may have arrived during the install
|
|
1924
|
-
if (hasActiveTasks()) {
|
|
1925
|
-
log("[auto-update] Active tasks appeared during install; aborting restart (will retry later)");
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
// Step 3: verify installed version using the globally resolved CLI entry point.
|
|
1930
1935
|
try {
|
|
1931
1936
|
const installedVersion = await readInstalledCliVersion();
|
|
1932
1937
|
if (installedVersion !== targetVersion) {
|
|
@@ -1938,7 +1943,6 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1938
1943
|
throw new Error(`Version verification failed: ${verifyErr?.message || verifyErr}`);
|
|
1939
1944
|
}
|
|
1940
1945
|
|
|
1941
|
-
// Step 4: repair and verify native dependencies before shutting down the healthy daemon.
|
|
1942
1946
|
try {
|
|
1943
1947
|
await repairAndVerifyGlobalNodePty({
|
|
1944
1948
|
packageManager: pm,
|
|
@@ -1950,26 +1954,33 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1950
1954
|
throw new Error(`Native dependency verification failed: ${verifyErr?.message || verifyErr}`);
|
|
1951
1955
|
}
|
|
1952
1956
|
|
|
1953
|
-
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
|
+
}
|
|
1954
1965
|
|
|
1955
1966
|
let logFd = null;
|
|
1956
|
-
if (
|
|
1957
|
-
if (!restartLauncherScript) {
|
|
1958
|
-
throw new Error("Missing daemon restart launcher script");
|
|
1959
|
-
}
|
|
1967
|
+
if (shouldRespawn) {
|
|
1960
1968
|
try {
|
|
1961
1969
|
mkdirSyncFn(DAEMON_LOG_DIR, { recursive: true });
|
|
1962
1970
|
} catch {
|
|
1963
1971
|
/* ignore */
|
|
1964
1972
|
}
|
|
1965
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
|
+
}
|
|
1966
1979
|
}
|
|
1967
1980
|
|
|
1968
|
-
|
|
1969
|
-
await shutdownDaemon("auto-update");
|
|
1981
|
+
await shutdownDaemon(reason);
|
|
1970
1982
|
|
|
1971
|
-
|
|
1972
|
-
if (isBackgroundProcess) {
|
|
1983
|
+
if (shouldRespawn) {
|
|
1973
1984
|
const handoffToken = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1974
1985
|
const handoffExpiresAt = Date.now() + 15_000;
|
|
1975
1986
|
try {
|
|
@@ -1989,7 +2000,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
1989
2000
|
},
|
|
1990
2001
|
});
|
|
1991
2002
|
child.unref();
|
|
1992
|
-
log(`[
|
|
2003
|
+
log(`[${reason}] New daemon spawned (PID ${child.pid})`);
|
|
1993
2004
|
} catch (error) {
|
|
1994
2005
|
cleanupLock();
|
|
1995
2006
|
exitFn(1);
|
|
@@ -2000,11 +2011,108 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
2000
2011
|
}
|
|
2001
2012
|
}
|
|
2002
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) {
|
|
2003
2023
|
log(
|
|
2004
|
-
`[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.`
|
|
2005
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;
|
|
2006
2115
|
}
|
|
2007
|
-
exitFn(0);
|
|
2008
2116
|
}
|
|
2009
2117
|
|
|
2010
2118
|
const getActiveTaskIds = () => [
|
|
@@ -3431,6 +3539,16 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3431
3539
|
if (event.type === "validate_project_path") {
|
|
3432
3540
|
void handleValidateProjectPath(event.payload);
|
|
3433
3541
|
}
|
|
3542
|
+
if (event.type === "ai_manager_request") {
|
|
3543
|
+
handleAiManagerRequest(client, aiManagerHandlers, event.payload).catch((error) => {
|
|
3544
|
+
logError(`Unhandled ai_manager_request failure: ${error?.message || error}`);
|
|
3545
|
+
});
|
|
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
|
+
}
|
|
3434
3552
|
}
|
|
3435
3553
|
|
|
3436
3554
|
function markWatchdogHealthy(signal, at = Date.now()) {
|