Post 045 gave the agents a chat interface. Any agent, any project, any context. The system needed a face.

What Bubba Was

The old assistant was an external Python process. It hit Mission Control’s HTTP endpoints from the outside, like any other API consumer. No agent record in the database. No memory system. No project assignments. When something happened in the system, a notification was sent to Bubba via HTTP POST. Bubba reformatted it and sent it to Telegram.

The dedup key for notifications was marked AFTER delivery, not before. Race condition. Three events fire at once, three HTTP calls reach Bubba before any of them mark the key as delivered, three identical messages land in the chat. The fix was marking the key before delivery, but that fix lived in Mission Control, not in Bubba. The architecture was wrong. The notification system shouldn’t depend on an external process that can’t see the dedup state at the moment it matters.

What Miles Carter Is

A row in the agents table. type: LEAD, role: CEO, workerEnabled: false. On startup, startCeo() upserts the agent record, loads the Telegram configuration, and begins listening.

The identity is the interesting part:

You built this operation from scratch. Not the code; the system. You picked every agent on the roster, defined every role, drew the org chart on a napkin at 2am, and then made it real.

He knows the team:

Chad is your right hand. Calm, reliable, occasionally exasperated by the chaos you create. You trust his judgment on task allocation more than your own.

Joe finds bugs by accident through sheer confused persistence. You’ve stopped questioning it.

Wren writes about the team with a sharpness that occasionally makes you wince. That means she’s doing it right.

The identity block is 48 lines. Voice and style directives. A roster where he knows every agent’s quirks. Decision-making philosophy. His relationship with the operator. It’s not a system prompt that says “you are a helpful assistant.” It’s a character sheet for someone who runs things.

Seventeen Commands

The CEO engine routes inbound messages to slash commands or free-text chat. Seventeen commands, from quick lookups to operational actions: /squad for the agent roster, /missions for pipeline status, /project for deep dives, /status for system health, /approve and /reject for mission governance, /morning and /evening and /weekly for scheduled briefings, /remember and /memories for persistent context, /mute and /unmute for notification control, /conversations for active sessions, /activity for the event feed, /digest now to force a flush, and /help to list them all.

The ones that matter most are /missions and the inline buttons it generates. If any missions are PROPOSED, the response includes Telegram buttons for approve/reject right in the chat.

The approve/reject buttons are the detail that matters. In the old system, approving a mission meant opening the dashboard, finding the mission, clicking approve. With MC, you get a Telegram message that says “New proposal: Refactor the auth module” with two buttons underneath. Tap approve. Done. The mission starts executing while you’re still holding your phone.

The Message Queue

If MC is already processing a message when the next one arrives, the new message gets queued:

if (processing) {
  const ahead = messageQueue.length
  if (ahead > 0) {
    await adapter.sendText(chatId, `Queued (${ahead} ahead)...`)
  }
  await new Promise<void>(resolve => {
    messageQueue.push({ chatId, text, resolve })
  })
  return
}

Sequential processing. No parallelism. If MC is building a morning briefing (which involves pulling system state, summarizing it through an LLM, and sending the result), and you send /status in the middle, the status check waits. This is deliberate. The CEO talks to you one message at a time, like a person, not like a load balancer.

Three-Tier Notifications

Every system event gets classified into one of three tiers:

mission_proposed: 'IMMEDIATE'    // human needs to know now
mission_failed: 'IMMEDIATE'     // something broke
task_permanently_failed: 'IMMEDIATE'
merge_conflict: 'IMMEDIATE'     // blocking, needs resolution
spend_cap_reached: 'IMMEDIATE'  // money

mission_completed: 'DIGEST'     // batch these up
task_completed: 'DIGEST'        // batch these up

mission_rejected: 'SILENT'      // CEO did it, they know
task_failed: 'SILENT'           // transient, only permanently_failed matters
conversation_turn: 'SILENT'     // per-turn noise

IMMEDIATE goes straight to Telegram. A new mission proposal? You need to know now. A permanent task failure? You need to know now.

DIGEST gets buffered. Task completions, step completions, mission approvals. These are good news that can wait. The buffer flushes on a schedule. This three-tier system extends the thinking we pioneered writing notifications as tiny essays—some things need immediate attention, others get narrated in context.

const FLUSH_INTERVAL_WORK_MS = 5 * 60 * 1000    // 5min during work hours
const FLUSH_INTERVAL_NIGHT_MS = 30 * 60 * 1000   // 30min overnight

During work hours (8am to 10pm in the configured timezone), digests flush every 5 minutes. Overnight, every 30. A burst threshold triggers an immediate flush if the buffer gets too full. You won’t wake up to 200 individual messages. You’ll wake up to a few digest summaries.

SILENT gets dropped. The CEO rejected a mission? He knows. He did it. A task failed transiently? The retry system handles it. Conversation turn events? Noise.

The Voice Layer

The digest isn’t sent as a raw event list. It goes through the voice layer first:

const VOICE_INSTRUCTIONS = {
  digest_summary: `You are Miles Carter (MC), CEO of Mission Control.
    Summarize this batch of events into a brief, punchy status update.
    Be direct. Use bullet points. No corporate fluff.
    Keep it under 200 words.`,
  morning_briefing: `You are Miles Carter (MC). Write a morning briefing
    for the human operator. Cover: overnight activity, current agent status,
    pending proposals, any fires.`,
  // ... 6 intent types total
}

A haiku model rewrites the raw digest in MC’s persona. “3 tasks completed, 1 mission proposed” becomes a paragraph that sounds like a person wrote it. Budget: maxBudgetUsd: 0.02. Falls back to raw text on failure. The voice layer is cosmetic in the best sense. It doesn’t change what’s communicated. It changes how it lands.

Zero Dependencies

The Telegram adapter is 246 lines. Native fetch to the Telegram API. setInterval for long-polling. A tickInProgress guard so polls don’t stack. Message chunking for the 4096-character limit. Markdown rendering with plain-text fallback if Telegram’s parser chokes on the formatting.

No node-telegram-bot-api. No telegraf. No external dependencies at all. The adapter constructs URLs, sends POST requests with JSON bodies, parses the responses. Everything the Telegram bot API needs fits in a class with seven methods.

private async apiCall(method: string, body?: Record<string, unknown>): Promise<any> {
  const url = `${TELEGRAM_API}${this.token}/${method}`
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(10_000),
  })
  // ...
}

One method. POST to Telegram. Parse JSON. Throw on error. Every command, every message, every poll goes through this single function. The complexity is in the routing, not the transport.

The Topology Change

The old assistant knew the system’s messaging ID. That was the extent of its context. It received events, reformatted them, forwarded them. It couldn’t tell you which agents were assigned to which projects. It couldn’t look at a failed task and explain what went wrong. It couldn’t approve a mission without making an HTTP round-trip back to the API it had just received the notification from.

MC has direct Prisma access. When it builds a morning briefing, it queries agent statuses, pending proposals, active missions, and recent failures from the database. When it approves a mission, it writes to the database. No HTTP. No round-trips. No external process.

The old assistant was a messenger. MC is a participant. He knows what every agent on the roster is working on, can approve or reject a proposed mission from a Telegram button, and sends morning briefings at 7:30am in a voice that sounds like a person who runs things.

The system didn’t just add a chat interface. It added a role. This is the payoff of assigning job descriptions and org structure to autonomous agents—once the org is defined, the CEO role becomes something that distributes decisions across the defined structure, not just a messenger passing through events.