fix(core,discord): surface server-wide mute in list_servers and stop dropping persisted world metadata - #13517
Conversation
…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, #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>
lalalune
left a comment
There was a problem hiding this comment.
Correct on both halves.
Stop dropping persisted world metadata (the real bug): the guild→World builder previously fabricated a fresh record whose metadata was only { source, accountId, discordGuildId, memberCount }, clobbering the persisted world's durable metadata (server-wide agentMuteState, ownership/roles) on every reconnect — so a muted server would silently un-mute and role state would be lost. The fix reads the persisted world and merges correctly: { ...persisted, <live id/name/messageServerId>, metadata: { ...persisted?.metadata, <live discord fields> } } — persisted base, live overlay, spread order verified right, and persisted?.metadata handles first-sight worlds. The method going sync→Promise.all async is typecheck-gated.
list_servers muted flags: resolveMutedWorldFlags answers a world that already carries agentMuteState directly and falls back to the persisted world under the same id otherwise — the same persisted-fallback shape as the verified list_channels/list_connections mute surfacing, now correctly extending "which servers am I muted in" to list_servers. LGTM.
0xSolace
left a comment
There was a problem hiding this comment.
Solid, symmetric fix for server-wide mute visibility in list_servers. Two coordinated parts:
- Core (
handleListServers+ newresolveMutedWorldFlags): resolves the world-wide mute (ROOM scope=server) per listed server and annotatesmuted+ a(N muted)count, mirroring list_channels' per-channel flag. The helper trusts mute metadata a connector already carries and otherwise falls back to the persisted world viagetWorld— andworldMuteActivesafely handles thenullgetWorldresult (world?.metadata). Read-only, expiry writes stay owned by the inbound due-check, consistent withresolveMutedTargetFlags. - Discord (
listConnectorServers): merges the persisted world (durable metadata: agentMuteState, ownership/roles) with live guild fields via spread order (name/memberCount/discordGuildId win over the stale snapshot), so the server-wide mute the ROOM action writes survives into the listing instead of being dropped by a fabricated bare World.
Tests cover: connector lists a bare World (mute resolved from persisted world), connector carries mute metadata directly (honored, with timed-expiry falsifying an expired mute), and the persisted-merge path (durable metadata preserved, live fields refreshed, no-persisted-world guild still listed). No secrets, matches description, tests included. LGTM. — [sol-orch]
|
Review (consolidation): distinct deep fix from #13500, but the two conflict in one shared test file — rebase after #13500. This is a real root-cause fix (verified on develop): One consolidation note: it and #13500 (list_channels/list_connections count truncation) are distinct fixes (different handlers/methods, hunks >40 lines apart in message.ts and ~270 apart in service.ts), so the source auto-merges — but they both edit the same test file |
0xSolace
left a comment
There was a problem hiding this comment.
Approve — MED tier — [sol-orch]
Read the full diff. Natural completion of the mute-visibility surface: list_servers gets the per-server muted flag mirroring list_channels, resolved via the new read-only resolveMutedWorldFlags (trusts connector-carried metadata including timed expiry, falls back to the persisted world by id — so visibility doesn't depend on listServers fidelity). The discord listConnectorServers change is the meatier half: starting from the persisted world and refreshing live guild fields on top stops the listing from dropping durable metadata (mute state, ownership/roles) that the fabricated bare World discarded. Merge-spread order is right (live name/memberCount/source win, persisted survives underneath).
One perf note, non-blocking: listConnectorServers now does a getWorld per cached guild on every call. Fine at normal guild counts; if anyone ever runs this on a many-hundred-guild bot it'd want a batch fetch, but that's hypothetical today.
Tests cover both resolution paths (bare world → persisted lookup; connector-carried metadata incl. expired timed mute) and the discord merge behavior incl. the no-persisted-world case. Complements #13500 cleanly (different ops, same file family — no textual overlap in message.ts, small overlapping context in the shared test file header; the queue will surface it if the rebase collides). Arming auto-merge.
|
CLAIMING PR #13517 — shepherd to merge (CI watch, queue, conflict handling). |
0xSolace
left a comment
There was a problem hiding this comment.
Full diff read + touched suites run locally in a worktree:
message.list-muted.test.ts5/5,mute-state.test.ts15/15,plugin-discord __tests__/list-servers-persisted-world.test.ts2/2 — all green. (Other message.* suite failures in my sandbox are worktree module-resolution artifacts — 'Cannot find package uuid' on import — not PR code.)resolveMutedWorldFlagsis read-only, honors timed expiry viaworldMuteActive, and short-circuits when the connector already carriesagentMuteState— onegetWorldper bare world is acceptable for list_servers cardinality.- Discord
listConnectorServersmerge order is right: persisted world first, live guild fields (name/memberCount/messageServerId) win on top; ownership/mute metadata survives.
MED tier, arming auto-merge. — [sol-orch]
|
Resolved the post-#13500 conflict by creating a merge commit from latest develop into fix/qaf-servers. The resolved tree is latest develop plus only the five PR files, preserving #13500's list_channels cap/count coverage and this PR's list_servers server-wide mute coverage. Current state: behind_by=0, mergeable=MERGEABLE; waiting on queued checks/merge queue. |
|
Reviewed (deep-verify): real fix surfacing server-wide mute state in list_servers (core + discord), complements the merged #13500 (channel counts) on the same list surface. Not surface-level. Queuing auto-merge on green. |
|
Closing this PR as already landed/redundant after the latest Current
I also rebuilt the intended five-file delta in a scratch worktree and ran the core focused suite against current develop: |
|
Closing as superseded — verified the entire mute-surfacing fix (server-wide mute in |
|
❌ PR title does not match the required pattern. Please use one of these formats:
|
Problem
QA found that a server-wide mute (set via the ROOM action
scope=server, #12892) is invisible at the server level:list_serverscarried no muted flag —handleListServers(packages/core/src/features/advanced-capabilities/actions/message.ts) returned the connector worlds raw, unlikelist_channelswhich resolves per-channelmutedflags. "Which servers are you muted in" was unanswerable.listServersdropped persistedworld.metadata—DiscordService.listConnectorServersfabricated freshWorldobjects from the live guild cache with hand-built metadata, discarding the persisted world record wheresetWorldMuteStatewritesagentMuteState(samecreateUniqueUuid(runtime, guild.id)id).Fix (structural)
resolveMutedWorldFlagsinservices/message/mute-state.ts(sibling ofresolveMutedTargetFlags): answers from connector-carried mute metadata when present, otherwise falls back to the persisted world under the same id — so server-level mute visibility does not depend on a connector'slistServersfidelity.list_serversnow surfacesmutedper server and a(N muted)summary, mirroringlist_channels.listConnectorServersstarts from the persisted world (runtime.getWorld(worldId)) and refreshes the live guild fields (name, memberCount, accountId) on top, instead of fabricating a bareWorld. Durable metadata (agentMuteState, ownership/roles) survives into the listing.Verify-first proof (bug is real on current develop)
Both new tests were run against unmodified
origin/develop(1561490) before the fix and failed exactly on the bug:message.list-muted.test.tslist_servers block:Tests 2 failed | 3 passed—expected undefined to be true(nomutedflag in list_servers output)list-servers-persisted-world.test.ts:Tests 1 failed | 1 passed—expected undefined to be 'MUTED'(persistedagentMuteStatedropped)Tests (real, fail on the bug)
packages/core/src/features/advanced-capabilities/actions/message.list-muted.test.ts— extended withop=list_servers: (a) server-wide mute on the persisted world resolvesmuted:truefor a bare connector-listed World, unmuted sibling staysfalse, summary shows(1 muted); (b) connector-carried mute metadata is trusted directly, including timed-mute expiry (agentMuteUntilIsoin the past →muted:false). Drives the realmessageAction.handler.plugins/plugin-discord/__tests__/list-servers-persisted-world.test.ts— realDiscordService.prototype.listConnectorServers(sameObject.create(prototype)harness asconnector-loop.harness.test.ts) + map-backed runtime world store: persistedagentMuteState/ownership survive, live guild fields win over the stale snapshot, guilds with no persisted world still list.Verification (real counts)
src/features/advanced-capabilities+src/services/message*→ 32 files / 274 tests passedmessage.list-muted.test.ts+mute-state.test.ts→ 20 passedlist-servers-persisted-world.test.ts+discord-events-mute-gate.test.ts→ 8 passedoutbound-attachment.test.ts,messageConnector.outbound-media.test.ts) reproduce identically on clean develop (verified by re-running on the stashed tree): pre-existing outbound-media fetch failures, unrelated to this changepackages/core(tsgo) exit 0;plugins/plugin-discord(tsgo) cleanEvidence N/A
🤖 Generated with Claude Code