-
Notifications
You must be signed in to change notification settings - Fork 408
Fix stuck chat + freezed view #2533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@agent-native/core": patch | ||
| --- | ||
|
|
||
| Stop the "chat looks stuck" banner from appearing on an idle chat whose turn already finished, and keep its Retry/Cancel buttons clickable after an automatic retry. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,16 @@ export interface RunStuckBannerProps { | |
| * render; omit to keep the unconditional Retry/Cancel behavior. | ||
| */ | ||
| hasInFlightWork?: () => boolean; | ||
| /** | ||
| * Returns true when this chat is actually waiting on a reply — typically | ||
| * backed by `chatHandle.isRunning()`. "Stuck" is a claim about THIS client, | ||
| * but the run row is the server's; a turn that finished normally can leave a | ||
| * row in `running` (terminal marking is best-effort, and the stale-run reaper | ||
| * runs later). Without this gate an idle, completed chat shows the warning | ||
| * and auto-retry re-prompts a thread the user considers done. Checked fresh | ||
| * on every render; omit to keep the unconditional behavior. | ||
| */ | ||
| isAwaitingResponse?: () => boolean; | ||
| /** | ||
| * Called whenever the stuck state transitions. Useful for surfacing | ||
| * observability events (Sentry, PostHog) at the call site. | ||
|
|
@@ -170,6 +180,7 @@ export function RunStuckBanner({ | |
| autoRetry = false, | ||
| autoRetryOwnerId, | ||
| hasInFlightWork, | ||
| isAwaitingResponse, | ||
| className, | ||
| }: RunStuckBannerProps) { | ||
| const state = useRunStuckDetection({ | ||
|
|
@@ -180,6 +191,11 @@ export function RunStuckBanner({ | |
| }); | ||
| const abortRun = useAbortRun(apiUrl); | ||
| const [busy, setBusy] = useState<BusyState>({ type: "none" }); | ||
| // "An automatic attempt was made for this run" is a different fact from | ||
| // "the controls are locked while a request is in flight". Sharing one flag | ||
| // for both is what previously forced the buttons to stay disabled just to | ||
| // keep the message on screen. | ||
| const [autoRetriedRunId, setAutoRetriedRunId] = useState<string | null>(null); | ||
| const autoRetriedRunIdsRef = useRef<Set<string>>(new Set()); | ||
| const generatedOwnerIdRef = useRef<string | null>(null); | ||
| if (!generatedOwnerIdRef.current) { | ||
|
|
@@ -204,6 +220,7 @@ export function RunStuckBanner({ | |
| // either signal says so. | ||
| const inFlightWork = | ||
| state.hasInFlightWork === true || (hasInFlightWork?.() ?? false); | ||
| const awaitingResponse = isAwaitingResponse?.() ?? true; | ||
| // Server-continued runs are recovered by the SERVER (chained continuation | ||
| // chunks + lost-handoff sweep); an automatic client abort would kill a live | ||
| // server-chained run. Auto-retry is therefore disabled unconditionally for | ||
|
|
@@ -261,6 +278,9 @@ export function RunStuckBanner({ | |
| // A live tool/A2A call in flight — never auto-abort real work (see | ||
| // `inFlightWork` comment above). | ||
| inFlightWork || | ||
| // Nothing is waiting on this run here; re-prompting would restart a | ||
| // thread the user already considers finished. | ||
| !awaitingResponse || | ||
| !state.isStuck || | ||
| !state.runId || | ||
| busy.type !== "none" || | ||
|
|
@@ -274,21 +294,26 @@ export function RunStuckBanner({ | |
| autoRetriedRunIdsRef.current.add(runId); | ||
| if (!claimed) return; | ||
| setBusy({ type: "retry", runId }); | ||
| setAutoRetriedRunId(runId); | ||
| trackEvent("agent_chat_stuck_auto_retry", { | ||
| runId, | ||
| threadId: threadId ?? null, | ||
| stuckSinceMs: state.stuckSinceMs ?? null, | ||
| }); | ||
| void abortRun(runId, "auto_stuck_retry").then((aborted) => { | ||
| if (aborted) { | ||
| onRetry?.(aborted); | ||
| return; | ||
| } | ||
| // Release the controls on BOTH outcomes. Leaving them spinning until | ||
| // polling observes a new run id assumes the retry produces one; when | ||
| // the abort does not move the run off `running`, the banner keeps | ||
| // rendering with Retry and Cancel permanently disabled and the user | ||
| // has no way out but a reload. Re-entry is already blocked by | ||
| // `autoRetriedRunIdsRef`, so clearing here cannot re-trigger the | ||
| // automatic attempt. | ||
| setBusy((current) => | ||
| current.type !== "none" && current.runId === runId | ||
| ? { type: "none" } | ||
| : current, | ||
| ); | ||
| if (aborted) onRetry?.(aborted); | ||
| }); | ||
| }); | ||
| }, [ | ||
|
|
@@ -304,17 +329,20 @@ export function RunStuckBanner({ | |
| state.runId, | ||
| state.stuckSinceMs, | ||
| threadId, | ||
| awaitingResponse, | ||
| ]); | ||
|
|
||
| // A stale progress timestamp is not actionable while the server is still | ||
| // heartbeating or an unresolved tool/A2A call is known to be running. Keep | ||
| // those healthy long-running states in the chat's inline activity UI; the | ||
| // warning is reserved for runs that may actually need recovery. | ||
| // warning is reserved for runs that may actually need recovery — and only | ||
| // while this chat is still waiting for one (see `isAwaitingResponse`). | ||
| if ( | ||
| !state.isStuck || | ||
| !state.runId || | ||
| backgroundWorkerStillAlive || | ||
| inFlightWork | ||
| inFlightWork || | ||
| !awaitingResponse | ||
| ) { | ||
| return null; | ||
| } | ||
|
|
@@ -380,7 +408,7 @@ export function RunStuckBanner({ | |
| No progress | ||
| {stuckSeconds != null ? ` for ${stuckSeconds}s` : ""}. The agent may | ||
| have hit a server timeout or lost its connection. | ||
| {autoRetry && busyType === "retry" | ||
| {autoRetry && autoRetriedRunId === state.runId | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Clear the automatic retry message before manual retryWhen an automatic retry settles without changing the run ID, |
||
| ? " Retrying automatically now." | ||
| : ""} | ||
| </span> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.