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
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:
Design
New Durable Object:
MayorDOA new DO keyed by
townId. One instance per town. Responsibilities:Message Flow (Before → After)
Before (current):
After:
MayorDO State
Key RPC Methods
configureMayor(config)sendMessage(message, model?)getMayorStatus()destroy()Wrangler Changes
{ "name": "MAYOR", "class_name": "MayorDO" }{ "tag": "v3", "new_sqlite_classes": ["MayorDO"] }Rig DO Changes
'mayor'fromSINGLETON_ROLESsendMessagetRPC mutation routes to MayorDO instead of creating beadsContainer 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.
sendMessageto townsendMessageThe container's
POST /agents/:agentId/messageendpoint 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
kilo servefor Agent Management #305) ✅Acceptance Criteria
MayorDOclass withconfigureMayor,sendMessage,getMayorStatus,destroysendMessagetRPC mutation routes to MayorDO (no bead creation)RigDO.SINGLETON_ROLES