---
title: "The Alert That Postponed Itself for Three Days"
canonical: https://dxdev.com/blog/alert-summary-deferred-itself-for-three-days/
datePublished: 2026-10-03
---
The alert queue grew for three days and nothing reached the author.

A reviewer's `defer` on the flush summary re-queued it forever. The alert flush kept re-deferring its own summary.

## The alert flush fix

The fix landed in `lib/alert.py` (48 lines changed). A `defer` on the flush summary now sends it. Nested summaries are discarded.

## Ghost rows from refused dispatches

The same commit fixed a second problem. `estimate_gate` runs on the same PreToolUse as our dispatch recorder, and it refuses some dispatches. A refused dispatch never gets a PostToolUse. The recorder had written a row for it, and that row said "running" forever. Three of those ghost rows were on one hub's board that morning.

The recorder now asks whether the gate will refuse the dispatch before it writes anything:

```python
return (estimate_gate.parse_estimate(prompt) is None
        and estimate_gate._dispatch_is_attended())
```

Those are the gate's own two predicates. The check mirrors the gate's decision without its side effects: the gate records an estimate or emits a blocked event, and this hook must do neither. `tests/test_agent_record_refused_dispatch.py` (92 lines) pins the helper to `estimate_gate.evaluate` so the two cannot drift apart. The helper never raises. If the question can't be answered, the dispatch is recorded as before. The gate already emits its own terminal pipeline event for the refused flight.

## Two rows from one dispatch

One dispatch became two rows, the second with no label. The content hash alone can differ between the PreToolUse and PostToolUse firings. The PostToolUse side now joins the result to the row its own PreToolUse wrote, by the harness's `tool_use_id`, when that row carries one:

```python
agent_id = _id_for_tool_use(sid, tool_use_id) or agent_id
```

## Where the unit gets created

Each dispatch is also recorded as a unit in its session, so a hub's lanes show up in the CLI's unit list with an outcome, not only on the board. That unit is created at PostToolUse, not PreToolUse. PostToolUse only fires for a dispatch that actually ran, so a call that any PreToolUse hook refused never leaves an orphan wip unit that would hold up pre-close. The new `units/dispatch_capture.py` is 179 lines, with 114 lines of tests in `tests/test_units_dispatch_capture.py`.

The whole commit touched 13 files, with 636 insertions and 6 deletions.

## What the three days came down to

The flush re-deferred its own summary, and the queue grew for three days. `defer` on that summary now sends it, and nested summaries are discarded.
