fix(antigravity-cli): read the per-generation timestamp from the agy 1.1.18 gen_metadata layout - #1196
Conversation
…1.1.18 gen_metadata layout agy 1.1.18 dropped `chatModel.#9.#4`, the `{#1: seconds, #2: nanos}` Timestamp this parser used to date each turn. `#9` now carries `#2` = u64::MAX (an int64 -1 "unset" sentinel) plus a new `#10` holding 8 length-delimited bytes. With `#4` gone every row fell through to the session-created stamp, so on a long-running session every turn was bucketed to the session start date and `--today` reported zero. `#9.#4` is still read first and unchanged, so pre-1.1.18 databases and older installs keep their exact behaviour. When it is absent, `#9.#10` is decoded as a nested Timestamp, a nested message holding the scalar in field 1 (varint or fixed64), or the payload itself as 8 raw fixed64-style bytes in either byte order. Every one of those readings is unit-detected by magnitude and range-checked against 2020-01-01..now+5y before it is accepted; anything outside that window is discarded and the session-created fallback takes over. `#9.#2` is never consulted, and u64::MAX is rejected explicitly so no path can promote the sentinel into a date. Constraint: no agy 1.1.18 install or gen_metadata database available, so `#10`'s encoding is inferred from a field dump in the issue, not observed Rejected: read `#9.#2` as the new timestamp | the only value ever seen there is the u64::MAX unset sentinel Rejected: decode the 8 bytes as an IEEE-754 f64 | any double in the 2^30-ish exponent range reads as a plausible epoch-second count, so it is the one candidate with a non-trivial false-positive rate against a non-timestamp payload Confidence: high that pre-1.1.18 parsing is unchanged; medium that the 1.1.18 reading fires on real data Scope-risk: narrow Directive: keep every inferred reading behind `plausible_epoch_ms` — a wrong date silently corrupts day buckets and the monotonic ratchet, which is worse than the session-start fallback this degrades to Not-tested: a real agy 1.1.18 `gen_metadata` row; if `#10` is neither a timestamp nor decodes in range, behaviour is identical to today's
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 32c164cc5f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let raw: [u8; 8] = payload.try_into().ok()?; | ||
| epoch_scalar_to_ms(u64::from_le_bytes(raw)) | ||
| .or_else(|| epoch_scalar_to_ms(u64::from_be_bytes(raw))) |
There was a problem hiding this comment.
Stop treating arbitrary bytes as epoch nanoseconds
When #9.#10 is an opaque identifier or hash rather than a timestamp, the plausibility check does not reliably reject it: the accepted nanosecond interval alone occupies about 2% of all u64 values, and trying both byte orders raises the chance of a uniformly distributed payload being assigned a plausible date to roughly 4%. Since the commit explicitly says this field's encoding is unverified, such a match silently mis-buckets usage and advances the server-side timestamp ratchet; decode only a confirmed wire representation rather than accepting raw integers in either endian order.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed, and fixed in 1a96891.
The probability estimate holds: with the plausibility window at 2020-01-01..now+5y, the nanosecond reading alone spans ~2.0% of the u64 space, so trying both byte orders gives an opaque payload roughly a 4% chance of being accepted. That is not an acceptable false-accept rate for a value that feeds day buckets and the monotonic ratchet, which has no correction path.
Rather than drop the raw readings outright, every inferred reading is now additionally constrained to the containing session's own lifetime: a generation cannot predate the session that holds it, and cannot land meaningfully in the future. session_timestamp was already threaded into parse_gen_metadata, so the anchor was available at the call site. The window is session_timestamp - 1h ..= now + 1h. For a week-long session that is ~0.0066% of u64 for both byte orders together, about a 600x reduction. When there is no trustworthy anchor (session_timestamp <= 0) every inferred reading is declined and the existing fallback takes over.
The confirmed #9.#4 path is deliberately left ungated — it is an explicitly typed Timestamp read off real databases, so it needs no corroboration and must not regress for pre-1.1.18 installs.
Three tests pin this: an opaque payload that passes the absolute window but sits outside the session window is rejected, a timestamp before session start is rejected, and a missing anchor declines inference. Removing the gate fails exactly those three and no others.
…sion window The agy 1.1.18 `chatModel.#9.#10` payload is 8 bytes whose encoding was never confirmed against a real database, so every candidate reading of it is a guess that has to earn acceptance. The only gate on those guesses was an absolute "is this a believable date" window running from 2020-01-01 to five years out. That is not a meaningful test for a raw integer: read as a nanosecond count the window alone covers ~2% of the u64 range, so trying both byte orders leaves an arbitrary payload — an id, a hash, a duration — a few percent chance of passing as a date. A false accept silently buckets a turn into the wrong day and feeds the server-side monotonic ratchet, which has no correction path, making it strictly worse than the known-wrong session-start dating it replaces. Require every inferred reading to land inside the containing session's own lifetime as well: at or after the session-created stamp less one hour, and at or before now plus one hour. A turn cannot predate its conversation nor happen after we read the file, and that pair of bounds is hours or days wide instead of a decade. When there is no positive anchor to corroborate against, decline inference entirely and let the caller fall back as before. The explicit `#9.#4` Timestamp is untouched: it is a confirmed representation read off real pre-1.1.18 databases, keeps its `ms > 0` filter, takes no session bound, and still outranks the inferred reading. Constraint: `#9.#10`'s encoding is inferred from a field dump, not observed Constraint: mis-dating is uncorrectable downstream; under-dating is not Rejected: tightening only the absolute window | no absolute date range is narrow enough to make a raw 8-byte integer a safe timestamp Rejected: day-wide tolerances | hands back the integer space the session window exists to remove Confidence: high Scope-risk: narrow Directive: the one-hour tolerances are load-bearing and pinned by tests; widening them re-opens the false-accept surface this closes Not-tested: a real agy 1.1.18 database — none was available, which is why the reading is inferred in the first place
Refs #1184
What broke
crates/tokscale-core/src/sessions/antigravity_cli.rsdates each generation fromchatModel.#9.#4, a{#1: seconds, #2: nanos}Timestamp. agy 1.1.18 removed that field. The reporter's decode of a live 1.1.18gen_metadatarow showschatModel.#9now carrying#2 = 18446744073709551615(u64::MAX — anint64-1, i.e. an "unset" sentinel) and a new#10holding 8 length-delimited bytes. With#4gone, every row fell through to the session-created stamp fromtrajectory_metadata_blob.#2, so on a session that has been open since Aug 22 every turn is bucketed to Aug 22 andtokscale models -c antigravity-cli --todayreports 0.What this does
#9.#4is still read first, unchanged, on its originalms > 0filter — pre-1.1.18 databases and older installs keep their exact behaviour, and a row carrying both fields still prefers the explicitly typed#4Timestamp.When
#4is absent,#9.#10is decoded, most-structured shape first: a nested{#1: seconds, #2: nanos}Timestamp; a nested message holding the epoch scalar in field 1 as a varint or afixed64; or the payload itself as 8 rawfixed64-style bytes, little-endian (protobuf's own byte order) then big-endian. Each candidate integer is unit-detected by magnitude — seconds, milliseconds, microseconds, nanoseconds — which is sound only because those four windows are disjoint over any believable date, so at most one unit can produce an in-range result.Every inferred reading is then range-checked against 2020-01-01 through five years from now. Anything outside that window is discarded and the next candidate is tried; when they all fail, the existing session-created fallback takes over. That direction is deliberate: a wrong per-turn date silently corrupts day buckets and the server-side monotonic ratchet, which is worse than the known-conservative bug this replaces, so the failure mode is always "degrade to today's behaviour", never "produce a plausible-looking wrong date".
#9.#2is never consulted as a time, andu64::MAXis rejected by name in the scalar decoder so no candidate path can promote the sentinel into a date.A raw IEEE-754
f64reading of the same 8 bytes is deliberately not attempted. It is the one candidate with a non-trivial false-positive rate against a non-timestamp payload — any double in the 2^30-ish exponent range decodes to a plausible epoch-second count — and nothing in the field dump points at it.Please verify before this is treated as closed
I have no agy CLI install and no 1.1.18
gen_metadatadatabase to decode against, so the#10layout here is inferred from the field dump in the issue, not observed. What is certain from the dump is the field number, the wire type (2, length-delimited) and the length (8 bytes); what is inferred is which of the shapes above those 8 bytes actually are. @yourlovelyscent-beep — if you can run a build of this branch against your machine and confirmtokscale hourly -c antigravity-clinow spreads turns across the days you actually used them (rather than all sitting under the session-start hour), that closes the loop. If it still buckets everything at session start, the 8 bytes are something else entirely and the raw bytes ofchatModel.#9.#10from one row would pin it down.Cache
ClientId::AntigravityClihas noparser_version()arm inmessage_cache.rsand the lane does not go throughparse_cached_lane— both call sites inlib.rsparsegen_metadatadirectly on every run. There are no cached session-start-dated messages to invalidate, so nothing is bumped here.Tests
cargo test -p tokscale-core antigravity— 45 passed, 0 failed.cargo clippy -p tokscale-core --all-targets -- -D warningsandcargo fmt --all -- --checkboth clean. Fullcargo test -p tokscale-coregreen (1850 passed).New coverage in
antigravity_cli.rs: every accepted#9.#10shape (nested Timestamp, nested varint/fixed64 scalar, raw 8 bytes in both byte orders, across all four units) dates the turn; the u64::MAX sentinel is never read as a time, whether it sits in#9.#2or arrives inside the#10payload; an opaque 8-byte payload and in-shape-but-out-of-window values (1990, ten years out) all fall back to the session stamp rather than producing a date; the explicit#9.#4Timestamp keeps priority over#9.#10; and an end-to-end database whose rows all use the 1.1.18 layout dates each row to its own turn instead of collapsing onto the session-created date.Verified by falsification, twice. Removing the
#9.#10branch fails exactly the two new-layout tests and nothing else. Separately, neutering the plausibility window so it accepts everything fails four tests including the fallback guard — so the range check is load-bearing, not decoration.Summary by cubic
Fixes per-generation timestamps for Antigravity CLI after agy 1.1.18 removed chatModel.#9.#4. Previously, every turn fell back to the session start; now we parse the new #9.#10 payload so turns are dated when they happened, while pre-1.1.17 stays unchanged.
Bug Fixes
#9.#10as a nested Timestamp, a nested scalar in field 1 (varint orfixed64), or 8 rawfixed64bytes (LE/BE).#9.#4as the first choice when present and >0.#9.#2and explicitly rejectu64::MAXso the unset sentinel never becomes a date.Written for commit 32c164c. Summary will update on new commits.