fix(session-start): scope summary lookup to repository identity (#3160)

This commit is contained in:
Affaan Mustafa
2026-09-18 18:59:41 -04:00
parent b15f7d8171
commit f5f1f6b45b
4 changed files with 229 additions and 13 deletions
+46 -6
View File
@@ -14,6 +14,7 @@ const {
getSessionSearchDirs,
getLearnedSkillsDir,
getProjectName,
getRepoIdentity,
findFiles,
ensureDir,
readFile,
@@ -254,6 +255,7 @@ function pruneExpiredSessions(searchDirs, retentionDays) {
* Session files written by session-end.js contain header fields like:
* **Project:** my-project
* **Worktree:** /path/to/project
* **Repo:** /path/to/main-worktree/.git
*
* This function reads each session file once, caching its content, and
* returns both the selected session object and its already-read content
@@ -261,11 +263,18 @@ function pruneExpiredSessions(searchDirs, retentionDays) {
*
* Priority (highest to lowest):
* 1. Exact worktree (cwd) match — most recent
* 2. Same project name match for legacy sessions without Worktree metadata
* 3. No injection when sessions belong to a different worktree/project
* 2. Repository identity match: the session was recorded in another
* worktree or subdirectory of the same repository. Identity is the
* main worktree's common git dir (issue #3160), taken from the
* recorded **Repo:** field or resolved from the recorded **Worktree:**
* path for older session files. Unrelated repositories never match.
* 3. Same project name match for legacy sessions without Worktree/Repo
* metadata
* 4. No injection when sessions belong to a different repository
*
* Sessions are already sorted newest-first, so the first match in each
* category wins.
* category wins; the scan continues past repository and project matches so
* an exact worktree match always takes precedence.
*
* @param {Array<Object>} sessions - Deduplicated session list, sorted newest-first.
* @param {string} cwd - Current working directory (process.cwd()).
@@ -279,7 +288,17 @@ function selectMatchingSession(sessions, cwd, currentProject) {
// Normalize cwd once outside the loop to avoid repeated syscalls
const normalizedCwd = normalizePath(cwd);
const currentRepoId = getRepoIdentity(cwd);
const repoIdByWorktree = new Map();
const repoIdOfRecordedWorktree = (recordedWorktree) => {
if (!repoIdByWorktree.has(recordedWorktree)) {
repoIdByWorktree.set(recordedWorktree, getRepoIdentity(recordedWorktree));
}
return repoIdByWorktree.get(recordedWorktree);
};
let repoMatch = null;
let repoMatchContent = null;
let projectMatch = null;
let projectMatchContent = null;
let readableSessions = 0;
@@ -289,9 +308,11 @@ function selectMatchingSession(sessions, cwd, currentProject) {
if (!content) continue;
readableSessions++;
// Extract **Worktree:** field
// Extract **Worktree:** and **Repo:** fields
const worktreeMatch = content.match(/\*\*Worktree:\*\*\s*(.+)$/m);
const sessionWorktree = worktreeMatch ? worktreeMatch[1].trim() : '';
const repoFieldMatch = content.match(/\*\*Repo:\*\*\s*(.+)$/m);
const sessionRepo = repoFieldMatch ? repoFieldMatch[1].trim() : '';
// Exact worktree match — best possible, return immediately
// Normalize both paths to handle symlinks and case-insensitive filesystems
@@ -299,9 +320,24 @@ function selectMatchingSession(sessions, cwd, currentProject) {
return { session, content, matchReason: 'worktree' };
}
// Repository identity match (#3160): the summary lookup is scoped to the
// repository, not the cwd path, so a session recorded in worktree A is
// eligible in worktree B only when both resolve to the same common git
// dir. Unrelated repositories never share.
if (!repoMatch && currentRepoId && (sessionRepo || sessionWorktree)) {
const sessionRepoId = sessionRepo
? normalizePath(sessionRepo)
: repoIdOfRecordedWorktree(sessionWorktree);
if (sessionRepoId && sessionRepoId === currentRepoId) {
repoMatch = session;
repoMatchContent = content;
}
}
// Project name match is only safe for legacy session files written before
// Worktree metadata existed. A different explicit Worktree is not a match.
if (!projectMatch && currentProject && !sessionWorktree) {
// Worktree/Repo metadata existed. A different explicit Worktree or Repo
// is not a match.
if (!projectMatch && currentProject && !sessionWorktree && !sessionRepo) {
const projectFieldMatch = content.match(/\*\*Project:\*\*\s*(.+)$/m);
const sessionProject = projectFieldMatch ? projectFieldMatch[1].trim() : '';
if (sessionProject && sessionProject === currentProject) {
@@ -311,6 +347,10 @@ function selectMatchingSession(sessions, cwd, currentProject) {
}
}
if (repoMatch) {
return { session: repoMatch, content: repoMatchContent, matchReason: 'repo' };
}
if (projectMatch) {
return { session: projectMatch, content: projectMatchContent, matchReason: 'project' };
}