Skip to content

[Gastown] PR 8: MayorDO — Town-Level Conversational Agent #338

@jrf0110

Description

@jrf0110

Parent: #204 | Phase 1 (moved up from Phase 2)

Replaces: #222 (closed — old per-rig ephemeral mayor design)

Problem

The current implementation has the Mayor as a per-rig agent that gets a new session for every user message (each message creates a bead, dispatches through the alarm cycle, starts a new kilo serve session, tears it down on completion). This diverges from the Gastown architecture spec:

  • Mayor is per-town, not per-rig. The spec defines the Mayor as a "Global coordinator" — a town-level singleton that operates across all rigs.
  • Messages should not create beads. The mayor is a conversational agent. It decides when to create beads and delegate work via tools.
  • The mayor has no tools. It currently can't sling work, create convoys, or list rigs.

Design

New Durable Object: MayorDO

A new DO keyed by townId. One instance per town. Responsibilities:

  • Owns the mayor's agent record and conversational kilo serve session
  • Routes user messages to the existing session (no bead created)
  • Provides the mayor with tools to delegate work to Rig DOs
  • Keeps the session alive while the container is running
User
  │
  ├─ sendMessage(townId, message) ──► MayorDO (keyed by townId)
  │                                       │
  │                                       ├── Persistent kilo serve session in TownContainerDO
  │                                       │     └── Mayor agent with tools:
  │                                       │           gt_sling(rigId, title, body)
  │                                       │           gt_list_rigs()
  │                                       │           gt_list_beads(rigId, status)
  │                                       │           gt_list_agents(rigId)
  │                                       │           gt_mail_send(rigId, agentId, message)
  │                                       │
  │                                       └── Calls into RigDO.slingBead(), etc.
  │
  └─ sling(rigId, title) ──────────► RigDO (keyed by rigId) ── unchanged

Message Flow (Before → After)

Before (current):

User sends message
  → getOrCreateAgent(rigId, 'mayor')
  → createBead(rigId, type='message', title=message)
  → hookBead(rigId, mayorId, beadId)
  → Rig DO alarm → schedulePendingWork → startAgentInContainer
  → New kilo serve session, sends bead as prompt
  → Agent completes → bead closed → session destroyed

After:

User sends message
  → MayorDO.sendMessage(townId, message)
  → MayorDO ensures session exists in container (creates if needed)
  → Sends follow-up message to existing kilo serve session
  → Mayor responds conversationally (no bead)
  → If mayor decides to delegate: calls gt_sling → RigDO.slingBead()

MayorDO State

type MayorConfig = {
  townId: string;
  userId: string;
  kilocodeToken?: string;
};

type MayorSession = {
  agentId: string;       // mayor agent ID in the container
  sessionId: string;     // kilo serve session ID
  status: 'idle' | 'active' | 'starting';
  lastActivityAt: string;
};

Key RPC Methods

Method Purpose
configureMayor(config) Store town config, arm alarm
sendMessage(message, model?) Send user message to mayor session (creates session if needed)
getMayorStatus() Return session status, last activity
destroy() Tear down session, cancel alarm

Wrangler Changes

  • New DO binding: { "name": "MAYOR", "class_name": "MayorDO" }
  • New migration: { "tag": "v3", "new_sqlite_classes": ["MayorDO"] }

Rig DO Changes

  • Remove 'mayor' from SINGLETON_ROLES
  • sendMessage tRPC mutation routes to MayorDO instead of creating beads

Container Session Lifecycle

The mayor session persists across messages. It starts on first user message and is reused for all subsequent messages in the same town.

Event Action
First sendMessage to town MayorDO creates session in container, sends message
Subsequent sendMessage MayorDO sends follow-up to existing session
Container destroyed/sleeps MayorDO detects via alarm, recreates on next message

The container's POST /agents/:agentId/message endpoint already supports follow-up messages — this is exactly what the mayor needs.

No Migration Needed

Nothing has been deployed. Existing per-rig mayor code is deleted. No data to migrate.

Dependencies

Acceptance Criteria

  • MayorDO class with configureMayor, sendMessage, getMayorStatus, destroy
  • Wrangler config: new DO binding + migration
  • sendMessage tRPC mutation routes to MayorDO (no bead creation)
  • Mayor session persists across messages (follow-up via existing session)
  • Mayor removed from RigDO.SINGLETON_ROLES
  • Container dispatches mayor session on first message
  • Alarm loop keeps container alive while mayor session exists
  • Mayor system prompt describes role and available tools

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions