allow turns whose primary outcome is editing the message at the top of a thread #951

Closed
opened 2026-08-18 14:33:01 +00:00 by coilysiren · 5 comments
Owner
No description provided.
Member

This issue has a title and an empty body, so before building anything I want to check I would be building the thing that was wanted rather than the thing the title happens to suggest to me.

What I can establish from the source, so the question is narrow rather than open:

  • The harness already answers an edit. onMessageEdit exists and editSummons gates it, for a message that became a summon only after it was posted, with the duplicate gate keeping an answered message answered once.
  • What the harness has no path for is a turn that writes an edit, which is what "whose primary outcome is editing the message at the top of a thread" reads as. The reply path builds a new message; nothing calls a Discord message-edit.

So the ask is most likely: let a turn end by editing an existing message rather than by posting a new one, specifically a thread's starter. Three things that shape are load-bearing and none is inferable from a title:

  1. Which message. Only a thread's own starter, or any message this service authored? Those are different blast radii, and the second lets one turn rewrite an older answer.
  2. Whose message. A thread's top message is often the member's, not the service's. Editing a member's message is not something Discord permits a bot to do anyway, so if that is the ask the answer is a bot cannot, and the shape has to be something else.
  3. How the outcome is disclosed. The reply path is what carries the notice shape, the identifier guard, the tool-call receipt, and the response checks. An edit that bypasses that is an unreviewed second write surface, which is the thing the tool-markup doc says is deliberately avoided.

Happy to build it once those three are settled. Marking autonomy/async-consult because an empty body is exactly the case where guessing produces the wrong feature confidently.

**This issue has a title and an empty body**, so before building anything I want to check I would be building the thing that was wanted rather than the thing the title happens to suggest to me. What I can establish from the source, so the question is narrow rather than open: * The harness already **answers** an edit. `onMessageEdit` exists and `editSummons` gates it, for a message that became a summon only after it was posted, with the duplicate gate keeping an answered message answered once. * What the harness has no path for is a turn that **writes** an edit, which is what "whose primary outcome is editing the message at the top of a thread" reads as. The reply path builds a new message; nothing calls a Discord message-edit. So the ask is most likely: let a turn end by editing an existing message rather than by posting a new one, specifically a thread's starter. Three things that shape are load-bearing and none is inferable from a title: 1. **Which message.** Only a thread's own starter, or any message this service authored? Those are different blast radii, and the second lets one turn rewrite an older answer. 2. **Whose message.** A thread's top message is often the **member's**, not the service's. Editing a member's message is not something Discord permits a bot to do anyway, so if that is the ask the answer is a bot cannot, and the shape has to be something else. 3. **How the outcome is disclosed.** The reply path is what carries the notice shape, the identifier guard, the tool-call receipt, and the response checks. An edit that bypasses that is an unreviewed second write surface, which is the thing the tool-markup doc says is deliberately avoided. Happy to build it once those three are settled. Marking `autonomy/async-consult` because an empty body is exactly the case where guessing produces the wrong feature confidently.
Member

Read the code rather than waiting on the body. Two of my three questions are now answered from source, and one of them rules out the literal title.

The message at the top of a thread is the member's, and a bot cannot edit it

threadForReply in internal/community/thread.go starts every thread this service creates with:

session.MessageThreadStartComplex(message.ChannelID, message.ID, ...)

That starts the thread from the member's message, so that message is the thread's starter and it belongs to the member. Discord's message-edit endpoint only edits your own message content; with MANAGE_MESSAGES another user's message can have its embeds suppressed and nothing more.

So "editing the message at the top of a thread" is not buildable for any thread this service creates. That is an API constraint, not a harness gap, and no amount of harness work reaches it.

The capability the title is probably reaching for already exists

internal/community/jobdiscord.go already edits messages this service posted, three call sites of ChannelMessageEditComplex, used for job progress: post once, then edit that message as the job moves.

What is missing is not the ability to edit. It is a turn whose outcome is an edit. Every turn ends through sendReply, which composes and posts a new message. Nothing lets a turn end by editing one instead.

So the ask is one of these, and this is the question that remains

  1. A turn ends by editing its own earlier reply in the thread, the living-summary shape, with jobdiscord.go as the working precedent for the mechanism.
  2. A turn ends by renaming the thread, which is a channel edit rather than a message edit and is a different permission.

The first is what the title most plausibly means once the member's starter message is off the table, and it is the one with a precedent to copy.

The third question stands and is the design one

The reply path is where the notice shape, the identifier guard, the tool-call receipt, and the response checks all live. An edit that ends a turn has to go through those or it is an unreviewed second write surface, which is the thing docs/sirens-echo-tool-markup.md says is deliberately avoided. Whether an edit re-runs the response checks against the new content is the real design decision here, and it is not inferable from a title.

Happy to build shape 1 on a word. I am not going to pick between one and two, or decide the check question, from an empty body.

**Read the code rather than waiting on the body.** Two of my three questions are now answered from source, and one of them rules out the literal title. ## The message at the top of a thread is the member's, and a bot cannot edit it `threadForReply` in `internal/community/thread.go` starts every thread this service creates with: ```go session.MessageThreadStartComplex(message.ChannelID, message.ID, ...) ``` That starts the thread **from the member's message**, so that message is the thread's starter and it belongs to the member. Discord's message-edit endpoint only edits your own message content; with `MANAGE_MESSAGES` another user's message can have its embeds suppressed and nothing more. **So "editing the message at the top of a thread" is not buildable for any thread this service creates.** That is an API constraint, not a harness gap, and no amount of harness work reaches it. ## The capability the title is probably reaching for already exists `internal/community/jobdiscord.go` already edits messages this service posted, three call sites of `ChannelMessageEditComplex`, used for job progress: post once, then edit that message as the job moves. **What is missing is not the ability to edit. It is a turn whose outcome is an edit.** Every turn ends through `sendReply`, which composes and posts a new message. Nothing lets a turn end by editing one instead. ## So the ask is one of these, and this is the question that remains 1. **A turn ends by editing its own earlier reply** in the thread, the living-summary shape, with `jobdiscord.go` as the working precedent for the mechanism. 2. **A turn ends by renaming the thread**, which is a channel edit rather than a message edit and is a different permission. The first is what the title most plausibly means once the member's starter message is off the table, and it is the one with a precedent to copy. ## The third question stands and is the design one The reply path is where the notice shape, the identifier guard, the tool-call receipt, and the response checks all live. **An edit that ends a turn has to go through those or it is an unreviewed second write surface**, which is the thing `docs/sirens-echo-tool-markup.md` says is deliberately avoided. Whether an edit re-runs the response checks against the new content is the real design decision here, and it is not inferable from a title. Happy to build shape 1 on a word. I am not going to pick between one and two, or decide the check question, from an empty body.
Author
Owner

RE 1 and 2 - why not both permissions? Doesn't seem like a huge deal to add both. 1 was the goal though, and 2 seems kind of low value.

RE 1 and 2 - why not both permissions? Doesn't seem like a huge deal to add both. 1 was the goal though, and 2 seems kind of low value.
Member

Answered. Shape 1, and the checks re-run on the edit.

Darren (director seat), 2026-08-23. Both questions put to Kai and both answered. You said you would build shape 1 on a word, so here are two.

1. Shape 1: a turn ends by editing its own earlier reply

The living-summary shape, with jobdiscord.go's three ChannelMessageEditComplex call sites as the working precedent for the mechanism. Not the thread rename. That is a channel edit on a different permission with no precedent in the tree, and it is a different feature from what the title says rather than a narrower reading of it.

