Skip to content

fix(copilot): classify CLI sessions by source provenance, not producer - #945

Merged
ozymandiashh merged 3 commits into
getagentseal:mainfrom
kelchm:fix/944-copilot-cli-transcript-misclassification
Aug 9, 2026
Merged

fix(copilot): classify CLI sessions by source provenance, not producer#945
ozymandiashh merged 3 commits into
getagentseal:mainfrom
kelchm:fix/944-copilot-cli-transcript-misclassification

Conversation

@kelchm

@kelchm kelchm commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #944. Helps #943 (the local copilot cost estimate that credit tracking would reconcile against is now accurate; the credit feature itself is not implemented here).

Problem

The Copilot CLI writes the same session.start producer (copilot-agent) that VS Code transcripts carry, so the parser's content-based detection classified every CLI session as a transcript and skipped its session.shutdown rollup — the only place CLI sessions record input, cache-read and cache-write tokens. Cache hit rate read 0.0% and cost was dramatically underreported.

Two adjacent consequences confirmed on real data:

  • The GitHub Copilot desktop app writes the same ~/.copilot/session-state/ store (client_name: github/autopilot), so its usage was equally broken — worse, app sessions using models whose toolCallIds match no known prefix (e.g. kimi) were dropped entirely by transcript model inference.
  • Copilot CLI subagent runs lost their agent attribution: newer CLIs write subagent.started/subagent.completed, not the subagent.selected event the parser handled.

Why provenance, not field sniffing

The issue proposes checking producer && !copilotVersion. Real VS Code transcripts carry copilotVersion too — verified on three real transcripts (extension versions 0.46.2 and 0.60.0), so that check would misclassify actual transcripts as CLI sessions. CLI session-state files and VS Code transcripts live in disjoint locations, so discovery already knows the answer; the parser now uses it.

Changes

  • src/providers/copilot.ts
    • VS Code transcript discovery tags sources sourceType: 'transcript'; CLI session-state keeps 'jsonl'. createJsonlParser takes isTranscript as an explicit parameter; the producer sniff is removed.
    • Transcript model inference seeds the model instead of gating the whole file, so transcripts with explicit model info but no tool calls still parse.
    • Transcript sessionId derives from the file basename (transcripts/<id>.jsonl), so transcripts no longer collapse into one shared "transcripts" session/dedup namespace.
    • Resumed sessions: each --resume leg appends a cumulative shutdown rollup. Supplementary calls are now emitted as per-leg deltas keyed by occurrence (copilot:<sid>:shutdown:<model>:<n>) — append-only under the durable union-by-key cache merge, each leg on its own timestamp. A cumulative total that goes backwards is treated as a counter reset (fresh delta baseline).
    • Subagents: subagent.started/completed are handled alongside the sticky subagent.selected; completion removes only its own run (matched by toolCallId), so nested runs fall back to the outer label instead of wiping attribution. Delegated usage lands in the parent rollup as its own model entry and is billed at that model's rates.
  • src/session-cache.ts — copilot parse-version bump (…-source-provenance-v1) so cached misparsed sessions re-parse once.
  • src/daily-cache.tsDAILY_CACHE_VERSION/MIN_SUPPORTED_VERSION 16 → 17 so finalized daily rollups re-derive under the fix (v14 carry-forward semantics preserve sourceless days).

Validation

  • Reproduced with a live Copilot CLI 1.0.78 session: raw shutdown carried 4 input + 24,678 cache-read + 24,791 cache-write tokens; codeburn reported 100 output tokens, $0 input/cache, 0.0% cache hit.
  • A/B against main on identical copies of real local data from all three surfaces (CLI incl. a 3-leg resumed session, GitHub Copilot app, VS Code chatSessions + transcripts), fresh caches both sides: the totals delta reconciles exactly to the three CLI-store sessions' shutdown rollups (input +26,657, cache-read +282,254, cache-write +49,995), VS Code-derived numbers are byte-identical, and on main the GitHub app session was missing entirely.
  • Delegating session: per-model accounting (sonnet parent + haiku subagent) reconciles exactly against the CLI's own tokenDetails; all subagent turns attributed, parent turns clean.
  • Tests are built from redacted real CLI 1.0.78 events: the misclassification regression, a 3-leg resumed session, a counter-reset fixture, nested + sticky subagent cases, real-layout transcript paths, discovery→parse wire-through, and a parser.ts integration test proving the durable merge appends only the new leg when a session file grows between parses.
  • Two independent adversarial review rounds (findings all addressed or consciously documented as pre-existing).

