Skip to content

Add v4 quota cycle contribution sync - #13

Merged
starkdmi merged 19 commits into
mainfrom
ai/quota-cycle-sync-f197
Aug 25, 2026
Merged

starkdmi merged 19 commits into
mainfrom
ai/quota-cycle-sync-f197

Conversation

@starkdmi

@starkdmi starkdmi commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

Builds on the sync-ready quota-history reconstruction. This PR does not recreate parsing, attribution, or cycle reconstruction.

Sync contract

  • Introduce sync_batch.v4 / sync_ack.v4 while still accepting v1–v3.
  • Add provider-neutral QuotaCycleContributionV1. Codex currently emits only attributed weekly cycles (nominal 10,080-minute window).
  • Hosted contributions stay compact: contribution ID, provider, account, optional limit ID and duration, reset timestamp, UTC daily envelopes, and exact usage slices for partial UTC days at cycle and schedule-transition boundaries.
  • Daily envelopes carry timestamped first, last, minimum, and maximum percentages. Boundary slices carry token-category totals and estimated cost in integer micro-USD.
  • Unattributed, five-hour, and monthly cycles stay local. Raw events, hashes, paths, source IDs, quota payloads, plans, credits, and fingerprints are not synced.
  • Dirty tracking, HTTP chunking, retries, snapshots, and successful-sync bookkeeping now include quota contribution IDs.
  • Reconstruction ignores quota observations recorded more than one hour after that window's resets_at, so import-time replay stamps cannot extend a closed cycle. Hosted merge applies the same bound on read and ingest for already-synced rows.

Tests

Collector coverage for weekly-scope selection, daily envelopes, exact boundary slices, stable contribution IDs, and exclusion of unattributed / five-hour / monthly cycles.

Open in Web Open in Cursor 

cursoragent and others added 19 commits August 22, 2026 22:30
Emit attributed weekly Codex cycles as compact hosted contributions
inside sync_batch.v4, with daily envelopes, exact boundary slices, and
dirty tracking. Keep v1-v3 batches accepted without quota collections.
Empty batches now use sync_batch.v4, so ack validation tests expect
sync_ack.v4 while still covering v1 and v3 pairing.
`cargo clippy --all-targets -D warnings` fails on this branch: sample_usage_event
takes nine arguments against a limit of seven. It is a test fixture whose
parameters are all distinct token counts, so a struct would obscure the call
sites rather than clarify them.

This branch targets sync-ready-quota-history rather than main, and ci.yml only
runs on pull requests into main, so the failure has never surfaced in CI.
A Codex weekly cycle starts at the first request after the previous reset, not
at the reset itself. Consecutive cycles therefore cannot overlap unless the
earlier one was reset ahead of schedule, which happens whenever a banked or
server-granted reset is used. Reconstruction already detects this and records it
as has_schedule_overlap, but the contribution dropped the flag before sync, so
the backend could only re-derive the geometry and had to treat every early reset
as a conflict.

On real local history that mislabelled 110 of 122 cycles, and 34 of 38 gaps
across the three most-used accounts in the last 30 days are early resets rather
than the nominal seven days.

Carrying the flag lets the backend separate an overlap a single device observed
from one that only appears after merging two devices, which is the case that
genuinely indicates records that failed to cluster or two accounts resolving to
one identity. inferred_start stays off the wire: it is exactly
representative_reset minus the nominal duration, which the backend already
computes.

The field defaults to false so payloads written before it deserialize
unchanged.
Every synced record is checked against a closed set of permitted keys, and an
unrecognized key refuses the whole batch. That guard is deliberate: an
unexpected key may carry a path, address, or message the contract excludes, and
refusing guarantees none of it is stored.

It has a second cause, though. A collector newer than the deployment it syncs to
trips the same guard, and until now both looked identical from the client: an
opaque 400 that stopped events and summaries as well as the affected collection.
Adding has_schedule_overlap reproduced exactly this against an endpoint without
the field.

The endpoint now returns the offending field name, nested keys included as a
dotted path, and collectors render it with the remedy instead of a truncated
body. Only the name crosses the wire, never the value. Malformed values inside a
known field stay a plain 400, since that is a client defect rather than version
skew.

