Repository navigation
Description
What happened
A coordinator agent ran two delegated tasks, A and B, started in the same run. When A finished while the coordinator was busy, its completion message was queued, and the coordinator cancelled that queued message (t3_queue action: "cancel") because it had already read A's result. B, still running, finished about 17 minutes later and never woke the coordinator: its result was saved, but no completion message was delivered. The coordinator found it only through a periodic status check.
The cancellation disposes the completion cohort of the run that spawned both tasks. It also sets B's delivery to disposed while B remains running. B subsequently completes normally, with its result saved and observedByRunId: null, but no completion wake is offered.
Expected: cancelling A's queued message removes that delivery. B, whose result was not in the message, keeps its promised completion wake.
t3_queue describes its scope as “Manage a thread's queued messages.” (tool definition) The running-task result says: “Completion wakes this thread, so end your turn. Do not wait on it or poll it.” (contract) Cancelling a message does not advertise disabling another task's future delivery.
Impact: major degradation or frequent failure
A coordinator can silently miss a finished review after deleting another queued result. The recorded case was found by a later periodic check. Frequency was not measured.
Diagnosis
At the installed build's upstream commit 47f2c8d29e981c7723467efe137ee19e91df924e:
dispatchQueuedRunCancelsees a queued message'sdelegatedCompletionand callsdisposeDelegatedCompletionCohortwith itsparentRunId, without restricting the disposal to the message's task IDs (Orchestrator.ts:9678).- That helper selects every app-owned task attached to the owning run (Orchestrator.ts:2617), disposes the cohort, and marks every task whose delivery is not already acknowledged, delivered or disposed as
disposed(Orchestrator.ts:2642). It does not require the task to have finished or to appear in the cancelled message. - When B finishes,
planDelegatedCompletionDeliveryreturnsoffer: falsefor its preserveddisposedstate (Orchestrator.ts:12131).
Still present on main 71c2d3a302092b94511eb7e6ab4e6844a66dbf22. The queued-cancel branch is unchanged at Orchestrator.ts:10074; the planner is unchanged at :12530. Main's helper at :2784 can preserve siblings through interruptedWakeTaskIds, but the queued-cancel caller does not supply it. Its default still disposes all outstanding tasks on the owning run.
Steps to reproduce
These steps follow from the source and the production event sequence below. No clean reproduction or tests were run for this investigation.
- In one coordinator run, start two async delegated tasks, A and B.
- Let A finish while the coordinator is busy, so A's completion message is queued. Keep B running.
- Cancel A's queued message with
t3_queue { "action": "cancel", "queuedRunId": "<A's queued wake run>" }. - End the coordinator turn and let B finish. B's result is saved, but it does not wake the coordinator.
Version
Server 0.1.8-nightly.20261106.4275, upstream 47f2c8d29e981c7723467efe137ee19e91df924e.
Environment
Debian 13, Linux x64 LXC, systemd user service; Claude coordinator, Codex gpt-6.1-sol reviewer. Local patches: thread-session-boundary and reconnect-fairness. Neither changes the queue handler, cohort disposal or delivery planner. They concern process/session boundaries and SQLite/storage scheduling respectively.
Evidence
Read-only orchestration_events evidence, UTC on 2026-11-07. P is mcp:0e63ae8e…; B is the Codex reviewer. Task titles, result text and unrelated repository identifiers are omitted.
sequence time stream event
6107922 20:16:42.160 P reviewer B started, owning parent run 4293
6110563 20:29:55.687 P completion wake run 4324 created queued
6110567 20:29:55.687 P wake message: parentRunId=4293, generation=2
taskIds contains only a different completed task, A
6110810 20:30:43.946 P t3_queue(action=cancel, queuedRunId=4324)
6110851 20:30:52.246 P run 4293: cohort disposition=disposed, delivery=null
6110852 20:30:52.246 P B still running; completionDelivery=disposed, observer=null
6110854 20:30:52.246 P queued wake run 4324 cancelled
6110859 20:30:52.327 P t3_queue success: sequence=6110856
6114660 20:47:14.535 B final assistant message saved
6114663 20:47:14.579 B provider turn completed
6114672 20:47:14.625 B run 1 completed
6114674 20:47:15.290 P B completed with result saved; delivery still disposed, observer=null
The disposal and cancellation events share command mcp:e585b9a5…; the queue tool's returned sequence identifies that batch. This establishes the cause before B's completion, rather than inferring it from a missing wake alone.
The scoped parent queries returned 3 events for B/owning run in 20:15–20:48, 2 for queued run 4324 and its message in 20:20–20:31, and 13 tool/message/run events in 20:30:20–20:31:10. The reviewer query returned 8 message/provider-turn/run events in 20:46:30–20:47:30. Every query used a specific thread and time window, with SQLite opened using mode=ro.
Related issues
- #28175, second problem: interrupting or steering an executing completion wake disposes siblings. This case uses
t3_queue cancelbefore the wake runs. Main's preservation option is used by the interrupt path, but not this path. - #23871: explicit Stop deliberately suppresses outstanding wakes. This incident is a single queued-message cancellation, without Stop.
Duplicate searches included dispatchQueuedRunCancel, disposeDelegatedCompletionCohort, queued-run.cancel, and combinations of t3_queue, completionDelivery, disposed, sibling and cohort.
Fix applied or workaround
Avoid cancelling queued delegated-completion messages while sibling tasks from the same owning run remain outstanding. If a cancellation already happened, read the saved child result explicitly; the coordinator did recover this review that way. Reading it does not restore future notification policy.
Possible fix: dispose only the task IDs carried by the cancelled message and keep unrelated pending tasks eligible for a later wake. Preserve the separate, deliberate Stop behavior.
Filed by
Claude (Opus 5.5) in T3 Code, on the owner's instruction.
Activity
[-][Bug]: Cancelling one queued completion message disables future results from still-running sibling tasks[/-][+]Cancelling one queued completion message disables future results from still-running sibling tasks[/+]aron-intframe commentedon Nov 8, 2026
Reproduced on
mainata1af1841with a server test. PassinginterruptedWakeTaskIdsfrom the queued-cancel caller is not enough on its own, and the same cancel also drops tasks the coordinator starts after it.Cause.
dispatchQueuedRunCancel(Orchestrator.ts:10076) callsdisposeDelegatedCompletionCohortwithoutinterruptedWakeTaskIds, sokeptTaskIdsis empty (:2814). Every outstanding app-owned task on the owning run is markeddisposed, and the cohort itself becomesdisposed(:2831).planDelegatedCompletionDeliverythen returnsoffer: falsefor B because of its row, and for any later task because the cohort is no longeropen(:12548).How I checked. I copied the "interrupted completion wakes" case (
DelegatedCompletionDelivery.test.ts:1383) and changed three things: the coordinator run staysrunningso A's wake is queued, not started;run.interruptbecomesqueued-run.cancel; B is adelegated_task.requestsibling that completes after the cancel. A second case starts the task only after the cancel, with no sibling outstanding.On
main:Why the one-line fix falls short. With only
interruptedWakeTaskIds: queuedMessage.delegatedCompletion.taskIds, B is kept, butemitQueuedRunCancellationis gated onkeptTaskIds.size === 0(:2876), so the queued message is never removed:Adding
cancelQueuedDelivery: falseand an explicitemitQueuedRunCancellationfixes the sibling case, but the late-task case still endslateTaskDelivery: "disposed", because with nothing kept the cohort is still disposed.Fix that passes both cases. Removing a queued wake disposes only the tasks that message carried, cancels the run, and leaves the cohort open. The terminal-run listener already handles a
cancelleddelivery run (:13156): it clearscohort.delivery, or reserves a successor for pending results, so the next result gets a new generation.With it:
DelegatedCompletionDelivery,ThreadStop,runtimeLayer,GuestShare,ThreadDeletion,SubagentProjectionandThreadManagementServicetests: 245 passed, 0 failed.effect-tsgo tsrs --noEmitinapps/serverreports no errors inOrchestrator.ts(only the existing brand suggestions at:200-:332, which are outside the patch).One call for you:
thread.settlealso goes throughdispatchQueuedRunCancelfor automatic queued runs (:3529). With this patch, settling no longer disposes those cohorts (the settle tests still pass). If settle should keep dropping later results, keep the olddisposeDelegatedCompletionCohortcall on that path only.thread.archiveis not affected; it disposes all cohorts separately (:4317).Test body after the shared seeding (same as :1383, minus the write that completes the origin run)
Not run: a real provider turn. This is the event-level test only.