You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A multi-member wave can emit its final digest twice, or emit a premature partial "wave finished", when one member's SubagentInfo.done = True is set before that member's terminal report reaches the gateway completion consumer (_subagent_done).
Mechanism (pre-existing on main)
batch_members_pending() treats a member as no longer outstanding the moment info.done flips, but the member's done-count contribution to the consumer's bp["done"] only lands when its (shielded, possibly slow) terminal report task actually invokes the consumer. In that gap, ANY sibling completion that reaches the consumer sees done < total AND batch_members_pending() == False, so the last-member fallback finalizes early; the in-flight report then re-creates the batch-progress record and "finalizes" the same batch again.
This is reachable with two RUNNING members alone (member A done-but-unreported while member B's report executes the consumer) — it predates the Stop-all work in #8270 and is merely easier to hit now that stopped/unqueued members also flow through the consumer. The synthetic unqueue announces added for #8270 already bridge their own version of this window with _batch_unqueued_pending + a per-batch bounded lock; NORMAL terminal reports have no equivalent "done-but-unreported" accounting.
Direction
Extend the same protocol to normal terminal reports: count a member as outstanding from done=True until its report enters the consumer (e.g. a per-batch done-but-unreported count decremented at the top of the report path), or make the consumer's finalize decision consult report-task liveness. Needs care around the report shield, _force_reap, and cancel-recovery paths — which is why it is split out rather than folded into #8270's fix.
References
Consumer fallback: src/kiro_crew/slack/gateway.py (_last = ... not batch_members_pending(batch_id))
Symptom
A multi-member wave can emit its final digest twice, or emit a premature partial "wave finished", when one member's
SubagentInfo.done = Trueis set before that member's terminal report reaches the gateway completion consumer (_subagent_done).Mechanism (pre-existing on main)
batch_members_pending()treats a member as no longer outstanding the momentinfo.doneflips, but the member'sdone-count contribution to the consumer'sbp["done"]only lands when its (shielded, possibly slow) terminal report task actually invokes the consumer. In that gap, ANY sibling completion that reaches the consumer seesdone < totalANDbatch_members_pending() == False, so the last-member fallback finalizes early; the in-flight report then re-creates the batch-progress record and "finalizes" the same batch again.This is reachable with two RUNNING members alone (member A done-but-unreported while member B's report executes the consumer) — it predates the Stop-all work in #8270 and is merely easier to hit now that stopped/unqueued members also flow through the consumer. The synthetic unqueue announces added for #8270 already bridge their own version of this window with
_batch_unqueued_pending+ a per-batch bounded lock; NORMAL terminal reports have no equivalent "done-but-unreported" accounting.Direction
Extend the same protocol to normal terminal reports: count a member as outstanding from
done=Trueuntil its report enters the consumer (e.g. a per-batch done-but-unreported count decremented at the top of the report path), or make the consumer's finalize decision consult report-task liveness. Needs care around the report shield,_force_reap, and cancel-recovery paths — which is why it is split out rather than folded into #8270's fix.References
src/kiro_crew/slack/gateway.py(_last = ... not batch_members_pending(batch_id))src/kiro_crew/subagent_manager/terminal.pysrc/kiro_crew/subagent_manager/cancellation.py::_unqueue_implFound by a model review lane while verifying the #8270 fix.