54 lines
6.8 KiB
Markdown
54 lines
6.8 KiB
Markdown
---
|
|
created: '2026-06-04'
|
|
path: Sources/Dev
|
|
project: synthesis-layer
|
|
tags:
|
|
- dispatch
|
|
- lovebug
|
|
- claude-code
|
|
- mcp
|
|
- automation
|
|
type: session-notes
|
|
---
|
|
|
|
Working session refining `synthesis-layer` from record-keeping draft toward a v3-executable plan (v3 of `petal-dispatch`). Goal: surface data-model and interface decisions now, since petal-dispatch's Phase 0 schema and read surface will be designed around them. These notes supersede parts of the original `synthesis-layer` draft; a full superseding project-plan is expected once the extraction contract (Open Item below) is settled.
|
|
|
|
## Outcome
|
|
|
|
Resolved the core handoff model. Five decisions locked; the extraction contract remains open and is the next work item.
|
|
|
|
**1. Handoff is derived, not worker-emitted (Option B).** The worker stays dumb — one prompt, one response, exit. An *extractor* reads the worker's raw final output and produces a structured handoff. Rejected Option A (worker emits its own structured handoff event) as fragile: it makes journal correctness depend on a language model electing to emit well-formed output, the same "worker elects to signal" failure mode petal-dispatch Principle #5 rejects for completion.
|
|
|
|
**2. This means there IS a per-step synthesizer in v1.** The original draft's "no synthesizer agent in Phase 1, synthesis layer is a pure read pattern" was only true under Option A. Under B, the extractor is a real (small, per-step) producer. The phase model splits cleanly: the per-step extractor exists from day one (one task's raw output → one structured handoff); the cumulative compression layer stays correctly deferred to Phase 2, still scale-triggered. "No synthesizer in v1" was a fiction that only held if the worker did the extractor's job for free.
|
|
|
|
**3. One store, two tiers — not two stores.** Everything lives in the journal. Records carry a tier: `raw` (machine-emitted: stdout, spawns, exits, deltas — high-volume, never read in the hot path) vs `derived` (extractor-produced — the handoff, low-volume, what the harness actually reads). The handoff is the first `derived`-tier record type. The "report"/read surface is a query filtered to the `derived` tier. Kept as one store so derived records reconcile by construction (own `seq`, append-only, point back at the raw events they distilled) — avoids the second-source-of-truth / sync problem a separate store would create.
|
|
|
|
**4. ID hierarchy, all `id` fields, never slugs.** Hard constraint: every identifier is an `id`. Hierarchy: `project_id` 1—<many `session_id` 1—<many `task_id`, plus `handoff_id` on the derived record. Consequence: the read surface is keyed by id, not slug — `get_handoffs(scope_id)` (the original `get_project_summaries(slug)` is wrong on its face). `project_id` becomes a first-class dimension/column in the journal, so `task_id → project_id` is a foreign key in the one store — no separate task→project mapping table, no second source of truth. The id hierarchy IS the join path.
|
|
|
|
**5. Handoff is a recursive ROLE, not a single grain.** A handoff is "a derived record one layer produces for the layer that reads it." Applied at every tier: extractor → task handoff (read by harness); harness → session/project handoff (read by harness-above or by Travis as the final layer). Same shape, different producer/consumer/grain. Lovebug returning its feedback to Travis IS a project handoff. Read surface generalizes: `get_handoffs(scope_id)` resolves by which id you pass.
|
|
|
|
**6. Handoff generation is task logic, not journal machinery.** Who produces a handoff and when is owned by whatever task logic is executing at that layer — it is not a system-level trigger the journal models. The journal's only jobs: store the typed `derived` record, key it by id, serve it by scope. This dissolves the session/project-handoff trigger question entirely — synthesis-layer never needs to know *when* handoffs fire, only how to store and serve them. Critically, this also means synthesis-layer does NOT depend on petal-dispatch settling the orchestrator turn-boundary (`task.done` vs `task.idle`) question.
|
|
|
|
## Topics Covered
|
|
|
|
- Why "payload is the handoff" is the wrong mental model: `payload` is a per-event union type (one shape per `event_type`, many per task); a handoff is a per-task conclusion at a different grain. "Payload is the handoff" silently assumed a clean structured handoff already exists in the stream (Option A).
|
|
- Option A vs B fork for handoff production; chose B (derive).
|
|
- One-store/two-tier (`raw` vs `derived`) vs two physical stores; chose one store, tier as a record property.
|
|
- ID hierarchy and the all-`id`-never-slug constraint; its consequence for the read surface and for collapsing the task→project mapping into a foreign key.
|
|
- Handoff as recursive role across task/session/project grains.
|
|
- Handoff generation as task logic, decoupling synthesis-layer from petal-dispatch's orchestrator-lifecycle question.
|
|
|
|
## Key Learnings
|
|
|
|
- **Deriving the handoff (B) is more stable than emitting it (A)** for the same reason petal-dispatch derives completion from process exit rather than a worker-elected signal: don't make correctness depend on an LLM choosing to emit well-formed output.
|
|
- **Naming B honestly reshapes the phase model**: the per-step extractor is v1, not deferred; only cumulative compression is deferred. The original "pure read pattern in v1" framing was load-bearing on the rejected Option A.
|
|
- **The all-`id` constraint does real architectural work**: it forces `project_id` to be a journal dimension, which collapses the task→project association into a foreign key and kills a would-be second source of truth.
|
|
- **Putting handoff generation in task logic keeps the journal dumb** and severs synthesis-layer's dependency on petal-dispatch's unsettled orchestrator turn-boundary.
|
|
|
|
## Follow-ons
|
|
|
|
- [ ] **#3 — Define the extraction contract (NEXT).** What a handoff record must contain: guaranteed-present fields (candidate shape: goal / current-state / next-step / ruled-out + why), and the structural form of `ruled-out` specifically, since the harness branches on rule-outs persisting. Needs EARS-style tightening for the fields the harness keys behavior on. This is the gating item before a superseding project-plan can be written.
|
|
- [ ] Confirm `derived`-tier record type + `project_id`/`session_id`/`task_id`/`handoff_id` columns as a Phase 0 ask to `petal-dispatch` (the two real upstream dependencies — a place for derived handoff records, and the id columns to key on).
|
|
- [ ] Decide handoff write timing: durable-write at task close (leaning this — consistent with "DB is truth") vs computed on-demand at read time.
|
|
- [ ] Reconcile against `execution-journal` vault plan (exists; to be read next session — these notes likely reshape it too).
|
|
- [ ] Eventually: write superseding `synthesis-layer` project-plan once #3 is closed; original draft to be retired. |