fix(dashboard): keep steers pending on an empty consumed echo (#8481) - #8826
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix that converges the last holdout call site onto the fail-closed rule its siblings already use, deleting the selector-less escape hatch entirely. The one cost — a visible duplicate run on a backend that injected the steer but echoed empty — is named at the call site, bounded (the requeued card runs as a plain turn, so no loop), and correctly traded against silent loss. The requeue dependency the old deferral rationale doubted is verified wired into [DESIGN-REVIEWED] 201cab2 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified. The parameter First-Principles-Verdict: PASS A reported silent-loss defect (#8481) fixed by deleting the evidence-free sweep concept outright — every item subtracts surface or pins the new invariant. What this change shipsIntent: stop a mid-turn correction from being silently dropped when the backend echoes an empty consumed frame — a FIX.
This is the shape this lane exists to endorse: the fix removes the decision that caused the defect (an opt-in that treated no evidence as proof), deletes the opt-in with zero surviving consumers, and leaves no unfixed code siblings — all 3 call sites of Subtractions
[FIRST-PRINCIPLES-REVIEWED] 201cab2 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
|
[SUPERSEDED — historical record; disposition marker removed] Earlier accept-and-defer response to the First Principles CONCERNS verdict on head 242d0a6. Superseded by the implementation on head 201cab2: the |
417b7ef to
88aca00
Compare
88aca00 to
201cab2
Compare
|
[SUPERSEDED — historical record; disposition marker removed] Implementation notice for the First Principles BLOCK, whose marker carried a mistyped head SHA and never matched. Superseded by the corrected disposition comment on head 201cab2. |
|
Problem / Motivation
A mid-turn steer (the user's correction typed while a turn is running) could be silently lost. When the ACP backend sends an
EVENT_STEER_CONSUMEDecho with empty text — a legitimate case persession_handle.py's emitter andsteer_settle's own docstring (older backend, dropped echo) — the main chat's_settle_consumed_steerscalledsettle_consumed_steers(..., settle_all_on_empty=True), which cleared all pending steers with no evidence. That suppressed_requeue_unconsumed_steers, so the correction ran nowhere while its transcript row claimed delivery.Why it matters
The user's correction is exactly the message they care most about mid-turn. Losing it silently — with the UI asserting it was delivered — is the worst failure shape: unrecoverable and invisible. Every sibling call site (the
_refusal_noticessettle in the same event branch, and the/sidesidecar) already settles nothing on an empty echo for this precise reason; the main chat was the sole holdout.What changed (motivation → approach → change)
settle_all_on_empty=Trueat the main chat's_settle_consumed_steerscall treats an evidence-free frame as proof of consumption. The stated reason for deferring the flip ("the requeue mechanism does not exercise this path") is stale:_requeue_unconsumed_steersis wired into_run_chat's outerfinallyand runs on every turn-exit path (normal return, exception, cancellation).settle_all_on_emptyopt-in outright — after the flip it had zero production selectors ofTrue(both review lanes converged on this), so the empty-echo rule is now unconditional insettle_consumed_steers: an empty echo settles nothing, entries stay pending, and the turn-end requeue degrades them to queue cards. The accepted cost, named at the call site: on a backend that injected the steer but echoed no text, the steer runs again — usually immediately via the turn-exit drain, but visibly as its own turn (and holds as a cancellable card when the drain is withheld, e.g. sign-in required), unlike a silent loss.chat_runner.py,steer_settle.py,state.py, andchat_delivery.pythat described the sweep as live behaviour were restated as present-tense invariants.Tests
test_empty_snapshot_settles_nothing(rewritten fromtest_empty_snapshot_falls_back_to_settling_all, which pinned the bug): an empty echo leaves_pending_steersuntouched.test_an_empty_echo_never_claims_consumption(rewritten as the regression test): empty echo → entry stays pending → no row promoted →_requeue_unconsumed_steersemits arequeuedcard carrying the steer.test_an_empty_echo_during_the_rpc_persists_as_written: updated — the entry now survives the empty echo, exercising the still-registered →writtenpath.test_settle_delegates_to_the_shared_rules: the delegation assert flipped tosettle_all_on_empty is False(load-bearing).Manual verification
N/A — unit coverage sufficient: the settle and requeue paths are exercised end-to-end through
steer_into_running_turn+_settle_consumed_steers+_requeue_unconsumed_steersin the tests above, and the full backend suite's failure set is byte-identical to pristinemainon this host (environmental only).Related Issues
Closes #8481. Context: #7997 (merged prerequisite; this flip was deliberately held until it landed), #7998 (distinct no-echo-arrives case, already requeues correctly — not changed here).
Pattern harvest
Rule candidate: review-prompt
Pattern: a boolean escape-hatch parameter whose only selector is "long-standing behaviour" — the deferral rationale goes stale when the blocking mechanism lands; audit such flags when their named prerequisite merges.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)