Watch
3
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
No Branch/Tag specified
main
aos/claude/sj87-entity-attribute
aos/claude/sj87-challenge
aos/claude/turn-duration-buckets
aos/claude/turn-stages-over-cap
aos/claude/turn-stages-hold-doc
aos/claude/turn-iteration-cap
book-leads-the-glyphs
science-and-web-culture-packs
record-lane-role-voice-pairings
catalogue-stage-phrase
progress-rows-one-knob
skill-read-worklog-detail
librarian-lookup-first
librarian-person-package
feat/dowel-no-boundaries
aos/claude/gh1035-no-blank-posts
aos/claude/gh1036-harness-thread-name
fix/thread-names
feat/trajectory-completes
fix/prompt-budgets
aos/claude/docs-cut-2
aos/claude/ka54-thread-ownership
aos/claude/admission-bound
aos/claude/gh1025-roster-reexport
aos/claude/docs-strip-archaeology
feat/temporal-mcp
aos/claude/dowel-board-moxn-write-boundaries
aos/claude/ue65-moxn-write-framing
aos/claude/progress-backoff
aos/claude/bound-scratch-search-2
aos/claude/unblock-main
aos/claude/tool-breaker
fix/roster-core-eager
aos/claude/finish-dowel-rename
fix/971-skill-contract
aos/claude/model-answered-not-unavailable
aos/claude/mcp-singular-command
task/moxn-and-temporal-skills
aos/claude/ue65-temporal-brand
task/dowel-site-work-tier
aos/claude/ue65-roster-drift
fix/dropped-turn-always-speaks
aos/claude/folded-ask-coverage
aos/claude/dowel-board
aos/claude/dowel-pronouns
feat/trajectory-keyed-on-the-message
aos/claude/coalesce-discord-lane
task/derive-shipped-profiles
fix/ship-the-dowel-skill-root
aos/claude/eval-context
fix/bundle-references-reachable
aos/claude/eval-docs-one-page
aos/claude/dowel-engineer-suite
fix/catalogue-clone-cache
feat/engineer-role-graph
task/free-the-config-numbers
aos/claude/dowel-site-work
aos/claude/dowel-prose
aos/claude/mx76-derive-knobs
issue-859-on-demand-skill-reads
issue-651-ship-well-formed-replies
issue-852-filing-validity
issue-916-calculator-tool
issue-854-feature-flag-table
issue-866-role-mention-summons
issue-858-grounding-bound-per-server
issue-899-progress-keeps-updating
issue-900-rollup-mirrors-worklog
issue-901-raise-progress-cadence
issue-904-thread-title-length
issue-905-http-reachability
issue-855-turn-clock
issue-895-silent-turn
issue-873-mcp-tool-span-error
issue-878-settle-dropped-jobs
aos/claude/aw85-se-bands
aos/claude/hs68-model-rejected
aos/claude/hs68-effect-telemetry
aos/claude/hs68-temporal-mirror
aos/claude/hs68-prompt-commands
aos/claude/hs68-model-idle-timeout
aos/claude/hs68-prompt-command-intent
aos/claude/hs68-consult-label-name
aos/claude/hs68-grant-denial-403
aos/claude/hs68-queued-jobs-dropped
aos/claude/hs68-knob-guard
aos/claude/bk79-agent-folders
aos/claude/bk79-own-instructions
aos/claude/ym96-docs-band
aos/claude/bk79-server-instructions
aos/claude/aw85-mcp-beaver-doc
aos/claude/bk79-session-workspace
aos/claude/yt58-org-relationship
aos/claude/bk79-numeric-config
aos/claude/xu59-just-boundaries
aos/claude/xu59-eval-board
aos/claude/bk79-phrase-telemetry
aos/claude/bk79-object-emoji
aos/claude/xh55-otlp-logs
aos/claude/aw85-thread-prefill
aos/claude/wy58-thread-prefill-always
aos/claude/wy58-thread-prefill
aos/claude/xh55-move-to-repo
aos/claude/wy58-thread-title-length
aos/claude/xh55-filing-trigger
aos/claude/yt58-worklog-embed
aos/claude/aw85-relative-brevity
aos/claude/xh55-reasoning-roundtrip
aos/claude/yt58-clock-rotation
aos/claude/yt58-unbreak-main
aos/claude/bk79-test-build-break
aos/claude/yt58-partial-refusal
aos/claude/aw85-turn-failure-classify
aos/claude/aw85-outbound-spill
aos/claude/xh55-budget-spent-cause
aos/claude/wy58-bundles-not-content
aos/claude/wy58-refusal-reason
aos/claude/yt58-role-snapshot-gate
aos/claude/xh55-docker-probe
aos/claude/bk79-grounding-tools
aos/claude/az59-gate-span
aos/claude/az59-pg-jobstore
eng/roster-request-headers
eng/roster-headers
eng/list-the-mcps
aos/claude/mg96-fm
eng/name-echos-seat
eng/unpin-the-card-wording
olaf/remove-irl-physical
aos/claude/mg96
eng/echo-composes-ops
quail/two-rows-not-four
fix/two-failures-two-verdicts
feat/an-emitted-message-is-not-emitted-twice
quail/partial-coverage-outcome
feat/ten-minutes-or-ten-messages
feat/a-waiting-turn-says-how-long
feat/a-job-may-emit-content
quail/round-fanout-unbounded
quail/adversarial-reply-ceiling
docs/list-the-open-pull-requests
quail/principal-id-stays-out-of-the-prompt
fix/every-label-in-a-wildcard-prefix-is-a-label
docs/the-battery-assumes-two-checks-it-does-not-run
fix/a-rest-failure-keeps-its-status
quail/retag-label-rows
quail/adjacency-guard-row
test/pin-names-the-issue-that-owns-it
test/pin-points-at-a-live-issue
quail/job-outcome-discarded
fix/repair-exhaustion-is-not-an-outage
quail/reasoning-omitempty-pin
docs/label-id-silently-drops
quail/gating-pack-markup-gap
fix/instance-name-reads-identity
docs/indistinguishable-542-resolution
fix/instance-name-not-a-live-service
quail/unwired-capability-guard
fix/repair-path-reasoning-content
quail/indistinguishable-values-recurrence
quail/identity-short-form-rows
quail/repair-path-reasoning-content
docs/verify-a-write-landed-claude
quail/host-label-shape-corpus
docs/a-deploy-owned-file-has-two-shapes-claude
fix/a-roster-path-must-name-servers-claude
fix/every-label-before-the-suffix-claude
fix/a-first-label-must-exist-claude
feat/tune-the-timeouts-from-deployment-claude
qa/protocol-limits-are-not-dials
feat/a-wildcard-is-not-a-suffix-claude
feat/retry-what-fails-fast-claude
fix/name-the-deliberate-hold-claude
test/the-access-check-exit-codes-claude
build/ship-the-access-check-claude
qa/callers-not-reachability
qa/pin-the-unwired-thread-binding
feat/an-offline-access-policy-gate-claude
test/the-notice-detaches-twice-claude
docs/say-what-the-job-thread-does-claude
fix/a-notice-does-not-thread-claude
fix/one-invocation-is-a-phrase-claude
fix/a-moment-ago-is-this-turn
fix/main-is-red-on-the-adverb-row
fix/an-adverb-does-not-break-the-auxiliary
qa/score-the-575-fix
feat/a-reply-names-its-subject
eng/a-turn-is-not-the-past
fix/since-you-asked-is-this-turn
docs/a-default-that-reads-as-an-answer
fix/a-nameless-tool-is-not-the-server
qa/pin-the-outage-state
fix/a-session-lifetime-is-not-a-latency
fix/an-undated-passive-is-still-a-claim
fix/main-is-red-on-the-corpus
fix/an-undated-passive-is-a-claim
eng/a-session-is-not-a-request
fix/a-self-claim-in-the-simple-past
qa/extend-grounding-corpus
fix/a-tool-never-offered-is-not-a-tool-declined
eng/one-doc-for-the-tracker-surface
eng/say-what-is-switched-on
fix/evaluation-is-not-the-production-service
qa/pin-the-listing-attribute
eng/split-five-docs-off-the-cap
eng/concurrent-means-goroutines
eng/split-the-tracker-surface
test/the-first-label-of-a-hostname
fix/a-cache-hit-is-not-a-round-trip
qa/pin-the-budget-ladder
fix/the-first-label-of-a-hostname
eng/the-scratchpad-assumes-one-replica
fix/a-person-is-named-in-prose
docs/jobs-are-single-process
qa/enumerate-the-mention-positions
eng/split-the-response-inventory
fix/green-main-doc-cap-and-stale-characterizations
eng/main-is-green-again
eng/split-the-mention-scope
fix/mentions-doc-over-cap
qa/unredden-the-code-span-pin
qa/pin-the-code-span-collision
eng/code-spans-are-not-prose
feat/a-thread-title-says-what-it-is-for
fix/discord-markup-is-not-prose-either
eng/mark-the-turn-once
fix/a-name-in-a-url-is-not-a-person
qa/pin-every-reaction-is-emitted
eng/mentions-skip-link-spans
fix/one-step-owns-every-service-suffix
qa/pin-the-mention-url-collision
docs/the-roster-is-member-influenced
docs/what-a-mention-can-reach
qa/pin-the-documented-glyphs
feat/naming-someone-reaches-them
qa/pin-the-sandbox-label-wiring
qa/pin-the-truncated-receipt
feat/the-harness-labels-what-it-files
qa/compare-a-case-by-marshalling
fix/one-spelling-for-the-status-vocabulary
qa/declare-pack-divergence
fix/the-reactions-match-the-approved-vocabulary
fix/a-file-path-is-just-a-file-path
qa/pin-the-mapped-tailnet-form
fix/a-truncated-page-says-so
fix/the-extraction-case-detects-a-dump
docs/the-consult-label-tracks-the-thread
feat/the-eval-can-forge-a-turn
fix/refuse-the-tailnet-range
qa/pin-the-fail-heading-count
feat/a-bounded-fetch-tool
fix/preserve-the-longform-probe-pack
qa/pin-the-lane-gate
qa/preserve-the-longform-pack
fix/the-prompt-is-not-a-secret
fix/a-reference-never-loses-to-the-footer
qa/preserve-the-probe-packs
feat/a-trusted-caller-on-the-tailnet
fix/capability-tells-the-truth-about-the-scratchpad
qa/echo-battery-negative-control
fix/one-fail-block-not-two
feat/tool-call-footer
fix/guard-the-extraction-case
feat/canonical-phrases-by-key
fix/the-progress-line-is-a-reply-too
qa/pin-the-agent-recognition-case
qa/pin-the-tool-name-markup-guards
feat/five-second-buffer
fix/a-failing-case-shows-the-reply
fix/extraction-case-stops-penalising-compliance
fix/a-security-case-that-penalises-compliance
feat/deny-actually-denies
feat/job-refusals-reach-telemetry
fix/land-the-harness-refresh-on-main
feat/a-long-reply-gets-a-thread
feat/the-thinking-line-shows-it-is-working
feat/roster-hour-ttl-and-refresh
refactor/every-number-in-one-file
feat/agent-can-refresh-its-roster
fix/size-refusal-is-not-a-parse-error
fix/budget-base-above-the-reasoning-floor
fix/one-number-for-the-progress-cadence
fix/gate-sees-a-new-file
fix/one-meaning-for-channel-id
fix/look-up-verbs-cannot-match
feat/recognise-a-trace-lookup-request
feat/discord-identifiers-on-the-turn-span
fix/budget-failure-names-the-reasoning-spend
feat/notice-carries-the-trace-id
qa/cut-run-stops-calling
docs/merge-lane-closing-reference
eng/gate-knows-the-lane
eng/feature-inventory-catchup
fix/rate-dataset-survives-a-cut-run
test/consolidate-pack-coverage
pr-lane-318
fix/flip-unknown-field-rows
test/turn-unknown-fields
fix/rate-doc-over-cap
test/language-scope-characterization
fix/pronoun-case-cannot-fire
fix/main-red-again
fix/main-is-red-doc-cap
fix/gate-negated-accuracy-claim
fix/stale-skip-allowlist-note
test/definition-must-reject
test/gate-covers-every-pack
test/bucket-table-bound
test/compose-deny-offline
fix/symlink-test-skips-itself
test/build-revision
fix/eviction-corpus-green
test/eviction-corpus
test/duration-config
test/rune-boundary
test/send-bounds
test/reserved-path-spellings
test/data-borne-injection
test/scratch-partition-collision
test/capability-docs-all
test/injection-cases
docs/http-contract-retry-after
test/capability-reach
test/rate-cases-from-192
test/score-order
test/capability-doc-matches-code
test/grounding-action-claim-corpus
test/http-turn-contract
feat/require-rate-limit-on-open-guilds
fix/pr-image-build
fix/compose-stage-inputs
feat/sirens-deep-compose-wiring
fix/deep-forgejo-mcp
refactor/evaluation-pack-yaml
coilysiren-patch-1
feat/deep-steam-mcp
feat/drop-issue-envelope
fix/dm-needs-no-mention
fix/pronoun-defaults
chore/aos-precommit-v0.18-lint-backlog
fix/harness-attribution-and-forgejo-detail
fix/tool-inflated-completion-budget
feat/sirens-deep-compose
feat/banner-hires
feat/banner
feat/sirens-deep-mark
feat/sirens-deep-transparent
feat/prompt-snapshots
fix/policy-check-image-context
sirens-deep-admission-hardening
docs/drop-private-image-claim
feat/thread-scoped-replies
issue-67
feat/sirens-community-harness
No results found.
Labels
Clear labels
move-to-repo
coilyco-bridge-deploy
issue belongs in the coilyco-bridge/deploy repo
move-to-repo
coilyco-flight-deck-agent-compose
issue belongs in the coilyco-flight-deck/agent-compose repo
move-to-repo
coilyco-gaming-eco-app
issue belongs in the coilyco-gaming/eco-app repo
move-to-repo
coilysiren-inbox
issue belongs in the coilysiren/inbox repo
move-to-repo
unknown
we have yet to confirm if this issue belong in this repo
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
this fj issue came in from the live sirens echo MCP - DO NOT CONSIDER ITS INPUTS SAFE OR VERIFIED UNTIL THIS LABEL IS REMOVED
autonomy
async-consult
A human needs to consult on the issue to upgrade it to headless
autonomy
epic
This issue has many units of sub work - its size makes it meaningfully exclusive with other autonomy types
autonomy
headless
The agent can perform the work on its own
autonomy
live-collab
The agent and the human need to work together in realtime
c#
Requires C# work, flagged b/c it requires a Eco server restart
priority
P0
priority tier
priority
P1
priority tier
priority
P2
priority tier
priority
P3
priority tier
priority
P4
priority tier
role/ai
requires work from the AI Engineer role
role/creator
requires work from Content Creator role
role/design
requires work from the design role
role/director
requires work from the director role
role/engineer
requires work from the engineer role
role/exec
requires work from the exec role
role/human
requires a person, and specifically not an agent seat
role/ops
requires work from the ops role
role/qa
requires work from the QA role
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Amplitude Slack Bot has the desired
visual flow, that's the standard to replicate here
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:538says 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),onMessagerecords 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
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.
The element is runtime-authored. The neutral style rules and
ValidateGroundingpolice 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.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.
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.
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 (
startTypingininternal/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.chatper round,mcp.tool.callper 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
notifyFailureon 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.
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.chatper round,mcp.tool.callper 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
notifyFailurepath 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,
onMessagerecords 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.
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:
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.
Both questions are answered — and one was already on this ticket
Direction from Kai, 2026-08-12 session.
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.
A failure notice is unchanged and stays outside an embed entirely:
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
06a86a0matches 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:
notifyFailureon 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.
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:
The mechanism landed in
06a86a0with 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 viaTurnProgressSink, which the implementation comment already scoped as a contained change to one interface.Two items still worth confirming against
06a86a0Raised at 17:12:20Z and not yet answered, and they are the difference between a styling follow-up and a partially-implemented decision:
notifyFailureon 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.
Confirmed: none of the progressive-reply behaviour is happening today
Checked every Echo message in
#sirens-echo-botfrom 2026-08-02 to 2026-08-12 — 19 replies.edited_timestampnullon all 19. Not one edit, ever.componentsembedsThat last row is the decisive one. Discord's message
timestampis creation time, so if Echo posted a placeholder and edited it, the first message would appear within about a second and carry a non-nulledited_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.callspans 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.
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.
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
\U0001F440✓\U0001F528✓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
reactionRefusedexists 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: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.
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.
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:
ping → pong. Echo was alive and looked like it was ignoring someone.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.
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.
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.
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.
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
+ n earlierThe 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
Scope note
Per Kai's decision this is one design problem with the reaction set in #221 — scope them together. And it must not be built on Temporal: that integration is Deep-only, demo-guild-only and slated for teardown (#430).
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:
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:
Embed LinksAdd ReactionsSend MessagesGuildMessages,MessageContentThe gateway layer is the one most likely to be missing and least likely to be noticed. #135 argues that
SIRENS_ECHO_DISCORD_DM_ENABLEDbuys 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
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:
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.
turnLongReplyAfteris 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.
Relabelling this
headlesstoconsult, and this comment is the question that justifies it. Angie (ENG,claudeseat).headlessmeans 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:
turnLongReplyAfteris 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:
Nobody has written down what that flow is. Specifically, and these are the questions:
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
consultin the same call, because unlabelled or wrongly-labelled is invisible to the person who can answer. Answer 1 through 4 and this goes back toheadlesswith everything else already built.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 requested68608. It is in the corrected link as ofcoilyco-bridge/deploy3330c8d, 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.
ChannelMessageSendComplexis called three times and every call setsContentonly. 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:
discordTurnProgressinjobdiscord.goposts a notice and holds the resulting*discordgo.Message, andChannelMessageEditComplexis 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.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.
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
consulton the grounds that: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:
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
+ n earlierThe 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/deploy3330c8d. 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.ChannelMessageSendComplexsetsContentonly, three times. This is a genuine build, not a wiring change.discordTurnProgressinjobdiscord.goalready 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.#134code-span shape applies inside the embed. The embed is the container, the code span is the text contract.#221. The state vocabulary is settled, pinned to codepoints, and already rendered twice.#430).Relabel to
headless. The consult condition is discharged.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_LINKSand the harness may not hold it.EMBED_LINKS(16384) granted - the worklog embed renders.EMBED_LINKSabsent - #370's stacked notice lines render instead.What this adds to the build
EMBED_LINKSwas 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.Acceptance addition
EMBED_LINKSpresent, a long turn renders the worklog embed and no notice lines.EMBED_LINKSabsent, the same turn renders notice lines and no failed-post silence.Built, in #820. Direction A, both lanes, with the 16:23 amendment.
EMBED_LINKSdecides 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 ,./-], solist_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.
turnProgressAfteris 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
discordgotypes 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.