aboutsummaryrefslogtreecommitdiff
path: root/docs/design/2026-07-20-signal-pager-runbook.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/2026-07-20-signal-pager-runbook.org')
-rw-r--r--docs/design/2026-07-20-signal-pager-runbook.org249
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".