import type { ComposerSendBlocker } from "../../lib/composer-send-blocking"; import type { FailedSessionRetryInput } from "../../lib/failed-session-retry"; import { OpenGeniApiError } from "@opengeni/sdk/browser"; import { useRef, useState } from "react"; import { RotateCcwIcon } from "lucide-react"; import { Button } from "../../../ui/button"; type RetryBlocker = | "draft" | "unsent" | "delivery" | "queued" | "loading" | "permission" | "paused"; /** Retry is an execution control, never a new user message. */ export function FailedSessionActions(props: { failureId?: string | null | undefined; composerBlocker?: ComposerSendBlocker | null | undefined; onRetry: () => Promise; retryBlocker: RetryBlocker | null; retryInput?: FailedSessionRetryInput | null | undefined; }) { const [identity, setIdentity] = useState(props.failureId ?? null); const [generation, setGeneration] = useState(0); if (props.failureId && props.failureId !== identity) { setIdentity(props.failureId); // Initial history hydration identifies the same failure. A later distinct // boundary permits a new retry even if React skipped the running state. if (identity) setGeneration(generation + 1); } return ; } function FailureActionsAttempt({ onRetry, composerBlocker, retryBlocker, retryInput, }: { onRetry: () => Promise; composerBlocker?: ComposerSendBlocker | null | undefined; retryBlocker: RetryBlocker | null; retryInput?: FailedSessionRetryInput | null | undefined; }) { const retryBlockedReason = composerBlocker || retryBlocker; const pending = useRef(false); const accepted = useRef(false); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); const [rejected, setRejected] = useState(false); async function submit() { if (pending.current || accepted.current || retryBlockedReason || rejected) return; pending.current = true; setSubmitting(true); setError(null); try { if (await onRetry()) { accepted.current = true; setSubmitted(true); } else setError("Could not retry this session."); } catch (failure) { // Pause and unsettled execution are transient; each explicit retry still // passes backend admission. A definitively unsafe failure, including a // tool call whose outcome is permanently unknown, is latched. if ( failure instanceof OpenGeniApiError && !failure.outcomeUnknown && (failure.code === "RETRY_UNSUPPORTED_FAILURE" || failure.code === "RETRY_TOOL_OUTCOME_UNKNOWN") ) { setRejected(true); } setError( failure instanceof OpenGeniApiError && !failure.outcomeUnknown && failure.status < 500 ? failure.code === "RETRY_UNSUPPORTED_FAILURE" ? "This failure cannot be retried safely." : failure.code === "RETRY_TOOL_OUTCOME_UNKNOWN" ? "A tool may already have run, so Retry could repeat it. Check its result, then send a message to continue." : failure.code === "RETRY_EXECUTION_UNRESOLVED" ? "Earlier work is still settling. Try again in a moment." : failure.code === "RETRY_PAUSED" ? "This session is paused." : failure.status === 409 ? "The session changed." : "Could not retry this session. Check your access and model." : "Retry not confirmed.", ); } finally { pending.current = false; setSubmitting(false); } } return ( {!retryBlockedReason && !rejected && !submitted ? ( ) : null} {submitted ? Retry requested. : null} {error ? ( {error} ) : null} ); }