Skip to content

fix(session-analytics): discover Claude/Codex sessions in WSL installs too - #380

Open
evolv3ai wants to merge 3 commits into
xiufengsun:mainfrom
evolv3ai:deltabot/wsl-session-browser
Open

fix(session-analytics): discover Claude/Codex sessions in WSL installs too#380
evolv3ai wants to merge 3 commits into
xiufengsun:mainfrom
evolv3ai:deltabot/wsl-session-browser

Conversation

@evolv3ai

@evolv3ai evolv3ai commented Jul 27, 2026

Copy link
Copy Markdown

Follow-up to the last section of #374 (comment) — the sessions pipeline scanning only the native home. Separate from the attribution fix in v0.86.1; no shared code.

Why

discoverSessionFiles built its Claude and Codex roots from os.homedir() alone. sync already walks both Claude homes (src/commands/sync.js), so on a Windows host with a WSL install the same work is counted in the token totals but missing from the session browser, its project list, and the tracker sessions export. TOKENTRACKER_WSL_MODE had no effect on any of them.

That split is the confusing part in practice: the numbers look right, so nothing signals that the session-level views are showing a subset.

What

Build the roots through the existing wsl-probe helpers (shouldProbeNative / shouldProbeWsl / discoverWslHome), the same way sync.js does, so one env var gates both sides consistently. Discovery is additive; duplicates synced between environments collapse via the existing Codex session-id pass and claudeMessageDedupKey downstream, so no new dedup logic.

WSL is probed only when home === os.homedir(). discoverWslHome resolves \wsl$ independently of whatever home it's called alongside, so probing on an injected home splices the machine's live WSL transcripts into what the caller expects to be an isolated fixture. I hit this on the first attempt — two suite tests started seeing 467 real sessions where they asserted 1.

Verification

Windows 11 + WSL2 Ubuntu, against main @ 0.87.0:

TOKENTRACKER_WSL_MODE rows projects
native-only 635 7
both 1082 20

native-only is byte-identical to pre-change output — the native path is untouched.

Tests: providerRoots is made dependency-injectable (platform, homedir, discoverWslHome) and exported, so root selection is exercised without needing a WSL install on the runner — the new file passes on Linux CI. Eight cases cover mode gating, an unresolvable WSL home, non-win32, and the isolation guard. Removing the guard fails the isolation test and leaves the other seven green.

Ran session-analytics + local-api + wsl + dual-install suites in both / native-only / wsl-first against this branch and against unpatched main: failure counts identical (11 / 13 / 11), pass counts +8 (the new tests). Those pre-existing failures are unrelated and environment-coupled — several suites read the real ~/.claude, ~/.codex and \wsl$ shares rather than fixtures, so they don't reproduce on the ubuntu CI runner. Happy to file that separately if useful.

