Summary
agent-mesh-bus/src/bus.rs::dial_reply_peer (introduced in #21, "ship replies via dial-back on the request's source address") dials a reply back to the request's source socket address. When that source is a link-local IPv6 (fe80::…) with no scope_id — which is exactly what mDNS surfaces on a CI runner and most Linux hosts — the QUIC send fails:
sendmsg error: Os { code: 22, kind: InvalidInput, message: "Invalid argument" }
dst=[fe80::…]:PORT
Dial-back then falls through to mDNS. But for a quiet-bind client (BusOptions { announce: false }) there is no mDNS record, so the reply is lost.
Impact
newt-mesh/tests/conversation_contract.rs::quiet_client_gets_replies_via_dial_back fails deterministically on hosts whose mDNS announces a link-local IPv6.
- Red on newt-agent's nightly Mesh-integration and inherited by any newt-agent PR touching the seam paths (
newt-core/**, …) — currently blocking newt-agent PRs #1112/#1113 (unrelated dormant code that merely trips the path filter).
- On
agent-mesh@main (237aa1d): dial_reply_peer (line ~607) still does endpoint.dial(pubkey, [addr]) with the single source addr, no loopback augmentation and no scope handling. No later commit addresses it.
Root cause
dial_reply_peer dials the single source addr. dial_peer already augments its candidate set with loopback (same-host works without a relay); dial_reply_peer does not — so a same-host quiet client whose only advertised path is a scopeless fe80:: is unreachable.
Proposed fix
- Minimal (greens the test + real same-host use): in
dial_reply_peer, include loopback at the source port alongside the source addr — endpoint.dial(pubkey, [addr, (V4_LOOPBACK, addr.port()), (V6_LOOPBACK, addr.port())]) — mirroring dial_peer's existing loopback rationale. iroh races them; loopback answers for same-host.
- Robust (cross-host correctness): capture the recv interface's scope_id and attach it to link-local dial-back destinations.
Coordination
Filed by Claude, authorized by Shawn, who is notifying the agent-mesh agent directly. A fix PR (minimal version, verified against the contract test in an isolated side-by-side worktree) is incoming and will reference this issue. Checked #58 (session-streams ADR), #59 (synchronous handler registration de-flake), #60 (docs) — no overlap with this transport fix.
Summary
agent-mesh-bus/src/bus.rs::dial_reply_peer(introduced in #21, "ship replies via dial-back on the request's source address") dials a reply back to the request's source socket address. When that source is a link-local IPv6 (fe80::…) with no scope_id — which is exactly what mDNS surfaces on a CI runner and most Linux hosts — the QUIC send fails:Dial-back then falls through to mDNS. But for a quiet-bind client (
BusOptions { announce: false }) there is no mDNS record, so the reply is lost.Impact
newt-mesh/tests/conversation_contract.rs::quiet_client_gets_replies_via_dial_backfails deterministically on hosts whose mDNS announces a link-local IPv6.newt-core/**, …) — currently blocking newt-agent PRs #1112/#1113 (unrelated dormant code that merely trips the path filter).agent-mesh@main(237aa1d):dial_reply_peer(line ~607) still doesendpoint.dial(pubkey, [addr])with the single source addr, no loopback augmentation and no scope handling. No later commit addresses it.Root cause
dial_reply_peerdials the single sourceaddr.dial_peeralready augments its candidate set with loopback (same-host works without a relay);dial_reply_peerdoes not — so a same-host quiet client whose only advertised path is a scopelessfe80::is unreachable.Proposed fix
dial_reply_peer, include loopback at the source port alongside the source addr —endpoint.dial(pubkey, [addr, (V4_LOOPBACK, addr.port()), (V6_LOOPBACK, addr.port())])— mirroringdial_peer's existing loopback rationale. iroh races them; loopback answers for same-host.Coordination
Filed by Claude, authorized by Shawn, who is notifying the agent-mesh agent directly. A fix PR (minimal version, verified against the contract test in an isolated side-by-side worktree) is incoming and will reference this issue. Checked #58 (session-streams ADR), #59 (synchronous handler registration de-flake), #60 (docs) — no overlap with this transport fix.