Repository navigation
Conversation
The Claude Code and Codex prompt hooks resolve the Scope before they prepare context, and a failed resolution ended the hook without output. A Server that is not running, a rejected credential, or a configured Scope that does not exist therefore produced no diagnostic, although both integration guides and the plugin contract promise one. The hooks now report the failure as a `scope_resolve` event through the same throttled `systemMessage` as their other automatic operations, and neither prepare nor capture. The failure goes through the classification that a failed preparation uses: an unreachable Server or HTTP 503 is `server_unavailable`, HTTP 401 is `authentication_failed`, a 404 without an error code is `version_mismatch`, and a coded error such as `scope_not_found` is `invalid_response` with its status and code. For this, the Scope client keeps the status of a 503 and, for error statuses other than 401 and 503, reads the error code from the body, which every caller of the client now waits for. An error body that outlasts the request deadline means the Server is unavailable, as it does for the hooks' own requests, and one over the size bound is an invalid response. An error body that is truncated or does not decode leaves the status without a code. A success response that the client could not decode used to escape it: http.client reports a truncated or malformed response with its own exception instead of OSError, and json rejects deep nesting and over-long integers with exceptions other than JSONDecodeError. SessionStart then neither reported the failure nor cleared a kept bootstrap receipt. The client now reports a truncated or malformed response as a transport failure and an undecodable body as an invalid response. SessionStart writes its failures to stderr, yet it recorded them in the throttle that the prompt hooks apply to `systemMessage`. With the Server down, the first prompt after a failed SessionStart therefore still showed nothing. SessionStart no longer records its failures there, since stderr is not a host-visible channel. The characterization fixtures record the change in the Scope resolution cases, and the SessionStart cases no longer write the throttle state. The integration guides and the troubleshooting page describe the `scope_resolve` event and the coded 404 in both locales.
SessionStart, the Claude Code statusline, and the Codex Stop hook read any 404 from Scope resolution as `version_mismatch`. The Server answers a configured Scope that does not exist with a coded 404, `scope_not_found`. In one Codex turn the prompt hook therefore reported an invalid response with that code while the Stop hook reported a version mismatch, and the Claude Code statusline showed "PC version mismatch". The advice for a version mismatch, reinstalling the package and plugin from one ref, does not help a user whose Scope is missing. As in the prompt hooks, only a 404 without an error code, which is how a missing route answers, is now a version mismatch. A coded 404 is an invalid response, and the SessionStart and Stop diagnostics carry the error code beside the status. The characterization fixtures do not change: none of their SessionStart or Stop cases receives a coded 404.
…oded Some responses still escaped the hooks' classification. http.client reports a truncated chunked body with its own exception instead of OSError, and json rejects deep nesting and over-long integers with exceptions other than JSONDecodeError. A prepared-context error body that did either escaped the prompt hook, which then showed nothing, and a bootstrap response that did either left SessionStart without a diagnostic and with its kept receipt. A body that stalls past the request deadline, resets, or breaks off is now a transport failure, `server_unavailable`, for an error status as for a successful one, in the Scope client, the prompt hooks, and SessionStart. That includes Python 3.9, on which Claude Code may run its hooks: a stalled read there raises `socket.timeout`, which is not yet a `TimeoutError`, so a stalled 404 error body read as a version mismatch. A body over the size bound, or a successful body that json rejects, is `invalid_response`, and an error body that json rejects leaves the status without a code. SessionStart also reads the error code of a failed bootstrap request, so a coded 404 from the bootstrap route is an invalid response with its code rather than a version mismatch. The Codex Stop hook reports the status of a 503 from Scope resolution, as the prompt hook does. The characterization fixtures record the error code of the failed bootstrap requests.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
http.client returns a Content-Length body that the connection cut short as if it were complete. A truncated coded 404 therefore lost its error code and read as a version mismatch, and a truncated success body read as an invalid response. The Scope client, the prompt hooks, and SessionStart now fail such a body as a truncated read, which they report as an unavailable Server, as they do for a truncated chunked body. On Python 3.9, socket.timeout is not TimeoutError, and SocketIO turns a bare TimeoutError raised from recv_into into IndexError. A body that trickled past the response deadline therefore escaped classification in the Claude Code hooks. The deadline socket now raises socket.timeout. The Codex hooks run on Python 3.11 or later, where the two are the same.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hireview001
left a comment
There was a problem hiding this comment.
- CI failed: Upgrade release gate (failure).
- CI failed: Recall effort (OceanBase) (failure).
- CI failed: tests (3.14, unit) (failure).
- CI failed: tests (3.13, unit) (failure).
- CI failed: tests (3.11, unit) (failure).
- CI failed: tests (3.12, unit) (failure).
- CI failed: upgrade-acceptance / Prepare upgrade artifacts and matrix (failure).
- CI failed: quality (failure).
- CI failed: Main (failure). Failed steps: quality / Run checks; upgrade-acceptance / Prepare upgrade artifacts and matrix / Run upgrade gate regressions; tests (3.12, unit) / Run unit tests.
- CI failed: E2E harness (failure). Failed steps: Recall effort (OceanBase) / Check code and types on Linux.
| chunk = response.read(min(chunk_bytes, remaining_bytes)) | ||
| if not chunk: | ||
| return bytes(content) | ||
| return complete_body(response, bytes(content)) |
There was a problem hiding this comment.
[P3] Classify truncated statistics responses as transport failures
When the Codex Stop hook receives a /v1/stats response cut short of its Content-Length, including a 503, the new IncompleteRead escapes _load_stats and produces invalid_response without the powercontext doctor recovery advice. Normalize HTTPException in the statistics caller, including while reading an HTTPError body, so this transport failure produces server_unavailable.
There was a problem hiding this comment.
Fixed in fb1b1a07: _load_stats now handles a truncated body like the recall client does — an OSError or HTTPException while reading either a successful or an HTTPError body raises _ServerUnavailableError, so the Stop hook reports server_unavailable with powercontext doctor. test_truncated_stats_response_reports_server_unavailable covers 200 and 503 in chunked and Content-Length framing; all four cases fail without the change.
The Claude Code statusline had the same gap on its /v1/stats read, so 44d4bd00 applies complete_body there and classifies HTTPException as server_unavailable; a truncated response now renders "PC offline · run powercontext doctor" instead of "PC invalid response".
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hidb4ai
left a comment
There was a problem hiding this comment.
Reviewed 44d4bd00f0a14c4bbd1955cb2102712ebf854d64; no blocking correctness findings. The Scope-resolution diagnostics, coded-404 distinction, bounded response handling, and SessionStart throttle change are consistent with the existing hook contracts. The previously reported truncated-statistics issue is fixed. No database tables or public HTTP/MCP/SDK interfaces are added.
CI note: the Windows managed-resource candidate check remains red because curl exits with code 3221225477 while downloading the platformdirs wheel in an unchanged build script. This does not appear caused by this PR, but the failed check still needs a successful rerun before merging.
hireview001
left a comment
There was a problem hiding this comment.
- Resolve merge conflicts with the target branch.
- CI failed: CI result (failure).
- CI failed: managed-resource-candidates / Candidate windows-x86_64 (failure).
- CI failed: Main (failure). Failed steps: managed-resource-candidates / Candidate windows-x86_64 / Produce twice, compare bytes, consume catalog, and run bundled launcher/Ops; CI result / Require successful selected checks.
| raw = _read_response(response, deadline=min(deadline, monotonic() + timeout)) | ||
| except HTTPError as error: | ||
| raise _HttpStatusError(error.code, path) from error | ||
| code = read_error_code(error, deadline=min(deadline, monotonic() + timeout)) |
There was a problem hiding this comment.
[P2] Preserve the request deadline when reading bootstrap errors
When bootstrap error headers consume most of request_timeout_seconds, this starts a fresh timeout for the body, allowing SessionStart to take nearly twice the configured per-request limit; Codex's hooks/session_binding.py does the same. Compute the absolute request deadline before open_bounded and reuse it for the error-body read in both hooks.
There was a problem hiding this comment.
Fixed in c77d5c45: both _post_jsons (Claude Code session_start.py, Codex session_binding.py) compute request_deadline = min(deadline, monotonic() + timeout) before open_bounded and reuse it for the success body and read_error_code. test_bootstrap_error_body_shares_the_request_deadline_with_its_headers (one per host) spends 0.9 s of a 1 s request timeout on headers and then trickles the error body; it now stops at 1.0 s, where it previously ran to 1.9 s. The branch also merges master (d74f661f) to resolve the tests/claude_code_plugin/test_hook.py import conflict with #2286.
…cope-diagnostics # Conflicts: # tests/claude_code_plugin/test_hook.py
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hidb4ai
left a comment
There was a problem hiding this comment.
Re-reviewed c77d5c4592a48da40e4ec3eb2b0c462a8b7de019. Both bootstrap hooks now establish one absolute request deadline before opening the connection and reuse it for successful and error bodies, addressing the deadline-reset finding. The earlier truncated-statistics handling remains fixed. No remaining blocking correctness findings, and no new database tables or public HTTP/MCP/SDK interfaces.
The latest CI checks, including the previously failing Windows candidate, are successful. Two local timing-sensitive cases also fail on the previously reviewed commit; their affected code is unchanged in this update.
Which issue or RFC does this PR close?
Part of #2250: the checklist item "Claude Code and Codex: report Scope resolution failures from the prompt hooks, as their guides promise, and stop reporting an unknown Scope as a version mismatch."
Rationale for this change
The Claude Code and Codex prompt hooks resolve the Scope before they prepare context, and a failed resolution ended the hook without output. A Server that is not running, a rejected credential, or a configured Scope that does not exist therefore produced no diagnostic, although both integration guides and the plugin contract promise one.
A missing Scope was also reported as a version mismatch. SessionStart, the Claude Code statusline, and the Codex Stop hook read any 404 from Scope resolution as
version_mismatch, but the Server answers a configured Scope that does not exist with a coded 404,scope_not_found. The advice for a version mismatch, reinstalling the package and plugin from one ref, does not help a user whose Scope is missing.Reporting these failures depends on the hooks classifying every response they cannot use, and some responses escaped that classification. A truncated chunked response, an error body over the size bound, deeply nested JSON, an over-long integer, or, on Python 3.9, a body that kept trickling past the response deadline raised an exception that no caller handled. The prompt hook then showed nothing, and SessionStart neither reported the failure nor cleared a kept bootstrap receipt. A body cut short of its Content-Length was read as if it were complete, so even a coded 404 such as
scope_not_foundcould lose its code and read as a version mismatch.What changes are included in this PR?
scope_resolveevent through the same throttledsystemMessageas the hooks' other automatic operations, and the hook then neither prepares nor captures. It is classified like a failed preparation: an unreachable Server or HTTP 503 isserver_unavailable, HTTP 401 isauthentication_failed, a 404 without an error code isversion_mismatch, and a coded error such asscope_not_foundisinvalid_responsewith its status and code.server_unavailable, for an error status as for a successful one; a body that ends before its announced Content-Length counts as broken off, althoughhttp.clientreturns it as complete. Claude Code may run its hooks on Python 3.9, where a stalled read raisessocket.timeout, which is not aTimeoutErrorthere; the hooks treat it as the same transport failure. The response deadline fails a socket read withsocket.timeoutas well, since Python 3.9's socket reader turns aTimeoutErrorraised there into anIndexError. A body over the size bound, or a successful body thatjsonrejects, including deep nesting and over-long integers, isinvalid_response. An error body thatjsonrejects leaves the status without a code.systemMessage. With the Server down, the first prompt after a failed SessionStart therefore showed nothing. SessionStart no longer records its failures there.scope_resolveevent and the coded 404, in English and Chinese.Are there any user-facing changes?
Yes, in diagnostics only:
scope_resolvediagnostic on the prompt instead of nothing. It follows the existing throttle.scope_not_found, not as a version mismatch. The Claude Code statusline no longer shows "PC version mismatch" for it.server_unavailableorinvalid_responseinstead of being dropped, and the SessionStart diagnostic for a failed bootstrap request carries the Server's error code.There are no API, configuration, or persisted format changes.
How was this change tested?
uv run pytest -n 6 tests/claude_code_plugin tests/codex_plugin tests/test_hook_characterization.py tests/e2e/test_bootstrap_host_service_chain.py --require-claude-code-python-floor: 591 passed and 4 skipped, whichtest_client_transport.pyskips by design. The Python 3.9 tests ran on the macOS system Python 3.9.6. Each new test fails without the change, except four per host that pin what SessionStart keeps: a 404 without a readable error code is still a version mismatch.uv run prek run -a: every hook passes,ty checkincluded.make unit-test: 7091 passed, 333 skipped, and 19 failed. The 19 fail on master0c1dfc28on the same machine, and none is in a file this PR changes: 4 intests/test_upgrade_release_gate.py, 3 intests/builtin/test_code_publication.py, 3 intests/builtin/persistence/test_mysql_vector_admission.py, and 9 intests/test_source_management.py.make e2e-test: 1193 passed, 510 skipped, and 5 failed. The 5, intests/e2e/test_source_collect.pyandtests/e2e/test_source_deployment.py, fail on master0c1dfc28as well, because Source background processes require Linux.make docs-test: passed.make checkstops atupgrade-policy-check, which fails on master0c1dfc28as well: "Persistence changed (file set or content; CRLF/LF normalized): update the compatibility review".AI usage statement
This PR was prepared with Claude Code (Claude Opus 5.5). It wrote the change and the tests, ran the checks, and reviewed the change for correctness. I reviewed the change and the results.