Skip to content

engine.sh: missing/placeholder Copilot token misclassified as rate-limit (status=1) — deterministic auth failure treated as retryable #1591

Closed

Description

@don-petry

Split out of review feedback on PR #1556 (implements #1546). Valid finding. #1546 fixes license/policy denial misclassification; this is the adjacent case it does not cover.

Defect

scripts/engine.sh (~line 297): a missing or placeholder Copilot token returns status 1. That is indistinguishable from a rate-limit result to run_writer_with_fallback, which then sets any_rate_limited=1 and can return exit 2 when Copilot is the only enabled engine, or when every other engine is unavailable.

Net effect: a deterministic authentication/configuration failure is classified as a retryable rate limit.

Why this matters more than it looks

  1. It is never going to succeed on retry. A retryable classification sends the retry cron back at a condition that cannot change without a human setting a secret. That burns cycles against the churn breaker and delays the real diagnosis.
  2. It very likely explains a standing trap. The fleet plan records: "the dev-lead/pr-review Copilot fallback engine is broken (fails in <1s every time, likely missing/invalid Copilot-scope PAT) — when Claude is unavailable there is currently no working fallback." A sub-second failure with a missing token is exactly this code path. That "no working fallback" is why the 7-day CLAUDE_CODE_OAUTH_TOKEN outage (#1587) was a total engine outage rather than a degraded one.
  3. It compounds #1568. #1568 tracks status=rate-limited being emitted for non-quota holds; this is a second, independent source of the same false signal, in a different script. Fixing one will not fix the other, and both mislead the same standing diagnostic rule ("distinguish throttled from not-provided").

Acceptance criteria

  • A missing/placeholder Copilot token yields a distinct unavailable/unconfigured result, not status 1.
  • run_writer_with_fallback does not set any_rate_limited for that result, and does not exit 2 on account of it.
  • The skip reason is tracked separately from quota exhaustion and surfaced (per #1552).
  • Test: unset / placeholder the Copilot token with Copilot as the only enabled engine → assert the outcome is unavailable, not rate-limited, and that no retry is scheduled.
  • Cross-check against #1568 so the two fixes agree on one reason-token vocabulary rather than inventing two.

Found by codeant-ai on #1556; kept out of that PR to avoid widening #1546's scope mid-review.

Activity

  1. github-actions commented on Aug 30, 2026

    @github-actions
    Contributor

    Thanks for the thorough write-up and for keeping this scoped out of #1556 — the separation makes both fixes much easier to reason about. The misclassification is clear and the acceptance criteria are well-defined. One question to unblock implementation: has any vocabulary for the reason token (e.g. unavailable, unconfigured, auth-failure) been tentatively agreed upon in #1568, or should the implementer propose one here and cross-reference it there?

  2. added
    dev-leadFor dev-lead agent pickup
    and removed
    needs-human-reviewFlagged by automated PR review agent
    on Aug 31, 2026
  3. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    Dev-Lead Implementation Plan

    Issue: #1591 — engine.sh: missing/placeholder Copilot token misclassified as rate-limit (status=1) — deterministic auth failure treated as retryable

    Scope

    Introduce a distinct unconfigured reason token for a missing/placeholder Copilot credential so a deterministic configuration gap is classified separately from a retryable quota exhaustion (rate-limited), and route it to a human-escalation (no retry) path rather than a retryable failure.

    Implementation Checklist

    • scripts/engine.sh — run_writer_with_fallback: track a new any_unconfigured aggregate when the Copilot config-gap guards skip the engine (missing/placeholder token, unsupported classic PAT); never set any_rate_limited for it; on a pure-config-gap outcome write reason=unconfigured and return 1 (not 2). Priority: missing-binary > rate-limited > unconfigured > engine-error.
    • scripts/dev-lead-fix-issue.sh — handle_engine_failure: add an unconfigured branch that escalates to needs-human (terminal marker, no retryable status=failed marker), mirroring the missing-binary branch.

    Reason-token vocabulary (cross-check #1568)

    Single shared vocabulary: rate-limited (throttled/quota), unconfigured (credential not-provided/placeholder), missing-binary (engine binary absent), timeout, engine-error. This gives #1568 the "throttled vs not-provided" distinction one token set rather than two.

    Tests to Write

    • tests/dev-lead/unit/test_engine_fallback.bats — copilot-only + unset token → exit 1, reason=unconfigured, not rate-limited; placeholder-token variant; classic-PAT (ghp_) variant.
    • tests/dev-lead/unit/test_fix_issue.bats — unconfigured → escalates to needs-human, applies the label, posts NO retryable status=failed marker (no retry scheduled).

    Test Command

    bats tests/dev-lead/unit/test_engine_fallback.bats tests/dev-lead/unit/test_fix_issue.bats (full: bats tests/**/*.bats), plus bash .dev-lead/scripts/dev-lead-lint.sh

  4. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    Dev-Lead: Progress (in progress — not yet durable)

    Provisional. Nothing is pushed yet; the automation posts the authoritative
    completion record (with PR number + head SHA) after the push. Treat the notes
    below as intent, not delivered results.

    Plan Execution (intended)

    • Added a distinct unconfigured reason token for a missing/placeholder/unsupported Copilot credential, tracked separately from quota exhaustion (rate-limited).
    • run_writer_with_fallback never feeds a config-gap into any_rate_limited; when it is the sole cause it returns exit 1 with reason=unconfigured (priority: missing-binary > rate-limited > unconfigured > engine-error, so a genuine quota hit still wins).
    • handle_engine_failure routes unconfigured to a needs-human escalation (terminal marker, no retryable status=failed), mirroring missing-binary — so the retry cron does not requeue a state that cannot change without a human.

    Local check output (provisional — not the pushed run)

    tests/dev-lead/unit: 76 suites, 0 failures (engine-fallback 35 ok, fix-issue 25 ok, dev-lead-retry 10 ok, provider-headroom 13 ok)
    .dev-lead/scripts/dev-lead-lint.sh: all checks passed (shellcheck 113 files @ warning, agent frontmatter, CODEOWNERS)
    

    Files touched (this workspace, not yet pushed)

    • scripts/engine.sh: new any_unconfigured aggregate + terminal unconfigured classification.
    • scripts/dev-lead-fix-issue.sh: unconfigured → needs-human escalation branch (no retry marker).
    • tests/dev-lead/unit/test_engine_fallback.bats: copilot-only missing/placeholder/classic-PAT → unconfigured; genuine rate-limit still wins.
    • tests/dev-lead/unit/test_fix_issue.bats: unconfigured → needs-human, no retry marker.

    Notes

    Reason-token vocabulary reconciled with #1568: rate-limited = throttled, unconfigured = not-provided. check_provider_headroom's copilot arm (~line 297) is left as-is — its return-1 path is unreachable for the missing-token case because run_writer_with_fallback guards the credential before probing; the classification fix lives in the caller where the misclassification actually occurred.

  5. don-petry commented on Aug 31, 2026

    @don-petry
    CollaboratorAuthor

    Dev-Lead: Implementation Complete — PR #1597

    The implementation for issue #1591 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.

    Review happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.

  6. added a commit that references this issue on Aug 31, 2026
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

    bugBug reportsdev-leadFor dev-lead agent pickup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions