fix(discord): add voice and command sync config knobs#19364
Conversation
keegoid-cc
left a comment
There was a problem hiding this comment.
════ PAI | NATIVE MODE ═══════════════════════
🗒️ TASK: Adversarial review of Discord config knobs PR
Summary
Adds HERMES_DISCORD_VOICE_TIMEOUT env knob, lets Discord command sync policy and guild ID come from config.extra or env, and threads optional guild scoping through _safe_sync_slash_commands (guild-scoped upsert/edit/delete) plus a bulk-sync copy_global_to path.
Bugs & Correctness
- [CAT-1]
gateway/platforms/discord.pybulk-sync branch inspectsgetattr(sync_method, "side_effect", None) or sync_methodto decide whethertree.syncacceptsguild=. That's test-mock plumbing leaking into production: it only meaningfully helpsunittest.mock.AsyncMock, and against the realdiscord.pyCommandTree.sync(a bound method)inspect.signaturewill succeed and reflect the real signature — fine — but if a futureMocklacksside_effect, the fallback inspects the mock itself and theaccepts_guild=Trueexcept branch hides the mismatch. More concretely: realCommandTree.syncsignature is(*, guild=None), soaccepts_guildis correctly True; but thecopy_global_tocall is unconditionally invoked when guild is set, whiletree.sync(guild=...)without first having calledcopy_global_towould publish only commands explicitly added to that guild. This is internally consistent, but the guard logic is doing nothing useful in production and adds a real failure mode ifinspect.signatureraises on a C-implemented method (it falls back toaccepts_guild=True, which is the safe direction — OK). Net: not a hard bug, but theside_effectlookup is a code smell that exists only to satisfy tests; consider whether the signature probe is needed at all given realtree.syncalways acceptsguild. - [CAT-2]
_float_envaccepts negative values and0/0.0; a zero or negative voice timeout would make the auto-disconnect fire immediately. Not strictly a bug (operator-controlled), but worth bounding to> 0or warning. - [CAT-2]
VOICE_TIMEOUTis evaluated at class-definition (import) time, so changingHERMES_DISCORD_VOICE_TIMEOUTafter import has no effect. Probably intended, but worth noting vs. a property/lookup at use-site.
Security & Safety
- [CAT-3] Guild ID accepted from
config.extrais parsed withstr.isdigit(), which rejects negatives and non-ASCII digits — adequate. No injection surface since it's passed as int todiscord.Object.
Design & Maintainability
- [CAT-3] The
inspect.signature(... side_effect ...)shim is production code shaped by test mocks. Cleaner: have the test pass anAsyncMock(spec=...)or wraptree.syncvia a thin adapter; production shouldn't branch on mock internals. - [CAT-3]
_get_discord_command_sync_policy/_get_discord_command_sync_guild_idduplicate the "extra → alias → env → default" lookup pattern. A small_resolve_config(name, aliases, env, default)helper would DRY this and make precedence visible. - [CAT-2] No test exercises the bulk-sync guild path (
copy_global_to+sync(guild=...)); only safe-sync guild path and the env parser are covered. - [CAT-3]
copy_global_tois called unconditionally on every bulk sync when guild is set. On a long-running adapter that reconnects, this re-copies global commands into the guild on each reconnect — fine, but not idempotent-aware. Acceptable for a debug-oriented knob.
Questions for the author
- Why probe
tree.sync.side_effectrather than always passingguild=(realdiscord.pyCommandTree.syncalways accepts it)? Is this purely to satisfy a specific mock shape? - Intended precedence:
command_sync_policy>sync_policy> env. Is the aliassync_policydocumented anywhere, or is it transitional? - Should
HERMES_DISCORD_VOICE_TIMEOUT <= 0be rejected, or is "disconnect immediately" a legitimate configuration?
Severity of findings
low — no correctness/security defects in the diff; main concerns are a test-shaped branch in production code and missing input bounds on the new env knob.
VERDICT: comment
keegoid-cc
left a comment
There was a problem hiding this comment.
════ PAI | NATIVE MODE ═══════════════════════
🗒️ TASK: Adversarial review of Discord voice/sync knobs PR
Summary
Adds HERMES_DISCORD_VOICE_TIMEOUT env override for the Discord voice idle disconnect, and lets the slash-command sync policy + optional guild ID flow from PlatformConfig.extra (with env-var fallback). Threads guild scoping through both bulk and safe-sync paths.
Bugs & Correctness
- [CAT-1]
VOICE_TIMEOUT = _positive_float_env(...)is evaluated at class definition / import time, somonkeypatch.setenv("HERMES_DISCORD_VOICE_TIMEOUT", ...)in tests or runtime config changes after import have no effect on the actual disconnect timer. The newtest_discord_voice_timeout_env_parseronly exercises the helper, not the class attribute, so this gap is not caught. Consider resolving the timeout when the voice client is created (instance-level) rather than freezing it at module import. - [CAT-3]
_safe_sync_slash_commands(guild_id=...): when switching from global → guild scope (or vice versa) on an existing deployment, previously-registered commands in the other scope are not cleaned up — they'll linger in Discord until manually purged. Worth a comment or a one-time cleanup story; not a correctness bug for first-use.
Security & Safety
- [CAT-3]
command_sync_guild_idonly validated viaraw.isdigit(). Discord snowflakes fit in 64-bit; an arbitrarily long digit string would still pass and later be sent to the API. Low risk (config is operator-controlled), but a length/range sanity check would be cheap.
Design & Maintainability
- [CAT-3] The
extra.get("command_sync_policy", extra.get("sync_policy", os.getenv(...)))double-alias pattern is repeated for both policy and guild ID. Two names for the same knob invites drift; pick one canonical key and document the other as deprecated, or drop the alias. - [CAT-2]
_positive_float_envlives at module top-level above thetry: import discordblock — fine functionally, but it means the helper exists even when the optionaldiscorddep is missing. Intentional given it's pure stdlib; flagging only because it's slightly unusual placement. - [CAT-3] No test covers the safe-sync path's
guild_idbranch for update, recreate, or delete — only thecreatedbranch. Coverage of the mutation branches with guild scope would catch a future copy-paste regression onedit_guild_command/delete_guild_command.
Questions for the author
- Is the import-time evaluation of
VOICE_TIMEOUTdeliberate (single-process, env set before import)? If Hermes ever reloads config without restart, this knob silently won't honor it. - When an operator flips from global to guild sync, what's the expected cleanup story for stale global commands (and vice versa)?
- Why two aliases (
command_sync_policy/sync_policy,command_sync_guild_id/sync_guild_id) rather than one canonical name?
Severity of findings
low — one real but narrow staleness issue on VOICE_TIMEOUT, plus design questions; no security or data-loss risk.
🔧 CHANGE: Reviewed diff; flagged import-time timeout staleness
✅ VERIFY: Findings cite specific lines and call paths
🗣️ Fig: One real bug — VOICE_TIMEOUT frozen at import, env override won't reach the live attribute. Rest is design questions.
VERDICT: request_changes
claude-review posting override: forced to --comment because reviewer lacks verified write permission (viewerPermission=READ; was --request-changes). GitHub only counts approvals from WRITE, MAINTAIN, or ADMIN reviewers.
keegoid-cc
left a comment
There was a problem hiding this comment.
════ PAI | NATIVE MODE ═══════════════════════
🗒️ TASK: Adversarial review of Discord voice/sync knobs PR
Summary
Adds HERMES_DISCORD_VOICE_TIMEOUT env knob, lets command sync policy/guild come from config.extra or env, and extends _safe_sync_slash_commands to target a guild scope.
Bugs & Correctness
- [CAT-3]
_get_voice_timeout()is read on each timeout schedule viaasyncio.sleep, so changing the env var mid-process affects new timers but not active ones — likely intentional, worth a one-liner doc note. - [CAT-2] In
_safe_sync_slash_commands, whenguild_idis provided the "recreated" branch now callsdelete_guild_command+upsert_guild_command, but the original global path useddelete_global_command+upsert_global_command. Symmetric and correct; flagging only because the recreate dance still races (delete then upsert) the same way it did pre-PR — not introduced here. - [CAT-3]
_get_discord_command_sync_guild_idrejects negative-signed digits (raw.isdigit()is false for-1), which is fine, but also rejects whitespace/underscored numerics silently after warning. Acceptable.
Security & Safety
- [CAT-3] Guild ID comes from config/env and is interpolated into Discord HTTP calls as an int after
isdigit()validation — safe. No injection surface added.
Design & Maintainability
- [CAT-3]
VOICE_TIMEOUTclass constant was renamed toVOICE_TIMEOUT_DEFAULT. If any external code or test referencedDiscordAdapter.VOICE_TIMEOUTit will now AttributeError. A grep of the repo would confirm; the included test file no longer references it but downstream consumers might. - [CAT-3] Two-key fallback chain (
command_sync_policy→sync_policy→ env) doubles the config surface. Pick one canonical key and document the alias, or drop the alias to avoid drift. - [CAT-2]
_positive_float_envis a private module-level helper but only used by one method; fine, but parameterizing_get_voice_timeoutto take overrides would make it unit-testable without monkeypatching env. Current tests work, so non-blocking. - [CAT-3] New bulk-sync guild-scoped path calls
tree.copy_global_to(guild=...)thentree.sync(guild=...). This duplicates global commands into the guild bucket every reconnect; over many reconnects this is idempotent (overwrite), but worth noting that operators switching from guild→global must manually clear the guild registrations — no cleanup path is added.
Questions for the author
- Was
VOICE_TIMEOUTchecked for external references before renaming toVOICE_TIMEOUT_DEFAULT? - Why dual config keys (
command_sync_policyandsync_policy)? Is one deprecated? - Should switching from guild-scoped sync back to global also clean up previously registered guild commands, or is that left to operators?
Severity of findings
low — no CAT-1 defects; renaming a public-ish class constant and dual config keys are the only concerns and both are design-level.
VERDICT: approve
claude-review posting override: forced to --comment because reviewer lacks verified write permission (viewerPermission=READ; was --approve). GitHub only counts approvals from WRITE, MAINTAIN, or ADMIN reviewers.
665db42 to
cc729e0
Compare
|
Overlaps with #11419 (voice timeout configurable) — this PR bundles that feature with slash-command sync policy knobs. |
Summary
HERMES_DISCORD_VOICE_TIMEOUTfor Discord voice-channel idle disconnectsTest Plan
python -m py_compile gateway/platforms/discord.py tests/gateway/test_discord_connect.pypython -m pytest tests/gateway/test_discord_connect.py -q -o 'addopts='git diff --check upstream/main...HEAD