TL;DR: Ompycord is the Python discord.py rewrite of Ompcord — the always-on OMP⇄Discord bridge. Omp is the bot persona. Each session runs in its own thread off a home channel; a single embed dashboard updates in place with live progress. Administrative config settings are modified via interactive components.

1. 🚀 Quick Start

Setting up Ompycord involves configuring a Discord bot application, preparing an environment with the required token and channel settings, and launching the daemon. The bot must be invited with specific permissions (permission integer 326417632336).

Ensure you have installed the required dependencies using pip:

pip install discord.py fastapi aiosqlite mcp pydantic

Define the application credentials and settings in your environment:

# .env Configuration
DISCORD_TOKEN=MTI...
DISCORD_CHANNEL_ID=$CHANNEL_ID
DISCORD_ALLOWED_USERS=$ALLOWED_USER_IDS
OMP_LAUNCH_CHANNEL_IDS=$CHANNEL_ID
OMP_LAUNCH_EMOJI=🚀

Bootstrap your bot tree and initialize hybrid slash commands using the discord.py framework:

import os
import discord
from discord.ext import commands
 
class OmpBot(commands.Bot):
    def __init__(self):
        intents = discord.Intents.default()
        intents.message_content = True  # Required for message parsing in threads
        super().__init__(command_prefix="!", intents=intents)
 
    async def setup_hook(self):
        # Sync hybrid commands globally or to a specific test guild
        await self.tree.sync()
        print(f"Synced bot slash tree for {self.user}")
 
bot = OmpBot()
 
@bot.hybrid_command(name="status", description="Get the current status embed")
async def status(ctx: commands.Context):
    # Hybrid commands automatically register as slash commands
    await ctx.defer(ephemeral=True)
    # Build status snapshot and return it
    await ctx.send("🟢 System Online")
 
if __name__ == "__main__":
    bot.run(os.environ["DISCORD_TOKEN"])

2. 🧬 Core Architecture

Product vs Persona: Ompycord vs Omp

Ompycord refers to the core codebase, plugin system, and daemon runtime executing the OMP⇄Discord bridge. Omp is the front-facing user persona, responding to mentions and slash commands. You install Ompycord in your environment; Omp answers in Discord.

Process Topology (Mermaid)

The bridge coordinates local execution via an asynchronous web app sidecar alongside the Discord client. The discord.py application embeds a FastAPI instance running in the same event loop to accept JSON event feeds and update database state.

graph TD
    subgraph Gateway["Discord Gateway"]
        User["Discord User"]
        DG["Discord Gateway API"]
    end

    subgraph Daemon["Ompycord Daemon (Python / discord.py)"]
        Bot["discord.py Bot Client"]
        API["FastAPI Web Server"]
        DB[("aiosqlite Database")]

        Bot <--> DB
        API <--> Bot
        API <--> DB
    end

    subgraph Work["OMP Execution Workspace"]
        OMP["omp Agent Process"]
        Ext["discord-notify.ts Extension"]
        OMP -->|"Trigger Event"| Ext
    end

    User -->|"Slash Command / Button"| DG
    DG -->|"Interaction Event"| Bot
    Ext -->|"HTTP POST JSONL Events"| API
    Bot -->|"Discord API"| DG

StatusSnapshot Contract

The following Python dataclasses specify the schema for tracking bot daemon execution, runtime context, resource usage, and active agent turns. The usage sub-model must remain honest; if token count or cost attributes cannot be determined, their source field must be set to "unavailable".

from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any
 
@dataclass
class DaemonStatus:
    ready: bool
    bot: str
    pid: int
    uptime_ms: int
    version: str
    started_at: int
    last_ready_at: int
 
@dataclass
class DiscordStatus:
    guild_id: int
    home_channel_id: int
    active_thread_id: Optional[int]
    allow_mode: str  # 'allow-list' or 'everyone'
 
@dataclass
class RuntimeStatus:
    cwd: str
    session_root: str
    active_session_dir: str
    model: str
    provider: str
    api: str
    mode: str = "headless-json"
    continuation: str = "-c when session jsonl exists"
 
@dataclass
class RunStatus:
    busy: bool
    active_thread_id: Optional[int]
    prompt_excerpt: str
    started_at: int
    elapsed_ms: int
    step_count: int
    last_tools: List[str] = field(default_factory=list)
    tool_errors: int = 0
    child_count: int = 0
    last_exit_code: Optional[int] = None
    last_done_at: Optional[int] = None
    last_error: Optional[str] = None
 
