# PR #23579 runtime capture: immediate webhook deliveries survive a SIGKILL restart

- **before** = `~/wt/pr-23579-before` @ `8d3b061121fe65669d5d43d011b48cd3027a4108` (main)
- **after** = `~/wt/pr-23579` @ `e3c21846c23fc6f412914a3590c73b94e3004e5e`
- **Environment:** fs-24, Linux 6.1.102 x86_64, node v24.22.1, sqlite3 CLI (read-only). The provider was a **real Claude turn**: `claudeAgent` / `claude-haiku-5-5` via Claude Code 2.1.303 and the ~/.claude proxy settings. No fixture provider was used.
- **Prompt:** `Webhook marker {{body.n}}. Reply with the single word OK. Do not use any tools and do not edit files.` Workspace strategy: worktree off `main`, `startFromOrigin: true`, with a local bare origin.
- **Isolation:** each row on each side started from a fresh `--base-dir /tmp/ev-23579/home-<side>-<row>` on ports 47912–47917.
  - The server ran as `node apps/server/src/bin.ts --base-dir … --port … --host 127.0.0.1 --no-browser`, with cwd = the home dir.
  - `bin.ts` has no `--home-dir` or `--no-seed` flags; those belong to `vp run dev`. A bare `bin.ts` never seeds from `~/.t3`, which satisfies the brief. All `T3CODE_*` variables were unset, and `~/.t3` was never touched.
- **Setup path, no UI:**
  - `mint-mcp-token.mjs` issues a full-access MCP client session against the isolated home, the same call `t3 thread` makes.
  - `/mcp` was then driven through `bin.ts acp-mcp-call`: `t3_project_create`, `t3_project_update` (default model) and `schedule_task` (webhook).
  - The hook token was read from the isolated `scheduled_tasks` row.
- **Rerun one row:** `driver.sh <checkout> <home-dir> <port> <R1|R2|R3> [label]`. R1 and R2 include R4 at the end. sha256 is printed in the final report.

## How the queue was produced, and a correction to the brief

A real server only queues an immediate delivery while the task's previous `threadLaunch.launch` command is in flight. `launch` returns once the thread exists, and worktree preparation and setup scripts run afterwards.

So a setup-script "hold" does **not** keep the per-task permit. My first attempt confirmed this: with a 120 s setup script, the second delivery launched its own thread at once.

`checkWorktreeBase` is not on the scheduled-task launch path, so stalling git couldn't hold the permit either. `gitwrap/git` is left on PATH but never armed in the final runs; it just passes through.

The final driver uses an **uninstrumented race** instead. `burst.mjs` POSTs the in-flight delivery ("hold") and then the queued one(s) concurrently, and SIGKILLs the server the moment the last 202 arrives. The dump taken right after the kill shows which deliveries were still queued.

## Matrix

| Row | before (8d3b061) | after (e3c2184) |
|---|---|---|
| **R1**: 1 immediate delivery behind an in-flight one, SIGKILL, restart | Both POSTs got 202. At kill time: deliveries `accepted`, **pending table empty**, 0 threads. After restart and a 120 s wait: still `accepted`, 0 threads, **nothing launched**; task shows `failed: Run was interrupted by a server restart.` **LOST** | Both POSTs got 202. At kill time: both persisted in pending as `dispatching`, 0 threads. Restart: the `webhook-coalescing` drain launched both within about 1 s. Both runs **completed** (real Claude), **1 thread per delivery**, 2 threads total, pending emptied. **DELIVERED** |
| **R2**: 2 concurrent immediate deliveries behind an in-flight one, SIGKILL, restart | All three POSTs got 202. At kill time: the in-flight delivery had a thread (`preparing`), r2-a and r2-b had none, pending empty. After restart and 120 s: in-flight run `cancelled` by recovery; **r2-a and r2-b never launched** (0 threads, still `accepted`). **LOST** | All three POSTs got 202. At kill time: pending rows were in-flight `running` + r2-a `dispatching` + r2-b `dispatching`; the project had 1 thread mid-launch. Restart: all three re-ran in order (the `running` row with the same command id) and **all completed**. **Exactly 1 thread per delivery, project total 3**, so the re-run did not duplicate the in-flight thread (C1). **DELIVERED** |
| **R3**: coalesced delivery (`coalesceKey {{body.k}}`, window 10 s), SIGKILL inside the window, restart | Pending row `pending` at kill. After restart: launched, run **completed**, 1 thread | Same: `pending` at kill, then launched after restart, run **completed**, 1 thread. **Unchanged path behaves the same** |
| **R4**: replay the same `body.id` (replayKey `{{body.id}}`) after the restart | Replay returned 202 and was logged `duplicate`, with no new thread. But the original was never run, so **the prompt ran 0 times** (the PR's "replay says duplicate but nothing ran" bug) | Replays returned 202 and were logged `duplicate` (R1: 1, R2: 2), with no new threads. **The prompt ran exactly once per delivery** (thread counts unchanged at 2 and 3) |
| **R5**: task replaced between accept and restart | not captured | not captured: **unit-test-only** (see below) |

The HTTP body is `{"deliveryId":…}` with status 202 for both accepted and duplicate deliveries. The `duplicate` outcome only shows in the delivery log, which is why the transcripts dump it.

## Unverified / unit-only

- **R5** is unit-test-only (`a replaced task's old drain leaves the new incarnation's delivery alone`).
  - Deleting a task cascades its pending rows, and a recreated task gets a new id unless it is recreated with the same id through the service. To reach the case, the old drain must still be parked on the old permit while the new incarnation accepts a delivery.
  - On a real server the permit is held only for the length of a launch command (tens of ms), and nothing here can stall it without code injection. The unit test stubs a blocking launch for exactly this.
- **The "running run" in R1** was a delivery whose launch was in flight, not a long-running provider turn, because a provider turn does not hold the task's permit on either side.
  - On the after side, R1's in-flight row had not yet reached `running` when the kill landed. R2 caught the `running` state and covers the C1 same-command-id re-run.
- The race is timing-dependent. All six captures landed the kill with deliveries still queued, as the dumps at kill time show.
- The T3 Connect relay and tunnel paths were not exercised.

## PIDs (all started by the driver and all stopped; no listeners on 47910–47929 at the end)

- probe 1723973
- after-R1 1748181 → 1748914
- before-R1 1750777 → 1754512
- before-R2 1750776 → 1754622
- after-R2 1750793 → 1754157
- before-R3 1750828 → 1754678
- after-R3 1750791 → 1754350
- Earlier discarded after-R1 attempts: 1729887 → 1734230 and 1741225 → 1742654 (setup-script hold) and 1746309 → 1747215 (gitwrap hold). Their transcripts were overwritten by the final after-R1.
