-
Notifications
You must be signed in to change notification settings - Fork 7
Description
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'fromSINGLETON_ROLES sendMessagetRPC 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
- PR 5.5: Container with kilo serve ([Gastown] PR 5.5: Container — Adopt
kilo servefor Agent Management #305) ✅ - PR 7: tRPC Routes ([Gastown] PR 6: tRPC Routes — Town & Rig Management #268) ✅
- PR 7.5: Agent Lifecycle Fixes ([Gastown] PR 7.5: Agent Lifecycle & Container Reliability Fixes #335) — in progress
Acceptance Criteria
-
MayorDOclass withconfigureMayor,sendMessage,getMayorStatus,destroy - Wrangler config: new DO binding + migration
-
sendMessagetRPC 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