fix: resolve path format mismatch in folder CLAUDE.md generation (#794) (#813)

The isDirectChild() function failed to match files when the API used
absolute paths (/Users/x/project/app/api) but the database stored
relative paths (app/api/router.py). This caused all folder CLAUDE.md
files to incorrectly show "No recent activity".

Changes:
- Create shared path-utils module with proper path normalization
- Implement suffix matching strategy for mixed path formats
- Update SessionSearch.ts to use shared utilities
- Update regenerate-claude-md.ts to use shared utilities (was using
  outdated broken logic)
- Prevent spurious directory creation from malformed paths
- Add comprehensive test coverage for path matching edge cases

This is the proper fix for #794, replacing PR #809 which only masked
the bug by skipping file creation when "no activity" was shown.

Co-authored-by: bigphoot <bigphoot@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alexander Knigge
2026-01-26 12:48:31 -08:00
committed by GitHub
parent 0b7ecedcd7
commit 182097ef1c
6 changed files with 257 additions and 57 deletions
+13 -34
View File
@@ -43,8 +43,10 @@ interface ObservationRow {
discovery_tokens: number | null;
}
// Import shared formatting utilities
// Import shared utilities
import { formatTime, groupByDate } from '../src/shared/timeline-formatting.js';
import { isDirectChild } from '../src/shared/path-utils.js';
import { replaceTaggedContent } from '../src/utils/claude-md-utils.js';
// Type icon map (matches ModeManager)
const TYPE_ICONS: Record<string, string> = {
@@ -135,19 +137,6 @@ function walkDirectoriesWithIgnore(dir: string, folders: Set<string>, depth: num
}
}
/**
* Check if a file is a direct child of a folder (not in a subfolder)
* @param filePath - File path like "src/services/foo.ts"
* @param folderPath - Folder path like "src/services"
* @returns true if file is directly in folder, false if in a subfolder
*/
function isDirectChild(filePath: string, folderPath: string): boolean {
if (!filePath.startsWith(folderPath + '/')) return false;
const remainder = filePath.slice(folderPath.length + 1);
// If remainder contains a slash, it's in a subfolder
return !remainder.includes('/');
}
/**
* Check if an observation has any files that are direct children of the folder
*/
@@ -288,37 +277,27 @@ function formatObservationsForClaudeMd(observations: ObservationRow[], folderPat
/**
* Write CLAUDE.md file with tagged content preservation
* Note: For the CLI regenerate tool, we DO create directories since the user
* explicitly requested regeneration. This differs from the runtime behavior
* which only writes to existing folders.
*/
function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
function writeClaudeMdToFolderForRegenerate(folderPath: string, newContent: string): void {
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
const tempFile = `${claudeMdPath}.tmp`;
// For regenerate CLI, we create the folder if needed
mkdirSync(folderPath, { recursive: true });
// Read existing content if file exists
let existingContent = '';
if (existsSync(claudeMdPath)) {
existingContent = readFileSync(claudeMdPath, 'utf-8');
}
const startTag = '<claude-mem-context>';
const endTag = '</claude-mem-context>';
let finalContent: string;
if (!existingContent) {
finalContent = `${startTag}\n${newContent}\n${endTag}`;
} else {
const startIdx = existingContent.indexOf(startTag);
const endIdx = existingContent.indexOf(endTag);
if (startIdx !== -1 && endIdx !== -1) {
finalContent = existingContent.substring(0, startIdx) +
`${startTag}\n${newContent}\n${endTag}` +
existingContent.substring(endIdx + endTag.length);
} else {
finalContent = existingContent + `\n\n${startTag}\n${newContent}\n${endTag}`;
}
}
// Use shared utility to preserve user content outside tags
const finalContent = replaceTaggedContent(existingContent, newContent);
// Atomic write: temp file + rename
writeFileSync(tempFile, finalContent);
renameSync(tempFile, claudeMdPath);
}
@@ -450,7 +429,7 @@ function regenerateFolder(
// Format using relative path for display, write to absolute path
const formatted = formatObservationsForClaudeMd(observations, relativeFolder);
writeClaudeMdToFolder(absoluteFolder, formatted);
writeClaudeMdToFolderForRegenerate(absoluteFolder, formatted);
return { success: true, observationCount: observations.length };
} catch (error) {