fix(notify): immediate Discord notifications silently lost on int snowflake target#97
Merged
Conversation
…wflake target meta_notify_user returned success but no Discord DM arrived: an int/str mismatch crashed the worker. User.serialize() keys discord_accounts by the int snowflake; notify_user passed that int as the celery task target, and the worker's .isdigit() channel-vs-DM classification assumed str, raising AttributeError on every attempt. Scheduled sends round-trip through a String DB column so the int was coerced; the immediate path skipped the DB. Fixes, layered: - Source: get_notification_channel coerces the Discord id to str (slack/email were already str). - Classification: discord_channel_for_target accepts str | int and normalizes. - Dispatch: send_via_discord str()s the target before send_dm/send_to_channel. Also coerce discord_accounts keys to str in User.serialize() and the _get_current_user Person fallback, so get_user's in-process output matches the str-keyed JSON-RPC wire shape clients already receive. Rename the immediate-send return key success -> queued: a fire-and-forget enqueue is not a delivery confirmation and shouldn't imply one. Scheduled sends keep success (a row is durably committed). Tests: int-target classification + dispatch, int->str channel coercion, str-keyed serialize() and get_user fallback, and the queued return shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
meta_notify_userreturned{"success": true, "channel_type": "discord"}but no Discord DM ever arrived — every immediate Discord notification since ~2026-05-30 was silently dropped. The worker crashed on all 3 celery attempts with:Root cause — int/str mismatch across the celery boundary
User.serialize()keysdiscord_accountsby the int snowflake (account.id).notify_userpicks that int and passes it as the celery tasktarget; JSON serialization preserves it as int.target.isdigit(), which assumesstr→AttributeError, task fails, message lost (no retry, no dead-letter, no DB record).Why only the immediate path broke: scheduled notifications round-trip through
ScheduledTask.notification_target(aStringcolumn), so the int is coerced to str by the DB. The immediate path (added 2026-05-29) skips the DB; the.isdigit()classification landed 2026-05-30. Each commit was fine alone — together they broke every immediate Discord send. Slack/email use string identifiers throughout and were unaffected.Fix (layered)
get_notification_channelcoerces the Discord id tostr(slack/email were already str).discord_channel_for_target/discord_target_is_channelacceptstr | intand normalize; the one canonical place that classifies a snowflake.send_via_discordstr()s the target beforesend_dm/send_to_channel(the collector API wants str).Related fix —
get_userpublic contractUser.serialize()and the_get_current_userPerson fallback emitteddiscord_accountskeyed by int. JSON object keys are always strings, so in-process callers saw{123456789: …}while MCP clients over the wire saw{"123456789": …}. Both now coerce to str, so the shapes match.Honesty fix —
success→queuedThe immediate-send return now reports
{"queued": true, "scheduled": false, …}instead ofsuccess: a fire-and-forget enqueue is not a delivery confirmation and shouldn't imply one. There is no row/metric to read back. Scheduled sends keepsuccess(a notification row is durably committed). Clients should branch on the sharedscheduledkey.Tests
discord_target_is_channel) and dispatch (send_via_discord) no longer crash and route correctlyget_notification_channelUser.serialize()and theget_userPerson fallbackqueuedimmediate-send return shape ("success" not in result)All passing; verified across
test_meta,test_meta_notify,test_users,test_notification_targets,test_scheduled_tasks,test_teams(no regressions).🤖 Generated with Claude Code