Copilot AI lite review requested due to automatic review settings August 7, 2026 19:11

Copilot AI 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.

Pull request overview

This PR fixes Copilot session parsing by classifying JSONL sources based on discovery provenance (CLI session-state vs VS Code transcript location) rather than sniffing session.start.producer, restoring correct session.shutdown rollup handling for Copilot CLI and GitHub Copilot desktop app sessions. It also improves attribution for newer CLI subagent lifecycle events and makes resumed-session shutdown accounting append-only via per-leg deltas.

Changes:

  • Tag discovered Copilot sources as sourceType: 'jsonl' | 'transcript' and pass an explicit isTranscript flag into the JSONL parser; derive transcript session IDs from filename; seed (not gate) transcript model inference.
  • Emit per-leg shutdown deltas (keyed by occurrence) for resumed CLI sessions with cumulative shutdown rollups; handle counter resets; support subagent.started/subagent.completed with nested attribution.
  • Bump Copilot parse cache version and daily cache version to force one-time re-derivation; expand/adjust tests and update changelog.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/providers/copilot.ts Provenance-based transcript vs CLI parsing; per-leg shutdown deltas; subagent lifecycle handling; transcript sessionId derivation.
src/session-cache.ts Copilot parse-version bump to invalidate misparsed cached sessions.
src/daily-cache.ts Daily cache version bump to re-derive finalized days under the new Copilot parsing behavior.
tests/providers/copilot.test.ts Adds regression + subagent + transcript-path + resumed-session delta coverage.
tests/parser.test.ts Integration test for durable merge behavior when a resumed CLI session file grows between parses.
CHANGELOG.md Documents the fixed Copilot CLI token/cache accounting and subagent attribution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/providers/copilot.ts Outdated
@kelchm

kelchm commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Attaching some real session files I captured while working on this — figured they'd be useful since #684's notes mention it was written without a complete real event file. All four CLI-store sessions were created just for this (throwaway marker prompts, nothing sensitive, no redaction needed), so they're byte-faithful examples of each format. README inside has expected parse numbers and a one-command repro.

codeburn-944-fixtures.tar.gz

What's Included

  • 60b6cceb-simple — plain CLI 1.0.78 -p run, the basic bug: all input/cache lives in the shutdown rollup (4 / 24,678 / 24,791)
  • bbb82018-resumed-3-legs — two --resume legs, so three shutdown events with cumulative modelMetrics — the per-leg delta case
  • e9158d09-subagent-delegationsubagent.started/completed events, with the Explore subagent's usage showing up in the parent rollup under its own model
  • c96e98b0-github-app — fun one: this was written by the GitHub Copilot desktop app, same store. It used kimi-k3, whose toolCallIds match no known inference prefix, so pre-fix this session vanished from codeburn entirely
  • vscode-workspaceStorage/ — one real VS Code session in both formats the extension writes (transcript + chatSessions journal).

Pre-fix these parse to 0 input / 0 cache / 0.0% hit rate with the app session missing; with this PR they match the README tables exactly.

kelchm added a commit to kelchm/codeburn that referenced this pull request Aug 7, 2026
Review feedback on getagentseal#945: the fallback relied on splice's negative-index
clamping to no-op on an empty stack. An explicit branch with pop() reads
as intended without knowing that semantics detail. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Blocking changes required on this exact head:

  1. Both commits contain the prohibited Co-Authored-By: Claude Fable 5 trailer. CONTRIBUTING.md explicitly bans Claude/Anthropic co-author trailers, and the repository workflow scans every PR commit. Please rewrite both commits to remove the trailers and rerun current CI.

  2. In src/providers/copilot.ts:799-812, a completion with a non-empty but unmatched toolCallId falls through to activeSubagents.pop(), removing an unrelated active run. Reproduction: start call-A/explore, complete call-unknown, then emit an assistant message. Expected attribution remains explore; actual attribution is missing. Only a genuinely ID-less legacy completion may use stack fallback, if that behavior is intended. A non-empty unmatched ID should no-op, with a regression test.

