helloloop 0.8.5 → 0.9.1
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 +57 -7
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/src/cli.mjs +56 -38
- package/src/cli_analyze_command.mjs +5 -29
- package/src/cli_args.mjs +12 -3
- package/src/cli_command_handlers.mjs +34 -73
- package/src/common.mjs +11 -0
- package/src/engine_process_support.mjs +6 -2
- package/src/engine_selection_settings.mjs +63 -35
- package/src/install_codex.mjs +1 -23
- package/src/runner_execute_task.mjs +11 -1
- package/src/runner_once.mjs +1 -0
- package/src/runner_status.mjs +12 -0
- package/src/runtime_engine_support.mjs +11 -1
- package/src/supervisor_cli_support.mjs +48 -0
- package/src/supervisor_runtime.mjs +33 -1
- package/src/supervisor_watch.mjs +320 -0
- package/src/terminal_session_limits.mjs +394 -0
|
@@ -24,6 +24,15 @@ function defaultEmailNotificationSettings() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function defaultTerminalConcurrencySettings() {
|
|
28
|
+
return {
|
|
29
|
+
enabled: true,
|
|
30
|
+
visibleMax: 8,
|
|
31
|
+
backgroundMax: 8,
|
|
32
|
+
totalMax: 8,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
function defaultUserSettings() {
|
|
28
37
|
return {
|
|
29
38
|
defaultEngine: "",
|
|
@@ -31,28 +40,40 @@ function defaultUserSettings() {
|
|
|
31
40
|
notifications: {
|
|
32
41
|
email: defaultEmailNotificationSettings(),
|
|
33
42
|
},
|
|
43
|
+
runtime: {
|
|
44
|
+
terminalConcurrency: defaultTerminalConcurrencySettings(),
|
|
45
|
+
},
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
function cloneJsonValue(value) {
|
|
38
|
-
return JSON.parse(JSON.stringify(value));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
49
|
function isPlainObject(value) {
|
|
42
50
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
53
|
+
function normalizeString(value, fallback = "") {
|
|
54
|
+
return typeof value === "string" ? value.trim() : fallback;
|
|
55
|
+
}
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
function normalizeBoolean(value, fallback = false) {
|
|
58
|
+
return typeof value === "boolean" ? value : fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizePositiveInteger(value, fallback, minimum = 1) {
|
|
62
|
+
const numericValue = Number(value);
|
|
63
|
+
if (!Number.isInteger(numericValue) || numericValue < minimum) {
|
|
64
|
+
return fallback;
|
|
54
65
|
}
|
|
55
|
-
return
|
|
66
|
+
return numericValue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeTerminalConcurrencySettings(settings = {}) {
|
|
70
|
+
const defaults = defaultTerminalConcurrencySettings();
|
|
71
|
+
return {
|
|
72
|
+
enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
|
|
73
|
+
visibleMax: normalizePositiveInteger(settings?.visibleMax, defaults.visibleMax, 0),
|
|
74
|
+
backgroundMax: normalizePositiveInteger(settings?.backgroundMax, defaults.backgroundMax, 0),
|
|
75
|
+
totalMax: normalizePositiveInteger(settings?.totalMax, defaults.totalMax, 0),
|
|
76
|
+
};
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
function mergeValueBySchema(schemaValue, baseValue, patchValue) {
|
|
@@ -75,16 +96,6 @@ function mergeValueBySchema(schemaValue, baseValue, patchValue) {
|
|
|
75
96
|
return next;
|
|
76
97
|
}
|
|
77
98
|
|
|
78
|
-
export function syncUserSettingsShape(settings = {}) {
|
|
79
|
-
return syncValueBySchema(defaultUserSettings(), settings);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function readRawUserSettingsDocument(options = {}) {
|
|
83
|
-
const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
|
|
84
|
-
const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
|
|
85
|
-
return syncUserSettingsShape(settings);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
99
|
export function resolveUserSettingsHome() {
|
|
89
100
|
return String(process.env.HELLOLOOP_HOME || "").trim()
|
|
90
101
|
|| path.join(os.homedir(), ".helloloop");
|
|
@@ -101,32 +112,49 @@ function normalizeEmailNotificationSettings(emailSettings = {}) {
|
|
|
101
112
|
const smtp = emailSettings?.smtp || {};
|
|
102
113
|
|
|
103
114
|
return {
|
|
104
|
-
|
|
105
|
-
...emailSettings,
|
|
115
|
+
enabled: normalizeBoolean(emailSettings?.enabled, defaults.enabled),
|
|
106
116
|
to: Array.isArray(emailSettings?.to)
|
|
107
117
|
? emailSettings.to.map((item) => String(item || "").trim()).filter(Boolean)
|
|
108
|
-
:
|
|
118
|
+
: defaults.to,
|
|
119
|
+
from: normalizeString(emailSettings?.from, defaults.from),
|
|
109
120
|
smtp: {
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
host: normalizeString(smtp?.host, defaults.smtp.host),
|
|
122
|
+
port: normalizePositiveInteger(smtp?.port, defaults.smtp.port),
|
|
123
|
+
secure: normalizeBoolean(smtp?.secure, defaults.smtp.secure),
|
|
124
|
+
starttls: normalizeBoolean(smtp?.starttls, defaults.smtp.starttls),
|
|
125
|
+
username: normalizeString(smtp?.username, defaults.smtp.username),
|
|
126
|
+
usernameEnv: normalizeString(smtp?.usernameEnv, defaults.smtp.usernameEnv),
|
|
127
|
+
password: typeof smtp?.password === "string" ? smtp.password : defaults.smtp.password,
|
|
128
|
+
passwordEnv: normalizeString(smtp?.passwordEnv, defaults.smtp.passwordEnv),
|
|
129
|
+
timeoutSeconds: normalizePositiveInteger(smtp?.timeoutSeconds, defaults.smtp.timeoutSeconds),
|
|
130
|
+
rejectUnauthorized: normalizeBoolean(smtp?.rejectUnauthorized, defaults.smtp.rejectUnauthorized),
|
|
112
131
|
},
|
|
113
132
|
};
|
|
114
133
|
}
|
|
115
134
|
|
|
116
|
-
export function
|
|
117
|
-
const settings = readRawUserSettingsDocument(options);
|
|
118
|
-
|
|
135
|
+
export function syncUserSettingsShape(settings = {}) {
|
|
119
136
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine),
|
|
137
|
+
defaultEngine: normalizeEngineName(settings?.defaultEngine) || "",
|
|
138
|
+
lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine) || "",
|
|
123
139
|
notifications: {
|
|
124
|
-
...(settings?.notifications || {}),
|
|
125
140
|
email: normalizeEmailNotificationSettings(settings?.notifications?.email || {}),
|
|
126
141
|
},
|
|
142
|
+
runtime: {
|
|
143
|
+
terminalConcurrency: normalizeTerminalConcurrencySettings(settings?.runtime?.terminalConcurrency || {}),
|
|
144
|
+
},
|
|
127
145
|
};
|
|
128
146
|
}
|
|
129
147
|
|
|
148
|
+
function readRawUserSettingsDocument(options = {}) {
|
|
149
|
+
const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
|
|
150
|
+
const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
|
|
151
|
+
return syncUserSettingsShape(settings);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function loadUserSettingsDocument(options = {}) {
|
|
155
|
+
return readRawUserSettingsDocument(options);
|
|
156
|
+
}
|
|
157
|
+
|
|
130
158
|
export function loadUserSettings(options = {}) {
|
|
131
159
|
const settings = loadUserSettingsDocument(options);
|
|
132
160
|
return {
|
package/src/install_codex.mjs
CHANGED
|
@@ -133,7 +133,6 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
133
133
|
const resolvedLocalRoot = resolveCodexLocalRoot(options.codexHome);
|
|
134
134
|
const targetPluginsRoot = path.join(resolvedLocalRoot, "plugins");
|
|
135
135
|
const targetPluginRoot = path.join(targetPluginsRoot, CODEX_PLUGIN_NAME);
|
|
136
|
-
const legacyTargetPluginRoot = path.join(resolvedCodexHome, "plugins", CODEX_PLUGIN_NAME);
|
|
137
136
|
const targetPluginCacheRoot = path.join(
|
|
138
137
|
resolvedCodexHome,
|
|
139
138
|
"plugins",
|
|
@@ -143,13 +142,9 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
143
142
|
);
|
|
144
143
|
const targetInstalledPluginRoot = path.join(targetPluginCacheRoot, "local");
|
|
145
144
|
const marketplaceFile = path.join(resolvedLocalRoot, ".agents", "plugins", "marketplace.json");
|
|
146
|
-
const legacyMarketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
147
145
|
const configFile = path.join(resolvedCodexHome, "config.toml");
|
|
148
146
|
const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
|
|
149
147
|
const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
|
|
150
|
-
const existingLegacyMarketplace = legacyMarketplaceFile === marketplaceFile
|
|
151
|
-
? existingMarketplace
|
|
152
|
-
: readExistingJsonOrThrow(legacyMarketplaceFile, "Codex legacy marketplace 配置");
|
|
153
148
|
|
|
154
149
|
if (!fileExists(manifestFile)) {
|
|
155
150
|
throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
|
|
@@ -159,9 +154,6 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
159
154
|
assertPathInside(resolvedCodexHome, targetPluginCacheRoot, "Codex 插件缓存目录");
|
|
160
155
|
removeTargetIfNeeded(targetPluginRoot, options.force);
|
|
161
156
|
removeTargetIfNeeded(targetPluginCacheRoot, options.force);
|
|
162
|
-
if (legacyTargetPluginRoot !== targetPluginRoot) {
|
|
163
|
-
removePathIfExists(legacyTargetPluginRoot);
|
|
164
|
-
}
|
|
165
157
|
|
|
166
158
|
ensureDir(targetPluginsRoot);
|
|
167
159
|
ensureDir(targetPluginRoot);
|
|
@@ -173,9 +165,6 @@ export function installCodexHost(bundleRoot, options = {}) {
|
|
|
173
165
|
|
|
174
166
|
ensureDir(path.dirname(marketplaceFile));
|
|
175
167
|
updateCodexMarketplace(marketplaceFile, existingMarketplace);
|
|
176
|
-
if (legacyMarketplaceFile !== marketplaceFile) {
|
|
177
|
-
removeCodexMarketplaceEntry(legacyMarketplaceFile, existingLegacyMarketplace);
|
|
178
|
-
}
|
|
179
168
|
upsertCodexPluginConfig(configFile);
|
|
180
169
|
|
|
181
170
|
return {
|
|
@@ -192,7 +181,6 @@ export function uninstallCodexHost(options = {}) {
|
|
|
192
181
|
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
193
182
|
const resolvedLocalRoot = resolveCodexLocalRoot(options.codexHome);
|
|
194
183
|
const targetPluginRoot = path.join(resolvedLocalRoot, "plugins", CODEX_PLUGIN_NAME);
|
|
195
|
-
const legacyTargetPluginRoot = path.join(resolvedCodexHome, "plugins", CODEX_PLUGIN_NAME);
|
|
196
184
|
const targetPluginCacheRoot = path.join(
|
|
197
185
|
resolvedCodexHome,
|
|
198
186
|
"plugins",
|
|
@@ -201,29 +189,19 @@ export function uninstallCodexHost(options = {}) {
|
|
|
201
189
|
CODEX_PLUGIN_NAME,
|
|
202
190
|
);
|
|
203
191
|
const marketplaceFile = path.join(resolvedLocalRoot, ".agents", "plugins", "marketplace.json");
|
|
204
|
-
const legacyMarketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
205
192
|
const configFile = path.join(resolvedCodexHome, "config.toml");
|
|
206
193
|
const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
|
|
207
|
-
const existingLegacyMarketplace = legacyMarketplaceFile === marketplaceFile
|
|
208
|
-
? existingMarketplace
|
|
209
|
-
: readExistingJsonOrThrow(legacyMarketplaceFile, "Codex legacy marketplace 配置");
|
|
210
194
|
|
|
211
195
|
const removedPlugin = removePathIfExists(targetPluginRoot);
|
|
212
|
-
const removedLegacyPlugin = legacyTargetPluginRoot === targetPluginRoot
|
|
213
|
-
? false
|
|
214
|
-
: removePathIfExists(legacyTargetPluginRoot);
|
|
215
196
|
const removedCache = removePathIfExists(targetPluginCacheRoot);
|
|
216
197
|
const removedMarketplace = removeCodexMarketplaceEntry(marketplaceFile, existingMarketplace);
|
|
217
|
-
const removedLegacyMarketplace = legacyMarketplaceFile === marketplaceFile
|
|
218
|
-
? false
|
|
219
|
-
: removeCodexMarketplaceEntry(legacyMarketplaceFile, existingLegacyMarketplace);
|
|
220
198
|
const removedConfig = removeCodexPluginConfig(configFile);
|
|
221
199
|
|
|
222
200
|
return {
|
|
223
201
|
host: "codex",
|
|
224
202
|
displayName: "Codex",
|
|
225
203
|
targetRoot: targetPluginRoot,
|
|
226
|
-
removed: removedPlugin ||
|
|
204
|
+
removed: removedPlugin || removedCache || removedMarketplace || removedConfig,
|
|
227
205
|
marketplaceFile,
|
|
228
206
|
configFile,
|
|
229
207
|
};
|
|
@@ -4,7 +4,7 @@ import { rememberEngineSelection } from "./engine_selection.mjs";
|
|
|
4
4
|
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
5
5
|
import { ensureDir, nowIso, writeText } from "./common.mjs";
|
|
6
6
|
import { isHostLeaseAlive } from "./host_lease.mjs";
|
|
7
|
-
import { saveBacklog } from "./config.mjs";
|
|
7
|
+
import { saveBacklog, writeStatus } from "./config.mjs";
|
|
8
8
|
import { reviewTaskCompletion } from "./completion_review.mjs";
|
|
9
9
|
import { updateTask } from "./backlog.mjs";
|
|
10
10
|
import { buildTaskPrompt } from "./prompt.mjs";
|
|
@@ -267,6 +267,16 @@ export async function executeSingleTask(context, options = {}) {
|
|
|
267
267
|
|
|
268
268
|
updateTask(execution.backlog, execution.task.id, { status: "in_progress", startedAt: nowIso() });
|
|
269
269
|
saveBacklog(context, execution.backlog);
|
|
270
|
+
writeStatus(context, {
|
|
271
|
+
ok: true,
|
|
272
|
+
sessionId: options.supervisorSessionId || "",
|
|
273
|
+
stage: "task-started",
|
|
274
|
+
taskId: execution.task.id,
|
|
275
|
+
taskTitle: execution.task.title,
|
|
276
|
+
runDir: execution.runDir,
|
|
277
|
+
summary: "",
|
|
278
|
+
message: `开始执行任务:${execution.task.title}`,
|
|
279
|
+
});
|
|
270
280
|
|
|
271
281
|
const state = {
|
|
272
282
|
engineResolution: execution.engineResolution,
|
package/src/runner_once.mjs
CHANGED
package/src/runner_status.mjs
CHANGED
|
@@ -90,6 +90,9 @@ export function renderStatusText(context, options = {}) {
|
|
|
90
90
|
const supervisor = fileExists(context.supervisorStateFile)
|
|
91
91
|
? readJson(context.supervisorStateFile)
|
|
92
92
|
: null;
|
|
93
|
+
const latestStatus = fileExists(context.statusFile)
|
|
94
|
+
? readJson(context.statusFile)
|
|
95
|
+
: null;
|
|
93
96
|
|
|
94
97
|
return [
|
|
95
98
|
"HelloLoop 状态",
|
|
@@ -108,8 +111,17 @@ export function renderStatusText(context, options = {}) {
|
|
|
108
111
|
`后台租约:${renderHostLeaseLabel(supervisor.lease)}`,
|
|
109
112
|
]
|
|
110
113
|
: []),
|
|
114
|
+
...(latestStatus?.taskTitle
|
|
115
|
+
? [
|
|
116
|
+
`当前运行任务:${latestStatus.taskTitle}`,
|
|
117
|
+
`当前运行目录:${latestStatus.runDir || "unknown"}`,
|
|
118
|
+
`当前运行阶段:${latestStatus.stage || "unknown"}`,
|
|
119
|
+
]
|
|
120
|
+
: []),
|
|
111
121
|
"",
|
|
112
122
|
nextTask ? "下一任务:" : "下一任务:无",
|
|
113
123
|
nextTask ? renderTaskSummary(nextTask) : "",
|
|
124
|
+
"",
|
|
125
|
+
"实时观察:helloloop watch",
|
|
114
126
|
].filter(Boolean).join("\n");
|
|
115
127
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { nowIso, writeText } from "./common.mjs";
|
|
4
|
+
import { appendText, nowIso, writeText } from "./common.mjs";
|
|
5
5
|
import { getEngineDisplayName } from "./engine_metadata.mjs";
|
|
6
6
|
import {
|
|
7
7
|
buildClaudeArgs,
|
|
@@ -165,6 +165,8 @@ export async function runEngineAttempt({
|
|
|
165
165
|
}) {
|
|
166
166
|
const attemptPromptFile = path.join(runDir, `${attemptPrefix}-prompt.md`);
|
|
167
167
|
const attemptLastMessageFile = path.join(runDir, `${attemptPrefix}-last-message.txt`);
|
|
168
|
+
const attemptStdoutFile = path.join(runDir, `${attemptPrefix}-stdout.log`);
|
|
169
|
+
const attemptStderrFile = path.join(runDir, `${attemptPrefix}-stderr.log`);
|
|
168
170
|
|
|
169
171
|
if (invocation.error) {
|
|
170
172
|
const result = {
|
|
@@ -208,6 +210,8 @@ export async function runEngineAttempt({
|
|
|
208
210
|
recoveryCount,
|
|
209
211
|
recoveryHistory,
|
|
210
212
|
});
|
|
213
|
+
writeText(attemptStdoutFile, "");
|
|
214
|
+
writeText(attemptStderrFile, "");
|
|
211
215
|
|
|
212
216
|
const result = await runChild(invocation.command, finalArgs, {
|
|
213
217
|
cwd: context.repoRoot,
|
|
@@ -226,6 +230,12 @@ export async function runEngineAttempt({
|
|
|
226
230
|
heartbeat: payload,
|
|
227
231
|
});
|
|
228
232
|
},
|
|
233
|
+
onStdout(text) {
|
|
234
|
+
appendText(attemptStdoutFile, text);
|
|
235
|
+
},
|
|
236
|
+
onStderr(text) {
|
|
237
|
+
appendText(attemptStderrFile, text);
|
|
238
|
+
},
|
|
229
239
|
shouldKeepRunning() {
|
|
230
240
|
return isHostLeaseAlive(hostLease);
|
|
231
241
|
},
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { launchSupervisedCommand, renderSupervisorLaunchSummary } from "./supervisor_runtime.mjs";
|
|
2
|
+
import { watchSupervisorSession } from "./supervisor_watch.mjs";
|
|
3
|
+
|
|
4
|
+
export function shouldUseSupervisor(options = {}) {
|
|
5
|
+
return !options.dryRun
|
|
6
|
+
&& process.env.HELLOLOOP_SUPERVISOR_ACTIVE !== "1";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function shouldAutoWatchSupervisor(options = {}) {
|
|
10
|
+
if (options.detach) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (options.watch === true) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (options.watch === false) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return Boolean(process.stdout.isTTY);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function launchAndMaybeWatchSupervisedCommand(context, command, options = {}) {
|
|
23
|
+
const session = launchSupervisedCommand(context, command, options);
|
|
24
|
+
console.log(renderSupervisorLaunchSummary(session));
|
|
25
|
+
|
|
26
|
+
if (!shouldAutoWatchSupervisor(options)) {
|
|
27
|
+
console.log("- 已切换为后台执行;可稍后运行 `helloloop watch` 或 `helloloop status` 查看进度。");
|
|
28
|
+
return {
|
|
29
|
+
detached: true,
|
|
30
|
+
exitCode: 0,
|
|
31
|
+
ok: true,
|
|
32
|
+
session,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log("- 已进入附着观察模式;按 Ctrl+C 仅退出观察,不会停止后台任务。");
|
|
37
|
+
const watchResult = await watchSupervisorSession(context, {
|
|
38
|
+
sessionId: session.sessionId,
|
|
39
|
+
pollMs: options.watchPollMs,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
detached: false,
|
|
43
|
+
exitCode: watchResult.exitCode,
|
|
44
|
+
ok: watchResult.ok,
|
|
45
|
+
session,
|
|
46
|
+
watchResult,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -6,6 +6,12 @@ import { createContext } from "./context.mjs";
|
|
|
6
6
|
import { nowIso, readJson, writeJson, readTextIfExists, timestampForFile } from "./common.mjs";
|
|
7
7
|
import { isHostLeaseAlive, renderHostLeaseLabel, resolveHostLease } from "./host_lease.mjs";
|
|
8
8
|
import { runLoop, runOnce } from "./runner.mjs";
|
|
9
|
+
import {
|
|
10
|
+
bindBackgroundTerminalSession,
|
|
11
|
+
cancelPreparedTerminalSessionBackground,
|
|
12
|
+
finalizePreparedTerminalSessionBackground,
|
|
13
|
+
prepareCurrentTerminalSessionForBackground,
|
|
14
|
+
} from "./terminal_session_limits.mjs";
|
|
9
15
|
|
|
10
16
|
const ACTIVE_STATUSES = new Set(["launching", "running"]);
|
|
11
17
|
const FINAL_STATUSES = new Set(["completed", "failed", "stopped"]);
|
|
@@ -78,6 +84,11 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
78
84
|
|
|
79
85
|
const sessionId = timestampForFile();
|
|
80
86
|
const lease = resolveHostLease({ hostContext: options.hostContext });
|
|
87
|
+
const terminalSession = prepareCurrentTerminalSessionForBackground({
|
|
88
|
+
command,
|
|
89
|
+
repoRoot: context.repoRoot,
|
|
90
|
+
sessionId,
|
|
91
|
+
});
|
|
81
92
|
const request = {
|
|
82
93
|
sessionId,
|
|
83
94
|
command,
|
|
@@ -85,8 +96,12 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
85
96
|
repoRoot: context.repoRoot,
|
|
86
97
|
configDirName: context.configDirName,
|
|
87
98
|
},
|
|
88
|
-
options: toSerializableOptions(
|
|
99
|
+
options: toSerializableOptions({
|
|
100
|
+
...options,
|
|
101
|
+
supervisorSessionId: sessionId,
|
|
102
|
+
}),
|
|
89
103
|
lease,
|
|
104
|
+
terminalSessionFile: terminalSession?.file || "",
|
|
90
105
|
};
|
|
91
106
|
|
|
92
107
|
fs.mkdirSync(context.supervisorRoot, { recursive: true });
|
|
@@ -122,6 +137,11 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
122
137
|
HELLOLOOP_SUPERVISOR_ACTIVE: "1",
|
|
123
138
|
},
|
|
124
139
|
});
|
|
140
|
+
finalizePreparedTerminalSessionBackground(child.pid ?? 0, {
|
|
141
|
+
command,
|
|
142
|
+
repoRoot: context.repoRoot,
|
|
143
|
+
sessionId,
|
|
144
|
+
});
|
|
125
145
|
child.unref();
|
|
126
146
|
writeState(context, {
|
|
127
147
|
sessionId,
|
|
@@ -137,6 +157,13 @@ export function launchSupervisedCommand(context, command, options = {}) {
|
|
|
137
157
|
pid: child.pid ?? 0,
|
|
138
158
|
lease,
|
|
139
159
|
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
cancelPreparedTerminalSessionBackground({
|
|
162
|
+
command,
|
|
163
|
+
repoRoot: context.repoRoot,
|
|
164
|
+
sessionId,
|
|
165
|
+
});
|
|
166
|
+
throw error;
|
|
140
167
|
} finally {
|
|
141
168
|
fs.closeSync(stdoutFd);
|
|
142
169
|
fs.closeSync(stderrFd);
|
|
@@ -184,6 +211,11 @@ export async function runSupervisedCommandFromSessionFile(sessionFile) {
|
|
|
184
211
|
const context = createContext(request.context || {});
|
|
185
212
|
const command = String(request.command || "").trim();
|
|
186
213
|
const lease = request.lease || {};
|
|
214
|
+
bindBackgroundTerminalSession(request.terminalSessionFile || "", {
|
|
215
|
+
command,
|
|
216
|
+
repoRoot: context.repoRoot,
|
|
217
|
+
sessionId: request.sessionId,
|
|
218
|
+
});
|
|
187
219
|
const commandOptions = {
|
|
188
220
|
...(request.options || {}),
|
|
189
221
|
hostLease: lease,
|