# After Stop, earlier delegated tasks can never notify the coordinator again, and nothing shows it · Issue \#23871 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/issues/23871)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: completed

Priority: 0

## Description

[Discussion and activity](https://git.cafe/pingdotgg/t3code/issues/23871)

---

### What happened

A coordinator thread had delegated app-owned child tasks with `completionWake: "always"` in earlier turns. Later, the owner pressed Stop on an unrelated answer, rolled that answer back, and told the coordinator to continue. The children finished afterwards and their results were saved, but the coordinator was never woken. It sat idle for 82 minutes until the owner came back. Neither the UI nor `task_status` showed that the notifications had been switched off.

### Diagnosis

UI Stop switches off completion notifications for **every** outstanding app-owned task in the thread, including tasks delegated in earlier turns. Nothing turns them back on: a new owner message, rollback, `/compact`, a wake-policy change and a restart all leave them off. When the child finishes, its result is saved as `completed` with delivery `disposed` and `observedByRunId: null`. No delivery is reserved, so no coordinator run starts.

Source at the published tag for the installed version (`680152e`):

- [`holdStoppedThread`](https://github.com/pingdotgg/t3code/blob/680152e27b2a961686389ca69a1557cbb2c55f08/apps/server/src/orchestration-v2/Orchestrator.ts#L9245-L9272) collects the owning run of every outstanding task in the thread and disposes its completion cohort. There is no current-turn filter.
- [`disposeDelegatedCompletionCohort`](https://github.com/pingdotgg/t3code/blob/680152e27b2a961686389ca69a1557cbb2c55f08/apps/server/src/orchestration-v2/Orchestrator.ts#L2309) marks the cohort `stopped` and each pending task `disposed`.
- [`planDelegatedCompletionDelivery`](https://github.com/pingdotgg/t3code/blob/680152e27b2a961686389ca69a1557cbb2c55f08/apps/server/src/orchestration-v2/Orchestrator.ts#L10380) makes no offer for a `disposed` task or a cohort that is not `open`, whatever `completionWake` says. No code path moves a cohort from `stopped` back to `open`.

Stop reaching across turns is deliberate (#15325 / #15331), and the regression test from #15331 asserts that a newer owner message does not revive the wakes. That guarantee should stay. The defect is what is missing around it:

1. There is no supported way to turn notifications back on for selected tasks after Stop, including results that were saved while notifications were off.
2. Neither the agent nor the UI can see that notifications are off, so promised follow-up work stalls with no signal.

### Recommended fix

Keep Stop exactly as it is, and keep plain "continue" from re-arming anything. Add an explicit, task-scoped way to resume, plus a visible status. This reuses the existing delivery machinery and needs no migration.

- **Resume selected tasks.** Add a command such as `delegated_task.completion-delivery.resume { parentThreadId, taskIds, authorizedByRunId }`. For each selected task, it reopens the owning `stopped` cohort and clears `disposed` on that task only, then hands it to the existing `planDelegatedCompletionDelivery` → reservation → continuation outbox path, as `dispatchDelegatedTaskWakePolicy` already does.
  - A child that is still running notifies normally when it finishes. A result that was saved while notifications were off gets one notification straight away. Children are never rerun.
  - Unselected tasks in the same cohort stay off, because every delivery path checks the per-task `disposed` state before the cohort.
  - An agent may call it only from a live run that Stop has not reached. This is the same check `delegated_task.request` already applies. A later Stop switches everything off again, as it does today.
  - Refuse tasks that were cancelled or already delivered or acknowledged, tasks in an archived or deleted thread, and tasks whose cohort was permanently `disposed`. By default, also refuse tasks whose delegating turn was rolled back.
- **Record why a task was switched off.** Add one optional per-task field, for example `completionDelivery.suspendedBy: "stop"`. Without it, once a cohort has been reopened for one task, its still-suspended siblings no longer show that Stop was the cause. Rows written before the field existed fall back to "cohort is `stopped`".
- **Make it visible.** Add a read-only `completionNotification` status to `task_status` (`armed`, `pending_delivery`, `delivered`, `acknowledged`, `suspended_by_stop`, `disabled`), and expose resume as one MCP tool. The UI already receives `completionDelivery`, so it could show a "Notifications off (stopped)" badge with a per-task Resume action.
- **Tests**, extending `DelegatedCompletionDelivery.test.ts`:
  - Stop, then continue with no resume, still never wakes (the #15331 test is unchanged).
  - Resume before and after the child finishes produces exactly one notification with a new generation.
  - An unselected sibling stays off.
  - Repeating a resume does nothing.
  - A Stop after a resume cancels the queued wake.
  - A resume from a run that Stop has reached is rejected.
  - Ineligible tasks are rejected.
  - A restart between commit and offer delivers once.
  - `task_status` reports `suspended_by_stop` without acknowledging the result.

"Exactly once" here means one accepted notification message per resume, not one model action. This adds a command and changes how recovery behaves after Stop, so it needs maintainer agreement first. I'm happy to send a PR once the direction is triaged. Out of scope here, and worth separate issues: failed or quota-limited wake runs marking tasks `delivered`, and terminal `task_status` reads acknowledging a result before the agent has acted on it.

### Steps to reproduce

1. Turn A delegates an async app-owned child with `completionWake: "always"`. Turn A finishes while the child is still running.
2. Send an unrelated message B and press Stop.
3. Optional: roll back B.
4. Send an explicit "continue" message and let that turn finish.
5. After the child has finished (before or after step 4), look at the task. It is `completed` with delivery `disposed` and no observer, no wake was reserved, and the coordinator stays idle.

### Version

0.1.2-nightly.20261021.3486. Source was read at that version's published tag, commit `680152e27b2a961686389ca69a1557cbb2c55f08`. The installed binary itself was not checked against it. `Orchestrator.ts` is byte-identical in v0.1.2, v0.1.2-nightly.20261022.3541 and main `2c8bdf6`, so none of them changes this.

### Environment

Linux, with a Claude coordinator and Codex children. The UI surface used for Stop, and the Node and agent CLI versions, were not recorded. The tests used Node 24.21.0.

### Evidence

```shell
# Incident ledger, 2026-10-22 UTC (read-only SQLite queries)
19:45:55.755  Stop on run 459 -> cohorts of earlier runs 454 and 458 set to stopped; their running children set to disposed
19:46:00      rollback of the stopped answer (run 459)
19:46:28      owner continuation run 460 starts; it later completes normally; cohorts stay stopped
19:53:21.196  child 1 completes: result saved, delivery disposed, observedByRunId null
20:11:48.670  child 2 completes: same state
21:15:40.877  next coordinator run is the owner's manual /compact; no wake run before it
# An earlier incident the same day, from a supplied export: task disposed while running at 17:29, completed and still disposed at 17:50

# Characterization tests (test-only change on 680152e, in-memory SQLite, synthetic terminal events)
$ vp test run apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts -t 'characterizes Stop then continue'
5 passed
  4 Stop cases (child finishes before or after the continuation, with and without the rollback event):
    all end idle, delivery disposed, observer null, no reservation
  no-Stop control: the result is reserved for delivery
```

Limitations: the tests check reservation state; they do not run the notification consumer or a provider. The ledger is what shows that no wake actually ran. The rollback case replays only the durable `run.updated: rolled_back` event. The test patch is available on request.

### Related issues

#15325 / #15331 made Stop block late wakes, and this keeps that behavior. #16900 and #19228 involve provider-native children, not app-owned tasks. Open PRs #23542, #20361 and #17885 fix other missing-wake paths; none of them re-arms deliveries disposed by Stop.

### Fix applied or workaround

None. Nothing was run to unblock the incident, and its live threads, tasks and database were only read. The tests ran in a separate local checkout.

### Filed by

Claude Opus 5.5 (Claude Agent in T3 Code). It was rewritten from an investigation and tests by GPT-6.1 Sol (Codex) and a source-level review of the fix by Claude Opus 5.5. Filed by hand using the via-triage template's structure, not through `npx t3 triage`.

