Skip to content

Bug: duplicate Discord replies from concurrent gateway/Codex runs #8

Description

@SebastianBoehler

Problem

The Discord bot can sometimes respond twice to a single user message. Reported behavior: one response asks for further information before committing work, while another independently running instance later times out or reports a different state after it may already have implemented changes.

This is not about the repository move. It points to a concurrency/idempotency bug in the gateway/harness around Discord mention handling and Codex runs.

Findings

Current safeguards are only process-local:

  • StudyBot._seen_message_ids deduplicates Discord message IDs, but only in memory inside one bot process.
  • StudyBot._active_mention_tasks and _mention_generations cancel/supersede active work, but only inside one bot process.
  • AgentGateway._channel_locks serializes Codex session resume per channel, but only inside one Python process.
  • ChannelSessionStore persists the Codex session id to JSON, but it does not act as a cross-process lock or job claim.
  • codex exec --json is spawned once per accepted mention. If two gateway processes/containers receive the same Discord event, each can start an independent Codex process and both can eventually reply.

This matches the symptom better than a single-process race: duplicate Discord gateway instances using the same bot token, or a restart/reconnect path that reprocesses the same message without a persisted idempotency record, would bypass the current in-memory dedupe.

Codex docs context

The OpenAI Codex docs describe two relevant integration modes:

  • codex exec is the documented non-interactive automation path. It emits JSONL events such as thread.started, turn.completed, item.*, and error.
  • codex app-server is a JSON-RPC integration surface with explicit thread/turn lifecycle (thread/start, thread/resume, turn/start, turn/steer, turn/completed) and bounded request queues/overload signaling.

The current gateway uses codex exec --json, not a long-lived app-server client. So the immediate race is in gateway dispatch/job ownership around each Discord event. If the project later moves to app-server, the same requirement still exists: one source message should map to one claimed turn/job, and retries should observe existing job state instead of starting a second turn.

Impact

  • Users see contradictory bot replies for one request.
  • One run may ask for clarification while another proceeds with implementation.
  • Timeout/error reporting can be misleading because another run may have already changed files or opened a PR.
  • Channel session JSON can be updated by whichever run emits a session id last.

Suggested fix

Add durable idempotency/job tracking for Discord-originated work:

  1. Persist a job/claim record keyed by discord_message_id (and probably channel_id) before spawning Codex.
  2. Claim creation must be atomic across processes, for example SQLite with a unique constraint, file lock plus atomic create, or another shared store already mounted in the deployment.
  3. If a duplicate event arrives while the job is running, do not start another Codex process. Optionally edit/reuse the existing progress message or reply that the request is already running.
  4. Persist final status: running, completed, failed, timed_out, cancelled, plus Codex session id, started/finished timestamps, and the Discord reply message id if available.
  5. Make timeout/cancel handling record status before posting failure text, so a later retry can distinguish "already completed" from "failed before producing output".
  6. Consider adding a deployment guard or startup warning if more than one gateway instance is expected to consume the same Discord bot token without shared job locking.

Acceptance criteria

  • Two concurrent handlers/processes receiving the same Discord message id result in only one Codex invocation.
  • Duplicate delivery while the first run is in progress does not produce a second Discord reply.
  • Duplicate delivery after completion does not rerun Codex and can reuse or ignore the completed result.
  • Same-channel follow-up messages still cancel/supersede the previous active task as currently intended.
  • Different channels can still run in parallel.
  • Tests cover same-process duplicate delivery and cross-process/shared-store duplicate claims.

Notes

Related files to inspect:

  • src/study_discord_agent/discord_bot.py
  • src/study_discord_agent/agent.py
  • src/study_discord_agent/command_runner.py
  • src/study_discord_agent/session_store.py
  • tests/test_discord_bot.py
  • tests/test_agent_sessions.py

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions