feat(discord): read the whole thread when a channel opts in #827

Merged
coilysiren merged 1 commit from aos/claude/aw85-thread-prefill into main 2026-08-15 17:13:15 +00:00
Member

Closes #769.

What changed

Every turn inside a thread prefills the whole thread. The walk pages Discord newest first until it runs out or the read bound stops it.

No toggle. The resolved spec called for a per-channel one and Kai removed it on the issue: a thread is a bounded conversation someone deliberately opened, so reading it whole is what the feature means everywhere rather than a per-channel preference. Being in a thread is the entire condition. What holds the cost down is the context budget and the bounded walk, not a switch.

Overflow drops oldest first and the reply says so, which is the rule decided on the issue over silent truncation, falling back to the partial window, and summarising the older half.

Where the annotation lives

It is a service-authored suffix, the same mechanism as the tool receipt, so it is appended after the reply checks and contends for the send budget with the others rather than being stapled on afterwards. It sits ahead of the receipt in the preference order: the last suffix is the first one cut, and a note about missing context outranks a record of what ran.

That needed the assembly step to carry more than the tool list, so serviceFacts replaces the bare []ExecutedTool inside it. AssembleReply keeps its signature, so every existing caller is unchanged. While there I folded the duplicated "append a short service line, answer yields" body out of AppendToolDisclosureWithin into appendServiceLine, which both suffixes now share.

A hole in my own first version, found and fixed

The walk is bounded so a pathological thread costs a known number of Discord calls. My first version annotated only when the byte budget dropped something - so a thread of 1000 short messages, all of which fit the budget, would have been cut by the walk bound and reported as complete. That is the silent truncation the issue rejected, arriving through a different door.

truncated() now covers the bound too, with its own sentence, and TestACappedWalkIsAnnotatedEvenWhenNothingWentOverBudget pins it. The annotation says at least before a length it only knows a floor for, so an absent hedge is a claim: a plain count means the read reached the start of the thread.

Mutation

Five, each caught: always reading the whole thread, dropping newest first instead of oldest, never annotating, treating a capped walk as complete, and reading the window inside a thread.

TestOutsideAThreadTheWindowIsUnchanged drives the real readTurnHistory against a paging fake rather than restating it, so the unchanged-outside-a-thread claim is exercised rather than asserted.

On the context risk

The issue asks to record the observed prefill size on a long thread before enabling this anywhere. With the toggle gone there is no "before": it applies from the first threaded turn after rollout. history.thread.read and history.thread.dropped land on the community.history span, so the real numbers are readable from day one rather than estimated, and the annotation makes any truncation visible to the member at the same time.

sirens-echo#750 raises the number of turns taken inside threads and therefore the cost of this. Measure after both land rather than after either.

ward exec gate passes on the rebased tree.

Closes #769. ## What changed **Every turn inside a thread prefills the whole thread.** The walk pages Discord newest first until it runs out or the read bound stops it. **No toggle.** The resolved spec called for a per-channel one and Kai removed it on the issue: a thread is a bounded conversation someone deliberately opened, so reading it whole is what the feature means everywhere rather than a per-channel preference. Being in a thread is the entire condition. What holds the cost down is the context budget and the bounded walk, not a switch. **Overflow drops oldest first and the reply says so**, which is the rule decided on the issue over silent truncation, falling back to the partial window, and summarising the older half. ## Where the annotation lives It is a service-authored suffix, the same mechanism as the tool receipt, so it is appended after the reply checks and contends for the send budget with the others rather than being stapled on afterwards. It sits **ahead of the receipt** in the preference order: the last suffix is the first one cut, and a note about missing context outranks a record of what ran. That needed the assembly step to carry more than the tool list, so `serviceFacts` replaces the bare `[]ExecutedTool` inside it. `AssembleReply` keeps its signature, so every existing caller is unchanged. While there I folded the duplicated "append a short service line, answer yields" body out of `AppendToolDisclosureWithin` into `appendServiceLine`, which both suffixes now share. ## A hole in my own first version, found and fixed The walk is bounded so a pathological thread costs a known number of Discord calls. My first version annotated only when the byte budget dropped something - so a thread of 1000 short messages, all of which fit the budget, would have been cut by the walk bound and reported as complete. That is the silent truncation the issue rejected, arriving through a different door. `truncated()` now covers the bound too, with its own sentence, and `TestACappedWalkIsAnnotatedEvenWhenNothingWentOverBudget` pins it. The annotation says `at least` before a length it only knows a floor for, so an absent hedge is a claim: a plain count means the read reached the start of the thread. ## Mutation Five, each caught: always reading the whole thread, dropping newest first instead of oldest, never annotating, treating a capped walk as complete, and reading the window inside a thread. `TestOutsideAThreadTheWindowIsUnchanged` drives the real `readTurnHistory` against a paging fake rather than restating it, so the unchanged-outside-a-thread claim is exercised rather than asserted. ## On the context risk The issue asks to record the observed prefill size on a long thread before enabling this anywhere. With the toggle gone there is no "before": it applies from the first threaded turn after rollout. `history.thread.read` and `history.thread.dropped` land on the `community.history` span, so the real numbers are readable from day one rather than estimated, and the annotation makes any truncation visible to the member at the same time. sirens-echo#750 raises the number of turns taken inside threads and therefore the cost of this. Measure after both land rather than after either. `ward exec gate` passes on the rebased tree.
feat(discord): read the whole thread when a channel opts in
All checks were successful
ci / image-build (pull_request) Successful in 26s
ci / test (pull_request) Successful in 42s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
b5a3422ce7
A turn inside a thread got the same partial window a channel turn gets, so a
thread longer than that window was answered from its own tail with nothing
saying so.

The prefill now reads the whole thread when its parent channel opted in, keyed
on the parent because threads are created and abandoned constantly and nobody
would maintain a list of them. `SIRENS_ECHO_THREAD_PREFILL_CHANNELS` is empty
by default, so every channel ships off and this lands without waiting on where
to enable it. A named channel the deployment does not admit fails at boot
rather than being ignored, and `discord.ready` states the resolved counts so
default-off is observable rather than assumed.

Overflow drops the oldest messages first and the reply says so, which is the
rule Kai chose over silent truncation, falling back to the partial window, and
summarising the older half. The annotation is a service-authored suffix like
the tool receipt and sits ahead of it in the preference order, since a note
about missing context outranks a record of what ran.

The walk is bounded, so a pathological thread costs a known number of Discord
calls. A thread longer than the walk is still annotated, and the annotation
says so rather than claiming a length the runtime does not know.

Outside a thread, and inside a thread on a channel that has not opted in, the
prefill is the same window read the same way.

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!827
No description provided.