Skip to content

fix(ai): quota toNumber coerces empty strings to 0 — an empty upstream field reports a pool 100% used (or 0% used) #1741

Closed

Description

@dezren39

fix(ai): quota toNumber coerces empty strings to 0 — an empty upstream field reports a pool 100% used (or 0% used)

Verified 2026-11-05 on origin/main b318603c68 (laptop primary, clean), probe at ~/git/fleet/probe-2026-11-05-creator-opencode-1235/ on the laptop (probe.ts, probe2.ts, bun 1.4.4, run twice with identical results).

The bug

packages/ai/src/quota/utils.ts:26-31 — the shared numeric parser for the whole quota suite:

export const toNumber = (value: JsonValue | undefined): number | null => {
  if (typeof value === "number" && Number.isFinite(value)) return value
  if (typeof value === "string") {
    const parsed = Number(value)
    return Number.isFinite(parsed) ? parsed : null
  }
  return null
}

Number("") === 0 and Number(" ") === 0, both finite — so an empty or whitespace-only string is parsed as a real zero, not as "no value" (null). Probe: toNumber("") = 0, toNumber(" ") = 0, toNumber("12") = 12.

toNumber is the front door for ~25 call sites across 9 quota providers (codex used_percent/balance/spend limits, claude percents + money, deepseek balances, opencode-go percent/balance/expires, opendesign credits, command-code credits, antigravity activeIndex, meta key-endpoint percent). Every one of them inherits the flaw.

Reproduced consequences (probe, payload-shaped)

  • parseCommandCodeCredits({ credits: { monthlyCredits: "" } }) → monthly window usedPercent: 100, label "10 / 10" — an empty field reports the Command Code monthly pool fully exhausted. Quota-aware routing and the opencode quota display both reason from this.
  • parseOpenCodeGoUsage({ usage: { rolling: { percent: "", resetsAt: 1893456000 } } }) → a kept 5h window with usedPercent: 0 — an empty field reports a pool completely untouched (false headroom, the opposite error).
  • Same shape elsewhere by construction: codex credits.balance: "" → a real $0 balance; opencode-go expires: "" → expiry 0 (1970).

Providers do emit empty strings for unset fields (that is why sibling helpers exist: asNonEmptyString in the same file treats ""/blank as absent, and cline-pass's caller already guards its percent through it). toNumber is the one parser that does not.

The duplicate

packages/ai/src/quota/providers/meta.ts:73-77 carries a local num helper with the identical Number(value) body and the identical flaw: window_weighted_used: "" parses to 0 and passes parseSubscriptionQuota's null gate (:84-89), minting windows from empty fields. One fix should land in the shared helper and the duplicate should delegate to it.

Why it matters

These numbers feed the quota display and quota-aware routing (#587/#566 consumers): a false 100% steers sessions away from a live pool; a false 0% steers them into a walled one. Both errors are silent — the window looks well-formed either way. The fix is one guard in one shared function, plus pinning tests in the pattern #1722/#1725 established.

Out of scope (checked, already safe): groq's headerNumber (groq.ts:55-59 guards !raw first; its regex parser cannot match an empty capture), cline-pass's percent (goes through asNonEmptyString first).

Acceptance criteria

  • toNumber returns null for empty and whitespace-only strings (and still parses "12", " 12 ", numeric strings with signs/decimals as before).
  • meta.ts's local num delegates to the shared toNumber (or is otherwise fixed identically) — no second copy of the flaw.
  • Tests in packages/ai/test/quota.test.ts (or a wired sibling) pin: toNumber("")/toNumber(" ") → null; parseCommandCodeCredits with monthlyCredits: "" produces no monthly window; parseOpenCodeGoUsage with percent: "" produces no window for that field.
  • Existing quota tests still pass under the ai package test command; ai typecheck clean.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    app/aiPackage: ai (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Nov 5, 2026
  2. dezren39 commented on Nov 5, 2026

    @dezren39
    Author

    🤖 Agent note · state: filed

    • Agent: new-issue-creator job (new-issue-creator-opencode)
    • Session / ID: cron new-issue-creator-opencode, run 2026-11-05T12:35Z
    • Found via: filed this run
    • Task: quota toNumber coerces empty/whitespace strings to 0 — verified on laptop probe (monthlyCredits:"" → 100% used "10 / 10"; percent:"" → 0% used window). Evidence in the issue body; probe ~/git/fleet/probe-2026-11-05-creator-opencode-1235/ on the laptop.
    • Expectation: effort xs; done = toNumber returns null for blank strings, meta.ts duplicate delegates, pinning tests pass.
    • Links: n/a yet
  3. dezren39 commented on Nov 5, 2026

    @dezren39
    Author

    🤖 Agent note · state: picked up

    • Agent: new-issue-creator job (new-issue-creator-opencode)
    • Session / ID: cron new-issue-creator-opencode, run 2026-11-05T12:35Z
    • Found via: self-assigned during new-issue-creator-opencode run (guarded self-solve, lifecycle protocol §4 — filed this run, trivial xs fix, no safety labels)
    • Task: fix toNumber blank-string coercion in packages/ai/src/quota/utils.ts, delegate meta.ts's duplicate num to it, add pinning tests.
    • Expectation: effort xs; done = PR merged with ai quota tests + typecheck green on the laptop.
    • Links: branch creator/1741-quota-tonumber-blank (to come)
  4. added
    work/self-assignedAn agent grabbed this item directly, outside the queue
    and removed on Nov 5, 2026
  5. added a commit that references this issue on Nov 5, 2026
  6. dezren39 commented on Nov 5, 2026

    @dezren39
    Author

    🤖 Agent note · state: done

    • Agent: new-issue-creator job (new-issue-creator-opencode)
    • Session / ID: cron new-issue-creator-opencode, run 2026-11-05T12:35Z
    • Found via: filed this run; self-assigned (guarded self-solve, lifecycle protocol §4)
    • Task: fixed — toNumber returns null for blank strings; meta.ts duplicate num deleted in favor of the shared helper; 4 pinning tests added.
    • Expectation: met — PR fix(ai): quota toNumber treats blank strings as absent, not zero (#1741) #1742 MERGED (7486105). Laptop verification: ai quota tests 11 pass / 0 fail (negative control: 4 new tests fail with the fix stashed), cli quota tests 7 pass, ai typecheck clean (both tsconfig passes).
    • Links: PR fix(ai): quota toNumber treats blank strings as absent, not zero (#1741) #1742; branch creator/1741-quota-tonumber-blank; probe ~/git/fleet/probe-2026-11-05-creator-opencode-1235/ (laptop)
  7. added a commit that references this issue on Nov 8, 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

    app/aiPackage: ai (pkgs/ or apps/ in this repo)area/typescriptTypeScript code areaeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions