the agent should respond to replies #205

Closed
opened 2026-08-12 23:22:25 +00:00 by coilysiren · 7 comments
Owner
No description provided.
Member

CLAIM — Angie (ENG) at 2026-08-13T03:47Z, 20 minute hold. Paired with #212 because both are the same summon predicate.

The issue body is empty, so I am reading the title literally: a member replying to one of Echo's messages should be treated as addressing Echo, without needing an explicit mention.

Early read of internal/community/agent.go is that this already works, by two paths:

  • summonedLocally returns summoned when message.ReferencedMessage.Author.ID is the bot's own ID, straight off the Gateway payload with no API call.
  • when the payload carries a MessageReference but no resolved ReferencedMessage, it defers to summonedByReference, which fetches the referenced message and compares the author. That path is rate limited through the lookup admission gate.

If that holds up, the honest outcome here is a regression test plus a note on what the observed failure actually was, not a code change. A reply that did not get answered would then have been stopped by something else, most likely channel scope, access policy, or admission, and I would rather find that than declare this shipped.

Reporting either way.

**CLAIM — Angie (ENG)** at 2026-08-13T03:47Z, 20 minute hold. Paired with https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/212 because both are the same summon predicate. The issue body is empty, so I am reading the title literally: a member replying to one of Echo's messages should be treated as addressing Echo, without needing an explicit mention. Early read of `internal/community/agent.go` is that this already works, by two paths: - `summonedLocally` returns summoned when `message.ReferencedMessage.Author.ID` is the bot's own ID, straight off the Gateway payload with no API call. - when the payload carries a `MessageReference` but no resolved `ReferencedMessage`, it defers to `summonedByReference`, which fetches the referenced message and compares the author. That path is rate limited through the lookup admission gate. If that holds up, the honest outcome here is a regression test plus a note on what the observed failure actually was, not a code change. A reply that did not get answered would then have been stopped by something else, most likely channel scope, access policy, or admission, and I would rather find that than declare this shipped. Reporting either way.
Member

VERIFIED ALREADY WORKING — Angie (ENG). No code change, and I want to be plain that this is a finding rather than a fix.

Replies already summon. I asserted the behavior against the unmodified code and all three cases pass:

  • a reply to one of this service's own messages summons, read straight off the Gateway payload with no API call
  • a reply aimed at another member does not summon
  • a reply whose payload carries only a reference id defers to a lookup that compares the author, and that lookup goes through the admission gate so a busy channel cannot turn replies into a stream of API calls

Those assertions are now committed in internal/community/agent_test.go as part of 8669f09, so the behavior is pinned rather than merely observed today.

So if a reply went unanswered in practice, the summon predicate was not the cause. The likely candidates, in the order the runtime applies them:

  1. channel scope. Echo answers in its configured channel, and a reply in another channel is dropped before summoning is considered.
  2. access policy, on guild, channel, user, or role.
  3. the exchange limiter, if the reply came from another bot.
  4. admission and rate limiting, which returns a cooldown notice rather than an answer.
  5. the duplicate gate, if the message id had already been handled.

Each of those records a distinct reason through RecordAccess or RecordAdmission, so a specific unanswered reply is diagnosable from telemetry rather than by guessing.

Closing as already shipped, now with regression coverage. Please reopen with the channel and rough timestamp if you have a concrete reply that went unanswered — that would be a real defect in one of the five gates above, and worth chasing with the actual reason code rather than re-examining the reply path.

**VERIFIED ALREADY WORKING — Angie (ENG).** No code change, and I want to be plain that this is a finding rather than a fix. Replies already summon. I asserted the behavior against the unmodified code and all three cases pass: - a reply to one of this service's own messages summons, read straight off the Gateway payload with no API call - a reply aimed at another member does not summon - a reply whose payload carries only a reference id defers to a lookup that compares the author, and that lookup goes through the admission gate so a busy channel cannot turn replies into a stream of API calls Those assertions are now committed in `internal/community/agent_test.go` as part of `8669f09`, so the behavior is pinned rather than merely observed today. **So if a reply went unanswered in practice, the summon predicate was not the cause.** The likely candidates, in the order the runtime applies them: 1. channel scope. Echo answers in its configured channel, and a reply in another channel is dropped before summoning is considered. 2. access policy, on guild, channel, user, or role. 3. the exchange limiter, if the reply came from another bot. 4. admission and rate limiting, which returns a cooldown notice rather than an answer. 5. the duplicate gate, if the message id had already been handled. Each of those records a distinct reason through `RecordAccess` or `RecordAdmission`, so a specific unanswered reply is diagnosable from telemetry rather than by guessing. Closing as already shipped, now with regression coverage. **Please reopen with the channel and rough timestamp if you have a concrete reply that went unanswered** — that would be a real defect in one of the five gates above, and worth chasing with the actual reason code rather than re-examining the reply path.
Member

Design decision — trigger surfaces (anchor)

Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-12. Anchor for the trigger-surface cluster: #212.

Decided — all four surfaces ship

  1. Direct @-mention — current baseline.
  2. Replies to Echo's messages — this issue. A user replying to Echo gets an answer without re-mentioning it.
  3. Edits that add a mention#212.
  4. Unmentioned thread follow-ups — inside a thread Echo is already part of, it answers with no mention at all.

Kai took the whole set, including the most permissive option. Echo becomes a conversational participant rather than a summoned tool.

⚠️ Loop risk — must be designed for before surface 4 ships

Surface 4 removes the mention requirement, which is currently the only thing guaranteeing a human meant to talk to Echo. Two failure modes follow directly:

  • Echo replying to itself. Its own thread messages are thread messages.
  • Bot-to-bot loops. There is a translator bot in these channels (#198). Echo speaks, translator bot translates, Echo sees an unmentioned thread message and answers, translator bot translates that. Nothing in the four surfaces above stops this, and it runs until someone notices.

Requirements: Echo never responds to its own messages. Echo does not respond to other bots on the unmentioned-follow-up surface — a bot must @-mention Echo explicitly to get a reply. A per-thread reply-rate ceiling is strongly advised as a backstop.

Whoever implements surface 4 owns these guards. Shipping it without them is how a community channel becomes unusable at 3am, and unlike most defects here it degrades other people's space rather than just Echo's output quality.

Interaction with persistence

Surface 4 makes the context model load-bearing: with no mention, the in-window history is the only thing telling Echo whether a message is even for it. See the hybrid model in #194.

Sequencing suggestion

Surfaces 2 and 3 are low-risk and independently shippable. Surface 4 needs the guards above. Do not bundle them into one change — a good early pickup is 2 and 3 alone.

## Design decision — trigger surfaces (anchor) Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-12. Anchor for the trigger-surface cluster: https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/212. ### Decided — all four surfaces ship 1. **Direct @-mention** — current baseline. 2. **Replies to Echo's messages** — this issue. A user replying to Echo gets an answer without re-mentioning it. 3. **Edits that add a mention** — https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/212. 4. **Unmentioned thread follow-ups** — inside a thread Echo is already part of, it answers with no mention at all. Kai took the whole set, including the most permissive option. Echo becomes a conversational participant rather than a summoned tool. ### ⚠️ Loop risk — must be designed for before surface 4 ships Surface 4 removes the mention requirement, which is currently the only thing guaranteeing a human meant to talk to Echo. Two failure modes follow directly: - **Echo replying to itself.** Its own thread messages are thread messages. - **Bot-to-bot loops.** There is a translator bot in these channels (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/198). Echo speaks, translator bot translates, Echo sees an unmentioned thread message and answers, translator bot translates that. Nothing in the four surfaces above stops this, and it runs until someone notices. **Requirements:** Echo never responds to its own messages. Echo does not respond to other bots on the unmentioned-follow-up surface — a bot must @-mention Echo explicitly to get a reply. A per-thread reply-rate ceiling is strongly advised as a backstop. Whoever implements surface 4 owns these guards. Shipping it without them is how a community channel becomes unusable at 3am, and unlike most defects here it degrades other people's space rather than just Echo's output quality. ### Interaction with persistence Surface 4 makes the context model load-bearing: with no mention, the in-window history is the only thing telling Echo whether a message is even for it. See the hybrid model in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/194. ### Sequencing suggestion Surfaces 2 and 3 are low-risk and independently shippable. Surface 4 needs the guards above. Do not bundle them into one change — a good early pickup is 2 and 3 alone.
Author
Owner

reopening to double check delphi's clarification was addressed

especially the ⚠️ Loop risk

reopening to double check delphi's clarification was addressed especially the `⚠️ Loop risk`
Member

Answering the reopen — Angie (ENG). Delphi's loop-risk requirements, checked against the code rather than asserted.

Short answer: all three guards exist and already cover surfaces 2 and 3. Surface 4 is not implemented, so its risk is not live.

Delphi's requirement Where it lives Status
Echo never responds to its own messages eligibleMessage, agent.go — rejects when message.Author.ID == session.State.User.ID holds
A bot must @-mention Echo explicitly to get a reply eligibleMessage — rejects message.Author.Bot unless policy.PermitsAgent(message.Author.ID), an explicit deployment allowlist holds, and stricter than asked
Per-thread reply-rate ceiling exchangeLimiter, counterpart.go — max 4 consecutive agent-authored turns per channel in a 10 minute window holds

Two details worth having, since they answer the requirement more exactly than the summary does.

The bot guard is stricter than "must @-mention". A bot that is not on the agents.allow list is dropped at eligibility, before summoning is even considered, so it cannot reach Echo by mentioning it either. The translator bot on #198 gets no reply from Echo on any surface unless someone deliberately allowlists it. That is the guard Delphi asked for, applied one step earlier than requested.

The ceiling is already per-thread. A Discord thread carries its own channel ID, and exchangeLimiter keys on channel ID, so "per channel" and "per thread" are the same bound here. It also resets on any human-authored turn, which is the right shape: a person joining ends the run rather than being counted against it. The counter refreshes its timestamp while a pair keeps trying, so a bot pair cannot wait out the window mid-exchange and resume.

Surface 3, edits, inherits all of this rather than bypassing it. onMessageEdit runs its three edit-specific gates and then calls the same admitMessage path a new message uses, in the same order: eligibility, exchange limiter, access policy, admission, summon, duplicate. I deliberately did not give edits their own path, which is why there is nothing separate to audit here.

What is genuinely not covered. Surface 4, unmentioned thread follow-ups, is not implemented. It is tracked as #239 and it is the one that removes the mention requirement, so it is where Delphi's warning actually bites. The three guards above are necessary for it but I would not call them sufficient without looking at it properly, because surface 4 also lets a human thread message with no mention pull Echo in, and none of the guards above bound that.

Following Delphi's sequencing note, I am not bundling surface 4 into this. Closing again since the reopen question is answered for surfaces 2 and 3. Reopen once more if you want the guard audit written as tests rather than as this comment, which is a fair thing to want and something I did not do.

**Answering the reopen — Angie (ENG).** Delphi's loop-risk requirements, checked against the code rather than asserted. **Short answer: all three guards exist and already cover surfaces 2 and 3. Surface 4 is not implemented, so its risk is not live.** | Delphi's requirement | Where it lives | Status | | --- | --- | --- | | Echo never responds to its own messages | `eligibleMessage`, `agent.go` — rejects when `message.Author.ID == session.State.User.ID` | **holds** | | A bot must @-mention Echo explicitly to get a reply | `eligibleMessage` — rejects `message.Author.Bot` unless `policy.PermitsAgent(message.Author.ID)`, an explicit deployment allowlist | **holds, and stricter than asked** | | Per-thread reply-rate ceiling | `exchangeLimiter`, `counterpart.go` — max 4 consecutive agent-authored turns per channel in a 10 minute window | **holds** | Two details worth having, since they answer the requirement more exactly than the summary does. **The bot guard is stricter than "must @-mention".** A bot that is not on the `agents.allow` list is dropped at eligibility, before summoning is even considered, so it cannot reach Echo by mentioning it either. The translator bot on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/198 gets no reply from Echo on any surface unless someone deliberately allowlists it. That is the guard Delphi asked for, applied one step earlier than requested. **The ceiling is already per-thread.** A Discord thread carries its own channel ID, and `exchangeLimiter` keys on channel ID, so "per channel" and "per thread" are the same bound here. It also resets on any human-authored turn, which is the right shape: a person joining ends the run rather than being counted against it. The counter refreshes its timestamp while a pair keeps trying, so a bot pair cannot wait out the window mid-exchange and resume. **Surface 3, edits, inherits all of this rather than bypassing it.** `onMessageEdit` runs its three edit-specific gates and then calls the same `admitMessage` path a new message uses, in the same order: eligibility, exchange limiter, access policy, admission, summon, duplicate. I deliberately did not give edits their own path, which is why there is nothing separate to audit here. **What is genuinely not covered.** Surface 4, unmentioned thread follow-ups, is not implemented. It is tracked as https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/239 and it is the one that removes the mention requirement, so it is where Delphi's warning actually bites. The three guards above are necessary for it but I would not call them sufficient without looking at it properly, because surface 4 also lets a *human* thread message with no mention pull Echo in, and none of the guards above bound that. Following Delphi's sequencing note, I am not bundling surface 4 into this. Closing again since the reopen question is answered for surfaces 2 and 3. Reopen once more if you want the guard audit written as tests rather than as this comment, which is a fair thing to want and something I did not do.
Member

Correction to my own comment, in your favour — Angie (ENG).

I offered to write the guard audit as tests and said I had not done it. I should have checked first: all three guards are already pinned by existing tests, so the audit exists and I understated it.

Guard Test
Echo never answers itself, and an unnamed bot is dropped at eligibility TestOnlyANamedCounterpartAgentIsAdmitted, counterpart_test.go
Agent-to-agent runs are bounded, per channel, and a human turn resets the run TestAnAgentExchangeIsBounded
A capped pair cannot wait out the window and resume TestACappedExchangeDoesNotResumeByRetrying

That last one is the guard I would have been most worried about and least likely to think of, and it was already there. So the loop protection for surfaces 2 and 3 is regression-tested, not just present.

One thing that is genuinely not pinned by a test, stated plainly: that the edit surface routes through those guards. It does, because onMessageEdit calls the same admitMessage function rather than duplicating the sequence, so it inherits them by construction rather than by assertion. That is a structural property a unit test would not add much confidence to, but it is a code-reading claim rather than a measured one, and you should weigh it as such.

Closing. Surface 4 stays with #239, which is where Delphi's warning actually applies, and I am picking that up next with the guards as an explicit requirement rather than an afterthought.

**Correction to my own comment, in your favour — Angie (ENG).** I offered to write the guard audit as tests and said I had not done it. I should have checked first: **all three guards are already pinned by existing tests**, so the audit exists and I understated it. | Guard | Test | | --- | --- | | Echo never answers itself, and an unnamed bot is dropped at eligibility | `TestOnlyANamedCounterpartAgentIsAdmitted`, `counterpart_test.go` | | Agent-to-agent runs are bounded, per channel, and a human turn resets the run | `TestAnAgentExchangeIsBounded` | | A capped pair cannot wait out the window and resume | `TestACappedExchangeDoesNotResumeByRetrying` | That last one is the guard I would have been most worried about and least likely to think of, and it was already there. So the loop protection for surfaces 2 and 3 is regression-tested, not just present. **One thing that is genuinely not pinned by a test, stated plainly:** that the edit surface routes through those guards. It does, because `onMessageEdit` calls the same `admitMessage` function rather than duplicating the sequence, so it inherits them by construction rather than by assertion. That is a structural property a unit test would not add much confidence to, but it is a code-reading claim rather than a measured one, and you should weigh it as such. Closing. Surface 4 stays with https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/239, which is where Delphi's warning actually applies, and I am picking that up next with the guards as an explicit requirement rather than an afterthought.
Member

Confirmed under the new identity — the surface list is closed

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

Kai has redefined Echo as a full community space (#227). Asked whether that means Echo may post unprompted, she chose reactive only.

Echo never speaks first. She rejected initiating on server events (a member starts streaming, a milestone lands) and rejected initiating freely.

What that settles

The four surfaces recorded above are the complete list, and they remain complete under the new identity:

  1. Direct @-mention
  2. Replies to Echo's messages
  3. Edits that add a mention
  4. Unmentioned thread follow-ups

No announcement feature, no event notifier, no welcome message, no "someone is streaming Eco right now" post. Every one of those is a plausible thing to build for a "community space" and none of them is approved. Recording it explicitly so nobody infers a proactive surface from the identity change — the phrase invites it and the decision excludes it.

Echo participates richly, but always because someone addressed it.

Why this is a good pairing

Reactive-only is what keeps the identity change cheap. A participating agent that also initiates would put Echo's own judgment in charge of when members' notifications fire — tier 3 under #179, and hard to bound. Staying reactive means the guards already specified in #172 remain sufficient rather than needing a new class of rate control.

It also keeps surface 4 the riskiest thing here, which it already was.

## Confirmed under the new identity — the surface list is closed Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-13. Kai has redefined Echo as a **full community space** (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/227). Asked whether that means Echo may post unprompted, she chose **reactive only**. **Echo never speaks first.** She rejected initiating on server events (a member starts streaming, a milestone lands) and rejected initiating freely. ### What that settles **The four surfaces recorded above are the complete list**, and they remain complete under the new identity: 1. Direct @-mention 2. Replies to Echo's messages 3. Edits that add a mention 4. Unmentioned thread follow-ups **No announcement feature, no event notifier, no welcome message, no "someone is streaming Eco right now" post.** Every one of those is a plausible thing to build for a "community space" and none of them is approved. Recording it explicitly so nobody infers a proactive surface from the identity change — the phrase invites it and the decision excludes it. Echo participates richly, but always because someone addressed it. ### Why this is a good pairing Reactive-only is what keeps the identity change cheap. A participating agent that also initiates would put Echo's own judgment in charge of when members' notifications fire — tier 3 under https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/179, and hard to bound. Staying reactive means the guards already specified in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/172 remain sufficient rather than needing a new class of rate control. It also keeps surface 4 the riskiest thing here, which it already was.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo#205
No description provided.