Skip to content

fix(antigravity-cli): read the per-generation timestamp from the agy 1.1.18 gen_metadata layout - #1196

Merged
junhoyeo merged 2 commits into
mainfrom
fix-antigravity-cli-agy-1118-timestamp
Aug 25, 2026
Merged

fix(antigravity-cli): read the per-generation timestamp from the agy 1.1.18 gen_metadata layout#1196
junhoyeo merged 2 commits into
mainfrom
fix-antigravity-cli-agy-1118-timestamp

Conversation

@junhoyeo

@junhoyeo junhoyeo commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Refs #1184

What broke

crates/tokscale-core/src/sessions/antigravity_cli.rs dates each generation from chatModel.#9.#4, a {#1: seconds, #2: nanos} Timestamp. agy 1.1.18 removed that field. The reporter's decode of a live 1.1.18 gen_metadata row shows chatModel.#9 now carrying #2 = 18446744073709551615 (u64::MAX — an int64 -1, i.e. an "unset" sentinel) and a new #10 holding 8 length-delimited bytes. With #4 gone, every row fell through to the session-created stamp from trajectory_metadata_blob.#2, so on a session that has been open since Aug 22 every turn is bucketed to Aug 22 and tokscale models -c antigravity-cli --today reports 0.

What this does

#9.#4 is still read first, unchanged, on its original ms > 0 filter — pre-1.1.18 databases and older installs keep their exact behaviour, and a row carrying both fields still prefers the explicitly typed #4 Timestamp.

When #4 is absent, #9.#10 is 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 a fixed64; or the payload itself as 8 raw fixed64-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.#2 is never consulted as a time, and u64::MAX is rejected by name in the scalar decoder so no candidate path can promote the sentinel into a date.

A raw IEEE-754 f64 reading 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_metadata database to decode against, so the #10 layout 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 confirm tokscale hourly -c antigravity-cli now 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 of chatModel.#9.#10 from one row would pin it down.

Cache

ClientId::AntigravityCli has no parser_version() arm in message_cache.rs and the lane does not go through parse_cached_lane — both call sites in lib.rs parse gen_metadata directly 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 warnings and cargo fmt --all -- --check both clean. Full cargo test -p tokscale-core green (1850 passed).

New coverage in antigravity_cli.rs: every accepted #9.#10 shape (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.#2 or arrives inside the #10 payload; 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.#4 Timestamp 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.#10 branch 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

  • Interpret #9.#10 as a nested Timestamp, a nested scalar in field 1 (varint or fixed64), or 8 raw fixed64 bytes (LE/BE).
  • Keep #9.#4 as the first choice when present and >0.
  • Detect seconds/millis/micros/nanos by magnitude and accept only plausible times (>= 2020-01-01 and <= now + 5y).
  • Ignore #9.#2 and explicitly reject u64::MAX so the unset sentinel never becomes a date.
  • Fall back to the session-created timestamp when nothing decodes in-range.
  • No cache invalidation or migrations; older databases behave the same.
  • Add tests covering all accepted shapes and the fallback guards.

Written for commit 32c164c. Summary will update on new commits.

Review in cubic

…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
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
tokscale Ignored Ignored Preview Aug 25, 2026 6:25pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +444 to +446
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)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@junhoyeo
junhoyeo merged commit 3cba89a into main Aug 25, 2026
21 checks passed
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.

1 participant