Repository navigation
feat(chat): take back a steer the running turn has not read yet - #23388
unstablebrainiac wants to merge 1 commit into
Conversation
Design Review (Opus 5.5) — ✅ PASSDesign-level review of Design-Verdict: PASS Nothing blocks: the take-back is opt-in per harness, the outcome comes from what the turn reports rather than the clear's answer, and screenshots and recordings of this revision are attached. Notes
[DESIGN-REVIEWED] 5d0b421 Description read[DESCRIPTION-READ] 294c09827cd48f583dfb2addc4b9b33b2e72cf146c2d448180e415f15f6b03ea That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks. One note: the description's compatibility claim about which harnesses get escaped steer text does not match the diff. Notes
What this change shipsInventory (8 items) — 7 justifiedIntent: let a person take back a steer the running turn has not read yet, by Unsend, Edit, or Queue instead. ADDITION.
[FIRST-PRINCIPLES-REVIEWED] 5d0b421 Description read[DESCRIPTION-READ] 1da02eb3232d0acf83953c73e9b4b5cf8c81b1bb3c869bf119a78249e07add9f That sha256 names the PR description this verdict read. Recompute it with |
UX Review (Opus 5.5) — ✅ PASSUX-level review of UX-Verdict: PASS Nothing blocks. Screenshots cover every new control and state (dark and light), and the reader read Queue instead, Edit and Unsend correctly. A recording is supplied for the bubble-to-note collapse. Notes
[UX-REVIEWED] 5d0b421 Description read[DESCRIPTION-READ] 1b0fac04d5644c0208d07651d11967a912e8e77c2d5020620e5652ede9e37dad That sha256 names everything this verdict read: the PR description, plus the 6 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
Opus 5.5 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 6.1 Review — 🔴 changes requested (blocking)GPT 6.1 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- website/src/hooks/useSteerRetract.ts:139 -- Different-chat locks allow recovery records to overwrite each other
BLOCKING -- website/src/pages/chat/page/composerDrafts.ts:61 -- Partial persistence duplicates restored text after reload
[BLOCK-MERGE] 5d0b421 Adjudication (Opus 5.5) — is blocking on each finding proportionate?Rulings: F2 is downgraded so it no longer blocks. F1 stays blocking because it is fenced, but I flagged it with a draft override rationale. Both rest on code I opened in this run. F2 — LOW. The record only fails to clear when the text write succeeds and the file-draft write fails ( F1 (fenced) — Two tabs would each have to read and then write the shared store at the same moment. Each tab's read and write run in one synchronous step ( 🏷️ Fenced finding(s) machine-flagged as likely edge caseThe security fence keeps these findings blocking regardless of adjudication; the only clearance path is a human override. The annotate-only pass judged the condition combination each one requires extreme, and pre-drafted the override rationale below. A repository writer must independently verify a rationale before posting it — it is machine-authored, and a wrong override on a security-class finding ships exactly the class the fence exists to stop.
Replace False positive or not applicable? A repository writer can comment: |
90c0d5e to
b876cf4
Compare
b876cf4 to
00c19ba
Compare
00c19ba to
b3fb295
Compare
b3fb295 to
478c7ac
Compare
478c7ac to
c6010cf
Compare
3edfb8d to
3a5cf40
Compare
3a5cf40 to
5d569f5
Compare
5d569f5 to
20316a1
Compare
20316a1 to
53202be
Compare
53202be to
09cc563
Compare
A pending steer that can be taken back now shows Unsend, Edit and Queue instead beside "Steering…". kiro-cli 2.33's `_session/steer/clear` drops the steers it still holds and reports them with AgentExecutionUserMessageCleared, which the turn yields as EVENT_STEER_DROPPED. The outcome is read from what the turn reports after the clear, never from the clear's own answer: a reported drop takes the steer back (the row becomes `steerState: retracted`), a consumption echo first means the turn read it, and a turn that ends first never took it. Edit hands the text back to the composer, Queue instead puts it in the queue. The clear drops every pending steer, so the others are sent again in order, and a steer sent while a clear is in flight waits for its answer. The capability is opt-in per harness (ACP_BACKENDS_STEER_RETRACT, kiro-cli only, on two live captures); KAS was measured and left out because its clear names steers its turn already read. Closes #18190 Closes #16300
09cc563 to
5d0b421
Compare
Problem / Motivation
Goal: A person can take back a steer the running turn has not read yet: Unsend it, Edit it, or send it as a queued message instead.
A message sent while the agent works becomes a steer, and it waits until the turn reaches a point where it can read it. Until now it could not be changed in that window. The only ways out were a second steer that contradicts the first, or stopping the whole turn.
Why it matters
Steers are sent fast: a typo, a half-thought, or Enter pressed in Steer mode when Queue was meant. A wrong steer redirects work that was on track. Taking it back before the agent reads it costs the turn nothing.
Not a goal
What changed (motivation → approach → change)
kiro-cli 2.33 can drop the steers it still holds.
_session/steer/clearanswers{cleared: true}, then sendsAgentExecutionUserMessageClearednaming exactly the steers it dropped. Two live captures intest/fixtures/acp_frames/kiro/pin the contract: a dropped steer never reaches the model, a steer the turn already read is never named, and a clear with nothing pending reports nothing.A pending steer that can be taken back shows Queue instead beside "Steering…", with Edit and Unsend one menu deep. The two controls sit apart from the status and its explainer, and Queue instead is the one-click choice because it is the action #18190 asks for: the message still runs, after this turn. The click arms the take-back and writes the clear. The outcome comes from what the turn reports next, never from the clear's answer, which is the same either way:
The clear drops every steer kiro-cli holds, so the other pending steers are sent again in their order. A steer sent while a clear is in flight waits for the clear's answer, so the clear cannot drop it, and it can reach the turn before the steers being sent again. Only one take-back runs at a time per chat, even across browser tabs. Crew escapes
<user_message>inside a steer's text, so kiro-cli's echo of what the turn read splits one way; a steer of unknown delivery is never taken back or sent again.The capability is opt-in per harness (
ACP_BACKENDS_STEER_RETRACT, kiro-cli only, read throughsupports_steer_retract). The gateway marks a steer row as retractable only when the composer's own human sent it on such a harness, so the dashboard draws the controls from the row alone. Each new turn first withdraws the mark from rows a hard stop or a restart left. A taken-back row keeps its place withsteerState: retractedbut loses its text, so nothing that later reads the transcript as conversation (a cold start's replay, memory, a summary, a fork) presents it as something the person said; a cold start's replay, an interrupted or stopped turn's restore and Regenerate step past the row to the request before it, and the pinned prompt banner never shows it.The note cannot quote the message, whose text is removed on purpose; the bubble collapses into it where it stood, and the text carries on where it went. The Edit and Queue instead notes are buttons to the message box, with the queue stack just above it. Edit also puts the cursor on the restored text. A row read back already taken back, or with reduced motion, shows the note without the collapse. The page keeps each chat's taken-back text until the row settles, through an unanswered request, a chat switch or a reload, and puts it back only into that chat's message box, even when the row is outside the loaded history. A request that fails says so under the progress line ("Couldn't confirm it was taken back. This updates when the agent reaches it.") while the row waits for what the turn reports.
src/kiro_crew/acp/,src/kiro_crew/agent_sdk/,src/kiro_crew/acp_backends.py,src/kiro_crew/providers/clear_pending_steerson the session handle, the dispatch yieldingEVENT_STEER_DROPPED,wrap_user_messagefor every steer write, theACP_BACKENDS_STEER_RETRACTtable (re-exported, classified off-card), and safeLLMProviderdefaultssrc/kiro_crew/dashboard/,src/kiro_crew/context_assembly/,src/kiro_crew/context.py,src/kiro_crew/slack/steer_retract.py, the route and its coded refusals, the retractable row mark, the settle and teardown hooks, the settle comparing each steer's escaped text (steer_settle.py); a cold start's replay, an interrupted or stopped turn's restore and Regenerate step past a taken-back steer; the stopped-turn restore reads whole rows, in a worker thread on the dashboard and in Slackwebsite/src/pages/,website/src/hooks/,website/src/api/,website/src/store/,website/src/utils/ErrorNoticeinUserMessage.tsx(refusals and a failed request);useSteerRetract.tsand its per-chat text, kept until the message box's own draft write is known to have stuck (slotDraftStore.saveand the composer'ssaveDraftsreport whether a write stuck);retractSteer;retractedas a settled state; the pinned prompt skipping itwebsite/src/i18n/scripts/,docs/check_harness_parity.pyrequires the new set's live capture; the steer contract inlearn-cron-dashboard.md, the H6 row, the onboarding table, a feature-map rowBackwards compatibility
Compatible: the change adds a route, a fourth steer state and optional row meta. Every existing steer state, route and row keeps its shape, and a steer on any harness without the request is drawn exactly as before. On kiro-cli and KAS, a steer whose text holds
<user_message>or</user_message>reaches the agent with those tags written as<user_message>and</user_message>, so it cannot close the wrapper the agent reads; the other harnesses receive the text as typed, and the chat shows it as typed.Removes nothing: no key, route, state or reader goes away;
steerState: retractedis written only by a take-back.Tests
test/test_steer_retract.py: the retractable mark (composer steer on a capable harness only, not a peer's); the route's refusals and their codes (unknown or non-string action, no longer pending, busy while a steer or another take-back is in flight, an app, whose refusal writes one denied SEL row, a refused clear withdrawing the offer, and a clear refused after the turn already settled the take-back, by ending or by reading the steer, answering accepted, withdrawing nothing and leaving the next take-back free); Queue instead keeping the audience it had when the person asked, whether kiro-cli reports the drop or the turn ends first, so a mirror added in between makes the drain drop the queued entry, and one with nothing changed draining as usual; a reported drop taking back Unsend (row text cleared, on the row and the frame), Edit and Queue instead (with attachments), and resending the other pending steers in order but not one sent after the clear; the turn reading the steer first; a read steer whose text holds</user_message>that can be neither taken back nor sent again; the turn ending first (Unsend not requeued, Queue instead left to the requeue); a new steer held at the clear's gate until its answer, under bounded waits, and handed back to the caller's queue, with no row written, when its turn ends or is hard-stopped while it waits; a take-back from an earlier turn blocking nothing; a row a stopped turn leftwrittennever taking back a newer steer with the same text; the session provider passing the offer and the clear through to its handle; the turn's event loop handing a drop to the take-back; a hard stop releasing an armed Unsend, Edit or Queue instead without touching its row, since the runner may not yet have processed kiro-cli's report that the turn read the steer (a late report leaves the row as it was), discarding it with the other pending steers, and releasing a steer held for the clear; the next turn withdrawing the offer a hard stop or a restart left on a row, but not while a steer is pending or either half of a take-back is armed; a steer a recovery or an ambiguous write marked possibly delivered: an armed take-back on it ends, a clear's drop neither takes it back nor resends it, and the turn end leaves it to the requeue, which runs it with the possibly-delivered note; two steers whose escaped keys collide, with an echo of one: both are possibly delivered, so neither can be taken back, a take-back armed on one ends, a clear's drop resends neither, and the turn end requeues both with the note.test/test_steer_settle.py:escape_user_message_markupandwrap_user_messageon text with either tag, neither, and the escape text itself; a steer holding either tag settles from its escaped envelope, two identical ones settle one at a time, one the echo does not carry stays pending, and a steer quoting another's envelope settles only itself; steers whose text copies other steers' envelopes, one or two of them, get echoes that name exactly what was injected; and an exhaustive check over every order of four such steers, every set kiro-cli could have injected and three separators settles exactly the injected ones;possibly_consumed_steersreturns a collision group the echo covers only in part, and nothing for one it covers fully, not at all, or whose texts are identical.test_acp_client_more_coverage_session_frames.py,test_acp_runtime_client_parity.py,test_codex_steering_request.py,test_claude_steering_request.py,test_goose_steer_request.py,test_opencode_concurrent_prompt_steer.py,test_pi_steer.py) send the escaped envelope for text holding</user_message>.test/test_regenerate_taken_back_steer.py: Regenerate steps past a taken-back steer to the prompt (with and without a reply after it), never crosses the request above it, and still takes a steer the turn read as the request.test/test_history_restore_preambles.py: a stopped turn's restore names the prompt, not a taken-back steer.test/test_providers_base_cov80.py: a provider that does not opt in offers no take-back and sends nothing.test/test_steer_clear_handle.py:clear_pending_steersthrough the real reader loop (the request and its answer, a refusal that stops the offer and sends nothing again, no answer in time, KAS never sent it), and the turn yieldingEVENT_STEER_DROPPED.test_acp_frame_replay.py;test_harness_parity.pyholds the handle's answer to the table for every backend.test_interrupted_turn_steers.py: a taken-back steer is not carried into an interrupted turn's restore, and a cold start'sbuild_session_replayleaves it out, from disk and from the live window.UserMessage.steerTakeBack.test.tsx(the two controls and the menu, when they show, rendered through the sharedBtn, the status and its explainer kept apart from the two controls, the progress line and its icon, refusals as anErrorNotice, a failed request's notice under the progress line until the row settles, the note; the row keeps one element from pending to taken back, a row read back taken back is drawn without the collapse, the Edit and Queue instead notes are buttons to the message box and the Unsend note is not),useSteerRetract.test.ts(the outcome read from the row; Edit restoring the message box and moving the cursor there, with a quoted steer's quote block kept in the restored text so the saved draft holds the whole message; a hard stop handing Edit and Queue instead back; the text kept through a failed or unanswered request, through leaving the chat and through a reload, with the stored copy removed once the row settles and a corrupt or expired one dropped; a network error or a 503 keeping the take-back pending and marked as failed until it settles; a page that has not seen the turn running restoring nothing; a missing row settling, an Edit's text going back to the message box first and Unsend's not, and a missing row or an ended turn changing nothing while the chat's history is still loading; one chat's take-back never touching another's; an Edit whose copy cannot be stored never sent and shown as failed, while Unsend and Queue instead still go; a restore whose draft write fails keeping the stored copy, restoring it once per page and again after a reload; another tab's take-back on the same chat that has not settled, or one clicked at the same moment for the same steer or another, refusing the new one as busy, sending nothing and keeping the stored copy, which a reload of the first tab restores),slotDraftStore.test.ts(savereports whether the write stuck),transcriptRowCallbacksStable.test.tsxandCrewChatWindow.test.tsx(the page's Regenerate steps past a taken-back steer, with and without a later reply, as the server does),ChatPageDrafts.test.tsx(Edit restored on switching back into a chat, from its cache or after a reload, even when the reply streams before the history arrives, lands in that chat's draft and files and never carries another chat's text, and a draft write that fails leaves the stored copy in place),pinnedPrompt.retractedSteer.test.ts(never pinned, never the next prompt), the stale-snapshot merge keepingretracted, and thesteer_pushframe trace carrying the mark.-n0, 10 runs at-n 4 --dist loadgroup --max-worker-restart=2besidetest_steer_requeue.pyandtest_side_steer_queue.py, and a shuffled order (seed 67181). The ten frontend test files this change touches passed 8 shuffled orders (--sequence.shuffle, seeds 1, 3, 11, 222, 777, 4242, 31337, 65535).KeyErroron the frame field it adds), 20 remove a name other code uses and fail with an import, name or attribute error instead of an assertion (9 before any test runs), and the 18 that pass are docstrings, imports and re-exports (an undefined name fails flake8), a type annotation, a capability description thattest_backend_cards.pychecks, the parity gate's own registry row (without it the gate stops checking the set's capture), and the two calls that run the stopped-turn restore in a worker thread, which no test can tell from a direct call.Manual verification
Against a real kiro-cli 2.33.1, with Crew's own
AcpRuntime,AcpSessionHandleand the dashboard'ssteer_into_running_turnand take-back handlers: a turn ransleep 20, two steers were sent ("end with pineapple", "start with mango"), and the first was taken back with Edit. Its row becameretracted, the second was resent and obeyed ("Mango: the command printedfirst-done."), the answer had no pineapple, and nothing was queued. The full path was then driven from the browser: the recording below is a scratch gateway on this branch with a real kiro-cli 2.33.1 turn runningsleep 75, and three steers taken back from the dashboard.Screenshots / video
The full flow, from a running turn: Queue instead (the note, and the message in the queue above the message box), Edit from the menu (the note, and the text back in the message box with the cursor on it), and Unsend.
recorded from b28eaf962a9dde544e52bb594e90bf87493fce0e (before this head's coded refusals, the read-steer parser fix, the failed-request notice, the Regenerate fixes, the reload-safe Edit and the divider between the status and the two controls; the steps shown are the same, and the stills below show the row as it is now) · this branch on origin/main 5cc82ba, dashboard served from its own build · mode: KIROCREW_PROFILE=standalone, agent.acp_backend=kiro (kiro-cli 2.33.1), scratch data directory, cropped to the chat pane from the moment the shell tool runs · real server + model, no fixtures
steer-take-back.mp4
The real
UserMessagewith the built stylesheet and the English catalog, in each state: pending with Queue instead and the open menu, a take-back in flight, one whose request failed, a refused one, a steer the turn read first, and the three notes.Related Issues
Closes #18190
Closes #16300
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)