Skip to content

fix(acp): improve prompt timeout handling with cancel support and configurable duration#1709

Merged
piorpua merged 4 commits intomainfrom
fix/acp-prompt-timeout
Mar 25, 2026
Merged

fix(acp): improve prompt timeout handling with cancel support and configurable duration#1709
piorpua merged 4 commits intomainfrom
fix/acp-prompt-timeout

Conversation

@kaizhou-lab
Copy link
Copy Markdown
Collaborator

@kaizhou-lab kaizhou-lab commented Mar 25, 2026

Summary

  • Replace aggressive disconnect() on timeout with ACP session/cancel to keep connection alive
  • Add user-configurable prompt timeout setting (30s–3600s) in system preferences
  • Clean up AcpAgent lifecycle methods: cancelPrompt() for lightweight cancel, kill() for full teardown

Changes

Core (AcpConnection)

  • cancelPrompt(): sends session/cancel notification and clears pending prompt requests
  • handlePromptTimeout(): unified timeout handler that calls cancel instead of disconnect
  • setPromptTimeout(): configurable timeout with 30s minimum
  • Timeout resets on streaming updates, pauses during permission dialogs

AcpAgent

  • New cancelPrompt(): cancels prompt, rejects pending permissions, emits finish signal
  • Renamed stop()kill() to match actual behavior (full process teardown)
  • applyPromptTimeoutFromConfig(): re-reads config before each prompt for immediate effect
  • Removed dead kill() wrapper and stop.stream handler in worker

AcpAgentManager

  • stop() now calls agent.cancelPrompt() (was disconnect)
  • kill() calls agent.kill() (clear naming chain)

Settings UI

  • Added prompt timeout InputNumber in system settings
  • i18n keys added for all 6 locales

Related Issue

Closes #1708

Test Plan

  • 14 unit tests for timeout, cancel, and lifecycle methods
  • Set timeout to 30s, trigger timeout, verify session remains usable after cancel
  • Change timeout from 30s to 300s mid-session, verify new value takes effect on next prompt
  • Click stop button during prompt, verify cancel (not disconnect) is sent
  • Verify prompt timeout setting persists across app restart

zk added 2 commits March 25, 2026 17:55
…figurable duration

- Use ACP session/cancel instead of disconnect on timeout to keep connection alive
- Add cancelPrompt() to AcpAgent for lightweight prompt cancellation
- Rename AcpAgent.stop() to kill() to clarify semantics; remove dead code
- AcpAgentManager.stop() now cancels prompt; kill() destroys process
- Re-read timeout config before each prompt so changes take effect immediately
- Add prompt timeout setting in system preferences (30s-3600s)
- Add i18n keys for all 6 locales
- Add 14 unit tests for timeout and cancel logic
@piorpua
Copy link
Copy Markdown
Contributor

piorpua commented Mar 25, 2026

Code Review:fix(acp): improve prompt timeout handling with cancel support and configurable duration (#1709)

变更概述

本 PR 将超时后的 disconnect() 替换为 ACP session/cancel 通知,保持连接存活;新增 AcpAgent.cancelPrompt() / AcpAgent.kill() 生命周期方法;在系统设置 UI 中添加了用户可配置的 prompt 超时时长(30s–3600s),并同步了 6 个语言的 i18n 文本。

⚠️ CI 状态警告:以下 job 在 review 时未通过:Code QualityUnit Tests (ubuntu-latest)Unit Tests (macos-14)Unit Tests (windows-2022)Coverage TestI18n Check(均为 CANCELLED)。本报告结论仅供参考,建议修复 CI 后重新 review。


方案评估

结论⚠️ 方案有缺陷

整体方向正确——用 session/cancel 替换 disconnect() 是合理的 ACP 协议使用方式,保持连接存活可避免重新建立 session 的开销。但 AcpAgent.cancelPrompt()AcpAgent.sendMessage() 的 catch 路径之间存在一个设计盲点:手动取消时,catch 路径会错误地向用户展示 "Request cancelled" 错误消息,同时触发两次 finish 信号,导致明显的 UX 问题。


问题清单

🟠 HIGH — 手动取消时向用户展示 "Request cancelled" 错误 + 双 finish 信号

文件src/process/agent/acp/index.ts,第 517–533 行(cancelPrompt)& 第 691–703 行(sendMessage catch)

问题代码

// AcpAgent.cancelPrompt() — 同步路径
cancelPrompt(): void {
  this.connection.cancelPrompt(); // 同步 reject 所有 pending session/prompt
  // ...
  this.onSignalEvent({ type: 'finish', ... }); // ① 第一次 finish
}

// AcpAgent.sendMessage() catch — 异步微任务路径
} catch (error) {
  const errorMsg = error instanceof Error ? error.message : String(error);
  // "Request cancelled" 不匹配任何分类条件,落入 UNKNOWN
  this.emitErrorMessage(errorMsg); // ② 用户看到 "Request cancelled" 错误气泡
  this.onSignalEvent({ type: 'finish', ... }); // ③ 第二次 finish

问题说明cancelPrompt() 调用 connection.cancelPrompt(),后者同步执行 request.reject(new Error('Request cancelled'))(调度一个微任务),然后 cancelPrompt() 继续同步执行并 emit 第一次 finish。待同步代码结束后,微任务队列处理 rejection,导致 sendMessage() 的 catch 块触发,emitErrorMessage('Request cancelled') 将一个错误气泡写入聊天记录,并再次 emit finish。最终:用户主动点击停止,却看到一条错误消息 + 两次 finish 信号。

注:这一问题在测试中被掩盖,因为测试完全 mock 了 connection.cancelPromptvi.spyOn((agent as any).connection, 'cancelPrompt').mockImplementation(() => {}) ——没有真正执行 request.reject,所以 catch 路径从未被触发。

修复建议:在 AcpAgent.sendMessage 的 catch 块开头检测用户主动取消,提前返回,不展示错误:

} catch (error) {
  const errorMsg = error instanceof Error ? error.message : String(error);

  // User-initiated cancellation — finish already emitted by cancelPrompt()
  if (errorMsg === 'Request cancelled') {
    return { success: false, error: createAcpError(AcpErrorType.UNKNOWN, errorMsg, false) };
  }

  this.emitErrorMessage(errorMsg);
  // ...
}

