claude-coder 1.4.0 → 1.5.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/README.md +4 -1
- package/docs/ARCHITECTURE.md +36 -9
- package/docs/README.en.md +2 -1
- package/package.json +1 -1
- package/src/config.js +1 -1
- package/src/indicator.js +38 -11
- package/src/prompts.js +67 -9
- package/src/runner.js +19 -0
- package/src/session.js +30 -6
package/README.md
CHANGED
|
@@ -98,7 +98,8 @@ your-project/
|
|
|
98
98
|
tests.json # 验证记录
|
|
99
99
|
test.env # 测试凭证(API Key 等,可选)
|
|
100
100
|
playwright-auth.json # Playwright 登录状态(可选,auth 命令生成)
|
|
101
|
-
.runtime/ #
|
|
101
|
+
.runtime/ # 临时文件
|
|
102
|
+
logs/ # 每 session 独立日志(含工具调用记录)
|
|
102
103
|
requirements.md # 需求文档(可选)
|
|
103
104
|
```
|
|
104
105
|
|
|
@@ -108,6 +109,8 @@ your-project/
|
|
|
108
109
|
|
|
109
110
|
**中断恢复**:直接重新运行 `claude-coder run`,会从上次中断处继续。
|
|
110
111
|
|
|
112
|
+
**长时间无响应**:模型处理复杂文件时可能出现 10-20 分钟的思考间隔(spinner 会显示红色警告),这是正常行为。超过 30 分钟无工具调用时 Harness 会自动中断并重试。可通过 `.env` 中 `SESSION_STALL_TIMEOUT=秒数` 调整阈值。
|
|
113
|
+
|
|
111
114
|
**跳过任务**:将 `.claude-coder/tasks.json` 中该任务的 `status` 改为 `done`。
|
|
112
115
|
|
|
113
116
|
**Windows 支持**:完全支持,纯 Node.js 实现。
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -21,9 +21,12 @@ Agent 在单次 session 中应最大化推进任务进度。**任何非致命问
|
|
|
21
21
|
- 缺少 API Key → 用 mock 或代码逻辑验证替代,记录到 `test.env`,继续推进
|
|
22
22
|
- 测试环境未就绪 → 跳过该测试步骤,完成其余可验证的步骤
|
|
23
23
|
- 服务启动失败 → 尝试排查修复,无法修复则记录问题后推进代码实现
|
|
24
|
+
- **长时间思考是正常行为**:模型处理大文件(如 500+ 行的代码文件)时可能出现 10-20 分钟的思考间隔,不代表卡死
|
|
24
25
|
|
|
25
26
|
**反面案例**:Agent 因 `OPENAI_API_KEY` 缺失直接标记任务 `failed` → 浪费整个 session
|
|
26
27
|
|
|
28
|
+
> Harness 兜底机制:当工具调用间隔超过 `SESSION_STALL_TIMEOUT`(默认 30 分钟)时自动中断 session 并触发 rollback + 重试。此阈值设计为远超正常思考时长,仅捕捉真正的卡死场景。
|
|
29
|
+
|
|
27
30
|
### 规则 2:回滚即彻底回滚
|
|
28
31
|
|
|
29
32
|
`git reset --hard` 是全量回滚,不做部分文件保护。
|
|
@@ -56,6 +59,16 @@ Agent 在单次 session 中应最大化推进任务进度。**任何非致命问
|
|
|
56
59
|
|
|
57
60
|
Agent 不应浪费工具调用读取 harness 已知的数据。所有可预读的上下文通过 prompt hint 注入(见第 5 节 Prompt 注入架构)。
|
|
58
61
|
|
|
62
|
+
### 规则 6:停顿检测 — 模型卡死自动恢复
|
|
63
|
+
|
|
64
|
+
模型可能长时间「思考」但不调用工具(20+ 分钟无进展)。Harness 通过 PreToolUse hook 追踪最后一次工具调用时间:
|
|
65
|
+
|
|
66
|
+
- 无工具调用 > `SESSION_STALL_TIMEOUT`(默认 1800 秒 / 30 分钟) → 自动中断 session
|
|
67
|
+
- 中断后进入 runner 的重试逻辑(连续失败 ≥ 3 次 → 标记 task failed)
|
|
68
|
+
- Spinner 在无工具调用 > 2 分钟时显示红色警告
|
|
69
|
+
|
|
70
|
+
配置方式:`.claude-coder/.env` 中设置 `SESSION_STALL_TIMEOUT=1800`(秒)
|
|
71
|
+
|
|
59
72
|
---
|
|
60
73
|
|
|
61
74
|
## 1. 核心架构
|
|
@@ -79,7 +92,7 @@ flowchart TB
|
|
|
79
92
|
direction TB
|
|
80
93
|
profile["project_profile.json<br/>tasks.json"]
|
|
81
94
|
runtime["session_result.json<br/>progress.json"]
|
|
82
|
-
phase[".runtime/<br/>phase / step /
|
|
95
|
+
phase[".runtime/<br/>phase / step / logs/"]
|
|
83
96
|
end
|
|
84
97
|
|
|
85
98
|
scan -->|"systemPrompt =<br/>CLAUDE.md + SCAN_PROTOCOL.md"| query
|
|
@@ -96,7 +109,8 @@ flowchart TB
|
|
|
96
109
|
**核心特征:**
|
|
97
110
|
- **项目无关**:项目信息由 Agent 扫描后存入 `project_profile.json`,harness 不含项目特定逻辑
|
|
98
111
|
- **可恢复**:通过 `session_result.json` 跨会话记忆,任意 session 可断点续跑
|
|
99
|
-
- **可观测**:SDK 内联 `PreToolUse` hook
|
|
112
|
+
- **可观测**:SDK 内联 `PreToolUse` hook 实时显示当前工具、操作目标和停顿警告
|
|
113
|
+
- **自愈**:编辑死循环检测 + 停顿超时自动中断 + runner 重试机制
|
|
100
114
|
- **跨平台**:纯 Node.js 实现,macOS / Linux / Windows 通用
|
|
101
115
|
- **零依赖**:`dependencies` 为空,Claude Agent SDK 作为 peerDependency
|
|
102
116
|
|
|
@@ -144,14 +158,14 @@ bin/cli.js CLI 入口:参数解析、命令路由、SDK peerDep 检
|
|
|
144
158
|
src/
|
|
145
159
|
config.js 配置管理:.env 加载、模型映射、环境变量构建、全局同步
|
|
146
160
|
runner.js 主循环:scan → session → validate → retry/rollback
|
|
147
|
-
session.js SDK 交互:query() 调用、hook
|
|
161
|
+
session.js SDK 交互:query() 调用、hook 绑定、停顿检测、日志流
|
|
148
162
|
prompts.js 提示语构建:系统 prompt 组合 + 条件 hint + 任务分解指导
|
|
149
163
|
init.js 环境初始化:读取 profile 执行依赖安装、服务启动、健康检查
|
|
150
164
|
scanner.js 初始化扫描:调用 runScanSession + 重试
|
|
151
165
|
validator.js 校验引擎:分层校验(fatal/recoverable/pass)+ git 检查 + 测试覆盖
|
|
152
166
|
tasks.js 任务管理:CRUD + 状态机 + 进度展示
|
|
153
167
|
auth.js Playwright 凭证:导出登录状态 + MCP 配置 + gitignore
|
|
154
|
-
indicator.js 进度指示:终端 spinner + phase/step 文件写入
|
|
168
|
+
indicator.js 进度指示:终端 spinner + 工具目标显示 + 停顿警告 + phase/step 文件写入
|
|
155
169
|
setup.js 交互式配置:模型选择、API Key、MCP 工具
|
|
156
170
|
templates/
|
|
157
171
|
CLAUDE.md Agent 协议(注入为 systemPrompt)
|
|
@@ -193,7 +207,7 @@ templates/
|
|
|
193
207
|
| `session_result.json` | 每次 session 结束 | 当前 session 结果(扁平格式,向后兼容旧 `current` 包装) |
|
|
194
208
|
| `playwright-auth.json` | `claude-coder auth` | Playwright 登录状态(cookies + localStorage) |
|
|
195
209
|
| `tests.json` | 首次测试时 | 验证记录(防止反复测试) |
|
|
196
|
-
| `.runtime/` | 运行时 | 临时文件(phase、step、
|
|
210
|
+
| `.runtime/` | 运行时 | 临时文件(phase、step、logs/);工具调用记录合并到 session log |
|
|
197
211
|
|
|
198
212
|
---
|
|
199
213
|
|
|
@@ -299,17 +313,27 @@ sequenceDiagram
|
|
|
299
313
|
participant SDK as Claude Agent SDK
|
|
300
314
|
participant Hook as inferPhaseStep()
|
|
301
315
|
participant Ind as Indicator (setInterval)
|
|
316
|
+
participant Stall as stallChecker (30s)
|
|
302
317
|
participant Term as 终端
|
|
303
318
|
|
|
304
319
|
SDK->>Hook: PreToolUse 回调<br/>{tool_name: "Edit", tool_input: {path: "src/app.tsx"}}
|
|
305
320
|
Hook->>Hook: 推断阶段: Edit → coding
|
|
306
321
|
Hook->>Ind: updatePhase("coding")
|
|
322
|
+
Hook->>Ind: lastToolTime = now
|
|
323
|
+
Hook->>Ind: toolTarget = "src/app.tsx"
|
|
307
324
|
Hook->>Ind: appendActivity("Edit", "src/app.tsx")
|
|
308
325
|
|
|
309
326
|
Note over SDK,Hook: 同步回调,return {decision: "allow"}
|
|
310
327
|
|
|
311
328
|
loop 每 500ms
|
|
312
|
-
Ind->>Term: ⠋ [Session 3] 编码中 02:15 |
|
|
329
|
+
Ind->>Term: ⠋ [Session 3] 编码中 02:15 | 读取文件: ppt_generator.py
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
loop 每 30s
|
|
333
|
+
Stall->>Ind: 检查 now - lastToolTime
|
|
334
|
+
alt 超过 STALL_TIMEOUT
|
|
335
|
+
Stall->>SDK: stallDetected = true → break for-await
|
|
336
|
+
end
|
|
313
337
|
end
|
|
314
338
|
```
|
|
315
339
|
|
|
@@ -364,9 +388,12 @@ Harness 在 `buildCodingPrompt()` 中预读 `tasks.json`,将下一个待办任
|
|
|
364
388
|
|
|
365
389
|
Harness 在 `buildCodingPrompt()` 中预读 `session_result.json`,将上次会话的 task_id、结果和 notes 摘要注入 user prompt。Agent 无需自行读取历史 session 数据。
|
|
366
390
|
|
|
367
|
-
###
|
|
391
|
+
### 自愈机制
|
|
392
|
+
|
|
393
|
+
**编辑死循环检测**:PreToolUse hook 追踪每个文件的编辑次数,同一文件 Write/Edit 超 5 次 → `decision: "block"`。
|
|
368
394
|
|
|
369
|
-
|
|
395
|
+
**停顿超时检测**:每 30 秒检查 `indicator.lastToolTime`,若距上次工具调用超过 `SESSION_STALL_TIMEOUT`(默认 1800 秒 / 30 分钟),自动 `break` 退出并触发 rollback + 重试。
|
|
396
|
+
> 注意:模型在处理复杂文件时可能出现 10-20 分钟的长时间思考,这是正常行为。超时设为 30 分钟以避免误杀正常思考。可通过 `.env` 中 `SESSION_STALL_TIMEOUT=秒数` 自定义。
|
|
370
397
|
|
|
371
398
|
### 文件权限模型
|
|
372
399
|
|
|
@@ -423,7 +450,7 @@ query({
|
|
|
423
450
|
### V2 迁移条件(等待稳定后)
|
|
424
451
|
|
|
425
452
|
1. V2 去掉 `unstable_` 前缀,正式发布
|
|
426
|
-
2. V2 支持 Hooks(当前项目依赖 PreToolUse 做 spinner
|
|
453
|
+
2. V2 支持 Hooks(当前项目依赖 PreToolUse 做 spinner 和日志记录)
|
|
427
454
|
3. V2 支持 Subagents(未来可能用于扫描 Agent / 编码 Agent 分离)
|
|
428
455
|
|
|
429
456
|
### 可利用但尚未使用的 V1 特性
|
package/docs/README.en.md
CHANGED
|
@@ -86,7 +86,8 @@ your-project/
|
|
|
86
86
|
tests.json # Verification records
|
|
87
87
|
test.env # Test credentials (API keys, optional)
|
|
88
88
|
playwright-auth.json # Playwright login state (optional, via auth command)
|
|
89
|
-
.runtime/ # Temp files
|
|
89
|
+
.runtime/ # Temp files
|
|
90
|
+
logs/ # Per-session logs (with tool call traces)
|
|
90
91
|
requirements.md # Requirements (optional)
|
|
91
92
|
```
|
|
92
93
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -63,7 +63,6 @@ function paths() {
|
|
|
63
63
|
runtime,
|
|
64
64
|
phaseFile: path.join(runtime, 'phase'),
|
|
65
65
|
stepFile: path.join(runtime, 'step'),
|
|
66
|
-
activityLog: path.join(runtime, 'activity.log'),
|
|
67
66
|
logsDir: path.join(runtime, 'logs'),
|
|
68
67
|
};
|
|
69
68
|
}
|
|
@@ -107,6 +106,7 @@ function loadConfig() {
|
|
|
107
106
|
defaultSonnet: env.ANTHROPIC_DEFAULT_SONNET_MODEL || '',
|
|
108
107
|
defaultHaiku: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
|
|
109
108
|
thinkingBudget: env.ANTHROPIC_THINKING_BUDGET || '',
|
|
109
|
+
stallTimeout: parseInt(env.SESSION_STALL_TIMEOUT, 10) || 1800,
|
|
110
110
|
raw: env,
|
|
111
111
|
};
|
|
112
112
|
|
package/src/indicator.js
CHANGED
|
@@ -9,9 +9,11 @@ class Indicator {
|
|
|
9
9
|
constructor() {
|
|
10
10
|
this.phase = 'thinking';
|
|
11
11
|
this.step = '';
|
|
12
|
+
this.toolTarget = '';
|
|
12
13
|
this.spinnerIndex = 0;
|
|
13
14
|
this.timer = null;
|
|
14
15
|
this.lastActivity = '';
|
|
16
|
+
this.lastToolTime = Date.now();
|
|
15
17
|
this.sessionNum = 0;
|
|
16
18
|
this.startTime = Date.now();
|
|
17
19
|
}
|
|
@@ -41,13 +43,7 @@ class Indicator {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
appendActivity(toolName, summary) {
|
|
44
|
-
|
|
45
|
-
const entry = `[${ts}] ${toolName}: ${summary}`;
|
|
46
|
-
this.lastActivity = entry;
|
|
47
|
-
try {
|
|
48
|
-
const p = paths();
|
|
49
|
-
fs.appendFileSync(p.activityLog, entry + '\n', 'utf8');
|
|
50
|
-
} catch { /* ignore */ }
|
|
46
|
+
this.lastActivity = `${toolName}: ${summary}`;
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
_writePhaseFile() {
|
|
@@ -74,8 +70,17 @@ class Indicator {
|
|
|
74
70
|
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
75
71
|
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
76
72
|
|
|
73
|
+
const idleMs = Date.now() - this.lastToolTime;
|
|
74
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
75
|
+
|
|
77
76
|
let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
|
|
78
|
-
if (
|
|
77
|
+
if (idleMin >= 2) {
|
|
78
|
+
line += ` | ${COLOR.red}${idleMin}分无工具调用${COLOR.reset}`;
|
|
79
|
+
}
|
|
80
|
+
if (this.step) {
|
|
81
|
+
line += ` | ${this.step}`;
|
|
82
|
+
if (this.toolTarget) line += `: ${this.toolTarget}`;
|
|
83
|
+
}
|
|
79
84
|
return line;
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -94,6 +99,14 @@ class Indicator {
|
|
|
94
99
|
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
95
100
|
const name = (toolName || '').toLowerCase();
|
|
96
101
|
|
|
102
|
+
indicator.lastToolTime = Date.now();
|
|
103
|
+
|
|
104
|
+
const rawTarget = typeof toolInput === 'object'
|
|
105
|
+
? (toolInput.file_path || toolInput.path || toolInput.command || toolInput.pattern || '')
|
|
106
|
+
: String(toolInput || '');
|
|
107
|
+
const shortTarget = rawTarget.split('/').slice(-2).join('/').slice(0, 40);
|
|
108
|
+
indicator.toolTarget = shortTarget;
|
|
109
|
+
|
|
97
110
|
if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
98
111
|
indicator.updatePhase('coding');
|
|
99
112
|
} else if (name === 'bash' || name === 'shell') {
|
|
@@ -119,9 +132,23 @@ function inferPhaseStep(indicator, toolName, toolInput) {
|
|
|
119
132
|
indicator.updateStep('查阅文档');
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
let summary;
|
|
136
|
+
if (typeof toolInput === 'object') {
|
|
137
|
+
const target = toolInput.file_path || toolInput.path || '';
|
|
138
|
+
const cmd = toolInput.command || '';
|
|
139
|
+
const pattern = toolInput.pattern || '';
|
|
140
|
+
if (target) {
|
|
141
|
+
summary = target;
|
|
142
|
+
} else if (cmd) {
|
|
143
|
+
summary = cmd.slice(0, 200);
|
|
144
|
+
} else if (pattern) {
|
|
145
|
+
summary = `pattern: ${pattern}`;
|
|
146
|
+
} else {
|
|
147
|
+
summary = JSON.stringify(toolInput).slice(0, 200);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
summary = String(toolInput || '').slice(0, 200);
|
|
151
|
+
}
|
|
125
152
|
indicator.appendActivity(toolName, summary);
|
|
126
153
|
}
|
|
127
154
|
|
package/src/prompts.js
CHANGED
|
@@ -211,25 +211,83 @@ function buildScanPrompt(projectType, requirement) {
|
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
213
|
* Build user prompt for add sessions.
|
|
214
|
+
* Structure: Role (primacy) → Context → CoT → TaskGuide → Instruction (recency)
|
|
214
215
|
*/
|
|
215
216
|
function buildAddPrompt(instruction) {
|
|
217
|
+
const p = paths();
|
|
218
|
+
const projectRoot = getProjectRoot();
|
|
216
219
|
const taskGuide = buildTaskGuide();
|
|
220
|
+
|
|
221
|
+
// --- Context injection: pre-read project state ---
|
|
222
|
+
let profileContext = '';
|
|
223
|
+
if (fs.existsSync(p.profile)) {
|
|
224
|
+
try {
|
|
225
|
+
const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
226
|
+
const stack = profile.tech_stack || {};
|
|
227
|
+
const parts = [];
|
|
228
|
+
if (stack.backend?.framework) parts.push(`后端: ${stack.backend.framework}`);
|
|
229
|
+
if (stack.frontend?.framework) parts.push(`前端: ${stack.frontend.framework}`);
|
|
230
|
+
if (stack.backend?.language) parts.push(`语言: ${stack.backend.language}`);
|
|
231
|
+
if (parts.length) profileContext = `项目技术栈: ${parts.join(', ')}`;
|
|
232
|
+
} catch { /* ignore */ }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let taskContext = '';
|
|
236
|
+
let recentExamples = '';
|
|
237
|
+
try {
|
|
238
|
+
const taskData = loadTasks();
|
|
239
|
+
if (taskData) {
|
|
240
|
+
const stats = getStats(taskData);
|
|
241
|
+
const features = taskData.features || [];
|
|
242
|
+
const maxId = features.length ? features[features.length - 1].id : 'feat-000';
|
|
243
|
+
const maxPriority = features.length ? Math.max(...features.map(f => f.priority || 0)) : 0;
|
|
244
|
+
const categories = [...new Set(features.map(f => f.category))].join(', ');
|
|
245
|
+
|
|
246
|
+
taskContext = `已有 ${stats.total} 个任务(${stats.done} done, ${stats.pending} pending, ${stats.failed} failed)。` +
|
|
247
|
+
`最大 id: ${maxId}, 最大 priority: ${maxPriority}。已有 category: ${categories}。`;
|
|
248
|
+
|
|
249
|
+
const recent = features.slice(-3);
|
|
250
|
+
if (recent.length) {
|
|
251
|
+
recentExamples = '已有任务格式参考(保持一致性):\n' +
|
|
252
|
+
recent.map(f => ` ${f.id}: "${f.description}" (category=${f.category}, steps=${f.steps.length}步, depends_on=[${f.depends_on.join(',')}])`).join('\n');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch { /* ignore */ }
|
|
256
|
+
|
|
217
257
|
return [
|
|
218
|
-
|
|
258
|
+
// --- Primacy zone: role + identity ---
|
|
259
|
+
'你是资深需求分析师,擅长将模糊需求分解为可执行的原子任务。',
|
|
260
|
+
'这是任务追加 session,不是编码 session。你只分解任务,不实现代码。',
|
|
219
261
|
'',
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
262
|
+
|
|
263
|
+
// --- Context layer ---
|
|
264
|
+
profileContext,
|
|
265
|
+
taskContext,
|
|
266
|
+
recentExamples,
|
|
267
|
+
`项目绝对路径: ${projectRoot}`,
|
|
224
268
|
'',
|
|
269
|
+
|
|
270
|
+
// --- CoT: explicit thinking steps ---
|
|
271
|
+
'执行步骤(按顺序,不可跳过):',
|
|
272
|
+
'1. 读取 .claude-coder/tasks.json 和 .claude-coder/project_profile.json,全面了解项目现状',
|
|
273
|
+
'2. 分析用户指令:识别核心功能点,判断是单任务还是需要拆分为多任务',
|
|
274
|
+
'3. 检查重复:对比已有任务,避免功能重叠',
|
|
275
|
+
'4. 确定依赖:新任务的 depends_on 引用已有或新增任务的 id,形成 DAG',
|
|
276
|
+
'5. 分解任务:每个任务对应一个独立可测试的功能单元,description 简明(40字内),steps 具体可操作',
|
|
277
|
+
'6. 追加到 tasks.json,id 和 priority 从已有最大值递增,status: pending',
|
|
278
|
+
'7. git add -A && git commit -m "chore: add new tasks"',
|
|
279
|
+
'8. 写入 session_result.json',
|
|
280
|
+
'',
|
|
281
|
+
|
|
282
|
+
// --- Quality constraints ---
|
|
225
283
|
taskGuide,
|
|
226
284
|
'',
|
|
227
|
-
'
|
|
228
|
-
'git add -A && git commit -m "chore: add new tasks"',
|
|
229
|
-
'写入 session_result.json',
|
|
285
|
+
'不修改已有任务,不实现代码。',
|
|
230
286
|
'',
|
|
287
|
+
|
|
288
|
+
// --- Recency zone: user instruction (highest attention) ---
|
|
231
289
|
`用户指令:${instruction}`,
|
|
232
|
-
].join('\n');
|
|
290
|
+
].filter(Boolean).join('\n');
|
|
233
291
|
}
|
|
234
292
|
|
|
235
293
|
module.exports = {
|
package/src/runner.js
CHANGED
|
@@ -301,6 +301,25 @@ async function run(requirement, opts = {}) {
|
|
|
301
301
|
lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
if (sessionResult.stalled) {
|
|
305
|
+
log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
|
|
306
|
+
consecutiveFailures++;
|
|
307
|
+
rollback(headBefore, '停顿超时');
|
|
308
|
+
if (consecutiveFailures >= MAX_RETRY) {
|
|
309
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
310
|
+
markTaskFailed();
|
|
311
|
+
consecutiveFailures = 0;
|
|
312
|
+
}
|
|
313
|
+
appendProgress({
|
|
314
|
+
session,
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
result: 'stalled',
|
|
317
|
+
cost: sessionResult.cost,
|
|
318
|
+
taskId,
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
304
323
|
// Validate
|
|
305
324
|
log('info', '开始 harness 校验 ...');
|
|
306
325
|
const validateResult = await validate(headBefore);
|
package/src/session.js
CHANGED
|
@@ -105,6 +105,16 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
105
105
|
|
|
106
106
|
const editCounts = {};
|
|
107
107
|
const EDIT_THRESHOLD = 5;
|
|
108
|
+
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
109
|
+
let stallDetected = false;
|
|
110
|
+
|
|
111
|
+
const stallChecker = setInterval(() => {
|
|
112
|
+
const idleMs = Date.now() - indicator.lastToolTime;
|
|
113
|
+
if (idleMs > stallTimeoutMs && !stallDetected) {
|
|
114
|
+
stallDetected = true;
|
|
115
|
+
log('warn', `无新工具调用超过 ${Math.floor(idleMs / 60000)} 分钟,自动中断 session`);
|
|
116
|
+
}
|
|
117
|
+
}, 30000);
|
|
108
118
|
|
|
109
119
|
try {
|
|
110
120
|
const queryOpts = buildQueryOptions(config, opts);
|
|
@@ -115,13 +125,20 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
115
125
|
hooks: [async (input) => {
|
|
116
126
|
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
117
127
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
129
|
+
const cmd = input.tool_input?.command || '';
|
|
130
|
+
const pattern = input.tool_input?.pattern || '';
|
|
131
|
+
const detail = target || cmd.slice(0, 200) || (pattern ? `pattern: ${pattern}` : '');
|
|
132
|
+
if (detail) {
|
|
133
|
+
logStream.write(`[${new Date().toISOString()}] ${input.tool_name}: ${detail}\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
|
|
137
|
+
editCounts[target] = (editCounts[target] || 0) + 1;
|
|
138
|
+
if (editCounts[target] > EDIT_THRESHOLD) {
|
|
122
139
|
return {
|
|
123
140
|
decision: 'block',
|
|
124
|
-
message: `已对 ${
|
|
141
|
+
message: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
|
|
125
142
|
};
|
|
126
143
|
}
|
|
127
144
|
}
|
|
@@ -135,21 +152,28 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
135
152
|
|
|
136
153
|
const collected = [];
|
|
137
154
|
for await (const message of session) {
|
|
155
|
+
if (stallDetected) {
|
|
156
|
+
log('warn', '停顿超时,中断消息循环');
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
138
159
|
collected.push(message);
|
|
139
160
|
logMessage(message, logStream, indicator);
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
clearInterval(stallChecker);
|
|
142
164
|
logStream.end();
|
|
143
165
|
indicator.stop();
|
|
144
166
|
|
|
145
167
|
const result = extractResult(collected);
|
|
146
168
|
return {
|
|
147
|
-
exitCode: 0,
|
|
169
|
+
exitCode: stallDetected ? 2 : 0,
|
|
148
170
|
cost: result?.total_cost_usd ?? null,
|
|
149
171
|
tokenUsage: result?.usage ?? null,
|
|
150
172
|
logFile,
|
|
173
|
+
stalled: stallDetected,
|
|
151
174
|
};
|
|
152
175
|
} catch (err) {
|
|
176
|
+
clearInterval(stallChecker);
|
|
153
177
|
logStream.end();
|
|
154
178
|
indicator.stop();
|
|
155
179
|
log('error', `Claude SDK 错误: ${err.message}`);
|