Skip to content

feat(jetbrains): retry failed turns and stop badging manual stops - #13482

Merged
kirillk merged 8 commits into
mainfrom
shiny-glacier
Aug 26, 2026
Merged

feat(jetbrains): retry failed turns and stop badging manual stops#13482
kirillk merged 8 commits into
mainfrom
shiny-glacier

Conversation

@kirillk

@kirillk kirillk commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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 Error badge 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 retry in 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 — only session.revert / session.unrevert.

Resulting behavior:

Path Badges / attention dot Transcript footer
User pressed Stop none one muted line, no icon, no card outline, no actions
Upstream/provider failure ERROR badge + Agents-tab dot error card: error icon, error kind, 5-line scrollable detail, Retry

Implementation

Stop is not a failure

Root cause was one line in KiloBackendActivityManager.handle(), which recorded every session.error as a session failure with no type filter. A Stop publishes session.error carrying MessageAbortedError, so the session entered errors, kind() returned SessionActivityKindDto.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.error type, the session.turn.close reason (interrupted vs error), and the persisted assistant info.error.type — and the transcript already consumed it correctly. Only the activity manager ignored it.

  • MessageErrorDto now owns ABORTED + an aborted predicate in shared/, replacing SessionController's private copy of the string. The filter is event.error?.aborted != true, deliberately not == false, so null-payload global errors keep their existing behavior.
  • SessionOutcomeView.showOutcome splits: INTERRUPTED renders one muted line with no icon and no card outline; FAILED / showError keep the card and now surface the error kind, which previously existed only as an icon tooltip.
  • OutcomeTone was deleted. It was 1:1 with Outcome and, once the two paths rendered differently, no longer decided anything Outcome didn't already answer. This accounts for most of the diff's file count.

Retry

SessionController.retryPrompt() already built exactly the right replay — parts = emptyList() plus messageID of the existing user message and its recorded model/agent/variant — and has shipped in production via resumeAfterLogin(). It was just gated behind isPaidModelAuthRequired. Retry generalises it.

retry() reverts to the failed assistant message, then prompts:

  • Reverting to the failed assistant removes exactly that message and spares the user message, because SessionRevert.cleanup drops everything with id >= messageID — and it does so on the next prompt, which is the one we issue.
  • The revert is what makes this safe when the failed turn already edited files. It is a genuine no-op server-side when the turn patched nothing (files.length === 0 skips both the snapshot requirement and the Effect.die), so one uniform path covers "failed immediately" and "failed after edits" with no branching.
  • Replay is a replay, not a duplicate: createUserMessage honours input.messageID (session/prompt.ts:841) and updateMessage upserts, so empty parts leaves 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 MessageAbortedError tail is explicitly not a failure, so a stopped turn offers no Retry.

