0061 — The maestro is a stateful package→reasoning→action-queue router
- Status: Accepted — supersedes ADR-0059
- Date: 2026-06-18
- Deciders: Gera, Fede
- Supersedes: ADR-0059 (the 3-rung hybrid command-ladder)
- Full design + schema + checkpoints + migration plan:
docs/planning/maestro-package-queue-handoff.md(the standard — read it for the detail this ADR summarizes). The visual spec is the maestro graph inpropflow-brand-assets/launch-video(MaestroGraph/MaestroFlow).
Context
ADR-0059 framed the maestro as a 3-rung precedence gauntlet (Safety → Resume-shortcut → Full-route) where each inbound message was bucketed into one rung via deriveEnteredRung(reason) and routed to one lane. Working through the model visually (the scenario video) with Gera exposed that the gauntlet framing is wrong in three load-bearing ways:
- Routing is per-PACKAGE, not per-message. A package is whatever the trigger hands over — a single text, a 5-min idle-debounce batch of texts, or a whole call transcript. One package can demand many ordered actions (a call: create WO → schedule → rate → relay). "One message → one lane" can't express that.
- The "rungs" were labels on one LLM decision, not three sequential gates. Resume-shortcut and Full-route were never two independent runtime checks — they were two possible answers to "what is this?". So the gauntlet over-implied a ladder where there's really one reasoning call.
- The maestro is stateful and that's the point. Its value is reasoning over its own memory (what it has done + the open-WO state, across both threads of a WO) — that's what resolves "I'm done with the toilet" to the right one of N toilet WOs, vs. blindly opening a new one.
ADR-0059's genuinely good bones — LLM proposes a closed vocabulary, a deterministic layer disposes, and the I3–I6 conversational-understanding invariants — are kept and re-homed, not discarded.
Decision
The maestro is one inbound PACKAGE → an ordered set of actions, executed by a stateful reasoning layer. The flow:
channel (SMS/call/in-app) → Clara gathers → flush trigger → PACKAGE → Pre-check (Haiku) → Reasoning brain (LLM) → ordered ACTION QUEUE → lanes → Temporal pipeline (WO actions) → write-back to memory.
- Flush triggers (how a package forms):
kickoff(Clara's explicit "I have enough" tool) ·conversation ended(hangup / text sign-off) ·idle timeout 5-min(fallback) ·urgency-expedite(a time-sensitive memory entry). Single-message is the degenerate case (a package of one). - Pre-check (Haiku) — a cheap triage emitting one of three routes: escalate (life-safety → alert PM, skip pipeline — the old "safety preempt"), reasoning (likely has actions), or hand back to Clara (soft / no-action → early break). Every pre-check outcome logs a
MaestroDecision(escalate AND hand-back); only→ reasoningdefers its log to the action queue. (The separate surface fast-path — chit-chat Clara answers without ever invoking the maestro — is the only no-log case.) Conservative: bias to reasoning when uncertain (a missed action costs more than a little latency). This is also the read/write tiering gate — not everything pays for the maestro. - Reasoning brain (LLM) reads the whole package + the maestro memory (windowed: ~last 20 messages + ~last 10–20 memory rows, NOT the whole conversation) and emits the ordered actions, each
{ lane, targetWoId, confidence, why }, from a closed action vocabulary (the ADR-0059 closed-vocabulary bone — it cannot be prompt-injected into arbitrary effects). When confidence (from self-consistency sampling, not self-report) is low it emits a clarify / fail-open action instead. - Action queue dispatches sequentially (create the WO before you schedule or rate it; later actions see earlier ones).
- Lanes: new WO · continuation/Clara · rating · scheduling · vendor/tech relay · relay-to-open-WO · clarify/fail-open. Each is just an action destination; "relay to open WO" is a lane, not a gate.
- Temporal pipeline (gather → judge → dispatch → persist) runs for WO-creating actions on the one durable workflow instance per conversation (reused across packages).
- Memory (
MaestroDecisionappend-only log +MaestroStatesnapshot, WO-keyed, shared across a WO's tenant+handyman threads) is read in and written back each cycle. It carries a structuredpriority/urgencythat drives expedite, derived from the trail (outstanding vs. addressed) — not a single mutable flag. It is a derived routing aid reconciled against the real WO store, never a source of truth.
What is removed: the 3-rung gauntlet, deriveEnteredRung + rung bucketing, resume-shortcut-as-a-gate, commandToRung. What is preserved (re-homed): safety-first precedence (→ the pre-check), the rating-hijack protection (now structural via the brain reading context+memory), I3–I6 (now requirements of the brain), and the closed-vocabulary / "LLM proposes, deterministic disposes" split.
Entity classification (per ADR-0027)
| Entity | Class | Naming | Spine trace (canonical) OR derived-from / rebuilt-by / drift-tolerance (derived) |
|---|---|---|---|
MaestroDecision |
canonical | bare name | the maestro's own decision record (born here). Keyed WO#<woId> / MAESTRO#<ULID>; spine trace via the WO's personId and the thread party's personId. Append-only. |
MaestroState |
derived | *Snapshot-class (working name MaestroState) |
from: the MaestroDecision trail for the WO · Rebuilt by: each decision append (transactional) · Drift tolerance: 0 vs. the trail; the trail's openWoSnapshot is itself a cache reconciled against the real WO store. |
Consequences
- Commits us to building the maestro memory (
MaestroDecision/MaestroStateDDB), the Haiku pre-check, the reasoning brain emitting an ordered action queue, sequential dispatch with idempotency, and self-consistency confidence — replacingrung.ts/command.ts's rung machinery. The authoritativedocs/architecture/maestro-router-architecture.md+docs/planning/maestro-flow-definition.mdget rewritten with that code (sequencing rule — docs land with their code, never ahead). The eval's maestro lens moves from gradingentered(rung) to grading the action queue; eval results persist to a DDBEVAL_RUN#row + an/admin/dev/evalstab (see the standard doc §2.6). - Easier: multi-intent packages (calls, batched texts); cross-WO disambiguation; an auditable, inspectable decision log; tiering cheap chit-chat away from the durable path.
- Harder / risks to manage: memory drift (mitigated: it's a reconciled cache, never authority); confidence calibration (mitigated: self-consistency + an eval that grades the action, not the prose); prompt-injection on a privileged path (mitigated: closed action vocabulary against resolved WO ids); latency/cost of windowed reads + sampling.
- Migration is a clean sweep, not a parallel path (repo "one source of truth" rule): the new model replaces the old in the same PRs; no
deriveEnteredRungleft behind a flag. Coordinated with the maestro code owner (session245816c7), whose in-flight branches hold the current code + ADR-0059.
Alternatives considered
- Keep the 3-rung gauntlet (ADR-0059 as-is). Rejected: can't express per-package multi-action routing, and over-implies a sequential ladder where there is one reasoning call.
- Pure LLM agent loop drives routing with tools (no deterministic layer). Rejected: an unconstrained LLM on a privileged create/close path is a prompt-injection and safety hazard. We keep the closed vocabulary + deterministic dispatch/precedence (the hybrid bone from 0059).
- Stateless maestro (re-derive intent from raw history each time). Rejected: misses dedup/resume ("done with the toilet"), and re-reading days of raw text is costlier and less reliable than a distilled memory.
- Conversation-keyed memory. Rejected: a WO spans two threads and a handyman thread spans many WOs — memory must be WO-keyed with a party→WOs index off the Person spine.