diff --git a/packages/acp-core/protocol/ClaudeCliProtocol.js b/packages/acp-core/protocol/ClaudeCliProtocol.js index 1dac057..7ee4f26 100644 --- a/packages/acp-core/protocol/ClaudeCliProtocol.js +++ b/packages/acp-core/protocol/ClaudeCliProtocol.js @@ -241,9 +241,13 @@ class ClaudeCliProtocol extends EventEmitter { return this._sendMockPrompt(text); } - // 按照用户需求取消闲置超时限制 (设置为 24 小时) - const IDLE_TIMEOUT_CHAT_MS = 24 * 60 * 60_000; // 已取消正常对话无活动上限 - const IDLE_TIMEOUT_TOOL_MS = 24 * 60 * 60_000; // 已取消工具执行无活动上限 + // 24h 曾经是刻意取消的"无上限",但代价是子进程已经吐出终态判词、只是 + // session/result 消息迟迟没到时,没有任何机制能把这个 promise 兜回来—— + // 会一直挂到 24h 耗尽(T-mt5esage:卡了 50+ 分钟才被 COO 心跳巡检发现)。 + // 收紧到与 process-guard.js 的 wallclock 看门狗同一量级(FRESH_DEFAULTS.maxMs=10min), + // 工具执行允许略长(跑测试/构建等可能超过 10min),对话态没有类似借口应该更短。 + const IDLE_TIMEOUT_CHAT_MS = 8 * 60_000; // 8 min:无工具态,理应持续有 token 流 + const IDLE_TIMEOUT_TOOL_MS = 15 * 60_000; // 15 min:工具执行中允许更长静默 return new Promise((resolve, reject) => { if (!this._child || !this.isConnected) { diff --git a/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js b/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js index 4104432..9d6d7e4 100644 --- a/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js +++ b/packages/acp-core/protocol/ClaudeCliProtocol.mock.test.js @@ -48,3 +48,33 @@ describe('ClaudeCliProtocol · mock mode', () => { p.disconnect(); }); }); + +describe('ClaudeCliProtocol · idle timeout (real-CLI path, non-mock)', () => { + // 复现 T-mt5esage: 子进程已经进入 tool_use(吐出终态判词前的最后一步), + // 但之后再也没有任何 stdout(session/result 迟迟不来)——旧代码 IDLE_TIMEOUT_TOOL_MS=24h + // 会让 sendPrompt() 的 promise 挂 24 小时才被兜住;现在应在 15min 兜住。 + it('rejects with a tool-mode timeout after 15min of silence following a tool_use start, not 24h', async () => { + vi.useFakeTimers(); + try { + const p = new ClaudeCliProtocol({ name: 'devloop-coder' }, {}); + // 伪造一个"已连接"的子进程:sendPrompt 只需要 _child.stdin.write 不抛错 + p.isConnected = true; + p._child = { stdin: { write: () => {} } }; + + const promise = p.sendPrompt('run the tests'); + const assertion = expect(promise).rejects.toThrow(/mode=tool/); + + // 模拟收到 tool_use content_block_start → 切到 tool 态长超时 + p._handleInnerStreamEvent({ type: 'content_block_start', content_block: { type: 'tool_use', id: 't1', name: 'Bash' } }); + + // 14min59s:还没到 15min 阈值,promise 不应 settle + await vi.advanceTimersByTimeAsync(14 * 60_000 + 59_000); + // 再推 2s 跨过 15min 边界,触发 fireTimeout + await vi.advanceTimersByTimeAsync(2_000); + + await assertion; + } finally { + vi.useRealTimers(); + } + }); +});