mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-27 06:15:24 +00:00
113 lines
5.7 KiB
Markdown
113 lines
5.7 KiB
Markdown
# Fabro Run Stream Strategy
|
|
|
|
A run's history is two logs, and its public event API is one ordered stream
|
|
over both:
|
|
|
|
- **Petri's records.** The engine writes every fact about execution: the run
|
|
starting and finishing, each step's firing, progress, output and outcome, a
|
|
question asked and answered, a scope acquired. Fabro stores them unchanged
|
|
in `petri_records` through `fabro-petri`'s `SqliteRunStore`, and reads them
|
|
through Petri's event contract (`RunEvent`, with its `derived` view).
|
|
- **Platform records.** Facts Fabro knows and Petri does not: the lifecycle
|
|
before and after the engine (`run.created`, `run.lifecycle`, `run.title`,
|
|
`run.parent`, `run.archived`, `run.superseded`, `run.notice`), who answered
|
|
a question (`interview.answered`), the branch and git identity a run works
|
|
under, a checkpoint commit with its diff, a collected artifact
|
|
(`artifact.collected`), the run's diff (`run.diff`), the pull request
|
|
requests and outcomes, a notification sent, a pairing. They are
|
|
`PlatformRecord` values in
|
|
`fabro-store::platform_records`, stored in `platform_records` with a
|
|
per-run `seq`.
|
|
|
|
The **projector** (`fabro-petri::projection`) folds both logs into the run's
|
|
`RunProjection`, the view `GET /runs/{id}/state` serves, and assigns each
|
|
record it consumes a `stream_seq` in `petri_stream`. That stream is what
|
|
`GET /runs/{id}/events` and the attach stream serve, item by item, as
|
|
`RunStreamItem`: `{run_id, stream_seq, kind: petri|platform, id, recorded_at,
|
|
item}`. `stream_seq` is the cursor a client resumes from; `id` is the item's
|
|
own identity (`<log>/<seq>/<index>` for a Petri event, the record's `seq` for
|
|
a platform record) for deduplication.
|
|
|
|
Tracing logs are separate. Tracing is developer diagnostics; the stream is
|
|
the product-facing record other systems consume. If something must be
|
|
visible after a reattach, it has to be a record, not a log line.
|
|
|
|
## Recording a fact
|
|
|
|
Petri's own facts need nothing from Fabro: the engine records them and the
|
|
projector's fold reads them. Add Fabro code only for a fact Petri cannot
|
|
know.
|
|
|
|
To record such a fact:
|
|
|
|
1. Add a variant to `PlatformRecord` and its kind to `PlatformRecordKind` in
|
|
`lib/components/fabro-store/src/platform_records.rs`. The kind is the
|
|
`kind` tag on the wire, lowercase dot notation (`pull_request.created`).
|
|
A record that belongs to a stage names its `execution` and `firing`.
|
|
2. Append it through the server's `run_records` module (or the worker's
|
|
client), which commits the record and wakes the projector. Never write
|
|
`platform_records` from anywhere else.
|
|
3. Fold it in `fabro-petri::projection` when the projection should show it.
|
|
A record nobody reads from the projection still reaches the stream.
|
|
4. Update the readers that match on record kinds: the CLI's pretty stream
|
|
rendering (`petri_stream.rs`), the web app's stream handling, the Slack
|
|
service, and the tests or fixtures that name kinds.
|
|
|
|
Do not add a platform record that restates a Petri record. The projection
|
|
already carries what the engine knows; read it there.
|
|
|
|
## Reading the stream
|
|
|
|
Servers and workers hold the stream through the projector: `stream_after`
|
|
for a page, `subscribe` for live items, `stream_head` for the cursor to
|
|
start from. The server's `stream_follower` reads every run's stream once and
|
|
fans it out to the in-memory run map and the global broadcast that `/attach`
|
|
and the Slack service take their items from.
|
|
|
|
Clients read `GET /runs/{id}/events?after=<stream_seq>` for a page and the
|
|
attach stream for live items; `fabro-client` exposes `list_run_stream`,
|
|
`list_run_stream_until` and `attach_run_stream`.
|
|
|
|
When matching items:
|
|
|
|
- A Petri event's name is `item.record.body.event` (`run.started`,
|
|
`step.started`, `step.progress.recorded`, `step.finished`,
|
|
`run.finished`); its parsed meaning is under `item.derived` (a pending
|
|
question is `derived.parsed.kind == "question"`).
|
|
- A platform record's kind is `item.record.kind`.
|
|
- Petri's coordinator `run.finished` commits the overall result after required
|
|
finalization and cleanup. Its optional `finalization_failure` carries the
|
|
host-defined code and message. Invocation and stage results remain execution
|
|
evidence. A subsequent platform `run.lifecycle` terminal transition reports
|
|
that same outcome; it cannot replace it. Early worker failures without a
|
|
Petri finish end at the platform terminal lifecycle record.
|
|
|
|
Never rebuild an item downstream: pass the `RunStreamItem` through as read.
|
|
|
|
## Agent events
|
|
|
|
Pebble's `CodingAgentEvent` stream is the agent event contract. Petri stores
|
|
each event a coding agent publishes for a step as that step's progress, and
|
|
the projector folds them into `StageProjection.agent` with pebble's
|
|
`SessionProjection`. Read `StageProjection.agent`, or the stored progress
|
|
record itself, instead of folding the stream again. Fabro adds nothing of
|
|
its own to this stream.
|
|
|
|
## Ask Fabro sessions
|
|
|
|
Ask Fabro sessions are not runs. Their events (`run.session.*`) live in
|
|
their own log, `run_session_events`, through `RunSessionEventStore`, numbered
|
|
per session and served by the sessions API. They never enter a run's stream.
|
|
|
|
## Persistence guarantees
|
|
|
|
A record is committed before it is visible: the projector reads only what
|
|
the store has committed, and the stream's `stream_seq` is assigned in the
|
|
same transaction as the projection that consumed the record. A client that
|
|
resumes from its last `stream_seq` sees every item exactly once.
|
|
|
|
A worker cannot continue past a record it failed to append: the store's
|
|
error reaches the engine and interrupts the run's lifetime. The worker
|
|
records no terminal lifecycle transition for that interruption. The server
|
|
resumes the run from its durable records, up to three times per server
|
|
process; a fourth interruption fails the run.
|