radio adoption: #182's repo filter never executes and fails open — route on from:, and stop testing a branch production can't reach - #211
Merged
Conversation
…ute on from:, and stop testing a branch production can't reach `_adopt_legacy_pm_inbox` decided which legacy-`pm` backlog messages to adopt from a message's `repo:` frontmatter field. `cmd_send` writes that field only when `--repo` is passed, and nothing passes it — 0 of 163 `radio send` call sites, 0 of 399 real messages — so the filter's inner branch had never executed in production, and its untagged fall-through *adopts*. A guard that never ran was indistinguishable from one that ran and said yes; the one line that would have exposed it (`left N foreign-repo message(s)`) could not fire either, because `skipped` only incremented inside the dead branch. Observed: one PM adopting 53 messages belonging to four other repos, silently. Route on `from:` instead — every message carries it, and it encodes the repo by construction. Direction is what makes the sender the right field: adoption candidates are worker→pm mail, so they are all literally `to: pm` and the recipient carries nothing (the mirror of #201's dead-letter routing). The test is one-sided — adopt iff `from` matches `worker-<my-repo>-*` / `reviewer-<my-repo>-*` / `pm-<my-repo>` — so only one candidate name is ever compared and `_resolve_pm_by_slug_split`'s hyphen ambiguity never arises. An unattributable message (`from: unknown`) keeps the first-come treatment, the only option available, but is now counted and logged as such. `repo:` stays the higher-precedence arm; `cmd_send` is deliberately NOT changed to start writing it (a wire-format change for no benefit `from:` doesn't already give). Twelve new tests build every fixture the way `cmd_send` really does, with no hand-injected `repo:`, and were confirmed to fail against the unfixed binary. The four existing `--repo` tests are kept and relabelled as what they are: the higher-precedence arm, a branch production does not currently reach — which is exactly why this shipped inert. No recovery migration: the 53 misadopted messages are all acked and their PRs merged, so re-routing would move dead mail between mailboxes for no reader. Closes #210 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #210
What was wrong
_adopt_legacy_pm_inboxrouted the legacypmbacklog on a message'srepo:frontmatter field.
cmd_sendwrites that field only when--repoispassed, and nothing passes it — 0 of the 163
radio sendcall sites acrossevery prompt and doc, 0 of the 399 real messages in the mailbox. So the
filter's inner branch had never once executed in production.
That alone would be harmless. What made it a bug is the direction it failed:
the untagged fall-through adopts, with a comment explaining why that is
correct. A guard that never runs and fails open is indistinguishable from one
that runs and says yes. And the single signal that would have exposed it could
not fire either —
skippedwas incremented only inside the dead branch, so theleft N foreign-repo message(s) for their own PMlog line was structurallyunreachable.
Observed 2026-09-14: this repo's PM registered and migrated 53 messages
belonging to four other repos out of
mailbox/pm/into its own inbox. Nowarning, no counter, no log line. One was live.
The fix
Route on
from:, which every message carries and which encodes the repo byconstruction (
worker-<reponame>-<slug>,reviewer-<reponame>-pr<N>,pm-<reponame>).Direction is what makes the sender the right field. Adoption candidates are
worker→pm mail, so they are all literally
to: pm— the recipient carries noinformation. This is the mirror of #201's dead-letter routing, where the sender
is also the signal, there because that mail is PM→worker and the PM that sent a
ping is who needs the report.
The test is one-sided: adopt iff
frommatchesworker-<my-repo>-*/reviewer-<my-repo>-*/pm-<my-repo>. Only one candidate name is eversubstituted into the pattern, so the hyphen-splitting ambiguity
_resolve_pm_by_slug_splithas to rank around never arises.Three outcomes now, in
_adopt_route:from:worker-?*-?*/reviewer-?*-?*/pm-?*)left N foreign-repo message(s)finally firesunknown, barepm, sluglessworker-a)repo:is kept as the higher-precedence arm for the day something startspassing
--repo. Per the spec this does not changecmd_sendto beginwriting it — a wire-format change affecting every sender, for nothing
from:doesn't already provide.
Everything else about adoption is untouched: the
pm-*-only andlive-literal-
pmguards, the fresh-register gate, the single-winner lock andits stale-lock reclaim, collision-safe migration, the
adopted-from:header,and the ordering that runs adoption before gc's piggyback — which #201 gave
a second reason to respect.
The tests are the point of the ticket
The four existing filter tests each fabricate a
repo:field via--repo.They pinned a branch production cannot reach, which is precisely why this
shipped inert and stayed that way for months.
Twelve new tests build every fixture the way
cmd_sendreally builds one —no hand-injected
repo:, senders in the real role grammar. Confirmed to failagainst the unfixed binary before the fix was applied:
The old four are kept and relabelled as what they are — coverage of the
higher-precedence arm, with a comment saying production doesn't reach it. Same
discipline as #205 for
$TASK_FORCE_HOMEand #208 forradio-env-injection:make the harness unable to lie about the thing it's checking.
Recovery sub-decision: no migration
The 53 already-misadopted messages retain their
from:and carry anadopted-from:header, so a one-shot re-route is mechanically possible. Notdoing it. All 53 are acked and their PRs are merged — re-routing would move
dead mail between mailboxes for no reader, and would add a one-shot code path
with no second use. Recording the decision rather than leaving it to omission,
as the spec asked.
Verification
./run_tests.sh— 1059 passed, 0 failed (31 inradio_legacy_pm_adopt.bats, up from 19)shellcheck -x bin/radio— cleantools/check-drift.sh— 21 groups checked, cleanDocs
CHANGELOG.md— entry under## [Unreleased], with the re-run noteclaude-*/steering/*-workflow.example.mdtemplates plus this repo's.claude/gh-workflow.mdUpgrading: re-run
task-init <loadout>in each configured repo to pick up therevised adoption paragraph. No migration needed — nothing in the mailbox
changes until the next fresh
pm-<repo>register.🤖 Generated with Claude Code