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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions