🔭 Ompcord — Ecosystem Learnings
What this is. The deliverable the Handoff & Research Charter commissioned: a grounded, source-read comparison of every relevant pi/omp/discord package, scored for what Ompcord should learn or adapt while it is still early-stage. Every claim is cited to a real file path (read under
.bun/install/cache/…) or a URL. Nothing here is hand-waved from a mockup.
How to read each entry (5-point contract).
- What it is / does — grounded one-liner. 2. Key patterns & APIs — the symbols/files worth knowing. 3. Verdict — 🟢 Adopt · 🟡 Adapt · 🔴 Skip (per feature). 4. Effort · risk —
S/M/L·low/med/high. 5. Cite — exact source.
Legend — 🟢 Adopt: lift as-is / consume directly · 🟡 Adapt: copy the pattern, re-implement for Discord/daemon · 🔴 Skip: wrong substrate or premature. Effort S≈hours, M≈a focused day, L≈multi-day. Risk = blast radius if it breaks a live turn.
flowchart LR subgraph Ompcord["🤖 Ompcord (repo pi-discord-amy, persona Amy)"] AMYD["amyd.mjs daemon<br/>spawns omp -p --mode json"] RUN["run.mjs<br/>JSONL parser → embed"] DASH["dashboard.mjs<br/>one evolving embed"] ASK["ask.mjs<br/>select + buttons + modal"] IDX["index.mjs<br/>Pi extension hooks + tools"] end subgraph Engine["⚙️ omp engine (read the source)"] PCA["pi-coding-agent<br/>extension/SDK API"] PAC["pi-agent-core<br/>loop · events · compaction · handoff"] PAI["pi-ai<br/>Usage · cost · model catalog"] PU["pi-utils<br/>stream · format · sanitize"] end subgraph Plugins["🔌 omp plugins (compare UX)"] STATS["omp-stats"] SWARM["swarm-extension"] LF["pi-langfuse"] BASE["pi-discord-remote (baseline)"] end subgraph Discord["💬 discord.js v14 ecosystem"] DJS["Components V2 · modals · collectors · REST limits"] end IDX -->|"hooks + registerTool"| PCA RUN -->|"consumes AgentEvent JSONL"| PAC DASH -->|"should read"| PAI RUN -->|"could swap parser"| PU DASH --> DJS ASK --> DJS AMYD -. "forks / supersedes" .-> BASE AMYD -. "future delegate mode" .-> SWARM AMYD -. "future turn telemetry" .-> LF AMYD -. "imitate per-thread aggregation" .-> STATS
🧩 Part 1 — Internal omp/pi packages (.bun/install/cache/@oh-my-pi/)
pi-coding-agent 15.10.7 — the extension & SDK API Ompcord plugs into
- What it is / does — One package exposing (a) the in-process
ExtensionAPI(export default (pi) => void, exactly Ompcord’sindex.mjsshape), (b) a narrowerHookAPI, and (c) a programmatic SDK (createAgentSession()+SessionManager) that drives the same loop and emits the same JSONL stream the daemon parses. - Key patterns & APIs
- ~40 lifecycle events (
src/extensibility/extensions/types.ts:899-952). Ompcord rides only 8. Unused but high-value:after_provider_response+ctx.getContextUsage()(ContextUsage = {tokens, contextWindow, percent}, types.ts:276-279,297),auto_compaction_start/_end,auto_retry_start/_end,turn_start/turn_end. pi.registerTool({name,label,description,parameters,execute,approval?,renderCall?,renderResult?})— accepts TypeBox or Zod (both injected viapi.typebox/pi.zod);execute()returns{content,details}wheredetailsis persisted + branch-aware. ⚠️ canonical signatureexecute(toolCallId, params, signal, onUpdate, ctx)(types.ts:407-415) but shipped examples pass(toolCallId, params, onUpdate, ctx, signal)— verify arg order against the installed version.pi.registerCommand(name,{handler,getArgumentCompletions}); file-based commands are now “prompt templates” (discoverPromptTemplates()).SessionManager.create(cwd, customDir?) / continueRecent / list / open— thecustomDirarg (no cwd-encoding) is the native primitive behind Ompcord’s~/.omp/amy-sessions/<threadId>; gives parent-tracking + branch nav for free.- Hooks that map to Ompcord:
permission-gate.ts(tool_call→{block,reason}, auto-blocks when!ctx.hasUI) is a real per-tool approval gate;status-line.ts(ctx.ui.setStatus) is the TUI analog of the Dashboard embed; the native ask usesselectionMarker:'radio'|'checkbox'+markableCount— the exact radio/multi-select Ompcord rebuilds in Discord.
- ~40 lifecycle events (
- Verdict — 🟢 Adopt
ctx.getContextUsage()/after_provider_responseto put live token-% on the embed; 🟢 Adopttool_call → {block,reason}as a server-side approval gate complementing the allow-list; 🟡 Adaptturn_start/turn_endas a cleaner heartbeat thanmessage_updatespam; 🟡 AdaptSessionManager.create(cwd,customDir)to replace bespoke--session-dirCLI plumbing; 🔴 Skip the FS/git safety hooks (dirty-repo-guard,protected-paths,git-checkpoint) — not a daemon concern. - Effort · risk — usage/heartbeat fields S · low; approval gate M · med (headless
hasUI=falsedefault-block + Discord round-trip ≤3s); SessionManager swap M · med (parity for the-cpath). - Cite —
pi-coding-agent@15.10.7/src/extensibility/extensions/types.ts:98-503,877-1103;…/hooks/types.ts:384-563;examples/{extensions,hooks,sdk}/*(esp.permission-gate.ts,status-line.ts,tools.ts,sdk/11-sessions.ts).
pi-agent-core 15.10.7 — agent loop · event taxonomy · compaction · handoff
- What it is / does — The engine under
omp: the streaming loop (agent-loop.ts), theAgentEventunion that becomes the JSONL streamrun.mjsparses, the compaction subsystem, and per-run telemetry. - Key patterns & APIs
AgentEventunion (src/types.ts:495-515):agent_start,turn_start,turn_end{message,toolResults},message_start{message},message_update{message,assistantMessageEvent},message_end{message},tool_execution_start{toolCallId,toolName,args,intent?},tool_execution_update{…,partialResult},tool_execution_end{…,result,isError?},agent_end{messages,telemetry?,coverage?}.agent_end.telemetry(run-collector.ts:68-114):AgentRunSummary.usage{inputTokens,outputTokens,cachedInputTokens,cacheWriteTokens,reasoningOutputTokens,totalTokens},cost{estimatedUsd,unavailableReasons[]},chats{byStopReason},errors{byType};AgentRunCoverage{toolsAvailable,toolsInvoked,toolsUnused}. Present only iff a telemetry config was supplied —[INFERENCE]it is not provable from this package whether headlessomp -pwires it; read defensively, degrade if absent.- Compaction (
compaction.ts:128-235):strategy:'context-full'|'handoff'|'shake'|'off',shouldCompact()fires pastcontextWindow - max(15%, reserveTokens). - Handoff prompt (
compaction/prompts/handoff-document.md): the fixed skeleton## Goal / Constraints & Preferences / Progress(Done,In Progress,Pending) / Key Decisions / Critical Context / Next Steps(+{{additionalFocus}}). This very page’s parent handoff used it.
- Verdict — 🟢 Adopt
tool_execution_end.isErrorinrun.mjs#handleEvent→ mark failed tool steps red instead of silently “success” (todayhandleEventhas no case for it). 🟢 Adoptagent_end.telemetry(guardundefined) for a per-thread spend/latency embed footer. 🟡 Adapthandoff-document.mdas a Discord “session handoff” — on thread archive //amy new, run one--mode jsonsummarization turn and post the 6-section handoff as a pinned embed or attached.md. 🔴 Skip the compaction trigger machinery (omp owns it) andtool_execution_update.partialResult/turn_*(noise for a single embed). - Effort · risk — isError case S · low; telemetry footer S · low; handoff embed M · med.
- Cite —
pi-agent-core@15.10.7/src/types.ts:495-515;agent-loop.ts:222-303,1270-1597;run-collector.ts:30-114,398-410;compaction/compaction.ts:128-235;compaction/prompts/handoff-document.md; consumer sidepi-discord-amy/run.mjs:8-30.
pi-ai 15.10.7 — model / provider / usage (kills usage: unavailable)
- What it is / does — omp’s model-provider abstraction: canonical message types, per-turn token+cost accounting (
Usage), the model pricing catalog (Model), and a separate provider-quota schema (UsageReport). - Key patterns & APIs — Two distinct “usage” notions, do not conflate:
- Per-turn
Usage(src/types.ts:474-519):{input, output, cacheRead, cacheWrite, totalTokens, premiumRequests?, reasoningTokens?, cost:{input,output,cacheRead,cacheWrite,total}}— lives onAssistantMessage.usage(types.ts:556-583) alongsideprovider/model/api/stopReason/duration/ttft, and is serialized verbatim into every session-JSONLmessageentry. - Pricing catalog
Model.cost($/M tokens) viagetBundledModel(provider, modelId)(models.ts:24-36) → recompute cost whenusage.cost.total===0. - Provider quota
UsageReport{provider,fetchedAt,limits:UsageLimit[]}withamount{used,limit,remaining,usedFraction}+status:'ok|warning|exhausted'(src/usage.ts).
- Per-turn
- Verdict — 🟢 Adopt now (highest-ROI fix).
status.mjshardcodesusage:{source:"unavailable"}(status.mjs:24,113) andusageField()prints"unavailable from current omp JSONL"(status.mjs:191-200) — yetrun.mjs#handleEventalready receivesev.messageonmessage_end(run.mjs:21-26) and just discardsev.message.usage. Liftusageoff that message and feedcreateRuntimeState/snapshotStatus;/amy statusbecomes honest with zero new deps. 🟢 AdoptgetBundledModel().costfor cost backfill. 🟡 AdaptUsageReportfor/amy health“quota remaining” — schema is ready but needs a credentialed fetch Ompcord lacks under headlessomp -p; surface only if a/v1/usageendpoint is reachable, else omit honestly. - Effort · risk — per-turn usage display S · low; quota/health M · med.
- Cite —
pi-ai@15.10.7/src/types.ts:474-519,556-583,845-860;src/usage.ts;src/models.ts:24-36; verified againstpi-discord-amy/status.mjs:24,113,191-200+run.mjs:21-26.
omp-stats 15.10.10 — per-thread usage aggregation (imitate, don’t import)
- What it is / does — Local observability dashboard: parses omp session JSONL into SQLite (
~/.omp/stats.db) and serves aggregated AI-usage stats (tokens, cost, cache/error rate, latency, tokens/s) by model/folder/time. - Key patterns & APIs — Source = JSONL under
getSessionsDir()=~/.omp/agent/sessions/(pi-utilsdirs.ts:417), discovered bylistSessionFolders/Files(parser.ts:280-296); it does NOT read~/.omp/amy-sessions/.extractStats/parseSessionFile(parser.ts:114-180) liftentry.message.usage(the pi-aiUsage) intoMessageStats.calculateCatalogCost/resolveStoredCost(db.ts:200-245) recompute $ from tokens. Aggregate types (AggregatedStats/ModelStats/FolderStats,shared-types.ts) are server-dep-free + import-safe. - Verdict — 🔴 Skip calling
getDashboardStats()/syncAllSessions()directly: DB-bound, globally rooted, aggregates all omp usage, blind to amy-sessions. 🟡 Adapt — copyextractStats+calculateCatalogCost(~80 LOC) to read a single thread’samy-sessions/<threadId>/*.jsonlon demand for/amy status(add tool-count tracking, which omp-stats omits). 🟢 Adopt theAggregatedStats/ModelStatstype shapes as the report structure (no runtime dep). - Effort · risk — imitate-parser per-thread aggregation M · low; reusing the live DB L · med (path/scope mismatch).
- Cite —
omp-stats@15.10.10/README.md;src/parser.ts:114-180,280-296;src/db.ts:55-130,200-245;src/shared-types.ts;src/index.ts:6-32;pi-utils/src/dirs.ts:368-419.
pi-tui 15.10.7 — rendering parity (copy the UX, never import)
- What it is / does — omp’s terminal UI toolkit: a
marked-based themed Markdown renderer, a brailleLoaderspinner, and box/table symbol sets — all ANSI-targeted. - Key patterns & APIs —
Loader(components/loader.ts): frames["⠋"…"⠏"],SPINNER_ADVANCE_MS=80,setMessage()rewrites one line in place — the exact model Ompcord’s evolving embed already follows.MarkdownTheme(components/markdown.ts:96-140): named slotsheading/link/code/codeBlock/quote/listBullet/bold/table+ LRU cache.SymbolTheme/BoxSymbols(symbols.ts): glyph vocabulary. - Verdict — 🟡 Adapt the layout decisions only (single-line spinner+message → the embed status line; the element taxonomy → consistent embed/code-fence styling). 🔴 Skip the actual renderer, swatches, Kitty graphics — a Discord bot cannot emit ANSI; importing pi-tui is wrong.
- Effort · risk — S · low (design crib, zero dependency added).
- Cite —
pi-tui@15.10.7/src/components/loader.ts,…/markdown.ts:96-140,…/symbols.ts,…/index.ts.
hashline 15.10.7 — adapt the diff preview, skip the engine
- What it is / does — A line-anchored LLM-edit format:
[PATH#TAG](TAG = 4-hex xxHash32 of normalized text) +replace/delete/insertops, applied by a stale-awarePatcherwith 3-way recovery. - Key patterns & APIs — Engine:
Patcher({fs,snapshots}),Patch.parse(), tree-sitter forreplace block. The valuable pure bit:buildCompactDiffPreview(diff,{maxAddedRunContext})(src/diff-preview.ts) — renumbers a numbered unified diff into a compact post-edit preview with added/removed stats +…run elision; dependency-free (~120 LOC). - Verdict — 🔴 Skip the Patcher/SnapshotStore/tree-sitter (Ompcord runs omp as a black box; it never applies patches). 🟡 Adapt
buildCompactDiffPreviewas a vendored pure function to render a compact diff in the Dashboard embed when omp emits anedit/writeevent; if omp already emits[PATH#TAG]strings, fence them verbatim — essentially free. - Effort · risk — adapt-preview S · low; full engine 🔴 N/A.
- Cite —
hashline@15.10.7/README.md;src/diff-preview.ts;src/format.ts:60-160;src/types.ts:1-160;package.json(depsdiff@9,lru-cache).
pi-mnemopi 15.10.7 / pi-mnemosyne 15.6.0 — cross-session memory (defer; watch native deps)
- What it is / does — Embeddable cross-session memory: SQLite + FTS + optional vector recall behind a
Mnemopifacade (remember/recall/sleep).pi-mnemosyne@15.6.0is the same engine, prior name — usemnemopi. - Key patterns & APIs —
remember(content,{source,importance,veracity,scope}),recall(query,k),recallEnhanced,getContext, scratchpad. Schema (src/types.ts) carrieschannel_id,author_id,author_type,topic,scope,trust_tier,importance,valid_until— maps almost 1:1 onto Discord per-user/per-thread/per-guild memory.noEmbeddings:true→ FTS-only (no native deps). Scoping:global/per-project/per-project-tagged. - Verdict — 🟡 Adapt, but defer. Right shape to back “Amy remembers prefs/context across threads” (
userId→author_id,threadId→channel_id/bank). Cost = deps: pullspi-ai+fastembed+onnxruntime-node(native ONNX) — heavy for a discord.js-only bot. Mitigate vianoEmbeddings(FTS-only) or run mnemopi as an out-of-process MCP sidecar. Early-stage: wait for a concrete memory feature. - Effort · risk — M · med (SQLite/scoping trivial; native ONNX footprint is the real risk — avoid via FTS-only or sidecar).
- Cite —
pi-mnemopi@15.10.7/README.md,src/index.ts,src/types.ts,package.json;pi-mnemosyne@15.6.0/README.md(identical twin).
pi-utils 15.10.7 (+ pi-natives) — delete hand-rolls under Bun
- What it is / does — Bun-targeted helper barrel (
src/index.ts): async, abortable, format, fetch-retry, sanitize-text, stream, snowflake, dirs, env.pi-nativesis the native (tree-sitter / hashing) layer those build on. - Key patterns & APIs vs Ompcord’s
helpers.mjs—stream.readJsonl(stream,signal)/readLines(robust, abortable JSONL — replacesrun.mjs’s bespoke buffer-and-split parser);async.withTimeout(promise,ms,msg,signal?)(superset of helpers’withTimeout, adds AbortSignal);abortable.*(a realAbortErrorclass, cleaner thanisAbortLikeErrorstring-sniffing);format.{formatDuration,formatNumber,formatBytes,formatAge,truncate}(Ompcord has none — perfect for the embed);sanitizeText(strip ANSI/control chars from tool stdout before posting). No equivalent:splitMessage(fence-aware 1900-char Discord chunking) — keep it local. - Verdict — 🟢 Adopt selectively under Bun:
stream.readJsonl,async.withTimeout,abortable.*,format.*,sanitizeText. 🟡 Tradeoff: Ompcord currently has only discord.js as a dep andpi-utilsis Bun-engine-locked (TS-sourcemain, usesBun.*). Since the omp ecosystem runs Bun for dev, adopt to delete duplication; if Node-portability is required, vendor the 3–4 functions instead. Keep Discord-domain helpers local. - Effort · risk — S–M · low (additive import; risk = Bun-only API lock-in, acceptable in this runtime).
- Cite —
pi-utils@15.10.7/src/{index,async,abortable,format,fetch-retry,sanitize-text,stream,snowflake}.ts; compared againstpi-discord-amy/helpers.mjs.
🔌 Part 2 — Installed omp plugins (~/.omp/plugins/package.json)
@oh-my-pi/swarm-extension 13.17.0 — model for “Amy delegates to sub-agents”
- What it is / does — Multi-agent orchestration: declare agents in one YAML (
swarm:key); it runs them as a DAG of topological waves until done, in-TUI (/swarm run) or standalone (omp-swarm file.yaml). - Key patterns & APIs — Spawn:
executeSwarmAgent()→runSubprocess({cwd,agent,task,onProgress,artifactsDir,…})(executor.ts:44-115) — each agent = a full omp subprocess with all tools. Coordination = topological waves, not IPC:buildDependencyGraph→detectCycles→buildExecutionWavesfromwaits_for/reports_to;PipelineController.#runIterationruns a wave viaPromise.all, waves sequential (pipeline.ts:56-210). Agents talk only through the shared workspace filesystem. State:.swarm_<name>/{state/pipeline.json,logs,context}withload()for resume (state.ts:16-150). Result merge is human/LLM-mediated —pi.sendMessage({customType:'swarm-result'},{triggerTurn:false})posts a markdown summary. - Verdict — 🟡 Adapt for a future “Amy delegates” mode: Ompcord already shells
omp -p, so a delegating turn spawns N childomp -pworkers in a shared WORKDIR + a final synthesizer. Transferable: (a) the YAML→DAG→waves topology with cycle detection, (b) the.swarm_<name>/live-state JSON to drive an evolving “fan-out” embed (one field per sub-agent), (c) filesystem-as-bus to skip building IPC. 🔴 Skip importingrunSubprocessdirectly — it targets in-process TUI, not the daemon’s CLI-subprocess substrate. - Effort · risk — L · med (DAG/state concepts cheap; a robust multi-process spawner with abort/backpressure + per-agent embed fields + merge is the bulk; concurrent-cost/rate-limit risk).
- Cite —
swarm-extension@13.17.0/README.md:1-12,90-200;src/swarm/{executor.ts:44-115,pipeline.ts:56-210,state.ts:16-150};src/extension.ts:24-60,200-230.
pi-langfuse 1.4.3 — run telemetry Ompcord can emit without loading it
- What it is / does — Observability-only omp extension: one Langfuse trace per user prompt — a root
agentobservation with per-requestgeneration+ per-calltoolchildren, plus usage/cost and 5 health scores. - Key patterns & APIs — Wires ~17 hooks (
index.ts:60-200):before_provider_request→startGeneration,after_provider_response→updateGenerationMetadata,message_update→recordTTFT,tool_execution_start/tool_call→startToolObservation,tool_result/tool_execution_end→finishToolObservation,agent_end→finishAgentRun. Two robustness patterns worth copying verbatim: (a) deferred flushsetTimeout(shutdownRuntime,0)afteragent_endso telemetry never delays the turn; (b)session_shutdowncloses dangling observations as cancelled so killed runs aren’t lost. Trace correlation:sessionId = basename(getSessionFile(),'.jsonl'). Scores:tool_call_count,turn_count,total_tool_errors,tool_success_rate,session_had_errors. Config:~/.pi/agent/pi-langfuse/config.jsonorLANGFUSE_*env. - Verdict — 🟡 Adapt the trace model + score formulas in the daemon’s JSONL loop (one Discord turn → one trace;
tool_execution_start/tool_resultpair → atoolobservation; compute the same 5 scores to surface on the embed). Use<threadId>as the LangfusesessionId. 🟢 Adopt the two robustness patterns verbatim (deferred flush; cancel-on-shutdown). 🔴 Skip loading the extension in-process — the daemon is out-of-process; emit from the parser instead. - Effort · risk — M · low (additive, non-load-bearing; swallow telemetry failures as this package does; token/cost fields depend on omp surfacing them in JSON events — see
pi-ai). - Cite —
pi-langfuse@1.4.3/index.ts:60-200;src/handlers/agent.ts:21-150;src/config.ts:7-130;package.json:11-47.
pi-discord-remote 0.2.4 — the baseline Ompcord forks (UX diff)
- What it is / does — The published omp↔Discord extension Ompcord descends from: each
startauto-creates a fresh text channel<project>-<mon><dd>-<HHMM>, treats channel messages as prompts, and deletes the channel onstop/shutdown to stay under Discord’s channel cap. (package.json:5, README.) - Key patterns & APIs — Commands
/pi-discord-remote setup|start|stop|status|open-config; config{token,guildId,categoryId,allowedUserIds[],reactions,toolResponses}+DISCORD_*env. Hooks (dist/index.js):agent_start(busy),message_update(one💭 Thinking…),tool_execution_start(🔧 bash/📄 read/✏️ editlabel),tool_result(opt-intoolResponses, ≤400 chars, capturesagent_browserimage),agent_end(flush). Reliability: reconnect backoff2s→60s×10;sendMessageViaDiscordRestREST fallback with explicit error codes (unknown_channel:<id>,discord_http_<status>) +withTimeout. Tools:discord_send_image(path/url/base64 +agent_browserfallback);discord_ask_user_question— blocks the TUI-onlyask_user_questionviatool_callinterception and hints the LLM viabefore_agent_start, falling back to the TUI dialog when disconnected. Needs Message Content intent + Manage Channels. - Verdict (vs Ompcord = always-on daemon, thread + one evolving embed, ASK_PROTOCOL under headless) —
- 🔴 Skip — channel-per-session. Ompcord’s persistent threads + single embed are strictly better for an always-on persona; avoids the Manage-Channels permission. Ompcord is ahead here.
- 🟢 Adopt/keep —
allowedUserIdsallow-list +wrong_guildgate (parity, already present). - 🟢 Adopt — reconnect backoff +
sendMessageViaDiscordRestREST fallback with explicit error codes. A daemon needs exactly this; cheap to lift intoamyd.mjs. - 🟡 Adapt — tool-label streaming. Keep Ompcord’s coalesced embed (less spam) but reuse the
toolLabelemoji map + 400-char truncation for the activity line. - 🟢 Adopt —
discord_send_imagesource-precedence + explicit send-error contract as the reference impl. - 🟡 Adapt —
ask_user_questioncoexistence: keep Ompcord’sASK_PROTOCOL, but adopt the baseline’s don’t-uninstall-the-TUI-tool, intercept-only-when-connected fallback story. - 🟡 Adapt —
toolResponsesopt-in toggle for verbose debug into the embed/thread.
- Effort · risk — reconnect + REST fallback S · low; ask coexistence M · med (honor ≤3s ACK); allow-list/env parity S · low.
- Cite —
pi-discord-remote@0.2.4/package.json:2,11-46;README.md:1-90;dist/index.js:1-300.
💬 Part 3 — discord.js v14 ecosystem
All builders/flags below are importable from discord.js@14.26.4 today (it re-exports @discordjs/builders@1.14.1, @discordjs/rest@2.6.1, @discordjs/ws@1.2.3, discord-api-types@0.38.48 — discord.js/src/index.js:249-254). No version bump required.
Components V2 (Container / Section+accessory / Separator / MediaGallery)
- What it is / does — A native layout system replacing
content+embeds: aContainer(rounded box, optional accent color) holdingTextDisplay(markdown),Section(1–3 texts + one button/thumbnail accessory),Separator,MediaGallery(1–10 images). Opt-in per message viaMessageFlags.IsComponentsV2(1<<15=32768). - Key patterns & APIs —
new ContainerBuilder().setAccentColor(0x0099FF).addTextDisplayComponents(…).addSectionComponents(…).addSeparatorComponents(…).addActionRowComponents(…);SectionBuilder().setButtonAccessory(btn); send via{components:[container], flags: MessageFlags.IsComponentsV2}.ComponentType: Section=9, TextDisplay=10, MediaGallery=12, Separator=14, Container=17. - Verdict — 🟡 Adapt — the single highest-value visual upgrade for the one-evolving Dashboard: buttons sit inline next to text via Section accessories (vs today’s separate ActionRow). Adapt-not-Adopt because of the hard one-way constraint below.
- Effort · risk — M · med. ⚠️ Once a message is sent with the flag it cannot be removed, and
content/embeds/poll/stickersstop working on it; capped at 40 components / 4000 chars. Ompcord’s debounced.edit()+splitMessagefallback must branch on whether the Dashboard message is V2. - Cite —
@discordjs/builders@1.14.1/dist/index.d.ts:1548-2010;discord-api-types@0.38.48/payloads/v10/message.d.ts:437,1106-1134,1719-1733; guide https://discordjs.guide/popular-topics/display-components; ref https://docs.discord.com/developers/components/reference.
Collectors vs persistent global handler · Modals · REST limits · Intents · Sharding
- Collectors —
message.createMessageComponentCollector({componentType,filter,time,idle,max})/awaitMessageComponent({…}). Verdict 🟡 Adapt: keep Ompcord’s persistent global router (durable across restarts — matches the “keep handler alive or components render dead” hazard); layer per-message collectors on top for boundedaskViaDashboard/interview wizards (free scoping + auto-timeout). ⚠️ the 3s ACK applies to every interaction a collector sees, even filter-rejected ones. S · low. Citediscord.js@14.26.4/typings/index.d.ts:702-707,6209-6214,7141-7154; https://discordjs.guide/popular-topics/collectors. - Modals —
interaction.showModal(modal)must be the first/only response (cannot defer first); pair withawaitModalSubmit({time})since Discord doesn’t signal dismissal;custom_id≤100, ≤5 rows, 1TextInputeach,setMinLength/setMaxLength/setRequired. Verdict 🟢 Adopt — Ompcord’sask.mjsalreadyshowModal()-first; adopt explicit length/required + custom_id namespacing as house style for thecustom: chatkind. S · low. Cite@discordjs/builders@1.14.1/dist/index.d.ts:879-936,2195;discord-api-types@…/message.d.ts:1450-1483; https://discordjs.guide/legacy/interactions/modals. - Rate limits & REST —
@discordjs/restauto-queues + retries: global 50 req/s + per-route buckets keyed by major param (message create/edit on one channel ≈ 5/5s). Verdict 🟡 Adapt: Ompcord’s debounced single-embed.edit()is the correct mitigation — coalesce status changes; do not hand-roll a limiter over the built-in queue; optionally subscribe to therateLimitedevent to surface bucket pressure. S · low. Cite@discordjs/rest@2.6.1/dist/index.d.ts:161-216,406-411; https://docs.discord.com/developers/topics/rate-limits; https://github.com/discord/discord-api-docs/issues/20 (edits count toward 5/5 per channel, 50/10 global). - Intents / Partials —
intents:[GatewayIntentBits.Guilds]alone deliversinteractionCreate(slash + components + modals); addGuildMessages+MessageContent(privileged) only to read free-text chat; addPartials.Channel/Messageonly for uncached DMs/old messages. Verdict 🟢 Adopt (minimize) — skippingMessageContentkeeps Ompcord below the privileged-intent/verification threshold. S · low. Citediscord.js@14.26.4/typings/index.d.ts:7684-7694,2109-2113; https://discordjs.guide/legacy/popular-topics/intents. - Sharding —
ShardingManager('./bot.js',{totalShards:'auto'}). Verdict 🔴 Skip — required only at 2,500 guilds (~1,000/shard); a single-persona bridge won’t approach it. L · high if forced; else N/A. Citediscord.js@14.26.4/typings/index.d.ts:3568-3580,4096; https://discordjs.guide/legacy/sharding.
⚠️ Deprecation in 14.26.4:
InteractionReplyOptions.ephemeralandfetchReplyare deprecated → useflags: MessageFlags.Ephemeral/withResponse(typings/index.d.ts:7160-7186). Audit Ompcord’s reply calls.
🌐 Part 4 — External comparables
@earendil-works/pi-coding-agent 0.78.0 — the sibling fork (handoff lineage)
- What it is / does — The upstream/sibling of oh-my-pi: same
export default (pi: ExtensionAPI) => voidextension shape, so Ompcord’sindex.mjsis portable across both. Ships sub-agents/plan-mode/handoff as example extensions, not core. - Key patterns & APIs — Same event union +
registerTool/Command/Shortcut/Provider.pi.sendUserMessage(content,{deliverAs:'steer'|'followUp'})/sendMessage({…},{triggerTurn,deliverAs:'steer'|'followUp'|'nextTurn'})— queue semantics for input arriving mid-turn. Session tree:ctx.newSession({parentSession,withSession}),fork(entryId),navigateTree(targetId,{summarize}). The shippedexamples/extensions/handoff.ts/handoff: pull branch → LLM-summarize →editor()review →newSession({parentSession, withSession}); ⚠️ the originalctxis stale after a successful session replacement — use theReplacedSessionContext.permission-gate.ts:tool_call → {block,reason}(the HITL gate). - Verdict — 🟢 Adopt the handoff-lineage model as a Discord-native
/handoff: dashboard summary → prefilled modal → new child thread carrying a generated context prompt, recording the parent thread likeparentSession. 🟢 AdoptdeliverAs:'steer'|'followUp'semantics for Discord messages that arrive whileagentBusy(mirror the steer-vs-followup queue instead of ad-hoc drop/serialize). 🟡 Adapttool_callblocking → Discord Approve/Deny buttons before a risky tool runs (HITL). 🔴 SkipregisterProvider/OAuth, custom editors, widgets — terminal-only. - Effort · risk — handoff/child-thread M · med (session-tree lifecycle + stale-ctx invariant); steer/followUp queue S · low; approval buttons M · med (≤3s ACK, hold handler alive).
- Cite —
@earendil-works/pi-coding-agent@0.78.0/examples/extensions/{handoff.ts,permission-gate.ts},dist/core/extensions/types.d.ts:67-80,760-792; https://www.npmjs.com/package/@earendil-works/pi-coding-agent; Ompcord sidepi-discord-amy/index.mjs.
Comparable agent↔Discord / chat-ops bridges
- What it is / does — Real projects putting an LLM/agent behind chat, harvested for streaming cadence, thread-vs-channel, approval UX, dashboards, and long-message chunking.
- Key patterns (compact comparison).
| Project | UX model | Notable pattern | Ompcord verdict |
|---|---|---|---|
| Anthropic Claude Code Channels (official) | Always-on Claude Code over Discord; plugin + account pairing | ”message your agent anytime” — same always-on thesis | 🟢 Adopt the always-on/pairing framing · 🔴 Skip vendor lock-in |
| zebbern/claude-code-discord | Claude Code bot: chat, shell/git, branches; Docker | 30s debounce of bursty alerts → creates a thread on the alert and streams the investigation there; env allow-list | 🟢 Adopt debounce-then-thread for bursty triggers (mirrors 🚀 launch) |
| jakobdylanc/llmcord | @-mention start, reply to continue | Streamed edits, message turns green on completion, auto-splits when long; reply-chain context cap 25, oldest dropped; EDIT_DELAY_SECONDS throttle | 🟢 Adopt edit-throttle math + green-on-done · 🟡 Adapt reply-chain cap |
| openai/gpt-discord-bot | /chat opens a public thread, whole thread replayed each turn | canonical thread-per-conversation | 🟢 Adopt (Ompcord already does) |
| ZeldaFan0225/ChatGPT-Discord-Bot | /chat single vs /chat thread | slash mode-switch (one-shot vs threaded) | 🟡 Adapt onto /amy subcommands |
| Kav-K/GPTDiscord | all-in-one, threads/channels | edit/redo prior turns | 🟡 Adapt redo · 🔴 Skip feature bloat |
| Slack native AI streaming + Block Kit | chat.startStream/appendStream; Thinking Steps (Task Card/Plan blocks) | 600–800 ms update cadence sweet spot (buffer chunks, flush on timer/N chars, final full payload) | 🟢 Adopt the cadence + tool-call “task card” rendering · 🔴 Skip Slack APIs |
| HITL approval (Slack/Temporal/LangGraph) | Approve&Execute / Reject buttons | state-managed interrupt/resume, never a blocking HTTP wait; tier actions by risk | 🟢 Adopt checkpoint/resume mental model for Discord approval |
- Verdict — 🟢 Adopt: thread-per-session (already Ompcord’s design); set the Dashboard debounce explicitly to ~600–800 ms within Discord’s 5/5-per-channel + 50/10 global edit limits; green-on-complete signal; debounce-then-thread for bursty triggers; risk-tiered approval buttons with checkpoint/resume (never block the gateway handler). 🟡 Adapt: reply-chain context cap + a
⚠️ only using last Nnotice;/amymode-switch subcommand; tool-call “Task Card/Plan” rendering into the embed instead of tool-spam. 🔴 Skip: self-bot approaches (ToS risk), native Slack/Matrix APIs (wrong platform), kitchen-sink feature sets, vendor-locked plugins. - Effort · risk — cadence + green-on-done S · low; debounce-then-thread S · low; HITL approval buttons M · med-high (dead handler renders components inert; a blocking wait stalls the gateway).
- Cite — Claude Code Channels https://www.datacamp.com/tutorial/claude-code-channels; zebbern https://github.com/zebbern/claude-code-discord; llmcord https://github.com/jakobdylanc/llmcord; openai/gpt-discord-bot https://github.com/openai/gpt-discord-bot; ZeldaFan0225 https://github.com/ZeldaFan0225/ChatGPT-Discord-Bot; GPTDiscord https://github.com/Kav-K/GPTDiscord; Slack streaming https://docs.slack.dev/changelog/2025/10/7/chat-streaming/ + Thinking Steps https://slack.dev/slack-thinking-steps-ai-agents/ + cadence https://www.digitalapplied.com/blog/build-ai-powered-slack-bot-tutorial-event-subscriptions-2026; Discord rate limits https://docs.discord.com/developers/topics/rate-limits.
🏆 Prioritized “early-stage wins” (value ÷ effort)
Ranked for now. Each is grounded above; the top tier is pure value with
S·loweffort — do these first.
| # | Win | Source | Value | Effort | Why now |
|---|---|---|---|---|---|
| 1 | Honest token/cost in /amy status — ✅ implemented for streamed message_end.message.usage (input/output/cacheRead/cacheWrite/total/cost) plus streamed model/provider/api; unavailable remains explicit when absent | pi-ai · run.mjs · status.mjs | 🟢 high | S·low | Kills false precision with zero new deps; data already arrives in the stream. |
| 2 | Red “tool failed” marker — ✅ implemented via tool_execution_end.isError in handleEvent + dashboard/runtime status marker | pi-agent-core | 🟢 high | S·low | Failed tools no longer read as success; one event case preserves honest status. |
| 3 | Reconnect backoff + REST fallback w/ explicit error codes in amyd.mjs | pi-discord-remote baseline | 🟢 high | S·low | An always-on daemon needs exactly this resilience; reference impl exists. |
| 4 | Explicit ~600–800 ms edit cadence, budgeted to the 5/5-per-channel edit bucket | Slack guide · @discordjs/rest | 🟡 med | S·low | Makes the evolving embed feel live without 429s; lean on the built-in queue. |
| 5 | sanitizeText tool output + format.* for the embed (durations/counts) | pi-utils | 🟡 med | S·low | Cleans ANSI from tool stdout; drops hand-rolled formatting. |
| 6 | Deferred telemetry flush + cancel-on-shutdown (when telemetry lands) | pi-langfuse | 🟡 med | S·low | Two verbatim-copyable robustness patterns; never delays a turn. |
| 7 | Discord-native /handoff — embed summary → modal → child thread (parent-tracked), using the handoff-document.md skeleton | earendil fork · pi-agent-core | 🟢 high | M·med | Turns the session-lineage model into a flagship Ompcord feature. |
| 8 | Components V2 Dashboard — Container + Section accessories (inline buttons) | discord.js | 🟢 high | M·med | Biggest visual leap; mind the one-way flag + 40-component/4000-char caps. |
| 9 | HITL approval buttons — surface risky tool_calls as Approve/Deny (checkpoint/resume, never block the handler) | earendil · HITL patterns | 🟢 high | M·med-high | Real safety upgrade over the allow-list; highest-risk (dead handler = inert components). |
| 10 | ”Amy delegates” fan-out (DAG waves + filesystem-as-bus, per-agent embed fields) | swarm-extension | 🟡 med | L·med | Powerful but the heaviest; revisit after 1–8 land. |
🔗 Related: Handoff & Research Charter · Ompcord — Always-on Discord Bridge · Responsive Interview Embed · SSOT Dashboard · Rocket Launch · Session Continuity.