@dataclass
class QueueStatus:
    depth: int
    next_prompt_excerpt: str
    policy: str = "reject"  # 'queue' | 'ask' | 'new-thread' | 'reject'
 
@dataclass
class UsageStatus:
    source: str = "unavailable"  # Do not fake usage; 'unavailable' if absent
    input_tokens: Optional[int] = None
    output_tokens: Optional[int] = None
    cache_read_tokens: Optional[int] = None
    cache_write_tokens: Optional[int] = None
    total_tokens: Optional[int] = None
    cost_usd: Optional[float] = None
 
@dataclass
class StatusSnapshot:
    daemon: DaemonStatus
    discord: DiscordStatus
    runtime: RuntimeStatus
    run: RunStatus
    queue: QueueStatus
    usage: UsageStatus

3. 🎚️ Slash Commands

Slash commands define administrative controls and session operations. In discord.py, these are mapped using hybrid commands. Every slow-running command must defer the initial response before starting expensive operations.

CommandStatusBehavior
/omp statusImplementedRenders full StatusSnapshot embed, displaying current state variables.
/omp healthImplementedDiagnostics dashboard mapping gateway latency, DB checks, and process status.
/omp new [topic]ExistingDefer reply, spin up target Discord thread, create session on-disk, return link.
/omp say <prompt>ExistingDefer reply, inject prompt to active thread or spawn new thread, output progress.
/omp cancelImplementedDefer reply, send SIGTERM to the active OMP process, verify termination state.
/omp stopExistingDefer reply, archive active thread, clear session state in database.
/omp queueFutureShow queue depth, next prompts, and the configuration policy.
/omp doctorFutureVerify discord channel permissions, thread scopes, and bot intents.
/omp versionFutureReturns system versions, process PID, and daemon uptime.

4. 📋 Embed Dashboard SSOT

Field Contract

The embed dashboard aggregates active session context into a single scannable interface.

Field NameExample Value
Gatewayready as Omp#0305 · last ready 16:56 UTC
Active session<#THREAD_ID> · busy · 1 child · step 12
Runtime contextcwd=/home/usr · mode=headless-json · continuation=-c
Model/providermodel=claude-opus-4-8 · provider=anthropic · api=anthropic-messages
Usageinput=12.5k · output=2.1k · cacheRead=50.2k · cost=$0.1250 (or unavailable)
Queuedepth=0 · policy=reject
Last event/errorlast done code=0 (or spawn failed: omp not found)
ActionsRefresh · New Session · Cancel · Open Thread (Components UI)

Color Rules

The dashboard panel color signals daemon state.

StateEmbed Color (HEX)Meaning
idle#00FF00 (Green)Ready for command, no active subprocess running.
busy#5865F2 (Blurple)Subprocess actively writing or invoking tools.
waiting#FFFF00 (Yellow)Interactive interview block awaiting user response.
degraded#FFA500 (Orange)Active error code or tool execution failure logged.
failed#FF0000 (Red)Daemon process crashed or connection interrupted.

Sticky Dashboard (Leapfrog Relocation)

To prevent dashboard visibility from being lost during busy console logging, the daemon relocates it. If new text messages push the dashboard out of the immediate viewport, the previous dashboard is archived (grayed out, components stripped) and a new one is sent at the bottom. A debounce of at least 2 messages since the last update prevents excessive relocations.

Interactive Pinning

Prompts and active questions are pinned to the thread header to keep instructions visible. They are automatically unpinned and released upon completion, skips, or timeout. Thread pinning system notices are swept and deleted automatically to avoid chat pollution.

Embed-Based Todo Checklists

Dashboard embeds render multi-phase tasks in a structured list. Phases show visual indicators (e.g. [x] for completed, [>] for active, and [ ] for pending). The active phase is expanded to show child checklists, capped under the 6000-character Discord size limits.

5. 🚀 Rocket Launch Channels

Flow

Rocket launch channels allow users to post normal chat text, edit it, and trigger execution on demand.

sequenceDiagram
    autonumber
    actor User as Developer
    participant Omp as Omp (discord.py)
    participant Channel as Launch Channel
    participant Thread as Run Thread

    User->>Channel: Posts prompt text
    Omp->>Channel: Reacts with configured launch emoji
    User->>Channel: (Optional) Edits message to refine prompt
    User->>Channel: Clicks launch emoji reaction
    Omp->>Channel: Sends outside Dashboard Mirror
    Omp->>Thread: Creates session thread & starts OMP
    Note over Thread: OMP runs prompt; mirrors state back

Safety Invariants

  1. Emoji Check: Only clicks on the configured launch emoji trigger execution.
  2. Channel Check: The message must originate from allow-listed launch channel IDs.
  3. Allow-list: The clicking user must be explicitly authorized.
  4. Author Check: Only the author of the original message can trigger the launch.
  5. Non-empty Prompt: The message content must contain readable text. Bot-generated messages and reactions are skipped. The source message ID is stored as “pending” before any async execution to prevent double-click thread spawns.

Config

Config variables for rocket launch behavior are defined in ompycord.env:

  • OMP_LAUNCH_CHANNEL_IDS: Comma-separated list of approved channel IDs.
  • OMP_LAUNCH_EMOJI: Target emoji character (defaults to rocket emoji).

6. 🔗 Session Continuity (CLI ⇄ Discord)

cwd vs session store

Keeping execution separate from workspace data is crucial to prevent collision.

Directory NameEnvironment ArgumentDescription
cwd--cwd / OMP_WORKDIRDirectory where code edits, terminal commands, and workspace files are run.
session store--session-dirFolder storing the agent’s run transcript JSONL file.

Ompycord daemon configures the session store override path as ~/.omp/omp-sessions/<threadId>.

Resume commands

You can take over any Discord conversation inside your local terminal by targeting the thread ID:

# Set variables
THREAD=$THREAD_ID
# Resume conversation
omp --allow-home --cwd /home/usr --session-dir ~/.omp/omp-sessions/$THREAD -c

Safety: never double-write

Both systems must never write to the same .jsonl session file at once. Before resuming via the CLI, verify the daemon is idle by running pgrep -f 'python.*ompycord'. If busy, send /omp cancel in Discord or wait for the step to conclude.

7. 🧠 Responsive Interview Pattern

ACK Laws

Responsive elements must adhere to strict timing rules to avoid interaction failures.

RuleMetricAction
Initial ACK≤ 3 secondsMust call interaction.response.defer_update() or show_modal().
Follow-up Token≤ 15 minutesPerform edits or follow-ups within the active token window.
State StorageServer-sidecustom_id has a 100-char limit; store state keys inside the daemon.
Unified Surface1 MessageRebuild and edit the same message in-place rather than sending new ones.
User ScopeAllow-listRestrict component interaction events to the session author only.
Prerequisite ACKImmediateNever execute heavy agent tools before sending the initial ACK response.

UX State Machine

The interview transitions through states as options are resolved.

stateDiagram-v2
    [*] --> Idle
    Idle --> Asking: User triggers question
    Asking --> Answered: User clicks button / select
    Answered --> Confirming: Input validated
    Confirming --> Complete: Confirmed -> Resume agent
    Asking --> Cancelled: User clicks Cancel
    Asking --> TimedOut: 15min idle
    Cancelled --> [*]
    TimedOut --> [*]
    Complete --> [*]

Component Design

Ompycord uses the best matching interaction component for data collection.

Required InputComponentConfiguration
2-4 Optionsdiscord.ui.ButtonDirect buttons labeled with explicit outcomes.
5-25 Optionsdiscord.ui.SelectSingle select component.
Multi-choicediscord.ui.SelectSelect component with max_values > 1.
Freeform textdiscord.ui.ModalOpen a text input form.
Final commitAction buttonsAction row with Confirm / Edit / Cancel outcomes.

8. 🏷️ Embed Placeholder Directory

Gateway placeholders

  • [BOT_TAG]: Full username and discriminator of the client (e.g. Omp#0305).
  • [GATEWAY_STATUS]: Current status emoji indicator (e.g. [Ready]).
  • [GUILD_ID]: Snowflake ID of the host server.
  • [HOME_CHANNEL_MENTION]: Mention format for the command channel (<#CHANNEL_ID>).
  • [ACTIVE_THREAD_MENTION]: Mention format for the active thread.
  • [GATEWAY_PING]: Websocket latency in milliseconds.

Run state placeholders

  • [RUN_STATE]: Operational status emoji (e.g. Busy or Idle).
  • [RUN_PHASE]: Label of the active execution segment (e.g. Verification).
  • [RUN_STEP]: Count of completed agent steps.
  • [RUN_LAST_TOOLS]: Text list of the last executed tools (e.g. read › search › edit).
  • [RUN_TOOL_GLYPHS]: Graphic representation of tools (e.g. page, search, edit symbols).
  • [RUN_ELAPSED]: Elapsed run duration.
  • [RUN_PROMPT_EXCERPT]: Truncated string of the active target goal.
  • [RUN_STATUS_MESSAGE]: Context description of the focus (e.g. Thinking…).

Usage/cost placeholders

  • [MODEL_NAME]: Identifier of the loaded model.
  • [MODEL_PROVIDER]: Service provider name (e.g. anthropic).
  • [MODEL_API]: API endpoint structure.
  • [USAGE_INPUT]: Total input tokens processed.
  • [USAGE_OUTPUT]: Total output tokens generated.
  • [USAGE_CACHE_READ]: Tokens retrieved from cache memory.
  • [USAGE_TOTAL]: Sum of input, output, and cache tokens.
  • [USAGE_COST]: Estimated dollar amount spent.
  • [USAGE_BILLING_SUMMARY]: Aggregated string displaying tokens and cost.

Config/settings placeholders

  • [CONFIG_REACTIONS]: Status indicator for emoji reactions.
  • [CONFIG_TOOL_LOGS]: Toggle status for output logging in threads.
  • [CONFIG_AUTO_START]: Auto-run execution trigger state.
  • [CONFIG_USE_THREADS]: Toggle indicator for routing executions inside threads.
  • [CONFIG_LOG_DETAIL]: Toggle indicator for verbose embed reports.

9. 🔌 Integration Tiers (Python Rewrite)

Tier 1: Webhook (discord.py)

This tier provides OMP → Discord one-way events without dependencies.

# python_webhook.py
import aiohttp
import os
import asyncio
 
async def send_webhook(url: str, content: str):
    payload = {"content": content}
    async with aiohttp.ClientSession() as session:
        async with session.post(url, json=payload) as response:
            return response.status
 
if __name__ == "__main__":
    url = os.environ["DISCORD_WEBHOOK_URL"]
    asyncio.run(send_webhook(url, "✅ OMP subprocess finished."))

Tier 2: MCP Server (discord.py + mcp SDK)

This tier exposes discord.py features to OMP via the MCP protocol.

# mcp_server.py
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
import discord
 
server = Server("ompycord-mcp")
intents = discord.Intents.default()
bot = discord.Client(intents=intents)
 
@server.tool()
async def discord_send(channel_id: int, message: str) -> str:
    """Send a message to a Discord channel."""
    channel = bot.get_channel(channel_id)
    if channel:
        await channel.send(message)
        return "Message sent successfully"
    return "Channel not found"
 
async def main():
    # Run the stdio MCP server in parallel with the bot start
    asyncio.create_task(bot.start("DISCORD_TOKEN"))
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())
 
if __name__ == "__main__":
    asyncio.run(main())

Tier 3: Full Sidecar (discord.py + FastAPI + SQLite)

A bidirectional bridge persisting session contexts in SQLite and exposing REST webhooks.

# sidecar.py
import asyncio
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel
from typing import Optional, Dict, Any
import aiosqlite
import discord
from discord.ext import commands
 
app = FastAPI()
bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())
 
class OMPEvent(BaseModel):
    session_id: str
    event_type: str
    tool_name: Optional[str] = None
    content: str
    metadata: Dict[str, Any] = {}
    timestamp: str
 
class SteeringCommand(BaseModel):
    session_id: str
    action: str
    content: str
 
async def init_db():
    async with aiosqlite.connect("sessions.db") as db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS agent_sessions (
                session_id TEXT PRIMARY KEY,
                discord_thread_id INTEGER,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        await db.execute("""
            CREATE TABLE IF NOT EXISTS pending_commands (
                command_id TEXT PRIMARY KEY,
                session_id TEXT,
                action TEXT,
                content TEXT,
                status TEXT DEFAULT 'pending',
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY(session_id) REFERENCES agent_sessions(session_id)
            )
        """)
        await db.commit()
 
@app.post("/api/webhook/event")
async def receive_event(event: OMPEvent, background_tasks: BackgroundTasks):
    # Retrieve mapped channel / thread from sqlite and send embed
    background_tasks.add_task(post_to_discord, event)
    return {"status": "event_queued"}
 
async def post_to_discord(event: OMPEvent):
    async with aiosqlite.connect("sessions.db") as db:
        async with db.execute(
            "SELECT discord_thread_id FROM agent_sessions WHERE session_id = ?",
            (event.session_id,)
        ) as cursor:
            row = await cursor.fetchone()
            if row:
                thread_id = row[0]
                thread = bot.get_channel(thread_id)
                if thread:
                    embed = discord.Embed(
                        title=f"Event: {event.event_type}",
                        description=event.content,
                        color=0x5865F2
                    )
                    await thread.send(embed=embed)
 
@bot.event
async def on_ready():
    await init_db()
    print(f"Bot connected as {bot.user}")
 
async def run_services():
    import uvicorn
    config = uvicorn.Config(app, host="127.0.0.1", port=8901, log_level="info")
    server = uvicorn.Server(config)
 
    # Run FastAPI web app and discord.py Client concurrently
    await asyncio.gather(
        server.serve(),
        bot.start("DISCORD_TOKEN")
    )
 
if __name__ == "__main__":
    asyncio.run(run_services())

10. 🔭 Ecosystem Learnings (Adopt/Adapt/Skip)

Priority table

RankWin / FeatureSource PackageActionValueEffortReasoning
1Honest usage reportspi-ai / run.mjsAdoptHighSFeed actual streamed usage stats; clear “unavailable” placeholder.
2Red tool error flagpi-agent-coreAdoptHighSUpdate live dashboard display on tool error markers.
3Reconnect retrypi-discord-remoteAdoptHighSAdd backoff retries to prevent connection drop timeouts.
4Embed debounceSlack API / Discord limitAdoptMedSLimit update frequencies to avoid HTTP 429 rate blocks.
5Sanitize console outputspi-utilsAdoptMedSClean ANSI formatting characters from tool message outputs.
6Queue policy optionspi-coding-agentAdaptMedMAdd select controls to update active queue execution modes.
7Session Handoff templatespi-agent-coreAdaptMedMGenerate summary text templates for archived thread outputs.
8Components V2discord.js / gatewayAdaptHighMMove dashboard items to inline grids using v2 components.

Key adoptions

  • pi-ai: Consume streamed usage logs on message_end turns.
  • pi-agent-core: Capture execution event errors to flag failed turns.
  • pi-discord-remote: Retain permissions allow-lists and backoff connections.
  • pi-utils: Strip control strings and formatting indicators from system logs before sending to embeds.

11. 🚨 Gotchas & Invariants

  • 3-Second ACK Limit: Components and interaction events must return a response within 3 seconds, or the Discord gateway invalidates the event context.
  • Modal Constraints: The show_modal() response must be called as the immediate response to the interaction.
  • Unavailable Usage Stats: If token details are missing from message_end.message.usage, output Usage: unavailable rather than inventing estimates.
  • Size Limit: Maximum embed text content is capped at 6,000 characters.
  • Rate Limits: Limit channel message edits to at most 5 edits per 5 seconds to prevent Discord 429 blocks.
  • Custom ID Scope: custom_id strings must not exceed 100 characters.
  • V2 Layout One-Way Flag: Sending components with V2 flags turns off normal embeds for that message context.
  • Interaction Ephemeral Deprecation: InteractionReplyOptions.ephemeral is deprecated; pass the MessageFlags.Ephemeral flags parameter.

12. 🔍 Verification Checklist

  • StatusSnapshot populates all data fields (daemon, discord, runtime, run, queue, usage).
  • Missing statistics display as unavailable or inferred without fallback mockups.
  • Relocation edits are debounced by at least 2 messages.
  • Rocket launch trigger limits activation to original message author.
  • Session restore correctly maps thread IDs to ~/.omp/omp-sessions/<threadId>.
  • Command interactions return an ACK callback within 3 seconds.
  • Embed placeholders are successfully replaced during rendering pass.
  • fastapi web app responds with 200 OK on mock event POST webhook.

13. 🗺️ Rollout Phases

PhaseScopeKey DeliverablesRisk
Phase 1Core DaemonSetup discord.py structure and hybrid administrative slash commands.Low
Phase 2Dashboard SSOTSQLite database schema setup and StatusSnapshot layout engine.Med
Phase 3ContinuityRocket launch trigger controls and thread resume commands.Med
Phase 4Interview flowsModal text boxes, confirmation buttons, and user interaction loop.Med
Phase 5OptimizationIntegration of ecosystem learnings and component grid layout.Low