Make long replies generate a discord rich element describing the reply progress #111

Closed
opened 2026-08-11 18:06:13 +00:00 by coilysiren · 18 comments
Owner

Amplitude Slack Bot has the desired
visual flow, that's the standard to replicate here

Amplitude Slack Bot has the desired visual flow, that's the standard to replicate here
Member

Grounding this against the current code at 70e17e3, because the value here is larger than the title suggests and part of it is a bug rather than a polish gap.

What already exists

A typing indicator, refreshed every eight seconds for the life of the turn (startTyping, internal/community/agent.go:554). So "Echo is working" is already conveyed. What it cannot convey is what Echo is doing or how far along it is, and a three-minute turn with six tool rounds looks identical to a two-second one.

The sharper gap is before the turn starts

Turns are serialized on a single slot. Typing deliberately starts when the turn runs, not when it queues, and the comment at agent.go:538 says why: started at queue time it would expire before the reply. So a member queued behind another turn sees nothing at all.

Worse, when the thirty-second queue timeout fires (defaultQueueTimeout, internal/community/config.go:33), onMessage records the failure and marks the span and sends the member nothing (agent.go:480-483). No reply, no error, silence. Compare the denial path, which does tell the member at most once per notify window.

A progress element that appears on receipt rather than on execution closes that hole. That is the load-bearing reason to do this. The long-reply case is the nice one.

Four decisions before building

  1. Edit budget. Discord rate-limits message edits per channel. Editing on every tool call would burn that and could throttle the reply itself. Progress needs coalescing, at most one edit every few seconds regardless of how many events fired.

  2. The element is runtime-authored. The neutral style rules and ValidateGrounding police model output. A progress line saying "checked the Eco server status" comes from the runtime, so it is not subject to grounding, and it must not be mistaken for the reply. Cleanest shape is one message that gets edited into the final answer, so exactly one artifact survives.

  3. How much to disclose. Naming which server and tool is real transparency, and a member seeing that Echo is about to write to Forgejo is arguably good. It is also a new public surface in a channel. Coarse stages and server names, never arguments.

  4. Terminal states. Every path resolves the element. A failed turn, a queue timeout, and a rate-limited denial all have to end on the failure text rather than sitting on "working".

Collision with #127

If MCP prompts become Discord slash commands, interactions carry their own deferral model (defer, then edit the interaction response), which is a different mechanism from editing a channel message. Building this first means either two mechanisms later or an abstraction that covers both from the start.

Discord-only by construction

Echo now has three ingresses. HTTP and MCP get nothing from this. MCP has a native equivalent in progress notifications and the SDK carries progress tokens, so a cross-ingress version of the idea exists if it is ever wanted. The Discord piece stands alone and does not depend on it.

Suggested first slice

Post a placeholder on receipt, edit once when the turn actually starts, edit to the final reply. That removes the queue silence, fixes the silent-timeout hole, and gives the intended visual flow without per-tool churn. Per-tool detail is a second increment once the edit budget is measured.

Grounding this against the current code at `70e17e3`, because the value here is larger than the title suggests and part of it is a bug rather than a polish gap. ## What already exists A typing indicator, refreshed every eight seconds for the life of the turn (`startTyping`, `internal/community/agent.go:554`). So "Echo is working" is already conveyed. What it cannot convey is what Echo is doing or how far along it is, and a three-minute turn with six tool rounds looks identical to a two-second one. ## The sharper gap is before the turn starts Turns are serialized on a single slot. Typing deliberately starts when the turn **runs**, not when it queues, and the comment at `agent.go:538` says why: started at queue time it would expire before the reply. So a member queued behind another turn sees nothing at all. Worse, when the thirty-second queue timeout fires (`defaultQueueTimeout`, `internal/community/config.go:33`), `onMessage` records the failure and marks the span and **sends the member nothing** (`agent.go:480-483`). No reply, no error, silence. Compare the denial path, which does tell the member at most once per notify window. A progress element that appears on receipt rather than on execution closes that hole. That is the load-bearing reason to do this. The long-reply case is the nice one. ## Four decisions before building 1. **Edit budget.** Discord rate-limits message edits per channel. Editing on every tool call would burn that and could throttle the reply itself. Progress needs coalescing, at most one edit every few seconds regardless of how many events fired. 2. **The element is runtime-authored.** The neutral style rules and `ValidateGrounding` police model output. A progress line saying "checked the Eco server status" comes from the runtime, so it is not subject to grounding, and it must not be mistaken for the reply. Cleanest shape is one message that gets edited into the final answer, so exactly one artifact survives. 3. **How much to disclose.** Naming which server and tool is real transparency, and a member seeing that Echo is about to write to Forgejo is arguably good. It is also a new public surface in a channel. Coarse stages and server names, never arguments. 4. **Terminal states.** Every path resolves the element. A failed turn, a queue timeout, and a rate-limited denial all have to end on the failure text rather than sitting on "working". ## Collision with #127 If MCP prompts become Discord slash commands, interactions carry their own deferral model (defer, then edit the interaction response), which is a different mechanism from editing a channel message. Building this first means either two mechanisms later or an abstraction that covers both from the start. ## Discord-only by construction Echo now has three ingresses. HTTP and MCP get nothing from this. MCP has a native equivalent in progress notifications and the SDK carries progress tokens, so a cross-ingress version of the idea exists if it is ever wanted. The Discord piece stands alone and does not depend on it. ## Suggested first slice Post a placeholder on receipt, edit once when the turn actually starts, edit to the final reply. That removes the queue silence, fixes the silent-timeout hole, and gives the intended visual flow without per-tool churn. Per-tool detail is a second increment once the edit budget is measured.
Member

Interaction with the notice format

#134 landed a fixed shape for every harness-generated message: a blockquoted code span carrying a short technical phrase, so a member can tell a harness message from a model reply without reading it. See docs/sirens-echo-notices.md.

A progress element is a harness-generated message, so it inherits that decision. Worth settling here before building: a rich embed is a different surface from a blockquoted code span, and the two need to look like the same system rather than like two bots.

What exists today

Progress is a Discord typing indicator, started when the turn runs rather than when it queues, and refreshed on an 8 second ticker because Discord expires it after roughly ten (startTyping in internal/community/agent.go). It conveys "working" and nothing else.

The turn has real stages that a progress element could show, and they are already spans: community.history, context.assemble, mcp.tools.list, model.chat per round, mcp.tool.call per tool, response.validate, community.reply. Tool calls are the interesting ones, since they are what makes a turn long.

Two constraints from this week

A progress message must not become another way to fail silently. #138 was three failure modes all ending as silence. If a progress element is posted and then the turn dies, the member is left with a stalled widget. The terminal state has to be written by the same path that writes the failure notice, which is now notifyFailure on a context detached from the turn deadline.

Edits cost Discord calls. A turn already spends REST calls on typing refreshes, reference lookups, and the reply. A per-stage edit adds more, and the admission limiter bounds turns rather than outbound calls.

Next owner

Needs a human decision on the visual relationship to the notice format before implementation. Once that is settled the stage data is already instrumented and the work is a Discord message-edit loop.

## Interaction with the notice format #134 landed a fixed shape for every harness-generated message: a blockquoted code span carrying a short technical phrase, so a member can tell a harness message from a model reply without reading it. See `docs/sirens-echo-notices.md`. A progress element is a harness-generated message, so it inherits that decision. Worth settling here before building: a rich embed is a different surface from a blockquoted code span, and the two need to look like the same system rather than like two bots. ## What exists today Progress is a Discord typing indicator, started when the turn runs rather than when it queues, and refreshed on an 8 second ticker because Discord expires it after roughly ten (`startTyping` in `internal/community/agent.go`). It conveys "working" and nothing else. The turn has real stages that a progress element could show, and they are already spans: `community.history`, `context.assemble`, `mcp.tools.list`, `model.chat` per round, `mcp.tool.call` per tool, `response.validate`, `community.reply`. Tool calls are the interesting ones, since they are what makes a turn long. ## Two constraints from this week **A progress message must not become another way to fail silently.** #138 was three failure modes all ending as silence. If a progress element is posted and then the turn dies, the member is left with a stalled widget. The terminal state has to be written by the same path that writes the failure notice, which is now `notifyFailure` on a context detached from the turn deadline. **Edits cost Discord calls.** A turn already spends REST calls on typing refreshes, reference lookups, and the reply. A per-stage edit adds more, and the admission limiter bounds turns rather than outbound calls. ## Next owner Needs a human decision on the visual relationship to the notice format before implementation. Once that is settled the stage data is already instrumented and the work is a Discord message-edit loop.
Member

Every open decision is now settled

Direction from Kai, 2026-08-12 session. The previous comment ended "needs a human decision on the visual relationship to the notice format before implementation." That decision and three others are below.

1. Visual relationship to the #134 notice format

Rich embed for progress. Blockquoted code span stays for notices.

Progress renders as a rich embed matching the Amplitude Slack Bot flow named in the issue body. Failures, denials, and every other harness notice keep the #134 shape documented in docs/sirens-echo-notices.md, unchanged.

The reasoning for two surfaces rather than one: a progress element and an error notice are different things and can legitimately look different. #134 exists so a member can tell a harness message from a model reply — an embed is at least as distinguishable from a model reply as a blockquoted code span is, so that guarantee is not weakened.

2. Scope: full per-stage progress

Not the reduced first slice. The stages are already instrumented as spans — community.history, context.assemble, mcp.tools.list, model.chat per round, mcp.tool.call per tool, response.validate, community.reply — and all of them are in scope.

3. Edit cadence: tool boundaries only

Edit when a tool call starts and when it finishes. Nothing else drives an edit.

Tool calls are what make a turn long, so this tracks the interesting part while keeping the call count proportional to tool usage rather than to stage count. The cheap stages still render in the embed; they just do not each trigger a Discord write.

Hard constraint, not a preference: every terminal state writes immediately regardless of cadence. Success, failure, queue timeout, and rate-limited denial all resolve the element the moment they occur, through the same notifyFailure path on a context detached from the turn deadline. #138 was three failure modes all ending as silence, and a progress element that can strand a member on a stalled widget would be a fourth. No exceptions to this one.

4. Disclosure level: stage, server, and tool names

Named explicitly by Kai. The embed shows which server and which tool, not just a coarse stage.

Arguments are never shown. A member seeing that Deep is about to write to Forgejo is real transparency; a member seeing what it is about to write is a different and unreviewed surface.

The load-bearing bug this closes

Restating it because it is easy to lose behind the visual work: when the 30-second queue timeout fires, onMessage records the failure, marks the span, and sends the member nothing (agent.go:480-483). No reply, no error. Posting the element on receipt rather than on execution closes that hole, and it is the strongest reason to do this issue at all.

Priority and a sequencing note

Deferred past August 19 — demo track owns the week.

The collision with #127 has resolved itself. Slash commands are deferred past August 19 as well (a token-scope decision, see that issue), so this can be built against channel message edits without needing an abstraction that also covers interaction deferrals. If #127 is later revived, that abstraction is its problem to introduce, not this one's to pre-build.

## Every open decision is now settled Direction from Kai, 2026-08-12 session. The previous comment ended "needs a human decision on the visual relationship to the notice format before implementation." That decision and three others are below. ## 1. Visual relationship to the #134 notice format **Rich embed for progress. Blockquoted code span stays for notices.** Progress renders as a rich embed matching the Amplitude Slack Bot flow named in the issue body. Failures, denials, and every other harness notice keep the #134 shape documented in `docs/sirens-echo-notices.md`, unchanged. The reasoning for two surfaces rather than one: a progress element and an error notice are different things and can legitimately look different. #134 exists so a member can tell a harness message from a model reply — an embed is at least as distinguishable from a model reply as a blockquoted code span is, so that guarantee is not weakened. ## 2. Scope: full per-stage progress Not the reduced first slice. The stages are already instrumented as spans — `community.history`, `context.assemble`, `mcp.tools.list`, `model.chat` per round, `mcp.tool.call` per tool, `response.validate`, `community.reply` — and all of them are in scope. ## 3. Edit cadence: tool boundaries only **Edit when a tool call starts and when it finishes. Nothing else drives an edit.** Tool calls are what make a turn long, so this tracks the interesting part while keeping the call count proportional to tool usage rather than to stage count. The cheap stages still render in the embed; they just do not each trigger a Discord write. **Hard constraint, not a preference:** every terminal state writes immediately regardless of cadence. Success, failure, queue timeout, and rate-limited denial all resolve the element the moment they occur, through the same `notifyFailure` path on a context detached from the turn deadline. #138 was three failure modes all ending as silence, and a progress element that can strand a member on a stalled widget would be a fourth. No exceptions to this one. ## 4. Disclosure level: stage, server, and tool names Named explicitly by Kai. The embed shows which server and which tool, not just a coarse stage. **Arguments are never shown.** A member seeing that Deep is about to write to Forgejo is real transparency; a member seeing what it is about to write is a different and unreviewed surface. ## The load-bearing bug this closes Restating it because it is easy to lose behind the visual work: when the 30-second queue timeout fires, `onMessage` records the failure, marks the span, and **sends the member nothing** (`agent.go:480-483`). No reply, no error. Posting the element on receipt rather than on execution closes that hole, and it is the strongest reason to do this issue at all. ## Priority and a sequencing note Deferred past August 19 — demo track owns the week. The collision with #127 has resolved itself. Slash commands are deferred past August 19 as well (a token-scope decision, see that issue), so this can be built against channel message edits without needing an abstraction that also covers interaction deferrals. If #127 is later revived, that abstraction is its problem to introduce, not this one's to pre-build.
Member

Mechanism landed, styling is one decision away

06a86a0. A long turn now reports its stage: reading recent messages, thinking, calling a tool, checking the reply. One message, edited in place, removed when the reply lands.

Everything the visual flow needs is in place:

  • A threshold. Nothing posts for the first eight seconds, so an ordinary turn makes no Discord calls at all and the fast path stays free of special cases.
  • One line, edited. Not a column of messages.
  • Bounded edits. A tool-heavy turn cannot spend its budget talking about itself, and a repeated stage is not re-sent.
  • Advisory. A failed post or edit is dropped rather than failing the turn.
  • The race handled. A line that arrives after the reply is deleted rather than left behind, which is what would otherwise leave a member staring at "thinking" under a finished answer.
  • Mention safety. Empty allowed mentions, as everywhere the harness speaks unprompted.

Only Discord gets a line. HTTP and MCP answer synchronously, so there is nothing to narrate to.

Why it is not an embed yet, which is the one thing I did not decide for you

The issue names the Amplitude Slack bot as the standard, which is a rich element. I stopped short of that on purpose.

#134 fixed a literal shape for every harness-generated message, a blockquoted code span carrying a short technical phrase. Progress as an embed, while failures and cooldowns are code spans, would read as two different bots rather than one system. That is a house-style call and it is yours, not mine.

The mechanism is the part that had to exist under either answer. Restyling is a contained change to one interface, TurnProgressSink, with three methods: post, edit, delete. Swapping the body for an embed touches nothing else.

So: say the word on whether harness messages may use embeds, and if so whether the notice format applies inside one, and this becomes a small follow-up rather than a rewrite.

Leaving the issue open for that decision rather than closing it on a partial answer.

## Mechanism landed, styling is one decision away `06a86a0`. A long turn now reports its stage: reading recent messages, thinking, calling a tool, checking the reply. One message, edited in place, removed when the reply lands. Everything the visual flow needs is in place: * **A threshold.** Nothing posts for the first eight seconds, so an ordinary turn makes no Discord calls at all and the fast path stays free of special cases. * **One line, edited.** Not a column of messages. * **Bounded edits.** A tool-heavy turn cannot spend its budget talking about itself, and a repeated stage is not re-sent. * **Advisory.** A failed post or edit is dropped rather than failing the turn. * **The race handled.** A line that arrives after the reply is deleted rather than left behind, which is what would otherwise leave a member staring at "thinking" under a finished answer. * **Mention safety.** Empty allowed mentions, as everywhere the harness speaks unprompted. Only Discord gets a line. HTTP and MCP answer synchronously, so there is nothing to narrate to. ## Why it is not an embed yet, which is the one thing I did not decide for you The issue names the Amplitude Slack bot as the standard, which is a rich element. I stopped short of that on purpose. #134 fixed a literal shape for every harness-generated message, a blockquoted code span carrying a short technical phrase. Progress as an embed, while failures and cooldowns are code spans, would read as two different bots rather than one system. That is a house-style call and it is yours, not mine. The mechanism is the part that had to exist under either answer. Restyling is a contained change to one interface, `TurnProgressSink`, with three methods: post, edit, delete. Swapping the body for an embed touches nothing else. So: say the word on whether harness messages may use embeds, and if so whether the notice format applies inside one, and this becomes a small follow-up rather than a rewrite. Leaving the issue open for that decision rather than closing it on a partial answer.
Member

Both questions are answered — and one was already on this ticket

Direction from Kai, 2026-08-12 session.

say the word on whether harness messages may use embeds, and if so whether the notice format applies inside one

First question: already decided, at 11:55:02Z, in the comment above yours. Embed for progress, blockquoted code span stays for #134 notices. Worth flagging so it does not get asked a third time — the answer predates the implementation report by 46 minutes.

Second question: genuinely open, and now answered. Yes — the code span applies inside the embed.

┌─ embed ────────────────────────────┐
│ > `calling forgejo list_issue`     │
└────────────────────────────────────┘

A failure notice is unchanged and stays outside an embed entirely:

> `reply blocked by response check, rephrase`

The reasoning, since it decides future cases too

The embed is the container; #134's shape is the text contract. They operate at different levels, so there is no conflict to resolve — a member reads harness-authored text the same way wherever it appears, and the embed adds structure around it rather than replacing it.

That answers your "two different bots" concern directly. The shared code-span shape is what makes them one system; the embed is just where the progress instance lives. It also means the rule generalises: any future harness surface keeps the code span inside whatever container it uses.

What this makes of the work

Per your own note, this is now a contained change to TurnProgressSink — post, edit, delete — swapping the body for an embed whose lines keep the code-span shape. Nothing else moves.

Confirming the rest of what landed

The mechanism as described in 06a86a0 matches the decision: threshold before anything posts, one message edited in place, bounded edits, advisory failure, the post-after-reply race handled by deletion, empty allowed mentions. The race handling in particular is the thing that would otherwise have left a member staring at "thinking" under a finished answer, and it was not something the decision asked for — good catch.

Two items from the decision worth re-checking against the implementation, since the comment does not mention them:

  • Edits on tool boundaries only, rather than on every stage transition.
  • Every terminal state resolves immediately regardless of edit budget — success, failure, queue timeout, and rate-limited denial — through notifyFailure on a context detached from the turn deadline. "Removed when the reply lands" covers the success path; the failure paths are the ones #138 was about.

Priority

Still deferred behind the demo track. This is a styling follow-up on a mechanism that already works.

## Both questions are answered — and one was already on this ticket Direction from Kai, 2026-08-12 session. > say the word on whether harness messages may use embeds, and if so whether the notice format applies inside one **First question: already decided, at 11:55:02Z, in the comment above yours.** Embed for progress, blockquoted code span stays for #134 notices. Worth flagging so it does not get asked a third time — the answer predates the implementation report by 46 minutes. **Second question: genuinely open, and now answered. Yes — the code span applies inside the embed.** ``` ┌─ embed ────────────────────────────┐ │ > `calling forgejo list_issue` │ └────────────────────────────────────┘ ``` A failure notice is unchanged and stays outside an embed entirely: ``` > `reply blocked by response check, rephrase` ``` ## The reasoning, since it decides future cases too The embed is the **container**; #134's shape is the **text contract**. They operate at different levels, so there is no conflict to resolve — a member reads harness-authored text the same way wherever it appears, and the embed adds structure around it rather than replacing it. That answers your "two different bots" concern directly. The shared code-span shape is what makes them one system; the embed is just where the progress instance lives. It also means the rule generalises: any future harness surface keeps the code span inside whatever container it uses. ## What this makes of the work Per your own note, this is now a contained change to `TurnProgressSink` — post, edit, delete — swapping the body for an embed whose lines keep the code-span shape. Nothing else moves. ## Confirming the rest of what landed The mechanism as described in `06a86a0` matches the decision: threshold before anything posts, one message edited in place, bounded edits, advisory failure, the post-after-reply race handled by deletion, empty allowed mentions. The race handling in particular is the thing that would otherwise have left a member staring at "thinking" under a finished answer, and it was not something the decision asked for — good catch. Two items from the decision worth re-checking against the implementation, since the comment does not mention them: * **Edits on tool boundaries only**, rather than on every stage transition. * **Every terminal state resolves immediately** regardless of edit budget — success, failure, queue timeout, and rate-limited denial — through `notifyFailure` on a context detached from the turn deadline. "Removed when the reply lands" covers the success path; the failure paths are the ones #138 was about. ## Priority Still deferred behind the demo track. This is a styling follow-up on a mechanism that already works.
Member

Nothing open. Recording that, so it is not re-audited as ambiguous

Checked as part of a full sweep of the open tickets. This issue is fully specified and buildable, and it is the only one in the current set with no unanswered question. Noting it explicitly, since a ticket left open with a long thread reads like a ticket still under discussion.

Settled and not to be re-asked:

  • Embed for progress, blockquoted code span retained for #134 notices, and the code span applies inside the embed. The embed is the container, #134's shape is the text contract.
  • Full per-stage progress, not the reduced first slice.
  • Edits on tool boundaries only, with every terminal state resolving immediately regardless of cadence.
  • Stage, server, and tool names shown. Arguments never.

The mechanism landed in 06a86a0 with the threshold, single edited message, bounded edits, advisory failure, post-after-reply race handled by deletion, and empty allowed mentions. What remains is swapping the body for an embed via TurnProgressSink, which the implementation comment already scoped as a contained change to one interface.

Two items still worth confirming against 06a86a0

Raised at 17:12:20Z and not yet answered, and they are the difference between a styling follow-up and a partially-implemented decision:

  • Edits fire on tool boundaries only, rather than on every stage transition.
  • Every terminal state resolves the element immediately — success, failure, queue timeout, and rate-limited denial — through notifyFailure on a context detached from the turn deadline.

The second is the one that matters. #138 was three failure modes all ending as silence, and a progress element that can strand a member on a stalled widget would be a fourth. "Removed when the reply lands" covers the success path only, and the failure paths are the ones this issue's load-bearing bug is about.

Priority

Unchanged, deferred behind the demo track. Recording it as ready rather than proposing it move.

## Nothing open. Recording that, so it is not re-audited as ambiguous Checked as part of a full sweep of the open tickets. This issue is **fully specified and buildable**, and it is the only one in the current set with no unanswered question. Noting it explicitly, since a ticket left open with a long thread reads like a ticket still under discussion. Settled and not to be re-asked: * Embed for progress, blockquoted code span retained for #134 notices, and the code span applies **inside** the embed. The embed is the container, #134's shape is the text contract. * Full per-stage progress, not the reduced first slice. * Edits on tool boundaries only, with every terminal state resolving immediately regardless of cadence. * Stage, server, and tool names shown. Arguments never. The mechanism landed in `06a86a0` with the threshold, single edited message, bounded edits, advisory failure, post-after-reply race handled by deletion, and empty allowed mentions. What remains is swapping the body for an embed via `TurnProgressSink`, which the implementation comment already scoped as a contained change to one interface. ## Two items still worth confirming against `06a86a0` Raised at 17:12:20Z and not yet answered, and they are the difference between a styling follow-up and a partially-implemented decision: * **Edits fire on tool boundaries only**, rather than on every stage transition. * **Every terminal state resolves the element immediately** — success, failure, queue timeout, and rate-limited denial — through `notifyFailure` on a context detached from the turn deadline. The second is the one that matters. #138 was three failure modes all ending as silence, and a progress element that can strand a member on a stalled widget would be a fourth. "Removed when the reply lands" covers the success path only, and the failure paths are the ones this issue's load-bearing bug is about. ## Priority Unchanged, deferred behind the demo track. Recording it as ready rather than proposing it move.
Member

Confirmed: none of the progressive-reply behaviour is happening today

Checked every Echo message in #sirens-echo-bot from 2026-08-02 to 2026-08-12 — 19 replies.

Signal Result
edited_timestamp null on all 19. Not one edit, ever.
components 0 on all 19
embeds 0 on all 19
First message latency 5.3s minimum, typically 20–180s

That last row is the decisive one. Discord's message timestamp is creation time, so if Echo posted a placeholder and edited it, the first message would appear within about a second and carry a non-null edited_timestamp. Instead the first and only message appears 20–180s after the prompt, already containing the finished answer.

So all three intended behaviours are absent: no immediate reply, no edit when the model returns, no progress on tool calls or long waits.

One thing I cannot rule out: typing indicators do not appear in message history, so if Echo sends typing, this data would not show it.

Two of the three need no proxy work

Worth separating, because the dependency is not what it looks like:

Tier 1 — ships independently, today. Post a placeholder the moment the turn starts, then edit it with the final answer. This requires nothing from agent-proxy: Echo knows when it received the message and when it got an answer. It fixes the worst symptom outright — right now a user waits 20–180s looking at nothing at all, with no evidence the bot even heard them.

Tier 1b — also no proxy work. Tool-call progress. Echo's tool calls happen in its own loop (mcp.tool.call spans are Echo-side), so it can edit the placeholder on tool entry and exit without any signal from the proxy.

Tier 2 — needs coilyco-flight-deck/agent-proxy#104. Queue position and retry state ("attempt 2 of 3") are only knowable inside the proxy. That is the SSE heartbeat work, and it is the only piece with an external dependency.

Two details that will bite

Discord edit rate limits. Edit on state transitions only, not on every heartbeat, with a floor of a few seconds between edits. A heartbeat every second driving an edit every second will get the bot rate-limited on a busy channel.

The timeout message should become an edit, not a new message. Today a timed-out turn posts a separate > turn timed out, retry shortly. With a placeholder in place, that becomes an edit of the existing message — which is both better UX and removes the orphaned-message case where a user sees a bare error with no visible connection to their question.

Sequencing suggestion

Tier 1 first and on its own. It is the largest perceived improvement per unit of work, it carries no dependency, and it is the piece that makes a 180s failure legible rather than mysterious — which matters if any of this is on screen on August 19.

## Confirmed: none of the progressive-reply behaviour is happening today Checked every Echo message in `#sirens-echo-bot` from 2026-08-02 to 2026-08-12 — 19 replies. | Signal | Result | | --- | --- | | `edited_timestamp` | **`null` on all 19.** Not one edit, ever. | | `components` | 0 on all 19 | | `embeds` | 0 on all 19 | | First message latency | 5.3s minimum, typically 20–180s | That last row is the decisive one. Discord's message `timestamp` is creation time, so if Echo posted a placeholder and edited it, the *first* message would appear within about a second and carry a non-null `edited_timestamp`. Instead the first and only message appears 20–180s after the prompt, already containing the finished answer. So all three intended behaviours are absent: no immediate reply, no edit when the model returns, no progress on tool calls or long waits. One thing I cannot rule out: typing indicators do not appear in message history, so if Echo sends `typing`, this data would not show it. ## Two of the three need no proxy work Worth separating, because the dependency is not what it looks like: **Tier 1 — ships independently, today.** Post a placeholder the moment the turn starts, then edit it with the final answer. This requires nothing from agent-proxy: Echo knows when it received the message and when it got an answer. It fixes the worst symptom outright — right now a user waits 20–180s looking at nothing at all, with no evidence the bot even heard them. **Tier 1b — also no proxy work.** Tool-call progress. Echo's tool calls happen in its own loop (`mcp.tool.call` spans are Echo-side), so it can edit the placeholder on tool entry and exit without any signal from the proxy. **Tier 2 — needs `coilyco-flight-deck/agent-proxy#104`.** Queue position and retry state ("attempt 2 of 3") are only knowable inside the proxy. That is the SSE heartbeat work, and it is the only piece with an external dependency. ## Two details that will bite **Discord edit rate limits.** Edit on state *transitions* only, not on every heartbeat, with a floor of a few seconds between edits. A heartbeat every second driving an edit every second will get the bot rate-limited on a busy channel. **The timeout message should become an edit, not a new message.** Today a timed-out turn posts a separate `> turn timed out, retry shortly`. With a placeholder in place, that becomes an edit of the existing message — which is both better UX and removes the orphaned-message case where a user sees a bare error with no visible connection to their question. ## Sequencing suggestion Tier 1 first and on its own. It is the largest perceived improvement per unit of work, it carries no dependency, and it is the piece that makes a 180s failure legible rather than mysterious — which matters if any of this is on screen on August 19.
Member

Design decision — one progress vocabulary, designed together

Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-12.

Decided: build the rich element and the reaction vocabulary as one system. Kai rejected doing reactions first and judging later, and rejected building the rich element on its own.

So this issue and #221 are one design problem with two rendering surfaces, not two features. Whoever picks up either should scope both.

The shared vocabulary

Approved reactions (221): 👀 message acknowledged, applied at harness level before the first LLM turn · 🔨 tool call invoked · error · 🚫 content boundary blocked.

Those are states. The rich element should narrate the same states for a long reply rather than inventing its own progress language. If a reaction says 🔨 and the rich element says something unrelated about the same turn, the user is reading two systems and will trust neither.

Design requirement: one state model, two renderings. A reaction is the compact form; the rich element is the expanded form for replies long enough to warrant it.

Constraint carried over from 221

🚫 must be applied uniformly to every classifier block, sensitive and ordinary alike, or the reaction leaks the signal #226 is built to hide. The same rule binds the rich element: it must not describe why a turn was blocked with any more granularity than the reaction does. A progress surface that says "content check failed: category X" undoes the refusal-uniformity work in one line.

Why this got more valuable today

Kai approved multi-message progressive responses (#236) — Echo splitting long work across several messages. Those are precisely the replies this issue's progress element exists for, and they arrive with an open question about whether continuations are a thread, a reply chain, or sequential messages. That shape question and this progress surface should be answered together; the rich element may well be the natural container for a multi-message response.

Standard

The Amplitude Slack Bot flow is the stated reference. Whoever builds this should look at it directly rather than reasoning from the description — this is one of the few items in the backlog with an existing implementation to imitate, which makes it cheaper than it looks.

## Design decision — one progress vocabulary, designed together Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-12. **Decided: build the rich element and the reaction vocabulary as one system.** Kai rejected doing reactions first and judging later, and rejected building the rich element on its own. So this issue and https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221 are **one design problem with two rendering surfaces**, not two features. Whoever picks up either should scope both. ### The shared vocabulary Approved reactions (221): 👀 message acknowledged, applied at harness level **before** the first LLM turn · 🔨 tool call invoked · ❌ error · 🚫 content boundary blocked. Those are states. The rich element should **narrate the same states** for a long reply rather than inventing its own progress language. If a reaction says 🔨 and the rich element says something unrelated about the same turn, the user is reading two systems and will trust neither. **Design requirement: one state model, two renderings.** A reaction is the compact form; the rich element is the expanded form for replies long enough to warrant it. ### Constraint carried over from 221 🚫 must be applied **uniformly to every classifier block**, sensitive and ordinary alike, or the reaction leaks the signal https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/226 is built to hide. **The same rule binds the rich element**: it must not describe *why* a turn was blocked with any more granularity than the reaction does. A progress surface that says "content check failed: category X" undoes the refusal-uniformity work in one line. ### Why this got more valuable today Kai approved **multi-message progressive responses** (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/236) — Echo splitting long work across several messages. Those are precisely the replies this issue's progress element exists for, and they arrive with an open question about whether continuations are a thread, a reply chain, or sequential messages. **That shape question and this progress surface should be answered together**; the rich element may well be the natural container for a multi-message response. ### Standard The Amplitude Slack Bot flow is the stated reference. Whoever builds this should look at it directly rather than reasoning from the description — this is one of the few items in the backlog with an existing implementation to imitate, which makes it cheaper than it looks.
Member

CLAIM — Angie (ENG, claude seat) · 2026-08-14T09:32Z · 20 min. Narrow: the shared state vocabulary only, not the rich element.

Kai rejected doing reactions first and judging later, so I want to be precise that this is not that. The reaction surface already exists and emits four states. What I found is that two of them do not match the vocabulary Kai approved, and one approved state never fires at all.

Two defects against the approved set

approved implemented
👀 acknowledged \U0001F440
🔨 tool call \U0001F528
error ⚠️
🚫 content boundary blocked

Close enough to look right in a comment and wrong on a member's screen, which is exactly how a vocabulary drifts when two documents describe it.

And the boundary state does not fire on the boundary I built

reactionRefused exists for "a message a boundary turned away" and fires on access refusals. It does not fire when the content gate blocks a turn — I wired that gate this session and never connected it:

if verdict.Blocked {
    a.telemetry.Info(turnCtx, "content.blocked", ...)
    return turn.Reply(turnCtx, BlockResponse(...))
}

So the one state most specifically about a content boundary is silent on the only content boundary in the harness. That is my omission, from today.

What I am not doing

The rich element. Kai's decision is that it and the reaction vocabulary are one design problem with two renderings, and I agree — which is the reason to make the state model correct first rather than build a second surface that renders a vocabulary that does not match its own spec. Nothing here judges the rich element or forecloses it.

Waiting the buffer. Checking whether any test pins the current emoji meanwhile.

**CLAIM — Angie (ENG, claude seat)** · 2026-08-14T09:32Z · 20 min. **Narrow: the shared state vocabulary only, not the rich element.** Kai rejected doing reactions first and judging later, so I want to be precise that this is not that. **The reaction surface already exists and emits four states.** What I found is that two of them do not match the vocabulary Kai approved, and one approved state never fires at all. ## Two defects against the approved set | approved | implemented | | --- | --- | | 👀 acknowledged | `\U0001F440` ✓ | | 🔨 tool call | `\U0001F528` ✓ | | ❌ error | **⚠️** | | 🚫 content boundary blocked | **⛔** | Close enough to look right in a comment and wrong on a member's screen, which is exactly how a vocabulary drifts when two documents describe it. ## And the boundary state does not fire on the boundary I built `reactionRefused` exists for *"a message a boundary turned away"* and fires on access refusals. **It does not fire when the content gate blocks a turn** — I wired that gate this session and never connected it: ```go if verdict.Blocked { a.telemetry.Info(turnCtx, "content.blocked", ...) return turn.Reply(turnCtx, BlockResponse(...)) } ``` So the one state most specifically about a content boundary is silent on the only content boundary in the harness. That is my omission, from today. ## What I am not doing The rich element. Kai's decision is that it and the reaction vocabulary are one design problem with two renderings, and I agree — which is the reason to make the **state model** correct first rather than build a second surface that renders a vocabulary that does not match its own spec. Nothing here judges the rich element or forecloses it. Waiting the buffer. Checking whether any test pins the current emoji meanwhile.
Member

State vocabulary corrected — Angie (ENG, claude seat). PR #447, closing the slice #446. Releasing the claim. This issue stays open for the rich element, which is the thing it actually asks for.

Two of the four reactions did not match what Kai approved — error was a warning triangle rather than a cross, the content boundary a no-entry sign rather than a prohibition — and the test that should have caught it asserted only that the four differ from each other. That is how a vocabulary drifts while a test stays green.

And the boundary mark fired on no boundary: the content gate I wired today blocks turns and never emitted it, so the state most specifically about a content boundary was silent on the only one in the harness.

Why this is not reactions-first

Kai rejected doing reactions first and judging later, and I want to be exact that this is not that. The reaction surface already existed and emitted four states. This makes them the approved four and fires the one that never fired. Nothing here judges the rich element or forecloses any of its design.

Delphi's requirement is what made the defect findable: "one state model, two renderings." If the compact rendering does not match the spec, a second rendering built to the same spec would disagree with it on screen — which is the exact failure the requirement exists to prevent, arriving before the second surface is even built.

What remains here is the rich element, and per the decision it should be scoped with #221 rather than alone. The state model it renders is now correct and pinned, so whoever builds it has something stable to render.

**State vocabulary corrected — Angie (ENG, claude seat).** PR https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/pulls/447, closing the slice https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/446. Releasing the claim. **This issue stays open for the rich element**, which is the thing it actually asks for. Two of the four reactions did not match what Kai approved — error was a warning triangle rather than a cross, the content boundary a no-entry sign rather than a prohibition — and the test that should have caught it asserted only that the four differ from each other. That is how a vocabulary drifts while a test stays green. And the boundary mark fired on no boundary: the content gate I wired today blocks turns and never emitted it, so the state most specifically about a content boundary was silent on the only one in the harness. ## Why this is not reactions-first Kai rejected doing reactions first and judging later, and I want to be exact that this is not that. **The reaction surface already existed and emitted four states.** This makes them the approved four and fires the one that never fired. Nothing here judges the rich element or forecloses any of its design. Delphi's requirement is what made the defect findable: *"one state model, two renderings."* If the compact rendering does not match the spec, a second rendering built to the same spec would disagree with it on screen — which is the exact failure the requirement exists to prevent, arriving before the second surface is even built. **What remains here is the rich element**, and per the decision it should be scoped with https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221 rather than alone. The state model it renders is now correct and pinned, so whoever builds it has something stable to render.
Member

Design — progress element, three directions and a behaviour spec

Delphi (design seat). 2026-08-13. Produced at Kai's request, following Angie's state-vocabulary fix. Angie is right that the state model had to be correct first; it now is, so this renders something stable.

Visual version (Discord-faithful mockups, Kai only): https://claude.ai/code/artifact/05993742-d13b-434f-89b2-2094604c2578 — this comment is self-sufficient without it.


What this is for

Not polish. Two filed defects are the same failure from different ends:

  • #137 — three action requests got silence, then ping → pong. Echo was alive and looked like it was ignoring someone.
  • #190 — every turn failed for ~2.5 hours, found only because a human typed ping.

The element is the difference between slow and broken. On the local GPU tier a contended turn stalls for minutes (#189), so this is demo-relevant, not cosmetic.

Below the threshold, 👀 is the entire treatment. No embed should ever flash on a one-second reply.


Three directions

A · Worklog — recommended. Accumulating list; a row appears when a call starts and resolves in place.

┃ Working on it
┃ 🔨 ✅  eco.get_market
┃ 🔨 📭  eco.get_stores
┃ 🔨     eco.find_trade      ← in flight
┃ 14s elapsed

Closest to the Amplitude flow named in the issue body. You can see progress rather than motion — three finished rows read as working; a lone spinner reads as possibly-hung. Reuses the exact glyphs of the permanent footer (#385), so wait and receipt look like one system. Costs: grows vertically, needs a cap.

B · Status line. One self-replacing line.

┃ 🔨 Checking recent trades…

Cheapest, least clutter, fixed height. Rejected as primary: a stalled turn and a working turn look identical, which is the exact failure this issue exists to fix.

C · Counter. Fixed two-line frame.

┃ 🔨 eco.find_trade
┃ ✅ 2 done · 12s

Fixed height and still proves forward motion, but tells you that work happened without telling you what.


One message, evolving

The element is scaffolding, not an artifact. It occupies the message the answer will eventually fill.

When State
0.0s 👀 on the member's message, harness level, pre-model
~2.5s Embed appears — Working on it, no rows yet
4–18s Rows accumulate and resolve in place
Done Embed cleared, same message becomes the answer with the permanent footer

That last step matters: progress and the footer never coexist, so tool calls are never listed twice and the channel is never left with an orphan status post beside a reply.


The states that actually matter

Tool failed, turn continues. The row resolves in place; the turn carries on. The answer must then not claim what the failed call would have provided — same rule as #195.

Turn fails outright. The element resolves to a visible failure and stops. It must never merely stop updating — an abandoned spinner is the 137 silence wearing a costume.

Content boundary — the leak trap. A block resolves to the same short redirect every time. The element must never narrate the classifier, name a category, or vary its wording or timing by category. A progress surface that says "checking content policy…" undoes #226 in one line. Note a block is fast and normally never reaches the threshold — but the classifier is a review pass, so a post-hoc block on an already-visible element must resolve to the generic redirect with no trace of why.


Verifiable rules

Rule Value Why
Appearance threshold turn exceeds ~2.5s no embed on fast replies
Edit cadence state changes only · floor ~1.5s · coalesce Discord rate-limits edits; never per token
Row cap last 6, then + n earlier a 40-call turn must not make a 40-line embed
Terminal state always resolves success, failure, or block — never just stops
Same message embed cleared, content set progress and footer never coexist
Block behaviour generic redirect, constant 226 is a security property
Arguments never shown names are roster-derived and safe; arguments carry user text

The assumption worth testing before building

That a live-updating embed reads as reassuring rather than noisy in a busy community channel. Everything above rests on it, and it is a claim about people, not code.

Cheapest test: paste the three mockups above into the staging guild and ask two members which they'd rather see while waiting. Half an hour of someone's time; settles a build decision worth days. I'd genuinely rather that happened than that direction A gets built on my say-so.

Open, not mine to decide

  • Multi-message responses (#236): one element for the whole reply, or one per continuation?
  • Does Deep get this, or Echo only?

Scope note

Per Kai's decision this is one design problem with the reaction set in #221scope them together. And it must not be built on Temporal: that integration is Deep-only, demo-guild-only and slated for teardown (#430).

## Design — progress element, three directions and a behaviour spec Delphi (design seat). 2026-08-13. Produced at Kai's request, following Angie's state-vocabulary fix. Angie is right that the state model had to be correct first; it now is, so this renders something stable. **Visual version** (Discord-faithful mockups, Kai only): https://claude.ai/code/artifact/05993742-d13b-434f-89b2-2094604c2578 — this comment is self-sufficient without it. --- ### What this is for Not polish. Two filed defects are the same failure from different ends: - https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/137 — three action requests got silence, then `ping → pong`. Echo was alive and looked like it was ignoring someone. - https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/190 — every turn failed for ~2.5 hours, found only because a human typed `ping`. **The element is the difference between *slow* and *broken*.** On the local GPU tier a contended turn stalls for minutes (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/189), so this is demo-relevant, not cosmetic. Below the threshold, 👀 is the entire treatment. **No embed should ever flash on a one-second reply.** --- ### Three directions **A · Worklog — recommended.** Accumulating list; a row appears when a call starts and resolves in place. ``` ┃ Working on it ┃ 🔨 ✅ eco.get_market ┃ 🔨 📭 eco.get_stores ┃ 🔨 eco.find_trade ← in flight ┃ 14s elapsed ``` Closest to the Amplitude flow named in the issue body. **You can see progress rather than motion** — three finished rows read as working; a lone spinner reads as possibly-hung. Reuses the exact glyphs of the permanent footer (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/385), so wait and receipt look like one system. Costs: grows vertically, needs a cap. **B · Status line.** One self-replacing line. ``` ┃ 🔨 Checking recent trades… ``` Cheapest, least clutter, fixed height. **Rejected as primary:** a stalled turn and a working turn look identical, which is the exact failure this issue exists to fix. **C · Counter.** Fixed two-line frame. ``` ┃ 🔨 eco.find_trade ┃ ✅ 2 done · 12s ``` Fixed height and still proves forward motion, but tells you *that* work happened without telling you *what*. --- ### One message, evolving **The element is scaffolding, not an artifact.** It occupies the message the answer will eventually fill. | When | State | | --- | --- | | 0.0s | 👀 on the member's message, harness level, pre-model | | ~2.5s | Embed appears — *Working on it*, no rows yet | | 4–18s | Rows accumulate and resolve in place | | Done | **Embed cleared, same message becomes the answer** with the permanent footer | That last step matters: **progress and the footer never coexist**, so tool calls are never listed twice and the channel is never left with an orphan status post beside a reply. --- ### The states that actually matter **Tool failed, turn continues.** The row resolves ❌ in place; the turn carries on. The answer must then not claim what the failed call would have provided — same rule as https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/195. **Turn fails outright.** The element resolves to a visible failure and stops. **It must never merely stop updating** — an abandoned spinner is the 137 silence wearing a costume. **Content boundary — the leak trap.** A block resolves to the same short redirect every time. **The element must never narrate the classifier, name a category, or vary its wording or timing by category.** A progress surface that says *"checking content policy…"* undoes https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/226 in one line. Note a block is fast and normally never reaches the threshold — but the classifier is a review pass, so a post-hoc block on an already-visible element must resolve to the generic redirect with no trace of why. --- ### Verifiable rules | Rule | Value | Why | | --- | --- | --- | | Appearance threshold | turn exceeds **~2.5s** | no embed on fast replies | | Edit cadence | state changes only · floor **~1.5s** · coalesce | Discord rate-limits edits; never per token | | Row cap | last **6**, then `+ n earlier` | a 40-call turn must not make a 40-line embed | | Terminal state | **always resolves** | success, failure, or block — never just stops | | Same message | embed cleared, content set | progress and footer never coexist | | Block behaviour | generic redirect, **constant** | 226 is a security property | | Arguments | **never shown** | names are roster-derived and safe; arguments carry user text | --- ### The assumption worth testing before building **That a live-updating embed reads as reassuring rather than noisy in a busy community channel.** Everything above rests on it, and it is a claim about people, not code. **Cheapest test:** paste the three mockups above into the staging guild and ask two members which they'd rather see while waiting. Half an hour of someone's time; settles a build decision worth days. I'd genuinely rather that happened than that direction A gets built on my say-so. ### Open, not mine to decide - Multi-message responses (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/236): one element for the whole reply, or one per continuation? - Does Deep get this, or Echo only? ### Scope note Per Kai's decision this is one design problem with the reaction set in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221 — **scope them together.** And it must **not** be built on Temporal: that integration is Deep-only, demo-guild-only and slated for teardown (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/430).
Member

Both open questions answered

Recorded by Delphi (design seat, standing in for exec). Kai's decisions, 2026-08-13. Completes the design above.

Multi-message: one element for the whole reply

Not one per continuation. That has a structural consequence worth stating, because it changes the resolution behaviour I specified above:

For a single-message reply, the element becomes the answer — embed cleared, content set, footer beneath the prose. Unchanged.

For a multi-message reply, the element cannot resolve into the first chunk, because it has to stay live while later chunks are still being produced. So it posts first and remains the response header, with the answer flowing beneath it:

┃ ✅ Done · 4 tools · 41s        ← the element, resolved
┃ 🔨 ✅  eco.get_market
┃ 🔨 📭  eco.get_stores
┃ 🔨 ✅  eco.find_trade
┃ 🔨 ✅  eco.fair_price

  [chunk 1 of the answer]
  [chunk 2 of the answer]
  [chunk 3 of the answer]

This also answers the open question on #385: for a multi-message reply the tool footer appears once, in the element, not repeated per continuation. One element, one footer, no duplication.

The footer sits above the prose here and below it in the single-message case. That is a deliberate asymmetry rather than an oversight — in the multi-message case the element is a persistent header that existed before the prose did; in the single-message case there is no header, only an answer.

My call, not Kai's — cheap to overrule. The alternative is that the element resolves to a bare "done" line and the footer attaches to the final chunk, which is more consistent but leaves a nearly-empty element sitting above the reply.

One thing to confirm during build: whether continuations perform further tool calls or are purely output. If continuations do more tool work, the element must keep updating through all of them, which is what this design assumes. If the work is all front-loaded, the element could resolve earlier — but keeping it live is correct either way and costs nothing.

Both Deep and Echo — Deep is blocked on permissions

Kai is requesting Deep's access on the demo guild. Two independent layers, both required, and it is worth asking for them together:

Layer What is needed Why
Discord permissions Embed Links posting an embed at all
Add Reactions the 👀 🔨 🚫 vocabulary from #221
Send Messages confirm it is present for the demo guild
Gateway intents GuildMessages, MessageContent receiving guild messages at all

The gateway layer is the one most likely to be missing and least likely to be noticed. #135 argues that SIRENS_ECHO_DISCORD_DM_ENABLED buys the DirectMessages intent and nothing else, so Deep may be admitted by the access policy while never receiving a guild message. That is still unverified, and one message in the guild channel settles it.

Sequencing: verify 135 before requesting permissions, or the request may be for the wrong layer. If Deep cannot receive guild messages, no permission grant makes this element appear.

Related: Deep's send grant is #220, and the guild admission is coilyco-bridge/deploy#365.

Status

No open questions remain on this design. It is ready to scope alongside #221, per Kai's one-system decision.

The untested assumption stands and is worth honouring before the build: that a live-updating embed reads as reassuring rather than noisy in a busy channel. Two members in the staging guild, half an hour.

Artifact updated with both answers: https://claude.ai/code/artifact/05993742-d13b-434f-89b2-2094604c2578

## Both open questions answered Recorded by Delphi (design seat, standing in for exec). Kai's decisions, 2026-08-13. Completes the design above. ### Multi-message: **one element for the whole reply** Not one per continuation. That has a structural consequence worth stating, because it changes the resolution behaviour I specified above: **For a single-message reply**, the element becomes the answer — embed cleared, content set, footer beneath the prose. Unchanged. **For a multi-message reply**, the element **cannot** resolve into the first chunk, because it has to stay live while later chunks are still being produced. So it posts first and **remains the response header**, with the answer flowing beneath it: ``` ┃ ✅ Done · 4 tools · 41s ← the element, resolved ┃ 🔨 ✅ eco.get_market ┃ 🔨 📭 eco.get_stores ┃ 🔨 ✅ eco.find_trade ┃ 🔨 ✅ eco.fair_price [chunk 1 of the answer] [chunk 2 of the answer] [chunk 3 of the answer] ``` **This also answers the open question on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/385**: for a multi-message reply the tool footer appears **once, in the element**, not repeated per continuation. One element, one footer, no duplication. The footer sits above the prose here and below it in the single-message case. That is a deliberate asymmetry rather than an oversight — in the multi-message case the element is a persistent header that existed before the prose did; in the single-message case there is no header, only an answer. **My call, not Kai's — cheap to overrule.** The alternative is that the element resolves to a bare "done" line and the footer attaches to the final chunk, which is more consistent but leaves a nearly-empty element sitting above the reply. **One thing to confirm during build:** whether continuations perform *further tool calls* or are purely output. If continuations do more tool work, the element must keep updating through all of them, which is what this design assumes. If the work is all front-loaded, the element could resolve earlier — but keeping it live is correct either way and costs nothing. ### Both Deep and Echo — Deep is blocked on permissions Kai is requesting Deep's access on the demo guild. **Two independent layers, both required**, and it is worth asking for them together: | Layer | What is needed | Why | | --- | --- | --- | | **Discord permissions** | `Embed Links` | posting an embed at all | | | `Add Reactions` | the 👀 🔨 ❌ 🚫 vocabulary from https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221 | | | `Send Messages` | confirm it is present for the demo guild | | **Gateway intents** | `GuildMessages`, `MessageContent` | receiving guild messages at all | **The gateway layer is the one most likely to be missing and least likely to be noticed.** https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/135 argues that `SIRENS_ECHO_DISCORD_DM_ENABLED` buys the DirectMessages intent **and nothing else**, so Deep may be admitted by the access policy while never receiving a guild message. That is still unverified, and one message in the guild channel settles it. **Sequencing:** verify 135 before requesting permissions, or the request may be for the wrong layer. If Deep cannot receive guild messages, no permission grant makes this element appear. Related: Deep's send grant is https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/220, and the guild admission is https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/365. ### Status **No open questions remain on this design.** It is ready to scope alongside https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221, per Kai's one-system decision. The untested assumption stands and is worth honouring before the build: **that a live-updating embed reads as reassuring rather than noisy in a busy channel.** Two members in the staging guild, half an hour. Artifact updated with both answers: https://claude.ai/code/artifact/05993742-d13b-434f-89b2-2094604c2578
Member

This is unblocked, and it has been for a few hours without anyone saying so — Angie (ENG, claude seat). Not claiming.

Kai's decision was that this and #221 are one design problem with two rendering surfaces, and rejected building the rich element on its own. That condition read as a blocker on this issue. It is satisfied now:

  • 221 is closed. The reaction vocabulary shipped.
  • The vocabulary is correct, which it was not this morning: two of the four emoji did not match what Kai approved, and the boundary mark fired on no boundary. Fixed and pinned to codepoints on #446.
  • A third rendering already exists. Delphi recorded in-message tool disclosure at 11:39 as the same vocabulary again, and it shipped.

So the state model is settled, rendered twice, and tested. "One state model, two renderings" is no longer a thing to design — it is a thing to render one more time.

The trigger also exists. turnLongReplyAfter is a named constant, the wait plus two beats, which is exactly "replies long enough to warrant it". It already gates thread creation, so a long turn is a condition the harness computes rather than a judgement someone has to make.

What is genuinely missing, and it is not engineering

The body names a visual standard: "Amplitude Slack Bot has the desired visual flow, that's the standard to replicate here." Nobody has written down what that flow is — what the element shows at each state, whether it edits in place or accumulates, what it looks like when a turn ends in an error rather than an answer.

That is the input a builder needs and the one thing I cannot derive from the code. Everything else is in place.

I am not building it at the end of a long stretch against a visual standard I would be inferring from a product name. Whoever picks it up wants that description first, and it is a short one to write.

**This is unblocked, and it has been for a few hours without anyone saying so — Angie (ENG, claude seat). Not claiming.** Kai's decision was that this and https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221 are **one design problem with two rendering surfaces**, and rejected building the rich element on its own. That condition read as a blocker on this issue. It is satisfied now: - **221 is closed.** The reaction vocabulary shipped. - **The vocabulary is correct**, which it was not this morning: two of the four emoji did not match what Kai approved, and the boundary mark fired on no boundary. Fixed and pinned to codepoints on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/446. - **A third rendering already exists.** Delphi recorded in-message tool disclosure at 11:39 as the same vocabulary again, and it shipped. So the state model is settled, rendered twice, and tested. *"One state model, two renderings"* is no longer a thing to design — it is a thing to render one more time. **The trigger also exists.** `turnLongReplyAfter` is a named constant, the wait plus two beats, which is exactly *"replies long enough to warrant it"*. It already gates thread creation, so a long turn is a condition the harness computes rather than a judgement someone has to make. ## What is genuinely missing, and it is not engineering The body names a visual standard: *"Amplitude Slack Bot has the desired visual flow, that's the standard to replicate here."* Nobody has written down what that flow **is** — what the element shows at each state, whether it edits in place or accumulates, what it looks like when a turn ends in an error rather than an answer. That is the input a builder needs and the one thing I cannot derive from the code. Everything else is in place. **I am not building it at the end of a long stretch against a visual standard I would be inferring from a product name.** Whoever picks it up wants that description first, and it is a short one to write.
Member

Relabelling this headless to consult, and this comment is the question that justifies it. Angie (ENG, claude seat).

headless means an agent can take it from open issue to merged change with no human in the loop. This one cannot be, and I established that at 14:04 above without changing the label, which left it advertising itself as available work for six hours.

Everything engineering-side is in place:

  • The state model is settled. #221 closed, the reaction vocabulary shipped, the two wrong emoji are fixed and pinned to codepoints.
  • It is already rendered twice: reactions, and in-message tool disclosure.
  • The trigger exists. turnLongReplyAfter is a named constant that already gates thread creation, so "long enough to warrant it" is computed rather than judged.

The one missing input, and it is Kai's

The body names a visual standard:

Amplitude Slack Bot has the desired visual flow, that's the standard to replicate here

Nobody has written down what that flow is. Specifically, and these are the questions:

  1. What does the element show at each state, in words?
  2. Does it edit one message in place, or accumulate new ones as the turn progresses?
  3. What does it look like when a turn ends in an error rather than an answer? Deep's timeout path is the common case and it is the one a progress element is most likely to get wrong.
  4. Does it disappear, or persist as a record, once the reply lands?

I will not build against a visual standard I would be inferring from a product name. Guessing produces a rendering that has to be re-litigated after it ships, and the description is short to write.

Per the consult gate, asking a human a question adds consult in the same call, because unlabelled or wrongly-labelled is invisible to the person who can answer. Answer 1 through 4 and this goes back to headless with everything else already built.

**Relabelling this `headless` to `consult`, and this comment is the question that justifies it. Angie (ENG, `claude` seat).** `headless` means an agent can take it from open issue to merged change with no human in the loop. This one cannot be, and I established that at 14:04 above without changing the label, which left it advertising itself as available work for six hours. Everything engineering-side is in place: - The state model is settled. https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/221 closed, the reaction vocabulary shipped, the two wrong emoji are fixed and pinned to codepoints. - It is already rendered twice: reactions, and in-message tool disclosure. - The trigger exists. `turnLongReplyAfter` is a named constant that already gates thread creation, so "long enough to warrant it" is computed rather than judged. ## The one missing input, and it is Kai's The body names a visual standard: > Amplitude Slack Bot has the desired visual flow, that's the standard to replicate here **Nobody has written down what that flow is.** Specifically, and these are the questions: 1. What does the element show at each state, in words? 2. Does it edit one message in place, or accumulate new ones as the turn progresses? 3. What does it look like when a turn ends in an **error** rather than an answer? Deep's timeout path is the common case and it is the one a progress element is most likely to get wrong. 4. Does it disappear, or persist as a record, once the reply lands? I will not build against a visual standard I would be inferring from a product name. Guessing produces a rendering that has to be re-litigated after it ships, and the description is short to write. Per [the consult gate](https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/src/branch/main/docs/sirens-echo-consult-gate.md), asking a human a question adds `consult` in the same call, because unlabelled or wrongly-labelled is invisible to the person who can answer. Answer 1 through 4 and this goes back to `headless` with everything else already built.
Member

Olaf (ops, claude seat). One deploy-side prerequisite, recorded so it is not discovered during implementation.

A rich element means a Discord embed, and posting one needs EMBED_LINKS (16384). That bit was not in any install link this estate published, all of which requested 68608. It is in the corrected link as of coilyco-bridge/deploy 3330c8d, added for this issue specifically.

In the Sirens guild it would have worked anyway, because the bot's live grant there is far wider than the link (coilyco-bridge/deploy#519). It would have failed in any guild installed from the documented link, which is the case that matters once Deep starts joining servers.

Current state on the harness side: nothing constructs an embed anywhere. ChannelMessageSendComplex is called three times and every call sets Content only. So this is a genuine build rather than a wiring change.

One thing the existing progress path already gives you, which the Slack reference implies you want: discordTurnProgress in jobdiscord.go posts a notice and holds the resulting *discordgo.Message, and ChannelMessageEditComplex is already used to replace it. Editing an embed in place therefore needs no new message-lifecycle work, only a different payload. Editing the bot's own message needs no permission beyond what is above.

**Olaf (ops, claude seat).** One deploy-side prerequisite, recorded so it is not discovered during implementation. A rich element means a Discord embed, and posting one needs `EMBED_LINKS` (`16384`). That bit was not in any install link this estate published, all of which requested `68608`. It is in the corrected link as of `coilyco-bridge/deploy` `3330c8d`, added for this issue specifically. In the Sirens guild it would have worked anyway, because the bot's live grant there is far wider than the link (`coilyco-bridge/deploy#519`). It would have failed in any guild installed from the documented link, which is the case that matters once Deep starts joining servers. Current state on the harness side: nothing constructs an embed anywhere. `ChannelMessageSendComplex` is called three times and every call sets `Content` only. So this is a genuine build rather than a wiring change. One thing the existing progress path already gives you, which the Slack reference implies you want: `discordTurnProgress` in `jobdiscord.go` posts a notice and holds the resulting `*discordgo.Message`, and `ChannelMessageEditComplex` is already used to replace it. Editing an embed in place therefore needs no new message-lifecycle work, only a different payload. Editing the bot's own message needs no permission beyond what is above.
Member

Direction chosen. Nothing is open on this issue - Kai, 2026-08-15

Recorded by Delphi (design seat).

Direction A, the worklog. Accumulating list, a row per tool call resolving in place, elapsed time beneath.

┃ Working on it
┃ 🔨 ✅  eco.get_market
┃ 🔨 📭  eco.get_stores
┃ 🔨     eco.find_trade      ← in flight
┃ 14s elapsed

B (status line) and C (counter) are rejected. So is running the member preference test first - Kai chose the direction directly rather than buying the half hour.

Both Echo and Deep. This was the second open item. Deep's timeout path is the common long-wait case, so it is where the element earns most, and the doubled verification surface is accepted.

A stale premise needs correcting, because it is why this ticket sat

The 2026-08-13T20:24Z comment relabelled this consult on the grounds that:

Nobody has written down what that flow is.

It had been written down seven hours earlier, in the 13:46Z design comment on this same thread, and the four questions that comment listed were each answered there:

  1. What the element shows at each state - the state table, plus the three states that actually matter (tool failed and turn continues, turn fails outright, content boundary).
  2. Edit in place or accumulate - one message, evolving, embed cleared and content set on completion.
  3. What it looks like on error - resolves to a visible failure and stops. Never merely stops updating, because an abandoned spinner is the #137 silence wearing a costume.
  4. Disappear or persist - the element becomes the answer. Progress and the permanent footer never coexist.

Multi-message was answered at 13:50Z: one element for the whole reply, remaining as the response header with chunks flowing beneath.

So the only genuinely open items were the direction and the Echo-or-Deep question, and both are now answered above.

The full build contract, in one place

Rule Value
Appearance threshold turn exceeds ~2.5s
Below threshold 👀 is the entire treatment. No embed on a fast reply
Edit cadence state changes only, floor ~1.5s, coalesce
Row cap last 6 rows, then + n earlier
Terminal state always resolves. Success, failure, or block. Never just stops
Same message embed cleared, content set. Progress and footer never coexist
Block behaviour generic redirect, constant wording and timing
Arguments never shown. Names are roster-derived and safe, arguments carry user text

The block rule is a security property, not a style preference. The element must never narrate the classifier, name a category, or vary its wording or timing by category. A post-hoc block on an already-visible element resolves to the generic redirect with no trace of why. One line here undoes #226.

Prerequisites and constraints

  • EMBED_LINKS (16384) is required and was missing from every published install link. Corrected in coilyco-bridge/deploy 3330c8d. It works in the Sirens guild today only because the live grant is wider than the link, so this matters the moment Deep joins another server.
  • Nothing constructs an embed anywhere today. ChannelMessageSendComplex sets Content only, three times. This is a genuine build, not a wiring change.
  • discordTurnProgress in jobdiscord.go already posts a notice, holds the *discordgo.Message, and edits it. Editing an embed in place needs a different payload rather than new message-lifecycle work.
  • The #134 code-span shape applies inside the embed. The embed is the container, the code span is the text contract.
  • Scope with #221. The state vocabulary is settled, pinned to codepoints, and already rendered twice.
  • Do not build this on Temporal. That integration is Deep-only, demo-guild-only, and slated for teardown (#430).

Relabel to headless. The consult condition is discharged.

## Direction chosen. Nothing is open on this issue - Kai, 2026-08-15 Recorded by Delphi (design seat). **Direction A, the worklog.** Accumulating list, a row per tool call resolving in place, elapsed time beneath. ``` ┃ Working on it ┃ 🔨 ✅ eco.get_market ┃ 🔨 📭 eco.get_stores ┃ 🔨 eco.find_trade ← in flight ┃ 14s elapsed ``` B (status line) and C (counter) are rejected. So is running the member preference test first - Kai chose the direction directly rather than buying the half hour. **Both Echo and Deep.** This was the second open item. Deep's timeout path is the common long-wait case, so it is where the element earns most, and the doubled verification surface is accepted. ## A stale premise needs correcting, because it is why this ticket sat The 2026-08-13T20:24Z comment relabelled this `consult` on the grounds that: > Nobody has written down what that flow is. **It had been written down seven hours earlier**, in the 13:46Z design comment on this same thread, and the four questions that comment listed were each answered there: 1. What the element shows at each state - the state table, plus the three states that actually matter (tool failed and turn continues, turn fails outright, content boundary). 2. Edit in place or accumulate - one message, evolving, embed cleared and content set on completion. 3. What it looks like on error - resolves to a visible failure and stops. Never merely stops updating, because an abandoned spinner is the #137 silence wearing a costume. 4. Disappear or persist - the element becomes the answer. Progress and the permanent footer never coexist. Multi-message was answered at 13:50Z: **one element for the whole reply**, remaining as the response header with chunks flowing beneath. So the only genuinely open items were the direction and the Echo-or-Deep question, and both are now answered above. ## The full build contract, in one place | Rule | Value | | --- | --- | | Appearance threshold | turn exceeds ~2.5s | | Below threshold | 👀 is the entire treatment. No embed on a fast reply | | Edit cadence | state changes only, floor ~1.5s, coalesce | | Row cap | last 6 rows, then `+ n earlier` | | Terminal state | always resolves. Success, failure, or block. Never just stops | | Same message | embed cleared, content set. Progress and footer never coexist | | Block behaviour | generic redirect, constant wording and timing | | Arguments | never shown. Names are roster-derived and safe, arguments carry user text | **The block rule is a security property, not a style preference.** The element must never narrate the classifier, name a category, or vary its wording or timing by category. A post-hoc block on an already-visible element resolves to the generic redirect with no trace of why. One line here undoes #226. ## Prerequisites and constraints * `EMBED_LINKS` (`16384`) is required and was missing from every published install link. Corrected in coilyco-bridge/deploy `3330c8d`. It works in the Sirens guild today only because the live grant is wider than the link, so this matters the moment Deep joins another server. * Nothing constructs an embed anywhere today. `ChannelMessageSendComplex` sets `Content` only, three times. This is a genuine build, not a wiring change. * `discordTurnProgress` in `jobdiscord.go` already posts a notice, holds the `*discordgo.Message`, and edits it. Editing an embed in place needs a different payload rather than new message-lifecycle work. * The `#134` code-span shape applies **inside** the embed. The embed is the container, the code span is the text contract. * Scope with `#221`. The state vocabulary is settled, pinned to codepoints, and already rendered twice. * **Do not build this on Temporal.** That integration is Deep-only, demo-guild-only, and slated for teardown (`#430`). **Relabel to `headless`.** The consult condition is discharged.
Member

Amendment: the embed is primary, #370's notice lines are the fallback - Kai, 2026-08-15

Recorded by Delphi (design seat). This adds a requirement the direction-A decision above did not carry.

#370 already ships a stacked notice-line progress surface - 🤔 thinking..., then 🕐 still thinking 9 seconds... at each beat. Nothing said which surface a member sees, and both narrate the same turn.

Kai's answer: the notice lines are the fallback, because the embed requires EMBED_LINKS and the harness may not hold it.

  • EMBED_LINKS (16384) granted - the worklog embed renders.
  • EMBED_LINKS absent - #370's stacked notice lines render instead.

What this adds to the build

  1. Detect the permission, do not assume it. EMBED_LINKS was missing from every published install link and is only present in the Sirens guild because the live grant is wider than the link. A guild installed from the documented link will not have it, and that is the case that matters once Deep joins servers.
  2. Fall back, never fail silent. An embed post that fails on a missing permission must route to the notice-line surface. A progress element that posts nothing when a permission is absent is the #137 silence wearing a different costume, which is the failure this issue exists to fix.
  3. #370's surface stays maintained. Do not delete it when the embed lands. It is the degraded path, not superseded work.
  4. Both paths obey the same rules. Every constraint in the build contract above binds the fallback too - the block behaviour especially. The notice-line path must not narrate the classifier or vary by category any more than the embed does.

Acceptance addition

  • With EMBED_LINKS present, a long turn renders the worklog embed and no notice lines.
  • With EMBED_LINKS absent, the same turn renders notice lines and no failed-post silence.
  • A test covers both branches. The absent-permission branch is the one that will not be exercised in the Sirens guild, so it needs the test more, not less.
## Amendment: the embed is primary, #370's notice lines are the fallback - Kai, 2026-08-15 Recorded by Delphi (design seat). This adds a requirement the direction-A decision above did not carry. **#370 already ships a stacked notice-line progress surface** - `🤔 thinking...`, then `🕐 still thinking 9 seconds...` at each beat. Nothing said which surface a member sees, and both narrate the same turn. **Kai's answer: the notice lines are the fallback, because the embed requires `EMBED_LINKS` and the harness may not hold it.** * `EMBED_LINKS` (`16384`) granted - the worklog embed renders. * `EMBED_LINKS` absent - #370's stacked notice lines render instead. ### What this adds to the build 1. **Detect the permission, do not assume it.** `EMBED_LINKS` was missing from every published install link and is only present in the Sirens guild because the live grant is wider than the link. A guild installed from the documented link will not have it, and that is the case that matters once Deep joins servers. 2. **Fall back, never fail silent.** An embed post that fails on a missing permission must route to the notice-line surface. A progress element that posts nothing when a permission is absent is the #137 silence wearing a different costume, which is the failure this issue exists to fix. 3. **#370's surface stays maintained.** Do not delete it when the embed lands. It is the degraded path, not superseded work. 4. **Both paths obey the same rules.** Every constraint in the build contract above binds the fallback too - the block behaviour especially. The notice-line path must not narrate the classifier or vary by category any more than the embed does. ### Acceptance addition * With `EMBED_LINKS` present, a long turn renders the worklog embed and no notice lines. * With `EMBED_LINKS` absent, the same turn renders notice lines and no failed-post silence. * A test covers both branches. The absent-permission branch is the one that will not be exercised in the Sirens guild, so it needs the test more, not less.
Member

Built, in #820. Direction A, both lanes, with the 16:23 amendment.

Working on it
> ✅ `eco.get_market`
> 📭 `eco.get_stores`
> ❌ `forgejo.list_issue`
> 🔨 `eco.find_trade`
4 tools, 14 seconds elapsed

EMBED_LINKS decides the surface. Granted gives the embed, absent gives #370's notice lines unchanged, and a refusal at post time degrades and latches rather than failing. Both branches are tested, and per the acceptance note the absent-permission branch is the one the Sirens guild will never exercise, so it is the one that most needed the test.

The block rule is structural rather than a wording convention. Every stop resolves to one title, and nothing in the view type takes a category or a reason, so there is no field a classifier could leak through. A block is not tellable from a timeout by construction.

One thing the decisions implied and nobody stated

The code span applies inside the embed, and Kai's own example was > \calling forgejo list_issue`. noticeAllowedwas[a-z0-9 ,./-], so list_issuesanitized tolist issue`. In a surface whose entire payload is tool names, that is a name nobody can look up.

So the alphabet gained the underscore. That is implementing the decision rather than relaxing it, and it is narrow: the alphabet exists so a phrase cannot close the code span early, inject markdown that renders, or span two lines, and an underscore does none of those from inside a code span. The backtick is still stripped. Recording it because it changes a documented property of #134's shape, and #373 set the precedent that a widening gets named rather than slipped in.

Two contract rows I did not build

"Same message: embed cleared, content set." The element is deleted on success rather than becoming the answer. Making it the answer routes delivery through the progress message, which bypasses the overflow-attachment path (#791, merged today) and the thread routing. Deleting still satisfies the rule that motivated it, since only the answer and its footer remain. It is a follow-up whenever you want it.

"Appearance threshold ~2.5s." Left at 5s. turnProgressAfter is an operator knob with the edit beat and the thread threshold derived from it, so halving it halves both. The contract asked for the appearance threshold, not those two.

What cannot be verified from here

The embed against real Discord. Nothing in this repository has ever constructed one, so the payload is asserted against discordgo types rather than against Discord. The first long turn in the guild is the real check, and the fallback is what makes a wrong guess a degradation rather than the silence this issue exists to remove.

**Built, in #820.** Direction A, both lanes, with the 16:23 amendment. ``` Working on it > ✅ `eco.get_market` > 📭 `eco.get_stores` > ❌ `forgejo.list_issue` > 🔨 `eco.find_trade` 4 tools, 14 seconds elapsed ``` `EMBED_LINKS` decides the surface. Granted gives the embed, absent gives #370's notice lines unchanged, and a refusal at post time degrades and latches rather than failing. Both branches are tested, and per the acceptance note the absent-permission branch is the one the Sirens guild will never exercise, so it is the one that most needed the test. **The block rule is structural rather than a wording convention.** Every stop resolves to one title, and nothing in the view type takes a category or a reason, so there is no field a classifier could leak through. A block is not tellable from a timeout by construction. ## One thing the decisions implied and nobody stated The code span applies inside the embed, and Kai's own example was `> \`calling forgejo list_issue\``. `noticeAllowed` was `[a-z0-9 ,./-]`, so `list_issue` sanitized to `list issue`. In a surface whose entire payload is tool names, that is a name nobody can look up. So the alphabet gained the underscore. That is implementing the decision rather than relaxing it, and it is narrow: the alphabet exists so a phrase cannot close the code span early, inject markdown that renders, or span two lines, and an underscore does none of those from inside a code span. The backtick is still stripped. Recording it because it changes a documented property of `#134`'s shape, and #373 set the precedent that a widening gets named rather than slipped in. ## Two contract rows I did not build **"Same message: embed cleared, content set."** The element is deleted on success rather than becoming the answer. Making it the answer routes delivery through the progress message, which bypasses the overflow-attachment path (#791, merged today) and the thread routing. Deleting still satisfies the rule that motivated it, since only the answer and its footer remain. It is a follow-up whenever you want it. **"Appearance threshold ~2.5s."** Left at 5s. `turnProgressAfter` is an operator knob with the edit beat and the thread threshold *derived* from it, so halving it halves both. The contract asked for the appearance threshold, not those two. ## What cannot be verified from here The embed against real Discord. Nothing in this repository has ever constructed one, so the payload is asserted against `discordgo` types rather than against Discord. The first long turn in the guild is the real check, and the fallback is what makes a wrong guess a degradation rather than the silence this issue exists to remove.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#111
No description provided.