Skip to content

waved: answer inbound RPCs to the envelope sender - #1110

Open
ellemouton wants to merge 4 commits into
mainfrom
agent/waved-reply-to-sender
Open

waved: answer inbound RPCs to the envelope sender#1110
ellemouton wants to merge 4 commits into
mainfrom
agent/waved-reply-to-sender

Conversation

@ellemouton

@ellemouton ellemouton commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

A response belongs to whoever sent the request. handleInboundRPC was addressing its response to the request's Rpc.ReplyTo verbatim, so a request could name a reply destination that disagrees with where it came from.

What changes

Recipient: env.Rpc.ReplyTo becomes Recipient: env.Sender.

Nothing changes for a well-formed peer. Every producer in this repo sets Sender and Rpc.ReplyTo to the same LocalMailboxID:

  • serverconn/unary_facade.go:129/:139
  • serverconn/actor.go:860/:869 and :1029/:1039
  • serverconn/heartbeat.go:102/:110

The operator's clientconn equivalents do the same. So the two fields already agree everywhere they are produced, and addressing the sender is the same destination by a more direct route.

It also fixes a dropped response

An absent ReplyTo produced an empty Recipient. The mailbox store rejects an empty recipient outright, so a caller that left the field unset lost its answer entirely rather than receiving it. That case now answers the sender like any other.

Testing

TestHandleInboundRPCAnswersTheSender covers matching, absent, and divergent ReplyTo. It dispatches a deliberately unregistered method so the handler takes its error path — that path still sends a response envelope, so it still has to address one, and it is the easier of the two to get wrong.

Verified the two changed cases (absent and divergent) fail against the previous behaviour and the matching case passes, so the test pins the change rather than restating it.

make unit pkg=waved, make lint-changed-local (0 issues) and make commitmsg-lint all pass.

@ellemouton

Copy link
Copy Markdown
Contributor Author

@claude please review this PR.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @ellemouton's task in 2m 36s —— View job


PR Review: answer inbound RPCs to the envelope sender

  • Read the PR diff and surrounding context
  • Verify the claims about producers (Sender vs ReplyTo)
  • Assess the test
  • Post review

Verdict: LGTM. The change is small, correct, and well-justified. The reasoning in the commit and the inline comment holds up under inspection.


Correctness — verified

waved/server.go:3624 swaps Recipient: env.Rpc.ReplyToRecipient: env.Sender. I confirmed the "every producer sets them equal" claim by reading the producers:

  • serverconn/unary_facade.go:129 Sender: cfg.LocalMailboxID / :139 ReplyTo: cfg.LocalMailboxID
  • serverconn/heartbeat.go:102 / :110 — same pair
  • serverconn/actor.go:861 / :1030 Recipient: cfg.RemoteMailboxID with matching Sender/ReplyTo = LocalMailboxID

So for a well-formed peer the destination is unchanged, and the change removes a request's ability to name a reply destination that disagrees with its own origin. Good.

The dropped-response fix is real: an absent ReplyTo previously yielded an empty Recipient, and the response path terminates in Edge.Send on the NonTxRoutes hoisted path — an empty recipient can't be delivered, so the caller silently lost its answer. Addressing env.Sender fixes that class.

Security angle is a net positive

NewAuthenticatedMailboxClient.Send (serverconn/mailbox_auth_rpc.go:44-49) derives the auth context from req.Envelope.Recipient. Routing to env.Sender — the authenticated origin the operator stamped — is a more trustworthy routing key than a caller-supplied ReplyTo, so this tightens rather than loosens the trust model.

Test — solid

TestHandleInboundRPCAnswersTheSender is a good pin:

  • Driving an unregistered method to force the error path is the right call — that path still constructs and addresses a response, and it's the easier one to get subtly wrong.
  • The three cases (matching / absent / divergent) mean the two new cases fail under the old ReplyTo behavior and pass under the new one, so it genuinely pins the change rather than restating it.
  • The recording edge cleanly isolates the assertion to Recipient.

Minor / non-blocking observations

  1. Rpc.ReplyTo is now effectively ignored by the responder while every producer still sets it. That's fine for this PR, but the field is now redundant on the response path. Worth a short note somewhere (or a follow-up) that ReplyTo is advisory-only from the responder's perspective, so a future reader doesn't assume it's still honored.

  2. serverconn/e2e_test.go:208 — the testServer double still models the old behavior (Recipient: env.Rpc.ReplyTo). It's the server-side mock and Sender == ReplyTo there, so it keeps working, but mirroring the production handler (Recipient: env.Sender) would keep the double honest against future divergence. Purely optional.

  3. Response Sender isn't exercised. newCompatTestServer leaves s.localMailboxID unset, so the response's Sender is empty in the test. The assertion is only on Recipient, so this is fine — just noting the test doesn't cover the Sender side of the envelope.

None of these block merge.
· agent/waved-reply-to-sender

@ellemouton

Copy link
Copy Markdown
Contributor Author

Thanks — all three addressed in c84f966, none behavioural.

1. ReplyTo is advisory from the responder. Said so in the comment. You are right that leaving it unstated invites a future reader to assume it is still honoured when nothing on this path reads it.

2. serverconn/e2e_test.go double. Mirrored to Recipient: env.Sender, with a note saying why. Agreed this is the more valuable of the three: the double kept working only because Sender == ReplyTo there, so it would have gone on modelling the old behaviour indefinitely and quietly diverged.

3. Response Sender unexercised. Good catch — newCompatTestServer left localMailboxID unset, so the test pinned one half of the envelope. Now sets it and asserts Sender alongside Recipient.

Also useful: the NewAuthenticatedMailboxClient.Send observation (serverconn/mailbox_auth_rpc.go:44-49 deriving auth context from req.Envelope.Recipient). I had argued this was neutral-to-safer on trust; deriving the auth context from the recipient makes it concretely tighter, since the recipient is now the authenticated origin rather than a caller-supplied value. Worth having on the record.

make unit pkg=waved, make unit pkg=serverconn, make lint-changed-local (0 issues) and make commitmsg-lint all pass.

handleInboundRPC addressed its response to the request's Rpc.ReplyTo
verbatim. A response belongs to whoever sent the request, so the
sender is the authoritative destination and is what we address now.

The two agree for every producer in this repo: serverconn's
unary_facade, actor and heartbeat paths all set Sender and
Rpc.ReplyTo to the same LocalMailboxID, as do the operator's
clientconn equivalents. So this changes nothing for a well-formed
peer. What it removes is a request's ability to name a reply
destination that disagrees with where the request came from.

It also fixes an absent ReplyTo, which produced an empty Recipient.
The mailbox store rejects that outright, so a caller that left the
field unset lost its answer rather than receiving it.

TestHandleInboundRPCAnswersTheSender covers all three cases through
the handler's error path, which still sends a response and so still
has to address one.
Three follow-ups from review, none behavioural.

Say in the comment that Rpc.ReplyTo is advisory from the responder's
point of view. Producers still set it and nothing on this path reads
it, which is worth stating so a future reader does not assume it is
still honoured.

Mirror the change in serverconn's testServer double, which still
modelled the old behaviour. Sender and ReplyTo are equal there, so it
kept working either way; matching the production handler keeps the
double honest if they ever diverge.

Assert the response Sender as well. newCompatTestServer left
localMailboxID unset, so the test only ever pinned one half of the
envelope.
@ellemouton
ellemouton force-pushed the agent/waved-reply-to-sender branch from c84f966 to fa52ed8 Compare August 10, 2026 20:46

@litbot-9000 litbot-9000 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FINAL — The change is correct and I could not break it. Recipient: env.Rpc.ReplyTo -> Recipient: env.Sender is behaviour-preserving for every well-formed peer, and it is a small hardening win. Two findings, neither a blocker, both about gaps around the change rather than the change itself.

Is env.Sender the right reply target? Yes. I traced every producer of a KIND_REQUEST in this repo, and each sets Sender and Rpc.ReplyTo to the same LocalMailboxID: serverconn/heartbeat.go:102/:110, serverconn/actor.go:860/:869, serverconn/actor.go:1029/:1039, serverconn/unary_facade.go:129/:139. There is no proxying, relaying or fan-out consumer of ReplyTo anywhere — nothing in the repo reads Rpc.ReplyTo at all outside these producers, so the author's "advisory from the responder's point of view" framing is accurate. This is not a regression risk.

It is also mildly better on trust grounds: ReplyTo lets a request nominate a destination that disagrees with where the request came from, and answering env.Sender removes that. Test case 3 ("reply-to naming another mailbox") pins exactly this — under the old code that response went to somebody-else.

Does the new test actually fail against the old code? Yes — I proved it rather than trusting it. I patched waved/server.go back to Recipient: env.Rpc.ReplyTo on top of the PR head and re-ran:

--- FAIL: .../absent_reply-to                    expected "operator-1", actual ""
--- FAIL: .../reply-to_naming_another_mailbox    expected "operator-1", actual "somebody-else"
--- PASS: .../matching_reply-to

2 of 3 subcases discriminate; the third is a deliberate control. That is real coverage. Worktree restored afterwards.

Checks: go vet ./waved/ clean. go test ./waved/ ./serverconn/ -count=1 both pass (18.8s / 5.6s).

One claim I could not verify. The new comment says an empty recipient is "rejected outright" by the mailbox store. The store is the operator's (lumos), which is not vendored here, so I could not confirm it in this tree — and the new test's recordingReplyEdge accepts every envelope unconditionally, so the test does not establish the drop either. I believe the claim is true, but nothing in this PR demonstrates it. Finding 1 is what makes that gap matter: because SendResponse.Status is discarded, whether the store rejects an empty recipient is precisely the thing waved cannot observe.

Inherited from my own earlier interim run and NOT re-verified here (the lumos checkout that run used is gone from this container, so treat as a lead, not evidence): on the operator side, clientconn/unary_facade.go:113/:118 and clientconn/actor.go:709/:716 likewise set Sender == ReplyTo, and lumos authenticates Envelope.Sender on Send against the TLS leaf fingerprint bound at Schnorr-verified registration (server_mtls.go:588, :740-753, server_autoregister.go:242) while nothing validates Rpc.ReplyTo. If that holds, it makes Sender the strictly better-attested field and strengthens the case for this change. It does not affect either finding below.

Verdict rationale: COMMENT, not REQUEST_CHANGES. The PR does what it says. Finding 1 is a genuine bug but pre-existing and only adjacent — it is flagged because the PR's own comment asserts the lost-answer problem is fixed, and it is only narrowed. Finding 2 is a three-line guard.

Comment thread waved/server.go
//
// It also fixes an absent ReplyTo. That used to produce an
// empty recipient, which the mailbox store rejects outright,
// so the caller lost its answer entirely.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major — This comment claims more than the change delivers. Removing the empty-ReplyTo cause is right, but the silence is what made that bug expensive, and the silence is still here.

SendResponse.Status is this codebase's canonical application-level failure channel — mailboxconn.StatusError, StatusMailboxVersionUnsupported, StatusArkVersionMismatch, StatusUpgradeRequired all travel that way, never as a transport error. Three of the four Edge.Send call sites in the repo fold it correctly through edgeResponseError (serverconn/unary_facade.go:143, serverconn/actor.go:1109, serverconn/heartbeat.go:117). This one, at line 3644, is the only one that discards it:

_, err = edge.Send(ctx, &mailboxpb.SendRequest{
        Envelope: responseEnv,
})
if err != nil {
        return fmt.Errorf("send RPC response: %w", err)
}

So any rejection that arrives as Status.Ok=false — a version mismatch, an unknown recipient, whatever the operator adds next — returns nil here. handleInboundRPC reports success, the ingress loop acks the request as dispatched, and the operator's waiter blocks until it times out. Nothing is logged on either side. That is the same failure mode the empty-ReplyTo bug had; this PR removed one cause of it and left the detector missing, so the next cause will be just as invisible.

This also makes the claim on these lines unfalsifiable from inside waved: whether the store really "rejects outright" is exactly what this send path cannot see. (The new test's recordingReplyEdge accepts everything, so it does not establish the rejection either.)

Suggest folding this send into the same helper as its three siblings:

resp, err := edge.Send(ctx, &mailboxpb.SendRequest{
        Envelope: responseEnv,
})
if sErr := edgeResponseError("send rpc response", resp, err); sErr != nil {
        return sErr
}

(edgeResponseError is unexported in serverconn, so this needs a small exported wrapper — worth it to stop waved being the one send path that cannot see a rejection.)

To be clear: this is pre-existing and not introduced here, and I am not blocking on it. But the comment at these lines asserts the lost-answer problem is fixed, and with the status still dropped it is only narrowed.

Comment thread waved/server.go
// Rpc.ReplyTo is therefore advisory from the responder's
// point of view: producers still set it, and nothing here
// reads it.
Recipient: env.Sender,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 minorenv.Sender is now the sole input that decides where the answer goes, and it is the one field this function does not guard.

The preamble checks its two siblings:

if env.Rpc == nil { ... }
if env.Body == nil { ... }

and serverconn's ingress validation (validateInboundEnvelope, serverconn/inbound_version.go:18) only compares the two version fields — it never looks at Sender. I grepped the inbound path: nothing anywhere asserts env.Sender != "".

So an inbound KIND_REQUEST with an empty Sender produces exactly the empty-Recipient response this PR set out to eliminate. The hazard moved from Rpc.ReplyTo to Sender rather than going away — and combined with the dropped SendResponse.Status above, the loss is silent. The new test covers absent ReplyTo but never absent Sender, so the one case that is still broken is the one case not exercised.

A third guard alongside the existing two closes it, and turns a silent drop into a dispatch error the ingress loop logs:

if env.Sender == "" {
        return fmt.Errorf("missing envelope sender")
}

Worth a fourth table entry in TestHandleInboundRPCAnswersTheSender asserting the handler errors and sends nothing.

The daemon's inbound-RPC responder was the one Edge.Send call site in
the repo that discarded SendResponse.Status. That status is the
mailbox's application-level failure channel: a rejected recipient or a
version mismatch arrives as Status.Ok=false, never as a transport
error. Discarding it meant handleInboundRPC returned nil for a response
that was never delivered, the ingress loop acked the request as
dispatched, and the caller blocked on its correlation ID until it timed
out, with nothing logged on either side.

Fold the status through the same check its three siblings already use.
edgeResponseError is unexported, so add a small exported wrapper for
the send case rather than re-implementing the check in waved.

Also guard an empty envelope Sender. Sender is now the sole input
deciding where an answer goes, and nothing upstream asserts it: the
ingress validation only compares the two version fields. An empty one
therefore produces the same empty recipient the absent-ReplyTo case
used to, so refuse it alongside the existing nil-Rpc and nil-Body
checks and let the ingress loop log the dispatch error.
@litbot-9000

Copy link
Copy Markdown
Collaborator

📚 Doc gardening advisory

This PR's Go changes in serverconn and waved leave those packages' CLAUDE.md/AGENTS.md stale: the new exported serverconn.SendResponseError helper is undocumented, and waved's inbound-RPC response routing now answers env.Sender (rather than env.Rpc.ReplyTo) and folds the mailbox send status into its error — neither invariant is recorded.

diff --git a/serverconn/AGENTS.md b/serverconn/AGENTS.md
index a88da6d0..433d8be7 100644
--- a/serverconn/AGENTS.md
+++ b/serverconn/AGENTS.md
@@ -31,6 +31,14 @@ background ingress polling with event routing.
 - `NewAuthenticatedMailboxClient` — `mailboxpb.MailboxServiceClient` decorator
   that signs and attaches the `x-mailbox-auth-sig` header to every `Send`
   before forwarding to the wrapped edge transport.
+- `SendResponseError` — Exported wrapper over the package-internal
+  `edgeResponseError` (`compatibility.go`) for the one `Edge.Send` call site
+  outside this package: `waved` answering an inbound mailbox RPC on its own
+  edge client. Maps a `(*mailboxpb.SendResponse, error)` pair to the single
+  error the caller should return — transport error wrapped with `op`, nil
+  response reported as a transport error, non-OK `Status` converted to a
+  `*mailboxconn.StatusError` — or nil when the send was accepted. It does not
+  drive the incompatibility transition; callers still pass the error on.
 - `AckState` — Four-cursor watermark state machine (PullCursor, DispatchCommittedTo, AckTarget, AckCommittedTo).
 - `SendUnaryRequest` — Durable typed unary request that becomes a real unary RPC after commit. The response arrives via KIND_RESPONSE and, if no in-memory waiter exists, falls back to durable route dispatch via the EventRouter.
 - `DurableUnaryRequestBuilder` — Interface for proof-gated request-body construction. Implementations build the actual proto request (e.g., with signed proofs) at send time, not at persist time. The interface is provided via `ConnectorConfig.DurableUnaryBuilder`.
@@ -84,6 +92,12 @@ background ingress polling with event routing.
   the logical request (see `mailboxrpc.Retry`). `SendRPC` mints a fresh key
   only when the caller leaves `RPCOptions.IdempotencyKey` empty, which is
   correct for a single-shot call and defeats deduplication for a retry.
+- A mailbox rejection (unknown recipient, empty recipient, version mismatch)
+  travels back as `SendResponse.Status.Ok = false`, **not** as a gRPC error,
+  so no `Edge.Send` caller may discard the response and check only `err` —
+  that reports a dropped envelope as a successful dispatch. Inside the
+  package every path routes through `edgeResponseError`; outside it, use the
+  exported `SendResponseError`.
 - Ingress loop checkpoints pull cursor and ack state; on restart, resumes from checkpoint.
 - `DurableUnaryQuery` values are handled generically in `ServerConnectionActor.Receive` via `buildDurableUnary`: the query is converted to a `SendUnaryRequest` using the configured `DurableUnaryRequestBuilder`. Adding a new durable indexer query type requires only implementing `DurableUnaryQuery` — no new `Receive` case is needed.
 - `DurableUnaryQuery` implementations must produce stable identity bytes in `BuildBody` so that `MsgID` and `IdempotencyKey` are deterministic across restarts (auto-derived via `mailboxconn.StableEventMsgID` / `StableEventIdempotencyKey` when the caller leaves them empty).
diff --git a/serverconn/CLAUDE.md b/serverconn/CLAUDE.md
index a88da6d0..433d8be7 100644
--- a/serverconn/CLAUDE.md
+++ b/serverconn/CLAUDE.md
@@ -31,6 +31,14 @@ background ingress polling with event routing.
 - `NewAuthenticatedMailboxClient` — `mailboxpb.MailboxServiceClient` decorator
   that signs and attaches the `x-mailbox-auth-sig` header to every `Send`
   before forwarding to the wrapped edge transport.
+- `SendResponseError` — Exported wrapper over the package-internal
+  `edgeResponseError` (`compatibility.go`) for the one `Edge.Send` call site
+  outside this package: `waved` answering an inbound mailbox RPC on its own
+  edge client. Maps a `(*mailboxpb.SendResponse, error)` pair to the single
+  error the caller should return — transport error wrapped with `op`, nil
+  response reported as a transport error, non-OK `Status` converted to a
+  `*mailboxconn.StatusError` — or nil when the send was accepted. It does not
+  drive the incompatibility transition; callers still pass the error on.
 - `AckState` — Four-cursor watermark state machine (PullCursor, DispatchCommittedTo, AckTarget, AckCommittedTo).
 - `SendUnaryRequest` — Durable typed unary request that becomes a real unary RPC after commit. The response arrives via KIND_RESPONSE and, if no in-memory waiter exists, falls back to durable route dispatch via the EventRouter.
 - `DurableUnaryRequestBuilder` — Interface for proof-gated request-body construction. Implementations build the actual proto request (e.g., with signed proofs) at send time, not at persist time. The interface is provided via `ConnectorConfig.DurableUnaryBuilder`.
@@ -84,6 +92,12 @@ background ingress polling with event routing.
   the logical request (see `mailboxrpc.Retry`). `SendRPC` mints a fresh key
   only when the caller leaves `RPCOptions.IdempotencyKey` empty, which is
   correct for a single-shot call and defeats deduplication for a retry.
+- A mailbox rejection (unknown recipient, empty recipient, version mismatch)
+  travels back as `SendResponse.Status.Ok = false`, **not** as a gRPC error,
+  so no `Edge.Send` caller may discard the response and check only `err` —
+  that reports a dropped envelope as a successful dispatch. Inside the
+  package every path routes through `edgeResponseError`; outside it, use the
+  exported `SendResponseError`.
 - Ingress loop checkpoints pull cursor and ack state; on restart, resumes from checkpoint.
 - `DurableUnaryQuery` values are handled generically in `ServerConnectionActor.Receive` via `buildDurableUnary`: the query is converted to a `SendUnaryRequest` using the configured `DurableUnaryRequestBuilder`. Adding a new durable indexer query type requires only implementing `DurableUnaryQuery` — no new `Receive` case is needed.
 - `DurableUnaryQuery` implementations must produce stable identity bytes in `BuildBody` so that `MsgID` and `IdempotencyKey` are deterministic across restarts (auto-derived via `mailboxconn.StableEventMsgID` / `StableEventIdempotencyKey` when the caller leaves them empty).
diff --git a/waved/AGENTS.md b/waved/AGENTS.md
index b778f21c..f5aa6530 100644
--- a/waved/AGENTS.md
+++ b/waved/AGENTS.md
@@ -136,6 +136,24 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
   exceeds `OORConfig.MaxTransientSubmitRetry` (default 1h), persisting the
   window start (`FirstRejectUnixNanos`) in the outgoing snapshot (version 5)
   so the bound survives restarts.
+- `handleInboundRPC` (the mux bridge registered by `buildRPCDispatchers` for
+  every `NonTxRoutes` dispatcher) addresses its response envelope to
+  `env.Sender`, **not** to `env.Rpc.ReplyTo`. Every producer in this repo sets
+  both to the same `LocalMailboxID` (`serverconn/unary_facade.go`,
+  `serverconn/actor.go`, `serverconn/heartbeat.go`, and the operator's
+  `clientconn` equivalents), so the two agree for a well-formed peer; routing
+  by sender stops a request naming a destination that disagrees with its
+  origin, and stops an absent `ReplyTo` producing an empty recipient that the
+  mailbox store rejects outright. `Rpc.ReplyTo` is therefore advisory from the
+  responder's side: producers still set it, nothing here reads it. An empty
+  `env.Sender` is refused up front (the ingress version check only compares the
+  version fields, so nothing upstream asserts it) and the ingress loop logs the
+  dispatch error.
+- The response `Edge.Send` in `handleInboundRPC` folds the send status in
+  alongside the transport error via `serverconn.SendResponseError`. A mailbox
+  rejection arrives as `Status.Ok = false` rather than as an error, so checking
+  only `err` would report a dropped answer as a successful dispatch and leave
+  the caller blocked on its correlation ID until it times out.
 - `operatorTermsFromResponse` and daemon `GetInfo` must preserve
   `FreeRefreshWindowBlocks` end to end.
 - The VTXO manager reads `FreeRefreshWindowBlocks` from the latest cached
diff --git a/waved/CLAUDE.md b/waved/CLAUDE.md
index b778f21c..f5aa6530 100644
--- a/waved/CLAUDE.md
+++ b/waved/CLAUDE.md
@@ -136,6 +136,24 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/waved.<S
   exceeds `OORConfig.MaxTransientSubmitRetry` (default 1h), persisting the
   window start (`FirstRejectUnixNanos`) in the outgoing snapshot (version 5)
   so the bound survives restarts.
+- `handleInboundRPC` (the mux bridge registered by `buildRPCDispatchers` for
+  every `NonTxRoutes` dispatcher) addresses its response envelope to
+  `env.Sender`, **not** to `env.Rpc.ReplyTo`. Every producer in this repo sets
+  both to the same `LocalMailboxID` (`serverconn/unary_facade.go`,
+  `serverconn/actor.go`, `serverconn/heartbeat.go`, and the operator's
+  `clientconn` equivalents), so the two agree for a well-formed peer; routing
+  by sender stops a request naming a destination that disagrees with its
+  origin, and stops an absent `ReplyTo` producing an empty recipient that the
+  mailbox store rejects outright. `Rpc.ReplyTo` is therefore advisory from the
+  responder's side: producers still set it, nothing here reads it. An empty
+  `env.Sender` is refused up front (the ingress version check only compares the
+  version fields, so nothing upstream asserts it) and the ingress loop logs the
+  dispatch error.
+- The response `Edge.Send` in `handleInboundRPC` folds the send status in
+  alongside the transport error via `serverconn.SendResponseError`. A mailbox
+  rejection arrives as `Status.Ok = false` rather than as an error, so checking
+  only `err` would report a dropped answer as a successful dispatch and leave
+  the caller blocked on its correlation ID until it times out.
 - `operatorTermsFromResponse` and daemon `GetInfo` must preserve
   `FreeRefreshWindowBlocks` end to end.
 - The VTXO manager reads `FreeRefreshWindowBlocks` from the latest cached

How to apply: save the diff above and git apply it, or regenerate locally with the doc-gardening skill scoped to the two packages (/doc-gardening serverconn, then /doc-gardening waved). Each CLAUDE.md must stay byte-identical to its sibling AGENTS.md, which the diff preserves.

Note on make doc-check: it reports one pre-existing, unrelated error on this runner — ./.claude-pr/CLAUDE.md exists but ./.claude-pr/AGENTS.md is missing. That path is a CI-side workflow artifact rather than a repo package, and this diff neither causes nor touches it.

This check is advisory only and does not block merge. Run log: https://github.com/lightninglabs/wavelength/pull/1110/checks

The doc-gardening advisory on this PR is right: the Go change left
both packages' docs stale.

serverconn gains SendResponseError, the exported seam that lets a
caller outside the package fold an application-level rejection through
the same check the package uses internally -- a Status.Ok of false is
this codebase's failure channel and never arrives as a transport
error, so a caller that only checks err reports success on a rejected
send.

waved records that an inbound RPC is answered to env.Sender and that
the field is now guarded, since it is the sole input deciding where
the answer goes and nothing on the ingress path asserts it is set.
@litbot-9000

Copy link
Copy Markdown
Collaborator

@ellemouton, remember to re-request review from reviewers when ready

1 similar comment
@litbot-9000

Copy link
Copy Markdown
Collaborator

@ellemouton, remember to re-request review from reviewers when ready

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.

2 participants