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.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +230 -498
  4. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  5. package/hosts/gemini/extension/gemini-extension.json +1 -1
  6. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  7. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  8. package/package.json +4 -2
  9. package/src/activity_projection.mjs +294 -0
  10. package/src/analyze_confirmation.mjs +3 -1
  11. package/src/analyzer.mjs +2 -1
  12. package/src/auto_execution_options.mjs +13 -0
  13. package/src/background_launch.mjs +73 -0
  14. package/src/cli.mjs +51 -1
  15. package/src/cli_analyze_command.mjs +12 -14
  16. package/src/cli_args.mjs +106 -32
  17. package/src/cli_command_handlers.mjs +73 -25
  18. package/src/cli_support.mjs +2 -0
  19. package/src/common.mjs +11 -0
  20. package/src/dashboard_command.mjs +371 -0
  21. package/src/dashboard_tui.mjs +289 -0
  22. package/src/dashboard_web.mjs +351 -0
  23. package/src/dashboard_web_client.mjs +167 -0
  24. package/src/dashboard_web_page.mjs +49 -0
  25. package/src/engine_event_parser_codex.mjs +167 -0
  26. package/src/engine_process_support.mjs +7 -2
  27. package/src/engine_selection.mjs +24 -0
  28. package/src/engine_selection_probe.mjs +10 -6
  29. package/src/engine_selection_settings.mjs +53 -44
  30. package/src/execution_interactivity.mjs +12 -0
  31. package/src/host_continuation.mjs +305 -0
  32. package/src/install_codex.mjs +20 -30
  33. package/src/install_shared.mjs +9 -0
  34. package/src/node_process_launch.mjs +28 -0
  35. package/src/process.mjs +2 -0
  36. package/src/runner_execute_task.mjs +15 -1
  37. package/src/runner_execution_support.mjs +69 -3
  38. package/src/runner_once.mjs +5 -0
  39. package/src/runner_status.mjs +72 -4
  40. package/src/runtime_engine_support.mjs +52 -5
  41. package/src/runtime_engine_task.mjs +7 -0
  42. package/src/runtime_settings.mjs +105 -0
  43. package/src/runtime_settings_loader.mjs +19 -0
  44. package/src/shell_invocation.mjs +227 -9
  45. package/src/supervisor_cli_support.mjs +49 -0
  46. package/src/supervisor_guardian.mjs +307 -0
  47. package/src/supervisor_runtime.mjs +142 -83
  48. package/src/supervisor_state.mjs +64 -0
  49. package/src/supervisor_watch.mjs +364 -0
  50. package/src/terminal_session_limits.mjs +1 -21
  51. package/src/windows_hidden_shell_proxy.mjs +405 -0
  52. 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 readJsonIfExists(filePath) {
28
- return fs.existsSync(filePath) ? readJson(filePath) : null;
35
+ function toSerializableOptions(options = {}) {
36
+ return JSON.parse(JSON.stringify(options));
29
37
  }
30
38
 
31
- function isPidAlive(pid) {
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
- process.kill(numberPid, 0);
38
- return true;
39
- } catch (error) {
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 buildState(context, patch = {}) {
45
- const current = readJsonIfExists(context.supervisorStateFile) || {};
46
- return {
47
- ...current,
48
- ...patch,
49
- updatedAt: nowIso(),
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 writeState(context, patch) {
54
- writeJson(context.supervisorStateFile, buildState(context, patch));
55
- }
56
-
57
- function toSerializableOptions(options = {}) {
58
- return JSON.parse(JSON.stringify(options));
59
- }
60
-
61
- export function readSupervisorState(context) {
62
- return readJsonIfExists(context.supervisorStateFile);
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 supervisor 已启动:${session.sessionId}`,
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 (existing?.status && ACTIVE_STATUSES.has(String(existing.status)) && isPidAlive(existing.pid)) {
82
- throw new Error(`已有 HelloLoop supervisor 正在运行:${existing.sessionId || "unknown"}`);
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(options),
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
- writeState(context, {
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 = spawn(process.execPath, [
122
- path.join(context.bundleRoot, "bin", "helloloop.js"),
123
- "__supervise",
124
- "--session-file",
125
- context.supervisorRequestFile,
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
- writeState(context, {
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
- status: "running",
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
- pid: child.pid ?? 0,
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 && FINAL_STATUSES.has(String(state.status || ""))) {
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 (!isPidAlive(session.pid)) {
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
- bindBackgroundTerminalSession(request.terminalSessionFile || "", {
212
- command,
213
- repoRoot: context.repoRoot,
214
- sessionId: request.sessionId,
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: process.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
- const stopped = {
233
- sessionId: request.sessionId,
234
- command,
235
- exitCode: 1,
236
- ok: false,
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
- writeState(context, {
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
- writeState(context, {
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
- writeState(context, {
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
  }