Resolved conflicts against main's shared-projection-cache rework:
- projection_cache.rs: kept main's projection_snapshot and dropped this
branch's last_seq accessor, which it subsumes; latest_event_seq now
reads the sequence from projection_snapshot.
- run_store.rs: kept main's EventScan cursor and added a seek_before
constructor so the backward-pagination range scan bounds its end key
through the same abstraction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The catalog comment showed `fabro provider login fireworks`, but
`--provider` is a required flag: `fabro provider login --provider fireworks`.
- The remote-server `fabro model test` example omitted `--provider fireworks`,
which could resolve the slug against a different provider.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolved conflict in run_store.rs tests: kept both the new
list_events_before_with_limit tests from this branch and the
append_event_rejects_sequences_beyond_key_order_limit test from main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rewrite the Fireworks tool round-trip E2E test on the shared
run_model_test deep-test pattern used by the OpenRouter and Poolside
opt-in provider tests, instead of a fourth hand-rolled copy of the
multiply-tool scaffold.
- Drop the "(via Fireworks)" display-name suffix from slugs that have no
first-party provider (kimi-k2.6, deepseek-v4-*, minimax-m2.7),
matching the OpenRouter convention; rename "Qwen 3.7 Plus" to
"Qwen3.7 Plus" to match existing Qwen entries.
- Fix kimi-k2.6 vision flag to false, matching the OpenRouter entry for
the same slug (the portability test asserts they are the same model).
- Assert small_default_for_provider and per-model family/vision/
reasoning in the catalog tests, mirroring sibling provider tests.
- Add Troubleshooting and Further reading sections to the Fireworks
docs page, matching the other opt-in provider pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A node cancelled (or lost to a crash) mid-flight and then resumed now
starts a new stage execution with the next StageId ordinal (work@2)
instead of reusing and clearing the cancelled execution's projection.
The old execution stays immutable with its own events, session, output,
timing, billing, and termination state.
Engine:
- Add a run-scoped StageExecutionTracker on RunServices with per-node
high-water marks. Ordinals are reserved after the StageStart hook
passes on the first attempt (retries reuse the reservation), ensured
at the composite checkpoint pre-step for hook-skips, and reserved in
on_terminal_reached for terminal nodes' synthetic events.
- Keep three concepts distinct: graph visit (max_visits/checkpoints,
unchanged), stage execution ordinal (the @N in StageId), and handler
attempt. The tracker is not checkpointed; the append-only stage event
history is its durable source of truth.
- resume() seeds the allocator from the run projection and computes a
node -> StageId provenance map of executions observed after the
selected checkpoint, threaded through execute_persisted_run,
RunSession, and InitOptions.
Events and projections:
- stage.started, parallel.branch.started, and checkpoint.completed
carry optional graph_visit and resumed_from_stage_id; StageProjection
stores both. Old events deserialize with None and legacy duplicate
stage.started replays keep last-attempt behavior.
- The CheckpointCompleted reducer is envelope-first: diffs and
skipped-stage synthesis attach to the exact execution StageId, an
existing Retrying projection finalizes as Skipped without losing
identity, and historical node_outcomes no longer create or collide
with newer ordinals (node_visits remains a legacy fallback).
Handlers:
- Parallel fan-out reserves child ordinals through the shared tracker,
derives worktree pass{N} from the parent's execution ordinal, and
seeds branch contexts with explicit child stage scopes so branch
lifecycle and nested handler events agree.
- Artifact capture and manager-loop child logs follow the ordinal.
API and UI:
- RunStage documents visit as the execution ordinal and adds optional
graph_visit and resumed_from_stage_id; Rust and TypeScript clients
regenerated.
- The web sidebar lists both executions chronologically; resumed stages
show a "Resumed from" link in the stage detail header and hover
popover, with the graph visit surfaced when it diverges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a disabled-by-default `fireworks` provider to the built-in catalog,
served through the existing openai_compatible adapter/codec. The curated
roster covers Kimi K2.7 Code (default), Kimi K2.6, DeepSeek V4 Pro/Flash,
GLM 5.2, MiniMax M2.7, Qwen 3.7 Plus, and GPT-OSS 120B/20B (small
default + probe), with serverless pricing including cached-input rates.
All api_ids were verified live against /chat/completions (Fireworks'
GET /v1/models only returns a featured subset), and serverless responses
were confirmed to report prompt_tokens_details.cached_tokens, so cache
billing works through the existing codec path.
FIREWORKS_API_KEY is registered as an optional vault secret; provider
login, vault storage, and diagnostics probing are catalog-driven and
need no code changes. Includes catalog/install tests, two live e2e
tests, an integrations docs page, and a provider logo for the web UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cumulative implement + simplify_fable diff recovered from the run's meta
branch (fabro/meta/01KY7YH7RYCJ1BDVTTP96ZA4HV, stage 006 diff.patch).
The run validated this tree clean: cargo nextest (7,007 passed), clippy,
fmt, TS client regen + typecheck, web tests (679 passed), docs check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The crate reorganization renamed lib/crates/ to lib/apps|components|foundation/.
Git followed all modified files across the rename; the only conflict was the
newly added codec/cache.rs, now placed at lib/components/fabro-llm/src/codec/cache.rs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anthropic prompt caching is opt-in per request: without explicit
ephemeral cache_control breakpoints in the body, no cache writes or
reads ever happen. The OpenAI-compatible codec never emitted them, so
every run on openrouter Claude models billed the full conversation at
the uncached input rate on every turn (0 cache tokens on the billing
page, confirmed by OpenRouter's activity portal).
- Add a `cache_control_breakpoints` model feature declaring that a
route only caches when the request marks the cacheable prefix; set it
on the builtin OpenRouter Claude rows. Catalog build rejects the flag
without `prompt_cache`.
- Teach the Chat Completions wire shape a parts-form content variant so
a message can carry the annotation; unmarked messages keep the
plain-string form for compatibility with strict servers.
- Mark the last system message (covers tools + system upstream) and the
second-to-last user turn, counting tool results as user turns —
mirroring the anthropic codec's placement so agent loops get
incremental cache hits.
- Extract the shared placement/opt-out policy into codec::cache and
refactor the anthropic codec onto it; anthropic wire snapshots are
unchanged.
- Honor `provider_options.<name>.auto_cache = false` as an opt-out and
consume the control key instead of merging it into the body.
- Mirror the new feature through settings (fabro-config), the OpenAPI
schema, and the generated TypeScript client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Document the contract of read_last_file_routing_json (terminal JSON
extraction only; routing validation happens downstream), extract a
shared sandbox_with_file test helper, and drop the misleading
"standalone" wording from the fallback docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add two lint rules so the graph format stops silently accepting
attributes that nothing reads:
- inert_attribute: handler-specific attributes (script, language,
duration, join_policy, max_parallel, output_schema, prompt) placed on
node types that never read them. Attributes read by several handlers
(timeout), resolved for every node (fidelity, retry_policy), or
injectable via model stylesheets (model, reasoning_effort, ...) are
deliberately excluded.
- parallel_branch_inert_attribute: fidelity/thread_id on parallel
branch nodes and fork->branch edges. Branch dispatch bypasses the
fidelity lifecycle, so these are dead letters today; the warning
points at the parallel node, where fidelity does take effect.
Also reconcile the loop_restart docs with actual executor behavior:
taking a loop_restart edge restarts from the target with a fresh empty
context (visit counts preserved), on success as well as failure; the
transient_infra guard applies only to failure crossings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Poolside as a built-in OpenAI-compatible provider and expose Laguna S 2.1 and XS 2.1 both directly and through OpenRouter. Include vault/env credential registration, secret redaction, live coverage, catalog tests, and user documentation.
## Summary
- add GPT-5.6 Sol, Terra, and Luna to the built-in OpenAI model catalog
with current capabilities, limits, aliases, and pricing
- route all three models through the OpenAI Responses API and keep the
Codex-safe 272K context policy
- make GPT-5.6 Sol the OpenAI default and update the model docs and Ask
Fabro API contract
## Testing
- cargo nextest run -p fabro-model
- cargo nextest run -p fabro-llm builtin_catalog_route_equivalence_table
- cargo nextest run -p fabro-server list_models
- cargo nextest run -p fabro-server --features test-support
run_responses_include_ask_fabro_affordance
- cargo +nightly-2026-04-14 fmt --check --all
- cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-llm
--all-targets -- -D warnings
- git diff --check
## Summary
Provider `extra_headers` previously required values to be typed TOML
tables (`{ env = "X" }`, `{ literal = "Y" }`, `{ vault = "Z" }`). This
PR migrates them to the project's standard interpolation string format:
plain text for literals, `{{ env.NAME }}` tokens for environment
variables, and `{{ secrets.NAME }}` tokens for vault secrets. This
brings `extra_headers` in line with the rest of the interpolation system
and unlocks mixed-segment values like `Bearer {{ secrets.GATEWAY_TOKEN
}}`.
### What changed and why
**Config authoring surface (`fabro-config`):**
`ProviderSettings.extra_headers` changes from `Option<HashMap<String,
HeaderValueRef>>` to `Option<HashMap<String, InterpString>>`. The
`Combine` impl and all re-exports are updated accordingly.
**Catalog layer (`fabro-model`):**
`ProviderCatalogSettings.extra_headers` and
`CatalogProvider.extra_headers` become `HashMap<String, String>` — raw
interpolation source strings. This is required by the crate dependency
direction: `fabro-types` (which owns `InterpString`) depends on
`fabro-model`, so `fabro-model` cannot hold `InterpString` without
creating a cycle. The source string is re-parsed and resolved in
`fabro-auth` at credential-build time.
**Credential resolution (`fabro-auth`):** Both `CredentialResolver`
(vault-backed) and `EnvCredentialSource` (env-only) are rewritten to
parse each header source string as an `InterpString` and resolve it with
a `ResolveCtx` scoped to `env` + `secrets` only. A new
`resolve_extra_headers` helper is shared between the two paths. Vault
resolution uses `vault_token_lookup`, which wraps `vault_get_token` and
maps any non-Token vault entry to `None` — so file and OAuth vault
entries fail closed rather than resolving incorrectly. `vars.*` and
`inputs.*` tokens are not in scope and produce `Unavailable` errors
automatically.
**New error variant:** `ResolveError::Interpolation { provider, source
}` surfaces header resolution failures as diagnosable auth issues. The
inner `source` (an `InterpResolveError`) names only the token namespace
and name — never a resolved value.
**`{ literal = "..." }` guardrail removed:** `HeaderValueRef`
deliberately rejected bare string header values to discourage pasting
credentials. `InterpString` accepts any string. This is an intentional
change; the mitigation is documentation — use `{{ secrets.NAME }}` for
credential-shaped values, not bare literals.
**Redactor registration gap (noted, not fixed here):** Secrets resolved
into provider headers at the credential boundary do not flow through the
run boundary's exact-match redaction registry. Exposure is low (headers
are host-side and outbound-only, never logged), but a follow-up should
thread a registering lookup through `VaultCredentialSource`. A code
comment at the resolution site marks the gap.
### Breaking change
Existing `extra_headers` config using `{ env = "X" }`, `{ literal = "Y"
}`, or `{ vault = "Z" }` table syntax **will fail to parse** after this
change. Users must migrate to the token form: plain strings for
literals, `{{ env.X }}` for env vars, `{{ secrets.X }}` for vault
secrets. A changelog entry is included.
### Plan Summary
- Update `ProviderSettings.extra_headers` → `InterpString` in
`fabro-config`
- Collapse authoring `InterpString` → source `String` in
`provider_settings_to_catalog` (allowlisted `as_source()` call)
- Delete `HeaderValueRef` and its serde/display/parse machinery from
`fabro-model`
- Rewrite both auth resolution paths to use `InterpString::parse +
resolve_with`; add `Interpolation` error variant
- Add `vault_token_lookup` helper for token-only fail-closed vault
resolution
- Update test TOML in `fabro-llm`, builtin catalog comment in
`openrouter.toml`, and all hand-written + generated docs
### Fabro Details
<details>
<summary>Ran 8 stages in 108m 43s for $34.44</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 5m 33s | – | 0 |
| preflight_lint | 5m 59s | – | 0 |
| implement | 43m 23s | $17.34 | 0 |
| simplify_fable | 32m 49s | $13.09 | 0 |
| simplify_gpt | 6m 25s | $4.01 | 0 |
| verify | 13m 58s | – | 0 |
| **Total** | **108m 43s** | **$34.44** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-8; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD. Be sure to use the rust-style-guide skill to help you follow this repo's Rust style conventions.", model="gpt-55", reasoning_effort="xhigh"]
simplify_fable [label="Simplify (Fable)", prompt="@prompts/simplify.md", model="claude-fable-5", reasoning_effort="xhigh"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, timeout="1800s", script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_fable -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
## Problem
The post-node run-branch checkpoint commit runs repository commit hooks
unless `skip_git_hooks` is enabled, but its sandbox command timeout was
hardcoded to 30 seconds. Consumers whose hooks run a multi-minute gate
cannot complete a checkpoint.
## Change
Adds `commit_timeout_ms` to the existing `[run.checkpoint]` table.
- Defaults to `30000`, preserving existing behavior.
- Threads the value through config raw layer -> merge -> resolve ->
resolved settings -> `RunOptions` -> `GitState` -> both checkpoint call
sites.
- Applies the configured timeout to checkpoint `git add -A` and `git
commit`.
- Keeps old serialized run manifests compatible via serde default.
## Testing
- `cargo +nightly-2026-04-14 fmt --all`
- `cargo +nightly-2026-04-14 clippy --locked --workspace --all-targets
-- -D warnings`
- `cargo nextest run --locked -p fabro-config -p fabro-types -p
fabro-workflow`
- 1795 passed, 31 skipped
- `cargo nextest run --locked -p fabro-cli
attach_json_errors_without_prompting_for_human_input`
- `cargo nextest run --locked --workspace --status-level slow --profile
ci --no-fail-fast`
- 6951 passed, 3 timed out, 187 skipped
- The 3 timeouts are preexisting on clean `upstream/main`: verified by
running `CARGO_TARGET_DIR=/data/projects/fabro/target cargo nextest run
--locked -p fabro-cli --profile ci --no-fail-fast workflow::acp::acp`
from a detached worktree at `upstream/main` (`8c7d5dc7d`), which timed
out the same three tests:
-
`workflow::acp::acp_artifacts_are_listed_when_touched_file_mtime_precedes_attempt_start`
-
`workflow::acp::acp_backend_does_not_inject_registered_provider_credentials`
- `workflow::acp::acp_backend_workflow`
## Compatibility
No behavior change without explicit opt-in. Omitted config resolves to
the existing 30 second timeout, and old serialized run manifests
deserialize unchanged.
---------
Co-authored-by: thewoolleyman <chad@thewoolleyman.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>