Skip to content

fix(stella-pipeline): bound the verdict reasoning at the point it is constructed (#1787) - #1932

Merged
macanderson merged 2 commits into
mainfrom
fix/1787-bound-reasoning
Aug 7, 2026
Merged

fix(stella-pipeline): bound the verdict reasoning at the point it is constructed (#1787)#1932
macanderson merged 2 commits into
mainfrom
fix/1787-bound-reasoning

Conversation

@macanderson

@macanderson macanderson commented Aug 6, 2026

Copy link
Copy Markdown
Owner

fix(stella-pipeline): bound the verdict reasoning at the point it is constructed

Verdict::reasoning was the verifier's whole reply, unbounded, and it travels:
into the worker's revision prompt and into the verdict cache. A reasoning model
that thinks out loud for 100 KB put 100 KB into the next worker turn — on every
revision round, at the full input rate, and left it in a cache entry that keeps
it.

Measured on the fixture in the witness below, the old code carried 114,006
characters
into the revision prompt.

bounded_reasoning clips at MAX_VERDICT_REASONING_CHARS (4,000 — roughly a
thousand tokens) and says it did.

Three decisions worth naming

Structural, not policy. The disclosure ladder already caps what crosses to
the worker, but as policy applied downstream by one consumer. This bounds the
value where it is constructed, so a consumer added later cannot be written
that forgets — which is what this issue means by "the cap should be
structural".

The head is kept, not the tail. The verifier prompt asks for the verdict
token and its reasons first, so the front is the part a revision acts on. The
witness asserts starts_with("FAIL") so a later "keep the tail" refactor has
to argue with a test rather than with a comment.

Clipped on a character boundary. A verifier answering in a language of
multi-byte characters is model output, which is runtime data — a &text[..N]
byte slice would panic the pipeline there (invariant 5).

Not re-unbounded downstream

Checked the two places that touch it afterwards: pipeline.rs's cache-reuse
arm appends a fixed ~130-character note, and heuristic_fallback builds its
reasoning from fixed strings. Both are bounded by construction, so the two
parse sites were the only ingress.

Witnesses

  • a_runaway_verifier_reply_is_clipped_before_it_reaches_the_worker — a

    100 KB reply yields a bounded reasoning that says it was clipped (a
    silent truncation reads as a verifier that simply stopped talking) and still
    leads with its verdict token. Fails on the old code:
    reasoning survived at 114006 chars.

  • an_ordinary_verdict_passes_through_the_bound_unchanged — the bound is
    invisible in the case that matters, so a fix that quietly edited every
    verdict cannot pass.
  • clipping_a_multibyte_reply_does_not_panic — the byte-slice trap.

Scope

This is item 2 of the three #1787 lists, which says "each sub-fix carries its
own witness". Item 1 (a structured-output path for verifiers whose replies open
with a preamble) needs a provider-parity posture per invariant 8 and is a
larger change; item 3 (per-candidate degradation facts instead of one
once-per-run AtomicBool) is independent. The issue stays open for both.

cargo test -p stella-pipeline — 557 passed, 0 failed. Clippy -D warnings,
fmt --check, rustdoc -D warnings, check-file-size, check-god-files and
check-left-behind clean.

Refs #1787

Summary by Sourcery

Bound verifier verdict reasoning at construction to prevent oversized reasoning payloads from propagating through the pipeline and cache.

Enhancements:

  • Introduce a character-based cap for stored verdict reasoning and apply it when parsing verifier responses to structurally limit reasoning size.

Tests:

  • Add tests asserting that oversized verifier replies are clipped with an explicit notice, ordinary replies pass through unchanged, and multibyte character replies are clipped safely without panicking.

…constructed

`Verdict::reasoning` was the verifier's whole reply, unbounded, and it travels:
into the worker's revision prompt and into the verdict cache. A reasoning model
that thinks out loud for 100 KB put 100 KB into the next worker turn — on every
revision round, at the full input rate, and left it in a cache entry that keeps
it.

Measured on the fixture in the witness below, the old code carried **114,006
characters** into the revision prompt.

`bounded_reasoning` clips at `MAX_VERDICT_REASONING_CHARS` (4,000 — roughly a
thousand tokens) and says it did.

## Three decisions worth naming

**Structural, not policy.** The disclosure ladder already caps what crosses to
the worker, but as policy applied downstream by one consumer. This bounds the
value where it is *constructed*, so a consumer added later cannot be written
that forgets — which is what this issue means by "the cap should be
structural".

**The head is kept, not the tail.** The verifier prompt asks for the verdict
token and its reasons first, so the front is the part a revision acts on. The
witness asserts `starts_with("FAIL")` so a later "keep the tail" refactor has
to argue with a test rather than with a comment.

**Clipped on a character boundary.** A verifier answering in a language of
multi-byte characters is model output, which is runtime data — a `&text[..N]`
byte slice would panic the pipeline there (invariant 5).

## Not re-unbounded downstream

Checked the two places that touch it afterwards: `pipeline.rs`'s cache-reuse
arm appends a fixed ~130-character note, and `heuristic_fallback` builds its
reasoning from fixed strings. Both are bounded by construction, so the two
parse sites were the only ingress.

## Witnesses

- `a_runaway_verifier_reply_is_clipped_before_it_reaches_the_worker` — a
  >100 KB reply yields a bounded `reasoning` that **says** it was clipped (a
  silent truncation reads as a verifier that simply stopped talking) and still
  leads with its verdict token. Fails on the old code:
  `reasoning survived at 114006 chars`.
- `an_ordinary_verdict_passes_through_the_bound_unchanged` — the bound is
  invisible in the case that matters, so a fix that quietly edited every
  verdict cannot pass.
- `clipping_a_multibyte_reply_does_not_panic` — the byte-slice trap.

## Scope

This is item 2 of the three #1787 lists, which says "each sub-fix carries its
own witness". Item 1 (a structured-output path for verifiers whose replies open
with a preamble) needs a provider-parity posture per invariant 8 and is a
larger change; item 3 (per-candidate degradation facts instead of one
once-per-run `AtomicBool`) is independent. The issue stays open for both.

`cargo test -p stella-pipeline` — 557 passed, 0 failed. Clippy `-D warnings`,
`fmt --check`, rustdoc `-D warnings`, `check-file-size`, `check-god-files` and
`check-left-behind` clean.

Refs #1787

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stella-cli-docs Ready Ready Preview Aug 7, 2026 12:12am

@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR introduces a structural character-level cap on verifier verdict reasoning at construction time, ensuring oversized verifier replies are trimmed safely and explicitly before being stored or reused, and adds focused tests to validate clipping behavior, correctness on normal replies, and multibyte safety.

Flow diagram for bounded verifier reasoning construction and usage

flowchart LR
    subgraph VerifierSide
        A[verifier_model_response text]
        B[parse_verifier_response]
        C[bounded_reasoning]
        D["Verdict.reasoning (bounded String)"]
    end

    subgraph DownstreamConsumers
        E[worker_revision_prompt]
        F[verdict_cache]
    end

    A --> B
    B -->|calls| C
    C -->|trim and clip to MAX_VERDICT_REASONING_CHARS| D
    D --> E
    D --> F
Loading

File-Level Changes

Change Details Files
Introduce a bounded_reasoning helper and MAX_VERDICT_REASONING_CHARS constant to cap Verdict::reasoning at construction time and use it in verifier response parsing.
  • Add MAX_VERDICT_REASONING_CHARS constant with documentation describing structural reasoning bound and design rationale.
  • Implement bounded_reasoning(text: &str) to trim input, clip on a character boundary at MAX_VERDICT_REASONING_CHARS, and append an explicit clipping notice when truncation occurs.
  • Update parse_verifier_response to construct Verdict::reasoning via bounded_reasoning instead of directly trimming into a String.
  • Document on Verdict::reasoning that the field is now structurally bounded by MAX_VERDICT_REASONING_CHARS.
crates/stella-pipeline/src/verify.rs
Add tests that witness the new bounding behavior, ensure ordinary verdicts are unchanged, and guard against multibyte clipping panics.
  • Import MAX_VERDICT_REASONING_CHARS into the parse tests module for use in assertions.
  • Add a_runaway_verifier_reply_is_clipped_before_it_reaches_the_worker test to assert large FAIL replies are clipped, explicitly mention clipping, and still start with the verdict token.
  • Add an_ordinary_verdict_passes_through_the_bound_unchanged test to assert small PASS replies are unaffected by the bound.
  • Add clipping_a_multibyte_reply_does_not_panic test to ensure clipping on multibyte characters remains safe and still signals clipping.
crates/stella-pipeline/src/verify/tests/parse.rs

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@macanderson
macanderson merged commit 515f387 into main Aug 7, 2026
15 checks passed
@macanderson
macanderson deleted the fix/1787-bound-reasoning branch August 7, 2026 00:12
macanderson added a commit that referenced this pull request Aug 7, 2026
… candidate in a fan-out (#1787) (#1951)

## What & why

Item 3 of #1787: verifier degradation was surfaced by a once-per-run
`AtomicBool` warning, so a best-of-N fan-out whose verifier died (or
answered off-protocol) reported one caveat for N candidates and recorded
nothing about **which** candidates were judged by the deterministic
heuristic.

Each candidate now emits one structured `ProofStep::VerdictDegraded {
candidate, reason }` fact — 1-based ordinal, `Oracle::run`'s convention
— the first time its verdict degrades. Dedup state is candidate-local
(`VerdictDegradation` rides `CandidateState`), never a shared flag reset
at candidate boundaries: candidates run concurrently
(`fanout_concurrency.rs`), and a run-wide reset would race
nondeterministically. The once-per-run transcript warning is unchanged;
the traces view renders the new step (`verdict degraded (candidate N):
reason`), and the replay fold keys it as
`proof:verdict_degraded:{candidate}`.

Two file-size-driven structural moves, both following existing
exemplars:

- `ProofStep` moves from `event.rs` (at its ceiling) to a new
`stella-protocol/src/proof.rs`, the exact `ladder.rs` pattern —
re-exported from `event` and the crate root so
`stella_protocol::ProofStep` and `event::ProofStep` never moved.
`docs/wire/` regenerated.
- `pipeline.rs` (also at its ceiling) stays under it by consolidating
the two once-per-run `AtomicBool` notices into `VerifierNotices` in
`verifier_stage.rs`, and `verifier()` now takes the crate's own `Spend`
envelope instead of loose `budget`/`total` (the `task_frame.rs` idiom).
`CandidateSlot` in `fanout_stage.rs` groups a candidate's ordinal with
its workspace so the fan-out driver stays under
`clippy::too_many_arguments` structurally rather than by `#[allow]`.

Refs #1787 — item 1 (provider-parity-aware structured verdict output)
remains open; item 2 shipped as #1932.

## The witness

- [x] This PR includes a witness test (fails on `main`, passes here)

`a_two_candidate_fanout_records_which_candidates_degraded` — the issue's
own Verify: two isolated candidates with a tokenless verifier record
facts for ordinals 1 and 2 beside exactly one transcript warning; every
post-triage scripted reply is tokenless so the assertion holds under any
completion order.
`a_candidate_degrading_on_every_round_records_one_fact` pins
per-candidate (not per-round) emission, with the provider's script
consumption asserted so the second escalation provably happened. Both
name `ProofStep::VerdictDegraded`, which does not exist on `main`. Plus
a wire round-trip pinning the serialized shape (`proof.rs`) and the
variant added to `wire_contract.rs`'s exhaustive list.

## The gate

- [x] `cargo fmt --check`
- [x] `cargo clippy --workspace --all-targets -- -D warnings`
- [x] `cargo test -p stella-protocol -p stella-pipeline -p stella-tui`
(all green; guards-fast, file-size, god-files, invariants,
module-reachability, wire-schema all green — CI runs the full workspace)
- [x] Docs updated where behavior changed (doc comments on every
moved/new item)
- [x] CLA signed
- [x] `Refs #1787` above and as a commit trailer — deliberately not
`Closes`: item 1 stays open

## Nothing left behind

Item 1 of #1787 (structured verdict output with a declared per-provider
posture) is untouched and tracked by the still-open #1787.

## Summary by Sourcery

Record verifier verdict degradation per candidate in fan-out runs while
refactoring proof and verifier wiring to keep file sizes and interfaces
within project constraints.

New Features:
- Emit a structured ProofStep::VerdictDegraded fact per candidate when
its model verdict falls back to the deterministic heuristic, keyed by
1-based candidate ordinal.
- Expose verdict degradation in traces and replay signatures so
consumers can see which candidates were judged by the heuristic and why.

Bug Fixes:
- Ensure best-of-N verifier fan-outs record which specific candidates
degraded to the deterministic heuristic rather than only surfacing a
single run-wide warning.

Enhancements:
- Split the proof-step vocabulary into a dedicated
stella-protocol::proof module and re-export it to keep existing
ProofStep paths stable while allowing the enum to grow independently of
the event envelope.
- Group once-per-run verifier caveat and fallback notices into a
VerifierNotices struct and thread candidate ordinals through fan-out and
candidate execution so degradation tracking is candidate-local and
concurrency-safe.
- Adjust the verifier interface to take a Spend envelope instead of
separate budget and total parameters, and introduce a CandidateSlot
helper to keep the fan-out driver below argument-count limits.

Documentation:
- Regenerate TypeScript wire typings and JSON schemas for proof events,
including the new verdict_degraded proof step and updated documentation
references.

Tests:
- Add pipeline tests that witness per-candidate verdict degradation in a
two-candidate fan-out and ensure repeated degradations for a single
candidate only emit one fact.
- Extend protocol wire-contract tests and add a round-trip test pinning
the serialized shape of the new VerdictDegraded proof step.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant