Skip to content

[DIAG] teammate 回报与后台通知共用同一队列,同批 ≥2 条时部分投递会整批丢弃 #162

Description

@WooDragon

结论先行

SendMessage(to:"main") 的 teammate 回报,与后台 subagent 完成通知共用同一个内存队列单例,且共用同一段丢弃逻辑。此前认为"teammate 走 mailbox、与完成通知是两套机制"的判断,只在入口端成立,在丢弃点上是错的

丢失的确切条件是同一 fold 批次内 ≥2 条且发生部分投递。单条批次结构上不可能丢。

机制(CC 2.1.220 二进制实证)

两个入口,同一队列:

xT = F_.enqueue                     // SendMessage(to:"main") → mode:"prompt", priority:"next"
dp = F_.enqueuePendingNotification  // 后台任务完成通知 → mode:"task-notification"

F_ = w7g() 是单例。to:"main" 分支原文:

case"main":{
  ...
  return xT({mode:"prompt",agentId:Si(),value:f,priority:"next",origin:g,skipSlashCommands:!0,isMeta:!0}),
         {data:{success:!0,message:"Message queued for the main conversation's next turn."}}
}

fold 批次的取用与丢弃(query 主循环内):

let dn = V.messageQueue.isMidTurnFoldSuspended()||un!==void 0 ? [] :
         ORu(V.messageQueue.getCommandsByMaxPriority("next"), {isMainThread:In, currentAgentId:ni});
V.messageQueue.registerFoldInFlight(dn);
try{
  let rt=0;
  for await(let Mr of Kpt(null,er,null,dn,f,[...Oe,...xe,...Qe],a))
    if(yield Mr, Qe.push(Mr), Mr.attachment.type==="queued_command" && Mr.attachment.commandMode!==void 0) rt++;
  let Jr = dn.filter((Mr)=>Mr.mode==="prompt"||Mr.mode==="task-notification");
  if(Jr.length>0 && rt===0)
    w(`[query] queued_command attachments degraded to 0 for ${Jr.length} consumed command(s) — leaving them queued instead of dropping them`,{level:"warn"});
  else{
    if(Jr.length>0 && rt<Jr.length)
      w(`[query] partial queued_command emission: ${rt}/${Jr.length} — removing commands that were not all delivered`,{level:"error"});
    if(gGu(Jr), Jr.length>0)
      if(er.abortController.signal.aborted)
        w(`[query] abort during mid-turn absorption — leaving ${Jr.length} command(s) queued`);
      else{ V.messageQueue.remove(Jr); ... }
  }
}finally{ V.messageQueue.unregisterFoldInFlight(dn) }

关键点逐条:

  1. Jr 同时筛 mode==="prompt"(teammate 回报)与 mode==="task-notification"(后台通知) —— 两类消息落在同一个丢弃批次里。
  2. rt 是本轮真正产出的 queued_command attachment 计数rt < Jr.length 时走 remove(Jr)——移除整批 Jr,不是仅已投递的子集。那 Jr.length - rt 条从未进入模型上下文却被一并移除,无补发。
  3. 该分支被官方自己标为 level:"error",说明是已知缺陷而非设计意图。
  4. rt === 0 时保留不丢leaving them queued instead of dropping themlevel:"warn");全部投递则正常。故只有"部分成功"这一档会丢,表现为间歇性、并发多时更易撞上。
  5. Jr.length === 1 时结构上不可能"部分"——rt 只能是 0(保留)或 1(成功)。单条批次天然安全。

rt 短于 Jr.length 的成因(在 attachment 计算侧):

async function Top(e,t,r,n,o,i,s){
  ...
  let l=Lc(), c=setTimeout((v)=>v.abort(),1000,l);   // 1000ms 硬超时,整个 attachment 计算被裁
  ...
  let g=[Ky("queued_commands",()=>gfn(n,a)), ...];    // Ky 包装器 catch 异常时返回 []

gfn 本身是 Promise.all 一对一映射(每条 queued command 生成一个 attachment),正常路径 rt === Jr.length。会短的是上面两条:1000ms 超时、以及 Ky 包装器吞异常返回空。回报越长越多,越可能触及超时。

补充:ORu 是批次过滤器,主线程上判据为 QH(o)QH(e){return e.agentId===Si()}):

function ORu(e,{isMainThread:t,currentAgentId:r}){
  return e.filter((o)=>{
    if(v7g(o))return!1;
    if(t)return QH(o);
    return o.mode==="task-notification"&&o.agentId===r
  })
}

to:"main" 入队时 agentId:Si(),故主线程上 QH 恒真——teammate 回报必然进入 fold 批次。

另需区分:SendMessage(to:"具名 teammate") 走的是文件 inbox(工具返回文案为 Message sent to <name>'s inbox),与 to:"main" 的内存队列(返回文案 Message queued for the main conversation's next turn)是不同 producer 路径。两条文案的差异本身即可作为路径判别依据。

真实 transcript 交叉验证

一个真实 team session(本仓工作区内,4 个 teammate:step1-script / step2-tests / step5-docs / pr-reviewer):

  • 全 session queue-operationenqueue=6, remove=6,逐一按内容比对6/6 严格匹配,零丢失
  • pr-reviewer 三次回报分别在三个不同时点 enqueue(各自单条成批),三次均完整送达。
  • step1-scriptstep5-docs 那次是合批送达(同一条 attachment 内含两个 teammate 的消息 + 各自的 idle_notification),该次侥幸全投递成功——但这正是有风险的形态。
  • step2-tests 从未产生任何 to:"main" 投递:主会话用 sleep 轮询判其卡死(8 分钟零改动),SendMessage 探测后再等 3 分钟无回应,最终 TaskStop 杀掉并自行接管。属"从未产出消息"而非"消息被丢",正反两面都不构成丢弃证据。

即:该 session 未复现丢失,但形态上已出现过合批(风险形态)。

事后可判性

丢失可事后判定:比对 transcript 里 queue-operationenqueue/remove 是否配对且内容一致,并交叉核对主会话后续消息是否引用/采纳了该内容。enqueue 有对应 remove 但内容从未出现在主上下文者即为丢失。

规避手段(暂不落地,仅记录)

判据不是"并发 teammate 数",而是同一 fold 批次内的条数 ≥2——同一个 teammate 连发两条同样会凑成批。故规避需两条同时成立:

  1. 每个 teammate 只发一条:把全部结论压进单条 SendMessage(to:"main"),发出后不再补发正文。
  2. Lead 侧错开回报时点:一次只让一个 teammate 处于"即将回报"状态,需要并行时错开回报时点,别让多个 teammate 在同一轮里同时回话。

本 issue 只记录机制,暂不改 team-ops skill(skill 落地另议)。

与既有工作的边界

本仓另有一道 PreToolUse 门禁在治「后台派发的完成通知丢失」(拦省略 run_in_background:false 的派发)。该门禁治不了本 issue 的问题,两点原因:

  1. 它的判定链刻意豁免 tool_input.name 非空——那是 teammate 的唯一入口,不豁免会拦死 Lead 起 teammate。故对 teammate 派发是 no-op。
  2. teammate 没有同步选项Agent.call() 的 teammate 分支根本不读 run_in_background,注册后立即返回。不存在"改同步派发"这条出路。

这是能力边界,不是门禁写漏。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions