Skip to content

fix(core,discord): surface server-wide mute in list_servers and stop dropping persisted world metadata - #13517

Merged
lalalune merged 5 commits into
developfrom
fix/qaf-servers
Jul 4, 2026
Merged

fix(core,discord): surface server-wide mute in list_servers and stop dropping persisted world metadata#13517
lalalune merged 5 commits into
developfrom
fix/qaf-servers

Conversation

@NubsCarson

Copy link
Copy Markdown
Member

Problem

QA found that a server-wide mute (set via the ROOM action scope=server, #12892) is invisible at the server level:

  1. list_servers carried no muted flaghandleListServers (packages/core/src/features/advanced-capabilities/actions/message.ts) returned the connector worlds raw, unlike list_channels which resolves per-channel muted flags. "Which servers are you muted in" was unanswerable.
  2. Discord listServers dropped persisted world.metadataDiscordService.listConnectorServers fabricated fresh World objects from the live guild cache with hand-built metadata, discarding the persisted world record where setWorldMuteState writes agentMuteState (same createUniqueUuid(runtime, guild.id) id).

Fix (structural)

  • core — new resolveMutedWorldFlags in services/message/mute-state.ts (sibling of resolveMutedTargetFlags): 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's listServers fidelity. list_servers now surfaces muted per server and a (N muted) summary, mirroring list_channels.
  • plugin-discordlistConnectorServers starts from the persisted world (runtime.getWorld(worldId)) and refreshes the live guild fields (name, memberCount, accountId) on top, instead of fabricating a bare World. 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:

  • core message.list-muted.test.ts list_servers block: Tests 2 failed | 3 passedexpected undefined to be true (no muted flag in list_servers output)
  • discord list-servers-persisted-world.test.ts: Tests 1 failed | 1 passedexpected undefined to be 'MUTED' (persisted agentMuteState dropped)

Tests (real, fail on the bug)

  • packages/core/src/features/advanced-capabilities/actions/message.list-muted.test.ts — extended with op=list_servers: (a) server-wide mute on the persisted world resolves muted:true for a bare connector-listed World, unmuted sibling stays false, summary shows (1 muted); (b) connector-carried mute metadata is trusted directly, including timed-mute expiry (agentMuteUntilIso in the past → muted:false). Drives the real messageAction.handler.
  • plugins/plugin-discord/__tests__/list-servers-persisted-world.test.ts — real DiscordService.prototype.listConnectorServers (same Object.create(prototype) harness as connector-loop.harness.test.ts) + map-backed runtime world store: persisted agentMuteState/ownership survive, live guild fields win over the stale snapshot, guilds with no persisted world still list.

Verification (real counts)

  • core (vitest, canonical runner): src/features/advanced-capabilities + src/services/message*32 files / 274 tests passed
  • core targeted: message.list-muted.test.ts + mute-state.test.ts20 passed
  • plugin-discord targeted: list-servers-persisted-world.test.ts + discord-events-mute-gate.test.ts8 passed
  • plugin-discord full suite: 217 passed, 4 failed — the 4 failures (outbound-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 change
  • typecheck: packages/core (tsgo) exit 0; plugins/plugin-discord (tsgo) clean

Evidence N/A

  • Screenshots / video / frontend logs: N/A — backend action-surface change, no UI.
  • Live-LLM trajectory: N/A — no prompt/model-behavior change; the fix is a deterministic data-surface (action output + connector listing) exercised end-to-end by the real handler/service tests above.

🤖 Generated with Claude Code

…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 lalalune left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 0xSolace left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid, symmetric fix for server-wide mute visibility in list_servers. Two coordinated parts:

  1. Core (handleListServers + new resolveMutedWorldFlags): resolves the world-wide mute (ROOM scope=server) per listed server and annotates muted + 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 via getWorld — and worldMuteActive safely handles the null getWorld result (world?.metadata). Read-only, expiry writes stay owned by the inbound due-check, consistent with resolveMutedTargetFlags.
  2. 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]

@0xSolace
0xSolace enabled auto-merge July 4, 2026 21:07
@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

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): listConnectorServers (service.ts:2423) fabricates bare World objects and never reads the persisted world, dropping agentMuteState written by setWorldMuteState under the same createUniqueUuid(runtime, guild.id) id, so list_servers never showed server-wide mute (#12892). The fix correctly starts from runtime.getWorld(worldId), and the new resolveMutedWorldFlags mirrors the existing resolveMutedTargetFlags/isServerMuted (honoring timed muteExpiryDue). Verify-first tests fail on develop, pass with the fix. Merge-ready on its own.

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 packages/core/src/features/advanced-capabilities/actions/message.list-muted.test.ts (both rewrite the header docblock lines 1-8 and insert a new describe block at the same anchor). I've enqueued #13500 first; please git rebase origin/develop after it lands and combine the docblock (mention channels AND servers) + keep both describe blocks (they're additive) — trivial resolution.

@0xSolace 0xSolace left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@NubsCarson

Copy link
Copy Markdown
Member Author

CLAIMING PR #13517 — shepherd to merge (CI watch, queue, conflict handling). [maintainer]

@0xSolace 0xSolace left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full diff read + touched suites run locally in a worktree:

  • message.list-muted.test.ts 5/5, mute-state.test.ts 15/15, plugin-discord __tests__/list-servers-persisted-world.test.ts 2/2 — all green. (Other message.* suite failures in my sandbox are worktree module-resolution artifacts — 'Cannot find package uuid' on import — not PR code.)
  • resolveMutedWorldFlags is read-only, honors timed expiry via worldMuteActive, and short-circuits when the connector already carries agentMuteState — one getWorld per bare world is acceptable for list_servers cardinality.
  • Discord listConnectorServers merge 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]

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

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.

@lalalune
lalalune merged commit 73ffc76 into develop Jul 4, 2026
19 of 42 checks passed
@lalalune
lalalune deleted the fix/qaf-servers branch July 4, 2026 22:11
@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

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.

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Closing this PR as already landed/redundant after the latest develop updates.

Current origin/develop already contains the substantive #13517 behavior:

  • resolveMutedWorldFlags and list_servers muted handling in core
  • server-mute tests in message.list-muted.test.ts
  • Discord listConnectorServers starts from the persisted world and preserves durable metadata
  • fix: complete channel counts in list_channels/list_connections past 50 #13500's uncapped listConnectorRooms contract is present (return this.dedupeConnectorTargets(targets), no slice(0, 50))

I also rebuilt the intended five-file delta in a scratch worktree and ran the core focused suite against current develop: bun test packages/core/src/features/advanced-capabilities/actions/message.list-muted.test.ts packages/core/src/services/message/mute-state.test.ts -> 24 pass / 50 expect(). The open branch is now stale/redundant and its PR diff duplicates the server test block, so merging it would add noise and risk regressing the list-room cap. Closing instead of merging.

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Closing as superseded — verified the entire mute-surfacing fix (server-wide mute in list_servers/list_connections for core + discord) is already on develop byte-identical: mute-state.ts resolveMutedWorldFlags, the message.ts list_servers muted flag + (N muted) summary, the discord persisted-world listConnectorServers, and both tests all match this branch's blobs exactly (landed via direct commit). This branch's net diff against current develop is empty, so there's nothing left to merge.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

❌ PR title does not match the required pattern. Please use one of these formats:

  • 'type: description' (e.g., 'feat: add new feature')
  • 'type(scope): description' (e.g., 'chore(core): update dependencies')
    Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants