wiki-vault/Sources/Dev/petal-dispatch-phase3-brief.md

10 KiB

created path project tags type
2026-06-05 Sources/Dev petal-dispatch-phase3-brief
petal-dispatch
lovebug
phase-3
web-client
fastapi
sse
trellis
postgres
p4
execution
project-plan

petal-dispatch — Phase 3 — Web Client — Worker Brief

Project: petal-dispatch (p4) · Thread: 588 · Repo: petal-power/petal-dispatch (Gitea) Plan ref: p4 Phase 3 · Satisfies FR-001, FR-003, FR-005, FR-006, FR-007, FR-012 Structure: Three sequential gated spawns (3a → 3b → 3c). Each lands a handoff to thread 588 before the next spawns.

Goal

A functional web client for petal-dispatch — the first real client, deliberately before Android (p4 D-1), to prove store → API → client → render, both directions, on the web stack before committing to Kotlin. The load-bearing half is the write loop (client → orchestrator → echo back through the store), because that is the path Android copies; a read-only client would not prove what Phase 3 exists to prove.

Phase 3 success bar: Travis sends a message and gets a conversational response from the orchestrator, round-trip through the store (FR-005), rendered. Orchestrator only — no worker spawn, no worker-completion dependency. Worker-task surfaces and stall detection (SC-7) are out of Phase 3; no workers exist this phase.

The existing static/index.html (Phase 2 poll-based cards) is reference-only, not a base. Mine it for markup/render logic; ship fresh. Standard worker discipline (zero-check, a-review per handoff) applies per env instructions.

Locked Decisions

Store boundary (p4 D-22) — governs the whole phase. Two stores by kind of truth. Journal = execution truth (events, handoffs). Trellis = context truth (threads, working picture). 3b is journal-only and survives Trellis-down. 3c is the only Trellis-backed surface. petal-dispatch's Phase 3 code reads Trellis; it does not write it (writes are synthesis, FR-013, Stage C — out of scope this phase).

Transport (p4 D-6, with shipped-WS drift). The Phase 3 web client targets SSE over the harness /events/stream. The app's shipped WebSocket (/api/tasks/events) is left in place untouched; Android repoints to REST+SSE in Phase 4. The existing v0 /ws chat endpoint is a separate question handled in 3a recon (see below).

Auth. OPEN_MODE=1 for v1 — no auth, tailnet-only (D-14). No login wall. New routes follow the same _check_auth + OPEN_MODE pattern as Phase 1/2: when OPEN_MODE=1, auth no-ops; otherwise the existing identity-header check applies.

Trellis read mechanism (3c). petal-dispatch gets its own dedicated DB role on petalbrain.trellis — separate from dispatch_journal, which has no business reading Trellis. The role is provisioned read/write (forward-looking: synthesis at Stage C will need write to land context keyed by thread_id), but Phase 3 code uses read (SELECT) only — FR-012/D-22 forbid petal-dispatch writing Trellis in v1. "Trellis unavailable" for the graceful-degradation test therefore means "the DB query failed" — the same failure surface as every other petal-dispatch query, wrapped in try/except. Rejected: calling trellis-mcp over its HTTP transport (adds a hard runtime dependency — trellis-mcp down would take 3c with it).

Rendering. Render-on-final, keyed by the reconcile key (PD-issued UUID, D-10/D-12) — the same id-keyed path optimistic render layers onto later (Phase 5); do not foreclose it.

Tap-to-switch is render-only this phase. No orchestrator-side "active thread" repointing is wired; switching focus changes the highlighted tile, nothing more.

Open Items

  • 3a recon, input/transport (the /ws fork). main.py already runs a WebSocket at /wshandle_ws(websocket, proc, bus), where proc is the Dispatch claude singleton — i.e. the orchestrator-only target Chat wants, already wired to the right backend. Recon must surface, not infer: does Chat (a) use the existing /ws, (b) add an HTTP POST + SSE path and coexist with / retire /ws? This brushes the p4 D-6 "SSE not WebSocket" line — reusing /ws is faster but deepens the WS drift p4 wanted gone; building POST+SSE is p4-faithful but more work. This is a Travis call, not a worker call — recon checkpoints the tradeoff before 3b's Chat transport is decided.
  • 3a recon, input-path state. Does a working client → REST → orchestrator-stdin → echo-via-USER_MESSAGE round-trip exist in v1, or was it only proven in the prototype? If unbuilt, 3a absorbs the FR-005 backend write path — surface as a scope finding at the checkpoint, do not silently expand.
  • 3a recon, reconcile-key population rule. Which event tiers/types carry a non-NULL message_id (reconcile key, D-12)? Chat-message rendering keys on it. (Fact lives in the Phase 0 schema + writer.)
  • 3a recon, harness SSE wire contract. Envelope shape, field names, event types on /events/stream, ?from_seq=N backfill semantics.
  • Synthesis-richness seam (3c). v1 renders native Trellis threads. When synthesis (Stage C) later writes a distilled working-context picture to Trellis (keyed by thread_id, migration-003 seam), the tiles should get richer with no client change. Build the render path to accommodate; do not build synthesis.

