Commit 7ead6ef424bca07712dea88964f9ad9214c1f064
1 parent
32227f12
fix(protocol): 收紧 ClaudeCliProtocol 空闲超时至 8/15min,不再是 24h
24h 的"取消超时"曾是刻意为之,但代价是子进程已产出终态判词、仅 session/result 消息迟迟不来时,sendPrompt() 的 promise 没有任何兜底会挂到 24h 耗尽 (T-mt5esage:T-mt5d3mi9-57cf 卡 50+ 分钟才被 COO 心跳巡检发现,且已连续 3 次)。 收紧到与 process-guard.js FRESH_DEFAULTS.maxMs(10min) 同一量级:对话态 8min (无工具执行理应持续有 token 流),工具执行态 15min(跑测试/构建允许更长静默)。 补测试复现"tool_use 开始后无输出"场景,验证 15min 兜底生效而非 24h。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
2 changed files
with
37 additions
and
3 deletions
packages/acp-core/protocol/ClaudeCliProtocol.js
| ... | ... | @@ -241,9 +241,13 @@ class ClaudeCliProtocol extends EventEmitter { |
| 241 | 241 | return this._sendMockPrompt(text); |
| 242 | 242 | } |
| 243 | 243 | |
| 244 | - // 按照用户需求取消闲置超时限制 (设置为 24 小时) | |
| 245 | - const IDLE_TIMEOUT_CHAT_MS = 24 * 60 * 60_000; // 已取消正常对话无活动上限 | |
| 246 | - const IDLE_TIMEOUT_TOOL_MS = 24 * 60 * 60_000; // 已取消工具执行无活动上限 | |
| 244 | + // 24h 曾经是刻意取消的"无上限",但代价是子进程已经吐出终态判词、只是 | |
| 245 | + // session/result 消息迟迟没到时,没有任何机制能把这个 promise 兜回来—— | |
| 246 | + // 会一直挂到 24h 耗尽(T-mt5esage:卡了 50+ 分钟才被 COO 心跳巡检发现)。 | |
| 247 | + // 收紧到与 process-guard.js 的 wallclock 看门狗同一量级(FRESH_DEFAULTS.maxMs=10min), | |
| 248 | + // 工具执行允许略长(跑测试/构建等可能超过 10min),对话态没有类似借口应该更短。 | |
| 249 | + const IDLE_TIMEOUT_CHAT_MS = 8 * 60_000; // 8 min:无工具态,理应持续有 token 流 | |
| 250 | + const IDLE_TIMEOUT_TOOL_MS = 15 * 60_000; // 15 min:工具执行中允许更长静默 | |
| 247 | 251 | |
| 248 | 252 | return new Promise((resolve, reject) => { |
| 249 | 253 | if (!this._child || !this.isConnected) { | ... | ... |
packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js
| ... | ... | @@ -48,3 +48,33 @@ describe('ClaudeCliProtocol · mock mode', () => { |
| 48 | 48 | p.disconnect(); |
| 49 | 49 | }); |
| 50 | 50 | }); |
| 51 | + | |
| 52 | +describe('ClaudeCliProtocol · idle timeout (real-CLI path, non-mock)', () => { | |
| 53 | + // 复现 T-mt5esage: 子进程已经进入 tool_use(吐出终态判词前的最后一步), | |
| 54 | + // 但之后再也没有任何 stdout(session/result 迟迟不来)——旧代码 IDLE_TIMEOUT_TOOL_MS=24h | |
| 55 | + // 会让 sendPrompt() 的 promise 挂 24 小时才被兜住;现在应在 15min 兜住。 | |
| 56 | + it('rejects with a tool-mode timeout after 15min of silence following a tool_use start, not 24h', async () => { | |
| 57 | + vi.useFakeTimers(); | |
| 58 | + try { | |
| 59 | + const p = new ClaudeCliProtocol({ name: 'devloop-coder' }, {}); | |
| 60 | + // 伪造一个"已连接"的子进程:sendPrompt 只需要 _child.stdin.write 不抛错 | |
| 61 | + p.isConnected = true; | |
| 62 | + p._child = { stdin: { write: () => {} } }; | |
| 63 | + | |
| 64 | + const promise = p.sendPrompt('run the tests'); | |
| 65 | + const assertion = expect(promise).rejects.toThrow(/mode=tool/); | |
| 66 | + | |
| 67 | + // 模拟收到 tool_use content_block_start → 切到 tool 态长超时 | |
| 68 | + p._handleInnerStreamEvent({ type: 'content_block_start', content_block: { type: 'tool_use', id: 't1', name: 'Bash' } }); | |
| 69 | + | |
| 70 | + // 14min59s:还没到 15min 阈值,promise 不应 settle | |
| 71 | + await vi.advanceTimersByTimeAsync(14 * 60_000 + 59_000); | |
| 72 | + // 再推 2s 跨过 15min 边界,触发 fireTimeout | |
| 73 | + await vi.advanceTimersByTimeAsync(2_000); | |
| 74 | + | |
| 75 | + await assertion; | |
| 76 | + } finally { | |
| 77 | + vi.useRealTimers(); | |
| 78 | + } | |
| 79 | + }); | |
| 80 | +}); | ... | ... |