The core #944 provenance/delta/cache work otherwise looked sound. Targeted tests (97/97), typecheck, CLI build, and diff check passed locally, but this head has no remote check runs.

kelchm added 3 commits August 8, 2026 21:14
The Copilot CLI writes the same session.start producer ('copilot-agent')
that VS Code transcripts carry, so the parser's content-based detection
classified every CLI session as a transcript and skipped its
session.shutdown rollup — the only place the CLI records input,
cache-read and cache-write tokens. Cache hit rate read 0.0% and cost was
dramatically underreported. The GitHub Copilot desktop app writes the
same session store, so its usage is equally affected — and sessions
whose toolCallIds match no known prefix (e.g. kimi models) were dropped
entirely by transcript model inference.

Whether a JSONL file is a transcript is now decided at discovery time
from where the file was found (~/.copilot/session-state vs
workspaceStorage GitHub.copilot-chat/transcripts): transcript sources
carry sourceType 'transcript' and createJsonlParser takes isTranscript
as an explicit parameter, with the producer sniff removed. Transcript
model inference seeds the model instead of gating the file, so explicit
model info still parses. Field sniffing was not an option: real VS Code
transcripts carry copilotVersion too (0.46.2/0.60.0 observed), so the
issue's proposed producer+copilotVersion check misfires on them.

Resumed sessions append one CUMULATIVE shutdown rollup per leg; the
supplementary calls are emitted as per-leg deltas keyed by occurrence so
a growing file stays append-only under the durable union-by-key cache
merge, each leg on its own timestamp. Subagent runs (which land in the
parent rollup as their own model entry) are billed per model, and
delegated turns are attributed via the subagent.started/completed events
newer CLIs write in place of subagent.selected.

The copilot session cache takes a parse-version bump and the daily cache
bumps from v16 to v17 so days finalized under the misclassification
re-derive once; sourceless days carry forward as-is per v14 semantics.

Verified by A/B against main on real sessions from all three surfaces
(CLI, GitHub Copilot app, VS Code): the totals delta reconciles exactly
to the three sessions' shutdown rollups, and VS Code chatSessions
output is byte-identical. Regression tests are built from redacted real
CLI 1.0.78 sessions, including a 3-leg resumed session and a delegating
session.

Fixes getagentseal#944
Review feedback on getagentseal#945: the fallback relied on splice's negative-index
clamping to no-op on an empty stack. An explicit branch with pop() reads
as intended without knowing that semantics detail. No behavior change.
…letion

Review feedback on getagentseal#945: a subagent.completed whose non-empty toolCallId
matches no active run referred to a run the parser never saw start, but
fell through to popping the most recent active run — stripping an
unrelated subagent's attribution from its remaining turns. A non-empty
unmatched id is now a no-op; only a genuinely ID-less completion (the
transitional-CLI shape, keyed like subagent.selected) ends the most
recent run.
@kelchm
kelchm force-pushed the fix/944-copilot-cli-transcript-misclassification branch from 69b8aca to e61a7d3 Compare August 9, 2026 01:14
@kelchm

kelchm commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Both blockers addressed on the new head (e61a7d3):

  1. Trailers — history rewritten; both original commits are content-identical with the Co-Authored-By lines stripped (my miss on CONTRIBUTING.md, sorry for the noise).
  2. Unmatched toolCallId completion — you're right, and your repro is now a regression test verbatim: started call-A/explorecompleted call-unknown → the following message keeps explore. A non-empty unmatched id no-ops; only an ID-less completion pops the most recent run. On the "if that behavior is intended" question — I kept the ID-less fallback deliberately: subagent.selected shows older CLIs keyed nothing, and an ID-less completed most plausibly ends the most recently started run. Happy to drop it to a pure no-op if you'd rather not carry the inference.

Targeted tests 98/98 and typecheck green locally on this head.

@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.

Re-reviewed exact head e61a7d3 after the history rewrite. The prohibited trailers are gone, the unmatched non-empty toolCallId now no-ops without evicting an unrelated active subagent, and the exact regression is covered. Targeted tests (189), typecheck, CLI build, and diff check pass locally. No remaining blocker found.

@ozymandiashh
ozymandiashh merged commit 3536a1d into getagentseal:main Aug 9, 2026
5 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.

copilot provider: cache hit rate is always 0.0% when using GitHub Copilot CLI (sessions misidentified as VS Code transcripts)

3 participants