Skip to content

fix(cli,tui): stop steering refusals painting over the deck's frame - #6493

Merged
macanderson merged 7 commits into
mainfrom
fix/deck-stderr-notices
Sep 11, 2026
Merged

fix(cli,tui): stop steering refusals painting over the deck's frame#6493
macanderson merged 7 commits into
mainfrom
fix/deck-stderr-notices

Conversation

@macanderson

@macanderson macanderson commented Sep 10, 2026

Copy link
Copy Markdown
Owner

The defect

The deck's bottom rows came back drawn several times over themselves after a
prompt submission, with the run tool's command line shredded together with a
skills-budget notice.

One cause. Three places in stella-cli announced turn-time advisories with a
bare eprintln! while the deck owned the terminal. ratatui paints stdout
from a buffer it assumes nothing else writes to, so those bytes land inside the
drawn frame — and a write at the bottom row scrolls the terminal, which puts
every cell the renderer believes it already painted one row off. The diff then
repaints nothing, and the stale halves stay on screen. That is why the damage
looked like several bars at once rather than one garbled line.

crates/stella-tui/src/term.rs already documents this failure mode and fixes
it for one producer: TerminalGuard::enter takes a stella_diag::TerminalHold
that silences the diagnostic plane for as long as a renderer owns the screen.
A bare eprintln! never consults that hold. #643 ruled on the principle
already — a library must not own the process's stderr — so this is a
regression of a settled rule rather than an open question.

The fix

The three deck-reachable reporters hand their lines back instead of printing:

  • memory/recall.rs gathers them into RecalledBlock::dropped, which rides
    out through telemetry_events()OpeningRecall::events → the turn's event
    channel. command_deck/lead_turn.rs already drains that, so the recall arm
    needed no call-site change.
  • tool_lean.rs's report_drops() became drop_advisories() -> Vec<String>.
  • ToolAllowance carries an optional AdvisorySink, so each door names its
    own destination. The deck sends events; agent/turn.rs and agent/resume.rs
    pass stderr_advisories, which is right for a door that owns its terminal.
    ToolAllowance::new is unchanged, so every existing call site still compiles.

They arrive as AgentEvent::SteeringDropped { advisory } and render as a
⚠ steering transcript row.

Why the transcript and not Inbound::Notice. command_deck/steering.rs
already draws the line: a notice is for something that "names no remedy to
scroll back to". Every one of these advisories names one — raise
skills.max_skills
, raise context.steering.max_tokens. And
NoticeState::dismiss is permanent for the session by design, so per-turn
reporting routed there would go silent the first time the user pressed a key.

Why one event per refusal. SkillInjected's own docs give the rule: each
becomes one transcript row, and a list would make the renderer split what the
emitter had already separated. My first pass carried a Vec<String> and every
renderer was awkward — textline::line() returns one EventLine.

Two fixes riding along

Both were required to land the above, per fix over file.

  1. The size ratchet. The new event's docs pushed kind.rs, recall.rs and
    textline.rs past 1500 lines. All three are trimmed in place rather than
    baselined; textline::steering_dropped gets textline/steering.rs, beside
    the gate and memory modules it matches. kind.rs lost a paragraph
    describing a FileChange producer that no longer exists.
  2. A guard defect. check-prose counted
    ConsumerPosture::RecordedOnly { issue: "#1234" } as an issue number in
    prose. That audit requires a non-empty value there, so no new event of
    that posture could be added without raising the file's count with nothing a
    reader could delete. The exemption is the field form alone — a number in a
    sentence still fails.

