๐ฌ Interviews
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 window | Defer/update fast, then edit or follow up during Discordโs 15-minute post-ACK 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: Ompcord Bridge