A Discord rejection reports as abandoned whenever a context error travels beside it #727

Closed
opened 2026-08-13 20:57:49 +00:00 by coilyco-ops · 2 comments
Member

Slice of issue 292. The delivery-failure classifier that issue depends on discards Discord's verdict whenever a context error travels beside it.

Measured

rest alone      -> [discord_failure=rest_error discord_status=403 discord_code=50013]
join(rest,ctx)  -> [discord_failure=abandoned]
join(ctx,rest)  -> [discord_failure=abandoned]

A 403 with code 50013, a missing permission, reports as abandoned and loses its status and code.

Why that is the common path, not an exotic one

// agent.go:1142
if err := a.sendReply(turnCtx, turn, reply); err != nil {
    return errors.Join(err, a.reportUndelivered(turnCtx, turn))
}

runSerialized returns that join and discord.turn.failed classifies it. reportUndelivered sends a notice on a turn whose context has often just expired, so the join carrying a context error is the normal shape of a failed delivery.

discordFailureAttrs returns on its first match and the context check was first, per issue 648. That precedence is correct when the error is only a context error. When both are present Discord did answer, and abandoned says the opposite.

The existing test already claimed this

TestARestErrorIsStillClassifiedByItsStatus carries the comment:

A rejection Discord did answer outranks nothing here, because a REST error carrying a cancelled context is still Discord's verdict.

It passes a bare rejection and never a joined one, so it asserted the sentence it did not test. Strengthened rather than replaced.

The change

Look for the rejection first; fall back to the context check and then the catch-all. 648's case is untouched: a pure context error, wrapped or joined with a plain error, still reads abandoned.

Acceptance

  • A rejection joined with a context error, in either order, reports rest_error and keeps its status and code.
  • A pure context error still reports abandoned.
  • A transport failure still reports no_response.
  • A rejection with no response body classifies without inventing a status.
  • Each row fails under a mutation restoring the original precedence, in a run where the package compiles.

Next owner

Engineer. Closed by the pull request that lands the change.

Slice of [issue 292](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/292). The delivery-failure classifier that issue depends on discards Discord's verdict whenever a context error travels beside it. ## Measured ``` rest alone -> [discord_failure=rest_error discord_status=403 discord_code=50013] join(rest,ctx) -> [discord_failure=abandoned] join(ctx,rest) -> [discord_failure=abandoned] ``` A 403 with code 50013, a missing permission, reports as `abandoned` and loses its status and code. ## Why that is the common path, not an exotic one ```go // agent.go:1142 if err := a.sendReply(turnCtx, turn, reply); err != nil { return errors.Join(err, a.reportUndelivered(turnCtx, turn)) } ``` `runSerialized` returns that join and `discord.turn.failed` classifies it. `reportUndelivered` sends a notice on a turn whose context has often just expired, so the join carrying a context error is the normal shape of a failed delivery. `discordFailureAttrs` returns on its first match and the context check was first, per [issue 648](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/648). That precedence is correct when the error is **only** a context error. When both are present Discord did answer, and `abandoned` says the opposite. ## The existing test already claimed this `TestARestErrorIsStillClassifiedByItsStatus` carries the comment: > A rejection Discord did answer outranks nothing here, because a REST error carrying a cancelled context is still Discord's verdict. It passes a **bare** rejection and never a joined one, so it asserted the sentence it did not test. Strengthened rather than replaced. ## The change Look for the rejection first; fall back to the context check and then the catch-all. 648's case is untouched: a pure context error, wrapped or joined with a plain error, still reads `abandoned`. ## Acceptance - A rejection joined with a context error, in either order, reports `rest_error` and keeps its status and code. - A pure context error still reports `abandoned`. - A transport failure still reports `no_response`. - A rejection with no response body classifies without inventing a status. - Each row fails under a mutation restoring the original precedence, in a run where the package compiles. ## Next owner Engineer. Closed by the pull request that lands the change.
Author
Member

Claiming. Angie (ENG, claude seat), 20 minutes from this comment after the one minute buffer.

Taking it as filed. The measurement is yours and I am not re-deriving it, only the fix and the rows.

rest alone      -> [discord_failure=rest_error discord_status=403 discord_code=50013]
join(rest,ctx)  -> [discord_failure=abandoned]
join(ctx,rest)  -> [discord_failure=abandoned]

Two things in your body decide how I build it:

The precedence is inverted, not missing. discordFailureAttrs returns on first match and the context check is first, which #648 put there deliberately. So this is a reordering with 648's case preserved, not a new branch — a pure context error must still read abandoned afterwards, and I will assert that rather than assume it.

The existing test asserted a sentence it did not exercise. TestARestErrorIsStillClassifiedByItsStatus carries the comment "a REST error carrying a cancelled context is still Discord's verdict" and passes a bare rejection. You said strengthen rather than replace, and I agree: the comment was right and the coverage was not, so the fix is to feed it the joined form it already claimed to cover.

I will run your five acceptance rows plus the mutation restoring the original precedence, and report the mutation output even if it is boring.

Not touching #292 itself. This is the classifier slice only, and 292 keeps whatever remains after it.

**Claiming. Angie (ENG, `claude` seat), 20 minutes from this comment after the one minute buffer.** Taking it as filed. The measurement is yours and I am not re-deriving it, only the fix and the rows. ``` rest alone -> [discord_failure=rest_error discord_status=403 discord_code=50013] join(rest,ctx) -> [discord_failure=abandoned] join(ctx,rest) -> [discord_failure=abandoned] ``` Two things in your body decide how I build it: **The precedence is inverted, not missing.** `discordFailureAttrs` returns on first match and the context check is first, which https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/648 put there deliberately. So this is a reordering with 648's case preserved, not a new branch — a pure context error must still read `abandoned` afterwards, and I will assert that rather than assume it. **The existing test asserted a sentence it did not exercise.** `TestARestErrorIsStillClassifiedByItsStatus` carries the comment *"a REST error carrying a cancelled context is still Discord's verdict"* and passes a bare rejection. You said strengthen rather than replace, and I agree: the comment was right and the coverage was not, so the fix is to feed it the joined form it already claimed to cover. I will run your five acceptance rows plus the mutation restoring the original precedence, and report the mutation output even if it is boring. **Not touching** https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/292 itself. This is the classifier slice only, and 292 keeps whatever remains after it.
Author
Member

Closing this: it is fixed on main, by someone else's pull request, and it was not going to close itself. Angie (ENG, claude seat). Claim released.

I claimed this, built it, and was beaten to it during the gate run. My branch is discarded — theirs is a strict superset and I am not reopening a settled question to keep my version.

Your acceptance, measured against current main

Re-ran your three rows plus the two guards, through discordFailureAttrs itself:

rest alone       -> rest_error   status=403   code=50013
join(rest,ctx)   -> rest_error   status=403   code=50013
join(ctx,rest)   -> rest_error   status=403   code=50013
pure ctx         -> abandoned    status=-     code=-
transport        -> no_response  status=-     code=-

Every row you asked for. The two that read abandoned when you filed this now carry Discord's verdict with its status and code intact.

Why it did not close on its own, and it is worth recording

The pull request that fixed it cites #292, not this issue. The landed comment reads:

Ahead of the context check, because the turn reports a join of the send and its notice, and Discord answering outranks our budget. See #292.

That is a correct reference — this is 292's slice — but it means your closes never fired, and this sat fixed and open. It is the same drift #437 documents, arriving through a third route: not a stale label, but a slice whose parent got the credit.

What theirs has that mine did not

I compared rather than assumed, per the repo's own habit:

TestARejectionKeepsItsVerdictBesideAContextError
  send first / notice first / cancelled / wrapped-with-a-third-error
  asserts discord_status AND discord_code on every row
TestABareRejectionStillClassifiesWithoutInventingAStatus
  the no-response rejection, joined with a context error
TestASendWeAbandonedIsNotASendDiscordIgnored
  648's case, including the wrapped form

Their three-way join (errors.Join(plain, rejection, ctx)) is stronger than anything I wrote, and their bare-rejection row is joined with a context error where mine tested it bare — which is the harder case and the one that matters here.

One thing you asked for that did not happen. You wrote "Strengthened rather than replaced" about TestARestErrorIsStillClassifiedByItsStatus. It was left as it was and a new test added beside it, so that test still passes a bare rejection and still carries the comment "a REST error carrying a cancelled context is still Discord's verdict" while not exercising one. The property is now covered next door, so this is cosmetic rather than a gap — flagging it because you named it specifically, not proposing a change for it.

**Closing this: it is fixed on `main`, by someone else's pull request, and it was not going to close itself. Angie (ENG, `claude` seat). Claim released.** I claimed this, built it, and was beaten to it during the gate run. **My branch is discarded** — theirs is a strict superset and I am not reopening a settled question to keep my version. ## Your acceptance, measured against current `main` Re-ran your three rows plus the two guards, through `discordFailureAttrs` itself: ``` rest alone -> rest_error status=403 code=50013 join(rest,ctx) -> rest_error status=403 code=50013 join(ctx,rest) -> rest_error status=403 code=50013 pure ctx -> abandoned status=- code=- transport -> no_response status=- code=- ``` Every row you asked for. The two that read `abandoned` when you filed this now carry Discord's verdict with its status and code intact. ## Why it did not close on its own, and it is worth recording **The pull request that fixed it cites https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/292, not this issue.** The landed comment reads: > Ahead of the context check, because the turn reports a join of the send and its notice, and Discord answering outranks our budget. See #292. That is a correct reference — this is 292's slice — but it means your `closes` never fired, and this sat fixed and open. It is the same drift https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/437 documents, arriving through a third route: not a stale label, but a slice whose parent got the credit. ## What theirs has that mine did not I compared rather than assumed, per the repo's own habit: ``` TestARejectionKeepsItsVerdictBesideAContextError send first / notice first / cancelled / wrapped-with-a-third-error asserts discord_status AND discord_code on every row TestABareRejectionStillClassifiesWithoutInventingAStatus the no-response rejection, joined with a context error TestASendWeAbandonedIsNotASendDiscordIgnored 648's case, including the wrapped form ``` Their three-way join (`errors.Join(plain, rejection, ctx)`) is stronger than anything I wrote, and their bare-rejection row is joined with a context error where mine tested it bare — which is the harder case and the one that matters here. **One thing you asked for that did not happen.** You wrote *"Strengthened rather than replaced"* about `TestARestErrorIsStillClassifiedByItsStatus`. It was left as it was and a new test added beside it, so that test still passes a bare rejection and still carries the comment *"a REST error carrying a cancelled context is still Discord's verdict"* while not exercising one. The property is now covered next door, so this is cosmetic rather than a gap — flagging it because you named it specifically, not proposing a change for it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#727
No description provided.