# Row 237: one decision publishes once (#1743) Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md` ยง "row 199 follow-up N1", blob dca67d310e1b. Source: Duckworth's row 199 round 1 note N1 (duckworth-0068). Base: `cb5fdb064` (row 199's landing). Reviewer Duckworth, gate owner Sage. ## The defect `checkPublish` opens a ticket after checking the decision is unspent. The decision is spent only at `recordPublish`, which runs after the bus process has pushed. Two `checkPublish` calls on one decision therefore both get tickets, and both push. With different branches, the commit lands on two fork refs: `refs/heads//feat` and `refs/heads//other`. The second `recordPublish` then refuses `decision-consumed`, but by then its push has already reached the fork. ## The fix, and why this option `checkPublish` refuses `publish-busy` when an open ticket in `#publishes` holds the same business and decision (`packages/bus/src/broker.mjs:1038-1040`). It runs after the authority and fact checks, so a spent decision still refuses `decision-consumed` first. The brief's other option, rechecking `#spent` in `publishTarget`, doesn't close the race. Both tickets reach `publishTarget` before either push ends, and nothing is spent until `recordPublish`. So both rechecks pass and both pushes run. The only state that exists while the first push runs is the open ticket, so the check reads that. A refusal (`publishRefused`) drops the ticket and leaves the decision unspent, so a retry on the same decision gets a new ticket. That is row 199's behaviour, unchanged. A within-role publish has no decision and isn't affected. ## Tests - `packages/bus/tests/publish.test.mjs:186`. A second ticket refuses `publish-busy`, on the same branch and on another. After `publishRefused` the retry gets a ticket and a third refuses busy. After `recordPublish` there is one `action.allowed` for git.push.remote and the next call refuses `decision-consumed`. Another agent's decision on the same task isn't held. - `packages/cli/tests/publish-socket.test.mjs:269`. End to end through the git socket, the bus runtime and the TLS fake Gitea. While the first publish holds its ticket, a second publish on the same decision to branch `other` gets `publish-busy`. The fake Gitea gets exactly one `git-receive-pack`, the fork has only `refs/heads/coder/feat`, and there is one `action.allowed`. Runs on this branch, single files: - publish.test.mjs: 13/13. - publish-socket.test.mjs: 8/8. ## Mutant M1 removes the `publish-busy` line. Both tests fail: - The bus test fails at the first busy assert. - The cli test fails because the second publish answers `ok: true` with `ref: refs/heads/coder/other`. That is the defect: two refs on the fork. Logs are in `/mnt/storage/scratch/megavolt-r237/` (m1-bus.log, m1b-cli.log). `cmp` confirmed broker.mjs matched the saved copy after the restore. The first cli mutant run (m1-cli.log) was against a hook that recursed on every checkPublish. I fixed the hook to fire only for branch `feat` and reran it (m1b-cli.log). ## Residuals, not fixed here 1. **A ticket that never ends holds its decision until the bus restarts.** `#publishes` is in memory. That happens when the host dies between checkPublish and recordPublish or publishRefused. It fails closed (`publish-busy`), and a restart clears it. 2. **publish-timeout (sage-1440).** The socket files `publishRefused` and drops the ticket, but the bus process's push may still be running. A retry on the same decision can then open a ticket while that push is live. This is the same residual the socket already logs ("the bus process's push may still reach the fork"). Closing it means the runtime's `remotes` must keep the decision busy until its push settles, which reaches into runtime.mjs. That is beyond the brief's two options. 3. **checkLand has the same shape.** `#landings` takes a ticket per call and the decision is spent at `recordLand`. Whether two landings on one decision can both reach main isn't checked here. That's a follow-up for Sage to row or dismiss. ## Gate Per the brief: Duckworth approves the manifest, the bus suite and every `scripts/test-*.sh` are green on Sage's gate, and M1 is killed. This row also changes a cli test file, so the cli suite (or that file alone) should run on the gate too.