helloloop 0.6.1 → 0.7.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 +4 -4
- package/README.md +49 -11
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +5 -4
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +4 -2
- package/hosts/gemini/extension/GEMINI.md +3 -1
- package/hosts/gemini/extension/commands/helloloop.toml +5 -4
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/helloloop/SKILL.md +5 -3
- package/src/config.mjs +9 -8
- package/src/discovery.mjs +21 -2
- package/src/discovery_paths.mjs +65 -1
- package/src/email_notification.mjs +343 -0
- package/src/engine_process_support.mjs +294 -0
- package/src/engine_selection_settings.mjs +75 -9
- package/src/global_config.mjs +21 -0
- package/src/install_shared.mjs +50 -2
- package/src/process.mjs +452 -428
- package/src/runner_execute_task.mjs +20 -66
- package/src/runner_execution_support.mjs +0 -9
- package/src/runtime_recovery.mjs +61 -60
- package/templates/policy.template.json +3 -5
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
buildDoneResult,
|
|
15
15
|
buildFailureResult,
|
|
16
16
|
bumpFailureForNextStrategy,
|
|
17
|
-
maybeSwitchEngine,
|
|
18
17
|
recordFailure,
|
|
19
18
|
resolveExecutionSetup,
|
|
20
19
|
} from "./runner_execution_support.mjs";
|
|
@@ -37,36 +36,16 @@ async function handleEngineFailure(execution, state, attemptState, engineResult)
|
|
|
37
36
|
state.engineResolution.engine,
|
|
38
37
|
previousFailure,
|
|
39
38
|
);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"engine-failed",
|
|
51
|
-
previousFailure,
|
|
52
|
-
state.failureHistory.length,
|
|
53
|
-
state.engineResolution,
|
|
54
|
-
),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (isHardStopFailure("engine", previousFailure)) {
|
|
58
|
-
return {
|
|
59
|
-
action: "return",
|
|
60
|
-
result: buildFailureResult(
|
|
61
|
-
execution,
|
|
62
|
-
"engine-failed",
|
|
63
|
-
previousFailure,
|
|
64
|
-
state.failureHistory.length,
|
|
65
|
-
state.engineResolution,
|
|
66
|
-
),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return { action: "continue", previousFailure };
|
|
39
|
+
return {
|
|
40
|
+
action: "return",
|
|
41
|
+
result: buildFailureResult(
|
|
42
|
+
execution,
|
|
43
|
+
"engine-failed",
|
|
44
|
+
previousFailure,
|
|
45
|
+
state.failureHistory.length,
|
|
46
|
+
state.engineResolution,
|
|
47
|
+
),
|
|
48
|
+
};
|
|
70
49
|
}
|
|
71
50
|
|
|
72
51
|
function handleVerifyFailure(execution, state, attemptState, verifyResult) {
|
|
@@ -91,36 +70,16 @@ function handleVerifyFailure(execution, state, attemptState, verifyResult) {
|
|
|
91
70
|
async function handleReviewFailure(execution, state, attemptState, reviewResult) {
|
|
92
71
|
const previousFailure = reviewResult.summary;
|
|
93
72
|
recordFailure(state.failureHistory, attemptState.strategyIndex, attemptState.attemptIndex, "task_review", previousFailure);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"task-review-failed",
|
|
105
|
-
previousFailure,
|
|
106
|
-
state.failureHistory.length,
|
|
107
|
-
state.engineResolution,
|
|
108
|
-
),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
if (isHardStopFailure("review", previousFailure)) {
|
|
112
|
-
return {
|
|
113
|
-
action: "return",
|
|
114
|
-
result: buildFailureResult(
|
|
115
|
-
execution,
|
|
116
|
-
"task-review-failed",
|
|
117
|
-
previousFailure,
|
|
118
|
-
state.failureHistory.length,
|
|
119
|
-
state.engineResolution,
|
|
120
|
-
),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
return { action: "continue", previousFailure };
|
|
73
|
+
return {
|
|
74
|
+
action: "return",
|
|
75
|
+
result: buildFailureResult(
|
|
76
|
+
execution,
|
|
77
|
+
"task-review-failed",
|
|
78
|
+
previousFailure,
|
|
79
|
+
state.failureHistory.length,
|
|
80
|
+
state.engineResolution,
|
|
81
|
+
),
|
|
82
|
+
};
|
|
124
83
|
}
|
|
125
84
|
|
|
126
85
|
function handleIncompleteReview(execution, state, attemptState, reviewResult) {
|
|
@@ -268,11 +227,6 @@ export async function executeSingleTask(context, options = {}) {
|
|
|
268
227
|
state,
|
|
269
228
|
buildAttemptState(execution.runDir, strategyIndex, attemptIndex, makeAttemptDir),
|
|
270
229
|
);
|
|
271
|
-
if (outcome.action === "switch") {
|
|
272
|
-
state.previousFailure = outcome.previousFailure;
|
|
273
|
-
state.engineResolution = outcome.engineResolution;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
230
|
if (outcome.action === "continue") {
|
|
277
231
|
state.previousFailure = outcome.previousFailure;
|
|
278
232
|
continue;
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from "./config.mjs";
|
|
14
14
|
import { getTask, selectNextTask, unresolvedDependencies, updateTask } from "./backlog.mjs";
|
|
15
15
|
import { makeRunDir } from "./runner_status.mjs";
|
|
16
|
-
import { resolveRuntimeRecoveryPolicy } from "./runtime_recovery.mjs";
|
|
17
16
|
|
|
18
17
|
function resolveTask(backlog, options) {
|
|
19
18
|
if (options.taskId) {
|
|
@@ -121,14 +120,6 @@ export function buildDoneResult(execution, finalMessage, attempts, engineResolut
|
|
|
121
120
|
});
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
export async function maybeSwitchEngine(execution, engineResolution, previousFailure, phaseLabel) {
|
|
125
|
-
const recoveryPolicy = resolveRuntimeRecoveryPolicy(execution.policy);
|
|
126
|
-
if (!recoveryPolicy.allowEngineSwitch) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
123
|
export function recordFailure(failureHistory, strategyIndex, attemptIndex, kind, summary) {
|
|
133
124
|
failureHistory.push({
|
|
134
125
|
strategyIndex,
|
package/src/runtime_recovery.mjs
CHANGED
|
@@ -3,21 +3,19 @@ import { tailText } from "./common.mjs";
|
|
|
3
3
|
|
|
4
4
|
const defaultRuntimeRecoveryPolicy = {
|
|
5
5
|
enabled: true,
|
|
6
|
-
allowEngineSwitch: false,
|
|
7
6
|
heartbeatIntervalSeconds: 60,
|
|
8
7
|
stallWarningSeconds: 900,
|
|
9
8
|
maxIdleSeconds: 2700,
|
|
10
9
|
killGraceSeconds: 10,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
maxUnknownRecoveries: 1,
|
|
10
|
+
healthProbeTimeoutSeconds: 120,
|
|
11
|
+
hardRetryDelaysSeconds: [900, 900, 900, 900, 900],
|
|
12
|
+
softRetryDelaysSeconds: [900, 900, 900, 900, 900, 1800, 1800, 3600, 5400, 7200, 9000, 10800],
|
|
15
13
|
};
|
|
16
14
|
|
|
17
15
|
const HARD_STOP_MATCHERS = [
|
|
18
16
|
{
|
|
19
17
|
code: "invalid_request",
|
|
20
|
-
reason: "
|
|
18
|
+
reason: "当前错误更像请求、参数、协议或输出格式问题,需要人工复核调用与提示词。",
|
|
21
19
|
patterns: [
|
|
22
20
|
" 400 ",
|
|
23
21
|
"400 bad request",
|
|
@@ -36,7 +34,7 @@ const HARD_STOP_MATCHERS = [
|
|
|
36
34
|
},
|
|
37
35
|
{
|
|
38
36
|
code: "auth",
|
|
39
|
-
reason: "
|
|
37
|
+
reason: "当前错误更像登录、鉴权、订阅或权限问题,需要等待环境恢复或人工修复。",
|
|
40
38
|
patterns: [
|
|
41
39
|
"401",
|
|
42
40
|
"403",
|
|
@@ -52,9 +50,22 @@ const HARD_STOP_MATCHERS = [
|
|
|
52
50
|
"insufficient permissions",
|
|
53
51
|
],
|
|
54
52
|
},
|
|
53
|
+
{
|
|
54
|
+
code: "billing",
|
|
55
|
+
reason: "当前错误更像额度、余额、支付或账单问题,短时间内通常不会自行消失。",
|
|
56
|
+
patterns: [
|
|
57
|
+
"payment required",
|
|
58
|
+
"billing",
|
|
59
|
+
"insufficient balance",
|
|
60
|
+
"credit",
|
|
61
|
+
"quota exceeded",
|
|
62
|
+
"hard limit",
|
|
63
|
+
"balance",
|
|
64
|
+
],
|
|
65
|
+
},
|
|
55
66
|
{
|
|
56
67
|
code: "environment",
|
|
57
|
-
reason: "当前错误更像本地 CLI
|
|
68
|
+
reason: "当前错误更像本地 CLI 缺失、权限不足或文件系统问题,需要人工修复环境。",
|
|
58
69
|
patterns: [
|
|
59
70
|
"command not found",
|
|
60
71
|
"is not recognized",
|
|
@@ -66,7 +77,7 @@ const HARD_STOP_MATCHERS = [
|
|
|
66
77
|
},
|
|
67
78
|
];
|
|
68
79
|
|
|
69
|
-
const
|
|
80
|
+
const SOFT_STOP_MATCHERS = [
|
|
70
81
|
{
|
|
71
82
|
code: "rate_limit",
|
|
72
83
|
reason: "当前引擎可能遇到配额、限流或临时容量不足。",
|
|
@@ -75,11 +86,9 @@ const RECOVERABLE_MATCHERS = [
|
|
|
75
86
|
"rate limit",
|
|
76
87
|
"too many requests",
|
|
77
88
|
"quota",
|
|
78
|
-
"credit",
|
|
79
89
|
"usage limit",
|
|
80
90
|
"capacity",
|
|
81
91
|
"overloaded",
|
|
82
|
-
"insufficient balance",
|
|
83
92
|
"try again later",
|
|
84
93
|
],
|
|
85
94
|
},
|
|
@@ -153,7 +162,6 @@ export function resolveRuntimeRecoveryPolicy(policy = {}) {
|
|
|
153
162
|
const configured = policy?.runtimeRecovery || {};
|
|
154
163
|
return {
|
|
155
164
|
enabled: configured.enabled !== false,
|
|
156
|
-
allowEngineSwitch: configured.allowEngineSwitch === true,
|
|
157
165
|
heartbeatIntervalSeconds: normalizeSeconds(
|
|
158
166
|
configured.heartbeatIntervalSeconds,
|
|
159
167
|
defaultRuntimeRecoveryPolicy.heartbeatIntervalSeconds,
|
|
@@ -170,36 +178,35 @@ export function resolveRuntimeRecoveryPolicy(policy = {}) {
|
|
|
170
178
|
configured.killGraceSeconds,
|
|
171
179
|
defaultRuntimeRecoveryPolicy.killGraceSeconds,
|
|
172
180
|
),
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
healthProbeTimeoutSeconds: normalizeSeconds(
|
|
182
|
+
configured.healthProbeTimeoutSeconds,
|
|
183
|
+
defaultRuntimeRecoveryPolicy.healthProbeTimeoutSeconds,
|
|
176
184
|
),
|
|
177
|
-
|
|
178
|
-
configured.
|
|
179
|
-
defaultRuntimeRecoveryPolicy.
|
|
185
|
+
hardRetryDelaysSeconds: normalizeSecondsList(
|
|
186
|
+
configured.hardRetryDelaysSeconds,
|
|
187
|
+
defaultRuntimeRecoveryPolicy.hardRetryDelaysSeconds,
|
|
180
188
|
),
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Math.trunc(normalizeSeconds(configured.maxUnknownRecoveries, defaultRuntimeRecoveryPolicy.maxUnknownRecoveries)),
|
|
189
|
+
softRetryDelaysSeconds: normalizeSecondsList(
|
|
190
|
+
configured.softRetryDelaysSeconds,
|
|
191
|
+
defaultRuntimeRecoveryPolicy.softRetryDelaysSeconds,
|
|
185
192
|
),
|
|
186
193
|
};
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
export function
|
|
190
|
-
|
|
191
|
-
? recoveryPolicy.
|
|
192
|
-
:
|
|
196
|
+
export function getRuntimeRecoverySchedule(recoveryPolicy, family = "soft") {
|
|
197
|
+
return family === "hard"
|
|
198
|
+
? recoveryPolicy.hardRetryDelaysSeconds
|
|
199
|
+
: recoveryPolicy.softRetryDelaysSeconds;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function selectRuntimeRecoveryDelayMs(recoveryPolicy, family, nextRecoveryIndex) {
|
|
203
|
+
const delays = getRuntimeRecoverySchedule(recoveryPolicy, family);
|
|
193
204
|
const offset = Math.max(0, Number(nextRecoveryIndex || 1) - 1);
|
|
194
|
-
const seconds = delays[
|
|
195
|
-
return Math.max(0, seconds) * 1000;
|
|
205
|
+
const seconds = delays[offset] ?? null;
|
|
206
|
+
return seconds == null ? -1 : Math.max(0, seconds) * 1000;
|
|
196
207
|
}
|
|
197
208
|
|
|
198
|
-
export function classifyRuntimeRecoveryFailure({
|
|
199
|
-
result = {},
|
|
200
|
-
recoveryPolicy = defaultRuntimeRecoveryPolicy,
|
|
201
|
-
recoveryCount = 0,
|
|
202
|
-
} = {}) {
|
|
209
|
+
export function classifyRuntimeRecoveryFailure({ result = {} } = {}) {
|
|
203
210
|
const normalized = normalizeText([
|
|
204
211
|
result.stderr,
|
|
205
212
|
result.stdout,
|
|
@@ -209,56 +216,49 @@ export function classifyRuntimeRecoveryFailure({
|
|
|
209
216
|
|
|
210
217
|
if (result.watchdogTriggered || result.idleTimeout) {
|
|
211
218
|
return {
|
|
212
|
-
recoverable: true,
|
|
213
219
|
code: "watchdog_idle",
|
|
214
|
-
|
|
220
|
+
family: "soft",
|
|
221
|
+
reason: "当前进程长时间没有可见进展,HelloLoop 将按软阻塞策略继续探测并恢复。",
|
|
215
222
|
};
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
for (const matcher of HARD_STOP_MATCHERS) {
|
|
219
226
|
if (hasMatcher(normalized, matcher)) {
|
|
220
227
|
return {
|
|
221
|
-
recoverable: false,
|
|
222
228
|
code: matcher.code,
|
|
229
|
+
family: "hard",
|
|
223
230
|
reason: matcher.reason,
|
|
224
231
|
};
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
234
|
|
|
228
|
-
for (const matcher of
|
|
235
|
+
for (const matcher of SOFT_STOP_MATCHERS) {
|
|
229
236
|
if (hasMatcher(normalized, matcher)) {
|
|
230
237
|
return {
|
|
231
|
-
recoverable: true,
|
|
232
238
|
code: matcher.code,
|
|
239
|
+
family: "soft",
|
|
233
240
|
reason: matcher.reason,
|
|
234
241
|
};
|
|
235
242
|
}
|
|
236
243
|
}
|
|
237
244
|
|
|
238
|
-
const emptyFailure = !normalized.trim() && !result.ok;
|
|
239
|
-
if (emptyFailure) {
|
|
240
|
-
return {
|
|
241
|
-
recoverable: recoveryCount < (recoveryPolicy.maxUnknownRecoveries || 0),
|
|
242
|
-
code: "empty_failure",
|
|
243
|
-
reason: "当前失败没有返回可判定的错误文本,HelloLoop 将按无人值守策略先尝试一次同引擎恢复。",
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (recoveryPolicy.retryOnUnknownFailure && recoveryCount < (recoveryPolicy.maxUnknownRecoveries || 0)) {
|
|
248
|
-
return {
|
|
249
|
-
recoverable: true,
|
|
250
|
-
code: "unknown_failure",
|
|
251
|
-
reason: "当前错误类型无法稳定归类,HelloLoop 将按无人值守策略先尝试一次同引擎恢复。",
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
245
|
return {
|
|
256
|
-
recoverable: false,
|
|
257
246
|
code: "unknown_failure",
|
|
258
|
-
|
|
247
|
+
family: "soft",
|
|
248
|
+
reason: "当前错误类型无法稳定归类,HelloLoop 将按软阻塞策略持续探测并恢复。",
|
|
259
249
|
};
|
|
260
250
|
}
|
|
261
251
|
|
|
252
|
+
export function buildEngineHealthProbePrompt(engine) {
|
|
253
|
+
return [
|
|
254
|
+
"HELLOLOOP_ENGINE_HEALTH_PROBE",
|
|
255
|
+
`当前只做 ${getEngineDisplayName(engine)} 引擎健康探测。`,
|
|
256
|
+
"禁止修改仓库、禁止执行开发任务、禁止输出解释。",
|
|
257
|
+
"只需确认自己当前能正常接收请求并返回简短结果。",
|
|
258
|
+
"若当前可用,请直接回复:HELLOLOOP_ENGINE_OK",
|
|
259
|
+
].join("\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
262
|
export function buildRuntimeRecoveryPrompt({
|
|
263
263
|
basePrompt,
|
|
264
264
|
engine,
|
|
@@ -287,15 +287,16 @@ export function buildRuntimeRecoveryPrompt({
|
|
|
287
287
|
].join("\n");
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
export function renderRuntimeRecoverySummary(recoveryHistory = []) {
|
|
290
|
+
export function renderRuntimeRecoverySummary(recoveryHistory = [], failure = null) {
|
|
291
291
|
if (!Array.isArray(recoveryHistory) || !recoveryHistory.length) {
|
|
292
292
|
return "";
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
return [
|
|
296
|
-
`HelloLoop
|
|
296
|
+
`HelloLoop 已按${failure?.family === "hard" ? "硬阻塞" : "软阻塞"}策略进行 ${recoveryHistory.length} 次自动探测/恢复。`,
|
|
297
297
|
...recoveryHistory.map((item) => (
|
|
298
|
-
`- 第 ${item.recoveryIndex}
|
|
298
|
+
`- 第 ${item.recoveryIndex} 次:等待 ${item.delaySeconds} 秒;探测 ${item.probeStatus || "unknown"};任务 ${item.taskStatus || "unknown"}`
|
|
299
299
|
)),
|
|
300
|
+
"自动恢复额度已用尽,当前已暂停等待用户介入。",
|
|
300
301
|
].join("\n");
|
|
301
302
|
}
|
|
@@ -9,15 +9,13 @@
|
|
|
9
9
|
"stopOnHighRisk": true,
|
|
10
10
|
"runtimeRecovery": {
|
|
11
11
|
"enabled": true,
|
|
12
|
-
"allowEngineSwitch": false,
|
|
13
12
|
"heartbeatIntervalSeconds": 60,
|
|
14
13
|
"stallWarningSeconds": 900,
|
|
15
14
|
"maxIdleSeconds": 2700,
|
|
16
15
|
"killGraceSeconds": 10,
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"maxUnknownRecoveries": 1
|
|
16
|
+
"healthProbeTimeoutSeconds": 120,
|
|
17
|
+
"hardRetryDelaysSeconds": [900, 900, 900, 900, 900],
|
|
18
|
+
"softRetryDelaysSeconds": [900, 900, 900, 900, 900, 1800, 1800, 3600, 5400, 7200, 9000, 10800]
|
|
21
19
|
},
|
|
22
20
|
"codex": {
|
|
23
21
|
"model": "",
|