Skip to content

fix: answer a repeated verbatim done with its stored receipt - #45

Merged
cgaravitoq merged 6 commits into
stagingfrom
cgaravitoq/bd3-a1-done-replay
Oct 8, 2026
Merged

cgaravitoq merged 6 commits into
stagingfrom
cgaravitoq/bd3-a1-done-replay

Conversation

@cgaravitoq

Copy link
Copy Markdown
Owner

A verbatim done milestone moves its task to done in the same request that posts it, so the task token died before its own milestone could be retried: a runner whose answer was lost re-posted the same deterministic id and met 401 unauthorized, then reported an outcome the delegation route refuses. A done task's token now authorizes only the milestone whose posted receipt it wrote, so the retry settles on that receipt (200 {"ok": true, "ts": "172.1"}) without a Slack call; a new id, another task's receipt and GET /progress/replies keep 401.

The last-resort narrated fallback also ran after store.complete had posted the receipt and moved the task, erasing the ts the thread already has and telling the retry the milestone was never posted; it now writes only while the receipt is still in flight.

Verify

  • Re-posting the same done body after done answers 200 {"ok": true, "ts": "172.1"} with no new Slack call: a repeated verbatim done answers its stored receipt and no new milestone passes the dead token asserts the stored receipt, an unchanged chat.postMessage count and an unchanged total Slack call count. Red on f28de36 (Expected: 200 / Received: 401 at the second post), green after.
  • A new id after done still 401: same test, {"error": "unauthorized", "ok": false} for done-2, with no receipt row for it and no Slack call. It is a guard, green before and after.
  • Another task's posted receipt stays 401: another task's posted receipt stays unauthorized once its task is done (two tasks, both done, the second task's token replaying the first's milestone id). Dropping task_id = delegation_tasks.id from the replay lookup turns its answer into 403 token_scope, so the binding is what the test pins.
  • The conductor's patch test (posted receipt kept after failing completion): a verbatim done whose completion keeps failing after the commit keeps its posted receipt, applied as the first test next to a verbatim done whose first completion pass fails after the commit still finishes the task. Red on f28de36: the receipt came back {"state": "narrated", "ts": null} where the batch had stored posted 172.1; green after.
  • complete() twice converges: running the done completion twice converges on one posted receipt and one done task runs the completion three times (the replay, then a direct store.complete with a different ts) and asserts one posted receipt holding the ts the first completion stored, one done task, no second Slack call. Red on f28de36 (the second post 401), green after.
  • bun run verify: exit 0, 376 pass / 0 fail across 17 files, format, lint, typecheck, knip, deploy:dry and bun audit (561 packages, 1 ignored) green.

Commits

  1. test f244b81: pins the replay, the dead-token refusals and the kept receipt.
  2. fix dcb1b2b: the completedTask authorization, the receipt's task binding and the in-flight guard on the narrated fallback.

Migration

0010_progress_receipts_task.sql adds progress_receipts.task_id because the replay must know which task a posted receipt belongs to, and the receipt stored no such binding: without it a done task's token could replay any posted receipt, since a milestone id alone does not say who wrote it. delegation.store.complete writes it in the same batch that moves the task to done, so a posted receipt carrying a task binding implies the task is done by that token.

Not covered

  • The live run: proving this against a real runner is the conductor's wave-end pass.
  • POST /progress now parses the body before it authorizes, so a malformed body with a dead token answers 400 invalid_request where it answered 401: the milestone id decides whether a done token may replay and the body is the only place it appears. No store or Slack call happens before either refusal.
  • The narrated fallback is unchanged for a milestone whose completion never committed: it still settles the in-flight receipt so a retry cannot post the milestone twice.

Goal: bloop-delegation-3 row A1.

cgaravitoq and others added 6 commits October 8, 2026 16:13
A verbatim done moves its task to done in the same request that posts it, so
the retry that reuses the deterministic milestone id must answer the receipt
the first post stored instead of the 401 a dead task token earns.

A completion that commits and then fails on both passes must keep that posted
receipt: the last-resort narrated fallback erases the ts the thread already
has and tells a retry the milestone was never posted.
A verbatim done moves its task to done in the same request that posts it, so
the task token died before its own milestone could be retried: a runner whose
answer was lost re-posted the same deterministic id and got 401, then reported
an outcome the delegation route refuses.

A done task's token now authorizes only the milestone whose posted receipt it
wrote, which is what the receipt's new task_id column records: the retry
settles on the stored receipt and makes no Slack call, while a new id, another
task's receipt and GET /progress/replies keep 401.

The last-resort narrated fallback also ran after store.complete had posted the
receipt and moved the task, erasing the ts the thread already has; it now
writes only while the receipt is still in flight.
Resolving the done replay needs the milestone id, so the handler started
parsing the body before authorizing it: an unauthenticated request with a
malformed body answered 400 invalid_request where the base answered 401.
The body is still read first to learn the replay id, but authorization now
decides the answer before the body's validity does.

Closes the A1 review P1 on the progress handler's refusal order.
…done-replay

# Conflicts:
#	apps/slack-agent/test/migrations.test.ts
…taging merge

Staging added the status length refusal to the milestone handler while this
branch moved the body read ahead of authorization, and together they push
handle to a complexity of 21, which fails the lint gate on the merged tree.
The pre-claim refusals move into one function with the same order and answers.
@cgaravitoq
cgaravitoq merged commit 5307788 into staging Oct 8, 2026
2 checks passed
@cgaravitoq
cgaravitoq deleted the cgaravitoq/bd3-a1-done-replay branch October 8, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant