Skip to content

fix(jetbrains): refresh a worktree's PR when its agent stops - #14902

Merged
kirillk merged 2 commits into
mainfrom
playful-sequoia
Oct 7, 2026
Merged

kirillk merged 2 commits into
mainfrom
playful-sequoia

Conversation

@kirillk

@kirillk kirillk commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Problem

A worktree's PR badge in Agent Manager can take up to ~10 minutes to appear after the agent working in that worktree opens the pull request.

From a local log, PR #14897 on branch fluffy-island was created at 09:39:45. The badge appeared at 09:44:23 — and only because an unrelated worktree was created at 09:43:52, which clears the resolver's caches wholesale:

09:38:57  PrResolver - pr lookup skipped, no pull request known here: .../fluffy-island head=9cdc84a5
09:39:45  PR #14897 created (gh pr create, inside the worktree, by the agent)
09:40:40  PrResolver - pr lookup skipped, no pull request known here: .../fluffy-island head=9cdc84a5
09:40:43  worktree PR refresh done items=10 maxAge=default
09:42:42  PrResolver - pr lookup skipped, no pull request known here: .../fluffy-island head=9cdc84a5
09:43:52  worktree add requested: branch=playful-sequoia        <- invalidate() + refreshPr(maxAge=0)
09:44:23  worktree PR refresh done items=11 maxAge=0            <- badge appears

gh was healthy throughout (every probe value=OK exit=0, 296–1946 ms, no UNAUTH/RATE_LIMITED/TIMEOUT), so this is cache invalidation, not a GitHub or auth problem.

PrResolver records "this checkout has no pull request" keyed by path + branch + head commit, for PR_ABSENT_TTL (10 min). gh pr create changes neither the branch nor the head, so the entry stays valid and gh is never re-run for that row. resolver.clear() only runs from IDE-initiated git mutations (worktree add/remove/PR import), and nothing connects an agent opening a PR to it. The 120 s poll and the focus path both ask with a ceiling loose enough to be served that entry, so neither notices either.

What changed

WorktreeStatusService watches live session activity and, for each worktree whose agent leaves RUNNING (turn ended, or stopped on a question, permission or error), records a PR lookup that must resolve that row from gh. It goes through the existing hold/spend path, so the 30 s floor or an in-flight lookup defers it rather than dropping it, and a burst of turn endings collapses onto one lookup carrying every path it named.

prStatus grows a fresh path list for this instead of reusing maxAge = 0. Naming paths keeps the cost proportional: the named rows re-run their lookup, every other row still answers from cache. Forcing maxAge = 0 globally would re-run every PR-less checkout's full three-call gh ladder to notice a pull request on one of them — the same fan-out that took 27 s across 13 worktrees in the log above.

Impact

A PR opened by an agent shows its badge within one floor window of that agent stopping, instead of whenever the absence entry happens to expire. No new polling and no extra gh calls for worktrees nothing happened in.

Not addressed

Two narrower gaps are left alone deliberately, both backstopped by the existing 10-minute TTL:

  • A PR created by hand (terminal, browser) while the IDE keeps focus and no agent is running still waits for the TTL.
  • Away.ceiling accepts a cached absence written during an absence, which can predate a change made in that same window. Tightening it would make every return from a >10 s absence pay a full fan-out; with this fix the agent-driven case no longer depends on it.

Testing

  • ./gradlew :frontend:test --tests "*WorktreeStatusServiceTest*" — 6 new tests (43 total, green). Verified they fail when the new transition handling is disabled.
  • ./gradlew :backend:test --tests "*KiloWorktreeRpcApiImplTest*" --tests "*PrResolverTest*" — green, including a new case for the per-path ceiling.
  • ./gradlew typecheck and the full ./gradlew :frontend:test suite — green.
  • ./gradlew :backend:test as a whole has pre-existing flakiness in KiloBackendAppServiceTest / KiloBackendWorkspaceTest: a different subset fails on each run and the same classes fail on main with these changes stashed. Neither touches worktree or PR code.

A pull request opened from inside a worktree — `gh pr create` in an agent
session, the usual way one appears in Agent Manager — changes neither the
branch nor the head commit. Those are exactly what the backend keys its
"this checkout has no pull request" answer by, so the badge stayed hidden
until that answer expired: up to PR_ABSENT_TTL, 10 minutes, plus a poll.
Nothing local moved, so neither the 120s poll nor a return to the IDE
could tell the difference, and only an unrelated worktree mutation (which
clears the resolver wholesale) dislodged it sooner.

Watch session activity for worktrees whose agent just stopped and resolve
those rows from `gh` on the next lookup. The request goes through the
existing hold/spend path, so a floor or an in-flight lookup defers it
instead of dropping it, and a burst of turn endings collapses onto one
lookup carrying every path it named.

`prStatus` grows a `fresh` path list for this rather than reusing
`maxAge = 0`: a repository with a dozen worktrees would otherwise re-run
every PR-less checkout's full `gh` ladder to notice a pull request on one
of them.
@kilo-code-bot

kilo-code-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Reviewed the new fresh per-path refresh path across the RPC boundary and the turn-ending signal in WorktreeStatusService. Verified: path normalization is consistent on both sides (prPaths/prAge vs the frontend's trimEnd('/') aggregation, both re-normalized with Path.normalize()); the Want merge preserves the strictest ceiling and accumulates named paths; held requests survive both the spend floor and an in-flight lookup via the invokeOnCompletion callback; maxAge = 0 genuinely forces the resolver ladder (usable(..., 0) is always false); the activity collector runs on the service-scoped CoroutineScope (no leaked subscription); and every KiloWorktreeRpcApi implementer (prod impl, fake, delegated test impl) was updated for the new parameter.

Files Reviewed (8 files)
  • .changeset/jetbrains-worktree-pr-badge-after-agent-stops.md
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImpl.kt
  • packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloWorktreeRpcApiImplTest.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/KiloWorktreeService.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeStatusService.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/worktree/WorktreeStatusServiceTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorktreeRpcApi.kt
  • packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorktreeRpcApi.kt

Reviewed by deepseek-v4.1-flash · Input: 181.7K · Output: 32.7K · Cached: 1.9M

Review guidance: REVIEW.md from base branch main

@kirillk
kirillk merged commit 955f55f into main Oct 7, 2026
24 checks passed
@kirillk
kirillk deleted the playful-sequoia branch October 7, 2026 18:41
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