fix(inspector): encode_cwd réplique l'encodage exact de Claude Code (cause racine du wedge no-reply)

`encode_cwd` ne remplaçait que `/` et `\` par `-`, laissant le `.` intact. Or
Claude Code aplatit le cwd via `replace(/[^a-zA-Z0-9]/g, '-')` : tout caractère
non alphanumérique devient `-`, y compris le `.`. Pour un run dir isolé
`…/IdeA/.ideai/run/<uuid>`, le `/.` doit collapser en double dash
(`…-IdeA--ideai-run-<uuid>`). L'ancienne version calculait `…-IdeA-.ideai-run-…`,
un dossier inexistant sur disque : le turn-watcher voyait un répertoire vide
(baseline 0, conversation <none>) et ne déclenchait jamais `turn_ended`, ce qui
wedgeait le backstop no-reply du rendez-vous inter-agents.

Validé EN LIVE : demandeur libéré ~grâce 2s (au lieu du timeout 600s), preuves
dans idea.log. Test de non-régression `encode_cwd_encodes_dot_in_run_dir_to_double_dash`
+ maj test Windows (`C:\Users\me` -> `C--Users-me`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 19:50:49 +02:00
parent 744de20f4b
commit c6f0f86f0e

View File

@ -23,18 +23,20 @@ use domain::project::ProjectPath;
/// Encodes a cwd the way Claude Code names its per-project transcript folder.
///
/// **Assumption (documented, isolated, and unit-tested):** Claude flattens the
/// absolute cwd into a single directory segment by replacing each path separator
/// (`/` or `\`) — and the leading separator — with `-`. So
/// `/home/anthony/Documents/Projects/IdeA` becomes
/// `-home-anthony-Documents-Projects-IdeA`. We keep this convention in one small,
/// tested function so that if Claude's exact encoding differs in some edge case, only
/// this seam needs to change. We do not attempt to encode `.` or other characters, as
/// the common case (clean absolute project paths) is covered and over-encoding would
/// risk pointing at the wrong folder.
/// **Verified against real `~/.claude/projects/` folders:** Claude flattens the absolute
/// cwd into a single directory segment by replacing **every character that is not an ASCII
/// alphanumeric** (`[a-zA-Z0-9]`) with `-` — this is Claude Code's `replace(/[^a-zA-Z0-9]/g,
/// '-')`. Crucially that means **`.` is encoded too**, not just path separators: a run dir
/// like `/home/anthony/Documents/Projects/IdeA/.ideai/run/<uuid>` becomes
/// `-home-anthony-Documents-Projects-IdeA--ideai-run-<uuid>` (the `/.` collapses to a
/// **double** dash). The earlier version only replaced `/` and `\`, leaving the `.`, so it
/// computed `-IdeA-.ideai-run-…` — a folder that never exists on disk; the turn-watcher
/// then saw an empty dir (baseline 0, conversation `<none>`) and never fired `turn_ended`,
/// wedging the no-reply backstop. We keep this convention in one small, tested seam so a
/// future Claude encoding change touches only here.
pub(crate) fn encode_cwd(cwd: &str) -> String {
cwd.chars()
.map(|c| if c == '/' || c == '\\' { '-' } else { c })
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
}
@ -83,8 +85,26 @@ mod tests {
"-home-anthony-Documents-Projects-IdeA"
);
assert_eq!(encode_cwd("/a/b"), "-a-b");
// Windows-style separators flatten too.
assert_eq!(encode_cwd("C:\\Users\\me"), "C:-Users-me");
// Every non-alphanumeric flattens to `-`, including `:` and `\` on Windows paths.
assert_eq!(encode_cwd("C:\\Users\\me"), "C--Users-me");
}
/// Non-regression for the no-reply backstop wedge: an IdeA agent's isolated run dir
/// contains a `.` (`/.ideai/`), which Claude encodes as `-` just like `/`. The `/.`
/// must therefore collapse to a **double** dash, matching the real on-disk folder
/// `-home-anthony-Documents-Projects-IdeA--ideai-run-<uuid>`. The earlier `/`-only
/// encoding produced `-IdeA-.ideai-run-…` (nonexistent) and broke the turn watcher.
#[test]
fn encode_cwd_encodes_dot_in_run_dir_to_double_dash() {
let uuid = "73c853d1-c0fd-463b-ad17-1d24fefa371f";
let cwd = format!("/home/anthony/Documents/Projects/IdeA/.ideai/run/{uuid}");
assert_eq!(
encode_cwd(&cwd),
format!("-home-anthony-Documents-Projects-IdeA--ideai-run-{uuid}")
);
// The hyphens already present inside the UUID survive (they are non-alphanumeric,
// re-encoded to the same `-`), so the segment matches byte-for-byte.
assert!(encode_cwd(&cwd).ends_with(uuid));
}
#[test]