Repository navigation
fix(ui): scope thinking timer to turn and reset on request id change #7769
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 |
|---|---|---|
|
|
@@ -140,22 +140,81 @@ function WaitingLine({ kind }: { kind: WaitingOnUser }) { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Maximum reasonable thinking duration for a single turn (15 minutes). | ||
| * Any timestamp older than this is considered stale from an abandoned/crashed turn. | ||
| */ | ||
| const MAX_TURN_THINKING_MS = 15 * 60 * 1000; | ||
|
|
||
| /** Thread ID -> { requestId: string, startedAt: number }. Scoped strictly to the active turn. */ | ||
| const threadTurnStarts = new Map<string, { requestId: string; startedAt: number }>(); | ||
|
|
||
| export function AgentRunningStatus() { | ||
| const { t } = useT(); | ||
| const summary = useTaskSummary(); | ||
| const strings = useAgentStatusStrings(); | ||
| const waiting = useWaitingOnUser(); | ||
| const parentWaits = useParentWaitsOnSubagents(); | ||
| const threadId = useAuiThreadId(); | ||
| const [tick, setTick] = useState(0); | ||
| const [startedAt] = useState(() => Date.now()); | ||
|
|
||
| const liveRequestId = useAppSelector(state => | ||
| threadId ? state.chatRuntime.liveRequestIdByThread?.[threadId] : undefined | ||
| ); | ||
| const streamingRequestId = useAppSelector(state => | ||
| threadId ? state.chatRuntime.streamingAssistantByThread?.[threadId]?.requestId : undefined | ||
| ); | ||
| const currentRequestId = liveRequestId ?? streamingRequestId ?? 'active'; | ||
|
|
||
| const thinking = summary.total === 0 && waiting === null; | ||
|
|
||
| const [startedAt, setStartedAt] = useState(() => { | ||
| if (threadId) { | ||
| const existing = threadTurnStarts.get(threadId); | ||
| if ( | ||
| existing && | ||
| existing.requestId === currentRequestId && | ||
| Date.now() - existing.startedAt < MAX_TURN_THINKING_MS | ||
|
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject future thinking timestamps. If the system clock moves backward while a thread is unmounted, both age checks accept its stored Also applies to: 195-195 🤖 Prompt for AI Agents |
||
| ) { | ||
| return existing.startedAt; | ||
| } | ||
| const now = Date.now(); | ||
| threadTurnStarts.set(threadId, { requestId: currentRequestId, startedAt: now }); | ||
| return now; | ||
| } | ||
| return Date.now(); | ||
| }); | ||
|
|
||
| // Keep startedAt synchronized strictly to the current response / request ID. | ||
| useEffect(() => { | ||
| if (threadId) { | ||
| const existing = threadTurnStarts.get(threadId); | ||
| if ( | ||
| !existing || | ||
| existing.requestId !== currentRequestId || | ||
| Date.now() - existing.startedAt >= MAX_TURN_THINKING_MS | ||
|
Comment on lines
+193
to
+195
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Adopt a matching start time when the thread changes. If this component changes from thread A to thread B and B already has a matching map entry, this condition is false. 🤖 Prompt for AI Agents
Comment on lines
+191
to
+195
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.
When the app-wide Useful? React with 👍 / 👎. |
||
| ) { | ||
| const now = Date.now(); | ||
| threadTurnStarts.set(threadId, { requestId: currentRequestId, startedAt: now }); | ||
| setStartedAt(now); | ||
| } else { | ||
| setStartedAt(existing.startedAt); | ||
| } | ||
| } | ||
| }, [threadId, currentRequestId]); | ||
|
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Restart the timer when thinking resumes for the same request. When 🤖 Prompt for AI AgentsThere 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.
When a request remains active while the WebView is suspended or backgrounded for more than 15 minutes, neither dependency changes, so this effect never re-evaluates the stale check even though the ticking state later rerenders the component. Useful? React with 👍 / 👎. |
||
|
|
||
| const elapsedMs = useTaskElapsed({ startedAt }, thinking); | ||
|
|
||
| useEffect(() => { | ||
| if (!thinking) return undefined; | ||
| if (!thinking) { | ||
| if (threadId) { | ||
| threadTurnStarts.delete(threadId); | ||
| } | ||
| return undefined; | ||
| } | ||
| const timer = window.setInterval(() => setTick(value => value + 1), 120); | ||
| return () => window.clearInterval(timer); | ||
| }, [thinking]); | ||
| }, [thinking, threadId]); | ||
|
|
||
| if (waiting !== null) return <WaitingLine kind={waiting} />; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a turn ends before
inference_startsupplies a request ID—for example,chatSendrejects afterbeginInferenceTurnor the workflow RPC throws—the status unmounts whilethinkingis still true, so its cleanup leaves the map entry behind. A retry on the same thread also resolves to the shared'active'value and reuses the failed turn's timestamp for up to 15 minutes, despite being a separate turn; use a per-turn lifecycle identity or invalidate this fallback entry when that lifecycle ends.Useful? React with 👍 / 👎.