feat(jetbrains): retry failed turns and stop badging manual stops - #13482
Merged
Conversation
Contributor
Code Review SummaryStatus: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
Files Reviewed (1 files)
Fix these issues in Kilo Cloud Previous Review Summaries (5 snapshots, latest commit 4e25086)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 4e25086)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
Files Reviewed (7 files)
Fix these issues in Kilo Cloud Previous review (commit 0d6f87d)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (2 files)
Fix these issues in Kilo Cloud Previous review (commit 13a8c29)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (2 files)
Fix these issues in Kilo Cloud Previous review (commit 0a22721)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (29 files)
Fix these issues in Kilo Cloud Previous review (commit ec94fdc)Status: No Issues Found | Recommendation: Merge Files Reviewed (33 files)
Reviewed by grok-4.6 · Input: 211.2K · Output: 10.1K · Cached: 430.1K Review guidance: REVIEW.md from base branch |
johnnyeric
approved these changes
Aug 26, 2026
marius-kilocode
approved these changes
Aug 26, 2026
Adds a Retry action to the error card. Retry reverts to the failed assistant message, which restores the workspace when that turn already edited files, then replays the original user message with the same model. Reusing the user message id means no synthetic message is appended, and SessionRevert.cleanup removes the failed message on the prompt that follows. Retry is offered only for failures, never for a user stop. Also clears a failed assistant tail that produced no visible output when the next prompt arrives, so an empty error placeholder stops lingering in history. Turns that emitted text or ran a tool are kept, since their record explains changes already on disk.
Retry replayed the model, agent and effort recorded on the failed turn, so switching away from a broken model and pressing Retry just failed the same way. It now resolves model/agent/effort from the live selection the way a normal send does, falling back to the recorded values when no selection has resolved yet. Login resume keeps using the recorded model: the user authenticated for the model that demanded it, so substituting the current selection there would silently run a different one.
The auto-routing selection is kilo/kilo-auto/free, so the model id itself contains a slash and only the provider may be split off the front. Pins that parseModel keeps the remainder intact on the retry path.
This was referenced Aug 26, 2026
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.
Issue
No existing issue — reported directly. Exception: behavior bug found while using the JetBrains plugin, with a follow-on gap (no way to retry a failed turn) found during the investigation.
Context
Two related problems with how a JetBrains session reports the end of a turn.
1. A manual Stop looked like a failure. Pressing Stop put a red
Errorbadge on the history/recents and Agent Manager worktree rows and lit the attention dot on the Agents tab. A deliberate user action was reported as a fault and stayed that way until the next turn.2. A failed turn left you stranded. When a turn died from a provider error, there was no way to retry it. The
retryin the codebase is the CLI's automatic backoff (SessionRetry.policy); once that gave up, the only option was to retype the request. The server had no retry primitive either — onlysession.revert/session.unrevert.Resulting behavior:
ERRORbadge + Agents-tab dotImplementation
Stop is not a failure
Root cause was one line in
KiloBackendActivityManager.handle(), which recorded everysession.erroras a session failure with no type filter. A Stop publishessession.errorcarryingMessageAbortedError, so the session enterederrors,kind()returnedSessionActivityKindDto.ERROR, and that fanned out to every badge surface. Both sibling clients already filtered that exact error name (kilo-vscode/src/services/attention/service.ts:121,tui/src/feature-plugins/system/notifications.ts:82); JetBrains was the outlier.The signal already existed on three channels — the
session.errortype, thesession.turn.closereason (interruptedvserror), and the persisted assistantinfo.error.type— and the transcript already consumed it correctly. Only the activity manager ignored it.MessageErrorDtonow ownsABORTED+ anabortedpredicate inshared/, replacingSessionController's private copy of the string. The filter isevent.error?.aborted != true, deliberately not== false, so null-payload global errors keep their existing behavior.SessionOutcomeView.showOutcomesplits:INTERRUPTEDrenders one muted line with no icon and no card outline;FAILED/showErrorkeep the card and now surface the error kind, which previously existed only as an icon tooltip.OutcomeTonewas deleted. It was 1:1 withOutcomeand, once the two paths rendered differently, no longer decided anythingOutcomedidn't already answer. This accounts for most of the diff's file count.Retry
SessionController.retryPrompt()already built exactly the right replay —parts = emptyList()plusmessageIDof the existing user message and its recorded model/agent/variant — and has shipped in production viaresumeAfterLogin(). It was just gated behindisPaidModelAuthRequired. Retry generalises it.retry()reverts to the failed assistant message, then prompts:SessionRevert.cleanupdrops everything withid >= messageID— and it does so on the next prompt, which is the one we issue.files.length === 0skips both the snapshot requirement and theEffect.die), so one uniform path covers "failed immediately" and "failed after edits" with no branching.createUserMessagehonoursinput.messageID(session/prompt.ts:841) andupdateMessageupserts, so emptypartsleaves the original user parts intact and no synthetic message is appended.Guards: retry needs an idle session, no operation in flight, and a tail that is an assistant which failed off the last user message. A
MessageAbortedErrortail is explicitly not a failure, so a stopped turn offers no Retry.Deliberately not a
POST /session/:id/retryendpoint. That would need an SDK regen plus a JetBrains CLI pin bump to wrap primitives every client already has. Worth promoting later if VS Code or the TUI want it. Also skipped: "Retry with a different model", which is the more useful action for a hard provider outage but needs a model picker on the card.Clearing the empty failed turn
Session.promptalready strips a dangling orfinish=errorassistant tail on every call, but both seams bail whentail.info.erroris set — so an "An error occurred" shell survived a follow-up forever. New sibling seamrecoverFailedAssistanthandles that case rather than wideningrecoverProviderFinishError, whose contract is the inverse (finish === "error"with noinfo.error).Its parts guard is an allowlist of turn scaffolding (
step-start/step-finish), not a denylist. A turn that emitted text or ran a tool keeps its message, because that record is what explains file changes still on disk, and any part type the seam doesn't recognise fails safe by blocking removal. Aparts.length === 0guard would never have fired: an errored turn almost always carries astep-start(processor.ts:583).This is the one change that reaches VS Code and the TUI, hence its own
@kilocode/clichangeset.Three pre-existing issues found while verifying
KiloBackendActivityManagerTestnever executed. They end withawait(...), sorunBlockingreturned aMapinstead ofUnitand JUnit silently skipped them — the class reported 8 tests for 11@Testmethods. Two were already broken onmain. Pinned torunBlocking<Unit>; now 11.SessionOutcomeViewTest.findAllClsdouble-counted. It added a matching child and recursed into it, so any hit that is itself aContainer(every Swing component) was counted twice.DialogViewTest's copy is correct; this one now matches.hasHeader()change initially regressedDialogView's empty-card default, sinceheaderTextstarts blank but Swing-visible. Caught by the existingtest empty card does not render a header row by default.Screenshots / Video
Not captured — see "Blocked checks" below. Verified through assertions on the real Swing component tree instead.
Errorbadge on the session row, attention dot on the Agents tab, warning-icon card reading "Response stopped"How to Test
Manual/local verification
All executed by the agent.
From
packages/kilo-jetbrains/:./gradlew typecheck— passing./gradlew test— full suite passingSessionOutcomeViewTest21/21,DialogViewTest40/40,SessionRetryTest7/7,KiloBackendActivityManagerTest11/11 (was silently 8/11)From
packages/opencode/:bun run typecheck— passingbun test test/session/— 416 pass, 0 fail. Includesprompt.test.ts, which covers both modified call sites.bun test test/kilocode/session/— 44 pass, 0 fail, including the 8 newrecoverFailedAssistantcasesFrom the repo root:
check-opencode-annotations.ts --worktree(all shared changes annotated),check-opencode-promise-facades.ts(no drift),check-md-table-padding.ts(clean),bun run lint(0 errors).Mutation-checked the three load-bearing guards — each new test was confirmed to fail without its production change and pass with it:
MessageAbortedErrorfilter inKiloBackendActivityManager→aborted error does not badge the sessionfailsif (!tail.info.error) returnin the new seam →leaves a tail with no error to the other recover seamsfailsif (!failed) return nullretry gate →retry is unavailable after a user stopfailsOne genuine bug was caught this way rather than by review: the seam initially stripped user-aborted tails too, which would have deleted the record of a Stop and contradicted the first half of this PR.
test/session/prompt.test.tsfailed on it. Fixed withMessageV2.AbortedError.isInstanceand pinned by a dedicated test.Reviewer test steps
From
packages/kilo-jetbrains/,./gradlew --no-configuration-cache runIdeSplitMode:seedOutcome()reproduces the muted note vs the error card from history.Blocked checks and substitute verification
runIdeSplitModesmoke test were not performed — no display available. Substitute: assertions against the real IntelliJ Swing component tree viaBasePlatformTestCase.test showOutcome renders interrupted note without iconasserts no visibleJBLabelcarries an icon;test setOutlined toggles outline colorasserts the outline drops and restores;test interrupted note offers no retryandtest error card offers retryassert the footer button, the latter by clicking it and observing the handler fire. Reviewer steps 1–3 are the outstanding human confirmation, particularly step 3 (snapshot rollback).packages/opencode/'s ownscript/test-runner.tscould not run (createSolidTransformPlugin is not a function, pre-existing and unrelated). Substitute:bun test --conditions=browserdirectly, which is what the runner wraps.git pushrequired--no-verify: the pre-push hook asserts bun^1.4.0and this machine has 1.3.14. The hook runs no code checks, and it was approved explicitly.bun.lockwas confirmed unmodified.Checklist
@kilocode/kilo-jetbrainsminor (Stop + Retry),@kilocode/clipatch (empty failed turn cleanup)Get in Touch