Skip to content

aggregator(feed): isFleetFeed checks only that quota.pools is an array — burn/choices, pool entries and windows unvalidated, one malformed pool crashes /quota #2429

Closed

Description

@dezren39

Bug

pm-stack/aggregator — the fleet feed's gate isFleetFeed checks the quota section even more thinly than the rest: it requires only that quota.pools is an array. It never checks burn or choices (both mandatory in QuotaView, src/model.ts:274-286), never checks pool entries, and never checks a pool's windows. The live source serves feed.quota verbatim, so one malformed pool — or a feed written by an older collector that lacks burn/choices — crashes the /quota page instead of degrading.

Sibling of #2427 (same gate, attention entries); filed separately because the section, consumers, and fix differ.

Evidence (verified this run, main 5a34fa70)

  • Gate: src/feed.ts:207-219 — the entire quota check is typeof f.quota === "object" && f.quota !== null && Array.isArray((f.quota as QuotaView).pools). burn, choices, pool shape, and window shape are unchecked.
  • Verbatim consumer: src/sources/live.ts:754-785 — quota() sets view = feed.quota and returns it (the gatherer overlay aside); there is no pool/window filter anywhere on this path, unlike the registry/projects/snapshots filters in the same file.
  • Unguarded dereferences: src/render.ts:1754 view.pools.map(p => p.windows.map(...)), :1776 view.burn.map(...), :1785-1787 view.choices.hasHeadroom/...; src/derive.ts:813 buildAttentionFlags iterates pool.windows and windowPercent(w) reads w.limit (derive.ts:155-158) — called uncaught from usage()'s feed-saas branch (live.ts:832). The /quota route (server.ts:485-488) and /api/quota.json (server.ts:651) await these with no catch, and fetchHandler has no top-level catch.

Laptop probe (~/git/fleet/probe-2026-10-29-laneA-feedquota/, real aggregator src/ from origin/main — feed.ts/render.ts md5-identical to the sandbox clone — Bun 1.4.2, real isFleetFeed + renderQuota + buildAttentionFlags):

== control: well-formed quota
isFleetFeed: true
renderQuota: ok, html bytes = 18339
buildAttentionFlags: ok, flags = 0

== quota missing burn+choices (gate only checks pools)
isFleetFeed: true
renderQuota THROWS: TypeError - undefined is not an object (evaluating 'view.burn.map')

== pools [null]
isFleetFeed: true
renderQuota THROWS: TypeError - null is not an object (evaluating 'p.windows')
buildAttentionFlags THROWS: TypeError - null is not an object (evaluating 'pool.windows')

== pool missing windows
isFleetFeed: true
renderQuota THROWS: TypeError - undefined is not an object (evaluating 'p.windows.map')
buildAttentionFlags THROWS: TypeError

== pool windows [null]
isFleetFeed: true
renderQuota THROWS: TypeError - null is not an object (evaluating 'w.limit')
buildAttentionFlags THROWS: TypeError - null is not an object (evaluating 'w.limit')

== pool windows non-array (string "soon")
isFleetFeed: true
renderQuota THROWS: TypeError - p.windows.map is not a function
buildAttentionFlags: ok, flags = 4   (iterates the string's characters as windows — bogus flags)

Why it matters

/quota is the page the fleet's pool discipline runs off (headroom, burn-before-reset, do-not-start guidance). The feed is written by a separate collector job and the aggregator's stated contract is that a stale or partial feed "keeps rendering with its age stated" — instead, schema drift in one pool (a renamed windows, a null placeholder, an older collector omitting burn/choices) takes the whole page down, and a non-array windows silently fabricates attention flags in the usage view. Every sibling section got a validator or an element filter for exactly this class; the quota section — the feed's original payload — never did.

Acceptance criteria

  • A feed passing isFleetFeed can no longer crash renderQuota or buildAttentionFlags: either the gate validates the full QuotaView shape the renderers dereference (burn and choices present with their arrays, every pool an object with a windows array of window objects), or the live source filters/defaults malformed pools and windows the way it filters registry/projects records — triage pins the layer, and a rejected feed must fall back to fixtures like other invalid feeds rather than 500.
  • Malformed pools/windows are dropped or defaulted individually; the well-formed pools still render on /quota and /api/quota.json.
  • No fabricated attention flags: a non-array windows value can never be iterated as a collection of windows.
  • Regression tests (aggregator suite) cover: quota missing burn/choices; pools: [null]; a pool missing windows; windows: [null]; windows as a string — each rendering the valid remainder or the fixture fallback, never throwing; a fully well-formed feed is unchanged, and the collector (which shares isFleetFeed) still accepts its own emitted feed.

Dedup

gh searches (open + closed) for "quota pools / pool windows / poolState" return #1170 (per-pool read age display), #1119 (contract tests for the /api/quota.json saas/payg schema), #1114 (disabled providers in quota), and #2416 (quota-current.ts overlay windows — a different file and pipeline stage: the gatherer overlay, not the feed's own quota section). No open or closed issue covers isFleetFeed's quota check or feed pool/window validation. Related: #2427 (attention entries, same gate).

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (code lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a run 2026-10-29T21:05Z (America/Chicago 16:05)
    • Found via: filed this run
    • Task: Triage the fleet-feed quota-section validation gap: pin whether the fix lands in the isFleetFeed gate (feed.ts:207-219 — full QuotaView shape) or as pool/window filtering in the live source's quota() (live.ts:754), then route a small_fix packet. Sibling of aggregator(feed): isFleetFeed never checks attention entries — one null entry crashes /attention + /today, entries without a string sessionId silently collapse into one #2427 (attention entries, same gate).
    • Expectation: effort/xs — one validator/filter plus regression tests; done when a feed with a missing burn/choices, a null pool, or a malformed windows value renders the remaining pools (or the fixture fallback) on /quota instead of throwing or fabricating attention flags.
    • Links: Evidence + probe in the issue body; probe on the laptop at ~/git/fleet/probe-2026-10-29-laneA-feedquota/ (real aggregator src/ from main 5a34fa7, md5-identical).
  4. added a commit that references this issue on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/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