fix: v0.5.1 工具调用链路复检修复 — SubAgent 孤儿工具拦截 + 确认弹框会话隔离
CI / 产物编译验证 (push) Successful in 10m9s
CI / 类型检查 + Lint + 单元测试 (push) Failing after 6m4s
CI / 全量测试 (Electron ABI) (push) Failing after 5m24s

背景:v0.5.0 发布后对工具调用链路(adapter 流式 tool_call → 引擎 PARSING →
preToolHooks 管道 → ToolRegistry → 结果回填)做全链路复检,发现并修复两处问题。

安全修复:
- SubAgent 中止时 pending 确认未清理(安全回归):SubEngine 以 taskId 为
  sessionId 写入 ConfirmationHook,abortSession 的 clearPending(sessionId)
  清不到它们。后果:中止会话后残留弹框若被补批,孤儿工具会真实执行副作用
  (v0.4.x 的全局清空反而能拦截)。修复:abortByParent 返回被中止的
  taskId 列表,abortSession 一并 clearPending(taskId)。

确认弹框会话隔离(前端对齐后端 v0.5.0 语义):
- ConfirmationRequest 新增 sessionId 字段(主会话为 sessionId,SubAgent 为
  taskId),弹框在会话 INIT/TERMINATED 时只清除该会话的请求 —— 修复并发
  会话下任意会话结束误清其他会话等待中确认的问题
- 选中计数按当前 requests 收敛(selectedIds 残留 id 无害化)

测试(207 → 215 用例):
- 新增引擎级工具调用链路集成测试 ×6(engine-toolchain.test.ts):真实
  PermissionCheckHook + RateLimitHook + ConfirmationHook 管道 + 真实
  ToolRegistry,覆盖 SAFE 工具直通 / HIGH 工具批准执行 / 拒绝以 Blocked
  错误回传 LLM / 会话隔离端到端 / 工具异常不中断循环 / 策略拦截系统路径
- 新增 FTS 触发器 × NULL content 删除安全性测试 ×2:验证 truncateAfter
  (编辑重发/重新生成)删除 content=NULL 的 assistant 消息(模型仅发
  tool_calls 的标准场景)不抛错且索引保持一致
- agent.test.ts 的 orchestrator mock 适配 abortByParent 新返回类型

验证: lint 0 problems / typecheck 双工程 0 errors / test:electron 215 全过 / build 成功
This commit is contained in:
2026-08-21 21:47:03 +08:00
parent 7e8b4882a0
commit c727ad075c
12 changed files with 488 additions and 19 deletions
+24 -6
View File
@@ -50,6 +50,12 @@ interface ConfirmationRequest {
args: Record<string, unknown>;
riskLevel: string;
reason: string;
/**
* 发起确认的会话 IDv0.5.1
* 主会话为 sessionIdSubAgent 委派的工具确认为 taskId。
* 用于会话 TERMINATED 时只清除该会话的请求(并发会话互不干扰)。
*/
sessionId?: string;
/** 过期时间戳(ms),由后端 ConfirmationHook 注入,用于倒计时 */
expiresAt?: number;
}
@@ -164,19 +170,29 @@ export function ConfirmationDialog(): React.JSX.Element | null {
return cleanup;
}, [refreshPending]);
// ===== 监听 Agent 状态变化:INIT(新 run 开始)或 TERMINATEDrun 结束/abort)时清空前端 state =====
// ===== 监听 Agent 状态变化:INIT(新 run 开始)或 TERMINATEDrun 结束/abort)时清空该会话的前端 state =====
// 解决:abort 场景下后端 clearPending() 清空了 Map,但前端 requests state 不会自动同步,
// 弹框会停留在已失效的请求上。用户操作后批量 IPC 返回 0 resolved,逻辑无害但 UX 差。
// 新会话 INIT 时也清空,防止上一会话的残留请求污染新会话 UI。
// v0.5.1: 按会话过滤 — 后端 pending 已按 sessionId 隔离(v0.5.0),
// 此前端点也只清除该会话的请求,并发会话等待中的确认不再被误清。
// selectedIds 中残留的已移除 id 无害(后端 resolveConfirmationsBatch 跳过不存在项,
// 展示计数已按 requests 收敛)。
useEffect(() => {
if (!window.metona?.agent?.onStateChange) return;
const unsubscribe = window.metona.agent.onStateChange((state: unknown) => {
const data = state as { state?: string; current?: string };
const data = state as { state?: string; current?: string; sessionId?: string };
const stateValue = data.state ?? data.current ?? '';
// INIT: 新 run 开始(新会话或新消息);TERMINATED: run 结束(正常完成/abort/超时/死循环)
if (stateValue === 'INIT' || stateValue === 'TERMINATED') {
setRequests([]);
setSelectedIds(new Set());
const sid = data.sessionId;
if (sid) {
setRequests((prev) => prev.filter((r) => r.sessionId !== sid));
} else {
// 无 sessionId 的兜底(理论上不出现):全清
setRequests([]);
setSelectedIds(new Set());
}
}
});
return unsubscribe;
@@ -306,9 +322,11 @@ export function ConfirmationDialog(): React.JSX.Element | null {
.filter((v): v is number => typeof v === 'number')
.sort((a, b) => a - b)[0];
const selectedCount = selectedIds.size;
// v0.5.1: 计数按当前 requests 收敛 — selectedIds 可能残留已被会话过滤移除的 id
// (后端 resolveConfirmationsBatch 对不存在项安全跳过,仅影响展示计数)
const selectedCount = requests.filter((r) => selectedIds.has(r.toolCallId)).length;
const totalCount = requests.length;
const allSelected = selectedCount === totalCount;
const allSelected = selectedCount === totalCount && totalCount > 0;
// 综合风险等级:取所有请求中最高的
const highestRisk = requests.reduce<string>((highest, r) => {
+6
View File
@@ -448,6 +448,12 @@ interface MetonaConfirmationRequest {
args: Record<string, unknown>;
riskLevel: string;
reason: string;
/**
* 发起确认的会话 ID(v0.5.1 新增)
* 主会话为 sessionIdSubAgent 委派的工具确认为 taskId。
* 确认弹框据此在会话 TERMINATED 时只清除该会话的请求。
*/
sessionId?: string;
/** v0.3.2: 过期时间戳(ms),由后端 ConfirmationHook 注入,用于倒计时 */
expiresAt?: number;
}