Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 92 additions & 7 deletions scripts/hooks/session-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
getSessionsDir,
getSessionSearchDirs,
getLearnedSkillsDir,
getProjectName,
findFiles,
ensureDir,
readFile,
Expand Down Expand Up @@ -53,6 +54,79 @@ function dedupeRecentSessions(searchDirs) {
.sort((left, right) => right.mtime - left.mtime || left.dirIndex - right.dirIndex);
}

/**
* Select the best matching session for the current working directory.
*
* Session files written by session-end.js contain header fields like:
* **Project:** my-project
* **Worktree:** /path/to/project
*
* This function reads each session file once, caching its content, and
* returns both the selected session object and its already-read content
* to avoid duplicate I/O in the caller.
*
* Priority (highest to lowest):
* 1. Exact worktree (cwd) match — most recent
* 2. Same project name match — most recent
* 3. Fallback to overall most recent (original behavior)
*
* Sessions are already sorted newest-first, so the first match in each
* category wins.
*
* @param {Array<Object>} sessions - Deduplicated session list, sorted newest-first.
* @param {string} cwd - Current working directory (process.cwd()).
* @param {string} currentProject - Current project name from getProjectName().
* @returns {{ session: Object, content: string, matchReason: string } | null}
* The best matching session with its cached content and match reason,
* or null if the sessions array is empty or all files are unreadable.
*/
function selectMatchingSession(sessions, cwd, currentProject) {
if (sessions.length === 0) return null;

let projectMatch = null;
let projectMatchContent = null;
let fallbackContent = null;

for (const session of sessions) {
const content = readFile(session.path);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (!content) continue;

// Cache first readable content for fallback
if (!fallbackContent) fallbackContent = content;

// Extract **Worktree:** field
const worktreeMatch = content.match(/\*\*Worktree:\*\*\s*(.+)$/m);
const sessionWorktree = worktreeMatch ? worktreeMatch[1].trim() : '';

// Exact worktree match — best possible, return immediately
if (sessionWorktree && sessionWorktree === cwd) {
return { session, content, matchReason: 'worktree' };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Project name match — keep searching for a worktree match
if (!projectMatch && currentProject) {
const projectFieldMatch = content.match(/\*\*Project:\*\*\s*(.+)$/m);
const sessionProject = projectFieldMatch ? projectFieldMatch[1].trim() : '';
if (sessionProject && sessionProject === currentProject) {
projectMatch = session;
projectMatchContent = content;
}
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

if (projectMatch) {
return { session: projectMatch, content: projectMatchContent, matchReason: 'project' };
}

// Fallback: most recent session with readable content (original behavior)
if (fallbackContent) {
return { session: sessions[0], content: fallbackContent, matchReason: 'recency-fallback' };
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

log('[SessionStart] All session files were unreadable');
return null;
}

async function main() {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const sessionsDir = getSessionsDir();
const learnedDir = getLearnedSkillsDir();
Expand All @@ -66,15 +140,26 @@ async function main() {
const recentSessions = dedupeRecentSessions(getSessionSearchDirs());

if (recentSessions.length > 0) {
const latest = recentSessions[0];
log(`[SessionStart] Found ${recentSessions.length} recent session(s)`);
log(`[SessionStart] Latest: ${latest.path}`);

// Read and inject the latest session content into Claude's context
const content = stripAnsi(readFile(latest.path));
if (content && !content.includes('[Session context goes here]')) {
// Only inject if the session has actual content (not the blank template)
additionalContextParts.push(`Previous session summary:\n${content}`);
// Prefer a session that matches the current working directory or project.
// Session files contain **Project:** and **Worktree:** header fields written
// by session-end.js, so we can match against them.
const cwd = process.cwd();
const currentProject = getProjectName() || '';

const result = selectMatchingSession(recentSessions, cwd, currentProject);

if (result) {
log(`[SessionStart] Selected: ${result.session.path} (match: ${result.matchReason})`);

// Use the already-read content from selectMatchingSession (no duplicate I/O)
const content = stripAnsi(result.content);
if (content && !content.includes('[Session context goes here]')) {
additionalContextParts.push(`Previous session summary:\n${content}`);
}
} else {
log('[SessionStart] No matching session found');
}
}

Expand Down