Repository navigation
fix(client): name the cause inside an exception group in every log (see #224) - #225
Merged
Merged
Conversation
#224) str(ExceptionGroup) is 'unhandled errors in a TaskGroup (1 sub-exception)' -- it names neither the type nor the message of what failed. Every remote transport path in ClientManager runs inside an anyio task group, so that was the entire content of 20 log sites, and it is what a week of CI hangs (#200) produced instead of a cause. describe_exception() flattens a group to its leaves and renders 'ConnectionResetError: peer went away'. Duck-types .exceptions rather than isinstance: 3.11+ raises builtin BaseExceptionGroup, 3.10 raises exceptiongroup.ExceptionGroup from the backport, and neither name imports on the other. Output goes through sanitize_auth_diagnostic -- flattening widens how much exception text reaches the log, and these come from an HTTP transport, so most sites are newly redacted as well as newly useful. Applied at all 20 sites, not just the 6 seen in the CI log, and to the error string disconnect_server returns to its caller. An AST guard fails if a future edit logs a caught exception directly -- that guard is what found the other 14.
…st in logs (see #224) Board review found the first cut was a narrower fix than it claimed, and its AST guard was a false green: it only looked inside logger.* calls, so it passed while seven sites still rendered a group as 'unhandled errors in a TaskGroup': - read_failure_reason = f'stdout read error: {e}' -- logged twice AND stored as last_error, via an intermediate the guard could not see - last_error = str(e) at four sites -- these reach pmcp status, pmcp doctor and health output, further than any log line - the error list connect_server returns to its caller, and thence to provision output The guard is now the broad rule: inside 'except ... as e', neither f'{e}' nor str(e) may appear anywhere; describe_exception(e) is a Call and passes. A test feeds it the indirect shape that defeated version one and requires a finding. New wiring test asserts last_error names the leaf.
…aw exception (see #224) Board finding: exc_info= passes the raw exception to the logging machinery, which appends the unredacted exception tree AFTER the sanitised message -- so a bearer token in a transport error reached the log in full while the line above it looked clean. The tests could not see it because they inspect record.getMessage(), which excludes exc_info output. Formatting the traceback in-process and passing it through sanitize_auth_diagnostic(max_length=None) keeps every frame and closes the hole. Two guards: no logger call may pass exc_info, and a formatted traceback carrying a secret must come back redacted with its frames intact.
This was referenced Sep 5, 2026
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.
See #224. First half of stage 2: make the failures legible. No behavioural change to the transport itself.
The defect. Every remote-transport path in
ClientManagerruns inside an anyio task group, andstr(ExceptionGroup)is"unhandled errors in a TaskGroup (1 sub-exception)"— it names neither the type nor the message of what actually failed. That string was the entire content of 20 log sites, and it is what a week of CI hangs (#200) produced instead of a cause:The fix.
describe_exception()flattens a group to its leaves and rendersConnectionResetError: peer went away..exceptionsrather thanisinstance: 3.11+ raises the builtinBaseExceptionGroup, 3.10 raisesexceptiongroup.ExceptionGroupfrom the backport (verified in this repo's 3.10 venv), and neither name imports on the other interpreter. Narrowed by requiring every element to be aBaseException, and depth-capped so a self-referential group cannot hang the thing doing the diagnosing.sanitize_auth_diagnostic. Not optional: flattening increases how much exception text reaches the log, and these come from an HTTP transport whose messages can carry a URL or anAuthorizationheader. Most of these sites logged the raw exception before, so they are newly redacted as well as newly useful.disconnect_serverreturns to its caller — that reached the public API as the useless string too.Scope note. #224 named 5 sites, from the CI log. The AST guard below found 14 more, including
SSE read errorin_read_sseandCatalog reconciliation failed— both directly on the path #224 is chasing. Fixing the class rather than the instances is deliberate.Tests (
tests/test_exception_group_diagnostics.py, 13):str(group)ever becomes useful, the helper should go..exceptionsis not treated as a group; a self-referential group terminates; many leaves are capped and the remainder counted.ConnectionResetError: peer went awayand return it. Every other test would pass with a call site reverted — which is how this defect survived.logger.*call interpolates a caught exception directly.Verification (each run in the step it is reported): new file
13 passed; full suite3195 passedvs main's 3182 (+13, exactly the new tests) with failures/errors/skips/deselects identical — the 107/106 are the documented/tmp/package.jsonhost quirk;ruff check,ruff format --check,mypy src/all exit 0.Also, unrelated to this diff but recorded on #224: the #200 flake now reproduces locally, 1 run in 25 of
tests/runtime/test_emitter_harness.pyalone — same test, same message as CI. Stage 2 no longer needs to wait on CI.🤖 Generated with Claude Code
https://claude.ai/code/session_018NR2LKqZgn7CXVH2816gaa
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.