Skip to content

fix: report refused sends at two remaining sendChat callers (#4198) - #4214

Merged
bolichen97 merged 1 commit into
mainfrom
fix/report-failed-send-callers-4198
Aug 20, 2026
Merged

fix: report refused sends at two remaining sendChat callers (#4198)#4214
bolichen97 merged 1 commit into
mainfrom
fix/report-failed-send-callers-4198

Conversation

@CrysisDeu

@CrysisDeu CrysisDeu commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

An HTTP 4xx/5xx resolves a fetch rather than rejecting it, so two api.sendChat callers that only handled rejection let a refused send look successful. Both were flagged in #4198 (a deliberate follow-up out of PR #4180's First Principles review) and both now get the treatment the chat surfaces already have: read the JSON receipt ({ok, queued, error}), say so where the message was typed, and hand the payload back.

Closes #4198

The two sites

website/src/App.tsx — the feature-request flow

Before: await api.sendChat(...) inside try { } catch { /* WS will handle response */ } — a refused send left the optimistic bubble on screen next to a slot stuck running, with nothing said.

Now:

  • Reads the receipt; !body.ok && !body.queued on resolve is failure (resolution is not success — no turn started, no WS response is coming).
  • Surface choice (stated per the issue's scope note): the flow creates a chat session and navigates to it, so the error is reported as a role:'error' transcript row in that session — the existing error presentation, no new component. It carries the server's own reason when present, else the existing pages.chatPage.send_failed catalog key (present in every locale — no new string ships).
  • The error row and both optimistic writes are addressed to the slot the flow created (appendSlotMessage), not the active slot: createSlot.fulfilled has a switched-away guard, so a mid-flight session switch must not put the bubble in an unrelated transcript (local review finding, fixed).
  • The optimistic running flag is set/undone only while that slot is on screen — slotRunning describes the active slot, and clearing it after a switch would clobber another session's live indicator. A stale flag self-heals from the next server snapshot.
  • No composer restore: the payload is a canned constant; the retry affordance is the feedback pill itself.

website/src/hooks/useSceneInteraction.tsx — the scene popover composer

Before: .then(r => { r.body?.cancel() }) then an unconditional setSendState('sent') — the 'failed' state (line 301) was reachable only via rejection, i.e. unreachable for precisely the errors that matter. The state name asserted the opposite of what happened.

Now:

  • Reads the receipt; refused sends reach 'failed' (red Retry) instead of 'sent'. The stale "sendChat streams SSE" comment is corrected — ?ws=1 answers with a JSON receipt and the turn streams over WS.
  • The payload is handed back by appending to whatever the draft holds now, with de-duplication — a draft edited mid-flight keeps the newer text and gets the failed payload appended, never clobbered (the regression class PR fix(chat): remove the unconfirmed-message notice and its timeout sweep #4180 hit; pinned by a test). Single space as separator: the composer is a single-line <input>, where a newline would be silently stripped.
  • The refused message is not echoed into the optimistic mini thread.
  • Send outcomes are guarded on the popover still targeting the same agent — a late failure must not flag the new target or splice the old payload into its draft.
  • The server's reason is surfaced as the Retry button's title (review finding; no new catalog key).
  • The 1.5s sent→idle reset is now conditional (s === 'sent' ? 'idle' : s) so a stale timer can't re-enable submit while a later send is in flight (review finding, fixed).
  • The steer branch is already safe: steerChat parses through j(), which throws on HTTP errors.

Screenshots

Feature-request transcript after a refused send (real row registry; before the fix this surface showed the bubble alone next to a stuck-running slot):

feature-request error row, dark

Scene popover after a refused send, captured end-to-end: the harness stubs /api/chat to answer 409 {ok:false} (which resolves — the exact shape the old code reported as 'sent'), the runner clicks the agent, types, sends, and asserts the draft is handed back before shooting:

scene popover retry, dark

Light theme: feature-request · scene popover. Harness: website/capture/failed-send-report.{tsx,html} + website/scripts/capture-failed-send-report.mjs (each scene asserts its marker and the runner exits nonzero on a mismatch).

Tests

  • website/src/test/App.featureRequestFailure.test.tsx (new): resolved-not-ok appends the error row with the server reason and undoes running; receipt-without-reason falls back to the shared key; transport reject reports the same way; an accepted send stays untouched.
  • website/src/test/UseSceneInteractionCoverage.test.tsx: resolved-not-ok reaches Retry with the payload kept and no mini-thread echo; an unreadable receipt (HTML error page) is a failure, not a success; a queued acceptance is sent; append-does-not-clobber (mid-flight edit keeps newer text, payload appended); retargeted failure does not flag the new composer; the server reason lands on the Retry title; a stale sent-reset can't clobber a later in-flight send. The obsolete "swallows a rejected stream cancel" test (which pinned the streaming misunderstanding) is replaced by the receipt contract; the shared sendChat mock now answers with a JSON receipt.

Verification

  • npx tsc -b clean; affected test files 76/76; full website vitest 21166 passed / 1 failed = CliPanelCoverage custom-theme repaint, a known pre-existing flake (passes in isolation).
  • Backend (unchanged by this diff): isort / flake8 / mypy clean at pinned versions; pytest 55695 passed — 7 failures in test_artifact_source.py / test_artifacts_handlers.py reproduce on main on this host (a .kiro directory at the temp root makes the classifier treat /tmp as a project root) and pass under a clean TMPDIR; not from this diff.

Local review round (pre-push)

Two model-pinned read-only reviewers (GPT 5.6-sol mirroring codex-review, Opus 5 mirroring claude-review + base-ref AUTOSDE). Verdicts: 0 blocking. All actionable findings fixed in this commit: optimistic-write slot addressing + guarded running (both reviewers), stale sent-timer race (GPT), scene reason surfaced (Opus). Two low-severity advisories are accepted as documented limitations rather than grown into new machinery:

  1. A client-only role:'error' row does not survive a switch-away-then-back rehydrate (server history wins). The stuck-running half self-heals from the server; only the textual reason is lost in that narrow window. Fixing it needs new persistence machinery — out of scope for this fix.
  2. In the scene popover, a refusal that lands after the user retargeted is reported nowhere on the original agent's side (the retarget reset already discarded the shared draft — pre-existing behaviour). Strictly better than before, which reported 'sent' for the same case.

CI round 1 responses (this head)

  • GPT 5.6 BLOCKING (draft duplication) — real, fixed: the scene restore's de-duplication is now containment, not equality. This composer is not cleared at send, so the normal mid-flight edit builds ON the sent text; equality missed that case and appended the payload a second time. Two tests pin both shapes (replaced draft → append; extended draft → no duplicate).
  • UX Watch 1 (unframed server reason) — real, fixed at both sites: the reason is now framed by the existing apps.designTweak.status.send_failed ("Send failed: {{error}}") catalog entry, present in every locale — still no new string. (The lane's suggested key apps.mochi.chat.send_failed is actually the connection-blame copy; the designTweak one is the framed pattern.)
  • UX Watch 2 (connection-blame fallback) — not applicable, disposition posted: the no-reason fallback here is pages.chatPage.send_failed = "Send failed" (en.manual.json), which carries no connection claim. The quoted copy belongs to apps.mochi.chat.send_failed, which this path never renders.
  • UX suggestion (hover-only reason) — adopted: the scene failure reason is now a visible role="status" line under the composer instead of a Retry-button tooltip, reachable by keyboard, touch, and AT.

@CrysisDeu
CrysisDeu requested a review from a team August 17, 2026 22:10
@CrysisDeu
CrysisDeu requested a review from a team as a code owner August 17, 2026 22:10
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 17, 2026
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of a243f2f981aea98ce060014001517e3620ec689d and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] a243f2f

False positive or not applicable? A repository writer can comment:
/ai-review override gpt a243f2f981aea98ce060014001517e3620ec689d: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of a243f2f981aea98ce060014001517e3620ec689d — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Both other sendChat callers now match the established ChatPage receipt pattern; the design is scoped, guarded, and tested. One structural observation worth passing on.

Design-Verdict: PASS

Extends the proven ChatPage receipt contract to the last two blind callers, with race guards pinned by tests — sound and proportionate.

Suggestions

  • The root cause — api.sendChat returning a raw Response that callers must remember to decode — still stands; three sites now hand-roll !body.ok && !body.queued. A shared receipt helper (e.g. readSendReceipt(r) in client.ts returning {accepted, reason}) would make the next caller unable to re-introduce this bug class; fine as a follow-up if out of scope here.

[DESIGN-REVIEWED] a243f2f

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of a243f2f981aea98ce060014001517e3620ec689d — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: PASS

Silent send failures now speak in place with a retry path — the fix lands where the user is looking, in both themes.

Suggestions

  • Scene popover: transport rejections call reportFailedSend() with no reason, so the role="status" line never renders and the only signal is the button flipping to "Retry" plus a mysteriously repopulated draft — render the line on every failure, falling back to the existing pages.chatPage.send_failed ("Couldn't send — check your connection and try again") when sendFailReason is empty.
  • Both surfaces interpolate the raw server reason verbatim — the screenshots show "Send failed: slot agent mismatch", implementation vocabulary with no next step; extend the frame to carry recovery ("Send failed: {{error}} — try again") or fall back to the generic key when the reason isn't user-vocabulary.

[UX-REVIEWED] a243f2f

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed a243f2f981aea98ce060014001517e3620ec689d — this comment is updated in place on each push.

Review details

I verified the retired key apps.designTweak.status.send_failed is absent from en.manual.json's status block (lines 231–270) and from all locales, that i18next is configured returnEmptyString: false with fallbackLng: en (so a missing key renders as the raw key string), and that the guard test sendFailedSharedKey.test.ts walks only website/src/ — leaving website/capture/ uncovered. Candidate 2 (binary PNGs) can't be grounded: image contents aren't inspectable from the diff, so (c) fails.

Capture script failed-send-report.tsx references the i18n key retired in #4240, so its feature scene renders the raw key and the script can never pass.

FINDING — website/capture/failed-send-report.tsx:96 — i18nT('apps.designTweak.status.send_failed', { error: 'slot agent mismatch' }) names a key deleted in #4240 (absent from every catalog; i18next with returnEmptyString:false renders the literal apps.designTweak.status.send_failed), so FeatureSite never shows "Send failed: slot agent mismatch" and capture-failed-send-report.mjs's waitForSelector('text=Send failed: slot agent mismatch') times out → the script hits process.exit(1) and the feature screenshot is never produced → Fix: use the core-owned key the fixed flow uses — i18nT('pages.chatPage.send_failed_with_error', { error: 'slot agent mismatch' }).

[OPUS-REVIEWED] a243f2f

Verdict parsed from the review's SHA-scoped output markers for commit a243f2f981aea98ce060014001517e3620ec689d.

False positive or not applicable? A repository writer can comment:
/ai-review override fable a243f2f981aea98ce060014001517e3620ec689d: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of a243f2f981aea98ce060014001517e3620ec689d — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Reviewing PR #4214 against the first-principles contract: I read the contract, the intent file, the full patch, and surveyed the repo for existing receipt-check mechanisms and sibling api.sendChat callers. Findings below.

First-Principles-Verdict: CONCERNS

Both sites are derived fixes for #4198, but the composer guard ships a redundant second ref, and the receipt check is now hand-rolled at a fourth call site.

What this change ships

Intent: make a send the server refused say so where it was typed — a FIX (closes #4198). 12 observable items; the 10 most visible:

  1. Feature-request flow: refused send now shows an error row with the server's reason — justified (the reported defect)
  2. Feature-request flow: refused send stops the false "running" indicator — justified
  3. Optimistic bubble now lands in the session the flow created, not whichever is active — declared, justified (mid-flight switch)
  4. Running flag set/cleared only while that session is on screen — declared, justified
  5. Scene popover: refused send reaches red Retry instead of a false "sent" tick — justified (the defect)
  6. Scene popover: failed text handed back, appended after mid-flight typing — justified (fix(chat): remove the unconfirmed-message notice and its timeout sweep #4180 regression class)
  7. Scene popover: input clears at send start, not on success — changed timing, declared via the mid-flight-edit rationale
  8. Scene popover: server reason shown as a visible status line — declared (UX-review response)
  9. Late outcomes ignored after retarget/reopen (target ref + epoch) — justified, but one of the two guards is redundant
  10. Screenshot harness + 4 committed PNGs — declared; repo-conventional evidence (189 sibling capture-*.mjs scripts)

Watch

  • The fix sits at mechanism level per caller; the cause is api.sendChat returning the raw fetch Response, so every caller re-derives the receipt. Counted after this PR: 4 inline !body.ok && !body.queued reads (ChatPane.tsx:343, ChatPage.tsx:4155, App.tsx:1804, useSceneInteraction.tsx:355) plus 2 status-only .ok checks (issue-radar/lib/agentSession.ts:143, auto-improvement/lib/agentSession.ts:237). No unfixed siblings remain, and parsing inside sendChat reshapes a 6-caller API — accepted-and-deferred, but a human should log it.
  • Description says the restore's "de-duplication is now containment, not equality"; the shipped hunk says "EQUALITY only… any containment heuristic beyond that guesses about intent." The code's choice looks right; the description narrates the decision it reverses (text truncated, so a later round may already correct this).

Subtractions

  • Drop composerTargetRef (useSceneInteraction.tsx) — composerEpochRef is bumped in the same effect that retargets, so epoch equality already implies the same target; sameComposer() (its only consumer) needs the epoch alone.

[FIRST-PRINCIPLES-REVIEWED] a243f2f

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 17, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/report-failed-send-callers-4198 branch from 881fdc4 to ca34a51 Compare August 17, 2026 23:17
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

ai-review-disposition (lane: UX Review, head ca34a51)

  • Watch 1 (unframed server reason): FIXED — both sites now frame the reason with the existing apps.designTweak.status.send_failed ("Send failed: {{error}}") entry, present in every locale; no new string ships. Note the lane's cited key apps.mochi.chat.send_failed is the connection-blame copy, not the framed pattern.
  • Watch 2 (connection-blame fallback on a resolved refusal): NOT APPLICABLE — the fallback this path renders is pages.chatPage.send_failed = "Send failed" (website/src/i18n/locales/en.manual.json:2874), which makes no connection claim. The quoted copy "Couldn't send — check your connection..." belongs to apps.mochi.chat.send_failed (en.json), which neither changed site renders.
  • Suggestion (hover-only reason): ADOPTED — the scene reason is now a visible role="status" line under the composer (useSceneInteraction.tsx), reachable by keyboard, touch, and AT; the tooltip is removed.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 17, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/report-failed-send-callers-4198 branch from ca34a51 to 4a1f592 Compare August 17, 2026 23:37
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 17, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/report-failed-send-callers-4198 branch from 4a1f592 to 114acb5 Compare August 18, 2026 00:01
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 18, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/report-failed-send-callers-4198 branch from 114acb5 to f309904 Compare August 18, 2026 00:28
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 18, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

ai-review-disposition (lane: Design Review, head 0f90542)

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Aug 20, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) August 20, 2026 03:54
bolichen97
bolichen97 previously approved these changes Aug 20, 2026
@CrysisDeu
CrysisDeu force-pushed the fix/report-failed-send-callers-4198 branch from 0f90542 to 0fccd9b Compare August 20, 2026 06:44
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Aug 20, 2026
An HTTP 4xx/5xx RESOLVES a fetch rather than rejecting it, so two
api.sendChat callers that only handled rejection let a refused send look
successful:

- App.tsx requestFeature left the optimistic bubble next to a slot stuck
  `running`, with nothing said. It now reads the JSON receipt, appends a
  role:'error' row (server reason, else the existing
  pages.chatPage.send_failed key) addressed to the slot that owns the
  bubble, and undoes the optimistic running state while that slot is on
  screen. Both optimistic writes are slot-addressed now, so a mid-flight
  session switch cannot put the bubble in an unrelated transcript.
- useSceneInteraction.sendToAgent unconditionally set 'sent' on resolve;
  the 'failed' state was reachable only via rejection. It now checks the
  receipt, reaches 'failed' for refused sends, hands the payload back by
  APPENDING to the draft with de-duplication (never clobbering text typed
  mid-flight), surfaces the server reason as the Retry tooltip, and no
  longer echoes a refused message into the mini thread. Send outcomes are
  guarded on the popover still targeting the same agent, and the 1.5s
  sent-reset timer can no longer clobber a later in-flight send.

Vitest coverage for both sites (resolved-not-ok, rejected, queued,
append-not-clobber, retarget guards, stale-timer guard) plus a capture
harness and SHA-pinned screenshots of both failure surfaces.

Closes #4198
@CrysisDeu
CrysisDeu force-pushed the fix/report-failed-send-callers-4198 branch from 0fccd9b to a243f2f Compare August 20, 2026 07:27
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 20, 2026
@bolichen97
bolichen97 merged commit 1750a86 into main Aug 20, 2026
96 of 98 checks passed
@bolichen97
bolichen97 deleted the fix/report-failed-send-callers-4198 branch August 20, 2026 08:08
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 20, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…ev#4198) (kirodotdev#4214)

An HTTP 4xx/5xx RESOLVES a fetch rather than rejecting it, so two
api.sendChat callers that only handled rejection let a refused send look
successful:

- App.tsx requestFeature left the optimistic bubble next to a slot stuck
  `running`, with nothing said. It now reads the JSON receipt, appends a
  role:'error' row (server reason, else the existing
  pages.chatPage.send_failed key) addressed to the slot that owns the
  bubble, and undoes the optimistic running state while that slot is on
  screen. Both optimistic writes are slot-addressed now, so a mid-flight
  session switch cannot put the bubble in an unrelated transcript.
- useSceneInteraction.sendToAgent unconditionally set 'sent' on resolve;
  the 'failed' state was reachable only via rejection. It now checks the
  receipt, reaches 'failed' for refused sends, hands the payload back by
  APPENDING to the draft with de-duplication (never clobbering text typed
  mid-flight), surfaces the server reason as the Retry tooltip, and no
  longer echoes a refused message into the mini thread. Send outcomes are
  guarded on the popover still targeting the same agent, and the 1.5s
  sent-reset timer can no longer clobber a later in-flight send.

Vitest coverage for both sites (resolved-not-ok, rejected, queued,
append-not-clobber, retarget guards, stale-timer guard) plus a capture
harness and SHA-pinned screenshots of both failure surfaces.

Closes kirodotdev#4198

Co-authored-by: Zezhen Xu <zezhexu@dev-dsk-zezhexu-2b-15d11a49.us-west-2.amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two api.sendChat callers still leave a failed send looking sent

2 participants