fix: report retry count on llm:response so a retried call stops looking clean - #38
Closed
Michael J. Jabbour (michaeljabbour) wants to merge 1 commit into
Closed
fix: report retry count on llm:response so a retried call stops looking clean#38Michael J. Jabbour (michaeljabbour) wants to merge 1 commit into
Michael J. Jabbour (michaeljabbour) wants to merge 1 commit into
Conversation
…ng clean A 30-minute failure was logged as a success. In session `eec9ae98` the resume-2 turn spent 14:19:26 -> 14:49:44 on three sequential 600 s timeouts and emitted one `llm:response` with `duration_ms: 1818942` (30.3 min) and `status: ok` -- a single "successful" call swallowing three timeouts, hiding the failure from every latency metric that keys on status. The same forensic section reports 4 of 10 provider failures never reached `events.jsonl` at all. Root cause, from `amplifier_core.utils.retry.retry_with_backoff`: `on_retry` is invoked only before a retry *sleep*. Two paths raise without ever calling it -- a non-retryable error, and the terminal failure once retries are exhausted. So the most important failure in any sequence is precisely the one the `provider:retry` stream never sees. Meanwhile `elapsed_ms` already spans the whole retry loop, so a long duration was indistinguishable from a slow model, and `llm:response` -- the one event every consumer already reads -- said nothing about attempts at all. Track the retry count inside the call and surface it as `retries` on `llm:response`: on the success path and on both error paths, so the two paths `on_retry` never reaches are covered. A 30-minute success now describes itself: `status: ok, duration_ms: 1818942, retries: 3`. Additive, no contract change. `retries` is always present -- an absent key is indistinguishable from an old producer, so consumers never have to guess whether zero means "none" or "not reported". Five tests cover the shapes that mattered in the incident: a success that swallowed retries reports them; a clean success reports `retries: 0`; a terminal failure reports what it cost (the only record, since `on_retry` never fires there); a non-retryable failure reports zero; and duration and retries are reported together, because neither is actionable alone. Also collapses one pre-existing `ruff format` drift in the touched function, which is why the repo's format drift drops from 6 files to 5. Verified: `ruff check` clean, 298 passed (baseline 293). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Contributor
Author
|
Withdrawing this. It was pushed as a branch directly into this repo; it should No conflict with anyone else's work here; withdrawing for consistency. The |
Michael J. Jabbour (michaeljabbour)
deleted the
fix/retry-observability-gaps
branch
August 20, 2026 10:33
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.
Summary
llm:responsenow carriesretries— the number of retries burned inside that one logical call — on the success path and on both error paths.The bug
From the forensic report on session
eec9ae98, §7:The same section reports 4 of 10 provider failures never appeared in
events.jsonlat all.Root cause
Verified by reading
amplifier_core.utils.retry.retry_with_backoff:on_retryis invoked only before a retry sleep. Two paths raise without ever notifying it:So the most important failure in any sequence is precisely the one the
provider:retrystream never sees.Meanwhile
elapsed_msalready spans the whole retry loop, so a long duration was indistinguishable from a slow model — andllm:response, the one event every consumer already reads, said nothing about attempts at all.What changed
amplifier_module_provider_vllm/__init__.py— track the retry count in the enclosing scope (_on_retryrecordsattemptvianonlocal) and emit it asretriesonllm:responsein all three exit paths: success, and the two error pathson_retrynever reaches.A 30-minute success now describes itself:
retriesis always present, including as0. An absent key is indistinguishable from an old producer, so consumers never have to guess whether "missing" means none or not-reported.Tests
tests/test_retry_observability.py— five tests, one per shape that mattered in the incident:test_a_success_that_swallowed_retries_reports_themstatus: okhiding retriestest_a_clean_success_reports_no_retriesretries: 0is present and zero, not absenttest_a_terminal_failure_reports_what_it_coston_retrynever fires theretest_a_non_retryable_failure_reports_zeroon_retrynever fires here eithertest_duration_and_retries_are_reported_togetherThat last one is the point of the whole change: 30 minutes with
retries: 0is a slow model; 30 minutes withretries: 3is three dead connections. The incident could not tell those apart.Verification
Baseline on
mainis 293; the five new tests bring it to 298.New tests in isolation:
What this does not fix
Stating this plainly so the PR isn't read as more than it is:
anchors:builderdelegate failures (§7d) are out of scope. They live in a different repo.This change makes the failures legible. It does not make them stop.
Note on pre-existing format drift
Five test files in this repo have
ruff formatdrift that predates this branch. They are deliberately left alone as out of scope.Confirmed by checking the pristine
HEADblobs directly (no worktree mutation): 6 files drift on pristinemain, 5 with this work applied. The difference isamplifier_module_provider_vllm/__init__.pyitself — this PR contains one incidental formatting hunk collapsing acontinuation_input.append(...)call in a function it touches, which is why the count drops by one. Both files this PR changes passruff format --check.Related
Sibling PRs from the same incident:
action == "modify"branch from orchestrator contract amplifier-core#102🤖 Generated with Amplifier