feat(scheduler): M6 任务级回执 + 断链点统一为终态事件

设计依据 docs/zh/input-scheduler-design.md §7(I5)、§11.3(X1/X2/X4)。

- 新增 emitSkippedReply:跳过路径(去重命中、空输入)给同步调用方一个
  skipped 终态,但**不发 agent_output 事件**(避免 WebUI 聊天记录凭空多出
  空消息)。修复前 cli/clawhubadapter 这类无超时的同步注入在去重命中时永久挂起
- 回执按任务归属:中断任务的回执只写自己的 ResponseCh,绝不误投给被挂起的等待者;
  被挂起任务恢复并结束后才拿到自己的回执
- 新增 task_terminal_test.go 3 项:X1 回执不误投、X2 空输入有 skipped 终态、
  X4 无超时同步调用方 1s 内拿到终态(回归判据)
- 更新 M3a 去重用例:由「不得回执」改为「必须有 skipped 终态」
- 验收:agent 全量 + -race;全仓 build/vet 通过
This commit is contained in:
JianFeeeee
2026-09-13 00:39:56 +08:00
parent a971fc8877
commit 4e4e0ad656
4 changed files with 180 additions and 2 deletions

View File

@ -102,8 +102,15 @@ func TestLifecycle_DuplicateSkippedHasTerminal(t *testing.T) {
if a.context.Len() != after1 {
t.Fatalf("去重命中不得提交上下文:%d → %d", after1, a.context.Len())
}
if len(ch2) != 0 {
t.Fatal("去重命中不得回执(原实现静默 return)")
if len(ch2) != 1 {
t.Fatal("去重命中必须回一个 skipped 终态,否则同步调用方永久挂起")
}
r := <-ch2
if skipped, _ := r.Payload["skipped"].(bool); !skipped {
t.Fatalf("去重回执必须带 skipped=true,实际 %+v", r.Payload)
}
if reason, _ := r.Payload["reason"].(string); reason != "duplicate" {
t.Fatalf("reason=%q,期望 duplicate", reason)
}
if outputs != outputs1 {
t.Fatalf("去重命中不得发输出事件:%d → %d", outputs1, outputs)