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,64 @@
|
|
|
1
|
+
import { nowIso, readJson, writeJson } from "./common.mjs";
|
|
2
|
+
|
|
3
|
+
export const ACTIVE_SUPERVISOR_STATUSES = new Set(["launching", "running"]);
|
|
4
|
+
export const FINAL_SUPERVISOR_STATUSES = new Set(["completed", "failed", "stopped"]);
|
|
5
|
+
|
|
6
|
+
export function readJsonIfExists(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
return filePath ? readJson(filePath) : null;
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isTrackedPidAlive(pid) {
|
|
15
|
+
const numberPid = Number(pid || 0);
|
|
16
|
+
if (!Number.isFinite(numberPid) || numberPid <= 0) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
process.kill(numberPid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return String(error?.code || "") === "EPERM";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildSupervisorState(context, patch = {}) {
|
|
28
|
+
const current = readSupervisorState(context) || {};
|
|
29
|
+
return {
|
|
30
|
+
...current,
|
|
31
|
+
...patch,
|
|
32
|
+
updatedAt: nowIso(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readSupervisorState(context) {
|
|
37
|
+
return readJsonIfExists(context.supervisorStateFile);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasActiveSupervisor(context) {
|
|
41
|
+
const state = readSupervisorState(context);
|
|
42
|
+
const activePid = Number(state?.guardianPid || state?.pid || 0);
|
|
43
|
+
return Boolean(
|
|
44
|
+
state?.status
|
|
45
|
+
&& ACTIVE_SUPERVISOR_STATUSES.has(String(state.status))
|
|
46
|
+
&& isTrackedPidAlive(activePid),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeSupervisorState(context, patch) {
|
|
51
|
+
writeJson(context.supervisorStateFile, buildSupervisorState(context, patch));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function writeActiveSupervisorState(context, patch = {}) {
|
|
55
|
+
const nextPatch = {
|
|
56
|
+
...patch,
|
|
57
|
+
exitCode: null,
|
|
58
|
+
completedAt: "",
|
|
59
|
+
};
|
|
60
|
+
if (!Object.hasOwn(nextPatch, "message")) {
|
|
61
|
+
nextPatch.message = "";
|
|
62
|
+
}
|
|
63
|
+
writeSupervisorState(context, nextPatch);
|
|
64
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
readJsonIfExists,
|
|
6
|
+
selectLatestActivityFile,
|
|
7
|
+
selectLatestRuntimeFile,
|
|
8
|
+
} from "./activity_projection.mjs";
|
|
9
|
+
import { fileExists, sleep } from "./common.mjs";
|
|
10
|
+
import { renderHostLeaseLabel } from "./host_lease.mjs";
|
|
11
|
+
import { loadRuntimeSettings } from "./runtime_settings_loader.mjs";
|
|
12
|
+
import { hasRetryBudget, pickRetryDelaySeconds } from "./runtime_settings.mjs";
|
|
13
|
+
import { FINAL_SUPERVISOR_STATUSES } from "./supervisor_state.mjs";
|
|
14
|
+
|
|
15
|
+
function writeLine(stream, message) {
|
|
16
|
+
stream.write(`${message}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildSessionSummary(supervisor) {
|
|
20
|
+
if (!supervisor?.sessionId) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
`[HelloLoop watch] 已附着后台会话:${supervisor.sessionId}`,
|
|
26
|
+
`[HelloLoop watch] 宿主租约:${renderHostLeaseLabel(supervisor.lease)}`,
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatSupervisorState(supervisor) {
|
|
31
|
+
if (!supervisor?.status) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const label = {
|
|
36
|
+
launching: "后台 supervisor 启动中",
|
|
37
|
+
running: "后台 supervisor 正在运行",
|
|
38
|
+
completed: "后台 supervisor 已完成",
|
|
39
|
+
failed: "后台 supervisor 执行失败",
|
|
40
|
+
stopped: "后台 supervisor 已停止",
|
|
41
|
+
}[String(supervisor.status)] || `后台 supervisor 状态:${supervisor.status}`;
|
|
42
|
+
|
|
43
|
+
const suffix = supervisor.message ? `:${supervisor.message}` : "";
|
|
44
|
+
return `[HelloLoop watch] ${label}${suffix}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatTaskStatus(status) {
|
|
48
|
+
if (!status?.taskTitle) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines = [`[HelloLoop watch] 当前任务:${status.taskTitle}`];
|
|
53
|
+
if (status.runDir) {
|
|
54
|
+
lines.push(`[HelloLoop watch] 运行目录:${status.runDir}`);
|
|
55
|
+
}
|
|
56
|
+
if (status.stage) {
|
|
57
|
+
lines.push(`[HelloLoop watch] 阶段:${status.stage}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatRuntimeState(runtime, previousRuntime) {
|
|
63
|
+
if (!runtime?.status) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const idleSeconds = Number(runtime?.heartbeat?.idleSeconds || 0);
|
|
68
|
+
const idleBucket = Math.floor(idleSeconds / 30);
|
|
69
|
+
const previousIdleBucket = Math.floor(Number(previousRuntime?.heartbeat?.idleSeconds || 0) / 30);
|
|
70
|
+
const signature = [
|
|
71
|
+
runtime.status,
|
|
72
|
+
runtime.attemptPrefix || "",
|
|
73
|
+
runtime.recoveryCount || 0,
|
|
74
|
+
runtime.failureCode || "",
|
|
75
|
+
runtime.failureReason || "",
|
|
76
|
+
runtime.nextRetryAt || "",
|
|
77
|
+
runtime.notification?.reason || "",
|
|
78
|
+
].join("|");
|
|
79
|
+
const previousSignature = previousRuntime
|
|
80
|
+
? [
|
|
81
|
+
previousRuntime.status,
|
|
82
|
+
previousRuntime.attemptPrefix || "",
|
|
83
|
+
previousRuntime.recoveryCount || 0,
|
|
84
|
+
previousRuntime.failureCode || "",
|
|
85
|
+
previousRuntime.failureReason || "",
|
|
86
|
+
previousRuntime.nextRetryAt || "",
|
|
87
|
+
previousRuntime.notification?.reason || "",
|
|
88
|
+
].join("|")
|
|
89
|
+
: "";
|
|
90
|
+
|
|
91
|
+
if (signature === previousSignature && (runtime.status !== "running" || idleBucket === previousIdleBucket || idleBucket === 0)) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (runtime.status === "running") {
|
|
96
|
+
if (idleBucket === 0 || idleBucket === previousIdleBucket) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
return `[HelloLoop watch] 仍在执行:${runtime.attemptPrefix || "当前尝试"},最近输出距今约 ${idleBucket * 30} 秒`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const labels = {
|
|
103
|
+
recovering: "进入同引擎恢复",
|
|
104
|
+
suspected_stall: "疑似卡住,继续观察",
|
|
105
|
+
watchdog_terminating: "触发 watchdog,准备终止当前子进程",
|
|
106
|
+
watchdog_waiting: "watchdog 等待子进程退出",
|
|
107
|
+
retry_waiting: "等待自动重试",
|
|
108
|
+
probe_waiting: "准备执行健康探测",
|
|
109
|
+
probe_running: "正在执行健康探测",
|
|
110
|
+
paused_manual: "自动恢复预算已耗尽,任务暂停",
|
|
111
|
+
lease_terminating: "宿主租约失效,正在停止当前子进程",
|
|
112
|
+
stopped_host_closed: "宿主窗口已关闭,后台任务停止",
|
|
113
|
+
completed: "当前任务执行完成",
|
|
114
|
+
failed: "当前任务执行失败",
|
|
115
|
+
};
|
|
116
|
+
const label = labels[String(runtime.status)] || `运行状态更新:${runtime.status}`;
|
|
117
|
+
const details = [
|
|
118
|
+
runtime.attemptPrefix ? `attempt=${runtime.attemptPrefix}` : "",
|
|
119
|
+
Number.isFinite(Number(runtime.recoveryCount)) ? `recovery=${runtime.recoveryCount}` : "",
|
|
120
|
+
runtime.nextRetryAt ? `next=${runtime.nextRetryAt}` : "",
|
|
121
|
+
runtime.failureReason || "",
|
|
122
|
+
].filter(Boolean).join(" | ");
|
|
123
|
+
|
|
124
|
+
return `[HelloLoop watch] ${label}${details ? ` | ${details}` : ""}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatActivityState(activity, previousActivity) {
|
|
128
|
+
if (!activity?.current?.label && !activity?.todo?.total) {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const activeCommandLabel = Array.isArray(activity?.activeCommands) ? activity.activeCommands[0]?.label || "" : "";
|
|
133
|
+
const signature = [
|
|
134
|
+
activity.current?.status || "",
|
|
135
|
+
activity.current?.label || "",
|
|
136
|
+
activity.todo?.completed || 0,
|
|
137
|
+
activity.todo?.total || 0,
|
|
138
|
+
activeCommandLabel,
|
|
139
|
+
].join("|");
|
|
140
|
+
const previousSignature = previousActivity
|
|
141
|
+
? [
|
|
142
|
+
previousActivity.current?.status || "",
|
|
143
|
+
previousActivity.current?.label || "",
|
|
144
|
+
previousActivity.todo?.completed || 0,
|
|
145
|
+
previousActivity.todo?.total || 0,
|
|
146
|
+
Array.isArray(previousActivity?.activeCommands) ? previousActivity.activeCommands[0]?.label || "" : "",
|
|
147
|
+
].join("|")
|
|
148
|
+
: "";
|
|
149
|
+
|
|
150
|
+
if (signature === previousSignature) {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const details = [];
|
|
155
|
+
if (activity.todo?.total) {
|
|
156
|
+
details.push(`todo=${activity.todo.completed}/${activity.todo.total}`);
|
|
157
|
+
}
|
|
158
|
+
if (activeCommandLabel) {
|
|
159
|
+
details.push(`cmd=${activeCommandLabel}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `[HelloLoop watch] 当前动作:${activity.current?.label || "等待事件"}${details.length ? ` | ${details.join(" | ")}` : ""}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readTextDelta(filePath, offset) {
|
|
166
|
+
if (!filePath || !fileExists(filePath)) {
|
|
167
|
+
return { nextOffset: 0, text: "" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const stats = fs.statSync(filePath);
|
|
171
|
+
if (stats.size <= 0) {
|
|
172
|
+
return { nextOffset: 0, text: "" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const start = Math.max(0, Math.min(Number(offset || 0), stats.size));
|
|
176
|
+
if (stats.size === start) {
|
|
177
|
+
return { nextOffset: start, text: "" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const handle = fs.openSync(filePath, "r");
|
|
181
|
+
try {
|
|
182
|
+
const buffer = Buffer.alloc(stats.size - start);
|
|
183
|
+
fs.readSync(handle, buffer, 0, buffer.length, start);
|
|
184
|
+
return {
|
|
185
|
+
nextOffset: stats.size,
|
|
186
|
+
text: buffer.toString("utf8"),
|
|
187
|
+
};
|
|
188
|
+
} finally {
|
|
189
|
+
fs.closeSync(handle);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function readAndWriteDelta(cursor, filePath, stream) {
|
|
194
|
+
if (cursor.file !== filePath) {
|
|
195
|
+
cursor.file = filePath || "";
|
|
196
|
+
cursor.offset = 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!filePath) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const delta = readTextDelta(filePath, cursor.offset);
|
|
204
|
+
cursor.offset = delta.nextOffset;
|
|
205
|
+
if (!delta.text) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
stream.write(delta.text);
|
|
210
|
+
if (!delta.text.endsWith("\n")) {
|
|
211
|
+
stream.write("\n");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveStatusForSession(status, sessionId) {
|
|
216
|
+
if (!status) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (!sessionId || !status.sessionId || status.sessionId === sessionId) {
|
|
220
|
+
return status;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildWatchResult(supervisor, result) {
|
|
226
|
+
const exitCode = Number(result?.exitCode ?? supervisor?.exitCode ?? (supervisor?.status === "completed" ? 0 : 1));
|
|
227
|
+
return {
|
|
228
|
+
sessionId: result?.sessionId || supervisor?.sessionId || "",
|
|
229
|
+
status: result?.ok === true
|
|
230
|
+
? "completed"
|
|
231
|
+
: (supervisor?.status || (exitCode === 0 ? "completed" : "failed")),
|
|
232
|
+
ok: result?.ok === true || exitCode === 0,
|
|
233
|
+
exitCode,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function watchSupervisorSession(context, options = {}) {
|
|
238
|
+
const pollMs = Math.max(200, Number(options.pollMs || 1000));
|
|
239
|
+
const stdoutStream = options.stdoutStream || process.stdout;
|
|
240
|
+
const stderrStream = options.stderrStream || process.stderr;
|
|
241
|
+
const expectedSessionId = String(options.sessionId || "").trim();
|
|
242
|
+
const stdoutCursor = { file: "", offset: 0 };
|
|
243
|
+
const stderrCursor = { file: "", offset: 0 };
|
|
244
|
+
let printedSessionId = "";
|
|
245
|
+
let lastSupervisorSignature = "";
|
|
246
|
+
let lastTaskSignature = "";
|
|
247
|
+
let previousRuntime = null;
|
|
248
|
+
let previousActivity = null;
|
|
249
|
+
let missingPolls = 0;
|
|
250
|
+
|
|
251
|
+
while (true) {
|
|
252
|
+
const supervisor = readJsonIfExists(context.supervisorStateFile);
|
|
253
|
+
const result = readJsonIfExists(context.supervisorResultFile);
|
|
254
|
+
const activeSessionId = expectedSessionId || supervisor?.sessionId || result?.sessionId || "";
|
|
255
|
+
const taskStatus = resolveStatusForSession(readJsonIfExists(context.statusFile), activeSessionId);
|
|
256
|
+
const runtimeFile = taskStatus?.runDir ? selectLatestRuntimeFile(taskStatus.runDir) : "";
|
|
257
|
+
const runtime = readJsonIfExists(runtimeFile);
|
|
258
|
+
const activityFile = runtime?.activityFile && fileExists(runtime.activityFile)
|
|
259
|
+
? runtime.activityFile
|
|
260
|
+
: (taskStatus?.runDir ? selectLatestActivityFile(taskStatus.runDir, runtime?.attemptPrefix || "") : "");
|
|
261
|
+
const activity = readJsonIfExists(activityFile);
|
|
262
|
+
|
|
263
|
+
if (!supervisor && !result) {
|
|
264
|
+
missingPolls += 1;
|
|
265
|
+
if (missingPolls >= 3) {
|
|
266
|
+
return {
|
|
267
|
+
sessionId: activeSessionId,
|
|
268
|
+
status: "",
|
|
269
|
+
ok: false,
|
|
270
|
+
exitCode: 1,
|
|
271
|
+
empty: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
await sleep(pollMs);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
missingPolls = 0;
|
|
278
|
+
|
|
279
|
+
if (supervisor?.sessionId && supervisor.sessionId !== printedSessionId) {
|
|
280
|
+
printedSessionId = supervisor.sessionId;
|
|
281
|
+
writeLine(stdoutStream, buildSessionSummary(supervisor));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const supervisorSignature = supervisor
|
|
285
|
+
? [supervisor.sessionId || "", supervisor.status || "", supervisor.message || ""].join("|")
|
|
286
|
+
: "";
|
|
287
|
+
if (supervisorSignature && supervisorSignature !== lastSupervisorSignature) {
|
|
288
|
+
lastSupervisorSignature = supervisorSignature;
|
|
289
|
+
writeLine(stdoutStream, formatSupervisorState(supervisor));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const taskSignature = taskStatus
|
|
293
|
+
? [taskStatus.sessionId || "", taskStatus.taskId || "", taskStatus.taskTitle || "", taskStatus.runDir || "", taskStatus.stage || ""].join("|")
|
|
294
|
+
: "";
|
|
295
|
+
if (taskSignature && taskSignature !== lastTaskSignature) {
|
|
296
|
+
lastTaskSignature = taskSignature;
|
|
297
|
+
writeLine(stdoutStream, formatTaskStatus(taskStatus));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const runtimeMessage = formatRuntimeState(runtime, previousRuntime);
|
|
301
|
+
if (runtimeMessage) {
|
|
302
|
+
writeLine(stdoutStream, runtimeMessage);
|
|
303
|
+
}
|
|
304
|
+
previousRuntime = runtime || previousRuntime;
|
|
305
|
+
const activityMessage = formatActivityState(activity, previousActivity);
|
|
306
|
+
if (activityMessage) {
|
|
307
|
+
writeLine(stdoutStream, activityMessage);
|
|
308
|
+
}
|
|
309
|
+
previousActivity = activity || previousActivity;
|
|
310
|
+
|
|
311
|
+
const activePrefix = runtime?.attemptPrefix || "";
|
|
312
|
+
const runtimeDir = runtimeFile ? path.dirname(runtimeFile) : "";
|
|
313
|
+
const stdoutFile = runtimeDir && activePrefix
|
|
314
|
+
? path.join(runtimeDir, `${activePrefix}-stdout.log`)
|
|
315
|
+
: "";
|
|
316
|
+
const stderrFile = runtimeDir && activePrefix
|
|
317
|
+
? path.join(runtimeDir, `${activePrefix}-stderr.log`)
|
|
318
|
+
: "";
|
|
319
|
+
|
|
320
|
+
readAndWriteDelta(stdoutCursor, stdoutFile, stdoutStream);
|
|
321
|
+
readAndWriteDelta(stderrCursor, stderrFile, stderrStream);
|
|
322
|
+
|
|
323
|
+
if (supervisor?.status && FINAL_SUPERVISOR_STATUSES.has(String(supervisor.status))) {
|
|
324
|
+
readAndWriteDelta(stdoutCursor, stdoutFile, stdoutStream);
|
|
325
|
+
readAndWriteDelta(stderrCursor, stderrFile, stderrStream);
|
|
326
|
+
return buildWatchResult(supervisor, result);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await sleep(pollMs);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function formatRetryAttachMessage(options, attemptNumber, delaySeconds) {
|
|
334
|
+
const sessionLabel = String(options.sessionId || "").trim();
|
|
335
|
+
const targetLabel = sessionLabel
|
|
336
|
+
? `后台会话 ${sessionLabel}`
|
|
337
|
+
: "后台会话";
|
|
338
|
+
return `[HelloLoop watch] 暂未检测到可附着的${targetLabel},将在 ${delaySeconds} 秒后自动重试(第 ${attemptNumber} 次)。`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function watchSupervisorSessionWithRecovery(context, options = {}) {
|
|
342
|
+
const observerRetry = loadRuntimeSettings({
|
|
343
|
+
globalConfigFile: options.globalConfigFile,
|
|
344
|
+
}).observerRetry;
|
|
345
|
+
const stdoutStream = options.stdoutStream || process.stdout;
|
|
346
|
+
let retryCount = 0;
|
|
347
|
+
|
|
348
|
+
while (true) {
|
|
349
|
+
const result = await watchSupervisorSession(context, options);
|
|
350
|
+
if (!result.empty) {
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const nextAttempt = retryCount + 1;
|
|
355
|
+
if (!observerRetry.enabled || !hasRetryBudget(observerRetry.maxRetryCount, nextAttempt)) {
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const delaySeconds = pickRetryDelaySeconds(observerRetry.retryDelaysSeconds, nextAttempt);
|
|
360
|
+
writeLine(stdoutStream, formatRetryAttachMessage(options, nextAttempt, delaySeconds));
|
|
361
|
+
retryCount = nextAttempt;
|
|
362
|
+
await sleep(delaySeconds * 1000);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { ensureDir, fileExists, nowIso, readJson, timestampForFile, writeJson } from "./common.mjs";
|
|
5
5
|
import { loadGlobalConfig } from "./global_config.mjs";
|
|
6
|
+
import { normalizeTerminalConcurrencySettings } from "./runtime_settings.mjs";
|
|
6
7
|
|
|
7
8
|
const SESSION_DIR_NAME = "terminal-sessions";
|
|
8
9
|
const RUNTIME_DIR_NAME = "runtime";
|
|
@@ -33,27 +34,6 @@ function isPidAlive(pid) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function normalizeNonNegativeInteger(value, fallbackValue) {
|
|
37
|
-
if (value === null || value === undefined || value === "") {
|
|
38
|
-
return fallbackValue;
|
|
39
|
-
}
|
|
40
|
-
const parsed = Number(value);
|
|
41
|
-
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
42
|
-
return fallbackValue;
|
|
43
|
-
}
|
|
44
|
-
return Math.floor(parsed);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function normalizeTerminalConcurrencySettings(settings = {}) {
|
|
48
|
-
const source = settings && typeof settings === "object" ? settings : {};
|
|
49
|
-
return {
|
|
50
|
-
enabled: source.enabled !== false,
|
|
51
|
-
visibleMax: normalizeNonNegativeInteger(source.visibleMax, 8),
|
|
52
|
-
backgroundMax: normalizeNonNegativeInteger(source.backgroundMax, 8),
|
|
53
|
-
totalMax: normalizeNonNegativeInteger(source.totalMax, 8),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
37
|
function resolveTerminalRuntimeConfig(options = {}) {
|
|
58
38
|
const globalConfig = loadGlobalConfig({
|
|
59
39
|
globalConfigFile: options.globalConfigFile,
|