The release order this implies is now written down: deploy the backend before
releasing a collector that adds a record field. New top-level collections stay
backward compatible and need no version bump, because per-collection ack counts
already turn an endpoint that ignores one into a loud count mismatch.
A window cannot be observed once it has reset: from that moment the provider
issues a fresh resets_at. Evidence that post-dates its own reset is therefore a
replay of historical records stamped with the import time, and reconstruction
was letting it back in — clustering it into the long-closed cycle by resets_at,
then bucketing it into a daily envelope by observed_at.

On one paired device that gave seven July cycles an envelope dated 2026-08-12,
every one of them written inside the same ten seconds at a different percentage,
up to 26 days past the cycle's end. 13 of that device's 106 synced envelopes fell
outside their own window; the other device had none of 299.

The bound is an hour rather than minutes because real recomputation lag exists:
this machine has a window whose provider kept reporting an elapsed reset with a
steady 77-79% for a continuous 47 minutes, and that evidence is genuine. Replays
look nothing like lag — weeks late, in bursts of unrelated windows sharing one
timestamp — so an hour separates them by more than two orders of magnitude.
Server merge now ignores daily envelopes observed more than an hour after the
contribution reset, matching collector reconstruction. Already-stored rows are
filtered on read; ingest strips them instead of rejecting the batch.
A window that reported zero usage for its whole life and was then replaced
never functioned as a cycle. Nothing was ever attributed to it, so it cannot be
told apart from a reschedule of the window that followed, and reconstruction
drew each one as a reset that never happened.

They arrive in runs. On 2026-07-10 one account produced four windows where two
announced resets occurred, three of them minutes apart at zero percent. Across
both paired devices 16 such windows exist, all superseded, 14 of them during the
July tier migration.

The rule is that a cycle must have spent something. Sitting at zero is not
enough to be told apart from a reschedule, while three percent is: a window that
peaked at 78 across a full week and another at three before a 51-day gap are
both real, and both survive. The newest cluster is exempt, since a cycle that
has only just begun legitimately reads zero.

Announced provider resets are unaffected. Every one of the ten global resets
publicly announced across July still reconstructs as its own cycle.
Consumption cannot fall inside a window, so a reading below one already seen is
stale rather than a refund. Concurrent sessions produce them routinely: two poll
at once and the slower one still holds the earlier figure, sometimes carrying
the same timestamp to the second. When such a reading lands last on a day, that
day closes below the one before it and the chart draws a decline that never
happened.

Three of 103 multi-day cycles fell by one to three points this way, and 5% of
envelope days closed below their own observed maximum.

Daily first and last now carry the running maximum for the cycle, which is what
consumed-by-then means. The observed minimum and maximum are left alone, since
they are the evidence that the jitter occurred.
A reset is a handover: once a window resets, the provider reports the new
schedule and never returns to the old one. A cluster that another cluster
brackets in observation time - reported before it appeared and again after it
vanished - therefore cannot be a cycle of its own, however many observations
back it.

During the July 2026 tier migration Codex answered scattered turns with a
blank snapshot: near-zero usage against a fresh weekly reset, interleaved with
the live schedule for as long as 34 hours. Each run became a cycle that took
days away from the cycle actually running, which is what drew two rising lines
over the same hours for a single account.

Requiring the bracketing cluster to have passed the suspect's peak by the time
it resumes preserves genuine early resets, which lead from the moment they
start. Across the local store this drops 14 phantom cycles and keeps 108; every
drop peaked at 4% or less while the live cycle stood between 8% and 100%, and
all of them fall between 8 and 11 July 2026.
Two overlapping lines for one account over the same days, twice more.

The tolerance was measured from the cluster's first point, as though a cycle's
reported reset had to fit inside a five-minute window. It drifts a second or
two at a time and has been seen wandering nine minutes over a cycle's life, so
the cycle was cut in two the moment it passed five and the remainder drawn as a
second cycle beside the first. Measured as a gap between neighbouring resets it
stays whole; consecutive resets sit hours apart at the least, so chaining
cannot reach across one.

The phantom rule also asked the bracketing schedule to have outrun the
bracketed one, which was over-cautious. Once a window resets its resets_at
moves forward and can never be reported again, so a schedule cannot resume
after another has taken over: bracketing alone settles it, and a genuine early
reset is never bracketed because the schedule it replaced simply stops. This
drops a cluster of 623 observations that ran to 99% over 19 hours while the
schedule either side of it sat near 54% - not a cycle that reset early but a
second counter reported under one slot for one account.

Across the local store this takes 98 weekly cycles to 94, all four from
starkovd and videochapter, and leaves every announced reset standing.
Every reconstruction rule throws away evidence that cannot describe a real
cycle, and every one of them is silent. If a provider change made them fire on
data they were never meant to judge, the only symptom would be fewer cycles,
indistinguishable from a quieter week.

`quota status` now counts the three: observations recorded after the window
they describe had already reset, windows that never left zero before being
superseded, and schedules another schedule was reported either side of. On this
store that reads 730, 33 and 28.

Status pays for one reconstruction it did not run before. That cost is inherent
to reporting what reconstruction discards, and this is a diagnostic run by hand
rather than anything on a hot path.
The comment justified the hour with a 47-minute event that turns out to
be a replay burst: 134 records from one rollout file sharing a timestamp
to the millisecond. The real evidence is 2026-06-01, where a live session
polled ~180 times over 37 minutes and the provider returned the expired
window at 96% every time.
The 5m and 1h counters break the aggregate down by lifetime rather than
adding to it: Claude reports 248 beside 148 + 100. Summing all three
doubled cache creation in every boundary slice and pushed the components
past the slice's own total_tokens, which computed_total derives from the
aggregate alone.

Also narrows the two sync-eligibility checks to the weekly window
exactly. Contributions already filter on equality, so a monthly window
counted toward weekly sync coverage and appeared in the sync-windows
export without ever being syncable.
A local store keeps quota observations and derives its own cycles from
them; there is no table for another device's contributions. Acknowledging
them as accepted told the sender to record them synced against this
target and stop offering them, certifying a write that never happened.

Refusing follows the authoritative-snapshot precedent: /v1/sync/batches
is documented as loopback diagnostics, and /api/sync/batches is the
contract that stores quota cycles. Reporting them rejected instead is not
available — the sender treats any rejected record as a failed batch and
separately requires accepted plus duplicates to equal what it sent.
A quota cycle can change without any summary changing: a reset moves, or
an observation carries no tokens. The pending count summed rollups,
passthrough summaries, code-change metrics and retirements, so those
uploads were invisible and the menubar reported nothing to send while a
sync would still have sent them.

The contributions were already being read here for retirement detection;
they now also go through pending_quota_cycle_contributions_for_sync and
into the total. Pending days stay summary-shaped, and the menubar derives
its day count from elapsed time whenever a sync has already succeeded.
ureq's request timeout covers reading the body, so the 30-second bound
meant for JSON replies also capped every artifact download. An archive
near the 256 MiB ceiling would have to sustain about 68 Mbit/s to finish
inside it, which is why downloads failed intermittently on a home
connection and succeeded on a well-connected machine.

Downloads now get ten minutes, enough for the ceiling at roughly
3.5 Mbit/s, and STATSAI_DEV_DOWNLOAD_TIMEOUT_SECONDS overrides it, with
zero meaning wait indefinitely. API calls keep the thirty seconds.
The wait loop retries the build lookup, not the transfer, so a read that
died partway ended the whole install on the first try. The 'attempt 1' in
the output is GitHub's own run attempt describing the CI build, not a
retry of ours — there were none.

A body cannot be resumed, so a retry re-requests it: two retries with a
short backoff. A read failure surfaces from read_to_end rather than from
call, so it is now marked retryable explicitly; without that the loop
would classify it as permanent. Exceeding the size ceiling stays
permanent, since re-requesting cannot make the artifact smaller.
@starkdmi
starkdmi changed the base branch from sync-ready-quota-history to main August 25, 2026 09:53
@starkdmi
starkdmi marked this pull request as ready for review August 25, 2026 10:06
@starkdmi
starkdmi merged commit 72f2db4 into main Aug 25, 2026
11 checks passed
@starkdmi
starkdmi deleted the ai/quota-cycle-sync-f197 branch September 1, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants