Amy SSOT Embed Dashboard
Goal: Amy should expose one canonical Discord embed dashboard for state, options, feedback, context, model/provider, usage, queue, and health. Slash commands and interactive buttons should render the same state snapshot β no duplicate truth, no stale status, no Markdown pretending to be UI.
π·οΈ 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.*
Rocket launch channel pattern
The channel-level entry point is deliberately reaction-gated:
launch channel message β Amy adds π β author edits if needed β author clicks π
β Amy replies with channel dashboard embed
β dashboard embed starts a session thread
β thread run dashboard mirrors progress back to channel dashboardThis gives the launch channel a visible outside powerpanel while the real run happens inside the thread. The current v1 mode is only π = normal run. Future reactions may map to plan, debug, or review.
The outside dashboard is not a separate source of truth: it is a mirror of the same Dashboard state used in the thread.
Quick-start: Amy Rocket Launch Channels.
Core doctrine
Amy needs a single state snapshot and multiple renderers, not scattered status strings.
runtime events β StatusSnapshot β embed renderer β slash/status/live dashboard
β components renderer β buttons/selects/modalsEverything user-visible should come from StatusSnapshot:
/amy status/amy health- run dashboard embed
- responsive interview embed
- busy prompt response
- queue/cancel controls
- live proof sender output
If the value is not in the snapshot, it is not part of the dashboard truth.
StatusSnapshot contract
const status = {
daemon: {
ready: true,
bot: "Amy#0305",
pid: process.pid,
uptimeMs: Date.now() - startedAt,
version: "0.3.0",
startedAt,
lastReadyAt,
},
discord: {
guildId,
homeChannelId,
activeThreadId,
allowMode: ALLOWED.length ? "allow-list" : "everyone",
},
runtime: {
cwd: WORKDIR,
sessionRoot: SESS_ROOT,
activeSessionDir,
model: lastModel || AMY_MODEL || "default",
provider: lastProvider || inferProvider(lastModel || AMY_MODEL),
api: lastApi || "",
mode: "headless-json",
continuation: "-c when session jsonl exists",
},
run: {
busy: busy.size > 0,
activeThreadId,
promptExcerpt,
startedAt,
elapsedMs,
stepCount,
lastTools: [],
toolErrors,
childCount: children.size,
lastExitCode,
lastDoneAt,
lastError,
},
queue: {
depth: 0,
nextPromptExcerpt: "",
policy: "reject" // future: queue | ask | new-thread | reject
},
usage: {
source: "unavailable", // do not fake usage
inputTokens: null,
outputTokens: null,
cacheReadTokens: null,
cacheWriteTokens: null,
totalTokens: null,
costUsd: null,
},
};Important: model/provider/context/usage must be honest.
| Field | Source | Rule |
|---|---|---|
model | message_end.message.model when observed; otherwise AMY_MODEL env or default | Prefer the actual streamed model from omp --mode json; never guess. |
provider | message_end.message.provider when observed; otherwise inferred from model/env | Show literal provider when omp emits it; mark inference only for fallback. |
api | message_end.message.api when observed | Blank if absent. |
cwd | WORKDIR | Exact working directory. |
sessionDir | ~/.omp/amy-sessions/<threadId> | Exact per-thread continuation root. |
context mode | prior JSONL exists β -c | Show whether continuity is active. |
usage/tokens/cost | message_end.message.usage | If absent, show unavailable; never invent numbers. |
Embed layout: status as a dashboard
Use fields as the SSOT surface. Avoid giant prose. Keep every field scannable.
Title: π§ Amy Β· Status
Description: π’ Online Β· running in #amy-jun10-1631 Β· 2m14s elapsed
Color: green/yellow/red by state
Fields:
1. Gateway
2. Active session
3. Runtime context
4. Model/provider
5. Usage
6. Queue
7. Last event/error
8. Actions
Footer: version Β· pid Β· uptime Β· last readyField contract
| Field | Example value |
|---|---|
| Gateway | π’ ready as Amy#0305 Β· last ready 16:56 UTC |
| Active session | <#1514275906170912769> Β· 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 | unavailable from current omp JSONL or input=12,345 Β· output=678 Β· cacheRead=0 Β· cacheWrite=38,454 Β· total=51,477 Β· cost=$0.24 |
| Queue | depth=0 Β· policy=reject or depth=3 Β· next=/goal ... |
| Last event/error | last done code=0 or spawn failed: omp not found |
| Actions | Refresh Β· New Session Β· Cancel Β· Open Thread |
Color rules
| State | Color |
|---|---|
| online idle | green |
| online busy | blurple |
| waiting for user input | yellow |
| degraded / last error | orange |
| offline / failed | red |
Slash command design
Implemented commands:
| Command | Status | Behavior |
|---|---|---|
/amy status | β implemented | Renders full StatusSnapshot embed, not a 3-field summary. |
/amy health | β implemented | Same SSOT dashboard for diagnostics-first workflows. |
/amy new [topic] | β existing | Defer first, open thread, return thread link. |
/amy say <prompt> | β existing | Defer first, run in active/new thread, update dashboard. |
/amy cancel | β implemented | Defer first, SIGTERM active child; SIGKILL after a grace window if still active. |
/amy stop | β existing | Defer first, archive active thread, clear active state. |
Future commands:
| Command | Purpose |
|---|---|
/amy queue | Show queue depth, next prompts, and policy. |
/amy doctor | Permission and setup checks: commands scope, thread perms, embed links, message content. |
/amy version | Version, pid, uptime, service mode, cwd, model. |
ACK rule: every slash command that may touch Discord/network/process state must deferReply({ flags: MessageFlags.Ephemeral }) before doing work. status/health reply immediately because they read an in-memory snapshot only.
Interactive dashboard components
Use components as dashboard actions, not decoration.
Row 1: [Refresh] [Open Thread] [Cancel Run]| Component | Behavior |
|---|---|
| Refresh | deferUpdate() β rebuild snapshot β edit same ephemeral message. |
| Open Thread | Link button to active thread; no interaction event expected. |
| Cancel Run | deferUpdate() β SIGTERM active child β refresh dashboard snapshot. |
Future controls:
| Component | Behavior |
|---|---|
| New Session | deferUpdate() β open thread β edit dashboard. |
| Queue Policy | Select: Reject, Queue, Ask Queue/New/Cancel, New Thread. |
| Doctor | deferUpdate() β render permission/setup diagnostic page. |
Do not let a refresh button call the model. Dashboard interactions are control-plane operations only.
Options, stats, feedback model
The dashboard should make Amy feel alive without spamming the thread.
| Category | What to show | What not to fake |
|---|---|---|
| Options | queue policy, model env, cwd, active thread, allow mode | hidden config guesses |
| Stats | uptime, pid, child count, run count, queue depth, step count | token/cost if unavailable |
| Feedback | current phase, last tool, last error, last exit code | optimistic βdoneβ before final event |
| Context | session dir, -c active, prompt excerpt | full prompt if sensitive/too long |
| Provider/model | streamed message_end model/provider/api, then exact AMY_MODEL fallback | provider certainty without source |
| Usage | parsed message_end.message.usage (input/output/cache/total/cost) when present | made-up tokens or cost |
When usage is unknown, say exactly:
Usage: unavailable from current omp JSONLThat is better than false precision.
Event capture: one feed, many views
Every run should update the same snapshot:
run_start(prompt, thread, sessionDir)
tool_start(name)
tool_end(name, isError)
message_update(text)
ask_start(questions)
ask_answered(answers)
run_done(exitCode)
run_failed(error)
queue_changed(depth)
child_spawned(pid)
child_exited(pid, code)This gives:
- dashboard embed fields
- slash
/amy status - logs
- future web dashboard
- testable state transitions
Implementation status
Implemented:
status.mjs:createRuntimeState()snapshotStatus(runtime)renderStatusEmbed(snapshot)renderStatusComponents(snapshot)
amyd.mjsmaintains one runtime object:startedAt,lastReadyAt,botTag,lastError,activeRun,queue,runCount,lastExitCode,lastDoneAt.- streamed
usage,lastModel,lastProvider,lastApi, andlastToolErrorsfromomp --mode json.
/amy status//amy healthand the/ompcordaliases use the SSOT embed renderer.amystatus:refreshandamystatus:cancelcomponents ACK first and refresh the same message.- Dashboard stream events feed active-run phase, step count, last tools, and
tool_execution_end.isErrorred markers into the same runtime object. - Unit tests cover provider inference, usage honesty, real JSONL usage rendering, active-run rendering, failed-tool markers, and status components.
Next:
- Add queue policy state beyond the current
reject/single-depth hint. - Add
/amy doctorwith real permission checks. - Fold the live responsive interview proof into production
ask.mjs.
Acceptance bar
-
/amy statusshows gateway, active thread, cwd, session, model, provider status, usage status, queue, last error, uptime, pid. - Dashboard fields are generated from one snapshot object.
- Unknown usage/provider values are explicitly marked unavailable/inferred.
- Refresh edits the same ephemeral message.
- Cancel ACKs first and terminates the active child safely.
- Queue policy selector does not block ACK.
- Unit tests cover status snapshot and embed field rendering.
- Live Discord smoke proves the status embed/components work in a real thread after this implementation.