Files

258 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [agent-通讯, jira, claude-code, websocket, ACP, bug修复]
create time: 2026-06-09 22:30
---
# Agent 通讯修复 Jira Task
## 概述
ACP Agent / Bridge / Remote Control Server / REPL Hook 生命周期的系统性修复方案。包含 1 个 Epic 和 10 个 Tickets(P0 x3, P1 x5, QA x2),覆盖 WebSocket 入站边界、abort listener 生命周期泄漏、prompt 队列优化、类型收敛等关键问题。本文档是唯一执行任务文档,每个 JIRA-* 小节可直接拆成 Jira issue。
## 正文
### 方案性质
本文档是目标状态式执行方案,不是临时补丁清单。每张 ticket 必须交付明确的代码终态、测试覆盖和回归边界;不得只用局部 workaround 掩盖问题。
### 执行总则
1. 先边界安全,后内部优化:先修 WS 入站大小与输入校验,避免线上风险扩大。
2. 单文件可回滚:每个文件内修改保持内聚,便于回滚与 bisect。
3. 不改协议语义,只修实现缺陷:除 `resource_link` 表达形式统一外,不改变主流程契约。
4. 每个文件必须有验收输出:要么测试用例,要么日志/指标验证。
5. 发布前必须确认协议层行为无回归:`stopReason` 决策与 `sessionUpdate` 发送顺序保持稳定。
### Epic
#### JIRA-EPIC-001:提升 Agent 通讯链路稳定性与边界安全
- Issue Type:Epic
- Priority:P0
- Owner:核心通讯 / 后端网关 / QA
- Scope:ACP Agent、ACP Bridge、Remote Control Server、REPL 初始化生命周期
- Goal:修复长会话资源泄漏、补齐 WebSocket 入站边界、统一 prompt 转换、收敛类型风险,并补充关键回归测试。
**Epic 验收标准**
- `bun run typecheck` 0 error。
- P0 WebSocket 超大消息拒绝逻辑已实现并覆盖测试。
- ACP bridge abort listener 生命周期无累积。
- prompt 转换实现单源化。
- settings/defaultMode 能真实影响 ACP permission mode,且 `_meta.permissionMode` 保持最高优先级。
- REPL 目标 hook suppress 清理完成,timer cleanup 完整。
---
### P0 Tickets
#### JIRA-001:为 session ingress WebSocket 补齐消息大小限制
- Issue Type:Bug | Priority:P0 | Story Points:3 | Owner:后端/网关
- Files:`packages/remote-control-server/src/routes/v1/session-ingress.ts`
- 后续票:JIRA-008(同文件 P1 类型与 decode path 收尾)
> [!info]
> 参考代码位置:`packages/remote-control-server/src/routes/v1/session-ingress.ts:100-106`
**背景**: `session-ingress` 当前缺少 WebSocket message size limit。ACP 路由已有类似限制,两个入口边界不一致,可能导致大包占用内存或绕过入口保护。
**实施要求**
- 新增 `MAX_WS_MESSAGE_SIZE = 10 * 1024 * 1024`,与 ACP 路由的 10MB 上限保持一致。
- 在 `onMessage` decode 后优先检查 payload size。
- 超限时执行 `ws.close(1009, "message too large")`。
- 日志记录 `sessionId`、payload size、limit。
- 对 `string`、`ArrayBuffer`、`Uint8Array` 进行统一 decode 分流。
- 非支持类型直接拒绝并记录,不进入业务 handler。
**验收标准**: 11MB payload 被 1009 close。1KB 合法 payload 仍正常进入 handler。非支持类型 payload 不进入 handler。不改变 URL、auth、session 解析逻辑。
**回归范围**: Remote Control Server session ingress WebSocket。正常会话消息转发。WebSocket close code 行为。
**风险等级**: 中。入口逻辑变更可能影响特殊客户端 payload 类型。
---
#### JIRA-002:修复 ACP bridge abort listener 生命周期泄漏
- Issue Type:Bug | Priority:P0 | Story Points:3 | Owner:核心通讯
- Files:`src/services/acp/bridge.ts`
> [!info]
> 参考代码位置:`src/services/acp/bridge.ts:576-585`
**背景**: ACP bridge 的 `Promise.race` abort 分支注册 listener 后缺少完整 cleanup。长会话或高频 next 场景可能出现 listener 累积。
**实施要求**
- 将 abort race 改为可清理监听器写法。
- 注册 listener 后保留 handler 引用。
- `sdkMessages.next()` 先返回时必须 `removeEventListener`。
- abort、throw、return 等路径都在 `finally` 中清理。
- 不改变 `stopReason` 决策逻辑。
- 不改变 `sessionUpdate` 发送顺序。
**验收标准**: 模拟 10k 次 next 且不 abort,listener 不增长。abort 场景仍返回 `cancelled`。原有 streaming/session update 行为无回归。
**回归范围**: ACP bridge streaming loop。用户取消请求。SDK generator 异常路径。
**风险等级**: 中。异步控制流变更需要覆盖取消与异常路径。
---
### P1 Tickets
#### JIRA-003:优化 ACP agent pending prompt 队列为 O(1) 出队
- Issue Type:Task | Priority:P1 | Story Points:5 | Owner:核心通讯
- Files:`src/services/acp/agent.ts`
> [!info]
> 参考代码位置:`src/services/acp/agent.ts:332-339`
**背景**: 当前 pending prompt 队列使用 `Map + sort` 获取下一项,排队量上升时会带来不必要的排序成本。
**实施要求**: 改为 `queue: string[]` + `pendingMap: Map<string, PendingPrompt>` 组合。入队执行 `queue.push(id)` 与 `pendingMap.set(id, prompt)`。出队从队首惰性跳过已取消项。取消只从 `pendingMap` 删除,不做数组中间删除。保持现有取消语义和出队顺序。
**验收标准**: 1000 pending prompt 场景下出队顺序正确。已取消 prompt 不会被 resolve。出队不再依赖全量 sort。1000 排队场景下出队耗时低于旧实现。
---
#### JIRA-004:接入真实 settings 读取并校验 ACP permission mode
- Issue Type:Bug | Priority:P1 | Story Points:3 | Owner:核心通讯
- Files:`src/services/acp/agent.ts`
> [!info]
> 参考代码位置:`src/services/acp/agent.ts:465-467`
**背景**: `getSetting()` 当前未真正接入项目配置,导致默认 permission mode 配置无法按预期生效。
**实施要求**: 接入项目现有 settings/config 读取逻辑。仅接受合法 permission mode 枚举值。非法值 fallback 到 `default`。`_meta.permissionMode` 继续保持最高优先级。不改变外部协议字段。
**验收标准**: settings/defaultMode 能影响默认 permission mode。`_meta.permissionMode` 能覆盖 settings。非法 settings 值不会传播到运行时。
---
#### JIRA-005:单源化 ACP prompt 转换逻辑
- Issue Type:Refactor | Priority:P1 | Story Points:5 | Owner:核心通讯
- Files:`src/services/acp/agent.ts`, `src/services/acp/bridge.ts`, `src/services/acp/promptConversion.ts`(新增)
> [!info]
> 参考代码位置:`src/services/acp/agent.ts:754-758`, `src/services/acp/agent.ts:764-785`, `src/services/acp/bridge.ts:522-537`
**背景**: ACP agent 与 bridge 存在重复 prompt 转换逻辑,`resource_link` 等 block 的输出策略容易分叉。
**实施要求**: 新增共享转换模块 `src/services/acp/promptConversion.ts`。`agent.ts` 与 `bridge.ts` 改为调用共享转换函数。删除 `bridge.ts` 中 `promptToQueryContent` 的真实实现。`resource_link` 输出改为稳定纯文本元信息,禁止 markdown link。保持其他 block 转换语义不变。
**验收标准**: 全仓库仅保留一个真实 prompt 转换实现。相同 input block 在 agent/bridge 输出一致。`resource_link` 不再输出 `[name](uri)` 形式。
---
#### JIRA-006:治理 REPL onInit effect 依赖并补齐 timer cleanup
- Issue Type:Task | Priority:P1 | Story Points:3 | Owner:终端 UI
- Files:`src/screens/REPL.tsx`
> [!info]
> 参考代码位置:`src/screens/REPL.tsx:654-662`, `src/screens/REPL.tsx:4996-5005`
**背景**: REPL 中目标初始化 effect 存在 hook dependency suppress,warm-up timer 也需要显式 cleanup,避免频繁挂载/卸载时留下悬挂任务。
**实施要求**: 整理 `onInit` 生命周期,使用稳定引用或 effect 内联。移除目标段 `exhaustive-deps` suppress。warm-up effect 中记录 timeout id,cleanup 中执行 `clearTimeout(timeoutId)`。保留 `alive` 判定作为并发保护。
---
#### JIRA-007:收敛 ACP route WebSocket 事件 any 类型
- Issue Type:Task | Priority:P1 | Story Points:2 | Owner:后端/网关
- Files:`packages/remote-control-server/src/routes/acp/index.ts`
> [!info]
> 参考代码位置:`packages/remote-control-server/src/routes/acp/index.ts:108-146`
**背景**: ACP route 中 WebSocket 事件和 socket 参数存在 `any`,降低编译期保护。
**实施要求**: 定义最小 WebSocket 事件类型:open/message/close/error。将 `_evt: any`、`evt: any`、`ws: any` 替换为窄类型。不改变 payload decode 与大小检查策略。
---
#### JIRA-008:收敛 session ingress WebSocket 事件类型与 decode path
- Issue Type:Task | Priority:P1 | Story Points:3 | Owner:后端/网关
- Files:`packages/remote-control-server/src/routes/v1/session-ingress.ts`
- 前置依赖:JIRA-001 已合并
> [!info]
> 参考代码位置:`packages/remote-control-server/src/routes/v1/session-ingress.ts:100-106`
**背景**: 在完成 P0 size guard 后,session ingress 仍需要进一步收敛事件类型与 decode path,减少隐式类型风险。
**实施要求**: 定义或复用最小 WebSocket message event 类型。将 message decode 分支集中到一个小函数。保持 P0 size guard 与 close code 语义。
---
### QA Tickets
#### JIRA-009:补充 ACP 通讯回归测试
- Issue Type:Test | Priority:P1 | Story Points:5 | Owner:QA/核心通讯
- Files:`src/services/acp/agent.ts`, `src/services/acp/bridge.ts`, `src/services/acp/promptConversion.ts` 及对应 `__tests__/` 文件
**覆盖场景**: 长会话 10k turn 无 abort listener 累积。prompt queue 1000 并发排队取消/出队顺序正确。settings/defaultMode 与 `_meta.permissionMode` 优先级正确。`resource_link` 转换在 agent 与 bridge 输出一致。
---
#### JIRA-010:补充 Remote Control Server WebSocket 入站回归测试
- Issue Type:Test | Priority:P1 | Story Points:3 | Owner:QA/后端
- Files:`packages/remote-control-server/src/__tests__/routes.test.ts`, `packages/remote-control-server/src/routes/v1/session-ingress.ts`
**覆盖场景**: 11MB session ingress payload 被 1009 close。合法小 payload 正常进入 handler。非支持 payload 类型被拒绝。日志包含 sessionId、payload size、limit。
---
### 推荐执行顺序
执行节奏:先完成 P0 全部改动和冒烟验证,再启动 P1 改造;测试票可穿插执行,但不得绕过 P0 gate。
```mermaid
flowchart LR
subgraph P0["P0 阶段"]
J1["JIRA-001 封入口大包风险"]
J2["JIRA-002 修 listener 生命周期"]
J10["JIRA-010 补 RCS 入站测试"]
end
subgraph P1["P1 阶段"]
J3["JIRA-003 优化 prompt queue"]
J4["JIRA-004 接入 settings"]
J5["JIRA-005 单源化 prompt 转换"]
J9["JIRA-009 补 ACP 回归测试"]
J6["JIRA-006 治理 REPL effect"]
J7["JIRA-007 收敛 ACP route 类型"]
J8["JIRA-008 收敛 ingress 类型"]
end
P0 --> P1
J1 --> J10
J2 --> J10
```
### Release Checklist
- [ ] `bun run typecheck` 0 error
- [ ] P0 tickets 已合并并测试通过
- [ ] ACP 回归测试通过
- [ ] RCS WebSocket 入站测试通过
- [ ] prompt conversion 单源化已通过代码搜索确认
- [ ] permission mode 优先级测试通过
- [ ] 协议层行为无回归(stopReason 决策、sessionUpdate 发送顺序)
- [ ] REPL hook/timer 改动通过 lint/typecheck
- [ ] 最终变更说明包含风险与未覆盖项
## 关联笔记
- [[agent-comm-fix-questions]] - Agent 通讯修复问题文档
- [[three-tier-gating]] - 三层门禁系统