Repository navigation
fix(progress): don't regress progress when an update is in flight - #60
Open
Tanish Gudise (TanishGudise) wants to merge 2 commits into
Open
Tanish Gudise (TanishGudise) wants to merge 2 commits into
Tanish Gudise (TanishGudise) wants to merge 2 commits into
Conversation
close() re-queued _last_emitted whenever _latest was empty, but the run loop may still hold a newer state (sleeping out the throttle delay or retrying). The stale state was then sent after the newer one, so clients saw progress go backwards (e.g. 4 -> 1), violating the MCP requirement that progress increase monotonically. The close telemetry event also reported the stale value. advance() and the monotonicity check had the same blind spot and could compute a new value from the stale base. Track the state the emitter has taken but not yet delivered and treat it as newer than _last_emitted. Skip the at-least-once re-emit on close when an update is in flight, since that update will be delivered. Fixes test_progress_telemetry_hooks_capture_events, which failed consistently on main, and adds regression tests for both paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GonJZXszkMqG1RmHhkVaFe
Tanish Gudise (TanishGudise)
marked this pull request as draft
October 7, 2026 19:17
Add a HeldSession fake that holds each send until the test releases it, so a state stays in flight without relying on sleeps. With emit_hz=0 the new tests don't depend on timing: - close() while a send is in flight doesn't re-send an older value - advance() builds on the in-flight state - close() while a failed send is being retried doesn't regress - set() below the in-flight value raises ValueError All four fail on every run against main. The existing throttle-delay tests are kept, since they cover the real-world path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tanish Gudise (TanishGudise)
marked this pull request as ready for review
October 7, 2026 20:11
This branch has not been deployed
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.
Description
The progress emitter can send progress backwards. This is also why
test_progress_telemetry_hooks_capture_eventsis flaky onmain(it failed 1 in 6 runs locally).runloop takes the newest state (_latest = None), then waits (for the throttle delay, or to retry a failed send) before sending it.progress()block exits during that wait,close()sees_latest is Noneand re-queues_last_emittedfor at-least-once delivery, but_last_emittedis older than the state the loop is holding.1 → 4 → 1, which violates the MCP requirement that progress increase monotonically. The close telemetry event also reports the stale value.advance()and the monotonicity check inset()have the same blind spot. Whileset(3)is in flight,advance(1)builds on_last_emittedand returns 2 instead of 4, andset(2)is accepted and sent after 3.Fix
_in_flight: the state the emitter has taken but not yet delivered. A state moves from_latestto_in_flightto_last_emitted, and each handoff happens under the lock._newest_state_locked()checks queued, then in-flight, then delivered state.advance()uses it for its base, and the monotonicity check uses it too.close()skips the at-least-once re-emit while an update is in flight, because the retry loop guarantees that update is delivered.Behavior change: calling
set()with a value below one that is still in flight now raisesValueError. Previously it quietly sent the lower value after the higher one. This enforces the monotonicity the method already documents and the MCP spec requires.Related Issue
No issue, since issue creation is restricted on this repo. I found this through the flaky test on
main.Type of Change
Checklist
Additional Notes
Tests. I added 6 tests. The 4 deterministic ones use a
HeldSessionfake that holds each send until the test releases it, withemit_hz=0, so nothing depends on timing. All 4 fail on every run againstmainand pass with this change.test_close_while_send_in_flight_does_not_regresstest_advance_builds_on_held_in_flight_sendtest_close_during_retry_does_not_regress(the retry path)test_set_below_in_flight_value_raises(the behavior change)test_close_during_throttled_send_does_not_regressandtest_advance_uses_in_flight_state_as_basecover the real-world throttle-delay path.Results.
tests/test_progress_flow.pypasses 11/11 across 10 repeated runs. The full suite passes (896).ruff format --checkis clean.ruff checkreports new findings in the test file, but they're the same rule types the file already has, and the lint step already fails onmain.AI disclosure (per CONTRIBUTING.md): I used Claude to help trace the race and draft the tests. I've reviewed the change and can walk through it.