Skip to content

Harden the Discord gateway: backoff, resume across restarts, identify budget, fetch upgrade - #65

Merged
jackm43 merged 3 commits into
mainfrom
claude/vibrant-johnson-1uhzda
Oct 5, 2026
Merged

jackm43 merged 3 commits into
mainfrom
claude/vibrant-johnson-1uhzda

Conversation

@jackm43

@jackm43 jackm43 commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Reviews the DiscordGateway Durable Object against dcartertwo/discord-gateway-cloudflare-do and adopts its reconnect and protocol handling where it fits ragbot. The full comparison and decisions are in docs/gateway-review.md.

Why

  • Token-reset risk. Reconnects retried every 5 s forever, with no check of Discord's daily session_start_limit. Exceeding that limit makes Discord reset the bot token.
  • Restarts dropped messages. Session state lived only in memory. Every deploy or runtime restart started a new session, so mentions sent while disconnected were lost.
  • Latent alarm bug. The processed: marker sweep could pass more than storage's 128-key delete limit. The resulting exception also killed the watchdog alarm until the next cron run.
  • Handshake failures were invisible. With new WebSocket(), every refused handshake looked like close 1006.

Changes

  • Exponential reconnect backoff with jitter (1 s to 5 min), reset on READY or RESUMED. Cron and the watchdog leave a pending reconnect alone; /gateway/start retries immediately.
  • GET /gateway/bot before every IDENTIFY. When the budget is spent, the gateway waits for the reset. Only wss URLs under discord.gg are accepted.
  • Resumable state is persisted under a new storage key, gatewaySession (session ID, resume URL, sequence, bot user ID). A restarted object resumes and Discord replays missed events; the existing processed: markers drop duplicates. The sequence is saved on heartbeat ACK, not on every event.
  • Close code 4003 now starts a new session. Invalid sessions (op 9) wait 1–5 s. The first heartbeat is jittered.
  • The watchdog alarm runs every 60 s instead of 5 min. The marker sweep runs hourly and deletes in 128-key batches. A failed alarm re-arms the watchdog.
  • The socket opens with a fetch() WebSocket upgrade:
    • 408, 429 and 5xx back off and respect Retry-After.
    • A resume host that refuses the upgrade starts a new session.
    • Any other refusal stops rapid retries, the same as fatal close codes; cron or /gateway/start retries.
  • Existing storage keys, the singleton name, the DO class and migration history are unchanged. No D1 migration is needed.

Testing

  • CI passes: pnpm run check, pnpm test (134 tests), pnpm run test:runtime and pywrangler deploy --dry-run.
  • The 18 new gateway tests use an injected socket and storage. Each changed policy was mutation-checked against them.
  • The runtime probe (tests/runtime_worker.py) stubs /gateway/bot, so it never contacts Discord. It now also covers:
    • connecting through the real fetch upgrade;
    • gatewaySession surviving the real Durable Object storage round trip;
    • a refused handshake reporting its status and Retry-After.

Deployment

Merge, then run op run --env-file=.env -- pnpm run deploy. On first start, the new code finds no gatewaySession, so it starts a fresh session through the budget check. Restarts after that resume. Afterwards, check GET /gateway/health for connected: true and smoke-test a mention.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W2eaxgWffaEQUDb3KhteYv

claude added 3 commits October 5, 2026 07:41
… budget

Review the DiscordGateway Durable Object against
dcartertwo/discord-gateway-cloudflare-do and adopt its reconnect and
protocol handling where it fits ragbot (docs/gateway-review.md).

- Exponential reconnect backoff with jitter (1 s to 5 min), reset on READY
  or RESUMED. Cron and the watchdog leave a pending reconnect alone.
- Check Discord's session start limit via GET /gateway/bot before every
  IDENTIFY and wait for the reset when it is spent; exceeding it resets
  the bot token. Accept only wss gateway URLs under discord.gg.
- Persist the resumable session (gatewaySession) so a restart or deploy
  resumes and Discord replays missed events; processed: markers drop
  duplicates, so the sequence is flushed on heartbeat ACK.
- Close 4003 starts a new session; op 9 waits 1-5 s; first heartbeat is
  jittered.
- One-minute watchdog, hourly marker sweep in 128-key delete batches,
  and re-arm the watchdog when the alarm handler fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2eaxgWffaEQUDb3KhteYv
A refused WebSocket handshake now reports its HTTP status instead of
looking like close 1006, so the gateway can react to it:

- 408, 429 and 5xx back off, respecting Retry-After.
- A resume host that refuses the upgrade starts a new session.
- Any other refusal stops rapid retries like a fatal close code; cron or
  /gateway/start retries. The storage update is awaited so the alarm
  that triggered the connect cannot immediately re-enable it.
- Network errors and timeouts back off instead of failing the caller.

The runtime probe connects through the real upgrade and checks that a
refused handshake surfaces its status and Retry-After across the FFI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2eaxgWffaEQUDb3KhteYv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2eaxgWffaEQUDb3KhteYv
@jackm43
jackm43 merged commit 08ab57f into main Oct 5, 2026
1 check passed
@jackm43
jackm43 deleted the claude/vibrant-johnson-1uhzda branch October 5, 2026 08:14
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