Commit 7ead6ef424bca07712dea88964f9ad9214c1f064

Authored by yibo
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>
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(&#39;ClaudeCliProtocol · mock mode&#39;, () =&gt; {
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 +});
... ...