Notes

  • No user-facing strings, no Swift, no parser changes — privacy rule unaffected (metadata only; this changes which directories are listed, nothing about what's read from them).
  • Grok is left native-only: resolveGrokHome has its own override handling and I didn't want to change that behaviour blind.

Summary by CodeRabbit

  • New Features

    • Enhanced session data discovery to scan both native Windows and WSL locations (multi-root).
    • Added/extended mode controls to support native-only, WSL-only, or combined discovery.
    • Improved multi-location discovery with de-duplicated results.
  • Bug Fixes

    • Added robust fallback when WSL locations can’t be resolved, ensuring native results still load.
    • Prevented WSL probing on non-Windows platforms and ensured safe handling of mismatched home paths.
  • Tests

    • Added coverage for WSL root discovery behavior, mode gating, and Codex vs. Claude path differences.

evolv3ai and others added 2 commits July 27, 2026 12:11
…s too

discoverSessionFiles built its Claude and Codex roots from os.homedir()
only, so on a Windows host with a WSL install the session browser and its
project list showed native sessions exclusively. sync already walks both
Claude homes (src/commands/sync.js), which left WSL work counted in the
token totals but invisible in the sessions/projects view -- the same gap
also affects the `tracker sessions` CLI export, which shares this path.

Build the roots via the existing wsl-probe helpers so TOKENTRACKER_WSL_MODE
gates both sides consistently. Duplicate files synced between environments
collapse via the Codex session-id pass and claudeMessageDedupKey downstream.

WSL is probed only when `home` is the real os.homedir(). discoverWslHome
resolves \wsl$ independently of `home`, so honouring an injected home
keeps callers that pass their own (tests, custom HOME) hermetic.

Verified on Windows 11 + WSL2 Ubuntu:
  sessions --from 2026-07-01, both:        1078 rows / 19 projects
  sessions --from 2026-07-01, native-only:  633 rows /  7 projects (unchanged)
  session-analytics + local-api suites green in both/native-only/wsl-first

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ation

Make providerRoots dependency-injectable (platform, homedir,
discoverWslHome) and export it, so the root selection can be exercised
without a WSL install on the host running the suite.

The isolation case is the one worth pinning: discoverWslHome resolves
\wsl$ independently of the `home` it is called alongside, so probing it
for an injected home (buildSessionAnalytics({ home }), a custom HOME)
splices the machine's live WSL transcripts into what the caller expects
to be an isolated fixture. Removing the guard fails that test and leaves
the other seven green.

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

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e54c44ab-e57c-4350-b95c-654f1a6389ee

📥 Commits

Reviewing files that changed from the base of the PR and between ec1b75a and 09f49a8.

📒 Files selected for processing (2)
  • src/lib/session-analytics.js
  • test/session-analytics-wsl-roots.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/session-analytics.js
  • test/session-analytics-wsl-roots.test.js

📝 Walkthrough

Walkthrough

Session analytics now supports native and WSL provider roots on Windows, applies TOKENTRACKER_WSL_MODE gating, scans Claude and Codex roots in parallel, de-duplicates results, and exposes providerRoots().

Changes

WSL-aware session discovery

Layer / File(s) Summary
Provider root selection and multi-root discovery
src/lib/session-analytics.js
providerRoots() selects native and optional WSL roots using platform, environment, and injected dependencies. Session discovery scans Claude and Codex roots in parallel, de-duplicates files, and preserves existing post-discovery processing.
WSL discovery behavior validation
test/session-analytics-wsl-roots.test.js
Tests cover mode gating, unresolved WSL fallback, non-Windows behavior, fixture-home isolation, probe overrides, and independent Codex roots.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant discoverSessionFiles
  participant providerRoots
  participant wslProbe
  discoverSessionFiles->>providerRoots: resolve Claude and Codex roots
  providerRoots->>wslProbe: discover optional WSL home
  wslProbe-->>providerRoots: native and optional WSL roots
  providerRoots-->>discoverSessionFiles: provider root lists
  discoverSessionFiles->>discoverSessionFiles: scan and de-duplicate session files
Loading

Possibly related PRs

Suggested reviewers: mm7894215

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding WSL-aware Claude/Codex session discovery.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: ec1b75a437

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

Promise.all(codexRoots.map((r) => listRolloutFilesDeep(path.join(r, "archived_sessions")))),
listGrokSessionFiles(path.join(grokHome, "sessions")),
]);
const allClaude = [...new Set(claudeGroups.flat())];

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 Deduplicate copied Claude transcripts across roots

When ~/.claude is mirrored between Windows and WSL, this Set only removes identical path strings, so the native and UNC copies remain distinct. scanClaudeSession deduplicates messages only within one file, and mergeSessionFragments later sums rows with the same session hash; consequently every copied transcript doubles its tokens, cost, turns, and summary session count. Add the content-identity dedup already used by the sync pipeline before scanning these roots.

Useful? React with 👍 / 👎.

