Skip to content

fix(claude-swe-agent): stop a fast turn from racing the Remote Control session URL (#183) - #191

Merged
imaustink merged 1 commit into
mainfrom
fix/ai-review-session-link-race-183
Aug 1, 2026
Merged

fix(claude-swe-agent): stop a fast turn from racing the Remote Control session URL (#183)#191
imaustink merged 1 commit into
mainfrom
fix/ai-review-session-link-race-183

Conversation

@k5s-bot

@k5s-bot k5s-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Fixes #183 — the "🤖 Starting work on this now. Watch live or take over the session here…" comment is posted for most runs but dropped intermittently on the ai-review flow. The maintainer's note nails it: "most of the time it does. Sometimes it fails to … This should be deterministic logic." It is a timing race, and this makes it deterministic.

Root cause

In runClaudeTurnRemoteControlled (apps/claude-code-swe-agent/src/claude-runner.ts), the poll loop:

  • emits the session URL the instant the transcript's bridgeSessionId line appears (remote-control-url progress event → gateway posts the comment), and
  • returns the instant it sees a turn_duration entry (turn finished).

The bridgeSessionId line is written when the Remote Control bridge's async registration handshake with claude.ai completes — which is not guaranteed to happen before the turn does. A long triage turn registers the bridge many polls before it finishes, so the URL is always emitted first. A fast, read-only review can finish in the same poll window the bridge is still registering in — and returning the moment turn_duration lands drops the link. Which of the two the transcript flushed first is a race, so the link appears "most of the time" and vanishes on the quick review turns. (ai-review runs use claude-code-swe-agent with Remote Control; e.g. AgentRun fc9f0896 referenced in the code.)

Everything downstream (agent-runtime → orchestrator remoteControlUrlListener → gateway onRemoteControlUrl) is already deterministic and ordered — the URL is only ever dropped because it was never emitted in this race.

Fix

When a turn completes and the URL has not been reported yet, hold the finished turn open for a bounded grace (REMOTE_CONTROL_URL_GRACE_MS, 15s, overridable via urlGraceMs) and keep re-reading the transcript so a just-registered bridge still surfaces its link before returning.

  • Zero cost on the happy path — skipped entirely whenever the bridge registered mid-turn and the URL was already emitted (every long/triage turn, and any review where registration won the race).
  • Bounded — a bridge that never registers (genuine failure) can't hang a completed review; the grace exits and the result is returned regardless.
  • The interactive child stays resident until the finally kill, so registration keeps progressing throughout the wait.

Tests

Adds a latebridge fake-script mode (writes turn_duration first, then appends the bridgeSessionId line ~60ms later) and a regression test asserting the URL is still reported when the bridge line lands after completion. Verified the test fails against the old return-on-completion behavior and passes with the fix; tsc --noEmit is clean.

Two pre-existing tests in this file (went silent as a stall, never gives up on a session reporting busy) flake under parallel load on the ARM CI box used here — they pass in isolation and exercise the running/busy modes, which never reach the turnComplete branch this PR touches.


Maintainer: apply the ai-review label to this PR to request an automated code review, or the ai-triage label to have that review feedback addressed and the branch brought back in sync with its base. (The automation can't label its own PR, so this needs a human.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01FLZRLE1mqh9ajMYyKyaWSr

…l session URL (#183)

The "watch live / take over the session" link is posted for most runs but
dropped intermittently on the `ai-review` flow (issue #183). Root cause is a
timing race in `runClaudeTurnRemoteControlled`'s poll loop:

  - the session URL is emitted the instant the transcript's `bridgeSessionId`
    line appears, and
  - that line is written when the bridge's ASYNC registration handshake with
    claude.ai completes -- not necessarily before the turn does.

A long triage turn registers the bridge many polls before it finishes, so the
URL is always emitted first. A fast, read-only review can finish in the same
poll window the bridge is still registering in; the loop returned the instant it
saw `turn_duration`, so which of the two the transcript flushed first decided
whether the link was ever emitted. That is the non-deterministic "sometimes it
fails to" the maintainer observed.

Make it deterministic: when a turn completes and the URL has not been reported
yet, hold the finished turn open for a bounded grace (`REMOTE_CONTROL_URL_GRACE_MS`,
15s, overridable) and keep re-reading the transcript so a just-registered bridge
still surfaces its link before returning. Skipped entirely on the common path
where the bridge registered mid-turn, so it costs nothing there; bounded so a
bridge that never registers can't hang a completed review.

Adds a `latebridge` fake-script mode and a regression test asserting the URL is
still reported when the `bridgeSessionId` line lands after `turn_duration`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLZRLE1mqh9ajMYyKyaWSr
@imaustink
imaustink merged commit b29e6aa into main Aug 1, 2026
4 checks passed
@imaustink
imaustink deleted the fix/ai-review-session-link-race-183 branch August 1, 2026 22:46
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.

Review Agent Doesn't Post a Session Link

1 participant