`lib/components/fabro-pebble-sandbox` moved into pebble as
`pebble_coding_agent::sandbox_driver` (lithoscomputer/pebble#27): the
`Environment` over a driver handle (`SandboxEnvironment`, was
`PebbleSandbox`), the `SandboxExec` policy, the port routes, and
`display_for_log`. The pebble pin moves to that branch head, 6d03b3b,
with the `sandbox-driver` feature on (`sandbox-driver-test-util` for the
server's tests, which take `MockSandbox` from pebble now). Nothing in the
crate was Fabro's by design; what was Fabro's stays: `SecretRedactor`
moves to `fabro-redact` as pebble's `Redactor` over `redact_string`, and
the log renderer takes it where a driver failure is rendered.
`fabro-petri` hands pebble types to Petri's crates, so Petri must pin the
same pebble revision: the petri pins move to lithoscomputer/petri#36
(9ee3f85), which pins pebble at the same head. Both re-pin to the pebble
merge commit together once #27 merges.
The 14 pebble commits between the pins fold the session projection's
lifetime tallies into `SessionProjection::totals` (and `PromptDelta`'s
into a flattened `totals`, which renames the prompt's `subagents` key to
`subagent_counts`, as the projection's already was). The stage progress
fold, the runs handler, the OpenAPI schema, the generated client model,
and the round-trip test follow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker's environment carried only the host, Docker and Daytona plugin
variables from the server's own environment, so a run on a third-party
provider kind never learned where its plugin was, although the server
kept `[server.sandbox.providers.<kind>]` plugin settings for its own
attach. The launch spec now derives `PETRI_SANDBOX_<KIND>_PLUGIN` and
`PETRI_SANDBOX_<KIND>_SHA256` from every enabled kind's plugin settings,
and `PETRI_SANDBOX_PLUGIN_DEV=1` when any of them sets `dev`, set after
the allowlist so the settings win over an ambient variable of the same
name and the allowlist stays the fallback.
The server's default plugin binary name is `sandbox-driver-<kind>`, the
name Petri looks up, since one executable serves both sides.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rest of the change whose deletions the previous commit carries (its
`git add` stopped at an already-removed path): `fabro_types::RunGraph`
and the `fabro-petri` builder that reads it off the admitted graph, the
server's create, validate, preflight and render paths on Petri's check
alone, the consumers moved to the new shape, the OpenAPI `RunGraph`
schemas with their parity tests, the regenerated TS client, and the
docs naming Petri's diagnostic codes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /runs/{id}/checkpoint duplicated what /state serves; the hidden
fabro parse command had no user; records, run_status, outcome, and
usage_rollup in fabro-workflow only re-exported fabro_types. The
importers now name fabro_types directly. format_cost keeps its two
callers (the pull request body and the CLI stage display) and moves to
fabro_types::usage; the usage rollup tests move beside the function in
fabro-types, with test_usage in its test support.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Artifacts reach the blob table through the hooks, so the POST on
/runs/{id}/stages/{stageId}/artifacts, its octet-stream and multipart
handlers, the RequireStageArtifact extractor, the client's upload
functions, and the generated TypeScript operation go. The spec loses
the operation, the multipart variant writeRunBlob never served, and
the batch manifest schemas; fabro-types loses ArtifactUpload, the
batch upload's only input type. Every list and download path stays,
and the server tests seed the artifact store directly to cover them.
fabro-server drops multer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker control bus was publish-only: the steer and interrupt
endpoints answered 202 once the control was forwarded, and a refusal
showed up only later as a `run.notice` on the run's stream.
A steer or an interrupt now carries a request id. The worker answers it
over the control stream it arrived on with `{request_id, outcome}`,
where the outcome is `delivered` (with the stage's label) or `refused`
(with the code and the reason). The server keeps the outstanding
requests in a registry and waits up to 5 s for the answer: the endpoint
answers 202 `{"outcome":"delivered","stage":…}`, 409 with the refusal's
code (`no_live_turn`, `no_such_stage`, `steer_refused`,
`interrupt_refused`) and message, or 202 `{"outcome":"pending"}` when
the worker gave no answer in time. The `run.notice` record on refusal
stays, under the same code, so a steer to a stage that is not running is
now `no_such_stage` there too. Pause and unpause are unchanged.
The in-process test path answers a steer or an interrupt from the run's
own controls at once. `FABRO_TEST_CONTROL_ACKS_MUTED=1` on the server
mutes the worker's answers, so a test can see the pending fallback.
`fabro steer` prints the worker's answer, and a refusal is its error.
The OpenAPI spec documents the 202 body and the 409 codes; the Rust and
TypeScript clients are regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro timeline <run>` prints the checkpoint table (or JSON); `fabro fork
<run> [target]` and `fabro rewind <run> <target>` make and start the new
run and name it, `--list` showing the timeline instead; `fabro retry
<run>` starts the retry and prints its id. The top-level help snapshot
and the generated CLI reference follow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`GET /runs/{id}/timeline` lists the run's checkpoints with their Petri
positions, stages, commits and diff summaries, and its fork origin.
`POST /runs/{id}/fork` resolves a target on that timeline, creates the
new run, seeds it through `fabro_petri::fork` and queues it in resume
mode, so its worker restores the checkpoint into a fresh workspace and
continues from the position; a position inside a parallel branch is
refused with 400 before the run exists. `POST /runs/{id}/rewind` is that
fork of a terminal run followed by the source's archive and its
`run.superseded` record (207 when the archive fails); `POST
/runs/{id}/retry` forks a terminal run at its last checkpoint, rerunning
the stage that failed. The projection carries `forked_from`. The Rust
and TypeScript clients gain the four calls.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri 639ce3e added `ControlService::interrupt_firing` and the `LiveTurns`
capability a host installs beside the pause hooks. Fabro now drives it:
`RunControls::interrupt(stage, text)` resolves its stage the way a steer
does (a label, a node name, or the run's one live agent stage) and stops
that stage's current model turn, keeping the session; the text, when
given, is the stage's next input. `engine::run` installs the live-turn set
as a runtime capability, so without it no interrupt could ever land.
The worker maps `run.interrupt` and `run.interrupt_then_steer`, both of
which now carry an optional `stage`, to that call. The control bus is
one-way, so a refusal is recorded the way a refused steer is: a
`run.notice` on the run's stream whose code says why (`no_live_turn` when
Petri refuses a stage with no turn in flight, `no_such_stage`,
`interrupt_refused`).
The server's `POST /runs/{id}/interrupt` and `POST /runs/{id}/steer` with
`interrupt=true` forward the control and answer 202, replacing the 501
`interrupt_unsupported` stub. The interrupt endpoint takes an optional
body (`stage`, `text`), refuses a finished run with 409
`run_not_interruptible`, and forwards an interrupt of a blocked run, since
an agent stage may be running a turn beside the question and the worker
judges each stage itself. `fabro events --pretty` prints the delivered
interrupt and the stage's `attractor.turn.interrupted` report.
Verified on the twin: `fabro steer --interrupt` during a long tool call
ends the turn, the text is the agent's next request, and the stream
carries the `$interrupt` record and the interrupted-turn report; an
interrupt of a gate stage is refused with `no_live_turn` and the gate's
question is untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the lithos-llm pin from 55add459 to 43a42ac28e9d9bcf40a91abc02be4f12ca274ebb,
and the three Pebble pins from a39f43e to 67c9f48, Pebble `main`, which pins
that same lithos-llm revision so Cargo holds one lithos-llm crate. lithos-llm
`main` (ca19fac) is one commit further; that commit touches only its nightly
workflow, so this pin stays on the revision Pebble unifies with.
The `openai`, `anthropic`, `gemini`, and `openai-compatible` features are
gone upstream; each expanded to `runtime`, which `bedrock` implies, so the
four names leave the fabro-llm feature list. Every other manifest already
names `runtime`.
The catalog schema now names one adapter and many codecs per provider.
`adapter` defaults to `http`, `codecs = [...]` replaces `codec` and defaults
to `["openai-chat"]`, and the loader rejects the old `codec` key and the four
protocol-named adapter ids. Every inline catalog in tests and docs moves to
the new shape: the `openai-compatible` + `openai-chat` pair is dropped as the
default, `adapter = "openai"` + `codec = "openai-responses"` becomes
`codecs = ["openai-responses"]`, and the one test that swaps in a custom
adapter id now adds the line instead of replacing one. The settings
reference, the API schema's `Provider.adapter` description, and the SDK page
describe the new fields; the three `docs/superpowers/plans/` files that show
the old shape are dated, unchecked historical plans and are left as they are.
The implied agent profile for an operator provider that declares none used
to read the removed protocol adapter ids; it now reads the provider's first
codec (Anthropic Messages and Gemini map to their harnesses, the `bedrock`
adapter to Anthropic, everything else to OpenAI), with a test for the codec
path.
Absorbing the rest of the range: OpenRouter and Fireworks now ship enabled,
so the two fabro-llm tests that used OpenRouter as the disabled fixture use
`bedrock-openai`, and the docs and comments that said the two ship disabled
are corrected. The built-in catalog grew past 100 enabled model rows
(Vercel, TypeSafe, and the enabled OpenRouter and Fireworks rosters), so the
pagination shape test walks `page[offset]` to the last page instead of
assuming one page fits.
`cargo update -p` on the four crates also re-resolved a few already-locked
edges to match the lithos-llm lockfile: `windows-sys` 0.61.2/0.60.2 ->
0.59.0 under dirs-sys, errno, nu-ansi-term, quinn-udp, rustix,
rustls-platform-verifier, tempfile, terminal_size, and winapi-util;
`windows-core` 0.61.2 -> 0.62.2 under iana-time-zone; `errno` 0.2.8 ->
0.3.14 under signal-hook-registry; and `indexmap` 2.13.0 as a new public
dependency of lithos-llm. No package version was added or removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`SteerRunRequest` takes an optional `stage`: the label the projection
shows (`node@visit`, or `node/e<execution>@visit` when two executions
share one) or the node's name. The server passes it on the worker control
message; the worker's `RunControls` resolves a label to the live agent
firing and steers that firing, and a node name through Petri's own
live-stage index. Unnamed, the one-live-agent rule stays, and the refusal
now names the live stages by their labels. `fabro steer --stage` sets it.
A controls scenario runs two agent stages side by side, sees the unnamed
steer refused with both named, and steers each apart, one over the API
and one through the flag.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`RunSandboxInstance` carries `ready_duration_ms` from the root scope's
`scope.acquired` and `retained` from its `scope.released`, so the view
says how long the sandbox took and whether it still exists after the run.
The OpenAPI schema, the TypeScript client and the web sandbox tab's
overview show both; the host sandbox scenario asserts them on a real run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
No store sits behind `[server.slatedb]` any more: the section leaves the
settings layer, the resolved server settings, the defaults, the API schema,
the TypeScript client, the install wizard and the docs, and `fabro install`
probes the bucket for the `artifacts/` prefix alone. A settings file that
still carries the section is rewritten once at startup by a temporary
migration that removes it with a backup beside the file.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`artifact.collected {execution, firing, attempt, path, blob, bytes, digest}`
records one file a stage left in its workspace, with the bytes in the blob
table; `run.diff {base_sha, head_sha, diff_summary, patch_blob}` records
the run branch against its base; `run.branch` names the workspace the
branch was created in. `RunProjection.artifacts` lists the collected
files, and `parse_blob_ref_encoded` reads Petri's `#json` marker so a
reader knows whether a blob is text or a JSON value.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`docs/internal/events-strategy.md` is now the run stream strategy: the
two logs (Petri's records and Fabro's platform records), the projector
that folds them and assigns `stream_seq`, how to record a fact Petri
cannot know, how to read the stream, and the Ask Fabro session log.
`docs/internal/events.md` (the 106-event catalog) and the event schema
v2 shape document described the deleted `EventBody` model and are
deleted; AGENTS.md routes to the strategy for platform records and
stream consumers. The testing strategy's `progress.jsonl` rules name
records and stream items instead, and the public API nav drops the
removed per-stage events endpoint.
The interview adapter's module docs and the fabro-petri README no
longer claim the adapter posts `interview.*` events: readers see a
question in Petri's own progress record, and the server records who
answered as the `interview.answered` platform record.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The TypeScript client's `RunCheckpoint` still carried the legacy
executor's resume fields; regenerating it from the spec gives it the
slim shape (`timestamp`, `current_node`, `git_commit_sha`). The CLI
test helpers that read a run's stream from its directory or the API
are named `run_stream_items`, so nothing but the dropped table's
migration history is still called `run_events`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, fourth commit: with no writer
and no reader left, the legacy event log goes.
- `fabro-types`: `run_event` (`EventBody`, `RunEvent` and every props
struct), `EventEnvelope` and the `RunEventDetail*` types are deleted.
What the projection and the API still use moves out of the event
vocabulary: `AgentEventProps`, `AgentSessionActivatedProps`,
`AgentToolsAvailableProps`, `StagePromptProps`, `SessionCapability`
and the coding event names to `agent_props`; `RunNoticeLevel` and
`RunNoticeCode` to `notice`; `InterviewOption` beside the question
types; `RunRunnableSource` beside the run status. `Checkpoint` is
what Fabro records for a Petri run: `timestamp`, `current_node`,
`git_commit_sha`; the conclusion's stage summaries derive from the
projection's stages instead of the checkpoint's node maps.
- `fabro-store`: the Slate bridge (`RunDatabase`, the Slate `Database`,
`keys`, `record`, `EventPayload`) and the reducer (`run_state`) are
deleted. `Database` is the blob table and the run summary store over
one pool; the blob store is SQLite only; the run summary store keeps
the `runs` row a projector writes and lists, and finds the pull
request creation candidates over `platform_records`; `build_summary`
and `projected_usage` live in `run_summary`. The SlateDB dependency
is gone. Test fixtures build the store from its two SQLite stores.
- `fabro-workflow`: the `event` module (the `Event` enum, its
conversion, sink, emitter, redaction, stored fields and names),
`runtime_store`, `StageScope` and the legacy seeding test helpers are
deleted; the tests that seeded legacy runs read platform records or
a projection instead.
- `fabro-sandbox` owns `GitRetryReason`.
- The server builds the store without an object store; the legacy
`POST /runs/{id}/events` tests go, an interrupt answers
`interrupt_unsupported` in the tests as it does in the handler, and
the tests that read a run back through the Slate handle read its
projection or its platform records. The projection folds a block
that lands while the run is paused as the pause's prior block, and a
pause or unpause clears the pending control it answers; a control
request's check-and-append holds a per-run lock so two concurrent
cancels record one request.
- The CLI's final output is the response of the last stage that
produced one; the workflow tests read completed nodes from the
succeeded stages.
- The spec's `RunCheckpoint` carries the three fields the type keeps.
Still failing until the next commits: the CLI tests that seed runs
through `POST /runs/{id}/events` or wait for legacy event names, and
the two Ask Fabro resume tests (the sandbox instance gap).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, third commit: the legacy event
API and every reader of it go, so that the next commits can delete the
event log, its reducer and the types beneath them.
The API:
- `GET /runs/{id}/events` pages the run stream only
(`PaginatedRunStreamList` by `after`); the legacy `since_seq`,
`before_seq` and `order` cursors, the `oneOf` envelope, the legacy
`EventEnvelope`, `PaginatedEventList`, `RunEvent`, `EventSeq`,
`AppendEventResponse` and `RunEventDetailResponse` schemas,
`POST /runs/{id}/events`, `GET /runs/{id}/events/{seq}` and
`GET /runs/{id}/stages/{stageId}/events` are deleted. `GET
/runs/{id}/attach` and `GET /attach` frame `RunStreamItem`s only.
- The Rust and TypeScript clients regenerate; the removed models leave
the TypeScript package.
The readers:
- `fabro-client` drops the legacy run event listing, tail and attach
methods and `RunEventStream`; `list_run_stream_until` bounds a stream
read.
- `fabro-tool`'s `fabro_run_events` lists, searches and details the run
stream: `after` is the exclusive `stream_seq` cursor, `event_id` the
item's id, filters match the item's name and `recorded_at`.
- `fabro-dump` writes the stream to `events.jsonl`; `fabro dump` reads
it.
- The CLI's progress renderer keeps only what the run stream drives:
the legacy event conversion, the sandbox and setup displays and their
styles go. `fabro system events` prints stream items.
- The server's demo mode folds its agent fixture straight into the
session projection and answers the attach stub with a stream item;
the demo stage events endpoint is gone.
- The web app: every run is a Petri run. The legacy event hooks,
renderer props, stage popover summary, run phases derivation and
live-event payload handling are deleted or ported to `RunStreamItem`;
toasts and board refreshes read the stream's platform records.
- Tests: the legacy API round trips and pagination tests are deleted;
the CLI's MCP, attach and system event mocks serve stream pages; the
CLI test helpers read stream items.
Still failing until the later commits: the CLI tests seeded through
`POST /runs/{id}/events`, the server tests over the legacy store, and
the legacy type tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, second commit. Ask Fabro's
sessions were the last writer of `run_events`: a session's creation, its
turns and their messages, tool calls and endings went into the run's
legacy event log, keyed by the run's sequence. They now have a log of
their own.
- `run_session_events` (migration `2026091802`): one row per session
event, numbered per session from 1, with the owning run, the turn, the
event name and its properties. `RunSessionEventStore` appends under the
write lock, lists a session from a sequence, names a session's owner
from its creation event, deletes a run's sessions with the run, and
publishes each committed event to its subscribers.
- `fabro_types::SessionEvent`: `seq`, `session_id`, `run_id`, `ts` and a
flattened body (`event` naming the kind, `properties` its fields), with
the same event names and property shapes the legacy events carried,
so the web app and the CLI read the same JSON. The property structs
move to `session_event`; `run_event::session` re-exports them under
their old names until the legacy event log goes.
- The API: `GET /sessions/{id}/events` pages `PaginatedSessionEventList`
by the session's own sequence, `GET /sessions/{id}/attach` replays and
streams `SessionEvent` frames (subscribed before the replay, so no
event falls between the two), the turn stream carries the same frames,
and an interrupt answers with the recorded event. The session
projection folds `SessionEvent`s; the legacy `find_session_owner` over
`run_events` is gone.
- The CLI's `run ask` and the web app's session stream read
`SessionEvent`; the web runtime no longer accepts the nested legacy
envelope shape.
The two session resume tests in the server keep failing for a reason
this commit does not touch: Ask Fabro reconnects to the run's sandbox
from the projection's sandbox instance, which the Petri projection does
not carry yet (`VIEWS.md`, the `scope.acquired` gap).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, first commit of several: step 4
spans commits because the legacy event log and its consumers cannot go
in one compiling change. This commit moves every writer off `run_events`;
the reducer, `EventBody`, the Slate bridge and the API's event types
still exist for the readers the next commits port or delete.
Writers:
- The server records a run's lifecycle (submitted, runnable, starting,
running, blocked, paused, control requests and effects, the terminal
status), its title, parent link, archive state, notices and pull
request state as platform records (`fabro_store::platform_records`),
through the new `server::run_records` module. Every append wakes the
projector and waits for its pass, so the read that follows a write
holds the record.
- Pull request creation is recorded as `pull_request.requested`,
`pull_request.created`, `pull_request.failed`, `pull_request.linked`
and `pull_request.unlinked`; the projection folds them into the run's
pull request and creation state.
- Answers to questions are recorded as `interview.answered` with the
answering principal and the answer text; the interview adapter no
longer posts legacy `interview.*` events (`QuestionSink` is now an
optional observer).
- The worker (`fabro run __run-worker`) records its lifecycle, notices
and pause state over `HttpPlatformRecords`; `HttpRunStore` for the
legacy event log and the worker's `run_store` are gone.
- `persist_created_run` appends `run.created` and `run.submitted`.
Readers:
- A stream follower (`server::stream_follower`) follows each live run's
stream (Petri events and platform records), folds lifecycle records
into the in-memory run state, forwards items to the global attach
broadcast, and syncs blocked and paused from the projection.
- Slack posts questions from the projection's pending interviews,
finishes them on `interview.answered` or `question_expired`, and sends
lifecycle notifications with `notification.sent` dedupe.
- `GET /runs/{id}/events` and the attach endpoints serve only the run
stream; the per-event, per-stage and `POST /runs/{id}/events`
endpoints and their tests are deleted.
- `Database::load_run_projection` reads the Petri projection only.
Deleted with the writers:
- The SQLite blob and run-history activation migrations and their
legacy Slate imports (`legacy_blob_import`, `legacy_run_history_import`,
the activation backup): a greenfield server has no Slate history to
import, and the run-history verification refused to start a server
whose runs have no legacy events.
- `fabro-workflow`'s `operations::archive` and `operations::run_store`.
- The server's legacy-event unit tests and the CLI's `HttpRunStore` tests.
The in-process answer transport is now set after the starting and
running records land, not gated on the live status still being
`Starting` (the records already moved it).
The manifest validation test for a `run.agent.mcps.<name>` catalog
reference now expects `unsupported.workflow_toml.run.agent.mcps.reference`:
Petri's Fabro frontend has no server catalog to resolve it against.
Legacy readers still fail their tests until the next commits: the
reducer and Slate tests in fabro-store, the fabro-workflow create tests
that read the run back through the legacy store, the CLI tests seeded
through `POST /runs/{id}/events`, the CLI's legacy attach and render
paths, the sessions API, the OpenAPI conformance test, and the web
fixtures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The crate list names what fabro-workflow still holds and adds
fabro-graphviz; the architecture page describes Petri as the engine
every run executes on, at create and in the worker, in place of the
deleted in-process engine.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro-mcp` held two things after the legacy executor went: the mapping
from Fabro's MCP server settings to the servers pebble starts, which
only `fabro exec` still uses, and a stdio MCP client the tests of
Fabro's own MCP server speak through. The mapping is now
`fabro-cli`'s `mcp_servers` module and the client its test support's
`McpStdioTestClient`; the crate is deleted. Its `config` module was a
re-export of `fabro_types::settings::run`, which callers import directly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The legacy executor replayed a run from a checkpoint; Petri resumes a
run from its records instead, and the checkpoint timeline, rewind, fork
and retry were the operations that replay carried. The server dropped
their handlers with the executor; this removes the rest:
- the API spec's `/runs/{id}/retry`, `/rewind`, `/fork` and `/timeline`
paths with the `ForkRequest`, `ForkResponse`, `RewindRequest`,
`RewindResponse` and `TimelineEntryResponse` schemas, and the
generated TypeScript models;
- `fabro rewind` and `fabro fork` (with the checkpoint timeline printer
and the repo-origin check only they used), their reference pages and
the checkpoints guide's rewind and fork sections;
- `fabro-client`'s `rewind_run`, `fork_run` and `run_timeline`;
- the web app's Retry action on the run list and the run page.
`Run.retried_from` stays on the run type: a run that was retried before
the cutover would still name its source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro-hooks` ran the legacy executor's hooks; Petri's Attractor steps
run Fabro's hooks now, so nothing in the workspace uses the crate. The
engine freeze (the CI workflow, the two scripts, and the AGENTS.md and
fabro-petri README sections) guarded the engine half of `fabro-workflow`,
which the previous commit deleted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Delete `Engine`, `RunEngine`, `[workflow] engine`, `[server.execution]
engine`, `FABRO_SERVER_ENGINE` and `fabro server start --engine`. The run
spec records what Petri admitted as `admission: PetriAdmission`; the
create handler always admits through `Runtime::check`; `execute_run`
always launches the Petri worker (or executes in process under the test
override); the CLI runner takes only the Petri worker path, and its
legacy control arm, artifact uploader, signal pause handlers and
credential helpers go with it. The CLI's `attach` and `events` read the
run stream only.
Two gaps this surfaced are closed here: the check adapter binds the
server's run variables as Petri compile variables (`{{ vars.* }}` in a
prompt no longer fails admission), and deleting a run removes its Petri
records, lease, platform records, projection and stream.
Tests: the config engine tests are replaced (an engine key is unknown),
the API round-trip test covers `PetriAdmission`, the server and CLI
Petri scenarios drop their engine settings, and the API tests that read
legacy event names now read the run stream or the session events. The
remaining red tests are fixtures and scenarios of the legacy executor
and the legacy event store (`fabro-store` `slate` and `run_state`,
`fabro-types` legacy `run.created` JSON, the server's handler-registry
scenarios, the CLI dry-run snapshots), which the next steps of the F4.3
series delete or port.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`GET /runs/{id}/events` and `GET /runs/{id}/attach` serve a Petri run's
public events and Fabro's platform records as one ordered stream in a
Fabro envelope (`RunStreamItem`: `run_id`, `stream_seq`, `kind`, `id`,
`recorded_at`, `item`), read from the projector's `petri_stream` table.
The cursor is `stream_seq` (`?after=`); the item's own identity (the
Petri `EventId` as `<log>/<seq>/<index>`, or the platform record's seq)
travels beside it for deduplication. A legacy run keeps its envelope on
the same endpoints; the OpenAPI response is the union of the two lists,
and the stream list reports Petri's `EVENT_CONTRACT_VERSION`.
The attached stream follows the projector's commit signal (a wake-up,
with a poll as the fallback) and ends after the platform record of the
run's terminal lifecycle transition, the analog of the legacy stream's
`run.completed`, or a bounded grace after the projection went terminal.
`RunSpec.engine` (`RunEngine`, `PetriAdmission`, `PetriGraphRef`) is
named in the spec and reuses the Rust types. `fabro-client` matches the
union and adds `list_run_stream`, `list_run_stream_page` and
`attach_run_stream`.
A server test attaches to a two-branch parallel run, disconnects once
both branches started, records a platform notice while both branch
scripts run, reconnects from the last `stream_seq`, and checks the
union is the whole stream: every item once, in order, no gap, no
duplicate, the notice between the branch events, and the same as the
paged listing. The Petri scenarios capture their settled projection and
stream as JSON fixtures for the web app under
`FABRO_CAPTURE_PETRI_FIXTURES`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro's hooks on a Petri run wrap the hooks the runtime installed for
`[[run.hooks]]` and forward every point. In `prepare_result`, before the
finish is recorded, they commit the stage's files on the run branch of
its host workspace with Fabro's author identity and the run, execution,
firing and attempt as trailers, and publish the commit to a snapshot
repository beside the run's workspaces under a ref per checkpoint. A
stage that failed on its own terms is committed like a successful one; a
commit that fails is fatal: the outcome becomes a `checkpoint_failed`
failure, the run is cancelled through the coordinator handle, and the
transition refuses the firing's routes. In `transition` they write the
platform checkpoint record, keyed on the Petri position and the
checkpoint's operation identity, and a failed write is a recorded
problem.
On restart the server runs the recovery protocol before it relaunches a
worker: a run with a failed checkpoint is reported failed; otherwise
every live execution's last durable finish names the snapshot its
workspace is verified against, reset to, or restored from, with a lost
record reconciled from the snapshot repository, and a finish with no
snapshot fails the run rather than resume it on stale files.
The worker reaches the platform records over two new worker-scoped
endpoints; the server reaches the table directly. A test gate directory
lets the CLI scenarios hold a checkpoint at a named point.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker shape of the integration plan (F1.3) needs a run's worker to
reach the run's Petri records over the server's API. This adds the
contract: six worker-scoped endpoints under `/api/v1/runs/{id}/petri/`
(open, release, list and append records of one log, write and read a
blob), their request and response schemas, and the generated Rust and
TypeScript clients.
A store error needs more than a code: `petri_run_leased` names the
holding owner and `petri_record_conflict` names the refused position.
`ErrorResponseEntry` gains an optional `meta` object for such
code-specific members, `ApiError` can carry it, and the client's
`ApiFailure` parses it beside the code so a caller can act on it.
Records travel as `{seq, recorded_at, record}`, the store's own unit,
with `seq` and `recorded_at` as `uint64`. The log path segment is the
log id's text (`coordinator`, `resources`, `execution <n>`), which the
generated client percent-encodes. The blob write reuses
`WriteBlobResponse`, since Petri's digest is Fabro's blob hash.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lithos-llm attaches a cost to every response at the client: the codec
keeps a provider-reported cost when the provider supplies one, and the
resolver fills the catalog's price for the route when it does not.
Pebble records that priced usage on every assistant turn and sums it,
so each AssistantMessage on the stream, and the store fold's live stage
usage, already carries the cost. Fabro's catalog re-pricing of the same
tokens was redundant, and is gone.
model_usage_from_llm, with_reported_cost, and every estimate_cost call
in fabro are deleted. The pebble handler's stage_usage groups pebble's
accounts by route and sums them with Usage::saturating_add, keeping the
cost and source pebble carried, so the terminal stage.completed usage is
the live fold's sum; it no longer fails when the catalog does not know a
provider. A one-shot prompt stage records the response's own usage and
cost as lithos-llm returned it. The per-model price cards in fabro-llm's
API module stay.
Tests: the pebble handler sums Catalog and Provider costs per row and
leaves a row's and the total's cost unknown once an answer was unpriced;
the store fold shows the same tokens and cost live and at completion,
and None at both for an unpriced answer; the agent integration test
compares the whole completed Usage with the live fold, cost included;
a one-shot prompt stage on a mocked OpenAI-compatible provider reports a
Catalog cost that lithos-llm's resolver attached, with no fabro pricing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
run.completed, run.failed, stage.completed, stage.failed, and
agent.message carry lithos-llm's Usage; the API reference navigation
names the run usage endpoint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-pin lithos-llm to 55add4596b861a0623d00c3a54aa5c147c8d504b and
pebble to c91810fe51aece80359b9cd8efea971af0c46925, where token usage
and cost travel together as Usage { tokens: TokenCounts, cost:
Option<Cost> }. Fabro now carries that one type everywhere it used to
carry BilledTokenCounts, BilledModelUsage, UsdMicros, or a token count
beside a cost_usd_micros.
fabro-types: billing.rs is usage.rs with ModelRef, ModelUsage { model,
usage }, sum_usage, and usage_is_empty; billing_rollup.rs is
usage_rollup.rs with ProjectionUsageStage, ProjectionUsageByModel,
ProjectionUsageRollup, and usage_rollup_from_projection. Every usage
field is named usage: StageProjection.usage and usage_by_model,
Outcome<Option<ModelUsage>>, stage.completed and stage.failed usage and
usage_by_model, prompt.completed usage, run.completed and run.failed
usage (total_usd_micros is gone), Conclusion.usage, StageSummary.usage,
Run.usage. RunSize buckets by Cost.
fabro-workflow: model_usage_from_llm prices tokens from the catalog with
a Catalog cost source, with_reported_cost keeps a provider cost, and the
pebble handler's stage_usage groups pebble's accounts by model and sums
rows with Usage::saturating_add, so a total has a cost only when every
priced part was priced. The store fold's live usage is the agent's
usage plus its descendants'.
API: the OpenAPI spec deletes BilledTokenCounts, BilledModelUsage,
CompletionUsage, CompletionCost, TokenUsage, and RunBillingSummary,
adds TokenCounts, Cost, Usage, and ModelUsage, and renames every
billing schema, property, tag, path, and operation to usage. fabro-api
reuses lithos-llm's and fabro-types' types through with_replacement,
with a round-trip test per replacement.
Old stored runs get no migration: their pebble events in the old shape
read back with zero usage, and their rebuilt projections lose agent
usage.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
StageProjection.agent_control and the AgentControlState schema go with the
field; the agent's activity on AgentSessionProjection carries the fact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble's CodingAgentEvent stream is the agent event contract: every event
except streaming deltas is stored verbatim under its derived name and
folded into StageProjection.agent with pebble's SessionProjection. The
events doc, the events strategy, and the v2 shape doc say so, list the
agent events fabro still emits for facts pebble cannot know, and tell
consumers to read the fold rather than fold the events again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble's stream is the agent event contract. The run's own agent.mcp.ready,
agent.mcp.failed, and agent.mcp.disconnected events, which mirrored pebble's
McpServer* events, are gone with their props, the sink arms that emitted
them, and their conversion and naming entries; pebble's stored
agent.mcp.server.* events are the only record and feed the stage's fold.
The sink no longer mirrors RouteFailover onto agent.failover either: an
agent stage's moves are pebble's agent.route.failover. The event is now
prompt.failover, emitted only by a one-shot prompt stage that walks its
fallback plan itself, and its props are trimmed to the two routes, the
attempt, and the error; nothing read the rest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
StageProjection loses todos, subagents, skills, mcp_servers, and
context_window, the types behind them, their fold arms and helpers, and
their OpenAPI schemas: every one of those facts is pebble's fold in
StageProjection.agent now. The context-window endpoint reads the fold's
snapshot, whose event_seq is the agent's own sequence. The parity module
keeps its assertions on the surviving own fields, usage and model, and
checks that what the stage view reads from agent is the whole-session
fold's for the stage's events. The TypeScript client is regenerated and
its stale models removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An agent stage that failed billed nothing: the backend returned a bare
error and the outcome built from it carried no usage. A terminal failure
now becomes the stage's failed outcome from the same fold that bills a
completed stage, with the tree's usage, the rows by model, the files it
wrote, and its active time; stage.failed carries billing and
billing_by_model and the store keeps both. Cancellation and retryable
failures still go up as the error.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
StageProjection.billing_by_model and a BilledModelUsage schema that reuses
fabro's type; the TypeScript client regenerated; the Billing tab's token
tooltip says subagent tokens are included and priced at each subagent's
model; the stage.completed docs describe the rows and the one usage rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AgentSessionProjection and the schemas nested in it reuse pebble's types
through with_replacement; the AgentSession prefix marks the projection's
own types where fabro already has a schema of that name, and pebble's
event-level types keep their names. The round-trip test builds a
projection over a scripted stream, validates it against the spec with the
spec as the root document, checks every serialized key is declared, and
validates every enum variant this build knows. The TypeScript client is
regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble's SessionProjection reads ProcessingEnd to complete a prompt and mark
the session idle, so a projection rebuilt from the run's log needs it: one
small event per prompt. The four pebble events the sink mirrored onto
fabro's own agent.failover and agent.mcp.* are now stored verbatim as well,
so the fold sees the route moves and the MCP outcomes; the mirrors stay
until every reader is on the projection.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>