fix: make channel/server mute enforceable end to end (mute hardening) - #12892
Conversation
- muted-room drop no longer bypassed by mentions (strict-mode deployments made mute a no-op) - server-wide mute via ROOM scope=server (world.metadata, consulted by the same inbound gate) - durationMinutes persisted as agentMuteUntilIso; inbound due-check auto-unmutes at the ISO time - muted flag in list_channels + mutedRoomCount in list_connections - plugin-discord drops muted channels before ingestion (persisted, runtime-mutable, threads inherit parent mute) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
lalalune
left a comment
There was a problem hiding this comment.
Reviewed the core resolver, ROOM action persistence, MESSAGE list visibility, and Discord pre-ingestion gate. The room/world ID mapping matches the existing Discord ensureConnection convention, the timed mute due-check clears the right stores, and the connector target fields line up with TargetInfo.
Local validation on the latest PR head ed9b7d9391fef553314fbaff0ce9c1a5766b8345:
git diff --check origin/develop...origin/pr/12892passedbunx biome checkon all touched source/test files passedbunx vitest run packages/core/src/services/message/mute-state.test.ts packages/core/src/services/message.mute-drop.test.ts packages/core/src/features/advanced-capabilities/actions/room.mute.test.ts packages/core/src/features/advanced-capabilities/actions/message.list-muted.test.ts plugins/plugin-discord/__tests__/discord-events-mute-gate.test.tspassed: 5 files, 34 tests
I initially found Biome formatting failures in the added files; the PR now includes a formatting commit and the rerun is clean.
|
Reviewed the full diff — this is sound and I've enabled auto-merge (merge-when-green). All 5 mute-enforcement gaps are correctly fixed: the mention-bypass drop now keys on One behavior tradeoff worth confirming: in-channel unmute is impossible by design — a muted channel drops even an 'unmute' spoken inside it, so recovery requires unmuting from another room/DM (naming |
…dropping persisted world metadata list_servers returned raw connector worlds with no muted flag, and the discord listing fabricated fresh World objects from the guild cache, dropping persisted world.metadata — so a server-wide mute (agentMuteState, elizaOS#12892) was invisible at the server level. - core: resolveMutedWorldFlags resolves per-world mute (connector-carried metadata first, persisted world fallback); list_servers surfaces a muted flag per server and a (N muted) summary, mirroring list_channels - discord: listConnectorServers starts from the persisted world and refreshes live guild fields on top instead of fabricating a bare World Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mute hardening: make channel/server mute enforceable end to end
The core channel mute feature (ROOM action writes participant
room_state; the message service dropsMUTEDrooms before the planner) exists but is structurally a no-op on real deployments. Five verified gaps on currentdevelop, fixed as one cohesive change:Gap 1 (blocker): the muted-room drop was bypassed by mentions
services/message.tsonly dropped a MUTED room when the message was not a mention/reply and did not contain the agent's name. On a mention-gated deployment (DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS=true, the default), strict mode already drops every non-mention at the connector (plugin-discord/messages.tsstrict-mode gate), so every turn that reached the mute check was a mention — and mentions skipped the check. "Mute this channel" did literally nothing.Fix: the drop now goes through
resolveEffectiveMuteState(newservices/message/mute-state.ts) and is independent of addressing — a muted room drops even a direct @mention. Unmute happens from another room or DM via the ROOM action's existing cross-room targeting (platform+chatName).Gap 2: no server/guild-wide mute
Participant mute is per-room only (the
participantstable FKsroom_idtorooms, so a world id can't reuse that row shape). Server-wide mute lives onworld.metadata.agentMuteState— the world record is where world-scoped agent state already lives (roles,settings) — and is consulted by the same resolver, so one op silences every channel of a guild, including channels created after the mute.ROOMgainsscope: "room" | "server"(mute/unmute only).Gap 3: muted state was invisible
list_channelsoutput now carries a per-channelmutedflag (summary line counts them:Listed 12 channels from Discord (1 muted)), andlist_connectionscarriesmutedRoomCountper connection — "which channels are you muted in" is now answerable.Gap 4: timed mute was silently permanent
scheduleAutoUnmuteIsohad zero consumers, anddurationMinuteswas ignored entirely on the default (current-room) path. Now the expiry is persisted (agentMuteUntilIsoon room metadata / world metadata) and enforced by a due-check insideresolveEffectiveMuteState: the first inbound message at/after the ISO time auto-unmutes (participant state + metadata cleared) and processes normally. Fires on both the connector gate and the core gate — no separate scheduler to drift.Gap 5: Discord channel gating was whitelist-only + boot-frozen
CHANNEL_IDSis parsed once at boot; the runtimedynamicChannelIdsset is additive-only and in-memory. Instead of growing that mechanism, the DiscordmessageCreatelistener now consults the persisted mute state before ingestion — a muted channel costs zero memory writes, zero embeddings, zero model calls. The muted set is runtime-mutable (ROOM action) and survives restarts (participants/worlds tables). Threads inherit their parent channel's mute. A failed lookup fails open withruntime.reportError(core's own gate still guards the planner).Tests (all green)
mute-state.test.ts(15): room/server mute, expiry due-check writes, thread-parent inheritance, read-only list flagsmessage.mute-drop.test.ts(4): end-to-end through the realDefaultMessageService.handleMessage— a MUTED room ends the turn with statusmutedand zero model calls even withmentionContext.isMention=true; guild mute drops a mention in an unmuted child room; expired timed mute auto-unmutes and proceedsroom.mute.test.ts(6): durationMinutes persists the expiry + returnsscheduleAutoUnmuteIso; scope=server writes world metadata and the sibling room drops via the resolver; preconditions + validate gatingmessage.list-muted.test.ts(3): muted flag in list_channels, guild-wide flags, mutedRoomCount in list_connectionsdiscord-events-mute-gate.test.ts(6): muted channel drops a direct @mention before the debouncer; guild mute; thread parent; expired-mute auto-unmute; fail-open + reportErrorVerification (real counts)
packages/corevitest: 3027 passed / 7 failed — all 7 fail identically with this branch stashed (submodule-dir callsite audit, SHELL tier surface, live-network media fetch); full suite otherwise greenplugins/plugin-discordvitest: 198 passed / 4 failed — the 4 are the same pre-existing live-network outbound-media tests, verified failing on pristine developtsgotypecheck: clean on both packagesaudit:error-policy-ratchet: no new fallback-slop; the one new catch iserror-policy:J7annotatedRuntime cost on the hot path: the core gate reuses the participant state already fetched for the LLM-off check; the world consult is 1–2 keyed lookups per message.
Follow-up (separate lane): app UI toggle surface for mute/unmute — filed as a [phone-ui] issue.