The skills-section advisory also lost its trailing (#3243), which told the
person reading it nothing; the assertion that pinned it now checks the sentence.

Evidence

Everything below was run in this worktree.

  • cargo test -p stella-protocol --test wire_contract — 8 passed. Covers the
    regenerated docs/wire/ and proves the vocabulary exhaustive against
    KNOWN_TYPE_TAGS.
  • cargo test -p stella-tui --lib steering — 8 passed, including three new
    render witnesses.
  • cargo test -p stella-cli --bins tool_lean — 6 passed; tool_stack — 13
    passed; steering_selection — 16 passed.
  • make guards-fast — all green, check-file-size and check-prose included.
  • cargo fmt --check — clean.

Six new tests. The two that pin the defect are
a_composed_stack_reports_its_cuts_to_the_named_sink_alone (an allowance with
no sink reaches no terminal, and the same cut reaches a sink once one is named)
and a_refused_candidate_leaves_the_block_as_its_own_event.

Known limit

One eprintln! remains in recall.rs, on the ContextRecallPort::recall
path. That trait returns Recall, which has nowhere to carry an advisory, and
I found no installer for the port in this tree. Widening a protocol trait for a
caller that may not exist is not something to do on the way past, so it is
named here rather than quietly changed.

The full workspace build and test suite are CI's — nothing here ran them.

Summary by Sourcery

Route steering budget refusals through events and transcript rendering instead of writing into the interactive terminal frame.

New Features:

  • Add a steering-dropped event that reports per-turn budget refusals and their remedies across the protocol, CLI, and TUI.

Bug Fixes:

  • Prevent steering advisories from writing directly to stderr while the interactive deck owns the terminal, avoiding corrupted frames and scrolling.

Enhancements:

  • Route advisories to door-specific sinks, display them as searchable transcript warning rows, and retain stderr output for terminal-owning modes.
  • Simplify stale event documentation and improve prose-audit handling for required issue fields.

Documentation:

  • Document the new steering-dropped diagnostic and update generated wire schemas and type declarations.

Tests:

  • Add coverage for advisory collection, sink routing, event propagation, transcript rendering, wire samples, and silent no-refusal cases.

Chores:

  • Remove an uninformative issue reference from steering advice and reorganize text-line helpers to stay within file-size limits.

Three reporters in stella-cli wrote turn-time advisories with eprintln!
while the deck owned the terminal. Under ratatui that lands inside the
drawn frame and scrolls the screen out from under the renderer's diff,
which is what shredded the status bar after a prompt submission.

They now travel as an AgentEvent::SteeringDropped to the turn's event
channel and onto the transcript, beside the turn that paid for them.
Headless doors keep stderr, which is theirs to write.
…ript

One AgentEvent::SteeringDropped per refusal, on SkillInjected's rule that
each event becomes one row. The deck draws it as a WARNING note; the
plain and exported transcripts take the same wording from
textline::steering_dropped, so the two surfaces cannot drift on a
sentence whose whole point is naming the right remedy.
…advisories

Regenerated docs/wire/ for AgentEvent::SteeringDropped and moved its
tags.rs entry beside SteeringWithheld, where kind.rs declares it — the
committed schema's order is proved against the declaration order.
…rame

Five tests across the two planes the fix touches: the tool allowance
reports to the sink its door named and to nowhere else, a refused recall
candidate leaves the block as its own event, a silent turn stays silent,
and the deck row splits headline from remedy at the em dash.
…ting a required issue field as prose

The new event's docs pushed kind.rs, recall.rs and textline.rs past the
1500-line guard. Each is trimmed in place rather than baselined, except
textline's new steering_dropped, which gets textline/steering.rs beside
the gate and memory modules it matches.

check-prose counted `ConsumerPosture::RecordedOnly { issue: "#1234" }`
as an issue number in prose. The audit requires that value, so no new
event of that posture could be added without raising the file's count
with nothing a reader could delete. The exemption is the value form
alone; a number in a sentence still fails.

Also drops the bare issue number from the skills-section advisory, which
told the person reading it nothing, and updates the assertion that
pinned it.

@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've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

SCR-003 DoD check waived by the closes-nothing label — this PR closes no issue by design.

@sourcery-ai

sourcery-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The PR eliminates deck corruption caused by steering advisories writing directly to stderr by making refusal reporting caller-routed, introducing a per-refusal SteeringDropped event, and rendering actionable warnings in the transcript while preserving stderr output for terminal-owning modes. It also updates protocol/wire/diagnostic surfaces, adds regression coverage, and adjusts size/prose guards required by the implementation.

Sequence diagram for caller-routed steering refusal advisories

sequenceDiagram
    participant Turn as Turn runner
    participant Allowance as ToolAllowance
    participant Lean as LeanToolSet
    participant Sink as AdvisorySink
    participant Deck as Command deck
    participant Stderr as stderr

    Turn->>Allowance: reporting(sink)
    Allowance->>Lean: drop_advisories()
    Lean-->>Allowance: Vec<String>
    alt deck-owned terminal
        Allowance->>Deck: SteeringDropped per refusal
        Deck->>Deck: transcript row
    else terminal-owning mode
        Allowance->>Sink: stderr_advisories(advisories)
        Sink->>Stderr: eprintln! per advisory
    end
Loading

Flow diagram for SteeringDropped transcript rendering

flowchart LR
    Recall["Recall and steering budget checks"] --> Event["AgentEvent::SteeringDropped"]
    Event --> Model["SessionModel transcript"]
    Model --> Entry["TranscriptEntry::SteeringDropped"]
    Entry --> Line["textline::steering_dropped"]
    Line --> Warning["⚠ steering warning with remedy"]
Loading

File-Level Changes

Change Details Files
Route steering-budget refusals through caller-owned sinks instead of writing directly to stderr.
  • Collect recall refusals in RecalledBlock and emit them through the turn event stream.
  • Return tool refusal advisories from LeanToolSet and add optional per-allowance sinks.
  • Use transcript events for deck turns and stderr only for terminal-owning non-interactive doors.
  • Add coverage proving silent defaults and sink-specific delivery.
crates/stella-cli/src/agent/tool_stack.rs
crates/stella-cli/src/agent/turn.rs
crates/stella-cli/src/agent/resume.rs
crates/stella-cli/src/command_deck/lead_turn.rs
crates/stella-cli/src/memory/recall.rs
crates/stella-cli/src/tool_lean.rs
crates/stella-cli/src/memory/tests/steering_selection.rs
Introduce and render a per-refusal SteeringDropped event across protocol, TUI, diagnostics, and wire contracts.
  • Add the event payload, task tagging, consumer posture, diagnostic bridge record, and generated wire samples/types.
  • Fold events into transcript rows with warning styling, searchable advisory text, headline/detail splitting, and trace classification.
  • Render one ⚠ steering row per refusal with the candidate and actionable budget remedy.
  • Add protocol and TUI rendering witnesses.
crates/stella-protocol/src/event/kind.rs
crates/stella-protocol/src/event/tags.rs
crates/stella-protocol/src/event/task_tag.rs
crates/stella-protocol/tests/wire_contract/samples.rs
crates/stella-tui/src/deck/classify.rs
crates/stella-tui/src/model.rs
crates/stella-tui/src/model/entry.rs
crates/stella-tui/src/render/entry.rs
crates/stella-tui/src/render/tests/steering.rs
crates/stella-tui/src/textline.rs
crates/stella-tui/src/textline/steering.rs
crates/stella-tui/src/transcript_build.rs
crates/stella-tui/src/transcript_nav.rs
docs/reference/diagnostics.md
docs/wire/agentevent.d.ts
docs/wire/agentevent.schema.json
docs/wire/serveframe.d.ts
docs/wire/serveframe.schema.json
Keep repository guards passing while documenting and testing the new advisory vocabulary.
  • Trim oversized documentation modules and extract steering text formatting into its own module.
  • Remove an unhelpful issue suffix from advisory prose and update its assertion.
  • Allow required issue field values in check-prose without exempting issue numbers in normal prose.
crates/stella-protocol/src/event/kind.rs
crates/stella-cli/src/memory/recall.rs
crates/stella-tui/src/textline.rs
crates/stella-cli/src/memory/tests/steering_selection.rs
scripts/check-prose.py

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 added the closes-nothing Substantial change that closes no issue by design (SCR-003) label Sep 10, 2026
The schema carries doc comments as descriptions, so trimming that
paragraph left the committed artifacts a sentence behind the types.
@macanderson
macanderson merged commit 40e3aaa into main Sep 11, 2026
26 of 27 checks passed
@macanderson
macanderson deleted the fix/deck-stderr-notices branch September 11, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

closes-nothing Substantial change that closes no issue by design (SCR-003)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant