feat(threads): a long reply gets somewhere of its own #380

Merged
coilyco-ops merged 3 commits from feat/a-long-reply-gets-a-thread into main 2026-08-13 11:34:15 +00:00
Member

closes #379

Slice of #239, delivering the trigger Kai specified on #354. Nothing in this repository created a thread before.

When

Two conditions, both required: the turn posted a progress line, and it ran past turnLongReplyAfter (the wait plus two beats — Kai's 3 + 6 + 6).

The second alone is not enough. A turn that never posted a line has nothing in the channel pointing at a thread, and a member would be left with a question and no visible answer.

I raised carry-or-announce as a blocking decision, and it was not one

I asked Kai whether a thread should carry the reply or announce it. The answer was already in code that predates the question: a long turn already posts a progress line in the channel, and that line is the announcement. The reply lands in the thread; the channel keeps the line.

That also makes the guild's hide-after-an-hour setting cheap — an auto-hidden thread takes nothing with it that the channel did not already have.

The name is derived, not authored

Discord requires a name and a name is member-facing. Letters, digits and spaces survive from the member's own message, everything else is dropped, truncated to Discord's 100-rune cap, with a fallback for a message that is all mention and punctuation.

Dropped rather than summarised. Summarising would be writing a member a title.

The property that matters more than the feature

A thread that cannot be made must never cost a member their reply. No permission, a channel type that cannot hold threads, an API failure, a turn already inside a thread — every path returns "no thread" rather than an error, and the reply goes to the channel exactly as today.

That is why the decision returns a channel id and a boolean and has no error to propagate: no failure here is worth failing a turn over. A feature that eats answers is worse than no feature.

Notes

  • Nesting is refused, resolved from cached gateway state with no API call.
  • turnLongReplyAfter had no consumer when it landed in #358. This is it.
  • Threads for jobs stay out. A job outlives its turn, so which thread it owns has a different answer, and BindJobToThread stays unwired.

Live verification is Ops'. I cannot confirm the bot actually holds the create-public-threads permission in the guild; if it does not, this is inert and every reply behaves exactly as it does today, which is the fallback working rather than a failure.

ward exec gate green: build, policy-check, vet, test, test-skips, pre-commit.

closes #379 Slice of https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/239, delivering the trigger Kai specified on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/354. **Nothing in this repository created a thread before.** ## When Two conditions, both required: the turn posted a progress line, **and** it ran past `turnLongReplyAfter` (the wait plus two beats — Kai's `3 + 6 + 6`). The second alone is not enough. A turn that never posted a line has nothing in the channel pointing at a thread, and a member would be left with a question and no visible answer. ## I raised carry-or-announce as a blocking decision, and it was not one I asked Kai whether a thread should carry the reply or announce it. The answer was already in code that predates the question: **a long turn already posts a progress line in the channel, and that line is the announcement.** The reply lands in the thread; the channel keeps the line. That also makes the guild's hide-after-an-hour setting cheap — an auto-hidden thread takes nothing with it that the channel did not already have. ## The name is derived, not authored Discord requires a name and a name is member-facing. Letters, digits and spaces survive from the member's own message, everything else is dropped, truncated to Discord's 100-rune cap, with a fallback for a message that is all mention and punctuation. **Dropped rather than summarised.** Summarising would be writing a member a title. ## The property that matters more than the feature **A thread that cannot be made must never cost a member their reply.** No permission, a channel type that cannot hold threads, an API failure, a turn already inside a thread — every path returns "no thread" rather than an error, and the reply goes to the channel exactly as today. That is why the decision returns a channel id and a boolean and has no error to propagate: no failure here is worth failing a turn over. A feature that eats answers is worse than no feature. ## Notes - Nesting is refused, resolved from cached gateway state with **no API call**. - `turnLongReplyAfter` had no consumer when it landed in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/358. This is it. - Threads for **jobs** stay out. A job outlives its turn, so which thread it owns has a different answer, and `BindJobToThread` stays unwired. **Live verification is Ops'.** I cannot confirm the bot actually holds the create-public-threads permission in the guild; if it does not, this is inert and every reply behaves exactly as it does today, which is the fallback working rather than a failure. `ward exec gate` green: build, policy-check, vet, test, test-skips, pre-commit.
feat(threads): a long reply gets somewhere of its own
All checks were successful
ci / test (pull_request) Successful in 31s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 18s
715fceb267
A turn that posted a progress line and ran past the long-reply window now puts
its reply in a thread hung off the member's own message. Nothing in this
repository created a thread before.

Both conditions are required. A turn that never posted a line has nothing in
the channel pointing at a thread, so a member would be left with a question and
no visible answer.

I raised carry-or-announce as a decision someone had to make and it was not
one. A long turn already posts a progress line in the channel, and that line is
the announcement. The reply lands in the thread and the channel keeps the line,
which also makes the guild's hide-after setting cheap: an auto-hidden thread
takes nothing with it that the channel did not already have.

The name is derived from the member's own message rather than authored.
Letters, digits and spaces survive, everything else is dropped, truncated to
Discord's cap, with a fallback for a message that is all mention and
punctuation. Dropped rather than summarised, because summarising is writing a
member a title.

The property that matters more than the feature: a thread that cannot be made
must never cost a member their reply. No permission, a channel that cannot hold
threads, an API failure, a turn already inside one. Every path returns no
thread rather than an error and the reply goes to the channel as before, which
is why the decision returns a channel and a boolean and has no error to
propagate.

turnLongReplyAfter had no consumer when it landed. This is it.

closes #379

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Merge branch 'main' into feat/a-long-reply-gets-a-thread
All checks were successful
ci / test (pull_request) Successful in 31s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 20s
15a328e67d
Merge branch 'main' into feat/a-long-reply-gets-a-thread
All checks were successful
ci / test (pull_request) Successful in 33s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 4s
2b755ba0e2
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!380
No description provided.