结论先行
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) }
关键点逐条:
Jr 同时筛 mode==="prompt"(teammate 回报)与 mode==="task-notification"(后台通知) —— 两类消息落在同一个丢弃批次里。
rt 是本轮真正产出的 queued_command attachment 计数。rt < Jr.length 时走 remove(Jr)——移除整批 Jr,不是仅已投递的子集。那 Jr.length - rt 条从未进入模型上下文却被一并移除,无补发。
- 该分支被官方自己标为
level:"error",说明是已知缺陷而非设计意图。
rt === 0 时保留不丢(leaving them queued instead of dropping them,level:"warn");全部投递则正常。故只有"部分成功"这一档会丢,表现为间歇性、并发多时更易撞上。
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-operation 的 enqueue=6, remove=6,逐一按内容比对6/6 严格匹配,零丢失。
pr-reviewer 三次回报分别在三个不同时点 enqueue(各自单条成批),三次均完整送达。
step1-script 与 step5-docs 那次是合批送达(同一条 attachment 内含两个 teammate 的消息 + 各自的 idle_notification),该次侥幸全投递成功——但这正是有风险的形态。
step2-tests 从未产生任何 to:"main" 投递:主会话用 sleep 轮询判其卡死(8 分钟零改动),SendMessage 探测后再等 3 分钟无回应,最终 TaskStop 杀掉并自行接管。属"从未产出消息"而非"消息被丢",正反两面都不构成丢弃证据。
即:该 session 未复现丢失,但形态上已出现过合批(风险形态)。
事后可判性
丢失可事后判定:比对 transcript 里 queue-operation 的 enqueue/remove 是否配对且内容一致,并交叉核对主会话后续消息是否引用/采纳了该内容。enqueue 有对应 remove 但内容从未出现在主上下文者即为丢失。
规避手段(暂不落地,仅记录)
判据不是"并发 teammate 数",而是同一 fold 批次内的条数 ≥2——同一个 teammate 连发两条同样会凑成批。故规避需两条同时成立:
- 每个 teammate 只发一条:把全部结论压进单条
SendMessage(to:"main"),发出后不再补发正文。
- Lead 侧错开回报时点:一次只让一个 teammate 处于"即将回报"状态,需要并行时错开回报时点,别让多个 teammate 在同一轮里同时回话。
本 issue 只记录机制,暂不改 team-ops skill(skill 落地另议)。
与既有工作的边界
本仓另有一道 PreToolUse 门禁在治「后台派发的完成通知丢失」(拦省略 run_in_background:false 的派发)。该门禁治不了本 issue 的问题,两点原因:
- 它的判定链刻意豁免
tool_input.name 非空——那是 teammate 的唯一入口,不豁免会拦死 Lead 起 teammate。故对 teammate 派发是 no-op。
- teammate 没有同步选项:
Agent.call() 的 teammate 分支根本不读 run_in_background,注册后立即返回。不存在"改同步派发"这条出路。
这是能力边界,不是门禁写漏。
结论先行
SendMessage(to:"main")的 teammate 回报,与后台 subagent 完成通知共用同一个内存队列单例,且共用同一段丢弃逻辑。此前认为"teammate 走 mailbox、与完成通知是两套机制"的判断,只在入口端成立,在丢弃点上是错的。丢失的确切条件是同一 fold 批次内 ≥2 条且发生部分投递。单条批次结构上不可能丢。
机制(CC 2.1.220 二进制实证)
两个入口,同一队列:
F_ = w7g()是单例。to:"main"分支原文:fold 批次的取用与丢弃(
query主循环内):关键点逐条:
Jr同时筛mode==="prompt"(teammate 回报)与mode==="task-notification"(后台通知) —— 两类消息落在同一个丢弃批次里。rt是本轮真正产出的queued_commandattachment 计数。rt < Jr.length时走remove(Jr)——移除整批Jr,不是仅已投递的子集。那Jr.length - rt条从未进入模型上下文却被一并移除,无补发。level:"error",说明是已知缺陷而非设计意图。rt === 0时保留不丢(leaving them queued instead of dropping them,level:"warn");全部投递则正常。故只有"部分成功"这一档会丢,表现为间歇性、并发多时更易撞上。Jr.length === 1时结构上不可能"部分"——rt只能是 0(保留)或 1(成功)。单条批次天然安全。rt短于Jr.length的成因(在 attachment 计算侧):gfn本身是Promise.all一对一映射(每条 queued command 生成一个 attachment),正常路径rt === Jr.length。会短的是上面两条:1000ms 超时、以及Ky包装器吞异常返回空。回报越长越多,越可能触及超时。补充:
ORu是批次过滤器,主线程上判据为QH(o)(QH(e){return e.agentId===Si()}):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):
queue-operation的enqueue=6, remove=6,逐一按内容比对6/6 严格匹配,零丢失。pr-reviewer三次回报分别在三个不同时点 enqueue(各自单条成批),三次均完整送达。step1-script与step5-docs那次是合批送达(同一条 attachment 内含两个 teammate 的消息 + 各自的 idle_notification),该次侥幸全投递成功——但这正是有风险的形态。step2-tests从未产生任何to:"main"投递:主会话用 sleep 轮询判其卡死(8 分钟零改动),SendMessage探测后再等 3 分钟无回应,最终TaskStop杀掉并自行接管。属"从未产出消息"而非"消息被丢",正反两面都不构成丢弃证据。即:该 session 未复现丢失,但形态上已出现过合批(风险形态)。
事后可判性
丢失可事后判定:比对 transcript 里
queue-operation的enqueue/remove是否配对且内容一致,并交叉核对主会话后续消息是否引用/采纳了该内容。enqueue有对应remove但内容从未出现在主上下文者即为丢失。规避手段(暂不落地,仅记录)
判据不是"并发 teammate 数",而是同一 fold 批次内的条数 ≥2——同一个 teammate 连发两条同样会凑成批。故规避需两条同时成立:
SendMessage(to:"main"),发出后不再补发正文。本 issue 只记录机制,暂不改 team-ops skill(skill 落地另议)。
与既有工作的边界
本仓另有一道 PreToolUse 门禁在治「后台派发的完成通知丢失」(拦省略
run_in_background:false的派发)。该门禁治不了本 issue 的问题,两点原因:tool_input.name非空——那是 teammate 的唯一入口,不豁免会拦死 Lead 起 teammate。故对 teammate 派发是 no-op。Agent.call()的 teammate 分支根本不读run_in_background,注册后立即返回。不存在"改同步派发"这条出路。这是能力边界,不是门禁写漏。