fix(rectification): keep skipped health out of holdout and repair empty exits (BUG-626, BUG-627)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,6 +52,8 @@ import {
|
||||
RECTIFICATION_INSUFFICIENT_CREDITS_NOTICE,
|
||||
RECTIFICATION_QUESTION_PREPARING_LABEL,
|
||||
RECTIFICATION_QUESTION_RELOAD_LABEL,
|
||||
RECTIFICATION_QUESTION_REPAIR_FAILED_COPY,
|
||||
RECTIFICATION_QUESTION_REPAIR_LIMIT,
|
||||
RECTIFICATION_QUESTION_RETRY_INTERVAL_MS,
|
||||
RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||||
RECTIFICATION_QUESTION_UNAVAILABLE_COPY,
|
||||
@@ -484,6 +486,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [nextUserActionId, setNextUserActionId] = useState<string | null>(() => caseSnapshotState(initialSnapshot)?.nextUserActionId ?? null);
|
||||
const [caseSnapshotLoaded, setCaseSnapshotLoaded] = useState(initialSnapshot !== null);
|
||||
const [questionRetryAttempts, setQuestionRetryAttempts] = useState(0);
|
||||
const [questionRepairAttempts, setQuestionRepairAttempts] = useState(0);
|
||||
const [questionRepairing, setQuestionRepairing] = useState(false);
|
||||
const [openingRequested, setOpeningRequested] = useState(false);
|
||||
const [acceptingCandidateId, setAcceptingCandidateId] = useState<string | null>(null);
|
||||
const [feedback, setFeedback] = useState<Record<string, "up" | "down" | undefined>>({});
|
||||
@@ -547,6 +551,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
setBusy(value);
|
||||
// A turn starting is a fresh chance for the next question to arrive.
|
||||
if (value) setQuestionRetryAttempts(0);
|
||||
if (value) setQuestionRepairAttempts(0);
|
||||
onPendingChange?.(value);
|
||||
}, [onPendingChange]);
|
||||
|
||||
@@ -649,6 +654,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
setCaseStatus(nextCaseStatus);
|
||||
setNextUserActionId(nextActionId || null);
|
||||
setCaseSnapshotLoaded(true);
|
||||
if (nextQuestion || nextChoice || acceptedTime || confirmedTime) {
|
||||
setQuestionRepairAttempts(0);
|
||||
}
|
||||
if (confirmedTime) {
|
||||
setSavedTime(confirmedTime);
|
||||
setSavedStatus("confirmed");
|
||||
@@ -1500,7 +1508,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
));
|
||||
// The gap between a settled turn and its next question has two visible
|
||||
// states: `preparing` (one live timeline row, timed refetches) and
|
||||
// `unavailable` (copy and a reload button) — never bare copy telling the
|
||||
// `unavailable` (copy and a repair button) — never bare copy telling the
|
||||
// reader to wait for the server.
|
||||
const questionGap = rectificationQuestionGapState({
|
||||
liveQuestionVisible: liveQuestionOnMessages,
|
||||
@@ -1532,7 +1540,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
});
|
||||
// Question recovery: the turn already refetched once on completion; while
|
||||
// the gap is `preparing`, refetch on a timer up to the retry limit, then
|
||||
// hand the reader a reload button. Attempts reset once a question arrives
|
||||
// hand the reader a repair button. Attempts reset once a question arrives
|
||||
// or another turn starts (both in handlers, never in an effect).
|
||||
useVisibilityAwarePoll({
|
||||
enabled: questionGap === "preparing",
|
||||
@@ -1546,9 +1554,40 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
async function refetchQuestion() {
|
||||
await loadCaseSnapshot();
|
||||
}
|
||||
function reloadQuestion() {
|
||||
setQuestionRetryAttempts(0);
|
||||
void refetchQuestion();
|
||||
async function repairQuestion() {
|
||||
if (questionRepairing || questionRepairAttempts >= RECTIFICATION_QUESTION_REPAIR_LIMIT) return;
|
||||
setQuestionRepairing(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/rectification/cases/${encodeURIComponent(caseId)}/repair-exit`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
cache: "no-store",
|
||||
body: JSON.stringify({ sessionId }),
|
||||
},
|
||||
);
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (response.ok && payload) {
|
||||
startTransition(() => {
|
||||
applyCaseSnapshot(payload);
|
||||
});
|
||||
const recovered = Boolean(
|
||||
currentQuestionFromSnapshot(payload.current_question)
|
||||
|| parseRectificationChoiceCard(payload.choice_card)
|
||||
|| payload.case?.accepted_time
|
||||
|| payload.case?.confirmed_time
|
||||
);
|
||||
if (!recovered) setQuestionRepairAttempts((current) => current + 1);
|
||||
} else {
|
||||
await loadCaseSnapshot();
|
||||
setQuestionRepairAttempts((current) => current + 1);
|
||||
}
|
||||
} catch {
|
||||
setQuestionRepairAttempts((current) => current + 1);
|
||||
} finally {
|
||||
setQuestionRepairing(false);
|
||||
}
|
||||
}
|
||||
|
||||
// A Case with no turns and no automatic first turn (the server never starts
|
||||
@@ -1768,10 +1807,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
)}
|
||||
{questionGap === "unavailable" && (
|
||||
<div className="rectification-message-wrap rectification-message-entry rectification-question-gap" role="status">
|
||||
<p className="rectification-question-gap__copy">{RECTIFICATION_QUESTION_UNAVAILABLE_COPY}</p>
|
||||
<Button type="button" variant="outline" onClick={reloadQuestion}>
|
||||
{RECTIFICATION_QUESTION_RELOAD_LABEL}
|
||||
</Button>
|
||||
{questionRepairAttempts >= RECTIFICATION_QUESTION_REPAIR_LIMIT ? (
|
||||
<p className="rectification-question-gap__copy">{RECTIFICATION_QUESTION_REPAIR_FAILED_COPY}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="rectification-question-gap__copy">{RECTIFICATION_QUESTION_UNAVAILABLE_COPY}</p>
|
||||
<Button type="button" variant="outline" onClick={() => void repairQuestion()} disabled={questionRepairing}>
|
||||
{RECTIFICATION_QUESTION_RELOAD_LABEL}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{questionGap === "verified_idle" && verifiedIdleCopy && !showSelectionCards && (
|
||||
|
||||
Reference in New Issue
Block a user