Repository navigation
fix: name the field or denial in four failed tool results that agents could not act on #2149
Description
Summary
The agent tool recovery evaluation of #2067 (#2146) found four failed PowerContext tool results that do not tell the
agent what to change. Three Server 422 responses omit the rejected field although the Server knows it, and the Codex
plugin reports an authorization denial as a Scope-binding failure. In each case agents guessed, gave up, or told the
user the wrong cause.
Each item below names its own owning boundary and can land separately. Items 1–3 change HTTP error details; item 4 is
plugin-only.
Evidence
Measured on master at e4efb39b against disposable real Servers through the MCP mount, with Claude Code 2.1.280 /
claude-opus-5-5 and Codex CLI 0.163.0 / gpt-6.1-sol, each with and without the repository plugin, three runs per
case. The cited code is unchanged on current master. Each item's row in
evaluation/agent_tool_recovery/results/remediation.json (#2146) lists its run count and transcript hashes.
1. create_dream_run: evidence selection rejected without a field
Observed. 12 of 12 dream-capacity sessions passed the Experience only as target, left artifacts empty, and
received 422 invalid_request with details.errors = [{"loc": [], "msg": "Value error, evidence_limit_exceeded"}].
Codex stopped there in 6 of 6 sessions; Claude Code changed the request and reached the case's 429 in 4 of 6. The
agents made 5 retries with changed arguments.
Current behavior.
openapi/powercontext.yaml:CreateDreamRunRequest.artifactshasdefault: []and nominItems, so the request
and MCP input schemas accept an empty selection.src/powercontext/builtin/dream/models.py,CreateDreamRunRequest.validate_selection: outsiderevise_tags, raises
DreamError("evidence_limit_exceeded")unless1 <= len(artifacts) <= 20andlen(artifacts) + len(sources) <= 32.
The model-level error reaches theinvalid_requesthandler with an emptyloc.- RFC 1510 uses
evidence_limit_exceededfor evidence beyond its bounds; an empty selection reuses that code.
Change. Report the failure against artifacts, distinguish an empty selection from one above the limit, and state
the bounds (1 to 20 Artifacts, at most 32 references with Sources). revise_tags keeps its own bounds.
Owning boundary. The Dream request model's validator and the Server's invalid-request mapping.
2. propose_experience: the known field is dropped
Observed. In mcp-family-rejected, MCP correctly declines replace_artifact for an Experience. In 5 of 6 Claude
Code sessions the model then used propose_experience without the target revision in artifact_refs and received 422
invalid_request, "The request is invalid.", details: null. Those sessions made 8 retries with changed arguments; two
found the accepted shape and three gave up. Adding the target revision to artifact_refs returns 201.
Current behavior.
src/powercontext/builtin/review/service.py,_validate_target: raises
InvalidCandidateError("artifacts", f"must include the exact target {family} Artifact").src/powercontext/server/app.py,_map_candidate_error: maps everyInvalidCandidateErrorto 422
invalid_requestwithdetails: None.- The internal field is
artifacts; the public request field isartifact_refs. The same error is also raised in
review/generation.py(_generation_input) andreview/service.py(_validate_approval_lineage).
Change. Put the public request field and a stable, content-free reason in details, translating internal field
names to each operation's public field. Cover every raise site a public operation can reach, or list the ones left out.
Owning boundary. The Server's Candidate error mapping.
3. capture_working_note: expiry rejected without expires_at
Observed. The schema-past-expiry case asks for a Working Note that expires two hours before the run. Nine
sessions (claude-code-plugin, codex, codex-plugin, three each) received 422 invalid_request, "The request is
invalid.", details: null. codex-plugin stopped in 3 of 3 sessions without a correction; the others inferred the cause
from the timestamp. An invalid note_kind, by contrast, returns its field in details.errors[].loc.
Current behavior.
src/powercontext/builtin/runtime/application.py,capture_working_note: raises
InvalidRuntimeRequestError("working-note-expiry")both for an expiry without a time zone and for one not in the
future, unless the call replays an existing note.src/powercontext/server/app.py,_map_domain_error: maps it to 422invalid_requestwithdetailsfrom
_invalid_request_details, which returnsNonefor this error.
Change. Name expires_at with a stable reason, and separate "not in the future" from "missing time zone". Replay
of an existing note is unchanged.
Owning boundary. The Server's invalid-request mapping, and the runtime's expiry checks for the separate reasons.
4. Codex plugin: authorization denial reported as a binding failure
Observed. With the plugin and a principal that holds only scope.viewer, resolve_scope_binding returned 403 and
the PreToolUse hook denied remember_memory as "PowerContext could not resolve the current Scope binding." All 3
sessions told the user the binding failed. codex exec --json records no event for the denied call. Without the plugin,
Codex received 403 forbidden and reported missing permission in 3 of 3 sessions.
Current behavior.
integrations/codex/plugins/powercontext/scripts/scope_binding.py,resolve_scope_id: a 403 surfaces as
urllib.error.HTTPError, anOSError.integrations/codex/plugins/powercontext/hooks/bind_tools.py,main: catchesOSErrorwith the other binding errors
and denies with the binding reason.hooks/session_binding.py,_emit_http_failure, already classifies 403 asauthorization_denied.
Change. Distinguish 401/403 from an unresolved binding and give the model a reason that names the denial,
consistent with session_binding.py. Separately, the access owners may decide that a read-only principal can resolve a
binding for a Scope it can read; then the Server authorization is the fix.
Owning boundary. The Codex plugin's PreToolUse hook, or the Server's authorization of binding resolution.
Constraints
- Items 1–3 change HTTP error details: update
openapi/powercontext.yaml, runmake api-generateand
make contract-test, and do not edit generated files by hand. - Preserve status codes, error codes, and generated-client compatibility. Details carry field names and stable reasons
only, no request content. - No automatic retries and no broad error-schema migration.
Acceptance criteria
-
create_dream_runwith emptyartifactsreturns details namingartifactsand the empty selection; 21
Artifacts namesartifactsand the limit. -
propose_experiencewithout the target revision returns details namingartifact_refsand the missing exact
target, with no internal field names. -
capture_working_notereturns details namingexpires_atfor a past expiry and, separately, for a missing time
zone. - With a viewer credential, the Codex plugin's denial reason names missing permission; transport failures and
unresolved bindings keep their reason, and plugin tests cover 401, 403, and transport failure. - Rerunning
dream-capacity,mcp-family-rejected,schema-past-expiry, andpermission-deniedwith
evaluation/agent_tool_recoveryshows each failure naming its field or denial.
Related
- evaluation(agent-tools): measure retry loops and specify actionable failure responses #2067: evaluation and follow-up rule
- feat(evaluation): measure agent recovery from failed MCP calls #2146: evaluation suite, results, and remediation rows
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
guamiao commentedon Oct 21, 2026
Hi! I'd like to take #2149. I can start with item 4 (the Codex plugin's 403 denial) as a standalone change, and then continue with items 1–3 including the OpenAPI regeneration and contract tests. I'll follow the constraints in the issue and keep each item independently reviewable. Could you assign it to me if that works?
fix: name the field or reason in four failed tool results (#2149) (#2150