🧠⚡ Discord Responsive Interview Embed — idiot-proof genius cheat sheet#
Goal: one Discord message becomes a tiny wizard: user clicks/buttons/types → bot ACKs instantly → message evolves in place → Amy gets clean structured answers → no dead air, no spam, no broken interactions.
🏷️ Naming — Ompcord vs Amy. Ompcord is the omp⇄Discord bridge (product/plugin); Amy is the bot persona it runs. You install Ompcord; Amy is who answers. Rename phases 1–5 are complete at docs/command/runtime/package level: package name
ompcord, wrapper runtimeompcordd.mjs/ompcordd.service, and compatibility foramyd.mjs,/amy,/pi-discord-remote, legacy config, and~/.omp/amy-sessions/. Repo directory stayspi-discord-amyuntil final filesystem cutover. See the rename plan.*
Default choice: use a normal embed + legacy message components. Use Components V2 only when you need layout primitives (Container, Section, Text Display) and accept that normal content/embeds are disabled for that message.
For the shared status/options surface behind these flows, see Amy SSOT Embed Dashboard.
✅ Live-send recipe that finally worked#
The success pattern is not “describe an embed in chat.” It is: launch a short-lived Discord gateway process that posts a real message with embeds + components into the active Amy thread and keeps running long enough to handle component interactions.
Observed live proof (2026-06-10):
| Item | Value |
|---|---|
| Thread | 1514275906170912769 |
| Message | 1514313332268597429 |
| Script | ~/pi-discord-amy/live-responsive-interview.mjs |
| Behavior | Real embed fields + select + multi-select + pagination buttons + busy buttons + Custom modal + Confirm/Cancel |
Exact launch shape:
cd ~/pi-discord-amy
node live-responsive-interview.mjs <active-thread-id>The script reads DISCORD_TOKEN / ALLOWED_USER_IDS from /home/usr/.config/amy/amyd.env unless env vars are already set. It prints only message/thread/session ids — never the token.
What made it succeed:
- Use the active Amy thread id from
amyd.logor the current Discord URL. - Send an actual Discord API payload:
channel.send({ embeds: [embed], components }). - Keep a gateway client alive for the interaction TTL; otherwise the message renders but buttons/selects are dead.
- Route every component by
customIdprefix (liveiv:<sessionId>:...) so this proof cannot steal normal Amy interactions. - ACK first:
showModal()is the first response forCustom….deferUpdate()is the first response for select/buttons/modal submit edits.
- Edit the same message after each answer:
liveMessage.edit({ embeds: [embed()], components: components() }). - Disable components on done/cancel/timeout so stale buttons do not remain clickable.
Minimal “do this again” checklist:
- Confirm Amy thread id.
- Run
node --check live-responsive-interview.mjs. - Launch
node live-responsive-interview.mjs <threadId>. - Verify console prints
sent message=<id> thread=<id> session=<id>. - Click a select/button in Discord.
- Confirm the same message edits in place.
- Use
Confirm PathorCancelto disable controls.
🚨 Non-negotiable laws#
| Law | Why it matters |
|---|---|
| ⚡ ACK every interaction within 3s | Miss it and Discord invalidates the token; user sees “application did not respond”. |
| 🕒 Token lives 15 min after ACK | Defer/update fast, then edit/follow up during the 15-min window. |
| 🧠 State lives server-side | custom_id is max 100 chars; store only a lookup key in Discord. |
| 🧵 One evolving message | Interview/dashboard edits in place; final answer can be separate. |
| 🔐 Allow-list every click | Gate chat, slash, buttons, selects, and modal submits. |
| 🛑 Never run agent work before ACK | deferUpdate() / deferReply() first, expensive work second. |
// ✅ correct: instant ACK, then work
await interaction.deferUpdate();
await saveAnswer(interaction);
await interaction.message.edit(renderNextQuestion(state));
// ❌ wrong: Discord token dies while the agent/tool runs
await runAgentFor30Seconds();
await interaction.update(renderNextQuestion(state));🧩 Best UX pattern#
idle → asking → answered → confirming → complete
↘ timeout / cancelled / failedRender each question as:
🧠 Interview · Q2/7
What kind of result do you want?
Current answer: —
Progress: 2 / 7
Why this matters: Chooses implementation depth.
[Quick] [Balanced] [Deep] [Custom…]
[Back] [Skip] [Cancel]Use the right input:
| Need | Best component |
|---|---|
| 2–4 single-choice options | Buttons |
| 5–25 options | String select |
| Multi-choice | String select with max_values > 1 + Submit |
| Freeform text | Modal opened by Custom… |
| Final commit | Confirm / Edit / Cancel buttons |
🧱 Minimal state contract#
const session = {
id: "short-random-id",
threadId,
messageId,
userId,
index: 0,
status: "asking", // asking | confirming | complete | cancelled | timed_out | failed
answers: {},
startedAt: Date.now(),
updatedAt: Date.now(),
};custom_id should be tiny and routable:
iv:<sessionId>:pick:<questionId>:<optionId>
iv:<sessionId>:custom:<questionId>
iv:<sessionId>:back
iv:<sessionId>:skip
iv:<sessionId>:cancelDo not put full JSON, prompts, secrets, or long labels inside custom_id.
🎛️ Button style rules#
| Style | Use for |
|---|---|
Primary | One recommended/default path only |
Secondary | Neutral alternatives (Back, Custom…) |
Success | Submit, Confirm, complete action |
Danger | Cancel, destructive/stop action |
Link | External URL only; no interaction event |
Copy rule: button labels should be outcomes, not vague commands.
✅ Choose Deep, Custom answer, Confirm plan
❌ OK, Yes, Option 1, Next maybe
📏 Hard Discord limits to memorize#
| Limit | Value |
|---|---|
| Initial interaction ACK | ≤ 3 seconds |
| Interaction follow-up/edit token | 15 minutes |
| Message content | 2000 chars; split at ~1900 |
| Buttons per action row | ≤ 5 |
| Rows per classic component message | ≤ 5 |
| Select options | ≤ 25 |
| Button label | ≤ 80 chars; keep ~34–38 visible chars |
| Select label/value/description | ≤ 100 chars each |
| Select placeholder | ≤ 150 chars |
custom_id | 1–100 chars |
| Modal title | ≤ 45 chars |
| Modal components | 1–5 |
| Components V2 total components | ≤ 40 |
| Embed description | ≤ 4096 chars |
| Embed field value | ≤ 1024 chars |
🧠 Amy/omp integration#
Current Amy pieces:
| File | Role |
|---|---|
~/pi-discord-amy/amyd.mjs / ompcordd.mjs | Discord daemon/runtime wrapper; thread chat + /amy//ompcord; spawns omp -p --mode json. |
run.mjs | JSONL stream parser + driveRun() ask loop. |
ask.mjs | Discord picker/modal implementation; returns answers[]. |
dashboard.mjs | One evolving embed per agent run. |
slash.mjs | /amy + /ompcord status/new/say/cancel/stop; mutating commands defer first. |
Headless ask bridge:
Agent needs input
→ emits ```amy-ask JSON block
→ run.mjs extracts questions
→ ask.mjs renders Discord components
→ user answers
→ answersToPrompt() feeds continuation with -c
→ agent continuesStable answer kinds:
option · multi · custom · chatKeep that contract stable so existing agent prompts/tools do not break.
🏆 Perfect interview behavior checklist#
- Posts first embed immediately: “Q1/N — choose one.”
- Every click/select/modal submit is allow-listed.
- Every interaction ACKs before storage, network, agent, or tool work.
- Selected answer appears instantly in the same message.
- Back/Edit works before final confirm.
- Timeout disables stale components and explains how to resume.
- Cancel disables components and marks state cancelled.
- Partial answers are preserved on timeout/error.
- Final confirm emits compact structured answers into the continuation prompt.
- Long final prose is chunked safely; only last chunk mentions the user.
- Errors are visible: no silent REST failures, no dead buttons.
🧯 Failure fixes#
| Symptom | Root cause → fix |
|---|---|
| “Application did not respond” | Missed 3s ACK or daemon down → deferReply() / deferUpdate() first; verify process online. |
| Button does nothing | Missing interactionCreate handler, wrong custom_id, stale message, or allow-list denied. |
| Modal never opens | showModal() was not the first response to that interaction. |
| Slash command invisible | Bot missing applications.commands scope or commands not registered to guild. |
| Cannot create session thread | Missing Create Public Threads / Send in Threads / View / Send permissions. |
| Select menu rejects options | More than 25 options or label/value/description >100 chars. |
| Components V2 message lost embed/content | Expected: V2 disables normal content and embeds; use Text Display/Container instead. |
| User clicks old question | Check session id + status; reply ephemeral “This interview expired.” |
🧪 Test matrix#
| Test | Expected proof |
|---|---|
| Button choice | ACK <3s, embed edits to next question. |
| Select + Submit | Multi answers saved in order; Submit disabled until selection. |
| Custom modal | Modal opens immediately; submit updates original message. |
| Back/Edit | Previous answer reloads and can be replaced. |
| Cancel | Components disabled, state cancelled, no agent continuation. |
| Timeout | Components disabled, partial answers retained, resume path visible. |
| Denied user | Ephemeral allow-list warning; state unchanged. |
| Agent continuation | [interactive answers] continuation prompt is generated; no amy-ask block leaks to user. |
🧬 Golden implementation skeleton#
client.on("interactionCreate", async (i) => {
if (!isInterviewInteraction(i)) return;
if (!isAllowed(i.user.id)) return i.reply({ content: "⛔ Not allowed.", ephemeral: true });
const action = parseCustomId(i.customId);
const state = sessions.get(action.sessionId);
if (!state || state.status !== "asking") {
return i.reply({ content: "⌛ This interview expired.", ephemeral: true });
}
if (action.type === "custom") {
return i.showModal(buildCustomAnswerModal(state, action.questionId));
}
await i.deferUpdate();
applyAction(state, action, i);
await i.message.edit(renderInterview(state));
});🔗 Source truth#
- Discord component reference: https://discord.com/developers/docs/components/reference
- Discord message components guide: https://discord.com/developers/docs/components/using-message-components
- Discord modal components guide: https://discord.com/developers/docs/components/using-modal-components
- Discord interaction timing: https://discord.com/developers/docs/interactions/receiving-and-responding
- Amy daemon source:
~/pi-discord-amy/ - Ompcord bridge doc: /dev/docs/agents/ompcord/