Skip to content

ErrorResponse rejects spec-defined trace_id, so commit rejections are misclassified #102

Open

Description

@amavashev

Summary

ErrorResponse in runcycles/models.py uses extra="forbid" (_SNAKE_CASE_CONFIG) and does not declare trace_id. The server includes trace_id in error bodies, and the protocol spec allows it: ErrorResponse in cycles-protocol-v0.yaml lists a trace_id property. As a result, CyclesResponse.get_error_response() fails validation and returns None for spec-conformant error responses.

Present in v0.5.3 and on main as of 2026-09-27.

Reproduction

from runcycles.models import ErrorResponse
ErrorResponse.model_validate({
    "error": "BUDGET_EXCEEDED", "message": "m", "request_id": "r", "trace_id": "t",
})
# pydantic ValidationError: trace_id — Extra inputs are not permitted

Server 0.1.25.59 response to a commit whose actual exceeds the estimate under overage_policy=REJECT:

HTTP/1.1 409
{"error":"BUDGET_EXCEEDED","message":"Budget exceeded for: Actual exceeds estimate and policy is REJECT","request_id":"...","trace_id":"..."}

Impact

In StreamReservation._handle_commit (runcycles/streaming.py), error_code is derived from get_error_response() and so comes back as None. A recognized rejection therefore does not take the _is_recognized_rejection branch (discard pending, do not release). It falls through to the "unclassifiable client error" branch and is scheduled for retry. Observed log with runcycles 0.5.3 against cycles-server 0.1.25.59:

Stream commit got unclassifiable client error (status=409, error=None); journaling for replay: id=...
Commit retry got non-retryable error: reservation_id=..., status=409, error=BUDGET_EXCEEDED

The final outcome is still correct: nothing is released, the retry engine then classifies BUDGET_EXCEEDED, and the settlement error raised under raise_on_commit_failure=True reports is_budget_exceeded(). But the synchronous classification is wrong, it sends one extra commit request, and the logs are misleading. Every other branch in that method that depends on error_code (LIMIT_EXCEEDED, RESERVATION_EXPIRED, RESERVATION_FINALIZED, IDEMPOTENCY_MISMATCH) is affected the same way whenever the server includes trace_id.

Suggested fix

Add trace_id: str | None = None to ErrorResponse. Also consider whether error envelopes should tolerate unknown fields (extra="allow" or "ignore"), so that future additive spec fields don't break classification. Add a test that parses an error body containing trace_id, and a streaming test asserting that a 409 BUDGET_EXCEEDED commit takes the recognized-rejection branch.

Found while writing the drill for runcycles/cycles-docs#964.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions