Skip to content

fix(discord): delete obsolete slash commands before creating new ones#2

Open
infinitycrew39 wants to merge 4 commits into
mainfrom
fix/discord-slash-command-sync-limit
Open

fix(discord): delete obsolete slash commands before creating new ones#2
infinitycrew39 wants to merge 4 commits into
mainfrom
fix/discord-slash-command-sync-limit

Conversation

@infinitycrew39

Copy link
Copy Markdown
Owner

Issue

Discord enforces a hard 100-command limit per app. When syncing slash commands, if we try to create new commands before deleting obsolete ones, we can temporarily exceed this limit, triggering error 30032 which silently breaks ALL slash commands.

Root Cause

The _safe_sync_slash_commands() function in plugins/platforms/discord/adapter.py was iterating over desired commands and trying to create new ones BEFORE deleting obsolete ones. If Discord already had 100 commands and a new command needed to be added, this would exceed the limit momentarily.

Solution

Reorder the sync operations to:

  1. Delete obsolete commands first (those on Discord but not in the desired set)
  2. Then create/update desired commands

This ensures we always stay at or below the 100-command limit during sync.

Changes

  • fix(discord): Reorder _safe_sync_slash_commands() to delete before create
  • test(discord): Add regression test to verify deletions happen before creations

Testing

The regression test test_safe_sync_deletes_before_creating() verifies that:

  • When having 100 commands on Discord with 1 obsolete and 1 new desired
  • The obsolete command is deleted FIRST
  • Then the new command is created AFTER
  • This prevents temporarily exceeding the 100-command limit

Fixes the "Cronjob ran without error and final message was created but still failed with runtime error" issue where error 30032 was breaking all Discord slash commands.

Discord enforces a hard 100-command limit per app. The sync function was
trying to create new commands before deleting obsolete ones, which could
temporarily push the total over 100 and trigger error 30032, silently
breaking all slash commands.

This fix reorders the sync to:
1. Identify and delete obsolete commands first
2. Then create/update desired commands

This ensures we always stay at or below the 100-command limit during sync.
Add a test to verify that _safe_sync_slash_commands deletes obsolete
commands before creating new ones. This ensures we never temporarily
exceed Discord's 100-command limit during sync, which would trigger
error 30032 and break all slash commands.

This test guards against the regression where sync could fail even though
the registration cap was properly enforced.
infinitycrew39 pushed a commit that referenced this pull request Jun 25, 2026
…eation snapshot (NousResearch#44585)

An unpinned cron job follows the global default provider (config.yaml
model.default + resolve_runtime_provider). If that global state is changed
after the job is created — e.g. a temporary switch to a paid provider like
nous/claude-fable-5 — the job silently inherits it on its next tick and spends
real money. This is the reported $7.73 incident: a job created under a
free/default provider later inherited a temporary paid switch.

Fix (ask #1 only) preserves the legitimate "unpinned job should follow
model.default" use case by detecting *drift* rather than freezing the model:

- create_job (cron/jobs.py): for UNPINNED, agent-backed jobs (no explicit
  provider, not no_agent), snapshot the provider that resolution WOULD pick
  right now into a new optional `provider_snapshot` field, resolved via the
  same resolve_runtime_provider() path the ticker uses. Fail-open to None on
  any resolution error so job creation never breaks.

- run_job (cron/scheduler.py): right after runtime resolution, if the job has
  a provider_snapshot AND is unpinned AND the currently-resolved provider
  DIFFERS from the snapshot, fail closed for that run — make no paid call and
  deliver a loud, actionable alert naming both providers and telling the user
  to pin explicitly (`cronjob action=update job_id=.. provider=..`).

Back-compat: jobs with no snapshot (pre-existing jobs, no_agent jobs, or any
job whose creation-time resolution failed) behave exactly as before — the
guard only engages when a snapshot exists. Explicitly-pinned jobs (job.provider
set) are unaffected since they don't drift with global state.

Tests: tests/cron/test_cron_provider_pin.py covers snapshot-matches (runs),
snapshot-differs (fail closed, no agent constructed), no-snapshot back-compat,
None-snapshot back-compat, explicitly-pinned (runs regardless), plus create_job
snapshot capture/skip/fail-open. The fail-closed case is load-bearing (fails
without the guard).

Issue NousResearch#44585 asks #2-4 (hard-stop a running job, gateway-stop containment,
fail-closed on provider mutation) are out of scope for this change.
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.

1 participant