helloloop 0.9.1 → 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/README.md +230 -506
- 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 +49 -1
- package/src/cli_analyze_command.mjs +9 -5
- package/src/cli_args.mjs +102 -37
- package/src/cli_command_handlers.mjs +44 -4
- package/src/cli_support.mjs +2 -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 +1 -0
- package/src/engine_selection.mjs +24 -0
- package/src/engine_selection_probe.mjs +10 -6
- package/src/engine_selection_settings.mjs +12 -19
- package/src/execution_interactivity.mjs +12 -0
- package/src/host_continuation.mjs +305 -0
- package/src/install_codex.mjs +20 -8
- 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 +4 -0
- package/src/runner_execution_support.mjs +69 -3
- package/src/runner_once.mjs +4 -0
- package/src/runner_status.mjs +63 -7
- package/src/runtime_engine_support.mjs +41 -4
- 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 +3 -2
- package/src/supervisor_guardian.mjs +307 -0
- package/src/supervisor_runtime.mjs +138 -82
- package/src/supervisor_state.mjs +64 -0
- package/src/supervisor_watch.mjs +92 -48
- 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();
|
|
@@ -109,47 +121,62 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
109
121
|
removeIfExists(context.supervisorStdoutFile);
|
|
110
122
|
removeIfExists(context.supervisorStderrFile);
|
|
111
123
|
writeJson(context.supervisorRequestFile, request);
|
|
112
|
-
|
|
124
|
+
writeActiveSupervisorState(context, {
|
|
113
125
|
sessionId,
|
|
114
126
|
status: "launching",
|
|
115
127
|
command,
|
|
116
128
|
lease,
|
|
117
129
|
startedAt: nowIso(),
|
|
118
130
|
pid: 0,
|
|
131
|
+
guardianPid: 0,
|
|
132
|
+
workerPid: 0,
|
|
119
133
|
});
|
|
134
|
+
refreshContinuation(context, sessionId);
|
|
120
135
|
|
|
121
136
|
const stdoutFd = fs.openSync(context.supervisorStdoutFile, "w");
|
|
122
137
|
const stderrFd = fs.openSync(context.supervisorStderrFile, "w");
|
|
138
|
+
|
|
123
139
|
try {
|
|
124
|
-
const child =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
const child = spawnNodeProcess({
|
|
141
|
+
args: [
|
|
142
|
+
path.join(context.bundleRoot, "bin", "helloloop.js"),
|
|
143
|
+
"__supervise",
|
|
144
|
+
"--session-file",
|
|
145
|
+
context.supervisorRequestFile,
|
|
146
|
+
],
|
|
130
147
|
cwd: context.repoRoot,
|
|
131
148
|
detached: true,
|
|
132
|
-
shell: false,
|
|
133
|
-
windowsHide: true,
|
|
134
149
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
135
150
|
env: {
|
|
136
|
-
...process.env,
|
|
137
151
|
HELLOLOOP_SUPERVISOR_ACTIVE: "1",
|
|
138
152
|
},
|
|
139
153
|
});
|
|
154
|
+
|
|
140
155
|
finalizePreparedTerminalSessionBackground(child.pid ?? 0, {
|
|
141
156
|
command,
|
|
142
157
|
repoRoot: context.repoRoot,
|
|
143
158
|
sessionId,
|
|
144
159
|
});
|
|
145
160
|
child.unref();
|
|
146
|
-
|
|
161
|
+
|
|
162
|
+
writeActiveSupervisorState(
|
|
163
|
+
context,
|
|
164
|
+
buildSupervisorSessionState(sessionId, command, lease, child.pid ?? 0, "后台守护进程正在运行。"),
|
|
165
|
+
);
|
|
166
|
+
refreshContinuation(context, sessionId);
|
|
167
|
+
|
|
168
|
+
registerActiveSession({
|
|
147
169
|
sessionId,
|
|
148
|
-
|
|
170
|
+
repoRoot: context.repoRoot,
|
|
171
|
+
configDirName: context.configDirName,
|
|
149
172
|
command,
|
|
173
|
+
pid: child.pid ?? 0,
|
|
174
|
+
guardianPid: child.pid ?? 0,
|
|
150
175
|
lease,
|
|
151
176
|
startedAt: nowIso(),
|
|
152
|
-
|
|
177
|
+
supervisorStateFile: context.supervisorStateFile,
|
|
178
|
+
supervisorResultFile: context.supervisorResultFile,
|
|
179
|
+
statusFile: context.statusFile,
|
|
153
180
|
});
|
|
154
181
|
|
|
155
182
|
return {
|
|
@@ -158,6 +185,7 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
158
185
|
lease,
|
|
159
186
|
};
|
|
160
187
|
} catch (error) {
|
|
188
|
+
unregisterActiveSession(sessionId);
|
|
161
189
|
cancelPreparedTerminalSessionBackground({
|
|
162
190
|
command,
|
|
163
191
|
repoRoot: context.repoRoot,
|
|
@@ -180,7 +208,7 @@ export async function waitForSupervisedResult(context, session, options = {}) {
|
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
const state = readSupervisorState(context);
|
|
183
|
-
if (state?.sessionId === session.sessionId &&
|
|
211
|
+
if (state?.sessionId === session.sessionId && FINAL_SUPERVISOR_STATUSES.has(String(state.status || ""))) {
|
|
184
212
|
return {
|
|
185
213
|
sessionId: session.sessionId,
|
|
186
214
|
command: state.command || "",
|
|
@@ -190,7 +218,7 @@ export async function waitForSupervisedResult(context, session, options = {}) {
|
|
|
190
218
|
};
|
|
191
219
|
}
|
|
192
220
|
|
|
193
|
-
if (!
|
|
221
|
+
if (!isTrackedPidAlive(session.pid)) {
|
|
194
222
|
return {
|
|
195
223
|
sessionId: session.sessionId,
|
|
196
224
|
command: state?.command || "",
|
|
@@ -211,44 +239,60 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
211
239
|
const context = createContext(request.context || {});
|
|
212
240
|
const command = String(request.command || "").trim();
|
|
213
241
|
const lease = request.lease || {};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
|
|
219
257
|
const commandOptions = {
|
|
220
258
|
...(request.options || {}),
|
|
221
259
|
hostLease: lease,
|
|
222
260
|
};
|
|
223
|
-
|
|
224
|
-
writeState(context, {
|
|
261
|
+
writeActiveSupervisorState(context, {
|
|
225
262
|
sessionId: request.sessionId,
|
|
226
263
|
command,
|
|
227
264
|
status: "running",
|
|
228
265
|
lease,
|
|
229
|
-
pid:
|
|
266
|
+
pid: supervisorPid,
|
|
267
|
+
guardianPid,
|
|
268
|
+
workerPid: guardianManaged ? process.pid : 0,
|
|
230
269
|
startedAt: nowIso(),
|
|
231
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
|
+
}
|
|
232
288
|
|
|
233
289
|
try {
|
|
234
290
|
if (!isHostLeaseAlive(lease)) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
stopped: true,
|
|
241
|
-
error: "检测到宿主窗口已关闭,HelloLoop supervisor 未继续执行。",
|
|
242
|
-
};
|
|
243
|
-
writeJson(context.supervisorResultFile, stopped);
|
|
244
|
-
writeState(context, {
|
|
245
|
-
sessionId: request.sessionId,
|
|
246
|
-
command,
|
|
247
|
-
status: "stopped",
|
|
248
|
-
exitCode: stopped.exitCode,
|
|
249
|
-
message: stopped.error,
|
|
250
|
-
completedAt: nowIso(),
|
|
251
|
-
});
|
|
291
|
+
writeStoppedState(context, request, "检测到宿主窗口已关闭,HelloLoop supervisor 未继续执行。");
|
|
292
|
+
refreshContinuation(context, request.sessionId);
|
|
293
|
+
if (!guardianManaged) {
|
|
294
|
+
unregisterActiveSession(request.sessionId);
|
|
295
|
+
}
|
|
252
296
|
return;
|
|
253
297
|
}
|
|
254
298
|
|
|
@@ -263,7 +307,7 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
263
307
|
results,
|
|
264
308
|
};
|
|
265
309
|
writeJson(context.supervisorResultFile, payload);
|
|
266
|
-
|
|
310
|
+
writeSupervisorState(context, {
|
|
267
311
|
sessionId: request.sessionId,
|
|
268
312
|
command,
|
|
269
313
|
status: payload.ok ? "completed" : (payload.results.some((item) => item.kind === "host-lease-stopped") ? "stopped" : "failed"),
|
|
@@ -271,6 +315,10 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
271
315
|
message: payload.ok ? "" : (payload.results.find((item) => !item.ok)?.summary || ""),
|
|
272
316
|
completedAt: nowIso(),
|
|
273
317
|
});
|
|
318
|
+
refreshContinuation(context, request.sessionId);
|
|
319
|
+
if (!guardianManaged) {
|
|
320
|
+
unregisterActiveSession(request.sessionId);
|
|
321
|
+
}
|
|
274
322
|
return;
|
|
275
323
|
}
|
|
276
324
|
|
|
@@ -284,7 +332,7 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
284
332
|
result,
|
|
285
333
|
};
|
|
286
334
|
writeJson(context.supervisorResultFile, payload);
|
|
287
|
-
|
|
335
|
+
writeSupervisorState(context, {
|
|
288
336
|
sessionId: request.sessionId,
|
|
289
337
|
command,
|
|
290
338
|
status: payload.ok ? "completed" : (result.kind === "host-lease-stopped" ? "stopped" : "failed"),
|
|
@@ -292,6 +340,10 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
292
340
|
message: result.summary || result.finalMessage || "",
|
|
293
341
|
completedAt: nowIso(),
|
|
294
342
|
});
|
|
343
|
+
refreshContinuation(context, request.sessionId);
|
|
344
|
+
if (!guardianManaged) {
|
|
345
|
+
unregisterActiveSession(request.sessionId);
|
|
346
|
+
}
|
|
295
347
|
return;
|
|
296
348
|
}
|
|
297
349
|
|
|
@@ -305,7 +357,7 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
305
357
|
error: String(error?.stack || error || ""),
|
|
306
358
|
};
|
|
307
359
|
writeJson(context.supervisorResultFile, payload);
|
|
308
|
-
|
|
360
|
+
writeSupervisorState(context, {
|
|
309
361
|
sessionId: request.sessionId,
|
|
310
362
|
command,
|
|
311
363
|
status: "failed",
|
|
@@ -313,5 +365,9 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
313
365
|
message: payload.error,
|
|
314
366
|
completedAt: nowIso(),
|
|
315
367
|
});
|
|
368
|
+
refreshContinuation(context, request.sessionId);
|
|
369
|
+
if (!guardianManaged) {
|
|
370
|
+
unregisterActiveSession(request.sessionId);
|
|
371
|
+
}
|
|
316
372
|
}
|
|
317
373
|
}
|