Skip to content

fix(session): prevent repository session path collisions - #913

Open
amh1k wants to merge 3 commits into
alibaba:mainfrom
amh1k:fix/session-repo-path-collision
Open

fix(session): prevent repository session path collisions#913
amh1k wants to merge 3 commits into
alibaba:mainfrom
amh1k:fix/session-repo-path-collision

Conversation

@amh1k

@amh1k amh1k commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes repository session path collisions by replacing the legacy separator-based directory name with a collision-resistant, versioned key derived from the canonical repository path.

Legacy sessions remain readable and are filtered by their recorded repository path. Viewer discovery, session listing, comments, details, and resume loading now preserve repository isolation.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional changes)
  • Documentation update
  • CI / Build / Tooling

How Has This Been Tested?

  • make test passes locally
  • Manual testing (collision scenarios were covered through automated integration tests)

Additional checks performed:

  • go test -race -count=1 ./...
  • make check
  • make coverage — 91.1% coverage
  • Windows test-package compilation
  • govulncheck ./...
  • git diff --check

Tests cover new-session storage, legacy compatibility, resume loading, comment loading, viewer discovery, mixed legacy/current sessions, and cross-repository isolation.

Checklist

  • My code follows the project's coding style (go fmt, go vet)
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly (not applicable)
  • I have signed the CLA

Related Issues

Fixes #894

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 6 issue(s) in this PR.

  • ✅ Successfully posted inline: 5 comment(s)
  • 📝 In summary (no line info): 1 comment(s)

bug · high

📄 internal/session/resume.go

⚠️ GitHub could not post this as an inline comment: No line information provided

Inverted logic in the error handling for os.ReadDir. The function attempts to read legacy sessions only if the current format directory does not exist. However, the current logic is:

  1. if !os.IsNotExist(err) -> Return an error (this happens when the directory exists but there is a permission issue, or any other error).
  2. if isVersionedRepoKey(...) -> Try legacy.
  3. return nil, fmt.Errorf(...) -> Return an error (this happens if the directory is missing AND the key is not versioned).

The issue is that if the directory does exist, err is nil. The code will skip the if err != nil block and proceed to read entries. If the directory does not exist, err is a "not exist" error. The code enters the block, skips the !os.IsNotExist check (because !true is false), and reaches the legacy check.

However, if the key is versioned, it calls listLegacySessions. If that function returns successfully, ListSessions continues and eventually returns those summaries. But if listLegacySessions returns an error, it returns that error. The logic seems to assume that if the directory is missing, it should fall back to legacy. But if the directory exists, it never falls back.

The logic should likely be: if the directory does not exist, return the legacy result (or empty if not legacy). If it exists, read it, and optionally append legacy results.

Currently, if isVersionedRepoKey is false and the directory is missing, it returns an error instead of an empty list, which contradicts the behavior in internal/session/list.go where missing directories are treated as empty.

Comment thread internal/session/list.go
Comment on lines +111 to +112
summaries := make([]Summary, 0)
readDir := func(dir string, legacy bool) error {

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.

performance · medium
Performance regression: The original code pre-allocated slice capacity using make([]Summary, 0, len(entries)) based on the directory entry count. The new code uses make([]Summary, 0) without capacity, which will cause multiple slice reallocations as sessions are appended. While individually small, this adds unnecessary allocations especially when listing repositories with many sessions across both directories.

Suggestion:

Suggested change
summaries := make([]Summary, 0)
readDir := func(dir string, legacy bool) error {
// Pre-allocate with estimated capacity; readDir will append from both directories
summaries := make([]Summary, 0, 32)
readDir := func(dir string, legacy bool) error {

Comment thread internal/session/list.go
Comment on lines +126 to +131
if legacy {
matches, matchErr := sessionFileBelongsToRepo(path, repoDir)
if matchErr != nil || !matches {
continue
}
}

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.

bug · medium
The sessionFileBelongsToRepo validation is only applied to legacy directory entries (legacy == true), not current directory entries. This asymmetry assumes the current directory path computation is collision-proof. While RepoSessionKey likely uses a more robust encoding (possibly with hashing based on test references to 'v2-' prefix), if the path computation ever has a bug or edge case, sessions from other repositories would be silently included from current directories without validation. Consider whether defensive validation should apply to both directories, or document why current directories are guaranteed to be isolated.

Comment thread internal/session/list.go
Comment thread internal/session/persist.go
Comment thread internal/viewer/store.go
Comment on lines +180 to +204
func readSessionCWD(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()

reader := bufio.NewReader(f)
for {
line, readErr := reader.ReadBytes('\n')
var rec struct {
Type string `json:"type"`
CWD string `json:"cwd"`
}
if json.Unmarshal(line, &rec) == nil && rec.Type == "session_start" {
return rec.CWD, nil
}
if readErr == io.EOF {
return "", nil
}
if readErr != nil {
return "", readErr
}
}
}

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.

bug · medium
In readSessionCWD, if a session file is truncated (no newline and EOF reached), the function returns "", nil instead of an error. This silent failure can cause the caller (DiscoverRepos) to group the session under a legacy key rather than skipping it. Consider returning an error for truncated sessions or at least documenting this behavior.

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.

Sessions from different repositories are mixed when encoded paths collide

1 participant