Skip to content

fix(copilot): prefer lastEventTimestamp for stampless shutdown calls - #1054

Merged
ozymandiashh merged 3 commits into
getagentseal:mainfrom
avs-io:fix/1051-copilot-rollup-dedup
Aug 21, 2026
Merged

ozymandiashh merged 3 commits into
getagentseal:mainfrom
avs-io:fix/1051-copilot-rollup-dedup

Conversation

@avs-io

@avs-io avs-io commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A Copilot CLI journal with several session.shutdown legs that carry sessionStartTime but no per-leg timestamp billed every leg onto one date. Keying those rollups by timestamp (or timestamp + journal basename) collapsed the same journal onto one row.

Closes nothing. #1051's two-journal collision is unreachable through discovery (discoverJsonlSessions only yields <sid>/events.jsonl).

Root cause

Shutdown timestamp fallback was event.timestamp || sessionStartTime || lastEventTimestamp. sessionStartTime is identical for every leg of a resumed session. Putting that stamp in the dedup key made three stampless legs share one key. A :n → journal-key migration then stripped cached occurrence keys and re-sent them under a new name (ledger double-count).

Change

  • Keep occurrence keys: copilot:<sid>:shutdown:<model>:<n>.
  • Timestamp only: event.timestamp || lastEventTimestamp || sessionStartTime.
  • Drop the journal-basename key, the :n strip, and the two-journal fixtures.

User impact

Stampless multi-leg CLI journals keep every leg's input/cache tokens and land on the last stamped event's time, not session start. Already-synced :n keys are unchanged.

Preservation

No LiteLLM rates. No store/OTel/JetBrains/transcript path changes. No CACHE_VERSION / fingerprint / ledger rewrite. Per-turn keys unchanged. #946 / #868 / #916 / #998 untouched.

Testing

  • 106 focused tests (tests/providers/copilot.test.ts + tests/parser.test.ts)
  • New test: 3 stampless legs → keys :1/:2/:3, timestamps = lastEventTimestamp not sessionStartTime, token sum 3500
  • Sabotage: sessionStartTime-first fails that timestamp assertion (2026-07-15T07:54:00.274Z vs last event)
  • tsc --noEmit clean

avs-io added 2 commits August 20, 2026 08:13
Two journals for one session both emitted :1 and the shared seenKeys
set dropped the second file's first leg. Identity is now
(session, model, shutdown timestamp). Bump the Copilot parse
fingerprint so present sources re-parse; durable orphans stay.
Extra High HOLD on #1054: timestamp-only keys still collided across
journals, and the provider-wide rollup-ts-v1 bump dropped present
OTel sources, erasing conversations already pruned from the DB.
Identity is now (session, model, timestamp, journal basename).
Legacy :n keys migrate on that JSONL file only.
@iamtoruk

Copy link
Copy Markdown
Member

Reviewed in depth against current main and against the #946 landing branch (c41b14f1), with fixtures. Requesting changes — the direction needs a rethink rather than patches.

The collision being fixed is unreachable through discovery. Only CLI jsonl sources emit :shutdown: keys (isTranscript sources continue at copilot.ts:846), discoverJsonlSessions hardcodes <stateDir>/<sid>/events.jsonl, and sessionId = basename(dirname(path)) — so two shutdown-emitting sources can never share a session id. A fixture with events-2.jsonl alongside confirms discovery returns one source. The PR's two-journal tests hand-build a source shape the product cannot produce, and journalId is the constant events.jsonl in every real key.

Meanwhile the new key regresses a reachable case. shutdownTimestamp falls back to sessionStartTime, which is identical for every leg of a session — one journal with 3 stampless session.shutdown legs goes from 3 keys / 7,000 tokens on main to 1 key / 1,500 tokens on this branch (copilot.ts:862-864, 908). That's #1051's failure mode moved from the unreachable case to a common one.

