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.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +230 -506
  3. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  4. package/hosts/gemini/extension/gemini-extension.json +1 -1
  5. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  6. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  7. package/package.json +4 -2
  8. package/src/activity_projection.mjs +294 -0
  9. package/src/analyze_confirmation.mjs +3 -1
  10. package/src/analyzer.mjs +2 -1
  11. package/src/auto_execution_options.mjs +13 -0
  12. package/src/background_launch.mjs +73 -0
  13. package/src/cli.mjs +49 -1
  14. package/src/cli_analyze_command.mjs +9 -5
  15. package/src/cli_args.mjs +102 -37
  16. package/src/cli_command_handlers.mjs +44 -4
  17. package/src/cli_support.mjs +2 -0
  18. package/src/dashboard_command.mjs +371 -0
  19. package/src/dashboard_tui.mjs +289 -0
  20. package/src/dashboard_web.mjs +351 -0
  21. package/src/dashboard_web_client.mjs +167 -0
  22. package/src/dashboard_web_page.mjs +49 -0
  23. package/src/engine_event_parser_codex.mjs +167 -0
  24. package/src/engine_process_support.mjs +1 -0
  25. package/src/engine_selection.mjs +24 -0
  26. package/src/engine_selection_probe.mjs +10 -6
  27. package/src/engine_selection_settings.mjs +12 -19
  28. package/src/execution_interactivity.mjs +12 -0
  29. package/src/host_continuation.mjs +305 -0
  30. package/src/install_codex.mjs +20 -8
  31. package/src/install_shared.mjs +9 -0
  32. package/src/node_process_launch.mjs +28 -0
  33. package/src/process.mjs +2 -0
  34. package/src/runner_execute_task.mjs +4 -0
  35. package/src/runner_execution_support.mjs +69 -3
  36. package/src/runner_once.mjs +4 -0
  37. package/src/runner_status.mjs +63 -7
  38. package/src/runtime_engine_support.mjs +41 -4
  39. package/src/runtime_engine_task.mjs +7 -0
  40. package/src/runtime_settings.mjs +105 -0
  41. package/src/runtime_settings_loader.mjs +19 -0
  42. package/src/shell_invocation.mjs +227 -9
  43. package/src/supervisor_cli_support.mjs +3 -2
  44. package/src/supervisor_guardian.mjs +307 -0
  45. package/src/supervisor_runtime.mjs +138 -82
  46. package/src/supervisor_state.mjs +64 -0
  47. package/src/supervisor_watch.mjs +92 -48
  48. package/src/terminal_session_limits.mjs +1 -21
  49. package/src/windows_hidden_shell_proxy.mjs +405 -0
  50. 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();
@@ -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
- writeState(context, {
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 = spawn(process.execPath, [
125
- path.join(context.bundleRoot, "bin", "helloloop.js"),
126
- "__supervise",
127
- "--session-file",
128
- context.supervisorRequestFile,
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
- writeState(context, {
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
- status: "running",
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
- pid: child.pid ?? 0,
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 && FINAL_STATUSES.has(String(state.status || ""))) {
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 (!isPidAlive(session.pid)) {
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
- bindBackgroundTerminalSession(request.terminalSessionFile || "", {
215
- command,
216
- repoRoot: context.repoRoot,
217
- sessionId: request.sessionId,
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: process.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
- const stopped = {
236
- sessionId: request.sessionId,
237
- command,
238
- exitCode: 1,
239
- ok: false,
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
- writeState(context, {
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
- writeState(context, {
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
- writeState(context, {
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
  }