5.4 KiB
5.4 KiB
| created | path | project | tags | type | |||||
|---|---|---|---|---|---|---|---|---|---|
| 2026-06-09 | Sources/Dev | petal-dispatch-worker |
|
session-notes |
Outcome
Built, deployed, and root-cause-debugged the worker→conductor watcher daemon (petal-dispatch-worker plan) end to end in one session. The daemon is implemented, tested (22 watcher tests + full suite green, zero-check 5/5), deployed via systemd, and verified working live after a root-cause fix. The branch watcher-wake-path carries three commits (03a180d daemon + provenance, eeb0d62 diagnostic instrumentation, d521c9f the terminal-kinds fix) off main, awaiting Travis's merge + a petal-dispatch-watcher restart to persist, plus a separate petal-dispatch restart to activate the conductor instruction flip.
The headline correction this note exists to record: the spec's D-2 names the wrong terminal events. See Key Learnings.
Topics Covered
- Watcher daemon build: asyncpg LISTEN/NOTIFY on
dispatch_event, fsync-stable seq cursor, sliding-window rate limiter, coalescing nap, httpx POST to/api/chatwith anX-Watcher-Tokenshared secret. Compose service + Dockerfile + systemd unit (systemd chosen — matches howpetal-dispatchitself runs). - Live deploy steps: migration
004_watcher_event_type.sql(idempotent INSERT intopetaldispatch.event_type), shared secret in~/.config/petal-dispatch/secrets.env(read by both services), dispatch restart, watcher systemd enable. - Cold-start backfill bug: a fresh watcher with no cursor file seeds from seq 0 and replays the entire history as one coalesced wake. Harmless under the idempotent-pointer design (D-5) but noisy; should seed cursor to current-max on cold start. Not yet fixed — deferred.
- Live failure diagnosis driven to root cause through the build code session (orchestrator is sandboxed off the herbydev host, so all
docker exec/journalctl/psqlhost work routes through an on-host code session).
Key Learnings
task.idleis the real worker terminal signal, NOTtask.done/task.crashed. The spec (petal-dispatch-workerplan, D-2) and the original build brief both namedtask.done/task.crashed. That matched design language but not operational reality. Workers run asclaude --print --input-format stream-json --resume <uuid>— in stream-json input mode the CLI does not exit after a turn, it stays alive on stdin. So the subprocess-exit codepath intasks.py:_emit_terminal_event(:544-575) that firestask.done/task.crashedalmost never triggers. Ground truth: across the system's entire history,dispatch_harness.eventsholds 36task.crashed, 18task.idle, 2task.deleted, and 0task.done. The fix (d521c9f) expandsTERMINAL_KINDStotask.done, task.crashed, task.stopped, task.deleted, task.idle(excludingtask.paused, which is reversible). Trust this note over D-2 in the frozen plan body.- Worker terminal events live in
dispatch_harness.events, notpetaldispatch.journal. Two independentBIGSERIALsequences in two different tables. The watcher's cursor value (e.g. 2208, 2810) is adispatch_harness.events.seq, unrelated to journal seq. A query ofpetaldispatch.journal WHERE seq > Nfor worker terminals will always return zero rows — wrong table.petaldispatch.journalregisters onlytask.spawned/task.exitamong task events. - The single NOTIFY channel is
dispatch_event, fired by an AFTER-INSERT trigger ondispatch_harness.events(db/migrations/001_dispatch_harness.sql:32-42). Publish path:Worker._emit_terminal_event→_make_publisher→broker.publish→harness.py:104-115INSERT. The watcher's channel/table were correct all along; only the kind filter was wrong. The LISTEN socket was healthy (notifies_received=17during the synthetic test) — the bug looked like a dead socket but was a too-narrow filter silently dropping every real event. - Semantic shift from the fix: the wake now means "worker hit a turn boundary / has output," not strictly "worker exited." For the one-shot worker pattern (spawn → one turn → idle) that equals "done" and is exactly what the conductor needs. But for long-lived/interactive workers that idle repeatedly mid-task (p4's deferred v4 stage), the wake would fire on each idle — revisit then.
- Live testing earns its keep. Unit tests mocked the event source, so a wrong-kind filter sailed through 22 green tests. Only a real worker on the real bus exposed it. Same lesson as the turn-boundary harness work earlier in the session: verify against ground truth, don't trust the claim.
Follow-ons
- Merge
watcher-wake-pathtomain(git merge --no-ff …) andsudo systemctl restart petal-dispatch-watcherto persist the fix (currently running in-memory only). sudo systemctl restart petal-dispatchto activate the conductor instruction flip (release-turn-after-spawn) — the last unverified acceptance criterion (a).- Re-run the spawn-a-worker live test after both restarts; confirm the full loop (spawn → release turn → watcher wakes on idle → conductor sweeps unprompted).
- Fix the cold-start cursor seeding: on missing cursor file, seed to current
MAX(seq)instead of 0, so a fresh deploy doesn't replay history. - Reap orphan worker process PID 1088451 (stream-json worker alive since the prior day) — Travis-gated.
- Consider folding the corrected terminal-kinds reality into a p5 revision of the plan, since the project-plan body is frozen.