Skip to content

fix: name the field or denial in four failed tool results that agents could not act on #2149

Closed

Description

@Doar999

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.artifacts has default: [] and no minItems, so the request
    and MCP input schemas accept an empty selection.
  • src/powercontext/builtin/dream/models.py, CreateDreamRunRequest.validate_selection: outside revise_tags, raises
    DreamError("evidence_limit_exceeded") unless 1 <= len(artifacts) <= 20 and len(artifacts) + len(sources) <= 32.
    The model-level error reaches the invalid_request handler with an empty loc.
  • RFC 1510 uses evidence_limit_exceeded for 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 every InvalidCandidateError to 422
    invalid_request with details: None.
  • The internal field is artifacts; the public request field is artifact_refs. The same error is also raised in
    review/generation.py (_generation_input) and review/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 422 invalid_request with details from
    _invalid_request_details, which returns None for 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, an OSError.
  • integrations/codex/plugins/powercontext/hooks/bind_tools.py, main: catches OSError with the other binding errors
    and denies with the binding reason.
  • hooks/session_binding.py, _emit_http_failure, already classifies 403 as authorization_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, run make api-generate and
    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_run with empty artifacts returns details naming artifacts and the empty selection; 21
    Artifacts names artifacts and the limit.
  • propose_experience without the target revision returns details naming artifact_refs and the missing exact
    target, with no internal field names.
  • capture_working_note returns details naming expires_at for 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, and permission-denied with
    evaluation/agent_tool_recovery shows each failure naming its field or denial.

Related

Activity

  1. guamiao commented on Oct 21, 2026

    @guamiao
    Contributor

    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?

  2. moved this from Todo to In progress in PowerContexton Oct 21, 2026
  3. moved this from In progress to Done in PowerContexton Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions