sirens-deep: per-channel addressing mode (trusted user implicit, others must mention) #136

Open
opened 2026-08-12 09:28:30 +00:00 by coilyco-ops · 9 comments
Member

Requested behavior

In a guild channel, decide whether a message is addressed to Deep:

  • Trusted user (identified by user ID via configuration) can summon Deep with or without an @ mention. Every message from that user is treated as directed at Deep unless the model determines otherwise.
  • Everyone else must @ mention Deep for their message to be treated as directed at it.
  • This operating mode is configurable per channel, not global.

Current state

Deep has no addressing logic, because it has never needed any. It is DM-only, where every message is inherently directed at it. Identity configuration today is a single trusted-speaker pair in sirens-deep-values.yaml:

- name: SIRENS_ECHO_PRINCIPAL_HANDLE
  value: coilysiren
- name: SIRENS_ECHO_PRINCIPAL_USER_ID
  value: "318190481467244544"

The values file notes the harness rejects a half-configured pair. That pair is the natural source for "trusted user," though this issue may need it to become a list rather than a single value.

The current de-facto mechanism is a plain mention — see message 1537024279743434822 in 1537024102886277210, which is a bare <@1534723490362429601>.

Design notes to settle

  1. Where the per-channel config lives. The access policy ConfigMap (sirens-deep-access-policy, mounted at /etc/sirens-deep/access-policy.yaml) already gates channels, so an addressing mode per channel entry is the natural shape — but that couples an access gate to a behavior setting. Alternative is a separate config block.
  2. Default for an unlisted channel. Recommend mention-required, so a channel gaining Deep access does not silently make it always-listening.
  3. "Unless the LLM determines otherwise" needs a definition. For a trusted user this is an opt-out (model may decline a message clearly not aimed at it). Confirm it is not also an opt-in for untrusted users, which would make the mention requirement advisory rather than enforced.
  4. Trusted user: one or many? Spec says "trusted user (via user ID through config)," singular. Flagging in case it needs to be a list.

Risk

Implicit addressing changes Deep from mention-triggered to always-listening for the trusted user in that channel. That raises token spend per channel message and widens exposure if the channel ever gains members. Both are acceptable in a two-party private channel; both scale badly if the mode is copied to a busier channel. This is the main reason the mode should be per-channel and default-off.

Depends on

Guild-channel operation must land first — Deep cannot address anything in a channel it cannot see.

Acceptance

  • In a channel configured implicit: trusted user's unmentioned message gets a response; a non-trusted user's unmentioned message does not; a non-trusted user's mentioned message does.
  • In a channel configured mention-required (and by default): trusted user's unmentioned message does not get a response.
  • DM behavior is unchanged.
  • Config is per channel — two channels with different modes behave differently in the same deployment.

Next owner

Engineer.

## Requested behavior In a guild channel, decide whether a message is addressed to Deep: - **Trusted user** (identified by user ID via configuration) can summon Deep **with or without** an `@` mention. Every message from that user is treated as directed at Deep unless the model determines otherwise. - **Everyone else** must `@` mention Deep for their message to be treated as directed at it. - This operating mode is **configurable per channel**, not global. ## Current state Deep has no addressing logic, because it has never needed any. It is DM-only, where every message is inherently directed at it. Identity configuration today is a single trusted-speaker pair in `sirens-deep-values.yaml`: ```yaml - name: SIRENS_ECHO_PRINCIPAL_HANDLE value: coilysiren - name: SIRENS_ECHO_PRINCIPAL_USER_ID value: "318190481467244544" ``` The values file notes the harness rejects a half-configured pair. That pair is the natural source for "trusted user," though this issue may need it to become a list rather than a single value. The current de-facto mechanism is a plain mention — see message `1537024279743434822` in `1537024102886277210`, which is a bare `<@1534723490362429601>`. ## Design notes to settle 1. **Where the per-channel config lives.** The access policy ConfigMap (`sirens-deep-access-policy`, mounted at `/etc/sirens-deep/access-policy.yaml`) already gates channels, so an addressing mode per channel entry is the natural shape — but that couples an access gate to a behavior setting. Alternative is a separate config block. 2. **Default for an unlisted channel.** Recommend mention-required, so a channel gaining Deep access does not silently make it always-listening. 3. **"Unless the LLM determines otherwise" needs a definition.** For a trusted user this is an opt-*out* (model may decline a message clearly not aimed at it). Confirm it is not also an opt-*in* for untrusted users, which would make the mention requirement advisory rather than enforced. 4. **Trusted user: one or many?** Spec says "trusted user (via user ID through config)," singular. Flagging in case it needs to be a list. ## Risk Implicit addressing changes Deep from mention-triggered to always-listening for the trusted user in that channel. That raises token spend per channel message and widens exposure if the channel ever gains members. Both are acceptable in a two-party private channel; both scale badly if the mode is copied to a busier channel. This is the main reason the mode should be per-channel and default-off. ## Depends on Guild-channel operation must land first — Deep cannot address anything in a channel it cannot see. ## Acceptance - In a channel configured implicit: trusted user's unmentioned message gets a response; a non-trusted user's unmentioned message does not; a non-trusted user's mentioned message does. - In a channel configured mention-required (and by default): trusted user's unmentioned message does **not** get a response. - DM behavior is unchanged. - Config is per channel — two channels with different modes behave differently in the same deployment. ## Next owner Engineer.
Author
Member

Two of the four design questions already have answers in the tree

Question 4, trusted user singular or plural. The access policy already models this as a list. GuildAccess.Users is an Allowlist accepting all or an explicit ID list, and Roles unions with it, so a role grant covers members nobody enumerated. SIRENS_ECHO_PRINCIPAL_* is a different thing: it is the one identity the prompt names as trusted, and the harness rejects a half-configured pair. Those two should not be merged. One is an admission gate, the other is a sentence in the system prompt about whose input to believe. A per-channel implicit-addressing list belongs with the former.

Question 1, where the per-channel config lives. The access policy entry is per guild, not per channel. Channels is a flat Allowlist of IDs with no room for a per-entry attribute, so an addressing mode cannot be hung off it without changing that type. That is the concrete cost of the "couples an access gate to a behavior setting" concern, and it is a real schema change rather than a naming preference.

The code change this needs

Addressing is decided in summonedLocally (internal/community/agent.go), which today is:

  • DM - summoned by definition
  • guild - summoned only by a mention, or by a reply to one of Deep's own messages

An implicit mode is a third branch there, keyed on the channel and the author. Note that the reply-reference path already costs a Discord REST lookup, rate-limited through a.lookups, and an always-listening channel would put every message from the trusted user through the turn path rather than through that gate. The token-spend risk in the issue is real and is not bounded by the mention gate any more.

On the dependency

Per my comment on #135, guild ingress is not a code gap. Gateway intents for guilds are already unconditional and the access policy already gates guild channels. So this issue's dependency is on a deploy change rather than on a build, which makes it startable sooner than the issue assumes.

Recommendation on question 3

Keep it an opt-out only. "Unless the model determines otherwise" should let the model decline a trusted user's message that clearly is not aimed at it, and must not let an untrusted user's unmentioned message in. Otherwise the mention requirement becomes advisory, which is a gate the operator cannot reason about.

Next owner

Still needs the human decision on questions 1 and 3 before implementation. Everything else is ready to build.

## Two of the four design questions already have answers in the tree **Question 4, trusted user singular or plural.** The access policy already models this as a list. `GuildAccess.Users` is an `Allowlist` accepting `all` or an explicit ID list, and `Roles` unions with it, so a role grant covers members nobody enumerated. `SIRENS_ECHO_PRINCIPAL_*` is a different thing: it is the one identity the *prompt* names as trusted, and the harness rejects a half-configured pair. Those two should not be merged. One is an admission gate, the other is a sentence in the system prompt about whose input to believe. A per-channel implicit-addressing list belongs with the former. **Question 1, where the per-channel config lives.** The access policy entry is per guild, not per channel. `Channels` is a flat `Allowlist` of IDs with no room for a per-entry attribute, so an addressing mode cannot be hung off it without changing that type. That is the concrete cost of the "couples an access gate to a behavior setting" concern, and it is a real schema change rather than a naming preference. ## The code change this needs Addressing is decided in `summonedLocally` (`internal/community/agent.go`), which today is: * DM - summoned by definition * guild - summoned only by a mention, or by a reply to one of Deep's own messages An implicit mode is a third branch there, keyed on the channel and the author. Note that the reply-reference path already costs a Discord REST lookup, rate-limited through `a.lookups`, and an always-listening channel would put every message from the trusted user through the turn path rather than through that gate. The token-spend risk in the issue is real and is not bounded by the mention gate any more. ## On the dependency Per my comment on #135, guild ingress is not a code gap. Gateway intents for guilds are already unconditional and the access policy already gates guild channels. So this issue's dependency is on a deploy change rather than on a build, which makes it startable sooner than the issue assumes. ## Recommendation on question 3 Keep it an opt-out only. "Unless the model determines otherwise" should let the model decline a trusted user's message that clearly is not aimed at it, and must not let an untrusted user's unmentioned message in. Otherwise the mention requirement becomes advisory, which is a gate the operator cannot reason about. ## Next owner Still needs the human decision on questions 1 and 3 before implementation. Everything else is ready to build.
Owner
  • 1: the config map yes
  • 2: agree with recommendation
  • 3: unsure... I guess what I meant, was that we send all messages to the LLM and the LLM makes a judgement call on whether or not it should act on the message???
  • 4: many
- 1: the config map yes - 2: agree with recommendation - 3: unsure... I guess what I meant, was that we send all messages to the LLM and the LLM makes a judgement call on whether or not it should act on the message??? - 4: many
Author
Member

All four design questions are now answered

Direction from Kai, 2026-08-12 session. Kai's inline answers earlier today closed 1, 2, and 4; this records the resolution of 3 and the schema shape that questions 1 and 4 imply.

The four

1. Where the per-channel config lives — the access policy ConfigMap. Confirmed.

2. Default for an unlisted channel — mention-required. Kai agreed with the recommendation. A channel gaining Deep access does not silently become always-listening.

3. "Unless the LLM determines otherwise" — hybrid. Kai's "unsure, I guess what I meant was we send all messages to the LLM and the LLM makes a judgement call" resolved to this, which is neither the pure opt-out recommendation nor a blanket model judgement:

trusted user, any message   -> model sees it, decides act or stay silent
untrusted, no mention       -> DROPPED IN CODE, never reaches the model
untrusted, with mention     -> model sees it, answers

So the mention gate stays a hard code-level rule for untrusted users — it is not advisory and an untrusted message can never argue its way in. The model judgement applies only inside the trusted lane, where it is an opt-out: the model may decline a trusted user's message that clearly is not aimed at it.

Enforcement is identical to the opt-out recommendation. The difference is that the trusted lane is explicitly a model judgement rather than a code rule, and token spend in an implicit channel scales with the trusted user's traffic rather than with total channel traffic.

4. Trusted user — many. It is a list.

Schema shape

Channels becomes a list of objects rather than a flat allowlist of ID strings:

guilds:
  "1300204416229441587":
    channels:
      - id: "1537024102886277210"
        addressing: implicit
      - id: "1537024102886277299"
        # addressing defaults to mention

Hard cutover, no dual-path parsing. Bare string IDs stop parsing; every deployed access policy ConfigMap is rewritten in the same window. This is a coordinated change across this repository and coilyco-bridge/deploy, and the engineer cannot verify the rollout from a sealed clone — the deploy side needs a merge and a live check from someone who can observe.

The trusted-user list lives inside the per-channel config, in the same ConfigMap. It is deliberately not merged with SIRENS_ECHO_PRINCIPAL_HANDLE / SIRENS_ECHO_PRINCIPAL_USER_ID. Those two remain what they are — the one identity the prompt names as trusted, a sentence about whose input to believe. Implicit addressing is an admission-adjacent behavior and belongs with the policy.

Dependency, restated

Per the #135 comment, guild ingress is a deploy change rather than a build, so this is startable sooner than the issue body assumes. The code change lands in summonedLocally (internal/community/agent.go) as a third branch keyed on channel and author.

Note the cost flagged there: the reply-reference path already spends a rate-limited Discord REST lookup, and an implicit channel puts every message from a trusted user through the turn path rather than through the mention gate. The token-spend risk is real and is no longer bounded by mentions.

Priority

Demo track, week to August 19.

## All four design questions are now answered Direction from Kai, 2026-08-12 session. Kai's inline answers earlier today closed 1, 2, and 4; this records the resolution of 3 and the schema shape that questions 1 and 4 imply. ## The four **1. Where the per-channel config lives — the access policy ConfigMap.** Confirmed. **2. Default for an unlisted channel — mention-required.** Kai agreed with the recommendation. A channel gaining Deep access does not silently become always-listening. **3. "Unless the LLM determines otherwise" — hybrid.** Kai's "unsure, I guess what I meant was we send all messages to the LLM and the LLM makes a judgement call" resolved to this, which is neither the pure opt-out recommendation nor a blanket model judgement: ``` trusted user, any message -> model sees it, decides act or stay silent untrusted, no mention -> DROPPED IN CODE, never reaches the model untrusted, with mention -> model sees it, answers ``` So the mention gate stays a **hard code-level rule** for untrusted users — it is not advisory and an untrusted message can never argue its way in. The model judgement applies only inside the trusted lane, where it is an opt-out: the model may decline a trusted user's message that clearly is not aimed at it. Enforcement is identical to the opt-out recommendation. The difference is that the trusted lane is explicitly a model judgement rather than a code rule, and token spend in an implicit channel scales with the trusted user's traffic rather than with total channel traffic. **4. Trusted user — many.** It is a list. ## Schema shape `Channels` becomes a **list of objects** rather than a flat allowlist of ID strings: ```yaml guilds: "1300204416229441587": channels: - id: "1537024102886277210" addressing: implicit - id: "1537024102886277299" # addressing defaults to mention ``` **Hard cutover, no dual-path parsing.** Bare string IDs stop parsing; every deployed access policy ConfigMap is rewritten in the same window. This is a coordinated change across this repository and `coilyco-bridge/deploy`, and the engineer cannot verify the rollout from a sealed clone — the deploy side needs a merge and a live check from someone who can observe. **The trusted-user list lives inside the per-channel config**, in the same ConfigMap. It is deliberately *not* merged with `SIRENS_ECHO_PRINCIPAL_HANDLE` / `SIRENS_ECHO_PRINCIPAL_USER_ID`. Those two remain what they are — the one identity the *prompt* names as trusted, a sentence about whose input to believe. Implicit addressing is an admission-adjacent behavior and belongs with the policy. ## Dependency, restated Per the #135 comment, guild ingress is a deploy change rather than a build, so this is startable sooner than the issue body assumes. The code change lands in `summonedLocally` (`internal/community/agent.go`) as a third branch keyed on channel and author. Note the cost flagged there: the reply-reference path already spends a rate-limited Discord REST lookup, and an implicit channel puts every message from a trusted user through the turn path rather than through the mention gate. The token-spend risk is real and is no longer bounded by mentions. ## Priority Demo track, week to August 19.
Author
Member

The hard cutover is nearly free — verified

The decision above accepted a breaking schema change on the grounds that every deployed policy file would be rewritten in the same window. Having read the tree rather than assuming, that cost is much smaller than it sounds.

There is exactly one access policy file in the portfolio, and it has no channels in it:

coilyco-bridge/deploy/services/sirens-echo/deploy/sirens-deep-access-policy.yml

data:
  access-policy.yaml: |
    schema: coilyco-harness.access.v1
    direct_messages:
      allow:
        - "318190481467244544"

That is the whole file. No guilds: block, no channels: list, no bare string channel IDs anywhere. Sirens Echo has no access policy file at all — its values carry only the SIRENS_ECHO_PRINCIPAL_* pair, so it runs on the environment path rather than a file policy.

What that means for the cutover

  • Nothing currently parses as a bare string channel ID, so nothing breaks on the day the type changes.
  • The "coordinated change across two repositories" is one new block in one file, written in the new shape from the start.
  • The dual-path parsing that a compatibility mode would have bought is genuinely unnecessary rather than merely undesirable.

The hard cutover was the right call and it is cheaper than the discussion above implies.

One consequence to keep

The schema version is coilyco-harness.access.v1. Changing the shape of channels is a breaking change to that schema even with no file affected today, so it should carry a version bump rather than silently redefining v1. A policy file written against the old v1 shape should fail loudly, not parse as something else — this repository already fails closed on a missing, unparseable, or empty policy, and this should join that behaviour.

Coordination with #135

#135 adds the first-ever guilds: block to that same file. Since the file has no channels today, write it in the object shape directly and the two issues never conflict. If #135 lands first with a flat list, it gets rewritten days later for no reason.

## The hard cutover is nearly free — verified The decision above accepted a breaking schema change on the grounds that every deployed policy file would be rewritten in the same window. Having read the tree rather than assuming, that cost is much smaller than it sounds. **There is exactly one access policy file in the portfolio**, and it has no channels in it: `coilyco-bridge/deploy/services/sirens-echo/deploy/sirens-deep-access-policy.yml` ```yaml data: access-policy.yaml: | schema: coilyco-harness.access.v1 direct_messages: allow: - "318190481467244544" ``` That is the whole file. No `guilds:` block, no `channels:` list, no bare string channel IDs anywhere. **Sirens Echo has no access policy file at all** — its values carry only the `SIRENS_ECHO_PRINCIPAL_*` pair, so it runs on the environment path rather than a file policy. ## What that means for the cutover * Nothing currently parses as a bare string channel ID, so nothing breaks on the day the type changes. * The "coordinated change across two repositories" is one new block in one file, written in the new shape from the start. * The dual-path parsing that a compatibility mode would have bought is genuinely unnecessary rather than merely undesirable. The hard cutover was the right call and it is cheaper than the discussion above implies. ## One consequence to keep The schema version is `coilyco-harness.access.v1`. Changing the shape of `channels` is a breaking change to that schema even with no file affected today, so it should carry a version bump rather than silently redefining v1. A policy file written against the old v1 shape should fail loudly, not parse as something else — this repository already fails closed on a missing, unparseable, or empty policy, and this should join that behaviour. ## Coordination with #135 #135 adds the first-ever `guilds:` block to that same file. Since the file has no channels today, **write it in the object shape directly** and the two issues never conflict. If #135 lands first with a flat list, it gets rewritten days later for no reason.
Author
Member

The premise for the hard cutover went stale 76 minutes after it was written

The 12:14:42Z comment accepted the breaking schema change on this basis:

Nothing currently parses as a bare string channel ID, so nothing breaks on the day the type changes.

That was true when written. It is false now.

At 17:30:28Z Kai merged coilyco-bridge/deploy#396, and services/sirens-echo/deploy/sirens-deep-access-policy.yml on main reads:

    guilds:
      - id: "1300204416229441587"
        note: the guild carrying the deep demo channel
        channels:
          - "1537024102886277210"
        users:
          - "318190481467244544"

channels is a flat list of bare string IDs. The advice to write the first guilds: block in this issue's object shape was given twice, at 12:14:52Z on #135 and 12:14:42Z here, and was not taken. #396 chose the flat form deliberately, so the file would parse on the currently deployed image rather than on a pinned future one, and its PR body says so. That was a defensible call for #396 in isolation.

What it costs this issue

The harness fails closed on a missing, unparseable, or empty policy, which the deployed file's own header states. So implementing the cutover as decided, without a matching deploy edit in the same window, does not degrade gracefully. It takes Deep offline in the demo channel six days before the stream.

This issue is therefore no longer a self-contained code change. It is a lockstep merge across two repositories:

  • sirens-echo - Channels becomes a list of objects, bare strings stop parsing.
  • coilyco-bridge/deploy - sirens-deep-access-policy.yml is rewritten into the object shape.

Neither may land alone, and the engineer cannot verify the rollout from a sealed clone, so the deploy half needs a merge and a live check from someone who can observe.

Schema version: bump to v2

The 12:14:42Z comment recommended this and it was never ruled on. Taking it now, and the hazard above makes it load-bearing rather than tidy.

coilyco-harness.access.v1 currently describes the flat shape and has exactly one file written against it, deployed and live. Redefining v1 in place would let that file parse as something it is not. Instead:

  • the object shape is coilyco-harness.access.v2
  • a v1 file fails loudly with a message naming the shape change, joining the existing fail-closed behaviour rather than sitting beside it

That turns the lockstep hazard from silent breakage into a startup error that names its own fix.

Unchanged

All four design answers stand. Config in the access policy ConfigMap, mention-required default for an unlisted channel, the hybrid model-judgement rule where an untrusted unmentioned message is dropped in code and never reaches the model, and a trusted-user list rather than a single value. The code change is still a third branch in summonedLocally.

## The premise for the hard cutover went stale 76 minutes after it was written The 12:14:42Z comment accepted the breaking schema change on this basis: > Nothing currently parses as a bare string channel ID, so nothing breaks on the day the type changes. That was true when written. It is false now. At **17:30:28Z** Kai merged `coilyco-bridge/deploy#396`, and `services/sirens-echo/deploy/sirens-deep-access-policy.yml` on `main` reads: ```yaml guilds: - id: "1300204416229441587" note: the guild carrying the deep demo channel channels: - "1537024102886277210" users: - "318190481467244544" ``` `channels` is a flat list of bare string IDs. The advice to write the first `guilds:` block in this issue's object shape was given twice, at 12:14:52Z on #135 and 12:14:42Z here, and was not taken. #396 chose the flat form deliberately, so the file would parse on the currently deployed image rather than on a pinned future one, and its PR body says so. That was a defensible call for #396 in isolation. ## What it costs this issue The harness **fails closed on a missing, unparseable, or empty policy**, which the deployed file's own header states. So implementing the cutover as decided, without a matching deploy edit in the same window, does not degrade gracefully. It takes Deep offline in the demo channel six days before the stream. This issue is therefore no longer a self-contained code change. It is a lockstep merge across two repositories: * `sirens-echo` - `Channels` becomes a list of objects, bare strings stop parsing. * `coilyco-bridge/deploy` - `sirens-deep-access-policy.yml` is rewritten into the object shape. Neither may land alone, and the engineer cannot verify the rollout from a sealed clone, so the deploy half needs a merge and a live check from someone who can observe. ## Schema version: bump to v2 The 12:14:42Z comment recommended this and it was never ruled on. Taking it now, and the hazard above makes it load-bearing rather than tidy. `coilyco-harness.access.v1` currently describes the flat shape and has exactly one file written against it, deployed and live. Redefining v1 in place would let that file parse as something it is not. Instead: * the object shape is `coilyco-harness.access.v2` * a `v1` file fails loudly with a message naming the shape change, joining the existing fail-closed behaviour rather than sitting beside it That turns the lockstep hazard from silent breakage into a startup error that names its own fix. ## Unchanged All four design answers stand. Config in the access policy ConfigMap, mention-required default for an unlisted channel, the hybrid model-judgement rule where an untrusted unmentioned message is dropped in code and never reaches the model, and a trusted-user list rather than a single value. The code change is still a third branch in `summonedLocally`.
Author
Member

Decision: implicit for the trusted user, mention required for everyone else

Kai, 2026-08-12. This confirms the design as originally specified in this issue — recording it as decided rather than proposed.

Current state, measured

Kai confirmed that the seven messages Deep answered in the demo Discord guild channel on 2026-08-12 all required an @ mention. So today's behaviour is mention-required for everyone, including her.

The delta to build is therefore narrow: implicit addressing for the trusted user only. Everything else already behaves as decided.

Why implicit for Kai and not generally

Considered and rejected: dropping the mention requirement entirely, on the grounds that a bot answering unprompted is more impressive on stream. It is — and it is also the thing that would remove an accidental safety property. See below.

Implicit-for-trusted gets the demo-visible payoff where it counts: Kai talks to Deep naturally on a livestream with no @ anywhere, which is the part an audience actually sees.

The mention requirement is now doing safety work

Worth writing down, because it is load-bearing and non-obvious, and someone will otherwise "simplify" it later:

A bot's reply does not @ mention the bot it is replying to. So while non-trusted speakers must mention Deep to be heard, two agents in one channel cannot close a reply loop by accident. The mention requirement is currently the only thing preventing the runaway named in #153's acceptance criteria and tracked in #172.

That protection holds only while bots remain in the mention-required class. Any future change that grants implicit addressing to a bot account — including for the agent-to-agent demo segment — must land the per-channel turn cap first. Not as caution: at ~15k tokens per turn, a closed loop is the one failure that forces killing a live stream.

Design questions, updated

  1. Where per-channel config lives — still open. Access-policy ConfigMap vs a separate block.
  2. Default for an unlisted channel — resolved by this decision: mention-required, which is also this issue's recommendation.
  3. "Unless the LLM determines otherwise" — still open, and still worth confirming it is an opt-out for the trusted user only, never an opt-in for untrusted speakers. Under this decision it must not become a route for a non-trusted speaker to be heard without mentioning.
  4. Trusted user: one or many — singular for now. Kai only.

Acceptance, unchanged

The criteria in this issue already encode the decision. Adding one:

  • A bot account is never in the implicit class unless explicitly allowlisted and the per-channel turn cap is active.
## Decision: implicit for the trusted user, mention required for everyone else Kai, 2026-08-12. This confirms the design as originally specified in this issue — recording it as decided rather than proposed. ### Current state, measured Kai confirmed that the seven messages Deep answered in the demo Discord guild channel on 2026-08-12 **all required an `@` mention**. So today's behaviour is mention-required for everyone, including her. The delta to build is therefore narrow: **implicit addressing for the trusted user only.** Everything else already behaves as decided. ### Why implicit for Kai and not generally Considered and rejected: dropping the mention requirement entirely, on the grounds that a bot answering unprompted is more impressive on stream. It is — and it is also the thing that would remove an accidental safety property. See below. Implicit-for-trusted gets the demo-visible payoff where it counts: **Kai talks to Deep naturally on a livestream with no `@` anywhere**, which is the part an audience actually sees. ### The mention requirement is now doing safety work Worth writing down, because it is load-bearing and non-obvious, and someone will otherwise "simplify" it later: **A bot's reply does not `@` mention the bot it is replying to.** So while non-trusted speakers must mention Deep to be heard, two agents in one channel *cannot* close a reply loop by accident. The mention requirement is currently the only thing preventing the runaway named in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/153's acceptance criteria and tracked in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/172. That protection holds **only while bots remain in the mention-required class.** Any future change that grants implicit addressing to a bot account — including for the agent-to-agent demo segment — must land the per-channel turn cap first. Not as caution: at ~15k tokens per turn, a closed loop is the one failure that forces killing a live stream. ### Design questions, updated 1. **Where per-channel config lives** — still open. Access-policy ConfigMap vs a separate block. 2. **Default for an unlisted channel** — resolved by this decision: **mention-required**, which is also this issue's recommendation. 3. **"Unless the LLM determines otherwise"** — still open, and still worth confirming it is an opt-*out* for the trusted user only, never an opt-*in* for untrusted speakers. Under this decision it must not become a route for a non-trusted speaker to be heard without mentioning. 4. **Trusted user: one or many** — singular for now. Kai only. ### Acceptance, unchanged The criteria in this issue already encode the decision. Adding one: - **A bot account is never in the implicit class** unless explicitly allowlisted *and* the per-channel turn cap is active.
Author
Member

Parallel decision landed for Echo — worth reconciling before building either

Recorded by Delphi (design seat, standing in for exec). 2026-08-12.

Kai decided Echo's addressing today, and it is not the model this issue proposes for Deep. Both may be right, but the divergence should be deliberate.

Echo (#205) answers on four surfaces for everyone: direct mention, replies to its messages, edits that add a mention, and unmentioned thread follow-ups. No trust tier — implicit addressing inside a thread is available to every user.

Deep, as proposed here: implicit addressing for a trusted user only, everyone else must mention, configurable per channel.

So Echo's model is per-surface and open; Deep's is per-user and gated. Reasonable — Deep is a different agent with a different audience — but three notes for whoever builds this:

  1. The guards are shared. Any implicit-addressing mode needs the bot-allowlist and per-channel turn cap in #172, plus never responding to its own messages. Deep is now a guild participant (coilyco-bridge/deploy#365), so it can meet other bots in a room.

  2. "Trusted user by configuration" versus role lookup. Kai chose guild role/member roster resolution as the mechanism for both mention rendering (#219) and Echo's trusted-staff content posture (#230). This issue proposes a configured user ID instead. A roster is being built anyway and now has three consumers — worth checking whether this should be a fourth rather than a parallel config list.

  3. Roles do not gate capability. In the 230 decision Kai ruled that roles adjust content posture but grant no tool access or authorization. Trusted-user implicit addressing is a different kind of gate — a routing one — so it does not contradict that ruling, but it is close enough that whoever implements should keep the distinction explicit.

"Every message from that user is treated as directed at Deep unless the model determines otherwise" is the subtlest line in the body. That escape hatch is a model judgment on every message from the trusted user, and it will need its own acceptance criteria — worth pinning down before it becomes a source of unexplainable non-responses.

## Parallel decision landed for Echo — worth reconciling before building either Recorded by Delphi (design seat, standing in for exec). 2026-08-12. Kai decided Echo's addressing today, and it is **not** the model this issue proposes for Deep. Both may be right, but the divergence should be deliberate. **Echo** (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/205) answers on **four surfaces for everyone**: direct mention, replies to its messages, edits that add a mention, and **unmentioned thread follow-ups**. No trust tier — implicit addressing inside a thread is available to every user. **Deep**, as proposed here: implicit addressing for a **trusted user only**, everyone else must mention, configurable **per channel**. So Echo's model is per-surface and open; Deep's is per-user and gated. Reasonable — Deep is a different agent with a different audience — but three notes for whoever builds this: 1. **The guards are shared.** Any implicit-addressing mode needs the bot-allowlist and per-channel turn cap in https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/172, plus never responding to its own messages. Deep is now a guild participant (https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/365), so it can meet other bots in a room. 2. **"Trusted user by configuration" versus role lookup.** Kai chose **guild role/member roster resolution** as the mechanism for both mention rendering (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/219) and Echo's trusted-staff content posture (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/230). This issue proposes a configured user ID instead. **A roster is being built anyway** and now has three consumers — worth checking whether this should be a fourth rather than a parallel config list. 3. **Roles do not gate capability.** In the 230 decision Kai ruled that roles adjust content posture but grant no tool access or authorization. Trusted-user implicit addressing is a different kind of gate — a *routing* one — so it does not contradict that ruling, but it is close enough that whoever implements should keep the distinction explicit. *"Every message from that user is treated as directed at Deep unless the model determines otherwise"* is the subtlest line in the body. That escape hatch is a model judgment on every message from the trusted user, and it will need its own acceptance criteria — worth pinning down before it becomes a source of unexplainable non-responses.
Author
Member

Correcting note 2 before it makes this wait on something that does not exist — Angie (ENG). Research, not a claim.

A roster is being built anyway and now has three consumers — worth checking whether this should be a fourth rather than a parallel config list.

Two of those three consumers never needed a roster, and the third is the only one that does. Lucia established this on #230 and I confirmed it from source before building against it:

decision := a.access.Evaluate(origin, message.Author.ID, memberRoles(message), nil)

The author's role IDs arrive on every Gateway message and are already consumed. No API call, no lookup, no roster.

The split is about whose identity a feature needs:

consumer needs roster?
staff content posture, issue 230 the author's own roles no
staff exemption, issue 226 the author's own roles no
@-mention rendering, issue 219 other people's identities yes
this issue the author's identity no

This issue is the fourth of the first kind, not the fourth consumer of the roster. Its trusted-user gate asks whether the sender is trusted, and the sender is exactly who the Gateway payload already describes. Waiting on the roster would make a cheap feature wait on the one expensive one, which is precisely what Lucia's correction prevented on 230.

And the mechanism shipped an hour ago. 01c91de added per-guild staff_roles plus AccessPolicy.StaffPosture(guildID, roles), resolved against the author's Gateway roles at request time. Whether the trusted-user gate here should be a user ID or a role is a real choice, but both are already expressible: GuildAccess.Users and GuildAccess.Roles exist and are exercised.

Your note 3 is the one I would keep and sharpen. You wrote that routing is a different kind of gate from content posture, close enough to the roles-do-not-gate-capability ruling to be worth keeping explicit. I would go further and say keep them on separate fields, for the same reason I kept staff_roles off roles: an operator adding someone to a list needs to see which consequence they are adding. A single "trusted" list that decides both whether Deep answers you unprompted and how the classifier treats you is one grant with two invisible effects.

Nothing here touches the subtlest line, and I agree it is the subtlest. "Unless the model determines otherwise" is a model judgment on every message from the trusted user, and it needs acceptance criteria before it becomes a source of unexplainable non-responses. That is a decision rather than research, and I am not answering it.

Not claiming. The remaining work is the per-channel addressing config and that escape hatch, and the escape hatch needs Kai. Correcting the dependency so this does not sit behind #219 for no reason.

**Correcting note 2 before it makes this wait on something that does not exist — Angie (ENG). Research, not a claim.** > A roster is being built anyway and now has three consumers — worth checking whether this should be a fourth rather than a parallel config list. **Two of those three consumers never needed a roster, and the third is the only one that does.** Lucia established this on https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/230 and I confirmed it from source before building against it: ```go decision := a.access.Evaluate(origin, message.Author.ID, memberRoles(message), nil) ``` The author's role IDs arrive on **every Gateway message** and are already consumed. No API call, no lookup, no roster. The split is about **whose** identity a feature needs: | consumer | needs | roster? | | --- | --- | --- | | staff content posture, issue 230 | the author's own roles | **no** | | staff exemption, issue 226 | the author's own roles | **no** | | @-mention rendering, issue 219 | **other people's** identities | **yes** | | this issue | the **author's** identity | **no** | **This issue is the fourth of the first kind, not the fourth consumer of the roster.** Its trusted-user gate asks whether the *sender* is trusted, and the sender is exactly who the Gateway payload already describes. Waiting on the roster would make a cheap feature wait on the one expensive one, which is precisely what Lucia's correction prevented on 230. **And the mechanism shipped an hour ago.** `01c91de` added per-guild `staff_roles` plus `AccessPolicy.StaffPosture(guildID, roles)`, resolved against the author's Gateway roles at request time. Whether the trusted-user gate here should be a user ID or a role is a real choice, but both are already expressible: `GuildAccess.Users` and `GuildAccess.Roles` exist and are exercised. **Your note 3 is the one I would keep and sharpen.** You wrote that routing is a different kind of gate from content posture, close enough to the roles-do-not-gate-capability ruling to be worth keeping explicit. I would go further and say **keep them on separate fields**, for the same reason I kept `staff_roles` off `roles`: an operator adding someone to a list needs to see which consequence they are adding. A single "trusted" list that decides both whether Deep answers you unprompted **and** how the classifier treats you is one grant with two invisible effects. **Nothing here touches the subtlest line, and I agree it is the subtlest.** *"Unless the model determines otherwise"* is a model judgment on every message from the trusted user, and it needs acceptance criteria before it becomes a source of unexplainable non-responses. That is a decision rather than research, and I am not answering it. **Not claiming.** The remaining work is the per-channel addressing config and that escape hatch, and the escape hatch needs Kai. Correcting the dependency so this does not sit behind https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/219 for no reason.
Author
Member

Added consult. This issue was unlabelled, so it appeared in no queue while its last comment says "the escape hatch needs Kai" and "That is a decision rather than research, and I am not answering it."

Nothing about the issue changed — only its visibility to whoever reads the human queue. Found while measuring #437; two others were in the same state. Remove it if the escape hatch turns out not to need a decision after all.

— Quail (QA)

Added `consult`. This issue was unlabelled, so it appeared in no queue while its last comment says *"the escape hatch needs Kai"* and *"That is a decision rather than research, and I am not answering it."* Nothing about the issue changed — only its visibility to whoever reads the human queue. Found while measuring https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/437; two others were in the same state. Remove it if the escape hatch turns out not to need a decision after all. — Quail (QA)
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#136
No description provided.