Skip to content

fix(jcode): prefer journal token_usage over stale snapshot + parse tz-less timestamps - #754

Merged
junhoyeo merged 2 commits into
mainfrom
fix/jcode-718-dedup-ts
Jun 22, 2026
Merged

fix(jcode): prefer journal token_usage over stale snapshot + parse tz-less timestamps#754
junhoyeo merged 2 commits into
mainfrom
fix/jcode-718-dedup-ts

Conversation

@junhoyeo

@junhoyeo junhoyeo commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Problem

In crates/tokscale-core/src/sessions/jcode.rs, parse_jcode_file returns snapshot messages first, then journal messages. The downstream dedup (should_keep_deduped_message, lib.rs ~1102-1108) keeps the first occurrence per dedup_key (jcode:{session_id}:{message_id}).

Three confirmed bugs (PR #718):

  • (a) journal dedup (high): the journal is append-only and Jcode only rewrites the snapshot at the next checkpoint, so the journal can carry an updated token_usage for a message_id already present in the snapshot. Because the snapshot was emitted first, the corrected journal value was silently discarded and stale token counts were reported.
  • (b) tz-less timestamps: naive ISO-8601 datetimes with no Z/offset (e.g. 2026-06-16T12:00:00) failed parse_from_rfc3339 and collapsed to the file mtime, scattering messages into the wrong day/month bucket.
  • (c) duplicated helper (nit): jcode_journal_path was duplicated verbatim in jcode.rs and message_cache.rs.

Fix

  • (a) parse_jcode_file now merges in place: a journal entry repeating a snapshotted dedup_key overwrites that entry (journal wins) while preserving the snapshot's turn-start flag. Each message_id still collapses to exactly one entry — no double-counting, and the journal value survives downstream dedup.
  • (b) parse_timestamp_str (sessions/utils.rs) gains a naive-datetime fallback (T or space separator, optional fractional seconds) interpreted as UTC.
  • (c) Extracted a single pub(crate) jcode_journal_path in sessions::jcode; message_cache.rs reuses it. The no-filename fallback uses suffix-append semantics (matching the previous cache behavior).

Tests

  • journal_update_for_snapshotted_id_wins_and_collapses_to_one_entry — one entry, journal value wins.
  • journal_update_replaces_value_after_downstream_dedup — journal value survives the lib.rs first-wins dedup contract.
  • parses_timezone_less_timestamps_instead_of_falling_back_to_mtime — end-to-end vs a forced-distinct mtime.
  • parse_timestamp_str_accepts_timezone_less_datetimes_as_utc — unit coverage (T/space/fractional/offset variants).

All cargo test -p tokscale-core pass; cargo clippy -p tokscale-core --tests is clean.

Residual concerns

The cache schema version (already bumped to 22 in message_cache.rs, note #19 references #718) ensures stale jcode caches reparse. Fractional precision beyond 3 digits and leap seconds are untested.

🤖 Generated with Claude Code


Summary by cubic

Fixes stale token counts and timestamp parsing in Jcode sessions by merging journal updates into the parsed snapshot and treating timezone-less timestamps as UTC. Also targets the first-win duplicate during merge and bumps the source-message cache schema to 23 to drop stale caches.

  • Bug Fixes

    • Journal entries replace matching snapshot messages in place by dedup_key (journal wins), preserve the snapshot’s turn-start flag, and keep one entry per message_id. The merge index now targets the first occurrence so the journal correction survives downstream first-wins dedup, even if the snapshot replayed the id.
    • parse_timestamp_str accepts ISO datetimes without a Z/offset (T or space, optional fractional) and interprets them as UTC to avoid mtime fallback and mis-bucketing.
  • Refactors

    • Consolidated jcode_journal_path into a single pub(crate) helper in sessions::jcode; message_cache.rs reuses it with suffix-append fallback semantics. Also bumped CACHE_SCHEMA_VERSION to 23 to force reparse.

Written for commit 39d6007. Summary will update on new commits.

Review in cubic

…-less timestamps

parse_jcode_file emitted snapshot messages before journal messages, and the
downstream dedup (should_keep_deduped_message) keeps the FIRST occurrence per
dedup_key. So when the append-only journal carried an UPDATED token_usage for a
message_id already present in the snapshot (the snapshot is only rewritten at the
next checkpoint), the corrected value was silently discarded and stale counts
were reported. Now the parser merges in place: a journal entry repeating a
snapshotted dedup_key overwrites that entry (journal wins) while preserving the
snapshot's turn-start flag, so each message_id still collapses to exactly one
entry with no double-counting.

Also: naive ISO-8601 datetimes without a Z/offset (e.g. "2026-06-16T12:00:00",
space-separated, or with fractional seconds) failed parse_from_rfc3339 and
collapsed to the file mtime, scattering messages into the wrong day/month
bucket. parse_timestamp_str now parses these as UTC.

Nit: jcode_journal_path was duplicated verbatim in jcode.rs and message_cache.rs;
extracted a single pub(crate) source of truth in sessions::jcode (the no-filename
fallback now uses suffix-append semantics, matching the previous cache behavior).

Tests: journal-update-wins (single entry, journal value survives downstream
dedup), tz-less timestamp parsing (unit + end-to-end vs mtime fallback).

Confidence: high
Scope-risk: narrow
Not-tested: leap-second / sub-millisecond fractional precision beyond 3 digits
@vercel

vercel Bot commented Jun 22, 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 Jun 22, 2026 10:12am

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: df72d343ff

ℹ️ 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".

std::iter::once((".journal.jsonl".to_string(), jcode_journal_path(path)));
let related_paths = std::iter::once((
".journal.jsonl".to_string(),
crate::sessions::jcode::jcode_journal_path(path),

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 Bump the source cache schema for the Jcode parser change

This parser fix is hidden for users who already have a schema-22 source-message-cache.bin entry for the same Jcode snapshot/journal contents: load_or_parse_source_with_fingerprint returns cached.messages whenever the fingerprint matches, so the new journal-wins merge and timezone-less timestamp parsing never run until the files change or the cache is manually cleared. Since CACHE_SCHEMA_VERSION is still 22 in this commit, upgrading can continue reporting the stale snapshot token usage this change is meant to fix; please bump the cache schema (or otherwise invalidate Jcode entries) with the parser output change.

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.

Fixed in 39d6007. Bumped CACHE_SCHEMA_VERSION 22 -> 23 in message_cache.rs so existing schema-22 source-message-cache.bin entries are invalidated and reparsed. Without this, load_or_parse_source_with_fingerprint would keep returning the cached stale snapshot token_usage on a fingerprint match, hiding the journal-wins merge and timezone-less timestamp parsing changes. Valid catch.

Comment on lines +235 to +239
let mut index_by_dedup_key: std::collections::HashMap<String, usize> = parsed
.iter()
.enumerate()
.filter_map(|(idx, message)| message.dedup_key.clone().map(|key| (key, idx)))
.collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the first replayed Jcode ID when merging journal updates

When a snapshot already contains replayed duplicate message IDs (a supported case covered by crates/tokscale-core/tests/jcode.rs), this collect() keeps the last index for a duplicate dedup_key. A journal update for that ID then replaces the later snapshot row, but the downstream Jcode dedup is first-wins, so the stale first snapshot row survives and the corrected journal token_usage is discarded. Track the first index or collapse/replace all existing duplicates before appending journal messages.

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.

Fixed in 39d6007. The merge index now uses HashMap::entry(key).or_insert(idx) instead of collect(), so it keeps the FIRST index for a repeated dedup_key. That matches the downstream first-wins dedup: a journal update for a replayed snapshot id now overwrites the surviving (first) row rather than a later row that dedup discards, so the corrected journal token_usage is preserved. Added regression test test_jcode_journal_corrects_replayed_snapshot_duplicate (snapshot replays an id twice, journal corrects it to 999). Valid catch.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/tokscale-core/src/sessions/jcode.rs Outdated
…schema

Address automated review on #754:

- jcode parser built the dedup-merge index with `collect()`, which keeps the
  LAST index for a repeated dedup_key. Downstream dedup is first-wins, so a
  journal update overwrote a row that was later discarded, preserving stale
  snapshot token_usage for replayed message ids. Build the index with
  `entry().or_insert()` so the journal correction targets the surviving
  (first) occurrence.
- Bump CACHE_SCHEMA_VERSION 22 -> 23 so existing source-message caches reparse
  and pick up the journal-wins merge + timezone-less timestamp fix instead of
  returning cached stale messages.

Add regression test covering a snapshot with a replayed duplicate id plus a
journal correction for that id.

Constraint: downstream jcode dedup is first-wins (filter on first occurrence)
Confidence: high
Scope-risk: narrow
@junhoyeo
junhoyeo merged commit 4bc2aa0 into main Jun 22, 2026
13 of 14 checks passed
t1000040 pushed a commit to tmobi-internal/tokscale that referenced this pull request Jun 30, 2026
…-less timestamps (junhoyeo#754)

* fix(jcode): prefer journal token_usage over stale snapshot + parse tz-less timestamps

parse_jcode_file emitted snapshot messages before journal messages, and the
downstream dedup (should_keep_deduped_message) keeps the FIRST occurrence per
dedup_key. So when the append-only journal carried an UPDATED token_usage for a
message_id already present in the snapshot (the snapshot is only rewritten at the
next checkpoint), the corrected value was silently discarded and stale counts
were reported. Now the parser merges in place: a journal entry repeating a
snapshotted dedup_key overwrites that entry (journal wins) while preserving the
snapshot's turn-start flag, so each message_id still collapses to exactly one
entry with no double-counting.

Also: naive ISO-8601 datetimes without a Z/offset (e.g. "2026-06-16T12:00:00",
space-separated, or with fractional seconds) failed parse_from_rfc3339 and
collapsed to the file mtime, scattering messages into the wrong day/month
bucket. parse_timestamp_str now parses these as UTC.

Nit: jcode_journal_path was duplicated verbatim in jcode.rs and message_cache.rs;
extracted a single pub(crate) source of truth in sessions::jcode (the no-filename
fallback now uses suffix-append semantics, matching the previous cache behavior).

Tests: journal-update-wins (single entry, journal value survives downstream
dedup), tz-less timestamp parsing (unit + end-to-end vs mtime fallback).

Confidence: high
Scope-risk: narrow
Not-tested: leap-second / sub-millisecond fractional precision beyond 3 digits

* fix(jcode): target first-win duplicate on journal merge + bump cache schema

Address automated review on junhoyeo#754:

- jcode parser built the dedup-merge index with `collect()`, which keeps the
  LAST index for a repeated dedup_key. Downstream dedup is first-wins, so a
  journal update overwrote a row that was later discarded, preserving stale
  snapshot token_usage for replayed message ids. Build the index with
  `entry().or_insert()` so the journal correction targets the surviving
  (first) occurrence.
- Bump CACHE_SCHEMA_VERSION 22 -> 23 so existing source-message caches reparse
  and pick up the journal-wins merge + timezone-less timestamp fix instead of
  returning cached stale messages.

Add regression test covering a snapshot with a replayed duplicate id plus a
journal correction for that id.

Constraint: downstream jcode dedup is first-wins (filter on first occurrence)
Confidence: high
Scope-risk: narrow
@junhoyeo
junhoyeo deleted the fix/jcode-718-dedup-ts branch July 13, 2026 02:17
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