fix: decode dotdir working directories in history session scanning

decodeProjectKey() couldn't recover a dotdir path (e.g. ~/.codeman) from
Claude Code's encoded project-key names: the encoder maps both '/' and
'.' to '-', so the decoder's candidate joins never matched a hidden
directory on disk. It silently fell through to bare $HOME instead,
which corrupted workingDir for any resumed session under a dotdir case
(observed on ~/.codeman itself: history rows and state.json recorded
"/home/timkjr" instead of "/home/timkjr/.codeman").

Add a dot-prefixed candidate to both the backtracking decoder and its
greedy fallback so a leading empty split segment (the signature of a
literal '.' in the original path) is retried as a hidden directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
timkjr
2026-08-04 17:11:51 -05:00
co-authored by Claude Sonnet 5
parent 7e357691af
commit f470f3a4e7
2 changed files with 72 additions and 1 deletions
+38
View File
@@ -2514,6 +2514,25 @@ export function registerSessionRoutes(
}
}
}
// The encoder maps both '/' and '.' to '-', so a literal '.' in the
// original path (e.g. "/home/timkjr/.codeman") collapses into an empty
// split segment here. Retry this window as a dotdir/dotfile: ".<join>".
if (segments[idx] === '' && idx + 1 < segments.length) {
const dotMaxLook = Math.min(idx + 1 + 4, segments.length);
for (let end = dotMaxLook - 1; end >= idx + 1; end--) {
const dotCandidates =
end === idx + 1
? [segments[idx + 1]]
: [segments.slice(idx + 1, end + 1).join('-'), segments.slice(idx + 1, end + 1).join('_')];
for (const child of dotCandidates) {
const candidate = current + '/.' + child;
if (await isDir(candidate)) {
const result = await tryDecode(end + 1, candidate);
if (result) return result;
}
}
}
}
return null;
}
@@ -2547,6 +2566,25 @@ export function registerSessionRoutes(
}
if (matched) break;
}
if (!matched && segments[i] === '' && i + 1 < segments.length) {
const dotMaxLook = Math.min(i + 1 + 4, segments.length);
for (let end = i + 1; end < dotMaxLook; end++) {
const dotCandidates =
end === i + 1
? [segments[i + 1]]
: [segments.slice(i + 1, end + 1).join('_'), segments.slice(i + 1, end + 1).join('-')];
for (const child of dotCandidates) {
const candidate = current + '/.' + child;
if (await isDir(candidate)) {
current = candidate;
i = end + 1;
matched = true;
break;
}
}
if (matched) break;
}
}
if (!matched) {
current = current + '/' + segments[i];
i++;