#+TITLE: agent-text relay fallback — decision record #+AUTHOR: Craig Jennings #+DATE: 2026-08-19 * What broke velox's 2026-08-13 reinstall wiped its =signal-cli= registration. Its account store came back empty. That alone would have cost velox one channel. =agent-text= made it cost the fleet, because the script hardcoded =VELOX_HOST= as its single ssh relay target. So a send from velox relayed to velox, hit the same empty store, and failed; and any third machine without a local account relayed into the same dead end. Only ratio still worked, and only because it holds the account as a linked device and takes the direct branch. The failure text said "velox down or unreachable?", which was actively misleading. velox was up and on the tailnet the whole time. It simply was not registered any more. * Why re-registering velox was not the fix velox held the *primary* registration, not a linked device, and its keys are gone. In Signal a linked device cannot authorize another device, so ratio (device 2) cannot link velox back. There is no QR to scan. Re-registering the number from velox would work, but it mints a new primary, drops ratio's link, and needs an SMS or voice code delivered to +15045173983 — and if that code cannot be received, the identity is unrecoverable. That is real downside risk taken on to restore exactly the single-primary topology that had just proved fragile. I chose the relay repoint instead. * Two proposals, one chosen Both =.emacs.d= (2026-08-14) and archsetup (2026-08-16) diagnosed this independently and proposed replacing the constant with an ordered relay list. I took archsetup's, on one difference. =.emacs.d='s version tries each host in order and stops at the first success. On velox that works — but only because ratio happens to be listed first. It never skips *self*, so the failure it fixes is still reachable: a fleet whose list led with velox would relay velox to velox exactly as before. It repairs the instance rather than the defect. archsetup's adds the guard: : [ "${host%%.*}" = "$self" ] && continue Reaching the relay branch *means* the account is not local. So an ssh round trip to this machine lands on the same store that just came back empty and cannot succeed, whatever the list order. That is the defect; the ordering was the symptom. archsetup's also makes the list overridable (=AGENT_TEXT_RELAYS=), which is a genuine improvement over both the old constant and a fixed array — the fleet assumption can be stepped around per invocation without editing the script. * The question neither proposal had answered =.emacs.d= raised the sharpest point in either handoff and left it open: a relay host that is *reachable but unregistered* looks identical to success from the caller's side unless the exit code is checked. The loop breaks on =rc -eq 0=, so if =signal-cli send= exits zero against an empty account store, the loop reports success for a message that went nowhere — the worst possible outcome for a channel whose entire job is reaching me when I am away. Answered empirically before applying, on velox, which is itself the reachable-but-unregistered case: : $ signal-cli -a +15045173983 send -m ... : User +15045173983 is not registered. : exit code: 1 So the failure is clean and the loop advances correctly. Recorded in =protocols.org= so the next reader does not have to re-derive it. * What else was stale Fixing the script exposed that =protocols.org= "Reaching Craig" asserted the same dead fact — the account "is registered on velox (primary) and ratio (linked device)". Rewritten to state the *derivation* (any machine whose local =signal-cli= holds it sends directly; check with =signal-cli listAccounts=) rather than the snapshot, per the host-identity rule. The snapshot form is what rotted; a second snapshot would rot the same way at the next rebuild. The same section's =signal-mcp= claim was also false by then: velox's global =~/.claude.json= carries only =linear= and =slack-deepsat= after the reinstall. Softened to "may or may not be registered on the machine you're on". * What the review caught The guard I chose this proposal *for* had the same shape of flaw I rejected the other proposal over, and I did not see it. It read: : [ "${host%%.*}" = "$self" ] && continue That strips the domain off the candidate host but not off =uname -n=. Both machines return a short nodename today, so it worked — and nodename is just whatever =/etc/hostname= holds. One =hostnamectl set-hostname= to an FQDN on any machine and the guard silently stops firing, restoring the 2026-08-13 bug on the away channel with nothing to announce it. So the defect fix was itself instance-dependent, which is the exact criticism I had made of .emacs.d's version one section above. Both sides are stripped now, and the test that used to assert the direction that already worked was repointed at the direction that was broken. Three more, all correct and all fixed here: - The failure message named the relay list on *both* branches, so a local =signal-cli= failure on the machine that actually holds the account — ratio, the fleet's only working sender — would send a debugger chasing the tailnet while the fault sat on the box in front of them. That is the same misdirection as the old "velox down or unreachable?" text that this change exists to remove. It now carries a per-branch reason. - The header's "Verified end to end ... 2026-08-16 (velox -> ratio, relay)" was archsetup's verification, not mine, sitting in a list of two I could vouch for and inheriting their credibility. Their handoff reports running the exact relay command from velox and getting a Signal timestamp with rc 0. That is real, and it is on report — the header now says so. - =protocols.org= and the script header both point at the runbook in =docs/design/= as authoritative, and that runbook still asserted the retired topology in four places. Correcting two surfaces and leaving the one they route to is worse than not having started: the contradiction reads as a disagreement between sources rather than one uniformly old story. The runbook now opens with a dated superseded note, and =signal-receive.sh='s header comment moved to the derivation form. * Verification - =bash -n= and =shellcheck -S warning= clean. - =agent-text.bats= 5 tests → 10, all green. The suite now stubs =uname= so the self-skip is exercised deterministically instead of depending on which machine runs it. - Mutation-proven rather than assumed. Deleting the self-relay guard reds tests 3, 4 and 9; making the loop break unconditionally reds test 5; seeding =rc=0= instead of =1= reds test 9. Each mutation is caught by a distinct test, so the suite guards the behavior rather than merely covering the lines. - Full suite: only the pre-existing =ai-launcher-runtime.bats= codex failure remains, which is a velox environment gap tracked separately. - The two review fixes are mutation-proven the same way rather than assumed: reverting the self-side strip reds the FQDN test, and making the failure message branch-blind again reds the local-failure test. Suite is 11 tests. * Provenance of the relay verification Recorded here because the working directory was filed away and this is the only surviving copy. archsetup's 2026-08-16 handoff states: "Verified from velox before proposing: signal-cli is at /usr/bin/signal-cli on ratio's non-interactive PATH, and the exact relay command agent-text would run returned a Signal timestamp with rc 0. Direct send from ratio also verified. bash -n and shellcheck -S warning are both clean on the attached file." That is the source for the header's 2026-08-16 line. I did not re-run a live relay send, because doing so delivers a real message to Craig's phone.