Deliberately not a POST /session/:id/retry endpoint. 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.prompt already strips a dangling or finish=error assistant tail on every call, but both seams bail when tail.info.error is set — so an "An error occurred" shell survived a follow-up forever. New sibling seam recoverFailedAssistant handles that case rather than widening recoverProviderFinishError, whose contract is the inverse (finish === "error" with no info.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. A parts.length === 0 guard would never have fired: an errored turn almost always carries a step-start (processor.ts:583).

This is the one change that reaches VS Code and the TUI, hence its own @kilocode/cli changeset.

Three pre-existing issues found while verifying

  1. Three tests in KiloBackendActivityManagerTest never executed. They end with await(...), so runBlocking returned a Map instead of Unit and JUnit silently skipped them — the class reported 8 tests for 11 @Test methods. Two were already broken on main. Pinned to runBlocking<Unit>; now 11.
  2. SessionOutcomeViewTest.findAllCls double-counted. It added a matching child and recursed into it, so any hit that is itself a Container (every Swing component) was counted twice. DialogViewTest's copy is correct; this one now matches.
  3. My hasHeader() change initially regressed DialogView's empty-card default, since headerText starts blank but Swing-visible. Caught by the existing test 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.

before after
Stop → red Error badge on the session row, attention dot on the Agents tab, warning-icon card reading "Response stopped" Stop → no badge, no dot, one muted line reading "Stopped", no buttons
Provider failure → error card, no way to recover except retyping the request Provider failure → error card showing the error kind, with a Retry button that rolls back and replays the turn

How to Test

Manual/local verification

All executed by the agent.

From packages/kilo-jetbrains/:

  • ./gradlew typecheck — passing
  • ./gradlew test — full suite passing
  • SessionOutcomeViewTest 21/21, DialogViewTest 40/40, SessionRetryTest 7/7, KiloBackendActivityManagerTest 11/11 (was silently 8/11)

From packages/opencode/:

  • bun run typecheck — passing
  • bun test test/session/ — 416 pass, 0 fail. Includes prompt.test.ts, which covers both modified call sites.
  • bun test test/kilocode/session/ — 44 pass, 0 fail, including the 8 new recoverFailedAssistant cases

From 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:

  • reverted the MessageAbortedError filter in KiloBackendActivityManageraborted error does not badge the session fails
  • weakened if (!tail.info.error) return in the new seam → leaves a tail with no error to the other recover seams fails
  • disabled the if (!failed) return null retry gate → retry is unavailable after a user stop fails

One 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.ts failed on it. Fixed with MessageV2.AbortedError.isInstance and pinned by a dedicated test.

Reviewer test steps

From packages/kilo-jetbrains/, ./gradlew --no-configuration-cache runIdeSplitMode:

  1. Force a provider failure (invalid API key). The error card shows the error kind and a Retry. Click it → the failed bubble disappears, the turn re-runs from the same request, and no new user message is appended.
  2. Press Stop mid-turn. One muted "Stopped" line, no icon, no card outline, no Retry, and no badge on the history row, worktree row, or Agents tab.
  3. Trigger a failure on a turn that already edited a file, then Retry → the edit is rolled back before the replay, so it is not double-applied.
  4. After a failure, type a normal follow-up instead of clicking Retry → the empty errored bubble is gone from history.
  5. Close and reopen the session tab in both the stopped and failed states → seedOutcome() reproduces the muted note vs the error card from history.

Blocked checks and substitute verification

  • Screenshots/video and the interactive runIdeSplitMode smoke test were not performed — no display available. Substitute: assertions against the real IntelliJ Swing component tree via BasePlatformTestCase. test showOutcome renders interrupted note without icon asserts no visible JBLabel carries an icon; test setOutlined toggles outline color asserts the outline drops and restores; test interrupted note offers no retry and test error card offers retry assert 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 own script/test-runner.ts could not run (createSolidTransformPlugin is not a function, pre-existing and unrelated). Substitute: bun test --conditions=browser directly, which is what the runner wraps.
  • git push required --no-verify: the pre-push hook asserts bun ^1.4.0 and this machine has 1.3.14. The hook runs no code checks, and it was approved explicitly. bun.lock was confirmed unmodified.

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A — not included; blocker and substitute verification described above
  • Changeset considered for user-facing changes — @kilocode/kilo-jetbrains minor (Stop + Retry), @kilocode/cli patch (empty failed turn cleanup)
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

Get in Touch

@kilo-code-bot

kilo-code-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 516 Reverting the failed assistant remaps to the previous user message, so Retry wipes the original prompt
Files Reviewed (1 files)
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/DialogViewTest.kt

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

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 516 Reverting the failed assistant remaps to the previous user message, so Retry wipes the original prompt
Files Reviewed (7 files)
  • .changeset/jetbrains-stopped-session-not-an-error.md
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt - 1 issue
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRetryTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt
  • packages/opencode/src/process.d.ts

Fix these issues in Kilo Cloud

Previous review (commit 0d6f87d)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 512 Reverting the failed assistant remaps to the previous user message, so Retry wipes the original prompt

WARNING

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 553 Retry treats any SessionState.Error as a failed last turn, even when the tail assistant succeeded
Files Reviewed (2 files)
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt - 2 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRetryTest.kt

Fix these issues in Kilo Cloud

Previous review (commit 13a8c29)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 512 Reverting the failed assistant remaps to the previous user message, so Retry wipes the original prompt

WARNING

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 553 Retry treats any SessionState.Error as a failed last turn, even when the tail assistant succeeded
Files Reviewed (2 files)
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt - 2 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRetryTest.kt

Fix these issues in Kilo Cloud

Previous review (commit 0a22721)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 512 Reverting the failed assistant remaps to the previous user message, so Retry wipes the original prompt

WARNING

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 553 Retry treats any SessionState.Error as a failed last turn, even when the tail assistant succeeded
Files Reviewed (29 files)
  • .changeset/clear-empty-failed-turn.md
  • .changeset/jetbrains-stopped-session-not-an-error.md
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt - 2 issues
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRetryTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt
  • packages/opencode/src/kilocode/session/prompt.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/kilocode/session/recover-failed-assistant.test.ts

Fix these issues in Kilo Cloud

Previous review (commit ec94fdc)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (33 files)
  • .changeset/jetbrains-stopped-session-not-an-error.md
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendActivityManager.kt
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendActivityManagerTest.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionState.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/TurnOutcome.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionOutcomeView.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/DialogView.kt
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties
  • packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/SessionOutcomeViewTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/DialogViewTest.kt
  • packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt

Reviewed by grok-4.6 · Input: 211.2K · Output: 10.1K · Cached: 430.1K

Review guidance: REVIEW.md from base branch main

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.
@kirillk kirillk changed the title fix(jetbrains): avoid error badges for stopped sessions feat(jetbrains): retry failed turns and stop badging manual stops Aug 26, 2026
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.
@kirillk
kirillk merged commit 87b6479 into main Aug 26, 2026
34 checks passed
@kirillk
kirillk deleted the shiny-glacier branch August 26, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants