Skip to content

pm-stack(aggregator): normalizeResetAt accepts impossible and non-ISO reset dates — its string branch is Date.parse-only, rolling 2026-02-30 to Mar 2 instead of returning null #4968

Open

Description

@dezren39

Problem

normalizeResetAt in pm-stack/aggregator/src/quota-gather-map.ts (lines 44–57, at main 67f97516c) normalizes a provider reset stamp to ISO. Its numeric branch is guarded (finite, > 0, valid Date), but its string branch is Date.parse-only:

if (typeof v === "string" && v !== "") {
  const t = Date.parse(v);
  return Number.isNaN(t) ? null : new Date(t).toISOString();
}

Date.parse rolls impossible calendar dates over to a different real date and accepts non-ISO formats, so a drifted/malformed provider stamp is silently converted into a wrong-but-plausible resetsAt instead of null. Garbage strings do return null — the gate is inconsistent within itself: "not-a-date" → null, but "2026-02-30" → a fabricated March 2 reset time.

This contradicts the aggregator's own date contract: isIsoDate/parseIsoDate (pm-stack/aggregator/src/validate.ts:116) rejects impossible dates by round-trip, and the feed gates built on it (isQuotaFreshness feed.ts:502, isQuotaWindow feed.ts:474) already refuse such values.

Evidence (verified this run, laptop bun probe at origin/main 67f97516c)

Probe: /tmp/probe-resetat.ts importing normalizeResetAt from the worktree copy of quota-gather-map.ts; output in /tmp/probe-resetat.log on the laptop:

input result
"2026-02-30" "2026-03-02T00:00:00.000Z" — impossible date rolled over
"2026-04-31" "2026-05-01T00:00:00.000Z" — impossible date rolled over
"2026-02-29" (2026 is not a leap year) "2026-03-01T00:00:00.000Z" — rolled over
"March 2, 2026" "2026-03-02T06:00:00.000Z" — non-ISO format accepted (and TZ-shifted)
"2026-03-02" "2026-03-02T00:00:00.000Z" — control OK
"2026-03-02T00:00:00.000Z" unchanged — control OK
"2024-02-29" (leap year) "2024-02-29T00:00:00.000Z" — control OK
"not-a-date" null — garbage already refused

Call sites that inherit the flaw (all feed the quota-current contract's resetsAt, which drives reset-time display and reset-soon guidance):

  • pm-stack/aggregator/src/clinepass-adapter.ts:106 — ClinePass window resetsAt
  • pm-stack/aggregator/src/quota-gather-map.ts:279 — Meta file-export windows
  • pm-stack/aggregator/src/quota-gather-map.ts:345 — entryFromResult provider windows
  • pm-stack/aggregator/src/quota-gather-map.ts:618 — Turso billing period end

Related sites in the same file worth checking in the same pass (not the primary scope): the credentialExpiresAt string branch in entryFromResult (Date.parse + toISOString, ~line 340) and the Turso period start/end Date.parse pair (~lines 621–622), from which windowSeconds is derived.

Why it matters

resetsAt is operator-facing: it tells Drew when a quota pool resets and feeds reset-soon choices. A provider payload that drifts (or a hand-edited file export) carrying an impossible date currently produces a confident, wrong reset time a day or two off, instead of the null the same function already returns for unparseable strings. Every sibling date gate in the aggregator was hardened to the isIsoDate contract for exactly this failure mode; this shared normalizer — the one four call sites funnel through — was missed.

Acceptance criteria

  • normalizeResetAt returns null for impossible calendar dates (2026-02-30, 2026-04-31, 2026-02-29) and for non-ISO date strings ("March 2, 2026").
  • Valid inputs are unchanged: ISO date/datetime strings normalize as today, 2024-02-29 passes, epoch-second and epoch-millisecond numbers behave as today, garbage strings still return null.
  • Regression tests added to pm-stack/aggregator/test/quota-gather-map.test.ts covering the refused and accepted cases above.
  • Existing aggregator test suites (at minimum quota-gather-map, clinepass-adapter, feed) still pass.
  • The related credentialExpiresAt / Turso period Date.parse sites in the same file are either fixed to the same contract or explicitly called out in the PR as follow-ups.

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/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Nov 3, 2026
  2. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A, slug new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-11-03 08:35 CST
    • Found via: filed this run
    • Task: Filed after verifying on the laptop at origin/main 67f9751: normalizeResetAt (pm-stack/aggregator/src/quota-gather-map.ts:44-57) string branch is Date.parse-only — bun probe (/tmp/probe-resetat.log on laptop) showed 2026-02-30 → 2026-03-02, 2026-04-31 → 2026-05-01, 2026-02-29 → 2026-03-01, "March 2, 2026" accepted; garbage → null and leap/ISO controls OK. Fix packet enqueued to repo-health-code-pilot.
    • Expectation: effort/s — small-fix: gate the string branch on the aggregator's isIsoDate contract (validate.ts:116) so impossible/non-ISO stamps return null; regression tests in test/quota-gather-map.test.ts.
    • Links: evidence in the issue body; packet packet-4968-fix.json in the 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 3, 2026
  4. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  5. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: opencode-claimant
    • Session / ID: ses_e5e5d22ab54ffee4nB9PyxJLlH · queue task 8116
    • Route: opencode-route/workhorse · build
  6. added
    user/reviewNeeds a user-look: Muse reviews first and handles what it can
    on Nov 4, 2026
  7. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · escalated

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: cron repo-fleet-monitor run 2026-11-04 04:12 CST
    • Found via: monitor reconciliation (in-progress tracker, Duty 3)
    • Task: escalation — item past 2x its effort/s deadline with no activity since handoff
    • Expectation: holder checkpoints, parks, or hands off; otherwise this is a requeue candidate once the queue DB is stable
    • Links: last holder note is the handoff of 2026-11-03 (opencode-claimant session); no comments, commits, or CI activity on this issue since

    This item carries queue/claimed + work/assigned + status/in-progress, but nothing has moved in ~13h against a 2h (effort/s) expectation. Adding user/review per the lifecycle protocol §6 escalation rule. Note: the repo-queue Supabase backend is in an outage window today (code#5370), so the monitor is NOT requeueing from GitHub alone this run — if the holding session is done, the next monitor run after DB recovery will reset the labels and requeue; if you are still holding this, please checkpoint now and the escalation clears.

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

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codekind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientqueue/claimedQueue worker lifecycle statesource/creatorFiled by the new-issue-creator jobstatus/in-progressQueue worker lifecycle statetask/small-fixQueue packet kind: bounded fix with reproduction and targeted testsuser/reviewNeeds a user-look: Muse reviews first and handles what it canwork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions