Skip to content

fix(integrations): report Scope resolution failures from prompt hooks - #2317

Open
Fengzdadi wants to merge 8 commits into
oceanbase:masterfrom
Fengzdadi:fix/prompt-hook-scope-diagnostics
Open

Fengzdadi wants to merge 8 commits into
oceanbase:masterfrom
Fengzdadi:fix/prompt-hook-scope-diagnostics

Conversation

@Fengzdadi

@Fengzdadi Fengzdadi commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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_found could lose its code and read as a version mismatch.

What changes are included in this PR?

  • Prompt hooks. A failed Scope resolution is reported as a scope_resolve event through the same throttled systemMessage as 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 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.
  • Responses that cannot be read or decoded. The Scope client, the prompt hooks, SessionStart, and the statistics reads of the Claude Code statusline and the Codex Stop hook classify every such response. A body that stalls past the request deadline, resets, or breaks off is a transport failure, server_unavailable, for an error status as for a successful one; a body that ends before its announced Content-Length counts as broken off, although http.client returns it as complete. Claude Code may run its hooks on Python 3.9, where a stalled read raises socket.timeout, which is not a TimeoutError there; the hooks treat it as the same transport failure. The response deadline fails a socket read with socket.timeout as well, since Python 3.9's socket reader turns a TimeoutError raised there into an IndexError. A body over the size bound, or a successful body that json rejects, including deep nesting and over-long integers, is invalid_response. An error body that json rejects leaves the status without a code.
  • Error codes. The Scope client keeps the status of a 503 and reads the error code of the other error statuses except 401. SessionStart also reads the error code of a failed bootstrap request, under the same per-request deadline as the response headers, so a slow header phase does not give the body a second timeout.
  • Coded 404 outside the prompt hooks. SessionStart, the Claude Code statusline, and the Codex Stop hook treat only a 404 without an error code, which is how a missing route answers, as a version mismatch. A coded 404 is an invalid response, and the SessionStart and Stop diagnostics carry the error code beside the status. The Stop hook also reports the status of a 503 from Scope resolution, as the prompt hook does.
  • SessionStart throttle. SessionStart writes its failures to stderr, which hosts do not show, 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 showed nothing. SessionStart no longer records its failures there.
  • Docs. The Claude Code and Codex integration guides and the troubleshooting page describe the scope_resolve event and the coded 404, in English and Chinese.
  • Tests. For each host, new tests cover a resolution failure that is reported while nothing else is sent, an error body that outlasts the budget, a truncated resolution response, a prepared-context response that cannot be read or decoded, a kept receipt cleared after a resolution or bootstrap response that cannot be read or decoded, a resolution or bootstrap 404 classified by its error code, and a failed SessionStart that no longer holds back the next prompt's diagnostic. Each truncated response is tested both with chunked framing and cut short of its Content-Length. Python 3.9 tests run the Claude Code prompt hook against a stalled error body and against a body that trickles past the deadline. The Claude Code statusline and Codex Stop hook tests add the coded 404 and a truncated statistics response, and the Stop hook test checks the reported status. A SessionStart test per host reads a slow bootstrap error body after slow headers and checks that both share one per-request deadline. The characterization fixtures from test(integrations): characterize the Claude Code and Codex hooks #2255 record the new Scope resolution behavior and the error code of a failed bootstrap request, and their SessionStart cases no longer write the throttle state.

Are there any user-facing changes?

Yes, in diagnostics only:

  • When the Scope cannot be resolved, Claude Code and Codex now show a scope_resolve diagnostic on the prompt instead of nothing. It follows the existing throttle.
  • A configured Scope that does not exist is reported as an invalid response with scope_not_found, not as a version mismatch. The Claude Code statusline no longer shows "PC version mismatch" for it.
  • A response that the hooks cannot read or decode is reported as server_unavailable or invalid_response instead 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, which test_client_transport.py skips 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 check included.
  • make unit-test: 7091 passed, 333 skipped, and 19 failed. The 19 fail on master 0c1dfc28 on the same machine, and none is in a file this PR changes: 4 in tests/test_upgrade_release_gate.py, 3 in tests/builtin/test_code_publication.py, 3 in tests/builtin/persistence/test_mysql_vector_admission.py, and 9 in tests/test_source_management.py.
  • make e2e-test: 1193 passed, 510 skipped, and 5 failed. The 5, in tests/e2e/test_source_collect.py and tests/e2e/test_source_deployment.py, fail on master 0c1dfc28 as well, because Source background processes require Linux.
  • make docs-test: passed.

make check stops at upgrade-policy-check, which fails on master 0c1dfc28 as 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.

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.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 00:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hireview001 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chunk = response.read(min(chunk_bytes, remaining_bytes))
if not chunk:
return bytes(content)
return complete_body(response, bytes(content))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copilot AI balanced review requested due to automatic review settings October 31, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hireview001 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI balanced review requested due to automatic review settings November 1, 2026 22:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AlexStocks AlexStocks added the 1.5.1 v1.5.1 release tracking label Nov 3, 2026

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.5.1 v1.5.1 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants