Description
Race condition in the workflow result delivery flush loop, verified by read-only audit (seal/gpt-5.6-sol subagent, verdict: PARTIAL).
flush() snapshots and removes current entries from pending while concurrent callers join the active promise:
extensions/workflows/result-delivery.ts:64-68 — if (flushing) return flushing
result-delivery.ts:73 — batch removed before await options.deliver(...)
An envelope settling during the await deliver(...) window is persisted and enqueued by releaseInline()/defer(), but its flush(true) sees flushing and returns the old promise (result-delivery.ts:144-154). The finalizer only clears flushing (result-delivery.ts:122-124) — it does not drain newly queued entries.
Impact
- Successful delivery path is mitigated: the send uses
pi.sendMessage(..., { deliverAs: "followUp", triggerTurn: true }) (index.ts:788-804), and the follow-up turn eventually emits agent_settled → parentSettled() → another flush edge.
- Failure path is not mitigated: if the in-flight delivery fails while another completion is enqueued, no follow-up turn was scheduled, and both entries can remain pending with no guaranteed later event.
Other flush triggers: session restoration (index.ts:990-993) and parent agent_settled (index.ts:996-997) only.
Test coverage gap
result-delivery.test.ts covers retries and partial receipts, but not enqueue-during-flush concurrency.
Suggested fix
After clearing flushing, automatically start another flush when pending.size > 0 and the host is idle (preserving the strongest wake request), or make flush loop until the queue is empty.
Found by automated workflow audit (run wf_56028a5b018b).
Description
Race condition in the workflow result delivery flush loop, verified by read-only audit (
seal/gpt-5.6-solsubagent, verdict: PARTIAL).flush()snapshots and removes current entries frompendingwhile concurrent callers join the active promise:extensions/workflows/result-delivery.ts:64-68—if (flushing) return flushingresult-delivery.ts:73— batch removed beforeawait options.deliver(...)An envelope settling during the
await deliver(...)window is persisted and enqueued byreleaseInline()/defer(), but itsflush(true)seesflushingand returns the old promise (result-delivery.ts:144-154). The finalizer only clearsflushing(result-delivery.ts:122-124) — it does not drain newly queued entries.Impact
pi.sendMessage(..., { deliverAs: "followUp", triggerTurn: true })(index.ts:788-804), and the follow-up turn eventually emitsagent_settled→parentSettled()→ another flush edge.Other flush triggers: session restoration (
index.ts:990-993) and parentagent_settled(index.ts:996-997) only.Test coverage gap
result-delivery.test.tscovers retries and partial receipts, but not enqueue-during-flush concurrency.Suggested fix
After clearing
flushing, automatically start another flush whenpending.size > 0and the host is idle (preserving the strongest wake request), or makeflushloop until the queue is empty.Found by automated workflow audit (run
wf_56028a5b018b).