Repository navigation
Description
Summary
The quota gatherer's snapshot dedup (recordsFromEntries, pm-stack/aggregator/src/quota-gather-map.ts:243-274) keys its "value last appended" state by serviceId|window only — but the gatherer produces one entry per credential (scripts/gather-quota.ts: fetchQuotaForProviderInstances → entryFromResult per result; serviceIdFor maps by provider, quota-gather-map.ts:63-78), and SnapshotRecord has no credential dimension (src/model.ts:1256-1267). When two credentials of one provider are both ok in a tick, their readings interleave into a single series and the dedup — whose stated purpose is "the store gains a point when a reading moves, not every 15 minutes" (quota-gather-map.ts:228-242) — appends a noise point every tick forever, even when neither value ever changes.
Mechanics, all in recordsFromEntries + the caller's merge (gather-quota.ts:322-340):
- The dedup key is
${entry.serviceId}|${w.name}(quota-gather-map.ts:254);entry.credentialIdis carefully carried onto the entry (:188) and then never used. - The comparison reads the input
lastAppendedmap (:256), never the values appended earlier in the same tick, so tick 1 appends both credentials' points for the same series at the samets. - The state map afterwards holds whichever credential was processed last. Next tick, the other credential's (unchanged) value differs from the stored stamp and appends; the merge flips the stored stamp; the following tick the first credential appends again — alternating 10, 20, 10, 20… at the 15-minute tick cadence, indefinitely.
Verified (this run, on main 9752a47)
Probe ON THE LAPTOP (Bun, driving the real recordsFromEntries from quota-gather-map.ts, sha256 b18d3fd9… identical in both clones; state merged between ticks exactly as gather-quota.ts:339 does; probe + log in the goal's hidden_files/probe-2026-10-28-laneB-gatherdedup/):
- Two ok entries, same serviceId
anthropic:claude-pro, window5h, credential A at 10% and credential B at 20%, values never changing:- tick 1 → 2 records appended, same series, same ts:
…|5h=10and…|5h=20 - tick 2 → 1 record (
=10), tick 3 → 1 record (=20), tick 4 → 1 record (=10) — one noise point per tick, alternating, forever.
- tick 1 → 2 records appended, same series, same ts:
- Control, single credential at a constant 10%: tick 1 → 1 record, ticks 2-3 → 0 records; a genuine move 10→15 appends exactly 1. The dedup works precisely as documented for the single-credential case.
Reachability: per-credential results are the gatherer's normal shape — gather-quota.ts reads OpenCode's credential store and fetches per credential (its own comments describe multiple credential rows per provider, e.g. Codex's token-sharing login row). Any provider with two ok credentials (two stored Claude/Codex logins) triggers it; error/unconfigured rows are skipped by the state !== "ok" guard, so a refused second credential does not.
Why it matters
The snapshot store is the hub's long-term usage series (§4): it feeds history, trend and cost views, and its writers are deliberately append-on-change so the series stays sparse and meaningful. In the multi-credential case the series instead gains a fabricated sawtooth — the line oscillates between two accounts' real readings at one point per 15-minute tick — which reads as usage swinging up and down, corrupts any per-service history derived from it, and grows the store ~96 points/day/window for data that never moved. It also makes the two accounts' readings indistinguishable: both are recorded under one serviceId with no credential attribution.
Distinct from #1540 (SnapshotStore append-time validation of non-finite/ non-ISO records — different layer, the store's gate; these records are individually valid) and from #1560 (registry merge forgetting fields).
Acceptance criteria
- With two ok credentials of one provider in a tick and unchanged values across ticks, the gatherer appends no per-tick noise: either the dedup/series key distinguishes credentials (and the record/series shape documents that), or the mapper's per-provider series rule is made explicit and only one deterministic reading per serviceId+window is recorded per tick — pinned by a test.
- Tick 1 with two ok credentials no longer appends two contradictory points for the same series at the same timestamp (or the two points are credential-distinguishable, per the pinned rule).
- Regression tests cover: two-credential constant values over ≥3 ticks (no alternating appends), the single-credential constant case (append once), and a genuine value move (append once).
- Existing aggregator test suites still pass.
Activity
developing-today commentedon Oct 28, 2026
CODE-647
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 28, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 28, 2026
🤖 Agent note · triage findings (queue task 1741)
Verdict — bug confirmed as filed; series rule PINNED
The snapshot series stays pool-level:
(serviceId, window). Per tick, exactly one deterministic reading per key is recorded: the maximumpctOfLimitacross that tick's ok credentials for that provider+window. NoSnapshotRecordshape change, no credential dimension in the store.Why not credential-distinguished series
Verified on current main
d39e6111(file contents re-read this run):recordsFromPools,recordsFromSaasServices,recordsFromProviderUsage(src/snapshot-records.ts). The gatherer is the only writer that ever sees credentials.SnapshotStore.byService()groups byserviceIdonly (src/snapshots.ts:82),UsageView.historyisRecord<serviceId, SnapshotRecord[]>(src/model.ts:1282), andsnapshotKeyis(serviceId|metricId|ts|value)(src/snapshots.ts:44).serviceIdFor's own contract (src/quota-gather-map.ts:52-57) is that gatherer records continue the series the dated-read writers started. Credential-keyed gatherer records would fork that series for every multi-credential pool and require plumbing a new dimension through validation (isSnapshotRecord), the store key, grouping, and the pace-projection/render consumers — a cross-cutting shape change to fix a dedup bug.quota-current.json, where entries carrycredentialId/credentialLabel(entryFromResult, quota-gather-map.ts:188-189). Snapshots are pool history; quota-current is per-credential present.Why max, specifically
Implementation spec (follow-up task enqueued from this triage)
In
recordsFromEntries(src/quota-gather-map.ts:243-274):serviceId|window; select maxpctOfLimitper key (tie-break: lowestcredentialIdlexicographically, so the emitted record'sts/resetsAtare deterministic too).lastAppendedAND track selections in the returnedappendedmap within the same call — the current code only reads the input map (line 256), which is the same-tick half of the bug. At most one record per key per call.snapshotKeyfresh-filter both behave onceappended/records carry one entry per key (note:snapshotKeyincludesvalue, so it never deduped the two contradictory tick-1 points — records-level selection is the only correct fix point).recordsFromEntriesdoc comment, replacing/augmenting the §"value changed" wording at lines 228-242.Regression tests (test/quota-gather-map.test.ts), replicating the caller's state merge between ticks:
Out of scope (unchanged from the packet): backfilling/cleaning already-interleaved points in existing stores; provider fetching / credential reading.
dezren39 commentedon Oct 28, 2026
🤖 Agent note · done
dezren39 commentedon Oct 28, 2026
Queue task 1873 progress on branch queue/1873-1624-snapshot-max-reading — PR: #1775
What changed (scope: quota-gather-map.ts recordsFromEntries + tests only):
Test results:
dezren39 commentedon Oct 28, 2026
🤖 Dispatcher binding · openchamber-dispatcher run 2026-10-28 13:45 CDT (burn-by-expiry run: Drew directive 13:44 CDT — fill free session slots, burn the Go window resetting 2:26 PM and the Meta window resetting 4:23 PM)
small_fix) ↔ issue aggregator: quota gatherer snapshot dedup ignores credentialId — two ok credentials of one provider interleave into one series and append an alternating noise point every tick #1624ses_e7c238f39ffeESC36BYotunrZn(laptop OpenChamber, agent build)meta/muse-spark-1.3-contributor(Meta pool; fresh quota read this run ~13:47 CDT: 5h 1% / weekly 11%, reset 4:23 PM — OPEN, plan quota only, never PAYG; seated on Meta per the run's burn-by-expiry directive)~/git/fleet/1873-1624-snapshot-max-readingon the laptop, branchqueue/1873-1624-snapshot-max-reading(from origin/main 35726e8)pm-stack/aggregator/test/quota-gather-map.test.tsin the bound worktree — the aggregator: quota gatherer snapshot dedup ignores credentialId — two ok credentials of one provider interleave into one series and append an alternating noise point every tick #1624 fix surface. The queue task itself remainsqueuedin the fleet queue (no targeted claim/binding verb exists yet — Dispatcher v1 live: scheduled launches of OpenCode/OpenChamber sessions for queued issues #1337/Routing policy alignment: map OpenChamber routing categories to the fleet's Workhorse-first tiers + POOL MAP gates #1272); the owning lane reconciles/harvests from here, and the babysitter shepherds the session.dezren39 commentedon Oct 29, 2026
🤖 Agent note · bound
queue/1873-1624-snapshot-max-readingnames the task), but the pickup never landed in the queue — task 1873 still sitsqueued, attempts 0, never claimed. The queued task is therefore to be treated as PARKED on this PR: resume as the PR's shepherd — review CI, merge when green, do NOT re-implement. Queue-side claim/park by the monitor is pending the by-id claim/bind verb (Dispatcher v1 live: scheduled launches of OpenCode/OpenChamber sessions for queued issues #1337); until it lands, this note is the binding record a lane's revalidation will find.