fix(reply): a blank never reaches the channel, the message gets a mark instead #1060

Merged
coilysiren merged 2 commits from aos/claude/gh1035-no-blank-posts into main 2026-08-19 06:38:44 +00:00
Member

Part of #1035, and deliberately not all of it.

Where the four blanks came from, and where they did not

Not the harness. That path is already guarded and has the telemetry to prove it fired:

  • ParseReply runs strings.TrimSpace, so whitespace-only arrives as empty
  • reply == "" routes to finishSilently, which posts nothing
  • renderPhrases returns an error rather than an empty string
  • fitWithOverflow always yields at least a notice
  • three days of turn.reply.delivered show no delivery below 54 bytes and none at zero

Dowel holds Discord write grants, and finishSilently's own comment says the agent answers through tools. A create_message carrying empty content posts a blank the harness never sees. That is the leak, and it is mcp-beaver#87, filed at Kai's direction for an opt-in per-tool control.

What this does close

sendReply is the single funnel every reply crosses and it had no final content check. Its non-Discord branch reaches turn.Reply without passing through deliverWithOverflow at all. A blank arriving there is now refused rather than sent.

So this closes the funnel rather than the leak. Worth being explicit, because a PR that looked like it fixed #1035 and did not would be worse than no PR.

The behaviour

Kai chose marking over posting, and the harness already had the shape: finishByReacting exists because "a mark is the whole answer for a turn that needs no words". This reuses the closed reaction key set rather than inventing a response, marking with acknowledge.

A mark that fails to land does not become a message. finishByReacting falls back to sending the glyph when React fails, which is right when the glyph is the answer. Here the answer is nothing, so that fallback would post the very blank this prevents. A transport with no reaction surface takes the same path and sends nothing.

turn.reply.blank records it under its own name, beside turn.reply.silent, keeping chosen silence and a defect apart the way #895 asked.

Tests

Four, reusing the existing markableTurn fake rather than adding another: every blank shape refused, the mark applied instead, a non-marking transport still sending nothing, and a real reply untouched including a two-character one.

Docs

The existing silence section absorbed this rather than the page growing a new one, since it sat at 7995 of an 8000-character cap. Its #895 retelling is shorter to pay for the space, which matches the decision-archaeology strip running on main tonight.

Not done

The acceptance criterion is not met by this alone. "No zero-content message reaches the channel from either layer" needs mcp-beaver#87 as well. Leave #1035 open.

Refs: #1035, #895, mcp-beaver#87

🤖 Generated with Claude Code

Part of #1035, and deliberately not all of it. ## Where the four blanks came from, and where they did not **Not the harness.** That path is already guarded and has the telemetry to prove it fired: * `ParseReply` runs `strings.TrimSpace`, so whitespace-only arrives as empty * `reply == ""` routes to `finishSilently`, which posts nothing * `renderPhrases` returns an error rather than an empty string * `fitWithOverflow` always yields at least a notice * **three days of `turn.reply.delivered` show no delivery below 54 bytes and none at zero** Dowel holds Discord write grants, and `finishSilently`'s own comment says the agent answers through tools. A `create_message` carrying empty content posts a blank the harness never sees. **That is the leak, and it is mcp-beaver#87**, filed at Kai's direction for an opt-in per-tool control. ## What this does close `sendReply` is the single funnel every reply crosses and it had **no final content check**. Its non-Discord branch reaches `turn.Reply` without passing through `deliverWithOverflow` at all. A blank arriving there is now refused rather than sent. So this closes the funnel rather than the leak. Worth being explicit, because a PR that looked like it fixed #1035 and did not would be worse than no PR. ## The behaviour Kai chose marking over posting, and **the harness already had the shape**: `finishByReacting` exists because "a mark is the whole answer for a turn that needs no words". This reuses the closed reaction key set rather than inventing a response, marking with `acknowledge`. **A mark that fails to land does not become a message.** `finishByReacting` falls back to sending the glyph when `React` fails, which is right when the glyph *is* the answer. Here the answer is nothing, so that fallback would post the very blank this prevents. A transport with no reaction surface takes the same path and sends nothing. `turn.reply.blank` records it under its own name, beside `turn.reply.silent`, keeping chosen silence and a defect apart the way #895 asked. ## Tests Four, reusing the existing `markableTurn` fake rather than adding another: every blank shape refused, the mark applied instead, a non-marking transport still sending nothing, and a real reply untouched including a two-character one. ## Docs The existing silence section absorbed this rather than the page growing a new one, since it sat at 7995 of an 8000-character cap. Its #895 retelling is shorter to pay for the space, which matches the decision-archaeology strip running on main tonight. ## Not done **The acceptance criterion is not met by this alone.** "No zero-content message reaches the channel from either layer" needs mcp-beaver#87 as well. Leave #1035 open. Refs: #1035, #895, mcp-beaver#87 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(reply): a blank never reaches the channel, the message gets a mark instead
Some checks failed
ci / image-build (pull_request) Successful in 4s
ci / test (pull_request) Failing after 1m12s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
1da4165c00
Part of #1035. Four of Dowel's visible posts in #moxn-temporal carried no
content at all.

WHERE THE FOUR CAME FROM, AND WHERE THEY DID NOT. The harness reply path is
already guarded and has the telemetry to prove it: ParseReply trims before
the silence check so whitespace-only is already silence, reply == "" routes
to finishSilently, renderPhrases errors rather than empties, and
fitWithOverflow always yields at least a notice. Three days of
turn.reply.delivered show no delivery below 54 bytes and none at zero.

So this does not close #1035. Dowel holds Discord write grants, and a
create_message carrying empty content posts a blank the harness never sees.
That path is mcp-beaver#87, filed at Kai's direction for an opt-in per-tool
control.

WHAT THIS DOES CLOSE. sendReply is the single funnel every reply crosses and
it had no final content check, and its non-Discord branch reaches turn.Reply
without passing deliverWithOverflow at all. A blank arriving there is now
refused rather than sent.

Kai chose the behaviour: mark the member's message rather than post. The
harness already had the shape in finishByReacting, where a mark is the whole
answer for a turn that needs no words, so this reuses the closed reaction key
set instead of inventing a response.

A MARK THAT FAILS TO LAND DOES NOT BECOME A MESSAGE. finishByReacting falls
back to sending the glyph when React fails, which is right when the glyph is
the answer. Here the answer is nothing, so falling back would post the blank
this exists to prevent. A transport with no reaction surface takes the same
path and sends nothing.

turn.reply.blank records it under its own name, beside turn.reply.silent, so
chosen silence and a defect stay apart the way #895 asked.

The docs section absorbed this rather than growing a new one, and its #895
retelling is shorter to pay for the space, matching the archaeology strip
running on main tonight.

Refs: #1035, #895, mcp-beaver#87
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
fix(test): the blank-reply comment is two lines, and drops the retelling
All checks were successful
ci / image-build (pull_request) Successful in 41s
ci / test (pull_request) Successful in 1m13s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
fd54adcc79
code-comments caps a contiguous block at two lines and this one ran to
three. The cut line narrated the incident, which the house rule sends to
a docs page rather than a code comment, so the surviving pair says what
the code cannot and cites the issue for the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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!1060
No description provided.