fix(desktop): map subagent timeout/error completion to terminal failed state - #85493
Closed
DavidMetcalfe wants to merge 3 commits into
Closed
fix(desktop): map subagent timeout/error completion to terminal failed state#85493DavidMetcalfe wants to merge 3 commits into
DavidMetcalfe wants to merge 3 commits into
Conversation
1 task
Collaborator
…ousResearch#73728) The backend emits terminal statuses including 'timeout' and 'error' in subagent.complete payloads, but asStatus() only recognised 'completed', 'failed', 'interrupted', and 'queued'. Unrecognised values fell through to 'running', making timed-out subagents immortal in the active status stack. Fix: map timeout/error to 'failed', cancelled/canceled to 'interrupted'. Nonterminal unknown statuses still default to 'running' for forward compatibility. Fixes NousResearch#73728
… surface timeout reason Follow-up to the NousResearch#73728 normalization fix (supersedes the event-agnostic fallback the maintainers flagged as incomplete): - subagent.complete is terminal by definition — an unrecognized status on it now renders as 'failed' instead of falling through to 'running', which would recreate the immortal false-active row for any future backend status (the keep_open request on NousResearch#73859). - Live events keep the lenient 'running' fallback. - Synthesize a 'Timed out after Xs' summary from duration_seconds when the backend completes with status 'timeout' and no summary, so the failed row explains itself. - Tests: timeout reason synthesis + pruning, event-aware fail-closed vs lenient live fallback (13 total).
…text Review feedback: prev?.summary could shadow the 'Timed out after Xs' reason when a live event had populated it. timeoutSummary() now wins for raw timeout status; add coverage for the missing-duration placeholder.
DavidMetcalfe
force-pushed
the
fix/subagent-timeout-status-stuck
branch
from
August 13, 2026 18:20
5b85f15 to
18ff61a
Compare
This was referenced Aug 13, 2026
Contributor
Author
|
Superseded by #86592 — @teknium1's salvage built directly on this branch: @RelaxJonh's normalization commit plus the event-aware fail-closed and timeout-summary commits are the first three commits there. Closing to keep the queue unambiguous; coverage and credits are tracked in #86592. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Timed-out (or exception-failed) subagents stay stuck as "running" in the desktop Agents panel and status-bar count forever — spinner glyph, counted in "Agents N running", never pruned — even though the parent conversation already received the failure. Fixes the immortal false-active rows reported in #73728 (also addresses #85492, filed as a duplicate; same root cause as #80018 and #81602, which this fix resolves too).
Root cause
Status-vocabulary drift between the Python backend and the desktop store. The backend completes subagents with statuses the UI never learned:
tools/delegate_tool.py:2479-2488— hard child timeout (delegation.child_timeout_seconds) emitssubagent.completewithstatus="timeout"(status introduced in fix(delegation): add hard timeout and stale detection for subagent execution #13770)tools/delegate_tool.py:2485— non-timeout exception exit emits the same event withstatus="error"The desktop store's
asStatus()(apps/desktop/src/store/subagents.ts) was an allowlist ofcompleted | failed | interrupted | queued; anything else fell through to'running'. A stuckrunningrow is never cleaned up becausepruneFinishedSessionSubagentsonly prunes terminal-status rows at themessage.startboundary, andactiveSubagentCountkeeps counting it in the status bar.Changes
apps/desktop/src/store/subagents.ts"timeout"/"error"completion statuses to the existing terminalfailedstatus;"cancelled"/"canceled"map tointerrupted(compatibility aliases).subagent.completeis terminal by definition — an unrecognized status on it now rendersfailedinstead of falling through torunning, so a future backend status can't recreate the immortal false-active row. Live events keep the lenientrunningfallback. (This is the event-aware handling requested in review on fix(desktop): normalise timeout/error subagent statuses to terminal (#73728) #73859.)Timed out after Xssummary (from the backend'sduration_seconds) when the raw status istimeoutand no summary arrived, so the row explains why it failed instead of rendering as a bare failure.apps/desktop/src/store/subagents.test.ts— normalization regression test (timeout/error → failed, cancelled/canceled → interrupted, all prunable), timeout reason synthesis, missing-duration placeholder, event-aware fail-closed vs. lenient live fallback. 14 tests.Relationship to #73859
This PR cherry-picks the commit from #73859 (by @RelaxJonh — same normalization mapping, authorship preserved in history) and adds the event-aware fail-closed handling that maintainer review requested on #73859 (@teknium1
keep_open, @GottZ triage 08-02/08-03). That PR has had no author response since 07-30; this PR supersedes it so the requested change doesn't stall the fix.Testing
npx vitest run src/store/subagents.test.ts— 14 passed (10 pre-existing + 4 new)npx eslint src/store/subagents.ts src/store/subagents.test.ts— cleanNotes
Open question: a distinct
timeoutstatus with its own glyph/label (instead of mapping tofailed) would be richer UI, but is a much larger surface change — type union, glyph mapping, i18n strings, status-bar counts — for the same user-visible outcome. This PR keeps the narrow mapping; the synthesized summary line preserves the reason.Closes #73728