Skip to content

aggregator(charges-producer): one Gmail message with a missing/garbled date throws RangeError and discards the whole Gmail candidate batch #2448

Closed

Description

@dezren39

Summary

In the charges producer's Gmail path, a message whose message_sent_at.user_local is empty/missing and whose fallback date is missing or unparseable does not get skipped — it throws. The per-message date line calls isoDay(new Date(m.date ?? "")), and isoDay is d.toISOString(), which throws RangeError: Invalid Date on an invalid Date. The throw happens before the format guard on the very next lines that was written to skip such messages, and there is no per-message try around it: it propagates out of gmailCandidates, so main()'s catch discards the entire Gmail candidate batch for the period (every good message already collected is lost with it) and logs a misleading gmail read failed … plaid candidates only. One malformed triage row silently zeroes the Gmail half of the charges evidence for that run.

Evidence (main 5a34fa7)

  • pm-stack/aggregator/scripts/produce-charge-candidates.ts:57-59 — isoDay(d) is d.toISOString().slice(0, 10); toISOString() on an invalid Date throws RangeError.
  • pm-stack/aggregator/scripts/produce-charge-candidates.ts:193 — per-message: const at = (m.message_sent_at?.user_local ?? "").slice(0, 10) || isoDay(new Date(m.date ?? "")). TriageMessage (lines 109-115) makes both message_sent_at and date optional, so the fallback is reachable with "" or any unparseable string.
  • pm-stack/aggregator/scripts/produce-charge-candidates.ts:194-197 — the regex guard + skipped++ that was meant to handle exactly this case sits after the throwing call and never runs for it.
  • pm-stack/aggregator/scripts/produce-charge-candidates.ts:242-246 — the only catch is in main() around the whole gmailCandidates() call; it logs gmail read failed and continues with Plaid only. The candidates array accumulated inside gmailCandidates is lost.
  • Same pattern, operator-input variant: :165 computes the Gmail before: bound as isoDay(new Date(Date.parse(end) + 86_400_000)) — an invalid --end value makes Date.parse return NaN and throws the same way, again reported as a Gmail read failure.

Verified this run with a laptop Bun probe using the verbatim isoDay + date-line logic (file md5 0f4dba2a441e7c0894cdd2ca2ceefe81, main 5a34fa7):

control-good-rfc2822   -> 2026-10-27
control-good-userlocal -> 2026-10-28
missing-date           THREW: RangeError Invalid Date
garbage-date           THREW: RangeError Invalid Date
empty-both             THREW: RangeError Invalid Date
batch-all-good         -> [ "g1@2026-10-27", "g2@2026-10-28" ]
batch-one-bad          THREW: RangeError — whole gmailCandidates result lost

Probe: ~/git/fleet/probe-2026-10-29-laneB-chargeproducer/probe.ts (laptop).

Why it matters

The producer is the only source of Gmail charge candidates for the charges matcher (#1057 pipeline; scheduling tracked separately in #2205). A single triage message with a missing/garbled date field — data the producer does not control — silently drops all Gmail-sourced charges for the period, and the log line blames a "gmail read" failure, sending any investigation toward the CLI/auth instead of the one bad message. The skip-and-count path the author wrote for malformed messages (the regex guard + skipped) is unreachable for precisely the malformed shape it targets.

Acceptance criteria

  • A triage message with empty/missing message_sent_at.user_local and a missing or unparseable date is counted as skipped (the existing skipped counter / log), and processing continues with the remaining messages — the rest of the Gmail batch is still produced.
  • isoDay (or its call sites) cannot throw on an invalid Date: invalid input is handled at the date-derivation site, not by the whole-batch catch in main().
  • An invalid --end value is rejected with an error naming the argument (or handled equivalently explicitly), not surfaced as a Gmail read failure.
  • Regression tests cover: missing date, garbage date, empty-both message interleaved with good messages (batch survives, good candidates present, skip counted), and the two control shapes (RFC 2822 date, user_local present) producing the same at values as today.

Dedup

gh searches (open + closed) for produce-charge-candidates / isoDay / charge-producer terms: #2194 (open) is the consumer side — parseChargeCandidates in charges-collect.ts accepting impossible calendar dates — a different file, function, and failure mode; #2205 (open) is about scheduling the producer; #1057 (closed) is the original charges-matching feature. No issue covers the producer's Gmail date derivation throwing and discarding the batch.

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

  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Triage complete (queue task 3663): revalidated against current main a17ae1362ba8575268bb8550b59427d5664b5316. pm-stack/aggregator/scripts/produce-charge-candidates.ts:57-59 calls toISOString() on any Date; the Gmail path at :221 can pass new Date(m.date ?? "") before the format/skip guard at :222-225. The exception escapes the message loop and reaches the batch-level catch at :263-270, which drops all Gmail candidates. The existing Bun probe reproduced RangeError for missing, garbage, and empty dates; [good, bad, good] lost the batch; RFC 2822 and user_local controls passed.

    Fresh GitHub state also shows #3333 separately covers missing/flag-like and invalid/inverted --start/--end arguments and explicitly distinguishes this Gmail message-date failure. To avoid overlapping work, follow-up #5335 is scoped only to safe per-message date handling and batch-survival tests; CLI argument/date validation remains with #3333.

    🤖 Agent note · handed off

  5. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 1, 2026
  6. 18 remaining items

  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. added a commit that references this issue on Nov 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

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)source/creatorFiled by the new-issue-creator jobstatus:donetask/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