helloloop 0.8.6 → 0.10.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +230 -498
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
- package/native/windows-hidden-shell-proxy/Program.cs +498 -0
- package/package.json +4 -2
- package/src/activity_projection.mjs +294 -0
- package/src/analyze_confirmation.mjs +3 -1
- package/src/analyzer.mjs +2 -1
- package/src/auto_execution_options.mjs +13 -0
- package/src/background_launch.mjs +73 -0
- package/src/cli.mjs +51 -1
- package/src/cli_analyze_command.mjs +12 -14
- package/src/cli_args.mjs +106 -32
- package/src/cli_command_handlers.mjs +73 -25
- package/src/cli_support.mjs +2 -0
- package/src/common.mjs +11 -0
- package/src/dashboard_command.mjs +371 -0
- package/src/dashboard_tui.mjs +289 -0
- package/src/dashboard_web.mjs +351 -0
- package/src/dashboard_web_client.mjs +167 -0
- package/src/dashboard_web_page.mjs +49 -0
- package/src/engine_event_parser_codex.mjs +167 -0
- package/src/engine_process_support.mjs +7 -2
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +53 -44
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -30
- package/src/install_shared.mjs +9 -0
- package/src/node_process_launch.mjs +28 -0
- package/src/process.mjs +2 -0
- package/src/runner_execute_task.mjs +15 -1
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +5 -0
- package/src/runner_status.mjs +72 -4
- package/src/runtime_engine_support.mjs +52 -5
- package/src/runtime_engine_task.mjs +7 -0
- package/src/runtime_settings.mjs +105 -0
- package/src/runtime_settings_loader.mjs +19 -0
- package/src/shell_invocation.mjs +227 -9
- package/src/supervisor_cli_support.mjs +49 -0
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +142 -83
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +364 -0
- package/src/terminal_session_limits.mjs +1 -21
- package/src/windows_hidden_shell_proxy.mjs +405 -0
- package/src/workspace_registry.mjs +155 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { nowIso, readJson, sleep, writeJson } from "./common.mjs";
|
|
4
|
+
import { createContext } from "./context.mjs";
|
|
5
|
+
import { refreshHostContinuationArtifacts } from "./host_continuation.mjs";
|
|
6
|
+
import { isHostLeaseAlive } from "./host_lease.mjs";
|
|
7
|
+
import { spawnNodeProcess } from "./node_process_launch.mjs";
|
|
8
|
+
import {
|
|
9
|
+
FINAL_SUPERVISOR_STATUSES,
|
|
10
|
+
readJsonIfExists,
|
|
11
|
+
readSupervisorState,
|
|
12
|
+
writeActiveSupervisorState,
|
|
13
|
+
writeSupervisorState,
|
|
14
|
+
} from "./supervisor_state.mjs";
|
|
15
|
+
import { bindBackgroundTerminalSession } from "./terminal_session_limits.mjs";
|
|
16
|
+
import { loadRuntimeSettings } from "./runtime_settings_loader.mjs";
|
|
17
|
+
import { hasRetryBudget, pickRetryDelaySeconds } from "./runtime_settings.mjs";
|
|
18
|
+
import { registerActiveSession, unregisterActiveSession } from "./workspace_registry.mjs";
|
|
19
|
+
|
|
20
|
+
const GUARDIAN_ACTIVE_ENV = "HELLOLOOP_SUPERVISOR_GUARDIAN_ACTIVE";
|
|
21
|
+
const GUARDIAN_PID_ENV = "HELLOLOOP_SUPERVISOR_GUARDIAN_PID";
|
|
22
|
+
|
|
23
|
+
function refreshContinuation(context, sessionId) {
|
|
24
|
+
try {
|
|
25
|
+
refreshHostContinuationArtifacts(context, { sessionId });
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore continuation snapshot refresh failures during guardian lifecycle
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readFinalOutcome(context, sessionId) {
|
|
32
|
+
const result = readJsonIfExists(context.supervisorResultFile);
|
|
33
|
+
if (result?.sessionId === sessionId) {
|
|
34
|
+
return {
|
|
35
|
+
sessionId,
|
|
36
|
+
ok: result.ok === true,
|
|
37
|
+
exitCode: Number(result.exitCode ?? (result.ok === true ? 0 : 1)),
|
|
38
|
+
result,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const state = readSupervisorState(context);
|
|
43
|
+
if (state?.sessionId === sessionId && FINAL_SUPERVISOR_STATUSES.has(String(state.status || ""))) {
|
|
44
|
+
return {
|
|
45
|
+
sessionId,
|
|
46
|
+
ok: state.status === "completed",
|
|
47
|
+
exitCode: Number(state.exitCode || (state.status === "completed" ? 0 : 1)),
|
|
48
|
+
result: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeLeaseStopped(context, request, message) {
|
|
56
|
+
const payload = {
|
|
57
|
+
sessionId: request.sessionId,
|
|
58
|
+
command: request.command,
|
|
59
|
+
exitCode: 1,
|
|
60
|
+
ok: false,
|
|
61
|
+
stopped: true,
|
|
62
|
+
error: message,
|
|
63
|
+
};
|
|
64
|
+
writeJson(context.supervisorResultFile, payload);
|
|
65
|
+
writeSupervisorState(context, {
|
|
66
|
+
sessionId: request.sessionId,
|
|
67
|
+
command: request.command,
|
|
68
|
+
status: "stopped",
|
|
69
|
+
exitCode: payload.exitCode,
|
|
70
|
+
message,
|
|
71
|
+
completedAt: nowIso(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeKeepAliveFailure(context, request, message, restartCount, keepAliveEnabled) {
|
|
76
|
+
const payload = {
|
|
77
|
+
sessionId: request.sessionId,
|
|
78
|
+
command: request.command,
|
|
79
|
+
ok: false,
|
|
80
|
+
exitCode: 1,
|
|
81
|
+
error: message,
|
|
82
|
+
};
|
|
83
|
+
writeJson(context.supervisorResultFile, payload);
|
|
84
|
+
writeSupervisorState(context, {
|
|
85
|
+
sessionId: request.sessionId,
|
|
86
|
+
command: request.command,
|
|
87
|
+
status: "failed",
|
|
88
|
+
exitCode: 1,
|
|
89
|
+
message,
|
|
90
|
+
guardianPid: process.pid,
|
|
91
|
+
workerPid: 0,
|
|
92
|
+
guardianRestartCount: restartCount,
|
|
93
|
+
keepAliveEnabled,
|
|
94
|
+
completedAt: nowIso(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildWorkerArgs(context, sessionFile) {
|
|
99
|
+
return [
|
|
100
|
+
path.join(context.bundleRoot, "bin", "helloloop.js"),
|
|
101
|
+
"__supervise-worker",
|
|
102
|
+
"--session-file",
|
|
103
|
+
sessionFile,
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function waitForChildExit(child) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
let settled = false;
|
|
110
|
+
child.once("error", (error) => {
|
|
111
|
+
if (!settled) {
|
|
112
|
+
settled = true;
|
|
113
|
+
reject(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
child.once("exit", (exitCode, signal) => {
|
|
117
|
+
if (!settled) {
|
|
118
|
+
settled = true;
|
|
119
|
+
resolve({
|
|
120
|
+
exitCode: Number(exitCode ?? 0),
|
|
121
|
+
signal: signal ? String(signal) : "",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeGuardianRunningState(context, request, keepAlive, restartCount, workerPid, message) {
|
|
129
|
+
writeActiveSupervisorState(context, {
|
|
130
|
+
sessionId: request.sessionId,
|
|
131
|
+
command: request.command,
|
|
132
|
+
status: "running",
|
|
133
|
+
lease: request.lease || {},
|
|
134
|
+
pid: process.pid,
|
|
135
|
+
guardianPid: process.pid,
|
|
136
|
+
workerPid,
|
|
137
|
+
guardianRestartCount: restartCount,
|
|
138
|
+
keepAliveEnabled: keepAlive.enabled,
|
|
139
|
+
message,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function runWorkerUnderGuardian(context, request, sessionFile, keepAlive, restartCount) {
|
|
144
|
+
writeGuardianRunningState(
|
|
145
|
+
context,
|
|
146
|
+
request,
|
|
147
|
+
keepAlive,
|
|
148
|
+
restartCount,
|
|
149
|
+
0,
|
|
150
|
+
restartCount > 0
|
|
151
|
+
? `后台守护进程正在执行第 ${restartCount} 次自动重拉起。`
|
|
152
|
+
: "后台守护进程已接管,正在启动执行 worker。",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const child = spawnNodeProcess({
|
|
156
|
+
args: buildWorkerArgs(context, sessionFile),
|
|
157
|
+
cwd: context.repoRoot,
|
|
158
|
+
detached: false,
|
|
159
|
+
stdio: "inherit",
|
|
160
|
+
env: {
|
|
161
|
+
HELLOLOOP_SUPERVISOR_ACTIVE: "1",
|
|
162
|
+
[GUARDIAN_ACTIVE_ENV]: "1",
|
|
163
|
+
[GUARDIAN_PID_ENV]: String(process.pid),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
writeGuardianRunningState(
|
|
168
|
+
context,
|
|
169
|
+
request,
|
|
170
|
+
keepAlive,
|
|
171
|
+
restartCount,
|
|
172
|
+
child.pid ?? 0,
|
|
173
|
+
restartCount > 0
|
|
174
|
+
? `后台守护进程已完成第 ${restartCount} 次自动重拉起,worker 正在运行。`
|
|
175
|
+
: "后台守护进程正在运行。",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return waitForChildExit(child);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function runSupervisorGuardianFromSessionFile(sessionFile) {
|
|
182
|
+
const request = readJson(sessionFile);
|
|
183
|
+
const context = createContext(request.context || {});
|
|
184
|
+
const keepAlive = loadRuntimeSettings({
|
|
185
|
+
globalConfigFile: request.options?.globalConfigFile,
|
|
186
|
+
}).supervisorKeepAlive;
|
|
187
|
+
|
|
188
|
+
bindBackgroundTerminalSession(request.terminalSessionFile || "", {
|
|
189
|
+
command: request.command,
|
|
190
|
+
repoRoot: context.repoRoot,
|
|
191
|
+
sessionId: request.sessionId,
|
|
192
|
+
});
|
|
193
|
+
registerActiveSession({
|
|
194
|
+
sessionId: request.sessionId,
|
|
195
|
+
repoRoot: context.repoRoot,
|
|
196
|
+
configDirName: context.configDirName,
|
|
197
|
+
command: request.command,
|
|
198
|
+
pid: process.pid,
|
|
199
|
+
lease: request.lease || {},
|
|
200
|
+
startedAt: nowIso(),
|
|
201
|
+
supervisorStateFile: context.supervisorStateFile,
|
|
202
|
+
supervisorResultFile: context.supervisorResultFile,
|
|
203
|
+
statusFile: context.statusFile,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
let restartCount = 0;
|
|
208
|
+
refreshContinuation(context, request.sessionId);
|
|
209
|
+
|
|
210
|
+
while (true) {
|
|
211
|
+
const finalOutcome = readFinalOutcome(context, request.sessionId);
|
|
212
|
+
if (finalOutcome) {
|
|
213
|
+
unregisterActiveSession(request.sessionId);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!isHostLeaseAlive(request.lease || {})) {
|
|
218
|
+
writeLeaseStopped(context, request, "检测到宿主窗口已关闭,HelloLoop 守护进程未继续执行。");
|
|
219
|
+
refreshContinuation(context, request.sessionId);
|
|
220
|
+
unregisterActiveSession(request.sessionId);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let workerExit;
|
|
225
|
+
try {
|
|
226
|
+
workerExit = await runWorkerUnderGuardian(context, request, sessionFile, keepAlive, restartCount);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
workerExit = {
|
|
229
|
+
exitCode: 1,
|
|
230
|
+
signal: "",
|
|
231
|
+
error: String(error?.stack || error || ""),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const outcomeAfterExit = readFinalOutcome(context, request.sessionId);
|
|
236
|
+
if (outcomeAfterExit) {
|
|
237
|
+
unregisterActiveSession(request.sessionId);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!keepAlive.enabled) {
|
|
242
|
+
writeKeepAliveFailure(
|
|
243
|
+
context,
|
|
244
|
+
request,
|
|
245
|
+
"后台守护进程检测到 worker 异常退出,但当前设置已禁用自动保活重拉起。",
|
|
246
|
+
restartCount,
|
|
247
|
+
keepAlive.enabled,
|
|
248
|
+
);
|
|
249
|
+
refreshContinuation(context, request.sessionId);
|
|
250
|
+
unregisterActiveSession(request.sessionId);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const nextRestartCount = restartCount + 1;
|
|
255
|
+
if (!hasRetryBudget(keepAlive.maxRestartCount, nextRestartCount)) {
|
|
256
|
+
writeKeepAliveFailure(
|
|
257
|
+
context,
|
|
258
|
+
request,
|
|
259
|
+
`后台守护进程自动重拉起额度已耗尽(已尝试 ${restartCount} 次),worker 仍未恢复。`,
|
|
260
|
+
restartCount,
|
|
261
|
+
keepAlive.enabled,
|
|
262
|
+
);
|
|
263
|
+
refreshContinuation(context, request.sessionId);
|
|
264
|
+
unregisterActiveSession(request.sessionId);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const delaySeconds = pickRetryDelaySeconds(keepAlive.restartDelaysSeconds, nextRestartCount);
|
|
269
|
+
const exitSummary = workerExit.error
|
|
270
|
+
? `spawn 失败:${workerExit.error}`
|
|
271
|
+
: `exit=${workerExit.exitCode}${workerExit.signal ? `, signal=${workerExit.signal}` : ""}`;
|
|
272
|
+
|
|
273
|
+
writeSupervisorState(context, {
|
|
274
|
+
sessionId: request.sessionId,
|
|
275
|
+
command: request.command,
|
|
276
|
+
status: "running",
|
|
277
|
+
lease: request.lease || {},
|
|
278
|
+
pid: process.pid,
|
|
279
|
+
guardianPid: process.pid,
|
|
280
|
+
workerPid: 0,
|
|
281
|
+
guardianRestartCount: nextRestartCount,
|
|
282
|
+
keepAliveEnabled: keepAlive.enabled,
|
|
283
|
+
lastWorkerExitCode: Number(workerExit.exitCode ?? 1),
|
|
284
|
+
lastWorkerSignal: workerExit.signal || "",
|
|
285
|
+
lastWorkerExitedAt: nowIso(),
|
|
286
|
+
message: `后台守护进程检测到 worker 异常退出(${exitSummary}),将在 ${delaySeconds} 秒后自动重拉起。`,
|
|
287
|
+
});
|
|
288
|
+
refreshContinuation(context, request.sessionId);
|
|
289
|
+
|
|
290
|
+
if (delaySeconds > 0) {
|
|
291
|
+
await sleep(delaySeconds * 1000);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
restartCount = nextRestartCount;
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
writeKeepAliveFailure(
|
|
298
|
+
context,
|
|
299
|
+
request,
|
|
300
|
+
String(error?.stack || error || ""),
|
|
301
|
+
0,
|
|
302
|
+
keepAlive.enabled,
|
|
303
|
+
);
|
|
304
|
+
refreshContinuation(context, request.sessionId);
|
|
305
|
+
unregisterActiveSession(request.sessionId);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
3
|
|
|
5
4
|
import { createContext } from "./context.mjs";
|
|
6
5
|
import { nowIso, readJson, writeJson, readTextIfExists, timestampForFile } from "./common.mjs";
|
|
6
|
+
import { refreshHostContinuationArtifacts } from "./host_continuation.mjs";
|
|
7
7
|
import { isHostLeaseAlive, renderHostLeaseLabel, resolveHostLease } from "./host_lease.mjs";
|
|
8
|
+
import { spawnNodeProcess } from "./node_process_launch.mjs";
|
|
8
9
|
import { runLoop, runOnce } from "./runner.mjs";
|
|
10
|
+
import {
|
|
11
|
+
FINAL_SUPERVISOR_STATUSES,
|
|
12
|
+
hasActiveSupervisor,
|
|
13
|
+
isTrackedPidAlive,
|
|
14
|
+
readJsonIfExists,
|
|
15
|
+
readSupervisorState,
|
|
16
|
+
writeActiveSupervisorState,
|
|
17
|
+
writeSupervisorState,
|
|
18
|
+
} from "./supervisor_state.mjs";
|
|
9
19
|
import {
|
|
10
20
|
bindBackgroundTerminalSession,
|
|
11
21
|
cancelPreparedTerminalSessionBackground,
|
|
12
22
|
finalizePreparedTerminalSessionBackground,
|
|
13
23
|
prepareCurrentTerminalSessionForBackground,
|
|
14
24
|
} from "./terminal_session_limits.mjs";
|
|
15
|
-
|
|
16
|
-
const ACTIVE_STATUSES = new Set(["launching", "running"]);
|
|
17
|
-
const FINAL_STATUSES = new Set(["completed", "failed", "stopped"]);
|
|
25
|
+
import { registerActiveSession, unregisterActiveSession } from "./workspace_registry.mjs";
|
|
18
26
|
|
|
19
27
|
function removeIfExists(filePath) {
|
|
20
28
|
try {
|
|
@@ -24,62 +32,66 @@ function removeIfExists(filePath) {
|
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
function
|
|
28
|
-
return
|
|
35
|
+
function toSerializableOptions(options = {}) {
|
|
36
|
+
return JSON.parse(JSON.stringify(options));
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
function
|
|
32
|
-
const numberPid = Number(pid || 0);
|
|
33
|
-
if (!Number.isFinite(numberPid) || numberPid <= 0) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
39
|
+
function refreshContinuation(context, sessionId) {
|
|
36
40
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return String(error?.code || "") === "EPERM";
|
|
41
|
+
refreshHostContinuationArtifacts(context, { sessionId });
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore continuation snapshot refresh failures
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
function writeStoppedState(context, request, message) {
|
|
48
|
+
const payload = {
|
|
49
|
+
sessionId: request.sessionId,
|
|
50
|
+
command: request.command,
|
|
51
|
+
exitCode: 1,
|
|
52
|
+
ok: false,
|
|
53
|
+
stopped: true,
|
|
54
|
+
error: message,
|
|
50
55
|
};
|
|
56
|
+
writeJson(context.supervisorResultFile, payload);
|
|
57
|
+
writeSupervisorState(context, {
|
|
58
|
+
sessionId: request.sessionId,
|
|
59
|
+
command: request.command,
|
|
60
|
+
status: "stopped",
|
|
61
|
+
exitCode: payload.exitCode,
|
|
62
|
+
message,
|
|
63
|
+
completedAt: nowIso(),
|
|
64
|
+
});
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export function hasActiveSupervisor(context) {
|
|
66
|
-
const state = readSupervisorState(context);
|
|
67
|
-
return Boolean(state?.status && ACTIVE_STATUSES.has(String(state.status)) && isPidAlive(state.pid));
|
|
67
|
+
function buildSupervisorSessionState(sessionId, command, lease, pid, message) {
|
|
68
|
+
return {
|
|
69
|
+
sessionId,
|
|
70
|
+
status: "running",
|
|
71
|
+
command,
|
|
72
|
+
lease,
|
|
73
|
+
startedAt: nowIso(),
|
|
74
|
+
pid,
|
|
75
|
+
guardianPid: pid,
|
|
76
|
+
workerPid: 0,
|
|
77
|
+
message,
|
|
78
|
+
};
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
export function renderSupervisorLaunchSummary(session) {
|
|
71
82
|
return [
|
|
72
|
-
`HelloLoop
|
|
83
|
+
`HelloLoop 后台守护进程已启动:${session.sessionId}`,
|
|
73
84
|
`- 宿主租约:${renderHostLeaseLabel(session.lease)}`,
|
|
74
85
|
"- 当前 turn 若被中断,只要当前宿主窗口仍存活,本轮自动执行会继续。",
|
|
86
|
+
"- 守护进程会在 worker 异常退出后按设置自动重拉起,尽量保持后台持续活跃。",
|
|
75
87
|
"- 如需主动停止,直接关闭当前 CLI 窗口即可。",
|
|
76
88
|
].join("\n");
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
export function launchSupervisedCommand(context, command, options = {}) {
|
|
80
92
|
const existing = readSupervisorState(context);
|
|
81
|
-
if (
|
|
82
|
-
throw new Error(`已有 HelloLoop supervisor 正在运行:${existing
|
|
93
|
+
if (hasActiveSupervisor(context)) {
|
|
94
|
+
throw new Error(`已有 HelloLoop supervisor 正在运行:${existing?.sessionId || "unknown"}`);
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
const sessionId = timestampForFile();
|
|
@@ -96,7 +108,10 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
96
108
|
repoRoot: context.repoRoot,
|
|
97
109
|
configDirName: context.configDirName,
|
|
98
110
|
},
|
|
99
|
-
options: toSerializableOptions(
|
|
111
|
+
options: toSerializableOptions({
|
|
112
|
+
...options,
|
|
113
|
+
supervisorSessionId: sessionId,
|
|
114
|
+
}),
|
|
100
115
|
lease,
|
|
101
116
|
terminalSessionFile: terminalSession?.file || "",
|
|
102
117
|
};
|
|
@@ -106,47 +121,62 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
106
121
|
removeIfExists(context.supervisorStdoutFile);
|
|
107
122
|
removeIfExists(context.supervisorStderrFile);
|
|
108
123
|
writeJson(context.supervisorRequestFile, request);
|
|
109
|
-
|
|
124
|
+
writeActiveSupervisorState(context, {
|
|
110
125
|
sessionId,
|
|
111
126
|
status: "launching",
|
|
112
127
|
command,
|
|
113
128
|
lease,
|
|
114
129
|
startedAt: nowIso(),
|
|
115
130
|
pid: 0,
|
|
131
|
+
guardianPid: 0,
|
|
132
|
+
workerPid: 0,
|
|
116
133
|
});
|
|
134
|
+
refreshContinuation(context, sessionId);
|
|
117
135
|
|
|
118
136
|
const stdoutFd = fs.openSync(context.supervisorStdoutFile, "w");
|
|
119
137
|
const stderrFd = fs.openSync(context.supervisorStderrFile, "w");
|
|
138
|
+
|
|
120
139
|
try {
|
|
121
|
-
const child =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
const child = spawnNodeProcess({
|
|
141
|
+
args: [
|
|
142
|
+
path.join(context.bundleRoot, "bin", "helloloop.js"),
|
|
143
|
+
"__supervise",
|
|
144
|
+
"--session-file",
|
|
145
|
+
context.supervisorRequestFile,
|
|
146
|
+
],
|
|
127
147
|
cwd: context.repoRoot,
|
|
128
148
|
detached: true,
|
|
129
|
-
shell: false,
|
|
130
|
-
windowsHide: true,
|
|
131
149
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
132
150
|
env: {
|
|
133
|
-
...process.env,
|
|
134
151
|
HELLOLOOP_SUPERVISOR_ACTIVE: "1",
|
|
135
152
|
},
|
|
136
153
|
});
|
|
154
|
+
|
|
137
155
|
finalizePreparedTerminalSessionBackground(child.pid ?? 0, {
|
|
138
156
|
command,
|
|
139
157
|
repoRoot: context.repoRoot,
|
|
140
158
|
sessionId,
|
|
141
159
|
});
|
|
142
160
|
child.unref();
|
|
143
|
-
|
|
161
|
+
|
|
162
|
+
writeActiveSupervisorState(
|
|
163
|
+
context,
|
|
164
|
+
buildSupervisorSessionState(sessionId, command, lease, child.pid ?? 0, "后台守护进程正在运行。"),
|
|
165
|
+
);
|
|
166
|
+
refreshContinuation(context, sessionId);
|
|
167
|
+
|
|
168
|
+
registerActiveSession({
|
|
144
169
|
sessionId,
|
|
145
|
-
|
|
170
|
+
repoRoot: context.repoRoot,
|
|
171
|
+
configDirName: context.configDirName,
|
|
146
172
|
command,
|
|
173
|
+
pid: child.pid ?? 0,
|
|
174
|
+
guardianPid: child.pid ?? 0,
|
|
147
175
|
lease,
|
|
148
176
|
startedAt: nowIso(),
|
|
149
|
-
|
|
177
|
+
supervisorStateFile: context.supervisorStateFile,
|
|
178
|
+
supervisorResultFile: context.supervisorResultFile,
|
|
179
|
+
statusFile: context.statusFile,
|
|
150
180
|
});
|
|
151
181
|
|
|
152
182
|
return {
|
|
@@ -155,6 +185,7 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
155
185
|
lease,
|
|
156
186
|
};
|
|
157
187
|
} catch (error) {
|
|
188
|
+
unregisterActiveSession(sessionId);
|
|
158
189
|
cancelPreparedTerminalSessionBackground({
|
|
159
190
|
command,
|
|
160
191
|
repoRoot: context.repoRoot,
|
|
@@ -177,7 +208,7 @@ export async function waitForSupervisedResult(context, session, options = {}) {
|
|
|
177
208
|
}
|
|
178
209
|
|
|
179
210
|
const state = readSupervisorState(context);
|
|
180
|
-
if (state?.sessionId === session.sessionId &&
|
|
211
|
+
if (state?.sessionId === session.sessionId && FINAL_SUPERVISOR_STATUSES.has(String(state.status || ""))) {
|
|
181
212
|
return {
|
|
182
213
|
sessionId: session.sessionId,
|
|
183
214
|
command: state.command || "",
|
|
@@ -187,7 +218,7 @@ export async function waitForSupervisedResult(context, session, options = {}) {
|
|
|
187
218
|
};
|
|
188
219
|
}
|
|
189
220
|
|
|
190
|
-
if (!
|
|
221
|
+
if (!isTrackedPidAlive(session.pid)) {
|
|
191
222
|
return {
|
|
192
223
|
sessionId: session.sessionId,
|
|
193
224
|
command: state?.command || "",
|
|
@@ -208,44 +239,60 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
208
239
|
const context = createContext(request.context || {});
|
|
209
240
|
const command = String(request.command || "").trim();
|
|
210
241
|
const lease = request.lease || {};
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
242
|
+
const guardianManaged = request.options?.guardianManaged === true
|
|
243
|
+
|| process.env.HELLOLOOP_SUPERVISOR_GUARDIAN_ACTIVE === "1";
|
|
244
|
+
const guardianPid = guardianManaged
|
|
245
|
+
? Number(request.options?.guardianPid || process.env.HELLOLOOP_SUPERVISOR_GUARDIAN_PID || process.pid)
|
|
246
|
+
: 0;
|
|
247
|
+
const supervisorPid = guardianPid > 0 ? guardianPid : process.pid;
|
|
248
|
+
|
|
249
|
+
if (!guardianManaged) {
|
|
250
|
+
bindBackgroundTerminalSession(request.terminalSessionFile || "", {
|
|
251
|
+
command,
|
|
252
|
+
repoRoot: context.repoRoot,
|
|
253
|
+
sessionId: request.sessionId,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
216
257
|
const commandOptions = {
|
|
217
258
|
...(request.options || {}),
|
|
218
259
|
hostLease: lease,
|
|
219
260
|
};
|
|
220
|
-
|
|
221
|
-
writeState(context, {
|
|
261
|
+
writeActiveSupervisorState(context, {
|
|
222
262
|
sessionId: request.sessionId,
|
|
223
263
|
command,
|
|
224
264
|
status: "running",
|
|
225
265
|
lease,
|
|
226
|
-
pid:
|
|
266
|
+
pid: supervisorPid,
|
|
267
|
+
guardianPid,
|
|
268
|
+
workerPid: guardianManaged ? process.pid : 0,
|
|
227
269
|
startedAt: nowIso(),
|
|
228
270
|
});
|
|
271
|
+
refreshContinuation(context, request.sessionId);
|
|
272
|
+
|
|
273
|
+
if (!guardianManaged) {
|
|
274
|
+
registerActiveSession({
|
|
275
|
+
sessionId: request.sessionId,
|
|
276
|
+
repoRoot: context.repoRoot,
|
|
277
|
+
configDirName: context.configDirName,
|
|
278
|
+
command,
|
|
279
|
+
pid: supervisorPid,
|
|
280
|
+
guardianPid,
|
|
281
|
+
lease,
|
|
282
|
+
startedAt: nowIso(),
|
|
283
|
+
supervisorStateFile: context.supervisorStateFile,
|
|
284
|
+
supervisorResultFile: context.supervisorResultFile,
|
|
285
|
+
statusFile: context.statusFile,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
229
288
|
|
|
230
289
|
try {
|
|
231
290
|
if (!isHostLeaseAlive(lease)) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
stopped: true,
|
|
238
|
-
error: "检测到宿主窗口已关闭,HelloLoop supervisor 未继续执行。",
|
|
239
|
-
};
|
|
240
|
-
writeJson(context.supervisorResultFile, stopped);
|
|
241
|
-
writeState(context, {
|
|
242
|
-
sessionId: request.sessionId,
|
|
243
|
-
command,
|
|
244
|
-
status: "stopped",
|
|
245
|
-
exitCode: stopped.exitCode,
|
|
246
|
-
message: stopped.error,
|
|
247
|
-
completedAt: nowIso(),
|
|
248
|
-
});
|
|
291
|
+
writeStoppedState(context, request, "检测到宿主窗口已关闭,HelloLoop supervisor 未继续执行。");
|
|
292
|
+
refreshContinuation(context, request.sessionId);
|
|
293
|
+
if (!guardianManaged) {
|
|
294
|
+
unregisterActiveSession(request.sessionId);
|
|
295
|
+
}
|
|
249
296
|
return;
|
|
250
297
|
}
|
|
251
298
|
|
|
@@ -260,7 +307,7 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
260
307
|
results,
|
|
261
308
|
};
|
|
262
309
|
writeJson(context.supervisorResultFile, payload);
|
|
263
|
-
|
|
310
|
+
writeSupervisorState(context, {
|
|
264
311
|
sessionId: request.sessionId,
|
|
265
312
|
command,
|
|
266
313
|
status: payload.ok ? "completed" : (payload.results.some((item) => item.kind === "host-lease-stopped") ? "stopped" : "failed"),
|
|
@@ -268,6 +315,10 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
268
315
|
message: payload.ok ? "" : (payload.results.find((item) => !item.ok)?.summary || ""),
|
|
269
316
|
completedAt: nowIso(),
|
|
270
317
|
});
|
|
318
|
+
refreshContinuation(context, request.sessionId);
|
|
319
|
+
if (!guardianManaged) {
|
|
320
|
+
unregisterActiveSession(request.sessionId);
|
|
321
|
+
}
|
|
271
322
|
return;
|
|
272
323
|
}
|
|
273
324
|
|
|
@@ -281,7 +332,7 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
281
332
|
result,
|
|
282
333
|
};
|
|
283
334
|
writeJson(context.supervisorResultFile, payload);
|
|
284
|
-
|
|
335
|
+
writeSupervisorState(context, {
|
|
285
336
|
sessionId: request.sessionId,
|
|
286
337
|
command,
|
|
287
338
|
status: payload.ok ? "completed" : (result.kind === "host-lease-stopped" ? "stopped" : "failed"),
|
|
@@ -289,6 +340,10 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
289
340
|
message: result.summary || result.finalMessage || "",
|
|
290
341
|
completedAt: nowIso(),
|
|
291
342
|
});
|
|
343
|
+
refreshContinuation(context, request.sessionId);
|
|
344
|
+
if (!guardianManaged) {
|
|
345
|
+
unregisterActiveSession(request.sessionId);
|
|
346
|
+
}
|
|
292
347
|
return;
|
|
293
348
|
}
|
|
294
349
|
|
|
@@ -302,7 +357,7 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
302
357
|
error: String(error?.stack || error || ""),
|
|
303
358
|
};
|
|
304
359
|
writeJson(context.supervisorResultFile, payload);
|
|
305
|
-
|
|
360
|
+
writeSupervisorState(context, {
|
|
306
361
|
sessionId: request.sessionId,
|
|
307
362
|
command,
|
|
308
363
|
status: "failed",
|
|
@@ -310,5 +365,9 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
310
365
|
message: payload.error,
|
|
311
366
|
completedAt: nowIso(),
|
|
312
367
|
});
|
|
368
|
+
refreshContinuation(context, request.sessionId);
|
|
369
|
+
if (!guardianManaged) {
|
|
370
|
+
unregisterActiveSession(request.sessionId);
|
|
371
|
+
}
|
|
313
372
|
}
|
|
314
373
|
}
|