Repository navigation
Session list change counts show the whole branch divergence, not the session's changes #330941
Agent suggestions
Public previewDescription
Update: struck-through passages below are corrections to my original report — two measurements came from the wrong repository, and the suggested fix turned out to be wrong. Details, re-measured figures and further evidence in this comment. The report itself stands.
Summary
The change counts shown against a session in the sessions list are the whole branch's divergence from its base branch, not that session's changes. On a long-lived shared branch the number grows without bound, is identical for every session in the repository, and wildly overstates what an agent did.
Repro
- Work on a branch that is well ahead of the default branch (a long-lived
dev, say). - Start a session there and let it edit one or two files.
- Look at the session in the sessions list.
Observed: a count in the thousands, close to identical for every session in that repository, growing over time.
Expected: what this session changed.
Cause
The list chip renders changes.insertions/deletions (agentSessionsViewer.ts:542), sourced from the Branch changeset summary (agentHostChangesetService.ts:1356), which is git diff against merge-base HEAD origin/<base> (agentHostGitService.ts:474).
That is correct when a session owns its branch, which is the cloud/PR shape. For a local session started on an existing shared branch, the merge-base is far behind and the diff is dominated by work no session performed.
Measured on a real repository: a session that touched two files sat on a dev branch whose git diff --shortstat $(git merge-base HEAD origin/main) was 104 files changed, 11448 insertions(+), 1285 deletions(-). A second repository whose branch was level with its remote correctly showed only its uncommitted 209 insertions, 6 deletions, which is the same code path behaving differently purely because of branch position.
Corrected. Both figures above were misreported: the first came from a different repository than the screenshot, and the second was not a second repository at all. Re-measured on the repository this report came from, on its dev branch:
$ git diff --shortstat $(git merge-base HEAD origin/main)
166 files changed, 11329 insertions(+), 634 deletions(-)
$ git ls-files --others --exclude-standard | wc -l # 17 untracked files,
17 # totalling 4741 lines
$ git diff --shortstat origin/dev # same repo, own remote
9 files changed, 209 insertions(+), 6 deletions(-) # HEAD level with origin/dev
166 + 17 = 183 files and 11329 + 4741 = 16070 insertions is exactly what the chip displays (183 files, +16070 -634); the branch changeset includes untracked files, --shortstat alone does not. The 209 insertions, 6 deletions is the same repository measured against its own remote branch rather than the default branch — which still makes the original point, and makes it on one repository: the identical code path reports 11329 or 209 purely according to which base it resolves.
Suggested fix
The host already captures a per-session baseline checkpoint per repository (IAgentHostCheckpointService.captureBaselineCheckpoint, retrievable via getBaselineCheckpoint). Anchoring the branch diff there makes the number the session's own footprint, and leaves a branch-owning session unchanged because its baseline is the branch point.
PR follows. Happy to close it if the current definition is deliberate, in which case the counts may still be worth relabelling, since as displayed they read as the session's work.
Withdrawn. That was tried in #330942 and closed: a baseline checkpoint is a parentless snapshot commit rather than an ancestor of HEAD, so it cannot be used as the base of a merge-base working-tree diff — doing so compared unrelated trees and produced a worse number than the one it replaced.
No new computation is needed in any case. The session changeset already diffs the session's baseline checkpoint against its latest turn checkpoint via the two-ref primitive (agentHostChangesetService.ts:1541-1566), which is the session's own footprint. The open question is only which changeset feeds the chip — and that is a product decision, since the branch-derived chip is deliberate (#323852, pinned by a characterization test). The counts may still be worth relabelling, since as displayed they read as the session's work.
AI disclosure: this issue was written with the assistance of AI.
Public patches and patcher scripts
Public patch catalog and patcher scripts · Source patch index. The public collection includes the maintained patchers, rollback instructions, regression scripts, and historical snapshots. Build restrictions and exact installer coverage are documented there.
Activity
RyanEwen commentedon Aug 14, 2026
Correction on the suggested fix, with more evidence.
I proposed anchoring the diff on the session's baseline checkpoint and opened #330942 for it. That is wrong, and the PR is closed. A baseline checkpoint is a detached snapshot commit rather than an ancestor of
HEAD, so using it as the base of a working-tree diff compares unrelated trees. Measured on a real session it produced17 files changed, 4741 deletions(-), worse than the branch-wide number. The existing code only ever uses baselines throughcomputeFileDiffsBetweenRefs(from, to)for compare-turns, which is consistent with that.The report itself is unchanged: a session on a long-lived shared branch still reports that branch's whole divergence, and the measurements above still hold.
Two further data points from investigating:
sessionchangeset is not the missing number either. With git available it computescomputeSessionFileDiffs(dir, { sessionUri })with no base, which resolves toHEAD, so it is "uncommitted changes", not "changes this session made".refs/agents/**/checkpoints/turn/0refs while another had none, so any remedy keyed on baselines is silently inert in the second.The session's own footprint does look derivable from
getTurnCheckpointPair(session, turnId)unioned across the session's turns, which is what_computeTurnDiffsPreferCheckpointalready does for a single turn. I have not implemented that, since it is a larger change and the choice between "session footprint" and "branch divergence" for this chip is a product decision.AI disclosure: this comment and the related code were written with the assistance of AI.
RyanEwen commentedon Aug 14, 2026
Correcting two things I got wrong above, and adding evidence that I think sharpens the report. Line references are against
ffb45b73c19onmain.The
sessionchangeset already is the session's footprintI claimed it computes
computeSessionFileDiffs(dir, { sessionUri })with no base, resolves toHEAD, and is therefore "uncommitted changes". That is wrong — I was describing theuncommittedchangeset. With git available,kind === 'session'diffs the session's baseline checkpoint against its latest turn checkpoint, through the two-ref primitive (agentHostChangesetService.ts:1541-1566):So the number I said would have to be built — "the union of
getTurnCheckpointPairdiffs across the session's turns" — already exists and is already computed per session. That makes the remedy smaller than I implied: it is a question of which changeset feeds the chip, not of computing a new one.Checked by sourcing the chip from that changeset and cross-checking each row against
git diff <baseline> <latest turn checkpoint>:47 files, +1096 -183and14 files, +800 -335match exactly, and two sessions that genuinely changed nothing report exactly zero.The measurements in the issue body are from the wrong repository
Re-measured on the repository the report actually came from, on
dev:166 + 17 = 183files and11329 + 4741 = 16070insertions, which is the chip's183 files, +16070 -634exactly — the branch changeset includes untracked files,--shortstatalone does not. The104 files / 11448 / 1285figures in the issue body are from a different repository; please disregard them.Sessions that never ran still get a branch-derived number written for them
There is a second path I had not looked at.
computeListEntryChanges(:310-361) returns the persisted summary, and when there is none it re-derives one from the branch changeset and persists it. Two consequences beyond the count merely being too large:turns = 0andfile_edits = 0— they never ran a single turn — and every one of them carried a branch-derived count written by that path. The values were frozen snapshots of the branch divergence taken at different times:108f +8458 -109(×10),110f +9096 -109(×9),183f +16070 -634(×3),154f +10336 -308(×2),106f +8455 -106,99f +8068 -51.A session that never ran a turn displaying eight to sixteen thousand insertions seems a clearer statement of the problem than "the count is inflated". The spread also shows the number is whatever the branch happened to be at some unrelated moment.
On whether the current behaviour is deliberate
It is, and I should have established that before proposing #330942.
0ab503b3ced(#323852, "use branch changes diff summary for sessions list and session title") flipped the chip from session-derived to branch-derived, including insidecomputeListEntryChangesitself (buildSessionChangesetUri→buildBranchChangesetUri,META_CHANGESET_SESSION→META_CHANGESET_BRANCH,restored.session→restored.branch), andsingle-folder summary stays branch-derived (characterization: byte-for-byte unchanged)(agentHostChangesetService.test.ts:1790) pins it.The rationale is stated in the code (
:1333-1348): sourcing the summary from the branch changeset "keeps the count stable across the active <-> inactive transition", because "that branch changeset IS the whole session footprint".That last clause is the assumption, and it is exactly what does not hold here: it is true when a session owns its branch, and false for a session started in an existing checkout on a shared long-lived branch. So this is a product decision rather than an oversight — but the zero-turn rows above are hard to read as intended under any definition of the chip.
I am running a local patch that sources both paths from the session changeset, which produces the numbers above. I am not proposing it as a PR: it reverts #323852 and breaks that characterization test, and because the persisted summary carries the all-folder aggregate in multi-folder sessions, preferring the primary-only session changeset would under-count those. Both are calls for whoever owns this area.
Environment
VS Code 1.133.0 (
a5b500951314efd502d07465bd138dfbd714a960), dev container (remote agent host), Claude provider. Single worktree on a long-liveddevbranch.AI disclosure: this comment was written with the assistance of AI.
RyanEwen commentedon Aug 17, 2026
Traced this to two separate places, and found that fixing the obvious one alone changes nothing. Recording it because the second half needs a decision I should not make unilaterally.
The read path derives the chip from branch divergence
computeListEntryChangesfalls back to the livechangeKind: 'branch'changeset when noMETA_CHANGES_SUMMARYis stored, and persists what it derives:Worth noting the helper it calls documents the opposite of what the call site does — "Only the
changeKind: 'session'entry feeds the summary" — so the contract and the caller already disagree.The write path does the same, deliberately
_doComputeStaticChangesetwrites the chip during the branch refresh for single-folder sessions, on this premise:That premise is the bug. A branch measures itself against its upstream, which includes every commit made before the session existed and by everyone else. A session that has never run a turn and owns no file edits still gets a count.
There is also a characterization test pinning it —
single-folder summary stays branch-derived (characterization: byte-for-byte unchanged)— so this was locked in deliberately during the multi-root work rather than drifting.Fixing only the read path is inert
I removed the branch-derived fallback and asserted a session with no summary of its own advertises no chip. The first assertion passes; the second fails:
The write path had already persisted the branch number, and the read path then serves it faithfully from
META_CHANGES_SUMMARY. So a read-path-only change is not worth shipping.The decision I am not going to make for you
Moving chip ownership to the
changeKind: 'session'changeset makes both paths session-scoped and is a small diff. But session diffs come from checkpoints:So a session with no checkpoints — checkpointing disabled, or a session that never completed a turn — would have no chip at all rather than a wrong one. I think that is the correct trade (a missing chip is honest; a branch-divergence chip is not), and it matches what the read-path change already implies. But it is a visible behaviour change for anyone running without checkpoints, and it is the kind of call that sank my earlier attempt at this issue in #330942, so I would rather ask than assume.
If that trade is acceptable I will send the full change: ownership moves to the session changeset, the branch fallback goes, the characterization test is rewritten to assert the new ownership, and
summariseDiffsdistinguishes "computed and empty" (a real zero) from "could not compute" (leave the chip unset) so the move cannot substitute a misleading0 filesfor a missing chip.Multi-folder sessions aggregate every repository's branch diff through
_updateMultiFolderChangesSummary, so they have the same defect by the same reasoning. I have left that alone pending the same decision.AI disclosure: this comment and the related code were written with the assistance of AI.
khanhkit commentedon Oct 9, 2026
I reproduced a closely related case in a remote code-server Agent Host folder session on VS Code 1.140/1.141:
444 files, roughly+547k/-1kin the observed repository);I tested a narrower UI-selection change than #331345: do not change the persisted session-list summary or branch changeset computation. When the provider has no explicit
defaultChangesetKind, select the input pill default from session isolation:folder->Session Changesworktree->Branch ChangesThat matches the ownership shape: a worktree session owns its branch, while a folder session is commonly running on an existing/shared branch whose divergence is not the session's work.
On current VS Code 1.141 (
2a59476c9bfcb90b3ddc372c36762471b7dfad1c) I added a browser regression that asserts a folder session requests the session changeset instead of the branch changeset. The completeAgentHostSessionInputPillsbrowser suite passes 22/22, and the fullbuild:vscode/core-cibuild also passes in the exact port used for coder/code-server#8047.This does not settle what the sessions-list chip should mean, and it deliberately avoids changing multi-root/persisted summary semantics. It only makes the interactive Changes pill default line up with isolation when no owner has explicitly chosen another default.