Skip to content

id(abuse): check_bytes/check_frame promise a retry for requests larger than bucket capacity — oversize can never be admitted, but refusal is temporary with retry_after_ms #3927

Open

Description

@dezren39

Problem

AbuseEvaluator::check_bytes (pkgs/id/src/abuse.rs:505-528) refuses a byte request larger than the bucket's burst capacity as a temporary BudgetExhausted refusal with a bounded retry_after_ms — but such a request can never be admitted. TokenBucket caps tokens_milli at capacity_milli on refill (abuse.rs:145-155), so a cost above capacity is unreachable at any future time. The refusal contract promises "wait this long and retry" for a wait that cannot help, forever.

The same trap exists on the frame path whenever a policy's frame_burst is 0 (cost 1 > capacity 0, abuse.rs:474-492).

Evidence (verified this run)

Laptop probe crate ~/git/fleet/probe-2026-11-01-laneB-abuseoversize/ — verbatim abuse.rs from main @3dbc7006 (sha256 625bcd348fe4dbe528dd67f813ce2b290d35ff8d1365774194d0b8441ed1358e, identical on sandbox and laptop clones), default AbusePolicy (byte_burst 4 MiB, bytes_per_sec 512 KiB):

  • Control: check_bytes(ep, true, 4*1024*1024, 0) → Ok (exact burst admitted).
  • check_bytes(ep, true, 4*1024*1024 + 1, 0) → Err(Refusal { reason: BudgetExhausted, retry_after_ms: Some(1000) }).
  • Retried at now = 1000 (the promised wait) → still Err, retry_after_ms: Some(1000) again.
  • Retried at now = 1_000_000_000 ms (~11.6 days) → still Err, retry_after_ms: Some(1000) again.
  • At that same instant the exact-burst request succeeds, proving the bucket is full — the oversize cost is simply above capacity, not below a refill horizon.
  • Zero-burst frame policy (frame_burst = 0): check_frame refuses with retry_after_ms: Some(1000) and still refuses at now = 1_000_000_000.

Why the retry number is also wrong on its own terms: TokenBucket::retry_after_ms (abuse.rs:176-186) computes deficit.div_ceil(rate_per_sec) against current tokens without checking cost against capacity — for the oversize case it yields 1 ms, clamped to the 1 s floor by Refusal::new (abuse.rs:103-113). Every retry is told "1 s" indefinitely.

Why it matters

The structured Refusal is the one contract every comms surface will consume (#2244 / #2033 design; the core landed in #3881, wiring is #3882). #3882 maps these refusals to HTTP 429 + Retry-After and frame-path refusals: an oversize request (e.g. a byte charge above 4 MiB) will be answered "retry in 1 s" forever, inviting well-behaved peers into an infinite retry loop and giving operators no signal that the request is permanently inadmissible rather than momentarily over budget. Fixing the contract is cheapest now, before Packet B (#3882) builds the mapping on top of it.

Acceptance criteria

  • A request whose cost exceeds the bucket's capacity is refused permanently: retry_after_ms is None (a dedicated reason such as RequestTooLarge, or documented reuse of the permanent-refusal shape) — on both check_bytes and check_frame.
  • retry_after_ms is only ever Some(..) for requests that can actually be admitted after the stated wait.
  • Regression tests: oversize byte request refused with no retry at t=0 and at a far-future time; zero-burst frame policy likewise; a within-capacity request over a drained bucket still gets a bounded temporary refusal; the exact-burst control is still admitted.
  • The module doc / Refusal doc states the permanent-oversize semantics so the id(comms): P0-A integration — frame-seam charging, HTTP 429+Retry-After, chat limiter consumer, counter visibility (phase of #2244) #3882 wiring maps it distinctly from 429 (e.g. a non-retryable refusal), or explicitly records why it still maps to 429.

Related: #3881 (core, closed — introduced the contract), #3882 (integration that will consume it), #2244 (parent), #2033 (design).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator job (lane B, new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b cron run, 2026-11-01 12:50 CST
    • Found via: filed this run
    • Task: Filed after verifying on current main (3dbc700) with a laptop probe over the verbatim abuse.rs: a byte request one byte over byte_burst is refused BudgetExhausted with retry_after_ms Some(1000) at t=0, after the promised wait, and after 1e9 ms, while the exact-burst control is admitted at the same instant. Triage via the enqueued repo-task/v1 packet.
    • Expectation: effort/s — triage confirms the oversize-permanent-refusal contract fix and routes it (ideally before id(comms): P0-A integration — frame-seam charging, HTTP 429+Retry-After, chat limiter consumer, counter visibility (phase of #2244) #3882 wiring builds the 429/Retry-After mapping on this contract)
    • Links: probe ~/git/fleet/probe-2026-11-01-laneB-abuseoversize/ (laptop); packet packets/packet-3927-triage.json (goal hidden_files)
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  4. 56 remaining items

  5. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: task 8301
    • Found via: monitor reconciliation
    • Task: Claim-eviction repair: the queue task for this issue is queued and unclaimed (its previous claim's lease expired without a complete/park/block), but the issue labels still said claimed. Labels reset to the truth: queue/queued + status/needs-triage. Checkpoint stage on the task: handed_off.
    • Expectation: a queue lane re-claims the task and resumes from the checkpoint stage named in the task progress.
  6. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    Launched queue task 8301 for execution in the bound OpenCode session ses_e5e5cb0aff8ffekXzrHOJiR5oR.

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    Coverage triage — 2026-11-03

  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: picked up
    task: 8899 (triage; queue repo-health-code-pilot; worker repo-worker-code; lane 9aca43b4-5439-4939-9f71-323f5d32521b — OpenChamber scheduled lane)
    by: repo-worker-code

    Claimed from the queue safety-net task. The packet reports an oversize abuse-budget request can receive a temporary retry interval even though it exceeds bucket capacity, with a laptop probe showing the condition persists after the advertised wait and after a long refill interval. I am revalidating issue/PR state and checking for an existing fix owner before deciding whether a follow-up task is still needed.

  9. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: triaged — confirmed, existing fix route
    task: 8899 (triage; repo-health-code-pilot; worker repo-worker-code; lane 9aca43b4-5439-4939-9f71-323f5d32521b — OpenChamber scheduled lane)
    by: repo-worker-code

    Verdict: confirmed. The recorded laptop probe shows byte cost byte_burst + 1 and zero-burst frame cost continue returning retry_after_ms: Some(1000) at the promised wait and at 1e9 ms, while exact-capacity byte cost succeeds. The root cause is the retry calculation not rejecting costs above bucket capacity before producing a temporary retry promise.

    Route: do not enqueue another implementation task. Existing PR #4555 (fix(id): permanently refuse over-capacity bucket costs, branch queue/6265-3927-permanent-oversize) is the active fix route, open against main with two files changed and checks SUCCESS; it implements RequestTooLarge and regression/docs coverage per the prior triage. Its status remains open with Code Owners listed as pending reviewer. Keep #3927 open until that PR merges and the behavior is verified on main. Removed stale queue/queued and status/needs-triage; task claim labels are removed on completion.

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

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions