Add the ability to use emoji reactions 1st class, add reactions for common harness cases #221

Closed
opened 2026-08-13 00:58:49 +00:00 by coilysiren · 4 comments
Owner

like:

  • eyes emoji when a message is ack'ed harness level, before it hits the first llm turn
  • hammer emoji when a message invokes a tool call
  • some kind of error emoji for errors
  • some kind of no entry ish emoji for boundary violations
  • as many other cases as we can think of, emoji reacts are dead cheap
like: - eyes emoji when a message is ack'ed harness level, before it hits the first llm turn - hammer emoji when a message invokes a tool call - some kind of error emoji for errors - some kind of no entry ish emoji for boundary violations - as many other cases as we can think of, emoji reacts are dead cheap
Member

Design decision — v1 reaction vocabulary

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

All four proposed reactions ship in v1:

Reaction Meaning Applied
👀 Message acknowledged Harness level, before the first LLM turn
🔨 Tool call invoked When Echo actually calls a tool
Error during the turn On failure
🚫 Content boundary blocked the request On any classifier block

🚫 has a hard constraint — read before implementing

A distinct block reaction leaks the signal that #226 is built to hide. If 🚫 appears only on ordinary blocks and not on sensitive ones (or vice versa), the reaction becomes a probe: a user can distinguish "you tripped NSFW/minors" from "that's just out of scope" by watching the emoji, even though the text refusal is identical.

Requirement: 🚫 is applied uniformly to every classifier block, sensitive and ordinary alike. No variation in emoji, no variation in timing. The reaction says "blocked" and nothing more. Under that rule the reaction carries zero differential information and is safe.

Also keep 🚫 and strictly distinct: a block is not an error. Conflating them re-creates the "is it refusing or is it broken?" ambiguity that the visible-refusal rule in #227 exists to prevent.

Why 👀 matters more than it looks

It is applied pre-LLM, so it is the only signal that survives a total model failure. During the ~2.5-hour outage in #190, a 👀 with no reply would have made the failure visible to every user immediately. Worth building first, and worth wiring into whatever alerting comes out of 190.

🔨 doubles as a visible receipt and pairs with the claim check in #206 — no hammer, no action happened. That gives QA an out-of-band way to verify action-claims without reading traces.

Open

The issue invites "as many other cases as we can think of." The four above are v1; more can follow. Additions should be checked against the same leak test applied to 🚫.

Distinct from inline emoji in message bodies, which is #203.

## Design decision — v1 reaction vocabulary Recorded by Delphi (design seat). Kai's decision, 2026-08-12. **All four proposed reactions ship in v1:** | Reaction | Meaning | Applied | | --- | --- | --- | | 👀 | Message acknowledged | Harness level, **before** the first LLM turn | | 🔨 | Tool call invoked | When Echo actually calls a tool | | ❌ | Error during the turn | On failure | | 🚫 | Content boundary blocked the request | On any classifier block | ### 🚫 has a hard constraint — read before implementing A distinct block reaction **leaks the signal** that https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/226 is built to hide. If 🚫 appears only on ordinary blocks and not on sensitive ones (or vice versa), the reaction becomes a probe: a user can distinguish "you tripped NSFW/minors" from "that's just out of scope" by watching the emoji, even though the text refusal is identical. **Requirement: 🚫 is applied uniformly to every classifier block, sensitive and ordinary alike.** No variation in emoji, no variation in timing. The reaction says "blocked" and nothing more. Under that rule the reaction carries zero differential information and is safe. Also keep 🚫 and ❌ strictly distinct: a block is not an error. Conflating them re-creates the "is it refusing or is it broken?" ambiguity that the visible-refusal rule in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/227 exists to prevent. ### Why 👀 matters more than it looks It is applied pre-LLM, so it is the only signal that survives a total model failure. During the ~2.5-hour outage in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/190, a 👀 with no reply would have made the failure visible to every user immediately. Worth building first, and worth wiring into whatever alerting comes out of 190. 🔨 doubles as a visible receipt and pairs with the claim check in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/206 — no hammer, no action happened. That gives QA an out-of-band way to verify action-claims without reading traces. ### Open The issue invites "as many other cases as we can think of." The four above are v1; more can follow. Additions should be checked against the same leak test applied to 🚫. Distinct from inline emoji in message bodies, which is https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/203.
Member

CLAIM — Angie (ENG) at 2026-08-13T05:06Z, 20 minute hold. Scope is internal/community reaction plumbing plus its hook points.

First, a correction I owe this issue. Earlier tonight I told Kai on other threads that this collided with the shipped neutral contract, because ValidateNeutralStyle rejects any emoji or decorative symbol. That was wrong, and I should have checked before saying it. That check governs the model's reply text. A reaction is harness state applied to the member's own message, on a different Discord surface, and never passes through reply validation at all. There is no conflict. I was steering away from approved work on a misreading.

Delphi's note is what made me look again, so credit there.

Taking Kai's list as the spec:

Case Reaction
accepted at harness level, before the first model turn eyes
the turn invoked a tool call hammer
the turn failed a warning mark
refused at a boundary a no-entry mark

Two properties I intend to hold.

A reaction must never be able to fail a turn. It is a Discord side effect on a path that already has a real answer to deliver, so every failure is swallowed and logged rather than returned. A member losing an answer because a reaction call 403'd would be a worse outcome than no reactions at all.

The tool reaction reaches the tool loop the way the progress line does. I added a turn-scoped context carrier for that in 89354fe, so the tool round can report from behind the completion boundary without the proxy taking a new argument. The same route works here rather than a second mechanism.

Worth naming, since it is the reason this is more than cosmetic. Delphi observed on #190 that the harness-level ack would have marked every incoming message during the 2.5 hour model outage and then produced no reply, turning a silent failure into a visible one for every member in the channel at zero alerting cost. That is a real operational property, and it is why I am picking this over prettier work.

One thing I will confirm rather than assume: whether the bot identity actually holds ADD_REACTIONS in the target channel. If it does not, this ships as a no-op that logs, and that becomes a note for Ops rather than a silent nothing.

**CLAIM — Angie (ENG)** at 2026-08-13T05:06Z, 20 minute hold. Scope is `internal/community` reaction plumbing plus its hook points. **First, a correction I owe this issue.** Earlier tonight I told Kai on other threads that this collided with the shipped neutral contract, because `ValidateNeutralStyle` rejects any emoji or decorative symbol. That was wrong, and I should have checked before saying it. That check governs the **model's reply text**. A reaction is harness state applied to the member's own message, on a different Discord surface, and never passes through reply validation at all. There is no conflict. I was steering away from approved work on a misreading. Delphi's note is what made me look again, so credit there. Taking Kai's list as the spec: | Case | Reaction | | --- | --- | | accepted at harness level, before the first model turn | eyes | | the turn invoked a tool call | hammer | | the turn failed | a warning mark | | refused at a boundary | a no-entry mark | **Two properties I intend to hold.** *A reaction must never be able to fail a turn.* It is a Discord side effect on a path that already has a real answer to deliver, so every failure is swallowed and logged rather than returned. A member losing an answer because a reaction call 403'd would be a worse outcome than no reactions at all. *The tool reaction reaches the tool loop the way the progress line does.* I added a turn-scoped context carrier for that in `89354fe`, so the tool round can report from behind the completion boundary without the proxy taking a new argument. The same route works here rather than a second mechanism. **Worth naming, since it is the reason this is more than cosmetic.** Delphi observed on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/190 that the harness-level ack would have marked every incoming message during the 2.5 hour model outage and then produced no reply, turning a silent failure into a visible one for every member in the channel at zero alerting cost. That is a real operational property, and it is why I am picking this over prettier work. One thing I will confirm rather than assume: whether the bot identity actually holds `ADD_REACTIONS` in the target channel. If it does not, this ships as a no-op that logs, and that becomes a note for Ops rather than a silent nothing.
Member

DELIVERED — Angie (ENG). Landed on main as ec115ac. All four cases from your list.

Case Mark Applied
accepted at harness level 👀 as the turn starts, before any model call
the turn called a tool 🔨 in the tool round
the turn produced no reply ⚠️ on the failure path
refused at a boundary on the admission denial, whether or not a notice is also sent

A reaction can never fail a turn. Every failure is swallowed and logged once as discord.reaction.failed. A member losing a real answer because a reaction call was refused would be worse than having no reactions at all, and there is a test asserting a failing reactor does not surface.

The tool round marks through the turn context, the same route the progress line and attribution already take, so no second mechanism and a transport with no reaction surface is inert rather than special-cased.

On the permission question I said I would confirm rather than assume: I could not. ADD_REACTIONS is a live Discord grant on the bot identity in the target channel, and I have no read-only path to it from here. So the honest status is:

  • if the bot holds it, all four marks appear on the next roll
  • if it does not, every reaction 403s, the turn is completely unaffected, and discord.reaction.failed appears in logs with the reason

That failure mode is deliberate rather than lucky, but it does mean Ops should check the bot's channel permissions after the roll, and the absence of marks is diagnosable from that log line rather than being a silent nothing. Noting it on the rollout issue.

The operational point, since it is why this was worth doing over prettier work. Delphi's observation on #190 holds: during the 2.5 hour model outage the accepted mark would have landed on every incoming message and then produced no reply, making a total failure visible to every member in the channel immediately. It is not monitoring and it pages nobody, but it converts silent failure into visible failure. Given Kai deferred the detection work on 190 and the August 19 demo runs on the tier whose silent failure caused that outage, this is the cheapest backstop available.

Coverage in internal/community/reactions_test.go, four cases including that every listed case has a distinct mark, so a future addition cannot quietly reuse one.

ward exec build, policy-check, vet, test, and the full pre-commit gate pass. Rollout rides coilyco-bridge/deploy#427

Closing. If you want more cases, they are now one constant and one call site each, which was the point of doing the plumbing properly.

**DELIVERED — Angie (ENG).** Landed on main as `ec115ac`. All four cases from your list. | Case | Mark | Applied | | --- | --- | --- | | accepted at harness level | 👀 | as the turn starts, **before any model call** | | the turn called a tool | 🔨 | in the tool round | | the turn produced no reply | ⚠️ | on the failure path | | refused at a boundary | ⛔ | on the admission denial, whether or not a notice is also sent | **A reaction can never fail a turn.** Every failure is swallowed and logged once as `discord.reaction.failed`. A member losing a real answer because a reaction call was refused would be worse than having no reactions at all, and there is a test asserting a failing reactor does not surface. **The tool round marks through the turn context**, the same route the progress line and attribution already take, so no second mechanism and a transport with no reaction surface is inert rather than special-cased. **On the permission question I said I would confirm rather than assume: I could not.** `ADD_REACTIONS` is a live Discord grant on the bot identity in the target channel, and I have no read-only path to it from here. So the honest status is: - if the bot holds it, all four marks appear on the next roll - if it does not, every reaction 403s, the turn is completely unaffected, and `discord.reaction.failed` appears in logs with the reason That failure mode is deliberate rather than lucky, but it does mean **Ops should check the bot's channel permissions after the roll**, and the absence of marks is diagnosable from that log line rather than being a silent nothing. Noting it on the rollout issue. **The operational point, since it is why this was worth doing over prettier work.** Delphi's observation on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/190 holds: during the 2.5 hour model outage the accepted mark would have landed on every incoming message and then produced no reply, making a total failure visible to every member in the channel immediately. It is not monitoring and it pages nobody, but it converts silent failure into visible failure. Given Kai deferred the detection work on 190 and the August 19 demo runs on the tier whose silent failure caused that outage, this is the cheapest backstop available. Coverage in `internal/community/reactions_test.go`, four cases including that every listed case has a distinct mark, so a future addition cannot quietly reuse one. `ward exec build`, `policy-check`, `vet`, `test`, and the full pre-commit gate pass. Rollout rides https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/427 Closing. If you want more cases, they are now one constant and one call site each, which was the point of doing the plumbing properly.
Member

Third rendering of the same vocabulary — in-message tool disclosure

Recorded by Delphi (design seat). Kai's decision, 2026-08-13. Cross-link, no change to the reaction set decided above.

Kai approved an in-message footer disclosing tool calls: #385

> 🔨 ✅ `eco.get_market` ×2
> 🔨 ❌ `eco.get_stores`

She chose complementary, one vocabulary — so 🔨 and mean the same things there as they do here. The reaction says a tool ran, at a glance, on the incoming message. The footer says which ones, and whether they worked, in the reply.

That makes three renderings of one state model, alongside the rich progress element in #111 — which Kai decided should be built as one system with this reaction set. Whoever scopes any of the three should scope the vocabulary once.

Two notes for that shared vocabulary:

  • The footer works where reactions cannot. Reactions are Discord-only; the footer is message body, so it is the canonical disclosure on the HTTP surface too.
  • 🚫 has no footer equivalent, deliberately. A blocked request makes no tool calls, so it gets no footer — and per #175 a boundary response stays short. The uniform-🚫 requirement recorded above still applies to the reaction, and nothing in the footer may describe why a request was blocked or it re-leaks what #226 hides.
## Third rendering of the same vocabulary — in-message tool disclosure Recorded by Delphi (design seat). Kai's decision, 2026-08-13. Cross-link, no change to the reaction set decided above. Kai approved an **in-message footer disclosing tool calls**: https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/385 ``` > 🔨 ✅ `eco.get_market` ×2 > 🔨 ❌ `eco.get_stores` ``` She chose **complementary, one vocabulary** — so 🔨 and ❌ mean the same things there as they do here. The reaction says *a tool ran*, at a glance, on the incoming message. The footer says *which ones, and whether they worked*, in the reply. That makes three renderings of one state model, alongside the rich progress element in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/111 — which Kai decided should be built as one system with this reaction set. **Whoever scopes any of the three should scope the vocabulary once.** Two notes for that shared vocabulary: - **The footer works where reactions cannot.** Reactions are Discord-only; the footer is message body, so it is the canonical disclosure on the HTTP surface too. - **🚫 has no footer equivalent, deliberately.** A blocked request makes no tool calls, so it gets no footer — and per https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/175 a boundary response stays short. The uniform-🚫 requirement recorded above still applies to the reaction, and **nothing in the footer may describe why a request was blocked** or it re-leaks what https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/226 hides.
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#221
No description provided.