From bab94d4b1f9be272d9d5251d1825d4ad8ba98104 Mon Sep 17 00:00:00 2001 From: Lovebug MCP Date: Thu, 4 Jun 2026 01:09:14 +0000 Subject: [PATCH] =?UTF-8?q?mcp:=20project-plan=20=E2=80=94=20Petal=20Dispa?= =?UTF-8?q?tch=20v1=20=E2=80=94=20Project=20Plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Dev/petal-dispatch-v1.md | 353 +++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 Sources/Dev/petal-dispatch-v1.md diff --git a/Sources/Dev/petal-dispatch-v1.md b/Sources/Dev/petal-dispatch-v1.md new file mode 100644 index 0000000..45d6ca4 --- /dev/null +++ b/Sources/Dev/petal-dispatch-v1.md @@ -0,0 +1,353 @@ +--- +created: '2026-06-04' +path: Sources/Dev +project: petal-dispatch-v1 +tags: +- petal-dispatch +- lovebug +- claude-code +- kotlin +- android +- execution-journal +- fastapi +- postgres +- sse +- tailscale +- homelab +- dev +type: project-plan +--- + +A self-hosted Claude Desktop replacement: a native Android + web client driving a Claude Code orchestrator ("Lovebug" / "petal-dispatch") and the workers it spawns, backed by a durable execution journal. Founding problem: workers stall silently and finished work can sit unseen. This build makes that failure mode structurally visible. + +This is **v1** — the canonical artifact. A working prototype currently lives at `petal-power/petal-dispatch` on Gitea; that repo will be renamed to `petal-dispatch-demo` and a fresh `petal-dispatch` repo created for v1. The prototype's *lessons* carry forward; its code does not. + +--- + +## Constitution + +Five principles. The agent treats violations as critical and resolves design forks toward these. + +1. **DB is truth, stream is display.** Durable state is the typed event journal. The screen is a lossy view. Displayed/streamed content MUST NOT become a source of truth — every displayed item reconciles to a durable record or is marked orphaned. +2. **Client is a view over the store, never coupled to stdout.** Data flows stdout → DB → client via API, never stdout → client. This is what makes multi-client viewing work (N clients = N subscribers to one store) and makes refresh/reconnect free. +3. **Active context is visible.** The orchestrator's working-set (current task, recent worker handoffs, active mode) MUST be inspectable in the client, not trapped in the session's head. Checkable: open the client, can you see what Lovebug is operating on? +4. **Session role and lifecycle are runtime properties, never hardcoded categories.** The data model and API MUST key on `session_id` and treat role (orchestrator / worker / …) as data and lifecycle (one-shot / persistent / held-open) as observed. Nothing may assume a singleton orchestrator, an ephemeral worker, or a fixed process model. *(The anti-corner principle — governs v2/v3/v4. The schema is the place this is most likely to be violated by accident; the agent should watch for it during build.)* +5. **[Scoped: concurrency only] Completion signal derives from process state (exit), not from the worker electing to call a signal tool.** The v1 one-shot worker model makes this literally true: one prompt → one response → exit, so process exit *is* the turn boundary, fired on OS process reap (rc=0), not on a parser seeing a `result` line. *Note: the v4 long-lived/interactive worker decouples idle from exit; that case gets its turn-boundary signal (`task.idle`) when v4 is built.* + +**Constitution cross-check (run at interview close):** No violations. Principle #3's control surface is partially deferred (visibility is v1; the steering/control levers are the headline open question) — this is an acknowledged gap, not a violation. + +--- + +## Spec + +### Goal +A seamless loop — *send a request → orchestrator spawns workers / synthesis as needed → results flow back → back to me* — with full visibility and a durable record of worker activity, reachable from phone and browser. + +### Problem +Workers stall silently. The orchestrator only learns a worker is done if the worker emits the right outbound message, and only acts when it gets a turn. A finished worker can sit unseen. The whole build exists to make that structurally impossible to miss. + +### Out of scope (non-goals) +1. **No document/file download from chat.** The tool is a control surface, not a file-transfer channel. +2. **No direct plugin/connector integrations** (GitHub, Gmail, etc.). The tool talks to the orchestrator; integration work is Lovebug's job, not the UI's. +3. **App build is Android ecosystem only** (native Android) — no iOS, no cross-platform mobile framework. +4. **No multi-user input arbitration in v1.** Single user (Travis), single concurrent writer. Multi-user is deferred to v2+ and may never be built. + +### Primary user journey +1. I open a client (phone or browser) and see current work state. +2. I send an instruction to the orchestrator. +3. The orchestrator spawns a worker (or workers), which run and return. +4. Worker results land in the journal; the orchestrator acts on them and reports back. +5. I see completion/stall state without watching a terminal. + +### Requirements +- **FR-001** WHEN a worker completes or stalls, THE SYSTEM SHALL surface that state in the client without the user watching a terminal. +- **FR-002** WHEN the orchestrator or a worker emits an event, THE SYSTEM SHALL persist it as a typed record in the journal keyed by `session_id`. +- **FR-003** WHEN a client connects, THE SYSTEM SHALL render state as a view over the store (never coupled to stdout). +- **FR-004** WHEN multiple clients are connected, THE SYSTEM SHALL present consistent state to all of them (multi-client read concurrency). +- **FR-005** WHEN the user sends input from a client, THE SYSTEM SHALL route it to the orchestrator session and record it as a `USER_MESSAGE` event that all clients render (echo via store round-trip). +- **FR-006 [PROSE]** The active orchestrator context SHALL be inspectable in the client. *(Visibility is specified; the control/steering surface is an open question, not yet a requirement.)* +- **FR-007** WHEN the user sends input while a worker is currently running, THE SYSTEM SHALL queue the input client-side, render it as a visible pending state, and deliver it to the orchestrator on the next turn boundary. *(Tested behavior: the orchestrator's stdin already accepts queued input; the contract here documents the UI surfacing.)* + +### Success criteria +1. Connect from phone in ≤3 refreshes. +2. ≤5 crashes/day tolerated during initial build (first Android app, explicit grace); trending toward zero is the real target. *(Grace allowance, not a shipped bar.)* +3. Context switch costs ≤1 action (at most one tap). +4. Harness completes 3 sequential actions unattended — one instruction, then the worker→harness loop closes itself twice without user input (turn 1 spawns worker → worker returns → harness acts + spawns (action 2) → returns → harness acts (action 3)). *(Can only pass if completion-signaling actually works — strongest metric.)* +5. ≥1 genuine takeaway from reading the journal (journal is legible, not just present). +6. 1 full project completed in the tool (top-line, end-to-end). +7. Zero silent stalls over the test project — every worker that finishes or hangs becomes visible in the client. *Caveat: partially bounded by upstream Claude Code signaling behavior outside Travis's control; measured best-effort, not a hard gate.* + +### Edge cases +- **Empty state** — no active tasks: client shows an empty but live view. +- **Worker stall / hang** — surfaces as a visible stuck state (the founding problem). +- **Orphaned display** (v1.1+, optimistic render) — streamed item with no durable record after timeout → marked orphaned, doubles as a stall detector. +- **Record disagrees with stream** (v1.1+) — final record wins; this is the case that justifies the reconcile pattern. +- **Concurrent clients** — both reflect the same store; v1 has no input collision (single user). +- **User types during worker run** — input queued, pending-state shown, delivered on next turn (FR-007). + +--- + +## Plan + +### Decisions (ADR-style) + +**D-1 — Native Android client.** +- Chose: native Android (Kotlin/Compose). +- Rejected: served web-on-mobile — cheaper, natural fit for view-over-store, but caps at browser-tab capabilities. +- Driver: full UI capabilities (gestures, navigation, notifications, home-screen presence, background behavior). +- One-way door, accepted: the journal/store is client-agnostic and survives any client; the Kotlin codebase is a real commitment. *(Web client is a v1 peer, not an alternative — see D-8.)* + +**D-2 — Journal is in scope (build it, don't consume it).** +- petal-dispatch builds the execution journal (first real instance), not against an existing one. +- Consequence: this is two builds — the durable store *and* the clients that read it. Journal is Phase 0. + +**D-3 — Relational journal, not flat.** +- Typed envelope columns + JSONB payload. Task/session dimension + events table referencing it; further relationships emerge in Phase 0. +- Exact relational shape derived in Phase 0 from a real `stream-json` dump, not guessed here. + +**D-4 — `event_type` as reference table.** +- Chose: event types in a reference/lookup table; application treats them as an open set. +- Rejected: Postgres `ENUM` (rigid, `ALTER TYPE` awkward) and hardcoded app constants (redeploy to extend). +- Two-way door. Extending event types is an insert, not a migration. + +**D-5 — App → API → backend.** +- Clients talk to a backend API on herbys-dev that owns the journal + orchestrator session. Clients never touch stdout. +- Rejected: app reaching into the session directly. + +**D-6 — SSE + Postgres LISTEN/NOTIFY for store→client push.** +- Carried from design thread. Asymmetric traffic (occasional writes in, streamy reads out). +- Rejected: full WebSocket as overkill for v1. + +**D-7 — Tailscale-only reachability for v1.** +- Phone on the tailnet, API over the mesh, nothing public. +- Rejected: Cloudflare Tunnel / public exposure for v1. +- Tradeoff accepted: app works when phone's on Tailscale. Other pathways (incl. 'direct' to homelab) deferred to v2. +- *(Status: already in place — `dispatch.herbylab.dev` Traefik route is tailnet-only; no DNS or routing changes needed for v1.)* + +**D-8 — Multi-client read concurrency in v1 (Android + browser); input arbitration deferred to v2.** +- v1: multiple clients as concurrent *readers* of one store; single writer (Travis), no input arbitration; echo via `USER_MESSAGE` round-trip. +- v2+: simultaneous *input* from multiple clients → backend becomes single-writer-to-stdin, clients queue, attribution matters. +- Enforcement: **convention only in v1, no lock/queue infrastructure.** The v2 mechanism is built when concurrent input becomes real; not pre-built. *(Confirmed: multi-user input arbitration may never be built — single user is the working assumption.)* + +**D-9 — Sideload v1, self-hosted F-Droid repo as fast-follow.** +- Rejected: F-Droid in v1 (competes with shipping) and public F-Droid repo (FOSS submission ceremony, wrong fit). +- Self-hosted F-Droid (private repo behind Traefik) gives a real update channel; added once the app is stable enough to update regularly. **Gate:** first stable v1 release on phone with no daily crashes for 3 consecutive days. + +**D-10 — Render-on-final v1, optimistic render v1.1.** +- v1 paints on durable-record arrival, keyed by `message_id`. No optimistic bubble, no reconcile-in-place, no orphan-timeout machinery. +- v1.1 layers optimistic paint + reconcile onto the *same* id-keyed render path — same bubble painted earlier from the stream, reconciled to record on arrival. +- Seam: v1 renders keyed by id so 1.1 is additive, not a rewrite. The reconcile-key unknown still matters for v1 (you need the id to key on). +- Rejected: optimistic-in-v1 — most intricate client code, and feel (not stall-detection) is what it buys; deferring cuts complexity from the first Android build. + +**D-11 — One-shot worker as v1 default.** +- Chose: default worker = one prompt → one response → exit. Process exit = turn boundary = completion, on OS reap. Satisfies Principle #5 with zero added mechanism; observe-only sufficient; no `/complete` endpoint, no wrapper. +- Rejected: long-lived-by-default worker — decouples idle from exit, forcing added signaling machinery. +- Lean, not a commitment (Principle #4 protects this): the schema never assumes one-shot, so if it's wrong, change the harness, not the data model. Long-lived/interactive worker is a v4 lifecycle. + +**D-12 — Reconcile key is a petal-dispatch-issued UUID, not the Anthropic message ID.** +- Chose: PD-issued UUID as the row primary key and the client reconcile key. Stable across all event types (user, assistant, lifecycle, system) — the Anthropic `message.id` only appears on assistant message events and would leave user/lifecycle events keyless. +- **Capture Anthropic's `message.id` in the payload JSONB as a forensic cross-reference field.** Costs nothing, helps debugging when stream-json behavior changes. +- Reasoning: keeps Principle #4 (no dependency on substrate's ID shape) while preserving forensic linkage. + +**D-13 — Two-service split.** +- **Service 1: `petal-dispatch` (Python / FastAPI).** Houses the journal writer, the API (REST + SSE), and the orchestrator-host (spawns + manages claude subprocesses). Static web client served from `/static`. +- **Service 2: `petal-dispatch-android` (Kotlin / Compose).** Existing repo (extending the v0 chat scaffold), talks to Service 1 over HTTPS + SSE. +- Rejected: three services (premature decomposition for single-user); one-repo (mixed Python+Kotlin operationally painful). +- Why this shape: the principles already give you the seams to split further later (journal swappable, orchestrator swappable, clients swappable) — you don't pay the operational cost of more services until you have a reason. Clean schema (Principle #4) makes "split later" cheap, not the deployment topology. + +**D-14 — Two-step auth rollout: OPEN_MODE first, Authentik+Tailscale once Android can auth.** +- v1 backend ships with `OPEN_MODE=1` (auth bypass; trust the tailnet) so the Android client can be developed against an unauthenticated backend. +- Once the Android client implements HTTPS auth correctly, flip the backend to Authentik-fronted via Traefik. Tailscale stays as the network gate throughout. +- Explicitly a two-step rollout, not a single deploy. + +### Data model +- **Relational, not flat.** Task/session dimension + events table. +- **Typed envelope columns:** `event_id` (PD-issued UUID, primary key), `task_id`, `session_id`, `message_id` (nullable — PD-issued UUID per message-grain, reconcile key for client render), `event_type`, `created_at`, plus a monotonic `seq`. +- **`event_type`** — reference table, open set, never PG `ENUM` or hardcoded. +- **JSONB `payload`** for variable structured body. Anthropic's `message.id` (when present) lives **inside** payload as a forensic field. No display markup (HTML) stored as truth — rendering is client-side from structured data. (Exception: HTML that is a worker's actual output content is data, held as a JSONB string field.) +- **`seq` is the ordering authority** — consumers order by `seq`, never by arrival time (see prototype findings). +- **`seq` invariant:** the writer guarantees `seq` reflects causal order within a `start_task` transaction (await the spawn publish before scheduling user_message — fixes the prototype ordering bug). +- **Exact relational shape (table count, immutable-handoff vs. ambient-chat split, normalized vs. JSONB boundaries) deferred to Phase 0**, derived from observed `stream-json` structure. **Must-resolve-before-journal-writer.** + +### Interfaces / contracts +- **Outbound (store → client):** SSE stream + LISTEN/NOTIFY-driven "new record" notifications. Multiple concurrent readers. +- **Inbound (client → orchestrator):** REST action → backend → orchestrator session stdin. Backend is the single writer to stdin (v1 trivially, since single user; v2 builds enforcement when needed). +- **Echo:** user input becomes a `USER_MESSAGE` event; all clients render it from the store. +- **Client render:** keyed by `message_id` (the reconcile key) so render-on-final (v1) and optimistic render (v1.1) share a path. + +### Deployment +- **Backend** (API + journal + orchestrator session) runs on herbys-dev. +- **Reachability:** Tailscale-only for v1 (phone on tailnet); already in place. +- **Android artifact:** sideload v1 (`adb install` / direct APK transfer); self-hosted F-Droid repo behind Traefik as fast-follow. +- **Web client:** served from petal-dispatch's `/static`, same Traefik route as the API. +- **GitHub pushes:** Travis only (standing model); Lovebug works on Gitea / `petal-power`. + +### Tests +- **Rigorous on journal/backend** — it's the truth layer; bugs corrupt everything downstream. zero-check pipeline applies. **Test fixtures:** the Phase 0 stream-json dump becomes the canonical fixture set for journal-writer tests. +- **Pragmatic on clients** — they're views; the reconciliation logic (v1.1) is the one client-side piece earning real tests. + +### NFRs +- **Latency / throughput:** n/a as hard budgets for v1 (single user). Render-on-final accepts slight screen lag behind the stream until v1.1. Acceptable lag: "Travis tolerates it" — not a measured gate. +- **Threat model:** single-user personal tool, Tailscale-only, not publicly exposed in v1. Trust boundary is the tailnet. Public exposure is a deliberate v2 decision with its own threat review. +- **Observability:** the journal *is* the observability surface — the system's diagnostic surface is the same event log the tool exposes (demonstrated: three prototype bugs found by observing the live stream). + +### Phases (high-level) +- **Phase −1 — Viability spike.** Bound the substrate-dependency risk before building. +- **Phase 0 — The journal.** Irreducible foundation; everything reads from it. +- **Phase 1 — Backend API + push.** +- **Phase 2 — Web client.** (Cheaper client first, proves architecture on familiar stack.) +- **Phase 3 — Native Android client.** +- **Phase 4 — v1.1 optimistic render.** + +--- + +## Tasks + +### Phases (detailed) + +**Phase −1 — Viability spike (bounds the OAuth pre-mortem).** +Confirm production auth permits the remaining required control flow. *Already partially cleared by the prototype test:* turn-injection works, the event stream is observable, `session_id` threads through every event. Remaining: confirm which credential the running session uses, and whether any concurrency-required controls hold under it. Framed as *measuring the size and shape of the dependency*, not erasing it. +Satisfies: de-risks FR-001/002/005 before build. + +**Phase 0 — The journal.** +Dump live `stream-json` → derive event schema from observed structure → build journal (task/session dimension + events table, reference-table event types, JSONB payload, `seq` ordering) + the writer that captures session/worker events. + +**Dump methodology — required scenario set** (curated, not happy-path): +1. One-shot worker, text-only response. +2. One-shot worker that emits tool_use blocks. +3. Long-lived worker running multiple turns (forward-compat for v4 / Principle #4). +4. Worker that errors out (returns assistant error or hits an internal failure). +5. Worker externally SIGTERMed mid-turn. +6. Worker that hits rate limits or auth refresh during run. + +This breadth informs the schema; running only happy-path biases toward one-shot patterns (Principle #4 risk). + +Must-resolve-before-code: (a) reconcile-key structure — does each assistant turn emit a `message_start` with a stable `id` (captured into payload, not relied on as the key — see D-12); (b) exact relational shape. +Satisfies: FR-002. + +**Phase 1 — Backend API + push.** +REST for actions, SSE for live read, LISTEN/NOTIFY → SSE fan-out, backend owns the single write path to orchestrator stdin. + +Backend bootstraps with `OPEN_MODE=1` (D-14). Web client served from `/static`. systemd unit + secrets.env pattern carried from the demo (proven working). Traefik dynamic config snippet matches the demo's pattern. +Satisfies: FR-003, FR-005 (mechanism), FR-007 (mechanism), foundation for FR-004. + +**Phase 2 — Web client.** +Browser client, render-on-final keyed by `message_id`, Tasks/Chat/Activity surfaces. Proves store→API→client→render end-to-end on the known stack before Android. + +**Home tab in Phase 2 layout:** deferred to Travis pick before Phase 2 build (see Home tab decision section below). Current lean: Task-first. +Satisfies: FR-001, FR-003, FR-006 (visibility), FR-007 (UI surface for queued input). + +**Phase 3 — Native Android client.** +**Extend the existing `petal-dispatch-android` repo** (v0 chat scaffold built during the demo phase). Carry forward: Kotlin/Compose setup, Material 3 theme, OkHttp networking, the chat screen as the v1 "Chat" tab. + +**New surfaces to add:** +- **Tasks tab** — list of running + recent worker tasks, status indicator, tap-to-drill into transcript + send follow-up. Surfaces the founding problem (stalled workers visible). +- **Activity tab** — chronological event feed across all sources (workers + Dispatch), the forensic view. Tap an event → drill to source's transcript. +- **Chat tab** — the existing v0 chat screen, repointed from `wss://.../ws` to the v1 API + SSE. + +**Networking layer rewrite:** drop WebSocket-only path, switch to REST (POST actions) + SSE (subscribe to events). Same OkHttp transport, different protocol shape. + +**Tailscale reachability:** the app assumes the phone is on the tailnet. If the backend host is unreachable, show a clear "off-tailnet" state with reconnect logic — don't crash. + +**Auth integration:** initially trust the network (matches `OPEN_MODE=1` backend); when D-14 flips, add HTTPS auth + Authentik header. Code TODOs left in place at the relevant call sites. + +**Sideload:** debug APK to dist/ path, transfer to phone via Tailscale share / Syncthing / USB. + +Satisfies: FR-004, FR-001 (mobile), success criteria 1 & 3. + +**Phase 4 — v1.1 optimistic render.** +Optimistic paint + reconcile-in-place onto the existing id-keyed path, both clients. Orphan-timeout handling. +Satisfies: the live-streaming feel; edge cases (orphaned display, record-disagrees-with-stream). + +### Home tab decision (carried open) +Three UI prototypes share a Tasks / Chat / Activity bottom-nav; the open question is which is *home*. Task-first surfaces stalled cards structurally (fits the founding problem); Chat-first matches how Travis works; Stream-first is forensics. *Travis to pick the one he kept returning to — the Task-first mockup is the current lean.* Unblocks Phase 2 layout. + +### Human-required handoffs +Coordinated live with Lovebug during execution rather than pre-scripted. Known touch-points: the `stream-json` dump and schema sign-off (Phase 0), GitHub pushes (all phases), Android-toolchain/physical-phone steps (Phase 3), Tailscale/cert wiring, the OPEN_MODE → Authentik flip (D-14). + +### Iterate-shaped phases +- **Phase 4 (optimistic render)** and the **home-tab/UI work** are converge-with-Travis loops — done when it feels right, not when a spec criterion is met. Agent treats these as iterate, not finish-and-handoff. + +--- + +## Cross-cutting + +### Codebase assumptions +- **Relies on, does not touch:** Postgres (journal home — petalbrain instance), Traefik (fronts API + web client), Tailscale (reachability), Gitea/`petal-power` (dev), existing `claude` Code sessions and their `--output-format stream-json` behavior. +- **Spawn mechanism: observe-only.** The build observes session/worker events; it does not modify how workers spawn (Claude Code native). A launch wrapper/tool is a *conditional fallback* — only if native completion signaling proves insufficient — and is moot under the v1 one-shot worker (exit is the signal) and only relevant in the concurrent + long-lived-worker world. +- **Substrate-dependency containment (the pre-mortem mitigation):** the harness/adapter is the *sole* layer that touches Claude Code's control surface. The journal, API, and clients view the store. If the substrate changes (CLI version, auth, stream format) or is swapped, you rebuild that one adapter, not the tool. Demonstrated by the prototype test — everything observed came through `/events/stream` → broker → Postgres, never by reaching into Claude Code directly. + +### What carries forward from the demo (proven patterns, not code) +The existing `petal-dispatch` (about to be renamed `petal-dispatch-demo`) ran a working prototype with the harness, worker→harness event wiring, Android chat APK, and Authentik+Tailscale integration. The *code* is a sandbox; the *lessons* go into v1 verbatim: + +- **Architecture pattern:** Postgres LISTEN/NOTIFY → SSE fan-out works at single-user scale. Confirmed. +- **Event taxonomy starting point:** `task.spawned`, `task.message`, `task.idle`, `task.user_message`, `task.crashed` — proven on the wire. Schema may evolve in Phase 0 from the dump, but these are the kinds list to start from. +- **`X-Authentik-Username` header pattern** — what Authentik injects through Traefik. Used when D-14 flips. +- **`OPEN_MODE` env flag pattern** — the auth-bypass toggle. Keep verbatim for v1 bootstrap. +- **systemd unit + `secrets.env`** — the petalbrain DSN delivery pattern. Works as-is. +- **Tailscale + Traefik dynamic config** — wiring proven; no changes needed for v1. +- **Kotlin scaffold structure** — package layout, Material 3 theme derived from prototype CSS, OkHttp transport. Extended, not restarted. + +What does **NOT** carry forward: +- The existing `dispatch_harness.events` rows — historical only, not migrated. Phase 0 dump informs the v1 schema from scratch. +- The existing tasks.json subprocess-manager persistence format — v1 derives its own. +- The existing FastAPI route layout — v1 derives from the API contract, not the demo's evolution. + +### Repo & naming +- **Existing Gitea repo `petal-power/petal-dispatch`** → renamed to `petal-power/petal-dispatch-demo`. Stays accessible for reference and as the runtime demo while v1 is built. The systemd unit on herbydev stays pointed at it during the transition. +- **New Gitea repo `petal-power/petal-dispatch`** → v1 backend (Service 1). +- **Existing Gitea repo `petal-power/petal-dispatch-android`** → continues as v1 client (Service 2). README updated to reflect v1 contract. + +### Companion vault projects +- **execution-journal** — this build is its first real instance (the durable store). *Note: the `execution-journal` slug currently resolves to a session-notes file, not a built system — consistent with "build it here."* +- **synthesis-layer** (status: idea) — v3. Consumes worker handoffs directly as per-step summaries (its own v1); no synthesizer agent built. Read-pattern over the journal, deferred until scale forces compression. +- **operating-mode** (status: idea) — the client is the natural surface to invoke and display the active mode; ties into Principle #3's visibility and the control-surface open question. + +### Prototype findings (from the live SSE test — open as repo issues against new petal-dispatch repo) +1. **`task.spawned` can arrive after `task.user_message`** (unordered `asyncio.create_task`). Journal-correctness issue. Fix: await the spawn publish before scheduling the user_message; *and* enforce the invariant "consumers order by `seq`, writer guarantees `seq` reflects causal order within a `start_task` transaction." Baked into D-3's `seq` invariant. +2. **`task.crashed.last_error` populated with the assistant's reply on clean SIGTERM** — conflates "last output" with "error cause." Fix: split `last_error` (real fault only) from `last_output`, or detect signal-exit (143/130) explicitly. Matters because a misleading `last_error` makes the journal lie about success/failure (undercuts success criterion 5). +3. **`task.done` unreachable under the long-lived v0.5 worker** — dissolves under the v1 one-shot worker model (done fires on every worker exit = every turn); re-emerges only as a v4 question. + +### Unresolved questions +- **Must-resolve-before-code:** reconcile-key structure (Phase 0 dump confirms the Anthropic `message.id` shape captured into payload); exact journal relational shape (Phase 0); remaining production-auth control confirmation (Phase −1). +- **Can-defer-and-tag:** home-tab choice (before Phase 2 layout); the three prototype bug-fixes (repo issues against new petal-dispatch); whether the spawn mechanism ever needs a wrapper (only if concurrent). +- **Headline open question (defer, do not block foundation):** the *control* surface for context — what levers Travis gets to steer/shape the orchestrator's context. v1 has **zero control surface beyond "send a message to Dispatch."** Steering levers — operating-mode invocation, context-trim, worker-tool-allowlist — are all v2+. Visibility ships in v1; control is undefined and explored later. Must-resolve before the *control features* get built, not before the truth/view/journal foundation. +- **Out of scope (this build):** concurrency machinery (open fork); multi-Dispatch (v2); synthesis compression/drift detection (synthesis-layer's own later phases); desktop app (v2); public reachability (v2); multi-user input arbitration (v2+, may never). + +### Pre-mortem +**Failure mode (Travis's, verbatim intent):** Six weeks out, the OAuth subscription doesn't permit the control flow needed to make this the tool he wants — the architecture is sound and the tool is still dead because the substrate won't allow the control. *This is structural, not a bug:* the foundation is someone else's tool, running under consumer auth, with control surfaces owned by Anthropic and subject to change. It cannot be eliminated, only bounded — by (a) the Phase −1 spike measuring the dependency before building on it, and (b) Principle #2 / the containment design keeping the dependency in one swappable adapter. The DIY-listener path sidesteps the *Channels-specific* auth gate but does not erase the deeper dependency. + +### Secret suspicion (surfaced) +The worker-model elegance (one-shot → exit = completion) is clean, but it's explicitly a *lean*, not a tested commitment — the interactive/long-lived worker is a real behavior already observed in the prototype, so the one-shot default may not survive contact with how Travis actually wants to work. Principle #4 is the deliberate hedge against this. + +--- + +## Version stack + +- **v1** — single orchestrator + workers + journal + Android & web clients; render-on-final; one-shot worker default; lifecycle unforced; multi-client read concurrency. +- **v1.1** — optimistic render + reconcile (next thing after v1). +- **v2** — multiple concurrent Dispatch sessions; simultaneous multi-client input arbitration (if ever); desktop client; other reachability pathways. +- **v3** — synthesis layer. +- **v4** — interactive / held-open workers (routable for direct input). +- **DB must be forward-compatible with v2/v3/v4 from v1** — Principle #4 is what makes these "more of what the schema already allows," not rewrites. + +--- + +## Interview telemetry +- Date: 2026-06-03 +- Skill predicted rounds: 1–2 (architecture well-specified; Phase 0 schema and the Android phase are the likeliest follow-up points) +- Travis predicted rounds: _[pending]_ +- Go/no-go: **go** +- Gaps named: none blocking; home-tab choice and the Phase −1 remaining-auth check are the first execution-time items +- Un-EARS criteria: FR-006 (control/visibility — left prose, control surface undefined) +- Vibe answers (left un-quantified by choice): success criterion 2 (crash tolerance, explicit grace not a real bar) +- Folded answers (v1 revision): + - Q1 — relabel existing as demo, start fresh; carry-forward list in "What carries forward from the demo" + - Q2 — PD-issued UUID is reconcile key + row PK; Anthropic `message.id` captured in payload as forensic field (D-12) + - Q3 — dump scenarios approved + listed in Phase 0 detail + - Q4 — two-step rollout: OPEN_MODE first, Authentik+Tailscale once Android ready (D-14) + - Q5 — two-service split: petal-dispatch (Python) + petal-dispatch-android (Kotlin) (D-13) + - Q6 — convention only for single-writer in v1; v2 may never build it + - Q7 — extend existing Android scaffold; add Tasks + Activity tabs alongside the Chat tab (Phase 3 detail) + - Q9 — queued + pending UI state + delivered on next turn boundary; orchestrator already accepts queued input (FR-007) + - Q10 — Tailscale-only already in place, no DNS work \ No newline at end of file