Skip to content

fix(jetbrains): recover pending question/permission after reconnect - #14440

Merged
kirillk merged 2 commits into
mainfrom
cosmic-cobble
Sep 22, 2026
Merged

kirillk merged 2 commits into
mainfrom
cosmic-cobble

Conversation

@kirillk

@kirillk kirillk commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes #

Context

After a plan finishes and calls plan_exit, the CLI intentionally stays busy while it asks a blocking follow-up question ("Ready to implement?"). If that question.asked event is lost during a backend SSE/chat-stream reconnect (those streams do not replay), the JetBrains session shows a generic "Considering next steps" busy state forever, even though the CLI's pending-question endpoint still reports it. The only known workaround was stopping the session and sending "continue".

Implementation

SessionController.recoverPending() already rehydrates pending permissions/questions/status after history load, but despite its own doc comment claiming to also cover "reconnect", it was never invoked on reconnect.

  • Track the previous app-readiness status (lastAppStatus). On a subsequent non-READY -> READY transition for an already-loaded local session (not the very first connect, not a READY -> READY config/profile refresh), call new recoverOnReconnect(), which re-runs recoverPending().
  • Guard the recovery commit with a monotonic promptRevision counter, bumped whenever a live permission/question ask/reply/reject is handled. The recovery snapshot captures the revision before its suspending REST calls and discards the result if the revision changed before the EDT commit — so a stale reconnect snapshot can never overwrite a fresher live question/permission, or resurrect one that was just answered/rejected.
  • Fixed a related latent bug found while auditing this path: session.status "idle" preserved AwaitingPermission but not AwaitingQuestion, unlike the existing session.idle handler. An idle status between turns could otherwise hide a blocking question card.

No CLI, backend RPC, or DTO changes were needed — the pending-list endpoints already existed and were already used for history-load recovery.

Screenshots / Video

N/A — no visual/UI change, only session-state recovery timing.

How to Test

Manual/local verification

  • Ran ./gradlew typecheck and the full :frontend:test suite from packages/kilo-jetbrains/ (agent-run) — all green.
  • Ran the new/changed tests individually first to confirm they exercise the exact race conditions (missed question during reconnect, stale-snapshot-vs-live-event races, idle-status guard) before running the full suite.

Reviewer test steps

  1. In a JetBrains sandbox (./gradlew runIdeSplitMode or runIde), start a Plan-mode session and let it call plan_exit.
  2. Force a backend reconnect during the window before the "Ready to implement?" card is delivered (e.g. via the Split Mode latency widget or by briefly killing/restarting the CLI process) so the question.asked event is dropped.
  3. Confirm the session now surfaces the "Ready to implement?" card instead of staying stuck on "Considering next steps", without needing to stop and resend "continue".

Blocked checks and substitute verification

  • Did not reproduce the exact reconnect timing in a live sandbox run (requires precise timing of a dropped SSE event); substitute verification is the new deterministic unit tests in SessionRecoveryTest.kt that simulate the same race via a gated fake RPC (pendingGate) and explicit app-state transitions, plus the ListenerLifecycleTest.kt idle-status regression test.

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A
  • Changeset considered for user-facing changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

Get in Touch

A completed plan's follow-up question (or any permission/question) asked
while the backend SSE/chat stream reconnects can be lost, since those
streams do not replay. The session then shows Busy indefinitely even
though the CLI's pending-list endpoints still report it.

SessionController.recoverPending() already rehydrates pending prompts
after history load, but was never invoked on reconnect despite its own
comment claiming to do so. Trigger it on a subsequent non-READY -> READY
app-state edge for an already-loaded session, guarded by a monotonic
prompt revision so a stale snapshot can never overwrite a fresher live
question/permission or resurrect one that was just answered/rejected.

Also fix session.status "idle" to preserve AwaitingQuestion, matching
the existing session.idle handler, so an idle status between turns
cannot hide a blocking question.
@kilo-code-bot

kilo-code-bot Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt
Previous Review Summary (commit 0800c04)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 0800c04)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 1128 The first-connect exclusion is ineffective: lastAppStatus is DISCONNECTED (not null) on the first emission, so recoverOnReconnect() also runs on the initial connect, re-running pending-list recovery and possibly duplicate auto-approve replies.

SUGGESTION

File Line Issue
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 1622 recoverPending's generic catch (e: Exception) swallows the CancellationException raised by the new cancel() / cs.cancel(), logging a spurious recovery failure.
packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt 1670 The revision guard only counts live events, so a locally answered question can be resurrected by an in-flight snapshot when the echoed question.replied is delayed or lost.
packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt 646 test initial ready does not trigger a second reconnect recovery never sets appRpc.state to READY, so it cannot exercise (or catch a regression in) the first-READY path it names.
Files Reviewed (5 files)
  • .changeset/jetbrains-reconnect-question-recovery.md
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt - 3 issues
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/ListenerLifecycleTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionRecoveryTest.kt - 1 issue
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

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.

2 participants