Skip to content

fix: unmute members when their mute expires - #194

Merged
dimoschi merged 3 commits into
mainfrom
fix/unmute-expired-mutes
Oct 6, 2026
Merged

dimoschi merged 3 commits into
mainfrom
fix/unmute-expired-mutes

Conversation

@ToxicBiohazard

Copy link
Copy Markdown
Member

Types of changes

What types of changes does your code introduce?
Put an x in the boxes that apply.

  • Bugfix (non-breaking change which fixes an issue).
  • New feature (non-breaking change which adds functionality).
  • Breaking change (fix or feature that would cause existing functionality not to work as expected).
  • Documentation Update (if none of the other choices applies).

Proposed changes

Automatic unmute never ran. unmute_time and unban_time are stored as Unix seconds, but the scheduler compared them to a millisecond cutoff, so every mute and ban matched, including permanent bans of about 500 weeks. The job then slept inside schedule() until the furthest deadline, and unbans run before unmutes, so a long ban blocked every unmute.

/mute also started its unmute task without keeping a reference. On Python 3.13 the event loop only holds a weak reference, so that sleeping task could be garbage-collected and never fire.

This change:

  • Compares deadlines in seconds and only selects records due within the next minute.
  • Runs unban and unmute work in the background instead of awaiting it inside the one-minute loop.
  • Keeps a strong reference to the unmute task created by /mute.

Checklist

Put an x in the boxes that apply.

  • I have read and followed the CONTRIBUTING.md
    doc.
  • Lint and unit tests pass locally with my changes.
  • I have added the necessary documentation (if appropriate).

Additional context

Covered by tests/src/cmds/automation/test_scheduled_tasks.py: a far-future ban does not block scheduling, and an expired mute is cleared while a mute still hours away is left in place.

Made with Cursor

The scheduler treated second-based deadlines as milliseconds and then slept inside the loop, so a long ban blocked every automatic unmute.

Co-authored-by: Cursor <[email protected]>
@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.32%. Comparing base (76b2f25) to head (675546e).
⚠️ Report is 20 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #194      +/-   ##
==========================================
+ Coverage   66.60%   68.32%   +1.72%     
==========================================
  Files          54       76      +22     
  Lines        3177     3770     +593     
==========================================
+ Hits         2116     2576     +460     
- Misses       1061     1194     +133     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Codecov rejected the patch because the new scheduler and unmute branches were untested.

Co-authored-by: Cursor <[email protected]>
Comment thread src/cmds/automation/scheduled_tasks.py
Comment thread src/cmds/core/mute.py Outdated
@dimoschi

dimoschi commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Nice find with the seconds vs milliseconds bug. I checked prod: there are 77 old mutes waiting, the oldest from January 2024, so this fix is really needed. Tests pass for me on top of current main.

One thing to keep in mind: the first run after deploy will process all of them at once.

The minute loop was leaving those rows in place and looking them up again forever. Expiry now has a single scheduler path.

Co-authored-by: Cursor <[email protected]>

@dimoschi dimoschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good. unban_member and unmute_member only use .id, so passing discord.Object works, and the stale records get cleared once. Tests pass on top of current main.

Small optional thing: for unbans we could skip get_member_or_user and pass Object(id=...) directly. A banned user is never a guild member, so the lookup always fails first.

@dimoschi
dimoschi merged commit fe68e1c into main Oct 6, 2026
4 checks passed
@dimoschi
dimoschi deleted the fix/unmute-expired-mutes branch October 6, 2026 12:21
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.

2 participants