Comment thread src/lib/session-analytics.js Outdated
Comment on lines +784 to +786
if (platform === "win32" && isRealHome && wsl.shouldProbeWsl(env)) {
const wslRoot = discoverWslHome(providerDir, { env });
if (wslRoot) roots.push(wslRoot);

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 Translate WSL session cwd values to UNC paths

When a newly discovered WSL transcript records a POSIX cwd such as /home/dev/project, both session scanners preserve that value as project_ref. Native Windows cannot resolve it: the browser copies an unusable directory for the required cd, and git-outcomes.js rejects it at fs.existsSync, so WSL sessions receive no Git attribution. Re-anchor WSL cwd values with the existing mapWslCwdToUnc(cwd, transcriptPath) helper when scanning files from this root.

Useful? React with 👍 / 👎.

const { listClaudeProjectFiles, listRolloutFilesDeep, claudeMessageDedupKey } = require("./rollout");
const { parseCodexRolloutFile } = require("./codex-rollout-parser");
const { computeRowCost } = require("./pricing");
const wsl = require("./wsl-probe");

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 all release versions for this src change

This commit changes src/ but leaves package.json, both MARKETING_VERSION entries, and the Windows <Version> at 0.87.0. The npm publishing workflow only publishes when the package version is new, and the desktop release workflow expects the synchronized versions, so this fix cannot ship through the documented release flow until npm version/sync-versions updates all four locations. The repository delegates its release requirements and conventions to CLAUDE.md.

AGENTS.md reference: AGENTS.md:L3-L5

Useful? React with 👍 / 👎.

… a silent heuristic

The home-identity check that keeps injected homes hermetic is inert on the
real CLI path (buildSessionAnalytics defaults home to os.homedir(), so the
comparison is always true there) and fires only for a threaded home. That
made it a latent trap: the whole reason to thread a non-default home is to
scan a tree that is not os.homedir(), and such a caller would lose WSL
discovery silently -- no error, and no test would fail.

Keep the same default, but express it as a `probeWsl` dep so the decision is
nameable and reachable. Two tests pin the escape hatch in both directions.

Raised by an independent verification run on a second WSL host, which also
confirmed the fix resolves \wsl$\Ubuntu-24.04\... with no UNC-prefix
fallback needed.

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

Copy link
Copy Markdown
Author

Pushed 09f49a8, and adding verification from a second WSL host.

The CI caveat, stated plainly

The green checks on this PR do not validate the WSL code paths. GitHub runners have no WSL distro and no \wsl$ share, so discoverWslHome returns null there and every WSL branch is skipped. CI confirms the change is inert where WSL is absent and that the unit tests are hermetic — nothing more. I should have said that when I opened this rather than letting a green tick imply coverage it doesn't provide.

Independent verification on real hardware

Run on a second Windows 11 + WSL2 box, distro Ubuntu-24.04, separate from the machine the PR was written on:

  • providerRoots resolved to \wsl$\Ubuntu-24.04\home\<user>\.claude; every probe succeeded, no WinError 3, no \wsl.localhost fallback needed. That box is the one where the UNC-prefix fragility noted in WSL dual-install: sessions are parsed and counted, but never attributed to a project (projectKey stays null) #374 would surface, and it didn't.
  • Sessions delta: 570 → 840 rows (+270), 43 → 52 projects. Strict multiset superset, zero rows lost.
  • Origin confirmed at the filesystem rather than inferred from the metadata output: 8 of the 9 gained projects have a WSL project directory and no native one. The largest single contributor (198 rows, 73% of the delta) traces to 198 WSL Codex rollout files and 0 native.
  • A third control run ruled out elapsed time as an explanation: native-only re-run after both returned byte-identical to the first run, 0 differences.

Full suite there: 1878 pass / 62 fail on this branch vs 1865 / 67 on 5e31c6e with no PR commits — same machine, same freshly-installed deps. Diffing the failure sets rather than the counts: 0 failures unique to this branch, 5 unique to base (timing-sensitive, flaky), 62 shared and pre-existing. Both failures whose names touch this area — readClaudeScienceFrames snapshots a WAL DB reached over a WSL UNC path and Vite dev server handles current repo session analytics — fail identically on base and branch.

Caveat worth keeping: one paired run can't fully exclude a regression that flaked green. The change is one function with dedicated tests, so I'd call that low but not zero.

What 09f49a8 changes

The verification flagged a latent trap I'd left in. The home-identity check that keeps injected homes hermetic is inert on the real CLI pathbuildSessionAnalytics defaults home to os.homedir(), so the comparison is always true there — and fires only for a threaded home. But threading a non-default home is precisely how you'd scan a tree that isn't os.homedir(), and such a caller would have lost WSL discovery silently, with no error and no failing test.

Same default, now expressed as a named probeWsl dep so the decision is reachable and documented, with two tests pinning the escape hatch in both directions (10 tests total). Red-green re-checked: forcing the default to always-probe fails the isolation test alone.

Happy to squash if you'd prefer a single commit.

@xiufengsun xiufengsun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the detailed real-hardware verification. I re-reviewed the current head 09f49a8b33747765eead8d113fbcbf7cff1c4ad5 against main, and there are two correctness issues that need to be addressed before merge.

  1. Mirrored Claude sessions are double-counted across native and WSL roots.

    new Set(claudeGroups.flat()) only deduplicates identical path strings, so the native path and its WSL/UNC copy both survive. claudeMessageDedupKey cannot collapse them here: scanClaudeSession() deliberately scopes seenMessages per file. Downstream, summarizeSessions() counts both rows, while mergeSessionFragments() merges the browser row by adding their token/cost totals.

    I reproduced this with byte-identical native and WSL copies of one Claude session containing 12 tokens:

    • expected: summary 1 session / 12 tokens, browser 1 session / 12 tokens
    • actual: summary 2 sessions / 24 tokens, browser 1 session / 24 tokens

    Please add deterministic cross-root duplicate ownership for identical mirrored copies while preserving legitimate divergent resume/subagent fragments that share a session ID. The regression coverage should include both an identical-copy case (count once) and a divergent-copy case (retain the non-overlapping fragments). A simple session-ID-only drop would lose legitimate fragments.

  2. The generic root selector turns wsl-first and native-first into both, including for Codex.

    shouldProbeNative() and shouldProbeWsl() both return true for the two *-first modes, so providerRoots() returns both roots whenever both exist. That contradicts the documented mode contract: wsl-first selects WSL with native fallback, native-first selects native with WSL fallback, and only both aggregates both.

    Claude sync intentionally unions allowed installs, but Codex sync still uses resolveInstallPaths() and selects one install in the *-first modes. The current PR therefore makes Codex session analytics scan native + WSL while Codex token totals scan only the selected install. Please split the provider policies (Claude may keep its intentional union; Codex should follow the existing resolver semantics) and add a Codex mode matrix covering wsl-first, native-first, both, wsl-only, and native-only.

The PR-specific and surrounding tests are otherwise healthy. I also ran the full local suite; its two failures reproduce unchanged on the base commit and are unrelated. The green CI does not catch the two issues above because the new tests exercise root selection only and currently assert the incorrect default union, without testing cross-root mirrored Claude content.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants