Bug Description
importLocalHistory documents that it never throws (hindsight-integrations/coding-agents/src/core/history.ts), but the Claude reader does. A single regular file sitting where a project directory is expected under ~/.claude/projects aborts the entire --import-conversations run instead of costing that one entry.
claudeHistory prefilters candidate project directories by encoded name and hands each match to jsonlFiles, which does existsSync(dir) — true for a file — and then readdirSync(dir), which throws ENOTDIR. The caller, importConversations in installer.ts, has no try/catch of its own, so the throw escapes the install.
Steps to Reproduce
# a normal Claude session for the repo, plus one stray file named like a project dir
touch ~/.claude/projects/-Users-x-dev-myrepo-sub
cd /Users/x/dev/myrepo
npx @vectorize-io/hindsight-coding-agents install claude-code --import-conversations
Equivalent as a test, against importLocalHistory directly:
writeFileSync(claudeProjectDir("/Users/x/dev/myrepo/sub", home), "not a folder");
importLocalHistory("claude-code", "/Users/x/dev/myrepo", home);
Expected Behavior
The stray entry is skipped and the real sessions still import — the same way dshHistory already handles it ("a stray file where a project directory was expected") and codexHistory wraps its walk.
Actual Behavior
Error: ENOTDIR: not a directory, scandir '…/.claude/projects/-Users-x-dev-myrepo-sub', and nothing is imported.
Measured across all five file-based readers, each seeded with one real session and one stray entry:
| reader |
no stray |
with stray |
| claude-code |
1 session |
THREW ENOTDIR |
| codex |
1 session |
1 session |
| dsh |
1 session |
1 session |
| pi |
1 session |
1 session (fixed, see below) |
| prime-agent |
1 session |
1 session |
Suggested Fix
Wrap the per-directory listing, exactly as the pi reader now does:
for (const dir of candidateDirs) {
try {
files.push(...jsonlFiles(join(root, dir)));
} catch {
// stray file where a project folder was expected — skip the entry, not the run
}
}
Notes
piHistory was modelled on claudeHistory and inherited this, which is how it was found; the pi side is already fixed and carries a regression test that fails with ENOTDIR without the guard. claude-code was deliberately left out of that change to keep it scoped, hence this issue.
Likelihood is low — the stray file has to be named like an encoded project directory — but the failure mode is the whole import dying rather than degrading.
Bug Description
importLocalHistorydocuments that it never throws (hindsight-integrations/coding-agents/src/core/history.ts), but the Claude reader does. A single regular file sitting where a project directory is expected under~/.claude/projectsaborts the entire--import-conversationsrun instead of costing that one entry.claudeHistoryprefilters candidate project directories by encoded name and hands each match tojsonlFiles, which doesexistsSync(dir)— true for a file — and thenreaddirSync(dir), which throwsENOTDIR. The caller,importConversationsininstaller.ts, has notry/catchof its own, so the throw escapes the install.Steps to Reproduce
Equivalent as a test, against
importLocalHistorydirectly:Expected Behavior
The stray entry is skipped and the real sessions still import — the same way
dshHistoryalready handles it ("a stray file where a project directory was expected") andcodexHistorywraps its walk.Actual Behavior
Error: ENOTDIR: not a directory, scandir '…/.claude/projects/-Users-x-dev-myrepo-sub', and nothing is imported.Measured across all five file-based readers, each seeded with one real session and one stray entry:
Suggested Fix
Wrap the per-directory listing, exactly as the pi reader now does:
Notes
piHistorywas modelled onclaudeHistoryand inherited this, which is how it was found; the pi side is already fixed and carries a regression test that fails withENOTDIRwithout the guard.claude-codewas deliberately left out of that change to keep it scoped, hence this issue.Likelihood is low — the stray file has to be named like an encoded project directory — but the failure mode is the whole import dying rather than degrading.