diff options
Diffstat (limited to 'docs/design/2026-07-20-signal-pager-runbook.org')
| -rw-r--r-- | docs/design/2026-07-20-signal-pager-runbook.org | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/docs/design/2026-07-20-signal-pager-runbook.org b/docs/design/2026-07-20-signal-pager-runbook.org new file mode 100644 index 0000000..393c1c6 --- /dev/null +++ b/docs/design/2026-07-20-signal-pager-runbook.org @@ -0,0 +1,249 @@ +#+TITLE: Signal Pager Runbook +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-20 + +The operational reference for the agent pager — how a page reaches Craig's +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. + +* 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. + +* What the pager is + +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=. +- 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. + +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 + +Two trigger words, two channels, and both work from any agent runtime (nothing +here is Claude-specific). protocols.org "Reaching Craig" is the short version +pointed at every project; this runbook is the full one for the Signal side. + +- *"page me"* — desktop notification, stays up until dismissed: + + #+begin_src bash + notify info "Title" "Message" --persist + #+end_src + +- *"text me"* — the phone, over Signal: + + #+begin_src bash + agent-text "Message for Craig's phone" + #+end_src + +- *"text and page me"* — both. The default when a run can't tell whether he's + away: the desktop one is free and the phone one reaches him if he is. + +* Sending a text + +=agent-text= (shipped at =claude-templates/bin/agent-text=, installed to +=~/.local/bin= by =make -C ~/code/rulesets install=) is the interface. It hides +the machine topology by checking whether the account is registered in the +local signal-cli: + +- If the account is local (velox's primary or a linked device like ratio), it + sends directly — no velox dependency. +- Otherwise it ssh-relays the send to velox over the tailnet. +- On failure (velox down or unreachable from a non-linked machine) it prints the + desktop fallback line and exits non-zero, so a caller can tell the page did not + land. + +*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 — *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 ratio.tailf3bb8c.ts.net \ + "signal-cli -a +15045173983 send -m 'your message' b1b5601e-6126-47f8-afaa-0a59f5188fde" +#+end_src + +Prefer =agent-text= over the raw command — it hardens the message for the remote +shell and handles the fallback. Reach for the raw form only when debugging. + +* Reading replies + +Craig replies to a page straight from Signal on his phone. The reply is a normal +data message *to* the pager account, so it is waiting in the pager's inbound +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 the machine holding the account: +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 +timeout elapses. A text reply from Craig arrives as an envelope from his UUID +carrying a =Body:= line — that line is the reply text. Most envelopes are +delivery/read receipts and typing indicators (no =Body:=); the reply you want is +the data message with body text. For a script that waits on a reply, add +=--send-read-receipts= so his phone shows the page was read, and parse stdout for +the =Body:= line on an envelope from =b1b5601e-…=. + +Note: =receive= is destructive — it consumes the queue. Whatever drains the +queue (an on-demand read, or the warm-keeping timer below) is what sees the +reply, and it is seen once. An agent that pages and then waits for an answer +should do its own =receive= rather than race the timer. + +* Keeping the account warm (receive timer) + +Signal expects a registered account to receive regularly. Left alone, the pager +account drifts stale — signal-cli warns "Messages have been last received N days +ago" (observed at 47 days on 2026-07-20 before a manual drain reset it). A stale +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. 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. +- Units: =scripts/signal-receive.service= + =.timer= (reference copies under + =scripts/systemd/=; the stowed copies live in =common/.config/systemd/user/= + of the dotfiles repo, so both machines get them). +- Cadence: every 15 minutes (=OnUnitActiveSec=15min=), matching roam-sync. + +Enable on each machine (one-time, per daily-drivers.md's one-time-setup class): + +#+begin_src bash +# After the dotfiles + rulesets pull, on each daily driver: +systemctl --user daemon-reload +systemctl --user enable --now signal-receive.timer +systemctl --user status signal-receive.service # confirm a clean receive +#+end_src + +* signal-cli setup notes + +- *Version:* signal-cli 0.14.5 on velox (2026-07-20). +- *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:* /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:* /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 the machine holding the PRIMARY registration: + signal-cli -a +15045173983 addDevice --uri "tsdevice:/?uuid=…" + #+end_src + +* Decision — linked device (2026-07-20) + +The topology question — ssh-relay only vs. registering daily drivers as linked +devices — was decided in favor of linked devices. ssh-relay only was simpler +(one identity, one receive point) but had a single point of failure: a page +failed when velox was down or off the tailnet. + +Registering ratio as a linked device removes that: ratio sends directly, so a +page lands even when velox is down. The costs, both paid: linked-device +provisioning per machine (the =link= / =addDevice= handshake above), and each +device wanting its own regular =receive= — so the warm-keeping timer moved from a +velox-only home to the shared =common= package, running on both. + +Adding another daily driver later is the same handshake plus a dotfiles stow; +the timer and =agent-text= already generalize to "any machine holding the +account." + +* History + +- 2026-07-04 — home retired ntfy (self-hosted on ratio) and tore it down, + switching agent paging to Signal. Handoff to rulesets to document and own. +- 2026-07-13 — reconciled to one pager identity on velox; =agent-page= shipped + (direct on velox, ssh-relay elsewhere, desktop fallback); protocols.org "Paging + Craig" rewritten around the two channels. +- 2026-07-20 — this runbook; receive-timer script + units added; a manual drain + cleared the 47-day staleness live; ratio linked as a device of the pager + account and its direct send verified; the tool (still named =agent-page= that + morning) generalized to send directly from any machine holding the account; + receive timer moved to the shared =common= package and enabled on both machines. +- 2026-07-20 (later) — notification vocabulary split: "page me" is the desktop + channel, "text me" is Signal, "text and page me" is both. The tool was renamed + =agent-page= → =agent-text= to match, with a deprecated =agent-page= shim + delegating to it. protocols.org section renamed "Paging Craig" → "Reaching + Craig". |