🔵 LOW — 未使用的 import os (lint 已确认)

文件src/process/agent/acp/AcpConnection.ts,第 28 行

问题代码

import os from 'os';

问题说明:oxlint 明确报告 'os' is imported but never used。当前 AcpConnection.ts 中没有任何地方引用 os

修复建议:直接删除该行。


🔵 LOW — 新增测试文件大量使用 no-explicit-any(lint 已确认,共 14 处)

文件tests/unit/acpTimeout.test.ts

问题代码(示例):

(conn as any).sessionId = 'test-session';
(conn as any).child = { stdin: { write: vi.fn() }, ... };
const pendingRequests = (conn as any).pendingRequests as Map<number, any>;

问题说明:oxlint 报告 14 处 no-explicit-any。使用 as any 访问私有成员会绕过类型系统,建议改用更类型安全的 vi.spyOn// @ts-ignore + 精确注释说明原因。

修复建议(示例)

// 访问私有字段时,用 vi.spyOn 代替 as any
const writeFn = vi.fn();
vi.spyOn(conn as AcpConnection & { child: { stdin: { write: () => void } } }, 'child', 'get')
  .mockReturnValue({ stdin: { write: writeFn }, killed: false, pid: 12345 });

// 访问私有 Map 时,可用类型断言 + unknown 做两步转换
const pendingRequests = (conn as unknown as { pendingRequests: Map<number, PendingRequest<unknown>> }).pendingRequests;

汇总

# 严重级别 文件 问题
1 🟠 HIGH src/process/agent/acp/index.ts:691 手动取消时展示 "Request cancelled" 错误 + double finish
2 🔵 LOW src/process/agent/acp/AcpConnection.ts:28 未使用的 import os from 'os'
3 🔵 LOW tests/unit/acpTimeout.test.ts 14 处 no-explicit-any lint 违规

结论

⚠️ 有条件批准 — 存在一个 HIGH 问题需要处理后合并。

"Request cancelled" 错误消息会在用户主动点击停止时显示为对话中的错误气泡,属于明确的 UX 回归;两次 finish 信号也可能在下游造成状态问题。该问题修复简单,建议处理后合并。两处 LOW 问题不阻塞合并。


本报告由本地 pr-review skill 生成,包含完整项目上下文,无截断限制。

Resolve pending prompt with null instead of rejecting, so the
sendMessage catch block is not triggered and no error message is
shown to the user when they manually stop generation.
@kaizhou-lab
Copy link
Copy Markdown
Collaborator Author

已修复 (58620b5)。

cancelPrompt() 中改为 request.resolve(null) 而不是 reject,这样 sendMessage 的 catch 不会被触发,用户手动取消时不再显示错误消息,也避免了重复的 finish 信号。

JerryLiu369 added a commit to JerryLiu369/AionUi that referenced this pull request Mar 25, 2026
Remove finish signal emission (covered by PR iOfficeAI#1709) and keep only the
cronBusyGuard/status cleanup on sendMessage failure, which iOfficeAI#1709 does
not address.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@piorpua piorpua merged commit 3c6f894 into main Mar 25, 2026
15 of 17 checks passed
@piorpua piorpua deleted the fix/acp-prompt-timeout branch March 25, 2026 11:16
JerryLiu369 added a commit to JerryLiu369/AionUi that referenced this pull request Mar 25, 2026
Remove finish signal emission (covered by PR iOfficeAI#1709) and keep only the
cronBusyGuard/status cleanup on sendMessage failure, which iOfficeAI#1709 does
not address.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
JerryLiu369 added a commit to JerryLiu369/AionUi that referenced this pull request Mar 25, 2026
Remove finish signal emission (covered by PR iOfficeAI#1709) and keep only the
cronBusyGuard/status cleanup on sendMessage failure, which iOfficeAI#1709 does
not address.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
JerryLiu369 added a commit to JerryLiu369/AionUi that referenced this pull request Mar 25, 2026
Remove finish signal emission (covered by PR iOfficeAI#1709) and keep only the
cronBusyGuard/status cleanup on sendMessage failure, which iOfficeAI#1709 does
not address.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(acp): prompt timeout causes session to become unusable

2 participants