Skip to content

tracking(widget): clarification form submission failure family #1487

Description

@codeacme17

tracking(widget): clarification form submission failure family

Umbrella for the customer-reported "Widget forms do not submit" incident: a visitor answers an agent's clarification/interaction form (e.g. parameters for a pending tool/MCP call), the submit fails with a generic "Failed to send response", and the task stays parked in WAITING_FOR_USER — the pending tool call never runs. Production triage found multiple independent failure legs plus adjacent defects discovered while fixing them. This issue tracks the whole family so nothing gets lost across the two repos.

Failure legs (seed issues)

Derived while fixing (spawned issues)

Split out of the durable-storage reviews (#1476 / #1480)

Split out of the WebSocket redaction work (#1479 / #1497)

Split out of the saas continuation work (saas#651 / saas#680)

Test/CI debt this family created

Adjacent, deliberately not in this family

Cross-PR contracts (agreed so parallel work does not conflict)

  1. Backend message_rejected ack message field is always user-presentable text; the frontend renders it verbatim (fix(frontend): clarification form gives up on transient upload failures and hides the real rejection reason #1468fix(web): form-response command fails permanently after ~19s when the resume reservation is held #1469 ↔ saas#680). Security caveat: security(websocket): agent RuntimeError text is surfaced verbatim to public chat visitors #1479/security(websocket): raw exception text still reaches chat clients through unrecognized producer shapes #1497 define what must NOT flow into that field — the redaction (PR security(websocket): redact incidental exception text from chat clients #1514) must land before or with fix(frontend): retry transient clarification uploads and surface the real failure reason #1472, or the frontend change turns a storage fault into a tenant-internals disclosure.
  2. The session transport gains its own distinct waiting-for-user refusal reason (saas#686, merged) so the frontend surfacing from fix(frontend): clarification form gives up on transient upload failures and hides the real rejection reason #1468 works unchanged on both transports.
  3. Delivery identity is caller-supplied (delivery_turn_id, saas#713) rather than derived, so a client-chosen dedup key probes the id space its delivery was recorded in. Every continuation caller must adopt it; a caller that keeps the derived id re-delivers on ack loss.

Landing order (updated 2026-08-20)

Every open PR in this family is either conflicted, un-reviewed, or the base of another PR, so the order matters more than the count.

Wave 0 — unblock the trains. saas#699 (approved, needs a rebase; root of a 5-PR stack) · saas#713 (clean, needs review; #687 waits on it) · #1476 (base of #1480) · #1514 (needs human review; contract dependency for #1472).

Wave 1 — the legs, in dependency order. #1480 (after #1476) · #1472 (after #1514) · #1484 (fold in or immediately follow #1499) · saas#687 (after #713, rebased and shrunk to routing) · saas#700 → saas#712 (after #699).

Wave 2 — P0 items with no PR yet. #1470 · #1500 · and a disposition for #1479 (does #1514 close it, or does it stay open for the residual?).

Wave 3 — P1. #1482 · #1485 · saas#685 · saas#663 · saas#675 · saas#682 · #1518 · plus the CI debt (#1492, saas#709) so the legs' own tests stop flaking.

Wave 4 — P2 hygiene. #1471 · #1473 · #1489 · #1515 · #1516 · #1517 · #1519 · #1520 · #1521 · #1490.

Open question: saas#651 vs saas#680

Both issues are the same customer report ("Failed to send response" in a session-mode widget) approached from opposite ends, and both now have open PRs that add parked-task routing to session_websocket.py (#687 from the #680 side; #702/#703 from the #651 side) and change answer/delivery identity in external_tasks.py (#713 vs #712). They will conflict textually and may overlap semantically. Decide one owner for the routing change and one for answer identity before any of them merges, and close the loser as superseded. Secondary cleanup: #701 is closed but is still the git base of #702 and #703.

Non-code follow-ups (ops, tracked here so they are not lost)

Related / upstream

Done means

A form submission on both widget transports either delivers (with retries absorbing transient storage faults and reservation contention) or fails with a specific, actionable, non-sensitive message — and never strands a task in WAITING_FOR_USER silently. The form must also become re-answerable when its turn does not run (#1500), and an answer must never be delivered twice or against a superseded question (saas#712, saas#713).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions