Skip to content

Fix cross-platform reply race: per-command context via AsyncLocalStorage - #346

Merged
htilly merged 1 commit into
masterfrom
claude/per-command-context
Sep 26, 2026
Merged

htilly merged 1 commit into
masterfrom
claude/per-command-context

Conversation

@htilly

@htilly htilly commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #340. index.js kept three module-level globals, currentPlatform, currentChannel and currentIsAdmin. Each command set them when it started, but they were read much later: after awaits, and in timers such as the 5-minute flush-vote timer. Slack commands run fire-and-forget, so overlapping Slack and Discord commands could overwrite each other's values. The result:

  • a reply could go to the wrong platform, e.g. a Slack channel ID sent to Discord or the other way round
  • help could make the wrong admin decision

Changes

  • lib/command-context.js (new): an AsyncLocalStorage holding {platform, channel, isAdmin, userName}. It follows a command through every await, promise chain and setTimeout the command starts.
  • lib/command-router.js: the public entry points (routeCommand, processInput, handleNaturalLanguage) now run inside the command's context. The optional setContext dependency is still accepted.
  • index.js:
    • Removed the three globals and the setContext closure.
    • The Slack and Discord reaction handlers run in their own context.
    • _slackMessage, help, Discord DMs, bestof and add-handlers' getCurrentPlatform now read the current command's context.
    • Sends made outside any command (startup and admin notices) go to Slack if it is configured, otherwise to Discord. The old code always defaulted to Slack, even in Discord-only setups.

Timers such as the flush vote, gong cleanup and TTS cleanup needed no changes, because they inherit the context of the command that created them.

Test plan

  • npm test: 867 passing
  • New test/command-context.test.mjs: the context survives awaits and timers, overlapping runs are isolated, and there is no context outside a command
  • New router tests: an overlapping Slack command and Discord command each see their own platform and channel, and a timer started by a handler keeps its command's context after another command has run
  • node --check on the changed files
  • Manual check on a mixed Slack + Discord install: run commands on both platforms at the same time, and try flushvote followed by a command on the other platform

🤖 Generated with Claude Code

https://claude.ai/code/session_01HC2Ba1dYTJaUgH63ks2N1q


Generated by Claude Code

…obals

currentPlatform/currentChannel/currentIsAdmin were module-level globals set
at the start of each command and read later (after awaits and in timers,
e.g. the 5-minute flush-vote timer). Overlapping Slack and Discord commands
could overwrite them, sending replies to the wrong platform and making
wrong admin decisions in help.

- lib/command-context.js: AsyncLocalStorage-backed {platform, channel,
  isAdmin, userName} that follows a command through awaits and timers
- Router entry points (routeCommand/processInput/handleNaturalLanguage)
  run inside the command's context; reaction handlers too
- _slackMessage, help, DMs, bestof and add-handlers read the context;
  outside a command, sends default to Slack if configured, else Discord
- Tests for overlapping commands and timers keeping their own context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HC2Ba1dYTJaUgH63ks2N1q
@htilly
htilly marked this pull request as ready for review September 26, 2026 17:24
@htilly
htilly merged commit 743dbdf into master Sep 26, 2026
8 of 9 checks passed
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