From 62a7fbf695405d8804220c1cf1b280ee8d612082 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 19 Aug 2026 15:02:32 -0700 Subject: fix(agent-text): relay through a host that holds the account The relay target was hardcoded to velox, and velox's reinstall wiped its signal-cli registration. A send from velox then relayed to itself, and any machine without a local account relayed into the same dead end. Only ratio still worked, because it holds the account as a linked device and sends directly. The old failure text blamed velox for being down. It was up, just unregistered. The target is now an ordered list, overridable per invocation, and the loop skips any candidate matching this machine's name. The skip is the actual fix. Reaching the relay branch means the account isn't local, so relaying to ourselves can't work whatever the list order. Both sides of that comparison are domain-stripped, or an FQDN nodename would silently disable the guard. A relay host that is reachable but unregistered exits 1, not 0, so the loop advances instead of reporting a delivery that never happened. I checked that rather than assume it. Silent non-delivery is the worst failure this tool has, and a test pins it. The failure message now names which branch failed, so a local send failure no longer sends anyone chasing the tailnet. protocols.org and the pager runbook both asserted the topology the reinstall destroyed. They now say how to derive which machine holds the account instead of naming one. The snapshot form is what rotted. The runbook's linking recipe is marked impossible for now, since the primary's keys are gone and a linked device can't authorize another. --- docs/design/2026-07-20-signal-pager-runbook.org | 113 ++++++++++----- ...26-08-19-agent-text-relay-fallback-decision.org | 155 +++++++++++++++++++++ 2 files changed, 237 insertions(+), 31 deletions(-) create mode 100644 docs/design/2026-08-19-agent-text-relay-fallback-decision.org (limited to 'docs/design') diff --git a/docs/design/2026-07-20-signal-pager-runbook.org b/docs/design/2026-07-20-signal-pager-runbook.org index f31ed18..393c1c6 100644 --- a/docs/design/2026-07-20-signal-pager-runbook.org +++ b/docs/design/2026-07-20-signal-pager-runbook.org @@ -7,26 +7,42 @@ phone, how his replies come back, how the account stays healthy, and the signal-cli setup behind it. This is the Signal successor to the retired ntfy runbook. Canonical home is rulesets because the pager is cross-machine tooling. -* What the pager is +* SUPERSEDED 2026-08-19 — the topology below is out of date + +velox's 2026-08-13 reinstall wiped its signal-cli registration, so everything +this runbook says about velox holding the primary is now false. I am flagging +it here rather than rewriting the whole document. Treat everything below as +predating the 2026-08-19 change except where a dated note says otherwise: the +send, reply-reading and setup sections all name velox specifically, and I have +marked each one inline rather than asking you to remember this banner four +screens later. + +Current, and stated as a derivation rather than a snapshot, since the snapshot +form is exactly what rotted: *any* machine whose local signal-cli holds the +identity sends directly, and a machine without it ssh-relays to the first +listed host that does. Check with =signal-cli listAccounts=. As of 2026-08-19 +that is ratio only. See +[[file:2026-08-19-agent-text-relay-fallback-decision.org]] for what broke, why +re-registering velox was not the fix, and the measurements behind the current +=agent-text= dispatch. -One Signal identity, =+15045173983=, registered in *velox's* signal-cli -(account file 465310, velox is the primary device). It is a dedicated pager -number, not Craig's personal Signal. Pages go *from* that identity *to* Craig's -own Signal account, which fires a normal mobile push on his phone. +* What the pager is -As of 2026-07-20 the identity spans two devices: velox (primary) and ratio -(linked device "ratio-pager"). Any machine holding the account sends directly; -a machine that doesn't relays to velox over the tailnet. +One Signal identity, =+15045173983=. It is a dedicated pager number, not +Craig's personal Signal. Pages go *from* that identity *to* Craig's own Signal +account, which fires a normal mobile push on his phone. Two constants the tooling depends on: -- Pager account: =+15045173983= (primary on velox, linked on ratio). +- Pager account: =+15045173983=. - Recipient: Craig's Signal account UUID =b1b5601e-6126-47f8-afaa-0a59f5188fde=. His phone *number* reads as unregistered in Signal's directory — always target the UUID, never the number. -velox is the laptop that travels with Craig, so the pager account rides with -him; ratio holds it too, so a page still lands when velox is down. +Which machines hold it is runtime state, not a constant — see the superseded +note above. Historical record: as of 2026-07-20 the identity spanned velox +(primary, account file 465310) and ratio (linked device "ratio-pager"). velox's +half is gone. * Choosing a channel @@ -63,16 +79,26 @@ local signal-cli: desktop fallback line and exits non-zero, so a caller can tell the page did not land. -The raw command it runs, for reference or a manual send from velox: +*Superseded 2026-08-19.* The relay and failure bullets above (the last two) +describe the single-host dispatch that was replaced. =agent-text= now walks an ordered list (=AGENT_TEXT_RELAYS=, +ratio first), skips any candidate matching =uname -n=, and advances past a host +that is reachable but does not hold the account. The failure line no longer +blames velox by name and distinguishes a local send failure from relay +exhaustion. + +The raw command it runs, for reference or a manual send from a machine that +holds the account: #+begin_src bash signal-cli -a +15045173983 send -m "your message" b1b5601e-6126-47f8-afaa-0a59f5188fde #+end_src -From another machine, the same send relayed over the tailnet: +From another machine, the same send relayed over the tailnet — *substitute a +host that actually holds the account* (=signal-cli listAccounts=); as of +2026-08-19 velox does not, so the example below is written against ratio: #+begin_src bash -ssh velox.tailf3bb8c.ts.net \ +ssh ratio.tailf3bb8c.ts.net \ "signal-cli -a +15045173983 send -m 'your message' b1b5601e-6126-47f8-afaa-0a59f5188fde" #+end_src @@ -87,11 +113,15 @@ queue until something receives it. Drain the queue and read what is there: +*Superseded 2026-08-19:* these named velox, which no longer holds the account. +Run the drain on a machine that does — =signal-cli listAccounts= names it; as of +2026-08-19 that is ratio. + #+begin_src bash -# On velox: +# On the machine holding the account: signal-cli -a +15045173983 receive --timeout 10 -# From another machine: -ssh velox.tailf3bb8c.ts.net "signal-cli -a +15045173983 receive --timeout 10" +# From any other machine: +ssh ratio.tailf3bb8c.ts.net "signal-cli -a +15045173983 receive --timeout 10" #+end_src =receive= prints every queued envelope and exits 0 once the queue drains or the @@ -116,10 +146,21 @@ account is a reliability risk on the one channel that reaches Craig when he is away. The fix mirrors roam-sync: a systemd user timer that drains the queue on a -cadence, keeping the account warm and, as a bonus, picking up async replies. With -the account linked on both machines, each device wants its own regular receive, -so the timer runs on *both* velox and ratio (the shared =common= dotfiles -package, same home as roam-sync). +cadence, keeping the account warm and, as a bonus, picking up async replies. Each +device *holding* the account wants its own regular receive, so the timer runs on +every daily driver (the shared =common= dotfiles package, same home as +roam-sync). It no-ops cleanly where the account is *genuinely* absent, which is why velox +running it since 2026-08-13 is harmless rather than an error — the 2026-07-20 +rationale here said "linked on both machines", which no longer holds even though +the arrangement it justifies still does. + +*Do not read that no-op as proof of health.* The guard is +=signal-cli listAccounts 2>/dev/null | grep -q=, which cannot tell "the account +is not on this machine" from "listAccounts failed" — the redirect discards the +error and grep simply finds nothing either way. On 2026-08-19 that was masking a +real outage on ratio, the only machine holding the account: a PATH version skew +made the timer run signal-cli 0.14.5 against a database 0.14.7 had upgraded, so +every 15-minute fire reported success while receiving nothing, for 17 days. - Script: =scripts/signal-receive.sh= (rulesets, so both machines get it on =git pull=). It no-ops cleanly on a machine that lacks the account. @@ -140,25 +181,35 @@ systemctl --user status signal-receive.service # confirm a clean receive * signal-cli setup notes - *Version:* signal-cli 0.14.5 on velox (2026-07-20). -- *Accounts:* velox's signal-cli holds the pager identity =+15045173983= as the - registered primary (account file 465310). ratio's signal-cli holds two +- *Accounts:* /as of 2026-07-20; velox's half is gone — its 2026-08-13 reinstall + wiped the registration./ velox's signal-cli holds the pager identity + =+15045173983= as the registered primary (account file 465310). ratio's signal-cli holds two accounts: Craig's personal number =+15103169357= (its own primary, note-to-self only — no phone push) and the pager identity as a *linked device* (Device 2, "ratio-pager", linked 2026-07-20). Both accounts coexist; target the pager with =-a +15045173983=. A future daily driver joins the same way. -- *signal-mcp:* on velox, Claude sessions may also expose a =signal-mcp= tool - (=send_message_to_user=, same pager identity) configured in velox's global - =~/.claude.json=. It works there but is invisible from any other machine and - from non-Claude runtimes, so =agent-text= is the portable habit. The old +- *signal-mcp:* /superseded 2026-08-19 — this said the tool is configured in + velox's global Claude config and "works there". It is not registered there any + more; that config now lists only =linear= and =slack-deepsat=./ Treat + =signal-mcp= as a per-machine nicety that may or may not be present, and use + =agent-text= as the portable habit. The old =page-signal= shell script was removed 2026-06-12 — do not resurrect it. -- *Linking a device:* to add a second signal-cli as a linked device of the pager - account (see the open decision below), provision it from the new machine and - approve the link from the account holder: +- *Linking a device:* /superseded 2026-08-19 — nobody can run this today./ The + recipe below needs the *primary* to approve the link, and the primary lived on + velox whose keys the reinstall destroyed. A linked device cannot authorize + another, so ratio cannot approve one either. Until the number is re-registered + there is no machine that can execute this, and re-registering is not free — it + mints a new primary, drops ratio's existing link, and needs an SMS or voice + code delivered to the pager number, which is unrecoverable if that code cannot + be received. Read + [[file:2026-08-19-agent-text-relay-fallback-decision.org]] before attempting + it; it carries that tradeoff in full. Kept here as the procedure to follow + *once* a primary exists again: #+begin_src bash # On the new machine — prints a tsdevice:/ URI (render as QR to approve): signal-cli link -n "ratio-pager" - # Approve from velox (the primary device): + # Approve from the machine holding the PRIMARY registration: signal-cli -a +15045173983 addDevice --uri "tsdevice:/?uuid=…" #+end_src diff --git a/docs/design/2026-08-19-agent-text-relay-fallback-decision.org b/docs/design/2026-08-19-agent-text-relay-fallback-decision.org new file mode 100644 index 0000000..48dd572 --- /dev/null +++ b/docs/design/2026-08-19-agent-text-relay-fallback-decision.org @@ -0,0 +1,155 @@ +#+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. -- cgit v1.2.3