Repository navigation
Description
Problem
isFleetFeed (pm-stack/aggregator/src/feed.ts:207-219) never validates the
optional chargesAttention section at all — it checks only version,
generatedAt, quota.pools being an array, and attention being an array.
But the live source dereferences every chargesAttention entry unguarded,
so one malformed entry in a feed that passes the gate crashes usage() —
and with it every route that awaits it.
Evidence (verified this run, main 5a34fa70)
-
feed.ts:25-52:FleetFeed.chargesAttention?: UnmatchedCharge[], where
UnmatchedCharge(charges-match.ts:44-50) requiresmerchant: string,
amountUsd: number,at: string,source. -
feed.ts:207-219:isFleetFeedcontains nochargesAttentioncheck of
any kind — not array-ness, not entry shape. -
sources/live.ts:820-827: inusage()'s feed branch (taken whenever
feed.saasis non-empty), the entries are mapped verbatim:const chargeFlags = (feed.chargesAttention ?? []).map((u) => ({ label: `${u.merchant} — unrecognized SaaS/AI charge`, detail: `$${u.amountUsd.toFixed(2)} on ${u.at} via ${u.source === "plaid" ? "Plaid" : "Gmail alert"} …`, severity: u.amountUsd, }));
There is no filter or per-entry guard — unlike the registry / projects /
snapshots consumers in the same file, which validate or filter first. -
server.ts:486,492,656,669:/quota,/quota/service/<id>,
/api/quota.jsonand/api/quota/service.jsonallawait source.usage()
with no catch, so the throw becomes a 500 for the whole usage hub — not
just a missing charge flag.
Laptop Bun probe (~/git/fleet/probe-2026-10-29-laneA-chargeattn/, real
feed.ts from main 5a34fa70, md5-identical, Bun 1.4.2; the map above
copied verbatim from live.ts:820-827):
chargesAttention |
isFleetFeed |
verbatim map |
|---|---|---|
| valid entry (control) | true |
OK — 1 flag, $12.50 |
[null] |
true |
throws TypeError: null is not an object (evaluating 'u.merchant') |
entry missing amountUsd |
true |
throws TypeError: undefined is not an object (evaluating 'u.amountUsd.toFixed') |
amountUsd: "12.50" (string) |
true |
throws TypeError: u.amountUsd.toFixed is not a function |
"oops" (non-array) |
true |
throws TypeError: (feed.chargesAttention ?? []).map is not a function |
Why it matters
chargesAttention is the exactly-once surfacing channel for unmatched
SaaS/AI charges (#1057): the collector emits these entries from Plaid/Gmail
candidate data, so a single drifted or hand-edited feed entry takes down
/quota and both quota JSON twins entirely instead of dropping one flag.
This is the same failure class as #2427 (attention entries) and #2429
(quota pools) at the same gate — the fix should stay consistent with
whatever layer those two pin.
Acceptance criteria
- A feed passing
isFleetFeedcan no longer crashusage(): either the
gate validateschargesAttention(array of entries with string
merchant/atand finite numberamountUsd), or the live source
filters/defaults malformed entries before the map — the layer pinned
consistently with the aggregator(feed): isFleetFeed never checks attention entries — one null entry crashes /attention + /today, entries without a string sessionId silently collapse into one #2427/aggregator(feed): isFleetFeed checks only that quota.pools is an array — burn/choices, pool entries and windows unvalidated, one malformed pool crashes /quota #2429 rulings. - Malformed entries are dropped individually; well-formed entries in the
same array still surface as attention flags. - Regression tests cover:
[null]entry, entry missingamountUsd,
stringamountUsd, non-arraychargesAttention— each rendering the
valid remainder (or fixture fallback), never throwing; a well-formed
feed unchanged, and the collector still accepts its own emitted feed.
Dedup
gh searches (open + closed) for chargesAttention and isFleetFeed charges returned no covering issue. Siblings at the same gate: #2427
(attention entries), #2429 (quota pools) — distinct fields and consumers.
#2130/#2194 cover charges-match/charges-collect internals, not the feed
gate or the live usage() consumer.
Activity
developing-today commentedon Oct 29, 2026
CODE-1246
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
Merge pull request #3435 from developing-today/queue/5328-2439-charge…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
ba2e97dd1b89843a0ef47597c6303c26916bb607); aggregator(feed): isFleetFeed never checks chargesAttention — one malformed entry crashes usage() and takes down /quota + the quota JSON twins #2439 closed.dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done (triage)
Verdict: CONFIRMED at current main
a17ae136(2026-11-01).Evidence (fresh, this run):
pm-stack/aggregator/src/feed.ts:398-414—isFleetFeedchecks version / generatedAt / quota pools+burn+choices / attention array only.chargesAttentionis unchecked in any form.loadFeed(feed.ts:422+) sanitizes ONLY the quota section viasanitizeQuotaView.pm-stack/aggregator/src/sources/live.ts:893-900—(feed?.chargesAttention ?? []).map(...)dereferencesu.merchant,u.amountUsd.toFixed(2),u.at,u.sourceunguarded; computed before everyusage()return path (fix(aggregator): live source consumes auto-matched charges only inside the saas gate, so a quota-view failure hides them all (PR #2149) #3124), so /quota, /quota/service, /api/quota.json and /api/quota/service.json all inherit the crash.[null]→ TypeError onu.merchant; missingamountUsd→ TypeError onu.amountUsd.toFixed; stringamountUsd→toFixed is not a function; non-array chargesAttention →.map is not a function— all with isFleetFeed=true.Layer pinned — the landed #2429 pattern in the same file (gate + loadFeed sanitizer, PR #3070), NOT a live-source filter:
isFleetFeed): section-level shape only —chargesAttentionpresent-but-non-array rejects the feed (mirrors the burn/choices treatment); absence stays valid (optional section).loadFeed): newisUnmatchedChargeelement validator +arrayOfdrop-and-count, count surfaced on the feed (mirroringquotaDropped). Element contract = exactly the fields live.ts dereferences: stringmerchant, finite numberamountUsd, stringat. (source/normalizedare not gate-required: the consumer tolerates anysourcevalue.)Regression tests named in the fix packet:
test/feed.test.ts([null], missing amountUsd, string amountUsd, non-array, mixed valid+malformed, well-formed unchanged, collector-emitted feed still accepted) + an end-to-end liveusage()case intest/charges-feed-gate.test.ts.Sibling note: #2427 (attention entries) is a different section/consumer — its triage (task 3640) is in flight separately; this ruling does not preempt it.
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up