Phases

3a — Backend: event bus + recon

Recon first, then wire. Checkpoint findings before building if anything changes scope. Lovebug indicates most of 3a is already in place — recon likely confirms the input path exists and returns the contract, leaving 3a light. Verify, don't assume.

Drift paid down here: Phase 1 shipped the extractor trigger in-process (fire-and-forget asyncio from TaskManager), not via LISTEN/NOTIFY as p4 D-6 specifies. 3a gets task/handoff events onto the bus so the SSE endpoint carries them. Without this, a client wired to /events/stream receives no task events.

Recon deliverables (answer before wiring):

  • Input/transport /ws fork (see Open Items) — surfaced + checkpointed for Travis's transport call.
  • Input → stdin path state (exists / built-in-3a / scope flag).
  • Reconcile-key population rule across event types.
  • Harness SSE wire contract (envelope, field names, event types, ?from_seq backfill).

Wiring:

  • Publish task lifecycle + handoff events onto LISTEN/NOTIFY → SSE so /events/stream carries real task events.

Handoff outputs (3b depends on these):

  • Documented SSE wire contract · reconcile-key rule · input-path + /ws transport finding.

3a done = recon answered · task/handoff events on /events/stream with ?from_seq backfill proven · input→stdin path confirmed-or-built · SSE contract + reconcile rule + transport decision in handoff.

3b — Web client (fresh)

Fresh client, static/ reference-only. Served from /static, same Traefik route. OPEN_MODE=1 — no login wall.

Two surfaces, both journal-backed — zero Trellis dependency (this is why 3b survives Trellis-down):

  • Chat (success-bar surface) — source: journal round-trip. Send input → orchestrator conversational response → echo via USER_MESSAGE through the store (FR-005), rendered. Orchestrator only, no worker spawn. Input-during-run queued client-side with visible pending state (FR-007). This is what "done" is measured on. Transport per 3a's decided fork.
  • Activity — source: journal event feed via SSE. Chronological event stream.

Rendering:

  • Render-on-final, keyed by reconcile key (D-10/D-12) — same path optimistic render layers onto later (Phase 5).
  • Consumes 3a's SSE contract over /events/stream. Survives reconnect (re-subscribe, re-render from store).
  • Layout: logical, function over form. Design iteration happens later with Lovebug — iterate-shaped surface (p4), not a spec gate. No home-tab/default-tab lock.

3b done = message sent → orchestrator conversational response → round-trip through store (FR-005), rendered, orchestrator only no worker · Activity renders from SSE, survives reconnect · layout logical.

3c — Trellis threads/context surface

Completes FR-012 (the backend read surface p4 assigned to Phase 1, which under-delivered) and renders it. The only Trellis-backed surface in Phase 3.

  • Backend: read-only /api/trellis/... over Trellis thread/context state, via the dedicated DB role on petalbrain.trellis (provisioned read/write, code uses SELECT only — see Locked Decisions). petal-dispatch reads Trellis; does not write it this phase.
  • Frontend: thread/context tiles rendering native Trellis state — thread list, live thread highlighted, recent events, tap-to-switch-focus (render-only, SC-3 context switch ≤1 action). Lovebug's mockups are the starting point. Iterate-shaped.
  • Synthesis seam (accommodate, do not build): render path built so Stage-C synthesized context drops in without a rewrite.
  • Graceful degradation (required test): Trellis DB query fails → tiles show unavailable state; journal-backed surfaces (Chat, Activity) unaffected (p4 edge case).

3c done = tiles render native Trellis threads (list, live thread, recent events, tap-to-switch render-only) · degrade gracefully when Trellis-role query fails · journal surfaces unaffected by Trellis outage.

Notes

Drift touched by Phase 3 (existing 7 items in checkpoint 1088 untouched): in-process-trigger → bus, paid down in 3a; FR-012 backend, completed in 3c (Phase-1 execution under-delivery, not Phase-3 scope creep); the repo-rename-not-done and shipped-WS items remain documented divergences, not corrected here.

Why 3b is journal-only. Splitting Chat+Activity (journal) from the Trellis surface (3c) makes the journal/Trellis boundary map one-to-one onto the 3b/3c split. 3b cannot break when Trellis is down because it never touches Trellis — the graceful-degradation edge case becomes near-free.

Gated structure. 3a recon → checkpoint (Travis decides Chat transport on the /ws fork) → 3a wires → 3b builds against the decided contract → 3c. Sequential spawns, handoff to 588 between each.