Skip to content

fix(stella-core,stella-pipeline): close the sub-agent bracket on cancellation (#1954) - #1978

Merged
macanderson merged 2 commits into
mainfrom
fix/1954-cancel-bracket
Aug 7, 2026
Merged

fix(stella-core,stella-pipeline): close the sub-agent bracket on cancellation (#1954)#1978
macanderson merged 2 commits into
mainfrom
fix/1954-cancel-bracket

Conversation

@macanderson

@macanderson macanderson commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What

A caller that drops a sub-agent future mid-flight — a latency ceiling, a hard cancel — left Started open forever. Every ceiling-bearing caller therefore had to forge its own Finished, and could only guess steps: 0, because the committed-call count lived inside the dropped turn. The pre-plan research stage (#1778, merged as 1bdf2da) was the first pipeline caller to hit this and forged exactly such a bracket.

CancelBracket moves the obligation into the primitive (crates/stella-core/src/subagent.rs) — design 2 of the two the issue proposed, so the next caller with a ceiling inherits the fix instead of repeating the bug. This is the same argument that moved the goal verifier onto Engine::run_sub_agent.

Drop order does the sequencing, which is what makes the numbers honest: the in-flight turn future is declared after the guard, so it drops first — the engine's own cancel guard has already emitted the abandoned call's UsageIncomplete { Cancelled } envelope and SettleChildOnDrop has already folded the money back by the time the bracket closes. The bracket therefore reports only what was committed; the in-flight call's usage rides its own envelope and is never guessed at here.

The two committed numbers travel as one CommittedTally type rather than two loose Arcs — a bracket reporting a step count without the cost that produced it would be half an answer, and bundling them also keeps run_child_turn under the argument cap without an #[allow].

research_stage.rs drops its forged bracket accordingly.

Witness

Both fail on the old code and pass with the change — verified by checking out origin/main's copies of the two production files and re-running:

  • subagent::tests::a_cancelled_child_closes_its_bracket_with_committed_steps_and_cost — on old code the event stream contains Started with no Finished at all. (The dump also shows UsageIncomplete { Cancelled } already present on old code, which is the evidence that only the bracket needed owning.)
  • pipeline::tests::research::a_child_past_the_ceiling_closes_its_bracket_with_committed_steps — the issue's verbatim scenario: balanced bracket, UsageIncomplete with reason cancelled, and Finished.steps equal to the committed StepUsage count. Old code fails on "Finished.steps is the committed StepUsage count, not a forged zero".

No protocol change was needed — UsageIncompleteReason::Cancelled already exists, so there is no wire-schema regeneration.

Verification

main is currently red (see #1971), so these were run on a local merge of this branch with unbreak-main-pipeline-tests:

  • cargo test -p stella-core -p stella-pipeline — 0 failures
  • cargo clippy -p stella-core -p stella-pipeline --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • scripts/check-file-size.sh on this branch alone — OK, none grew

This PR's CI will stay red until #1971 lands, for reasons that have nothing to do with this diff.

Closes #1954

Summary by Sourcery

Ensure sub-agent cancellations close their Started/Finished bracket with accurate committed steps and cost, and rely on the core primitive rather than callers to emit synthetic finishes.

Bug Fixes:

  • Preserve a balanced Started/Finished sub-agent bracket on mid-flight cancellation, reporting the true committed step count and cost instead of leaving it open or forging zeros.

Enhancements:

  • Track committed sub-agent step count and cost via a shared tally that survives cancellation, and introduce a cancel guard to emit the final Finished event when the sub-agent future is dropped.

Tests:

  • Add core and pipeline tests that deterministically cancel hanging sub-agents to verify brackets close correctly, usage incomplete events are emitted, and budgets settle the committed spend.

…ellation

A caller that drops a sub-agent future mid-flight — a latency ceiling, a
hard cancel — left `Started` open forever, so every ceiling-bearing caller
had to forge its own `Finished` and could only guess `steps: 0`: the
committed-call count lived inside the dropped turn.

`CancelBracket` moves that obligation into the primitive. It is armed
between `Started` and the normal `Finished`, and drop order does the
sequencing — the turn future drops first, so the engine's cancel guard has
already emitted the abandoned call's `UsageIncomplete { Cancelled }`
envelope and `SettleChildOnDrop` has folded the money back before the
bracket closes. It therefore reports only committed steps and cost, tallied
by `child_sender` as each `StepUsage` crosses the boundary.

The research stage drops its forged bracket accordingly; the next caller
with a ceiling inherits the fix instead of repeating the bug.

Closes #1954
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Preview Aug 7, 2026 2:24am

@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

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a cancellation-aware sub-agent completion mechanism in stella-core so that dropped sub-agent futures still emit a truthful Finished bracket with committed steps and cost, replaces the pipeline’s locally-forged Finished on timeout with this primitive behavior, and adds focused tests plus helpers to witness the fix in both core and pipeline research stages.

Sequence diagram for sub-agent cancellation with CancelBracket

sequenceDiagram
    actor Pipeline
    participant TokioTimeout as tokio_time_timeout
    participant Engine as Engine
    participant CancelBracket as CancelBracket
    participant CommittedTally as CommittedTally
    participant Events as EventSender

    Pipeline->>TokioTimeout: timeout(ceiling, engine.run_sub_agent_with_sender(...))

    alt normal_completion
        TokioTimeout->>Engine: run_sub_agent_with_sender(host, spec, carve, budget, events)
        Engine->>CommittedTally: new CommittedTally::default()
        Engine->>CancelBracket: create CancelBracket{ events, agent_id, tally, armed=true }
        Engine->>Engine: run_child_turn(host, spec, carve, budget, events, &tally)
        Engine->>Events: send(AgentEvent::SubAgent Finished{ steps: tally.steps(), cost_usd: tally.cost_usd() })
        Engine->>CancelBracket: armed = false
        TokioTimeout-->>Pipeline: Ok(SubAgentOutcome)
    else timeout_or_cancellation
        TokioTimeout-->>Pipeline: Err(_elapsed)
        Note over Engine,CancelBracket: caller drops sub-agent future
        CancelBracket-->>Events: Drop::drop() -> send(AgentEvent::SubAgent Finished{ status: Incomplete, steps: tally.steps(), cost_usd: tally.cost_usd() })
    end
Loading

File-Level Changes

Change Details Files
Introduce CommittedTally and CancelBracket in the sub-agent engine so cancelled children still close their Started/Finished bracket with accurate committed steps and cost.
  • Add CommittedTally struct to track committed step count and USD cost outside the child turn, updated from StepUsage events.
  • Change child_sender to update CommittedTally on StepUsage instead of only counting steps, and pass a shared tally Arc into run_child_turn.
  • Wrap run_child_turn in a CancelBracket guard in Engine::run_sub_agent, arming it between Started and normal Finished and disarming it on successful completion.
  • Implement Drop for CancelBracket to emit a SubAgentPhase::Finished with status Incomplete, committed steps, cost, and a cancellation reason when a caller drops the sub-agent future mid-flight.
  • Update run_child_turn to read the step count from CommittedTally instead of a local AtomicUsize, and propagate the tally through the pipeline.
crates/stella-core/src/subagent.rs
Add deterministic cancellation tests for sub-agent cancellation semantics in stella-core.
  • Introduce HangAfterScript provider test double that serves a scripted sequence then hangs and signals when hang is reached.
  • Add test a_cancelled_child_closes_its_bracket_with_committed_steps_and_cost to assert a balanced Started/Finished bracket, committed steps and cost, presence of UsageIncomplete::Cancelled, and correct budget settlement when the sub-agent future is dropped.
crates/stella-core/src/subagent/tests.rs
Make the pipeline research stage rely on the primitive CancelBracket behavior instead of forging Finished on research timeouts, and add an integration-style witness test.
  • Add HangScript enum, HangTailProvider provider, and OneHangProvider resolver test doubles in research tests to simulate a hanging research child while allowing later scripted calls.
  • Add test a_child_past_the_ceiling_closes_its_bracket_with_committed_steps to assert pipeline-level behavior: balanced bracket, UsageIncomplete::Cancelled, and Finished.steps equal to committed StepUsage count when a research child exceeds its latency ceiling.
  • Simplify research_stage timeout handling to treat Err(timeout) the same as non-usable outcomes (Ok(_)), removing manual emission of a forged Finished with steps: 0 and relying on sub-agent CancelBracket to close the bracket.
  • Adjust research_stage comments to document that the primitive now owns the cancel bracket and UsageIncomplete emission.
crates/stella-pipeline/src/pipeline/tests/research.rs
crates/stella-pipeline/src/pipeline/research_stage.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#1954 Ensure that sub-agent futures dropped mid-call (e.g., via latency ceilings or hard cancels) produce a balanced Started/Finished bracket whose Finished carries the true committed step count and cost, without requiring callers to forge a Finished event.
#1954 Ensure that when a research-stage child exceeds its latency ceiling, the pipeline still completes while producing: (a) a balanced sub-agent Started/Finished bracket with Finished.steps equal to the committed StepUsage count, and (b) a UsageIncomplete envelope with reason Cancelled for the abandoned in-flight call.

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 ab5a239 into main Aug 7, 2026
15 checks passed
@macanderson
macanderson deleted the fix/1954-cancel-bracket branch August 7, 2026 02:32
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.

Engine-turn sub-agents cancelled mid-call leave no UsageIncomplete envelope (research stage's latency ceiling is the first pipeline caller)

1 participant