news / engineering

receipts before retries

the dispatch receipt contract — how the factory stopped losing agent outcomes by forwarding the run id.

a dispatch system that cannot reconcile its outcomes against the runs that produced them is a dispatch system that loses work. before path 0.5 — the receipt-by-run-id work shipped in #219 — the factory had a recurring failure mode where an agent’s outcome was written to disk but the foreman could not link the outcome back to the dispatch that ran it. the run completed; the receipt existed; the receipt was orphaned. the foreman, lacking the link, treated the dispatch as in-flight or unstarted, and either re-dispatched it or stalled the phase. either way the work either re-ran or stopped.

the contract that broke this open is small. every agent dispatch is assigned a LOSWF_RUN_ID at start. every agent writes a receipt at end. the receipt’s filename or metadata contains the run id. the post-agent hook reads the receipt, finds the run id, and forwards the run id into the reconcile step which advances the phase against the right dispatch. the failure mode before #219 was that the run id was generated in the foreman’s dispatch shell but was not consistently forwarded through to the hook’s reconcile step. the hook saw the receipt, could not associate it with a run, and either dropped it or matched it heuristically (timestamp, issue number) which produced wrong matches under parallel dispatch.

the corrected forwarding pattern, from commit 189c0e3:

# correct:
LOSWF_RUN_ID="$run_id" ./reconcile-receipt.sh "$issue_num"

# what it replaced:
RUN_ID="$run_id" ./reconcile-receipt.sh "$issue_num"

the variable name matters. the reconcile script reads LOSWF_RUN_ID; the dispatcher was exporting RUN_ID. the receipt arrived with the right id; the reconcile step read an empty string; the match logic fell back to heuristics; the heuristics misfired on parallel dispatches where two builds for adjacent issues had timestamps within seconds of each other. the fix was one variable rename. the bug had been silently mis-attributing receipts for an unknown number of sweeps.

the broader contract #219 codified is that the run id is the primary key. receipts are keyed by run id, not by issue number, not by timestamp, not by branch name. that is a deliberate choice: issue numbers can recur across parents and children; timestamps collide under parallel dispatch; branch names mutate during rebases. the run id is generated once at dispatch start, is unique across the lifetime of the foreman, and follows the dispatch through every hop. any system that wants to reconcile against a receipt has to know the run id. forwarding the run id explicitly is the way the contract is honored.

the operational rule that follows: any subprocess that needs to reconcile work back to a dispatch must inherit LOSWF_RUN_ID from the foreman’s environment, and any reconcile script must read LOSWF_RUN_ID (not RUN_ID, not $1, not the timestamp of the receipt file). the rule is small but it is the rule the bug shipped by violating, and the post-fix discipline is to grep for receipt-handling code that does not read LOSWF_RUN_ID and either fix it or document the exception.

a property worth naming: receipts before retries is a precondition for autonomous operation. an autonomous factory cannot decide whether to retry a dispatch unless it can determine the dispatch’s outcome, and it cannot determine the dispatch’s outcome unless the receipt is correctly associated. without that association, retry policy defaults to either “retry every dispatch that did not advance the phase” (which causes duplicate work) or “never retry” (which causes stalls). neither is acceptable for unattended operation. the receipt-by-run-id contract is the data-plane fix that makes retry-on-failure a tractable policy decision.

the bug class the fix prevents is silent mis-attribution. a wrong receipt-to-dispatch match does not crash; it just routes the wrong outcome to the wrong issue. the issue’s label transitions on a verdict that does not belong to it. the next sweep reads the wrong label and proceeds. nothing in the pipeline announces the error. the only signal is that issues advance to states that do not match their actual work, which the operator notices only on inspection. a debugging session that does not start by checking the receipt-to-dispatch mapping will not find the bug.

the dispatch receipt format codified by #219:

{
  "run_id": "loswf-<timestamp>-<random>",
  "agent": "loswf-builder",
  "issue": 217,
  "outcome": "success",
  "reason_code": "build-shipped",
  "phase_from": "building",
  "phase_to": "review",
  "started_at": "<iso8601>",
  "completed_at": "<iso8601>"
}

every field is required. the run_id is the primary key; everything else is descriptive. the reconcile step matches on run_id exclusively. the secondary fields exist for audit, not for matching. that separation is what makes the contract robust under parallel dispatch — two receipts for two adjacent issues can never collide on run_id because the run id was generated by the foreman at dispatch start with sufficient entropy.

a related discipline call surfaced in the same work: parallel dispatch requires worktree isolation. when two builders run simultaneously against the same repo, one builder’s branch checkout can clobber the other’s. the foreman-on-its-own-worktree fix from the velocity-unlock retro is the same shape as the receipt-by-run-id fix: each parallel actor owns its own isolated copy of the state it mutates. the receipt fix addresses the data plane (which run produced which receipt); the worktree fix addresses the workspace plane (which agent’s checkout is current). both are required to operate parallel dispatch safely. either one alone is insufficient.

a second-order property of receipts-as-primary-key is auditability. with every dispatch keyed by a run id and every receipt carrying that key, the full history of a dispatch is reconstructible after the fact. the iteration retros for the 2026-05-21 session rely on this property: each iteration’s PR count and re-review count come from grepping receipts by date and reading the run-id-keyed outcomes. without the run-id key, the retros would have to reconstruct iteration boundaries from PR merge timestamps, which is fragile across iteration boundaries that fall mid-PR-cycle.

the meta-lesson is that retry policy is downstream of receipt design. an autonomous system that wants to retry with grounded decisions has to first answer “did this run succeed?” reliably. the answer is reliable only when receipts can be matched to the run that produced them. the matching is reliable only when the receipt carries a key that the dispatcher generated and forwarded. the chain of dependencies is short — three steps — but each step has to be honored. #219 shipped the third step (forwarding); the prior receipt-writer work shipped the first two (key generation, key inclusion in receipts). once all three were in place, the factory could decide retry policy as a policy question rather than as a data-plane question, and the decision shifted from “the system cannot tell what happened” to “the system can tell what happened and now the policy decides what to do about it.” that shift is the operational difference between a dispatch system that loses work and one that does not.