fix(cli,tui): stop steering refusals painting over the deck's frame - #6493
Conversation
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.
There was a problem hiding this comment.
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.
|
SCR-003 DoD check waived by the |
Reviewer's GuideThe 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 advisoriessequenceDiagram
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
Flow diagram for SteeringDropped transcript renderingflowchart 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"]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
The schema carries doc comments as descriptions, so trimming that paragraph left the committed artifacts a sentence behind the types.
The defect
The deck's bottom rows came back drawn several times over themselves after a
prompt submission, with the
runtool's command line shredded together with askills-budget notice.
One cause. Three places in
stella-cliannounced turn-time advisories with abare
eprintln!while the deck owned the terminal.ratatuipaints stdoutfrom 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.rsalready documents this failure mode and fixesit for one producer:
TerminalGuard::entertakes astella_diag::TerminalHoldthat silences the diagnostic plane for as long as a renderer owns the screen.
A bare
eprintln!never consults that hold. #643 ruled on the principlealready — 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.rsgathers them intoRecalledBlock::dropped, which ridesout through
telemetry_events()→OpeningRecall::events→ the turn's eventchannel.
command_deck/lead_turn.rsalready drains that, so the recall armneeded no call-site change.
tool_lean.rs'sreport_drops()becamedrop_advisories() -> Vec<String>.ToolAllowancecarries an optionalAdvisorySink, so each door names itsown destination. The deck sends events;
agent/turn.rsandagent/resume.rspass
stderr_advisories, which is right for a door that owns its terminal.ToolAllowance::newis unchanged, so every existing call site still compiles.They arrive as
AgentEvent::SteeringDropped { advisory }and render as a⚠ steeringtranscript row.Why the transcript and not
Inbound::Notice.command_deck/steering.rsalready 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, raisecontext.steering.max_tokens. AndNoticeState::dismissis permanent for the session by design, so per-turnreporting 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: eachbecomes 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 everyrenderer was awkward —
textline::line()returns oneEventLine.Two fixes riding along
Both were required to land the above, per fix over file.
kind.rs,recall.rsandtextline.rspast 1500 lines. All three are trimmed in place rather thanbaselined;
textline::steering_droppedgetstextline/steering.rs, besidethe
gateandmemorymodules it matches.kind.rslost a paragraphdescribing a
FileChangeproducer that no longer exists.check-prosecountedConsumerPosture::RecordedOnly { issue: "#1234" }as an issue number inprose. 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 theperson 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 theregenerated
docs/wire/and proves the vocabulary exhaustive againstKNOWN_TYPE_TAGS.cargo test -p stella-tui --lib steering— 8 passed, including three newrender witnesses.
cargo test -p stella-cli --bins tool_lean— 6 passed;tool_stack— 13passed;
steering_selection— 16 passed.make guards-fast— all green,check-file-sizeandcheck-proseincluded.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 withno 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 inrecall.rs, on theContextRecallPort::recallpath. That trait returns
Recall, which has nowhere to carry an advisory, andI 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:
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores: