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.
@@ -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
- const nextResolution = await maybeSwitchEngine(execution, state.engineResolution, previousFailure, "执行阶段");
42
- if (nextResolution) {
43
- return { action: "switch", previousFailure, engineResolution: nextResolution };
44
- }
45
- if (engineResult.recoveryFailure?.recoverable === false) {
46
- return {
47
- action: "return",
48
- result: buildFailureResult(
49
- execution,
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
- const nextResolution = await maybeSwitchEngine(execution, state.engineResolution, previousFailure, "任务复核阶段");
96
- if (nextResolution) {
97
- return { action: "switch", previousFailure, engineResolution: nextResolution };
98
- }
99
- if (reviewResult.raw?.recoveryFailure?.recoverable === false) {
100
- return {
101
- action: "return",
102
- result: buildFailureResult(
103
- execution,
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,
@@ -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
- maxPhaseRecoveries: 4,
12
- retryDelaysSeconds: [120, 300, 900, 1800],
13
- retryOnUnknownFailure: true,
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 RECOVERABLE_MATCHERS = [
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
- maxPhaseRecoveries: Math.max(
174
- 0,
175
- Math.trunc(normalizeSeconds(configured.maxPhaseRecoveries, defaultRuntimeRecoveryPolicy.maxPhaseRecoveries)),
181
+ healthProbeTimeoutSeconds: normalizeSeconds(
182
+ configured.healthProbeTimeoutSeconds,
183
+ defaultRuntimeRecoveryPolicy.healthProbeTimeoutSeconds,
176
184
  ),
177
- retryDelaysSeconds: normalizeSecondsList(
178
- configured.retryDelaysSeconds,
179
- defaultRuntimeRecoveryPolicy.retryDelaysSeconds,
185
+ hardRetryDelaysSeconds: normalizeSecondsList(
186
+ configured.hardRetryDelaysSeconds,
187
+ defaultRuntimeRecoveryPolicy.hardRetryDelaysSeconds,
180
188
  ),
181
- retryOnUnknownFailure: configured.retryOnUnknownFailure !== false,
182
- maxUnknownRecoveries: Math.max(
183
- 0,
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 selectRuntimeRecoveryDelayMs(recoveryPolicy, nextRecoveryIndex) {
190
- const delays = Array.isArray(recoveryPolicy?.retryDelaysSeconds) && recoveryPolicy.retryDelaysSeconds.length
191
- ? recoveryPolicy.retryDelaysSeconds
192
- : defaultRuntimeRecoveryPolicy.retryDelaysSeconds;
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[Math.min(offset, delays.length - 1)] || 0;
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
- reason: "当前进程长时间没有可见进展,HelloLoop 已按看门狗策略终止并准备同引擎恢复。",
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 RECOVERABLE_MATCHERS) {
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
- reason: "当前错误无法判断为可安全自动恢复,已停止本轮自动恢复。",
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 已按无人值守策略进行 ${recoveryHistory.length} 次同引擎自动恢复。`,
296
+ `HelloLoop 已按${failure?.family === "hard" ? "硬阻塞" : "软阻塞"}策略进行 ${recoveryHistory.length} 次自动探测/恢复。`,
297
297
  ...recoveryHistory.map((item) => (
298
- `- 第 ${item.recoveryIndex} 次恢复:${item.reason}(等待 ${item.delaySeconds} 秒)`
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
- "maxPhaseRecoveries": 4,
18
- "retryDelaysSeconds": [120, 300, 900, 1800],
19
- "retryOnUnknownFailure": true,
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": "",