Two more blockers:

  • Migration destroys correct history: stripLegacyCopilotShutdownCalls removes cached :n calls unconditionally before knowing the re-parse can reproduce them; a main-shaped cache with :1+:2 for a stampless journal ends with one leg permanently gone (parser.ts:2831-2839).
  • Sync re-send: the ledger is a raw key set with no retraction for usage spans; every already-synced …:shutdown:<model>:<n> returns under a new name and is pushed again — permanent receiver double-count. (feat(copilot): read per-request input/cache from session-store.db #946's residual-key comment states this exact rule.)

And with #946 (either landing order): #946 reorders the stamp fallback to event.timestamp || lastEventTimestamp || sessionStartTime; under #1054 the stamp is in the key, so that reorder re-keys every stampless rollup, the #946 carry-forward union appends the re-parse, and the old key doesn't match isLegacyCopilotShutdownKey → silent duplicate.

What does hold up (fixture-verified): legacy keys with real per-leg stamps migrate cleanly and stably; orphaned legacy rollups survive untouched; reconcileCopilotCalls and the sync shape-freeze both treat the key tail as opaque and keep working.

Recommendation:

  1. Land feat(copilot): read per-request input/cache from session-store.db #946 first, unchanged.
  2. Retarget this PR to the real defect it surfaced: the timestamp fallback collapses stampless legs — take the lastEventTimestamp-first ordering for the call's timestamp only, leaving the key as :<n>. ~1 line, no migration, no ledger churn.
  3. Close copilot: rollup dedup key uses a per-file occurrence counter, so two journals for one session collide on the same leg index #1051 as unreachable with the discovery analysis (happy to transfer the write-up there).
  4. If the key shape ever genuinely must change: CACHE_VERSION bump + a ledger key-rewrite migration, not a targeted strip.

Also noted for whoever lands anything here: verify:upgrade's corpus has no copilot jsonl sources, so it never exercises this class — worth adding one.

Maintainer review on #1054: two-journal collision is unreachable
through discovery. Keying by timestamp+journal collapsed a 3-leg
stampless journal onto one row, and the :n strip re-sent ledger
keys. Keep occurrence keys. Take lastEventTimestamp before
sessionStartTime for the call timestamp only. No migration.
@avs-io avs-io changed the title fix(copilot): key shutdown rollups by timestamp, not per-file index fix(copilot): prefer lastEventTimestamp for stampless shutdown calls Aug 21, 2026

@ozymandiashh ozymandiashh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the current head and effective merge. The fallback changes only the timestamp while preserving the :n identity keys, so cache and sync continuity remain intact. The realistic stampless JSONL repro retained all calls/tokens; 106/106 targeted tests, typecheck, and all CI checks pass. No blocking issue found.

@ozymandiashh
ozymandiashh merged commit 67f085a into getagentseal:main Aug 21, 2026
15 checks passed
iamtoruk added a commit to kelchm/codeburn that referenced this pull request Aug 22, 2026
Resolves the conflicts getagentseal#946 accumulated while it was in validation. Eight
files conflicted; the session-store accounting is unchanged.

src/daily-cache.ts — version collision. This PR minted 25 when main was at
24; getagentseal#1056 (`codex-auto-review` pricing) then spent 25 on main. The bump moves
to 26/MIN 26 and daily-cache.v26.json, with main's full comment ladder kept as
the foundation and this PR's paragraph rewritten to name 26 and record the
collision. PENDING_REDERIVE_PROVIDERS and the B1 migration semantics from
b6481c1 carry over intact, retargeted at 26.

src/models.ts, src/parser.ts, src/audit-report.ts, src/models-report.ts —
getagentseal#1075/getagentseal#1078 replaced the per-site "reasoning is already inside output" tests
with billableOutputTokens() and REASONING_INCLUDED_IN_OUTPUT. This PR had
added copilot to that case at three sites independently. Union: all three
sites take main's helper call verbatim, and copilot joins claude and codex in
the set — same accounting this PR shipped, now through main's single source of
truth. It also reaches parser.ts activeGeneratedTokens (a fourth site, from
getagentseal#1079), which is the same correction: a copilot supplementary call carries
reasoning with output 0, so counting it as generated repeats the per-turn
output. The audit legend already said so on this side.

src/providers/copilot.ts — comment-only. getagentseal#1054's lastEventTimestamp-first
shutdown fallback was derived from this branch, so the code was already
identical on both sides: the `shutdownTimestamp` expression and the
`copilot:<sid>:shutdown:<model>:<n>` key are byte-for-byte main's. Both
rationales are kept (leg-collapse on date, and residual anchoring).

src/session-cache.ts — PROVIDER_PARSE_VERSIONS.copilot takes this PR's
`-session-store-v3` suffix; main's getagentseal#1051 note about why a fingerprint change
is expensive is kept above it. Codex keeps main's getagentseal#1092 suffix chain untouched.

src/main.ts — getagentseal#1067 deleted the unreachable live dailyMap fallback that this
PR had taught behavioral weight. Main's deletion wins; the now-unused
isBehavioralTurn import goes with it.

tests/parser.test.ts — import union.

Also: scripts/upgrade-path/run.mjs NEW_DAILY_CACHE -> daily-cache.v26.json,
CODEBURN_COPILOT_SESSION_STORE_DB added to the getagentseal#1064 env-isolation CLEARED
list, and the CHANGELOG entry's stale "v21" corrected to v26.

Verified: tsc clean; 3132 tests pass across 223 files; test:locks 26/26;
verify:upgrade PASSED, re-deriving daily-cache.v26.json and holding durable
copilot history across the bump. getagentseal#1054's regression ("keeps three stampless
shutdown legs as :n keys with lastEventTimestamp") passes on the merged tree.
Real-corpus A/B against origin/main over 2026-07-01..2026-08-22: codex, grok,
kimicode and opencode byte-identical in export, audit and models; claude drifts
only monotonically with run order (a live session writing transcripts, confirmed
by interleaving four runs). This machine has no copilot data, so the copilot
recovery semantics rest on the suites and the upgrade-path corpus.
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.

3 participants