Guard EventScan seeks against sequences past MAX_EVENT_SEQ: a
seven-digit start prefix sorts below six-digit event keys, so an
unvalidated since_seq like 5000000 returned an incorrect slice of
history instead of an empty page. An end bound past MAX_EVENT_SEQ now
delegates to the unbounded scan, which is equivalent because no stored
sequence exceeds it.
Clamp the descending exclusive end to just past the newest stored
event, so an oversized before_seq cursor pages from the newest event
instead of probing empty key space and returning nothing.
Split RunEventListParams out of EventListParams so before_seq and
order are only accepted by /runs/{id}/events; the session, stage,
pair transcript, and demo endpoints go back to ignoring them instead
of accepting order=desc while returning ascending results.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves conflicts with the shared-checkout parallel rewrite (#607) and the
cached-run/billing dedup (de60eb900):
- handler/parallel.rs: rebuilt on main's shared-checkout version. Branch
ordinals are still reserved inside the branch task right before
ParallelBranchStarted (with graph_visit/resumed_from_stage_id), and the
reserved StageScope is shared with post-await error paths via a OnceLock
slot instead of main's dispatch-time visit=1 scope, so completion events
are never emitted under a guessed ordinal.
- billing.rs: keep this branch's run_stage_from_projection (RunStage grew
graph_visit/resumed_from_stage_id and a typed id), adopt main's
state.cached_run() and drop the removed run_stage_from_stage_id import.
- run_projection.rs: adopt main's typed parallel_results
(Option<Vec<ParallelBranchResult>>).
- run_event/misc.rs: union of both sides' imports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
- Add AppState::cached_run with the standard 500/404 mapping and use it
everywhere handlers read the shared run-projection cache. This also
normalizes two inconsistencies: graph-source cache errors now map to
500 (was 502), and a missing projection in PR create/unlink now maps
to the canonical 404 (was a bespoke 500).
- Extract an EventScan cursor shared by the four run-event scan loops,
delegate list_events_from to the paginated variant, and stop the
stage-event scan once its page is full instead of walking the rest of
the log.
- Hold Arc<RunProjection> in the local projection cache so opening a run
no longer deep-copies the projection (copy-on-write via Arc::make_mut),
and drop the now-unreachable shared-cache branch in last_event_seq.
- Trim hot-path clones: run_files serves the projection Arc directly,
run-state serializes by reference, artifacts only checks existence, and
the command-log handler opens a reader only for the CAS-blob branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves conflicts with main's typed reasoning_effort field (#609) and
the usage-buckets test (#616). The handler's manual string parse is
superseded by serde-level validation of the typed enum, so it is
removed along with its test; the client-side unsupported-effort
validation and 400 error mapping from this branch are kept.
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>
The fallback provider set was always catalog.all_provider_ids(), computed
at every call site and threaded through five layers alongside the catalog
itself. Fold it into Catalog::resolve_selection_with_catalog_fallback and
carry only a catalog_fallback flag through the transform/validate/
materialize entry points.
- materialize_run delegates to resolve_run_model again instead of
re-inlining its provider normalization and selection
- run_preflight derives ready providers from llm_result instead of
taking both, so callers cannot pass inconsistent pairs; the legacy
tests now exercise the production ready-first routing path
- AppState::resolve_llm_client_with_ready_ids replaces three copies of
resolve-then-extract-provider-ids, and ready_llm_provider_ids
delegates to it
- the unreachable "model resolution failed" preflight check becomes an
invariant error where the materialized run is produced
- validate_prepared_manifest_with_vars/_for_preflight share the
ValidateInput construction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the per-field if-let cascade in the structured completion path
with a single struct-update expression. The cascade had to be extended
by hand for every request field and silently dropped stop_sequences and
provider_options, which the non-structured path already forwarded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract a shared fetch_run_events_page helper so the three client
paging loops (full list, until, tail) no longer repeat the request/
convert/has_more skeleton; fold the tail loop's two descending-order
checks into one and drop its redundant had_events flag.
- Skip the latest-seq lookup in list_events_before_with_limit when the
caller supplies a before_seq cursor, so a cold projection cache costs
at most one full history scan per pagination session instead of one
per page.
- Remove the dead before_seq max(1) clamp and the passthrough order()
accessor from EventListParams.
- Document the CLI --tail 0 --follow seeding trick and the reader
event_seq placeholder invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Return InvalidRequest (400) for unsupported speed too, matching the
reasoning_effort check and the complete()/stream() doc comments
- Centralize fabro_llm::Error -> ApiError mapping in a From impl so the
completions handler, playground handler, and Error::Llm arm agree on
the InvalidRequest -> 400 / else -> 502 split
- Reject unparseable reasoning_effort values with 400 instead of
silently dropping them
- Add classify_sdk_invalid_request test per fabro-workflow convention
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract the quadruplicated watchdog check-and-clear logic in
schedule_worker_cancel_escalation into ManagedRun methods
(escalation_still_current, clear_escalation_for)
- Derive strum::IntoStaticStr for WorkerRef instead of a hand-written
variant-to-string match in kind()
- Use the generated AgentControlState constant instead of the raw
"waiting_for_steer" literal in run-detail.tsx
- Replace optimisticCancellationRunId state with a boolean; the
component is keyed by run id, so the stored id could only ever be
this run's own
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>