Skip to content

fix(dial): bare-nick dial surfaces the same unsendable reason wire send emits#354

Merged
laulpogan merged 2 commits into
mainfrom
fix/dial-surfaces-unsendable-reason
Jun 27, 2026
Merged

fix(dial): bare-nick dial surfaces the same unsendable reason wire send emits#354
laulpogan merged 2 commits into
mainfrom
fix/dial-surfaces-unsendable-reason

Conversation

@laulpogan

Copy link
Copy Markdown
Collaborator

What

A peer can be pinned in trust yet unsendable: its relay endpoint's slot_token stays empty until the peer's pair_drop_ack lands (common right after pairing, an MCP/daemon restart, or when we're not federation-reachable so the ack can't arrive). wire send already classifies this and returns an actionable peer_unknown reason — but both dial surfaces short-circuited a pinned peer to a bland already_pinned with no sendability check.

Result: an operator/agent whose sends were bouncing peer_unknown would re-dial the bare nick forever, seeing a reassuring already_pinned each time and never the real cause. (This is the exact loop a real cross-machine session hit — bare-nick re-dial → already_pinned → send → peer_unknown, repeat.)

Fix

Extract one shared classifier send::unsendable_reason(peer) -> Option<String> (None = a usable route exists; Some = the same reason string the send path emits) and route all three surfaces through it so they can't drift:

  • delivery_json PeerUnknown arm now calls it — output preserved exactly.
  • cmd_dial (CLI) PinnedPeer arm pushes a warning step when unsendable; the human printer shows the detail inline (WARN: …) instead of a bare warning label.
  • tool_dial (MCP) PinnedPeer arm adds sendable: bool + an optional reason. status stays already_pinned — additive, no consumer break.

The classifier mirrors the send loop's routability exactly:

  • an HTTP endpoint counts only when relay_url + slot_id + slot_token are all non-empty (matches the send loop's continue skip — a token on a malformed endpoint is not a route);
  • a peer reachable only over Nostr (recorded nostr_transport + a local nostr key, the RFC-007 D3 fallback) reads as sendable, so the dial warning never contradicts what a wire send would actually do.

Tests

send::unsendable_reason_reads_live_state covers: not-pinned → Some; usable HTTP slot → None; pinned-but-empty-token → Some("no token yet"); Nostr-reachable → None; malformed endpoint (token but empty relay_url/slot_id) → Some. Full lib suite: 611 pass, fmt + clippy -D warnings clean.

🤖 Generated with Claude Code

…end` emits

A peer can be pinned in trust yet unsendable: its relay endpoint's
slot_token stays empty until the peer's pair_drop_ack lands (common right
after pairing, an MCP/daemon restart, or when we're not federation-reachable
so the ack can't arrive). `wire send` already classifies this precisely and
returns an actionable `peer_unknown` reason. But both dial surfaces —
CLI `cmd_dial` and MCP `tool_dial` — short-circuited a pinned peer to a
bland `already_pinned` with no sendability check, so an operator/agent whose
sends were bouncing would re-dial the bare nick forever, never seeing the
cause (the loop a real cross-machine session hit).

Extract one shared classifier `send::unsendable_reason(peer) -> Option<String>`
(None = a usable route exists; Some = the same reason the send path emits) and
route all three surfaces through it so they can't drift:
- `delivery_json` PeerUnknown arm now calls it (output preserved exactly).
- `cmd_dial` PinnedPeer arm pushes a `warning` step when unsendable; the
  human printer shows the detail inline instead of a bare "warning" label.
- `tool_dial` PinnedPeer arm adds `sendable: bool` + an optional `reason`
  (status stays `already_pinned` — additive, no consumer break).

The classifier mirrors the send loop's routability exactly: an HTTP endpoint
counts only when relay_url + slot_id + slot_token are all non-empty, and a
peer reachable only over Nostr (recorded nostr_transport + a local nostr key,
the RFC-007 D3 fallback) reads as sendable — so the dial warning never
contradicts what a send would actually do.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying wireup-landing with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8bb40dd
Status: ✅  Deploy successful!
Preview URL: https://8c2400bf.wireup-landing.pages.dev
Branch Preview URL: https://fix-dial-surfaces-unsendable.wireup-landing.pages.dev

View logs

…IFIED

Follow-up to the same root cause. `resolve_name_to_target` — the shared
dial/whois resolver — passed the raw stored `tier` straight through, so a
dial or whois on a pinned-but-not-yet-acked peer reported `VERIFIED` while
`wire status`/`wire peers` (which already use `trust::effective_tier`)
reported `PENDING_ACK` for the same peer at the same moment. The dial
surfaces contradicted every other surface.

Reuse the existing state instead of inventing a parallel one: the resolver
now computes `effective_tier` (the same VERIFIED-vs-PENDING_ACK calculation
the rest of the system uses), so dial/whois/status/peers all agree. Combined
with the `sendable`/`reason` transport detail from the prior commit, a dial
now reads coherently — relationship tier AND can-I-send-now, both honest.
No change to `effective_tier` itself (a trust primitive) — only its caller.

Regression test locks it: a VERIFIED pin with an empty reply-slot token
resolves to PENDING_ACK, and flips to VERIFIED once the token lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@laulpogan laulpogan merged commit d9b47f8 into main Jun 27, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant