A message that never summons leaves no trace, so 'it ignored me' and 'the turn died' look identical #992

Closed
opened 2026-08-18 22:31:34 +00:00 by coilyco-ops · 0 comments
Member

Filed from a live report: Kai has asked three times why Dowel never answers comments in threads. The behaviour itself is #750, still unbuilt. This issue is the reason it took three asks and a source read to establish that.

The gap

A message that fails the summon gate produces nothing. No reply, correctly. Also no log line, no metric, no span, and no counter. From outside, a message that was never admitted is indistinguishable from a message that was admitted and then died.

Those two have opposite fixes. The first is #750, a feature that was specified and never built. The second is #939, turns dying silently, which was worked tonight. A member or an operator seeing silence in a thread cannot tell which one they are looking at, so the report arrives as "the agent is broken" and the diagnosis costs a trip through summonedLocally.

That is exactly what happened here. Three reports, and the answer was only reachable by reading the source and confirming OwnerID appears nowhere.

Not asking for a member-facing reply

Answering an unaddressed message is the flood the mention gate exists to prevent, and #750's own thread rejects that direction. Nothing here should become member-visible.

The ask is an operator-visible signal.

Shape

At the point summonedLocally returns false, emit a bounded counter or debug span carrying the refusal reason and the origin kind, distinguishing at minimum:

  • not addressed, ordinary channel message
  • not addressed, message inside a thread (the #750 case specifically, since that is the one people report)
  • refused by access policy, which already has accessDecision.Reason and is the model to copy
  • reply reference unresolved

accessDecision already names its reasons as a typed accessReason. The summon decision returns a bare bool and throws its reason away. Giving it the same treatment is the whole change.

Cardinality matters: the refusal reason and origin kind are bounded, and no channel, guild, user, or message identifier should ride along.

Acceptance

  • A plain message in a thread that does not summon leaves an operator-visible record naming why.
  • That record distinguishes "never admitted" from "admitted then failed", so #750-shaped and #939-shaped silence are separable without reading source.
  • Nothing new is posted to Discord on a non-summoning message.
  • No unbounded label is introduced.

Why it is worth doing beyond this one bug

Every future "why didn't it answer" costs the same investigation. The summon gate is the first of seven admission checks and the only one whose refusals are invisible, which makes it the most expensive one to be wrong about.

Related: #750 is the behaviour, #939 is the other silence this is confused with, and accessDecision in internal/community/access.go is the existing pattern to follow.

Filed from a live report: Kai has asked three times why Dowel never answers comments in threads. The behaviour itself is #750, still unbuilt. **This issue is the reason it took three asks and a source read to establish that.** ## The gap A message that fails the summon gate produces **nothing**. No reply, correctly. Also no log line, no metric, no span, and no counter. From outside, a message that was never admitted is indistinguishable from a message that was admitted and then died. Those two have opposite fixes. The first is #750, a feature that was specified and never built. The second is #939, turns dying silently, which was worked tonight. A member or an operator seeing silence in a thread cannot tell which one they are looking at, so the report arrives as "the agent is broken" and the diagnosis costs a trip through `summonedLocally`. That is exactly what happened here. Three reports, and the answer was only reachable by reading the source and confirming `OwnerID` appears nowhere. ## Not asking for a member-facing reply Answering an unaddressed message is the flood the mention gate exists to prevent, and #750's own thread rejects that direction. **Nothing here should become member-visible.** The ask is an operator-visible signal. ## Shape At the point `summonedLocally` returns false, emit a bounded counter or debug span carrying the refusal reason and the origin kind, distinguishing at minimum: * not addressed, ordinary channel message * not addressed, message inside a thread (the #750 case specifically, since that is the one people report) * refused by access policy, which already has `accessDecision.Reason` and is the model to copy * reply reference unresolved `accessDecision` already names its reasons as a typed `accessReason`. The summon decision returns a bare bool and throws its reason away. Giving it the same treatment is the whole change. Cardinality matters: the refusal reason and origin kind are bounded, and no channel, guild, user, or message identifier should ride along. ## Acceptance * A plain message in a thread that does not summon leaves an operator-visible record naming why. * That record distinguishes "never admitted" from "admitted then failed", so #750-shaped and #939-shaped silence are separable without reading source. * Nothing new is posted to Discord on a non-summoning message. * No unbounded label is introduced. ## Why it is worth doing beyond this one bug Every future "why didn't it answer" costs the same investigation. The summon gate is the first of seven admission checks and the only one whose refusals are invisible, which makes it the most expensive one to be wrong about. Related: #750 is the behaviour, #939 is the other silence this is confused with, and `accessDecision` in `internal/community/access.go` is the existing pattern to follow.
Sign in to join this conversation.
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#992
No description provided.