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 pydanticDefine 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: UsageStatus3. 🎚️ 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.
| Command | Status | Behavior |
|---|---|---|
/omp status | Implemented | Renders full StatusSnapshot embed, displaying current state variables. |
/omp health | Implemented | Diagnostics dashboard mapping gateway latency, DB checks, and process status. |
/omp new [topic] | Existing | Defer reply, spin up target Discord thread, create session on-disk, return link. |
/omp say <prompt> | Existing | Defer reply, inject prompt to active thread or spawn new thread, output progress. |
/omp cancel | Implemented | Defer reply, send SIGTERM to the active OMP process, verify termination state. |
/omp stop | Existing | Defer reply, archive active thread, clear session state in database. |
/omp queue | Future | Show queue depth, next prompts, and the configuration policy. |
/omp doctor | Future | Verify discord channel permissions, thread scopes, and bot intents. |
/omp version | Future | Returns 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 Name | Example Value |
|---|---|
| Gateway | ready as Omp#0305 · last ready 16:56 UTC |
| Active session | <#THREAD_ID> · busy · 1 child · step 12 |
| Runtime context | cwd=/home/usr · mode=headless-json · continuation=-c |
| Model/provider | model=claude-opus-4-8 · provider=anthropic · api=anthropic-messages |
| Usage | input=12.5k · output=2.1k · cacheRead=50.2k · cost=$0.1250 (or unavailable) |
| Queue | depth=0 · policy=reject |
| Last event/error | last done code=0 (or spawn failed: omp not found) |
| Actions | Refresh · New Session · Cancel · Open Thread (Components UI) |
Color Rules
The dashboard panel color signals daemon state.
| State | Embed 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
- Emoji Check: Only clicks on the configured launch emoji trigger execution.
- Channel Check: The message must originate from allow-listed launch channel IDs.
- Allow-list: The clicking user must be explicitly authorized.
- Author Check: Only the author of the original message can trigger the launch.
- 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 Name | Environment Argument | Description |
|---|---|---|
| cwd | --cwd / OMP_WORKDIR | Directory where code edits, terminal commands, and workspace files are run. |
| session store | --session-dir | Folder 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 -cSafety: 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.
| Rule | Metric | Action |
|---|---|---|
| Initial ACK | ≤ 3 seconds | Must call interaction.response.defer_update() or show_modal(). |
| Follow-up Token | ≤ 15 minutes | Perform edits or follow-ups within the active token window. |
| State Storage | Server-side | custom_id has a 100-char limit; store state keys inside the daemon. |
| Unified Surface | 1 Message | Rebuild and edit the same message in-place rather than sending new ones. |
| User Scope | Allow-list | Restrict component interaction events to the session author only. |
| Prerequisite ACK | Immediate | Never 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 Input | Component | Configuration |
|---|---|---|
| 2-4 Options | discord.ui.Button | Direct buttons labeled with explicit outcomes. |
| 5-25 Options | discord.ui.Select | Single select component. |
| Multi-choice | discord.ui.Select | Select component with max_values > 1. |
| Freeform text | discord.ui.Modal | Open a text input form. |
| Final commit | Action buttons | Action 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.BusyorIdle).[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
| Rank | Win / Feature | Source Package | Action | Value | Effort | Reasoning |
|---|---|---|---|---|---|---|
| 1 | Honest usage reports | pi-ai / run.mjs | Adopt | High | S | Feed actual streamed usage stats; clear “unavailable” placeholder. |
| 2 | Red tool error flag | pi-agent-core | Adopt | High | S | Update live dashboard display on tool error markers. |
| 3 | Reconnect retry | pi-discord-remote | Adopt | High | S | Add backoff retries to prevent connection drop timeouts. |
| 4 | Embed debounce | Slack API / Discord limit | Adopt | Med | S | Limit update frequencies to avoid HTTP 429 rate blocks. |
| 5 | Sanitize console outputs | pi-utils | Adopt | Med | S | Clean ANSI formatting characters from tool message outputs. |
| 6 | Queue policy options | pi-coding-agent | Adapt | Med | M | Add select controls to update active queue execution modes. |
| 7 | Session Handoff templates | pi-agent-core | Adapt | Med | M | Generate summary text templates for archived thread outputs. |
| 8 | Components V2 | discord.js / gateway | Adapt | High | M | Move dashboard items to inline grids using v2 components. |
Key adoptions
- pi-ai: Consume streamed usage logs on
message_endturns. - 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, outputUsage: unavailablerather 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_idstrings 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.ephemeralis deprecated; pass theMessageFlags.Ephemeralflags parameter.
12. 🔍 Verification Checklist
-
StatusSnapshotpopulates all data fields (daemon, discord, runtime, run, queue, usage). - Missing statistics display as
unavailableorinferredwithout 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
| Phase | Scope | Key Deliverables | Risk |
|---|---|---|---|
| Phase 1 | Core Daemon | Setup discord.py structure and hybrid administrative slash commands. | Low |
| Phase 2 | Dashboard SSOT | SQLite database schema setup and StatusSnapshot layout engine. | Med |
| Phase 3 | Continuity | Rocket launch trigger controls and thread resume commands. | Med |
| Phase 4 | Interview flows | Modal text boxes, confirmation buttons, and user interaction loop. | Med |
| Phase 5 | Optimization | Integration of ecosystem learnings and component grid layout. | Low |
14. 🔗 Related
- UniDialog — schema-driven Discord dialog engine.
- SSOTEmbed — pinned embed editor patterns.
- Red vs OMP — Red-DiscordBot ↔ OMP architecture mapping.
- Ompycord Review — reflective architecture review.
- Ompycord Cheat Sheet — quick-start command reference.
- Stack: omp · ompkeep