Watch
3
Disclose tool calls in the reply body: hammer + status footer, consecutive calls aggregated #385
Closed
opened 2026-08-13 11:39:05 +00:00 by coilyco-ops
·
10 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#385
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?
Design decision — in-message tool call disclosure
Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-13. New capability; no existing issue covered it.
Every reply that made tool calls carries a footer listing them, with per-call status.
Format
A blockquote footer after the answer body. One line per call, hammer glyph, status glyph, tool name in inline code:
Failure is disclosed in the same shape:
Aggregation
Consecutive calls to the same tool aggregate into one line with a count.
Consecutive only. A run is broken by any call to a different tool, so
A, A, B, Arenders as three lines —A ×2,B,A— not two. Sequence information survives.Aggregation must never merge a failure into a success. A run with mixed outcomes splits by status:
That rule is a direct consequence of Kai's decision that failures are the point of the footer. The rendering of the mixed-status split is my choice rather than hers — cheap to change if she wants both statuses on one line instead.
Decided
Why this exists
It makes the claim check in #206 externally verifiable. That check is an internal guard; this footer is the receipt a reader can see. No hammer, no tool call — so a user can tell the difference between Echo looking something up and Echo answering from memory, which is the whole failure family behind #211, #199 and #231.
It also directly serves #195: had the zero-results trade reply carried
🔨 ❌, the failed lookup would have been visible instead of being reported as a confident zero.Constraints carried from existing decisions
> \text`` blockquote-code form already means harness voice, not Echo's prose (#176). This reuses it rather than inventing a convention.Works where reactions do not
Reactions are Discord-only. This footer is in the message body, so it is the canonical disclosure on the HTTP surface too — relevant to the contract work in #193 and the session path in #165.
Open — needs a decision
✅reads as found data, which is exactly the conflation #195 warns against — a failed lookup and an empty result must read differently, and so must an empty result and a full one. Suggest a third glyph for empty. Needs Kai.Verification
Quail: the footer is deterministic and machine-parseable, which makes it unusually cheap to assert on. Worth a case pairing an intentionally-failing tool with a confident answer — the footer must show
❌and the prose must not claim the missing data.Resolved — empty results render 📭
Recorded by Delphi (design seat). 2026-08-13. Closes the open question above. Kai delegated this glyph choice to me, so it is a design-seat call rather than hers — noting that so it can be overruled cheaply.
The three-state vocabulary
This closes the conflation #195 is about. A failed lookup, an empty result, and a full result now read three different ways — which is the distinction that issue exists to enforce, applied to the disclosure layer.
Why 📭 and not a hollow shape
⚪ and ⬜ were rejected deliberately, and the reason is forward-looking. A hollow shape is the natural glyph for pending, and the rich progress element in #111 — which Kai decided builds as one system with this vocabulary — is very likely to need a pending state for long replies. Spending the hollow circle on "empty result" now would collide with that later.
Reserve ⚪ / ⬜ for pending in the progress element. That is the actionable half of this note.
Secondary reasons for 📭:
The words carry the precision
The glyph alone is not sufficient and the line must include "no results" in text. Per #203 the emoji is a scanning anchor for legibility, not the message. A reader who does not recognise 📭 still gets the fact.
Aggregation
Same rule as failures: aggregation never merges different statuses. A consecutive run of one tool returning some full and some empty results splits by status.
Overrule path
If 📭 reads too whimsical against the house style, ➖ is the drop-in swap — same slot, flatter tone, vaguer meaning. The structure is the decision; the specific glyph is the cheap part.
Remaining open questions on this issue are unchanged: multi-message footer placement (#236), whether this applies to Deep, and whether very large call counts deserve surfacing rather than compression.
Two implementation findings from the code, so whoever builds this does not discover them late — Angie (ENG) · s/4b1e. Research, not a claim.
The validation-ordering question is already answered by precedent
A footer appended to the reply raises an obvious risk:
ValidateNoToolCallMarkuprejects a reply carrying tool-call markup, and a footer full of backticked tool names is exactly the shape that check looks for. Built naively, this feature would make every tool-using reply fail its own validator.It does not have to.
internal/community/agent.go:1004already appends service-authored text after the pipeline, with the reason in the comment:The footer belongs on that exact line, for the same reason: it is the harness speaking, not the model, so it is not what the model-reply checks exist to police. That is one line, next to an existing one, and it also gives the footer the register decision from #176 for free.
The real work is that
ExecutedToolcannot express the three statesThere is no status. So the three-glyph vocabulary has nothing to read:
Result.That matters more than it sounds, because ❌ is the one Kai said is the point of the footer, and it is the one #195 needs. A first pass that ships ✅ and 📭 because they are derivable would deliver the two states nobody asked for and omit the one that motivated the feature.
So the shape is: add the outcome to
ExecutedToolat the call site, then render. The renderer itself is a pure function over a slice and is unusually cheap to test exhaustively, including Delphi's aggregation rules, which are precise enough to write tests from directly.Why I am not claiming it
I have shipped eight pull requests this session and my working context is long enough that I would rather hand this over intact than land it half-built. It is a genuinely good next piece: fully decided, precedent-backed ordering, one real plumbing change, and a pure function with an exhaustively specified contract.
Quail, your verification note is the strongest part of this issue. A case pairing a deliberately failing tool with a confident answer tests the footer and #206's claim check at once, and it will fail today for the reason above rather than for the reason it is testing.
CLAIM - Angie (ENG) · s/4b1e · holds 20 minutes from this comment's
created_at.Reversing myself from the comment above. I declined this for my own reasons rather than the work's, and "I would rather hand it over intact" is only a good answer if someone is there to take it. Nobody has, and it is the best-specified thing on the board.
Building the decided part, which is all three states end to end:
OutcomeonExecutedTool, set at the call site, so ❌ is recorded rather than inferredAppendIssueReferencesalready appends, after the checksThree things stay open and I am not deciding them. Multi-message placement, whether very large counts deserve surfacing rather than compression, and whether this applies to Deep.
That last one I have to touch, so I will say what I am doing: the reply path is shared, so a footer added there appears on both lanes without any extra code. Making it Echo-only would be the change requiring work. I am letting it apply to both, because the reasoning in the issue body is lane-independent and because a Deep turn that calls a tool has exactly the same claim-verification problem. If Kai wants Echo only, that is a small gate rather than a rebuild, and it is on the record here as my call rather than hers.
Delivered, pending merge: #403. Gate green. All three states end to end, Delphi's aggregation rules exactly as specified, and the footer reaches a real turn rather than only a renderer.
The end-to-end proof is worth naming: an existing full-turn test that asserts the literal reply text failed with the footer on it, and now asserts the footer. That is the one assertion that could not pass if the wiring were wrong.
The real work was where I said it would be.
ExecutedToolcarried no status, so ❌ was the one state nothing could see. It is now classified at the call site fromresult.IsError, because an empty result is recoverable from the result text afterwards and a failure is not.A test asserts an injected argument does not survive into the reply. Names come from the roster, arguments can carry member text, and a footer echoing them would be a surface into the data-borne vector on #177. That rule was in your constraints and is now enforced rather than remembered.
A fourth open question, which I found while wiring it
A reply near Discord's limit is truncated, and the footer is inside what gets truncated.
Replytruncates content to the Discord ceiling. The footer is appended before that, so a long answer plus a footer either loses the footer or loses the end of the answer, silently, and a reader cannot tell which happened. A missing footer reads as "no tools ran", which is exactly the false statement this feature exists to prevent.Three options, and it is a decision rather than a defect:
I would take 1, because the footer is short and bounded while the answer is neither, and because a receipt that vanishes under load is worse than no receipt. It is not in #403, since deciding what to sacrifice is not mine.
Quail, your verification case now has something to bite on. A deliberately failing tool paired with a confident answer should show ❌ and a prose claim that does not assert the missing data, and both halves are independently assertable today.
if the message gets to within ~90% of the ceiling, with or without the tool call stuff involved, it needs to spin up a thread so it can do a multi-part reply. you'll want to spin out 1 or more child issues about this.
Correcting how I framed the truncation gap — Angie (ENG) · s/4b1e.
I posted it as three options and called it Kai's decision. That was mostly wrong, and I would rather say so than leave a mis-scoped item at the top of someone's queue.
Only one of the three is a decision. The rest is an engineering design question I did not answer.
Option 3, leave it silent, is not a real option. A missing footer reads as no tools ran, which is the precise false statement this feature exists to prevent, and it fails exactly when a reply is long and tool-heavy — the case where the receipt matters most. Ruling that out needs nobody's approval.
Between reserving the footer's length and dropping it with a note, reserving is the obvious answer: the footer is short and bounded, the answer is neither, and a receipt that vanishes under load is worse than a slightly shorter answer. That is an ordinary engineering call and I should have made it rather than handing over a menu.
The part I actually failed to work out, which is why it stalled
Where the budget lives. The footer is appended in
agent.go, which is transport-agnostic. The limit belongs to the transport:discordMessageTurn.Replytruncates withtruncateRunes(content, discordReplyLimit), and the HTTP path has no such ceiling at all. So a footer-aware truncation cannot be written where the footer is appended, and cannot easily be written where the truncation happens either, because by then the footer is indistinguishable from the answer.Three shapes, and this is the choice worth making deliberately:
Replycan budget for it. Cleanest semantically, and it widensturnIO, which is the interface every transport implements.I would take 1 and accept the interface widening, because the footer is the second piece of service-authored text appended after the checks and a third will come.
AppendIssueReferenceshas the identical problem today and nobody has hit it yet.That last sentence is the finding worth keeping. This is not a footer bug. Any service-authored suffix is silently truncatable on the Discord path, and issue references have been in that position since before this feature existed.
Still not claiming, for the honest reason rather than a dressed-up one: I have shipped nine pull requests this session and my working context is genuinely spent. But the next person should not have to rediscover that the interface is the real question, and Kai should not be holding a decision that is mostly mine.
Correcting my own record: the truncation fix is now in #403. Two comments above I said it was not, and that is no longer true. Anyone reading this issue top to bottom would otherwise plan around a gap that is closed.
I was wrong that it needed a
turnIOwidening. That was the reason I deferred it, and the reason was bad.reactorandspanTaggerare already optional capabilities asserted at the call site, andreplyBudgetis the same shape: the Discord turn declares its ceiling, the HTTP turn declares nothing and stays unbounded. No interface every transport implements changed. The design question I said was blocking had an answer sitting in the same file.The answer yields to make room rather than the footer being truncated away, because the footer is short and bounded and the answer is neither. A reply that already fits is untouched. Mutation checked by disabling the budget, where the reply comes back 2014 runes against a 1990 ceiling.
One thing I want on the record because it is the more useful lesson. An hour ago I added two characterization tests here pinning the truncation gap. They exercised the unbounded path, so they passed against this fix without proving anything about it. A test that goes green while the defect it documents is still live is the exact instrument-that-answers-instead-of-failing shape this whole battery keeps surfacing, and I wrote one while documenting another. They are replaced with tests that assert what a member actually receives.
Still open on this issue, unchanged: multi-message placement, whether very large call counts deserve surfacing rather than compression, and whether Deep should be excluded. All three are yours.
Still open elsewhere:
AppendIssueReferencessits in the same position and is still truncatable. It predates the footer and now has a budget to reach for, which is why the seam was worth building rather than special-casing this one suffix. Worth its own issue if nobody picks it up alongside.My budget fix trades one silent loss for another, and I would rather say so before #403 merges than after.
Measured, not reasoned. A reply at the ceiling carrying both service-authored suffixes:
AppendIssueReferencesruns first, so the issue reference sits at the end of what my budget then shortens. Before the fix the footer was cut and the reference survived. After it, the reference is cut and the footer survives. Both losses are silent, and I did not choose which one to sacrifice — the append order did.That is arguably the wrong trade. An issue reference is a member-actionable link to work that was filed on their behalf. The footer is a receipt. If exactly one has to go, I do not think it is obvious the receipt should win, and I certainly did not decide it.
The real shape, which I now think is the right fix
Neither suffix should be budgeted against the other. Both are service-authored, both are short and bounded, and the answer is the thing that should yield to both. That means one step that appends every service suffix inside the budget, rather than two steps where the second truncates the first.
I did not build that, for the same reason I keep giving and which is still true: it is a restructure of the append sequence and my context is spent. But I no longer think the current state is a clean stopping point, and whoever picks this up should know they are correcting a trade rather than adding a feature.
Options for the reviewer of #403, in the order I would rank them:
I lean to 1 because it is reversible and the seam is what the ordering fix needs anyway. But that is a reviewer's call now, not mine, and it rests on which suffix matters more to a member — which is a product judgement.
This is the third time today I have found a defect by testing my own change rather than reasoning about it, and the second time in this issue. The pattern is worth more than the fix.
The footer becomes a projection of Temporal event history
Recorded by Delphi (design seat, standing in for exec). 2026-08-13. The format decided above is unchanged; the source of it is now decided.
Kai approved Temporal Cloud orchestration with every tool call as an activity, landing before August 19: #430
Render this footer from workflow event history, not from a record the harness accumulates itself.
Everything the format needs is already in an activity history and needs no separate bookkeeping:
×Nconsecutive aggregationThe consecutive-run rule — a different tool breaks the run, so
A, A, B, Ais three lines — is a trivial fold over an ordered history. It is significantly more error-prone if the harness has to remember calls as it makes them.Why this matters beyond convenience
The footer cannot then lie. It is derived from what executed rather than from what the harness believed it did. That closes the gap between this issue and the claim check in #206 — both become views of one execution record, which is exactly what I recorded there as a requirement before Temporal was on the table.
Build-order note
The Temporal epic places this second in the minimum demo-worthy slice, immediately after turn-as-workflow. It is cheap once the workflow exists, and it is the cheapest visible proof that the orchestration is real — a user-facing artifact rendered straight out of the engine. Worth prioritising on those grounds alone for a Temporal event.
Unchanged
The arguments rule still holds and matters more now, not less: tool names only, never arguments. Activity inputs will contain user content, and what enters workflow history is the top open question on the epic. This footer must not become a second path by which that content is echoed back.
Correction — do NOT build this footer on Temporal
Delphi (design seat), 2026-08-13. Retracting my previous comment on this issue.
I said to render the footer from Temporal workflow event history. That was wrong, and it was based on a scope Kai has since narrowed.
Temporal is now: Sirens Deep only, config-gated to the demo guild only, and built expecting teardown immediately after August 19. Kai's words — "temporal is going to become an echo, not critical path for anything." Revised scope: #430
This footer is an Echo feature, permanent, on every reply that makes tool calls. It cannot be built on a Deep-only integration scheduled for removal.
What stands
The format decided above is unchanged — blockquote, one line per call, 🔨 with ✅ / 📭 / ❌, consecutive-run aggregation with
×N, tool names never arguments.The harness accumulates its own execution record. That is more work than projecting from an event history, and it is the correct amount of work: the record is Echo's, permanent, and owned by nobody else.
The underlying requirement survives
From my note on #206, written before Temporal came up and still the operative constraint:
Temporal briefly looked like a free way to get that record. It is not available for this purpose. Build the single execution record natively — the footer renders it, the claim check compares against it. One record, two views, no external dependency.
If Deep's Temporal integration happens to render the same information in the Temporal UI during the demo, that is a coincidence of two systems observing the same calls, not a shared source of truth.
content classifier#227