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 runtime ompcordd.mjs/ompcordd.service, and compatibility for amyd.mjs, /amy, /pi-discord-remote, legacy config, and ~/.omp/amy-sessions/. Repo directory stays pi-discord-amy until 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 dashboard

This 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/modals

Everything 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.

FieldSourceRule
modelmessage_end.message.model when observed; otherwise AMY_MODEL env or defaultPrefer the actual streamed model from omp --mode json; never guess.
providermessage_end.message.provider when observed; otherwise inferred from model/envShow literal provider when omp emits it; mark inference only for fallback.
apimessage_end.message.api when observedBlank if absent.
cwdWORKDIRExact working directory.
sessionDir~/.omp/amy-sessions/<threadId>Exact per-thread continuation root.
context modeprior JSONL exists β‡’ -cShow whether continuity is active.
usage/tokens/costmessage_end.message.usageIf 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 ready

Field contract

FieldExample value
Gateway🟒 ready as Amy#0305 · last ready 16:56 UTC
Active session<#1514275906170912769> Β· busy Β· 1 child Β· step 12
Runtime contextcwd=/home/usr Β· mode=headless-json Β· continuation=-c
Model/providermodel=claude-opus-4-8 Β· provider=anthropic Β· api=anthropic-messages
Usageunavailable from current omp JSONL or input=12,345 Β· output=678 Β· cacheRead=0 Β· cacheWrite=38,454 Β· total=51,477 Β· cost=$0.24
Queuedepth=0 Β· policy=reject or depth=3 Β· next=/goal ...
Last event/errorlast done code=0 or spawn failed: omp not found
ActionsRefresh Β· New Session Β· Cancel Β· Open Thread

Color rules

StateColor
online idlegreen
online busyblurple
waiting for user inputyellow
degraded / last errororange
offline / failedred

Slash command design

Implemented commands:

CommandStatusBehavior
/amy statusβœ… implementedRenders full StatusSnapshot embed, not a 3-field summary.
/amy healthβœ… implementedSame SSOT dashboard for diagnostics-first workflows.
/amy new [topic]βœ… existingDefer first, open thread, return thread link.
/amy say <prompt>βœ… existingDefer first, run in active/new thread, update dashboard.
/amy cancelβœ… implementedDefer first, SIGTERM active child; SIGKILL after a grace window if still active.
/amy stopβœ… existingDefer first, archive active thread, clear active state.

Future commands:

CommandPurpose
/amy queueShow queue depth, next prompts, and policy.
/amy doctorPermission and setup checks: commands scope, thread perms, embed links, message content.
/amy versionVersion, 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]
ComponentBehavior
RefreshdeferUpdate() β†’ rebuild snapshot β†’ edit same ephemeral message.
Open ThreadLink button to active thread; no interaction event expected.
Cancel RundeferUpdate() β†’ SIGTERM active child β†’ refresh dashboard snapshot.

Future controls:

ComponentBehavior
New SessiondeferUpdate() β†’ open thread β†’ edit dashboard.
Queue PolicySelect: Reject, Queue, Ask Queue/New/Cancel, New Thread.
DoctordeferUpdate() β†’ 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.

CategoryWhat to showWhat not to fake
Optionsqueue policy, model env, cwd, active thread, allow modehidden config guesses
Statsuptime, pid, child count, run count, queue depth, step counttoken/cost if unavailable
Feedbackcurrent phase, last tool, last error, last exit codeoptimistic β€œdone” before final event
Contextsession dir, -c active, prompt excerptfull prompt if sensitive/too long
Provider/modelstreamed message_end model/provider/api, then exact AMY_MODEL fallbackprovider certainty without source
Usageparsed message_end.message.usage (input/output/cache/total/cost) when presentmade-up tokens or cost

When usage is unknown, say exactly:

Usage: unavailable from current omp JSONL

That 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:

  1. status.mjs:
    • createRuntimeState()
    • snapshotStatus(runtime)
    • renderStatusEmbed(snapshot)
    • renderStatusComponents(snapshot)
  2. amyd.mjs maintains one runtime object:
    • startedAt, lastReadyAt, botTag, lastError, activeRun, queue, runCount, lastExitCode, lastDoneAt.
    • streamed usage, lastModel, lastProvider, lastApi, and lastToolErrors from omp --mode json.
  3. /amy status//amy health and the /ompcord aliases use the SSOT embed renderer.
  4. amystatus:refresh and amystatus:cancel components ACK first and refresh the same message.
  5. Dashboard stream events feed active-run phase, step count, last tools, and tool_execution_end.isError red markers into the same runtime object.
  6. Unit tests cover provider inference, usage honesty, real JSONL usage rendering, active-run rendering, failed-tool markers, and status components.

Next:

  1. Add queue policy state beyond the current reject/single-depth hint.
  2. Add /amy doctor with real permission checks.
  3. Fold the live responsive interview proof into production ask.mjs.

Acceptance bar

  • /amy status shows 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.