fix(chat): stop a slow kiro-cli probe from disabling Continue - #4194
Conversation
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS Continue is realigned with the readiness module's own send/gated contract, root cause tracked separately, and the refusal reuses the existing shared surface. The gate removal is not a workaround dressed as a fix: [DESIGN-REVIEWED] 7413979 |
First Principles Review (Fable 5) — ✅ PASSAdvisory premise-level review of All evidence is read; running the lens checks against the repo confirmed the claims (the shared First-Principles-Verdict: PASS Two real defects, each fixed at its cause for this caller, both joining mechanisms that already exist instead of growing new ones. What this change shipsIntent: make the Continue button actually recover a failed turn, and say why when the server refuses — a FIX.
The gate removal is cause-level for this endpoint: the misclassification (a send treated as a fourth gated class) is deleted, not guarded around. The deeper probe-timeout-reads-as-signed-out defect is named, scoped out with the five still-gated endpoints listed, and tracked (#4201) — exactly what lens 6 asks of an out-of-scope cause. Sibling count for the visibility fix: 0 unfixed — grepped WatchThe description's follow-up section still says generalising the notice "is tracked as #4202 rather than widened here," contradicting its own rebase note that #4202 is closed and #4214 shipped it. Stale paragraph only — the code has one surface. [FIRST-PRINCIPLES-REVIEWED] 7413979 |
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: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe change is fully coherent: the readiness gate removal on No findings. [OPUS-REVIEWED] 7413979 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — ✅ PASSAdvisory UX-level review of UX-Verdict: PASS Turns a silent double-failure into a visible, self-retiring notice on the same surface two sibling presses already use — copy, placement, and recovery all hold. Suggestions
[UX-REVIEWED] 7413979 |
4ce659a to
87d87a5
Compare
Zero-context usability review — dispositionA reviewer with no builder context judged the two committed frames as a first-time, APPLIED — the two red bars read as "two things are wrong", not cause and effect
Real, and the consequence is a user pressing Continue repeatedly. The notice now The first attempt used The same finding also made the evidence wrong: both frames showed the readiness 503, DECLINED — "Kiro CLI setup or sign-in is required…" is unactionable prose
Correct about the string, and this PR is why it stops appearing here: the readiness The refusals that DO reach this notice ( DECLINED — two buttons for one apparent job (
|
Review disposition — Design Review (advisory CONCERNS)FINDING: description↔diff mismatch on locale change
accepted-and-fixed. The sentence was true at head FINDING: the diagnosed trigger survives on every path still gated
accepted, tracked, not folded in. Correct and precisely the reason this PR is Deliberately not widened here: making |
Review disposition — First Principles Review (advisory CONCERNS)SUBTRACTION: delete the "no locale change" paragraph
accepted. Replaced rather than deleted, because the diff shows the key but not the FINDING: two unfixed sibling presses swallow refusals the same way
accepted — the asymmetry was the real defect in the submission, and it is now Not fixed in this diff, and the reason is the invariant itself. Generalising the notice What this PR does leave behind for it: the state, the retirement rules (turn start, Framing noteThe lane's own summary is the accurate one: "every item earns its place" — the six |
…tice (#4202) A regenerate or switch-variant press the server refuses under the slot lock (a turn already running, a stop in progress, a pending approval, a readiness probe that timed out) died in console.warn: the control flicked to disabled and straight back with nothing on screen. Generalize the refusal surface: one refused-press ErrorNotice above the composer, fed by any refusable press through showRefusedPress with a per-action title (pages.chatPage.could_not_regenerate / could_not_switch_variant across the 12 shipped locales + generated en-XA), showing the server's own message. The notice retires when a turn starts, the session changes, or it is dismissed. The continue press keeps its own path (open PR #4194 owns that surface). Tests lock in: refused regenerate renders title + server reason; refused switch-variant renders and dismiss clears; a starting turn retires the notice. Capture harness + temp-screenshots included.
…tice (#4202) A regenerate or switch-variant press the server refuses under the slot lock (a turn already running, a stop in progress, a pending approval, a readiness probe that timed out) died in console.warn: the control flicked to disabled and straight back with nothing on screen. Generalize the refusal surface: one refused-press ErrorNotice above the composer, fed by any refusable press through showRefusedPress with a per-action title (pages.chatPage.could_not_regenerate / could_not_switch_variant across the 12 shipped locales + generated en-XA), showing the server's own message. The notice retires when a turn starts, the session changes, or it is dismissed. The continue press keeps its own path (open PR #4194 owns that surface). Tests lock in: refused regenerate renders title + server reason; refused switch-variant renders and dismiss clears; a starting turn retires the notice. Capture harness + temp-screenshots included.
…tice (#4202) (#4238) A regenerate or switch-variant press the server refuses under the slot lock (a turn already running, a stop in progress, a pending approval, a readiness probe that timed out) died in console.warn: the control flicked to disabled and straight back with nothing on screen. Generalize the refusal surface: one refused-press ErrorNotice above the composer, fed by any refusable press through showRefusedPress with a per-action title (pages.chatPage.could_not_regenerate / could_not_switch_variant across the 12 shipped locales + generated en-XA), showing the server's own message. The notice retires when a turn starts, the session changes, or it is dismissed. The continue press keeps its own path (open PR #4194 owns that surface). Tests lock in: refused regenerate renders title + server reason; refused switch-variant renders and dismiss clears; a starting turn retires the notice. Capture harness + temp-screenshots included.
87d87a5 to
891ae97
Compare
891ae97 to
1ff455d
Compare
The Continue button on a failed turn was dead: it flicked to disabled and
straight back, with nothing on screen. Two independent defects met.
The endpoint was readiness-gated. `api_chat_slot_continue` opened with
`reject_if_kiro_unverified`, which authorizes on a probe no older than 30s and
therefore RE-PROBES `kiro-cli`. A probe that merely times out is indistinguishable
from signed-out, so on a host where that probe runs slow every press answered 503
`kiro_prerequisite_required` while typing the same request by hand worked. Continue
is an ordinary send — it queues one synthetic continuation and lets the runner
dispatch it, mutating nothing durable up front — so by `kiro_readiness`'s own
contract the ACP attempt is its authority and a signed-out install reports
`AcpAuthRequired` in the transcript. It is not one of the three gated classes
(poll-driven spawn sites, destructive reruns, `/v1/chat/completions`).
The refusal was also invisible. `handleContinue` swallowed every failure into
`console.warn`, so a server refusal the user could act on — the slot started a
turn, sub-agents are still delivering, an approval is pending — reached them as a
control that promises recovery and then says nothing. The reason now renders above
the composer via the shared `ErrorNotice`, dismissible, and is retired when a turn
starts or the session changes. One surface covers both presses: the error card
hosts one button and the composer the other, and both call `handleContinue`. It
carries a titled lead ("Couldn't continue"), because two equally-red bars stacked
flush read as "two things are wrong" rather than "this is the answer to the button
you just pressed" — a first-time reader then concludes the click did nothing and
presses again.
Reproduced end to end against an isolated gateway before and after. Both new tests
were confirmed to fail against the unfixed code: `test_not_readiness_gated` wires a
not-ready service the way `kiro_readiness._service` resolves it, so re-adding the
gate fails there rather than in production, and the fixture no longer stubs a
readiness gate — that stub is what hid this.
1ff455d to
7413979
Compare
Review disposition — First Principles Review (advisory CONCERNS)SUBTRACTION: the evidence harness duplicated
|
…tice (kirodotdev#4202) (kirodotdev#4238) A regenerate or switch-variant press the server refuses under the slot lock (a turn already running, a stop in progress, a pending approval, a readiness probe that timed out) died in console.warn: the control flicked to disabled and straight back with nothing on screen. Generalize the refusal surface: one refused-press ErrorNotice above the composer, fed by any refusable press through showRefusedPress with a per-action title (pages.chatPage.could_not_regenerate / could_not_switch_variant across the 12 shipped locales + generated en-XA), showing the server's own message. The notice retires when a turn starts, the session changes, or it is dismissed. The continue press keeps its own path (open PR kirodotdev#4194 owns that surface). Tests lock in: refused regenerate renders title + server reason; refused switch-variant renders and dismiss clears; a starting turn retires the notice. Capture harness + temp-screenshots included.
…tdev#4194) The Continue button on a failed turn was dead: it flicked to disabled and straight back, with nothing on screen. Two independent defects met. The endpoint was readiness-gated. `api_chat_slot_continue` opened with `reject_if_kiro_unverified`, which authorizes on a probe no older than 30s and therefore RE-PROBES `kiro-cli`. A probe that merely times out is indistinguishable from signed-out, so on a host where that probe runs slow every press answered 503 `kiro_prerequisite_required` while typing the same request by hand worked. Continue is an ordinary send — it queues one synthetic continuation and lets the runner dispatch it, mutating nothing durable up front — so by `kiro_readiness`'s own contract the ACP attempt is its authority and a signed-out install reports `AcpAuthRequired` in the transcript. It is not one of the three gated classes (poll-driven spawn sites, destructive reruns, `/v1/chat/completions`). The refusal was also invisible. `handleContinue` swallowed every failure into `console.warn`, so a server refusal the user could act on — the slot started a turn, sub-agents are still delivering, an approval is pending — reached them as a control that promises recovery and then says nothing. The reason now renders above the composer via the shared `ErrorNotice`, dismissible, and is retired when a turn starts or the session changes. One surface covers both presses: the error card hosts one button and the composer the other, and both call `handleContinue`. It carries a titled lead ("Couldn't continue"), because two equally-red bars stacked flush read as "two things are wrong" rather than "this is the answer to the button you just pressed" — a first-time reader then concludes the click did nothing and presses again. Reproduced end to end against an isolated gateway before and after. Both new tests were confirmed to fail against the unfixed code: `test_not_readiness_gated` wires a not-ready service the way `kiro_readiness._service` resolves it, so re-adding the gate fails there rather than in production, and the fixture no longer stubs a readiness gate — that stub is what hid this. Co-authored-by: t <t@t>
What was broken
Pressing Continue on a failed turn did nothing. The button flicked to disabled
and straight back — no message, no turn. Two independent defects met.
1. The endpoint was readiness-gated, and a slow probe reads as signed-out
api_chat_slot_continueopened withreject_if_kiro_unverified. That gateauthorizes on a probe no older than 30s, so it re-probes
kiro-cli— and aprobe that merely TIMES OUT is indistinguishable from signed-out. On a host where
the probe runs slow, every press answered
503 kiro_prerequisite_requiredwhiletyping the same request by hand worked fine.
Reproduced end to end against an isolated gateway (own
KIROCREW_HOME, own port,live instance untouched) — create slot → send → get the
[user, error]transcripta real failed turn leaves →
POST /continue:Continue does not belong behind that gate.
kiro_readiness's own contract namesthree gated classes — poll-driven
kiro-clispawn sites (/api/models,/api/sessions/usage), destructive reruns (regenerate, edit-resend, rewind), andPOST /v1/chat/completions— and states that ordinary sends are ungatedbecause a stale latch must not block them. Continue is an ordinary send: it queues
one synthetic continuation and lets the runner dispatch it, mutating nothing
durable up front, so the ACP attempt is its authority and a signed-out install
reports
AcpAuthRequiredin the transcript. Gating one of two paths that dispatchthe same turn is the defect.
2. The refusal was invisible
handleContinueswallowed every failure intoconsole.warn. The endpoint alsore-checks under
slot._lockand can legitimately refuse a press the clientbelieved was available (
slot_running,slot_subagents_running, an approvalpending) — all of which reached the user as a control that promises recovery and
then reports nothing.
What changed
api_chat_slot_continueno longer callsreject_if_kiro_unverified; itsdocstring and
kiro_readiness.reject_if_kiro_unverified's contract now nameContinue as a send, so it is not re-added by someone reading either side.
one entry in
REFUSED_PRESS_TITLE_KEYS, oneshowRefusedPress('continue', e)inthe catch. Nothing else on the frontend.
docs/system-specs/modules/learn-cron-dashboard.md§ readiness updated in thesame commit.
Rebased onto a main that now owns that surface. #4214 landed the shared
refused-press notice (
refusedPress+REFUSED_PRESS_TITLE_KEYS+showRefusedPress) forregenerateandswitch_variant— the generalisation anearlier revision of this PR had filed as #4202, now closed. The rebase therefore
DELETES this PR's own parallel implementation (its
continueErrorstate, its twoeffects, its render block) and its refusal test moves into
ChatPage.refusedPress.test.tsxbeside the other two presses. Two mechanismsrendering the same notice would have been this PR's own defect, one level up.
One new string:
pages.chatPage.could_not_continue("Couldn't continue"), the notice'stitled lead, translated in all 11 non-English catalogs with
en-XAregenerated. Therefusal text itself is the server's own prose and stays uncatalogued, as backend-owned
strings are today. A first attempt used
"Couldn't continue:"andcheck-source-stringscorrectly refused the trailing connector — a key endingmid-sentence spans two keys and a translator cannot reorder them.
Tests
Both new tests were confirmed to FAIL against the unfixed code, then pass:
test_not_readiness_gatedwires a not-ready service both wayskiro_readiness._serviceresolves it, so re-adding the gate fails there ratherthan in production. Verified: re-adding the two lines reddens exactly this test.
_patchedfixture no longer stubs a readiness gate — that stub is why noexisting test caught this.
ChatPage.refusedPress.test.tsxgains the third press: a rejectedcontinueSlotrenders
Couldn't continueplus the server reason on the shared surface. Itsexisting cases already pin dismissal and turn-start retirement for all three.
ChatPage.continueGate.test.tsxkeeps the complement — an ACCEPTED continue mustleave no notice — so the surface cannot drift into firing on the happy path.
Gates run locally after the rebase:
pytest(56512 passed),isort,flake8,mypy(987 files clean),tsc -b,vitest(21440 passed), andi18n:check(16/16 PASS, 1 new key, 0 badly shaped).Pre-existing on this host, unrelated to this change:
test_file_sheet(16 — thisvenv has no
openpyxl, which CI installs with the dev group),test_artifact_source(5),
test_artifacts_handlers(2),test_ci_surface_tests(1), and the knownCliPanelCoveragevitest flake (41/41 pass in isolation).Evidence
Captured through the existing shared harness (
website/capture/refused-press.tsxscripts/capture-refused-press.mjs, landed by fix(dashboard): surface refused regenerate/switch-variant via ErrorNotice (#4202) #4238), which now gains acontinuescene rather than a second harness of its own — see the disposition comment. It mounts
the real
ErrorCard,ErrorNoticeandChatInputout ofsrc/and reads the titlesfrom the live catalog through
i18nT, so a frame also proves the new key resolves.Every scene asserts its rendered text before a file is written.
The Continue scene renders BOTH controls that raise this refusal — the error card of
the turn that failed and the composer in its recovery state — because that pair is why
a silent refusal here is worse than for the other two presses: with nothing on screen
the reader's theory is that recovery itself is broken.
Light theme:
The silent state this replaces is the harness's own
beforescene (composer, nonotice), unchanged by this PR.
Follow-up, deliberately not in this PR
The probe that triggered this is itself broken on the affected host, and its own
SEL audit events say so:
kiro_prerequisite_probe_versionfailed withtimeout200 consecutive times (every probe for ~15 hours), while the
whoamiidentityprobe kept succeeding — the install is signed in; only the
--versionprobe isslow.
_PROBE_TIMEOUT_SECSis 10s and a timeout is read as "unverified", so everygated endpoint (
/api/models,/api/sessions/usage, regenerate, rewind) wassilently refused with nothing on screen for the whole window.
Distinguishing "the probe could not run" from "you are signed out" is a separate
change with its own blast radius, so it is not folded in here. Tracked as #4201.
The five endpoints that keep the gate by design (
/api/models,/api/sessions/usage, regenerate, edit-resend, rewind) therefore still answer 503forever on such a host — this PR routes around the trigger for
continueonly,because
continueis the one caller that never belonged behind the gate.Two sibling presses swallow their refusals into
console.warnexactly ascontinuedid (
regenerate failedatChatPage.tsx:4829,switch-variant failedat:5810),and regenerate is readiness-gated, so #4201 reaches it as the same invisible 503.
Generalising this PR's notice from one press to any refused press is tracked as
#4202 rather than widened here.