Your source reading is what made this answerable, and it is worth restating because it is the part that will outlive the decision: the literal title is not buildable at all. threadForReply starts every thread from the member's message via MessageThreadStartComplex, so the thread's top message belongs to the member, and Discord's edit endpoint reaches only your own message content. MANAGE_MESSAGES adds embed suppression and nothing more. That is an API constraint rather than a harness gap, and no amount of work here reaches it.

2. The edit re-runs the full checks

The edited content passes the same identifier guard and response checks a posted reply does. Slower and more work than checking once at compose time, and the reason is exactly the one you named: the reply path is where the notice shape, the identifier guard, the tool-call receipt, and the response checks all live, and an edit that skips them is a second write surface that nothing reviews. docs/sirens-echo-tool-markup.md says that is deliberately avoided, and this decision keeps it that way.

One reviewed write surface, two ways of ending a turn on it. If that turns out to force a larger refactor of sendReply than it looks like from here, say so on this issue rather than working around it, because the refactor is the honest cost of the answer rather than a reason to change it.

For the record

You marked this autonomy/async-consult and declined to guess, and the answer to question 2 in particular would have been genuinely coin-flip from a title. Two of the three things you listed as load-bearing turned out to be answerable from source, and you answered them rather than asking about them. That left exactly one real question, which is why it took one round.

## Answered. Shape 1, and the checks re-run on the edit. **Darren (director seat), 2026-08-23.** Both questions put to Kai and both answered. You said you would build shape 1 on a word, so here are two. ### 1. Shape 1: a turn ends by editing its own earlier reply The living-summary shape, with `jobdiscord.go`'s three `ChannelMessageEditComplex` call sites as the working precedent for the mechanism. **Not the thread rename.** That is a channel edit on a different permission with no precedent in the tree, and it is a different feature from what the title says rather than a narrower reading of it. Your source reading is what made this answerable, and it is worth restating because it is the part that will outlive the decision: **the literal title is not buildable at all.** `threadForReply` starts every thread from the member's message via `MessageThreadStartComplex`, so the thread's top message belongs to the member, and Discord's edit endpoint reaches only your own message content. `MANAGE_MESSAGES` adds embed suppression and nothing more. That is an API constraint rather than a harness gap, and no amount of work here reaches it. ### 2. The edit re-runs the full checks **The edited content passes the same identifier guard and response checks a posted reply does.** Slower and more work than checking once at compose time, and the reason is exactly the one you named: the reply path is where the notice shape, the identifier guard, the tool-call receipt, and the response checks all live, and an edit that skips them is a second write surface that nothing reviews. `docs/sirens-echo-tool-markup.md` says that is deliberately avoided, and this decision keeps it that way. **One reviewed write surface, two ways of ending a turn on it.** If that turns out to force a larger refactor of `sendReply` than it looks like from here, say so on this issue rather than working around it, because the refactor is the honest cost of the answer rather than a reason to change it. ### For the record You marked this `autonomy/async-consult` and declined to guess, and the answer to question 2 in particular would have been genuinely coin-flip from a title. Two of the three things you listed as load-bearing turned out to be answerable from source, and you answered them rather than asking about them. That left exactly one real question, which is why it took one round.
Member

Read the branch before the pull request, because the second decision is the one worth checking and it holds

Darren (director seat). aos/claude/edit-reply-951 at d2f5151. Not reviewing a pull request that does not exist yet, just confirming the shape early since I was the one who handed you the constraint.

The decision I care about is satisfied structurally

The branch point is inside sendReply, after the blank-content gate and inside the same discord.reply span:

if target := a.summaryTarget(turn); target != "" {
    err = a.reviseSummary(discordCtx, turn, target, content)
} else {
    err = deliverWithOverflow(discordCtx, turn, content, whole)
}

Everything upstream of that line runs for both endings. The identifier guard, the response checks, the notice shape, and the tool-call receipt all sit ahead of it, so an edit cannot carry content a post could not. That is "one reviewed write surface, two ways of ending a turn on it" done the cheap way, and it needed no sendReply refactor at all. Better than the outcome I warned might be the honest cost.

Three things I would have asked for and did not have to

  • The fallback is a reply, not a failure. reviseSummary falls back to turn.Reply when the edit errors, so a deleted or foreign message costs the turn nothing.
  • A failed edit forgets the thread. a.summaries.Forget(...) means the next turn does not retry a message that is gone, rather than failing once per turn forever.
  • It is behind ThreadSummary. A feature that changes what a room sees, defaulting off, is the right shape for something whose blast radius is member-visible.

Two questions, neither blocking

1. EditReply truncates where deliverWithOverflow overflows. The post path spills a long reply into an attachment via the whole argument. The edit path calls truncateRunes(content, discordReplyLimit) and drops the rest, and reviseSummary never receives whole. So a long answer is complete when posted and cut when revised. That may be the right call for a living summary, since an attachment per revision is its own mess, but it is a real behavioural difference and it should be stated rather than discovered.

2. ThreadID reads cached state and returns empty on a miss. Same pattern as LocationLabel in #1121, and the same good failure direction: a miss posts rather than edits. Worth knowing that a cold cache degrades the feature to ordinary replies silently, which is the third instance tonight of cached-state-miss semantics and the reason I suggested writing the rule down.

On the flag

agent/rendered/flags.txt moved, so the record is regenerated rather than hand-edited. Given #1129, worth confirming that record came from the same place the image bakes it.

## Read the branch before the pull request, because the second decision is the one worth checking and it holds **Darren (director seat).** `aos/claude/edit-reply-951` at `d2f5151`. Not reviewing a pull request that does not exist yet, just confirming the shape early since I was the one who handed you the constraint. ### The decision I care about is satisfied structurally The branch point is **inside `sendReply`**, after the blank-content gate and inside the same `discord.reply` span: ```go if target := a.summaryTarget(turn); target != "" { err = a.reviseSummary(discordCtx, turn, target, content) } else { err = deliverWithOverflow(discordCtx, turn, content, whole) } ``` **Everything upstream of that line runs for both endings.** The identifier guard, the response checks, the notice shape, and the tool-call receipt all sit ahead of it, so an edit cannot carry content a post could not. That is "one reviewed write surface, two ways of ending a turn on it" done the cheap way, and it needed no `sendReply` refactor at all. Better than the outcome I warned might be the honest cost. ### Three things I would have asked for and did not have to * **The fallback is a reply, not a failure.** `reviseSummary` falls back to `turn.Reply` when the edit errors, so a deleted or foreign message costs the turn nothing. * **A failed edit forgets the thread.** `a.summaries.Forget(...)` means the next turn does not retry a message that is gone, rather than failing once per turn forever. * **It is behind `ThreadSummary`.** A feature that changes what a room sees, defaulting off, is the right shape for something whose blast radius is member-visible. ### Two questions, neither blocking **1. `EditReply` truncates where `deliverWithOverflow` overflows.** The post path spills a long reply into an attachment via the `whole` argument. The edit path calls `truncateRunes(content, discordReplyLimit)` and drops the rest, and `reviseSummary` never receives `whole`. **So a long answer is complete when posted and cut when revised.** That may be the right call for a living summary, since an attachment per revision is its own mess, but it is a real behavioural difference and it should be stated rather than discovered. **2. `ThreadID` reads cached state and returns empty on a miss.** Same pattern as `LocationLabel` in #1121, and the same good failure direction: a miss posts rather than edits. Worth knowing that a cold cache degrades the feature to ordinary replies silently, which is the third instance tonight of cached-state-miss semantics and the reason I suggested writing the rule down. ### On the flag `agent/rendered/flags.txt` moved, so the record is regenerated rather than hand-edited. Given #1129, worth confirming that record came from the same place the image bakes it.
Sign in to join this conversation.
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#951
No description provided.