`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>
123 lines
5.3 KiB
Rust
123 lines
5.3 KiB
Rust
//! Shared Claude Code transcript-path helpers.
|
|
//!
|
|
//! Claude Code records each conversation as a JSONL transcript under the user's
|
|
//! home directory:
|
|
//!
|
|
//! ```text
|
|
//! <home>/.claude/projects/<encoded-cwd>/<engine-session-id>.jsonl
|
|
//! ```
|
|
//!
|
|
//! where `<encoded-cwd>` is the agent's working directory with its path separators
|
|
//! flattened to `-` (see [`encode_cwd`]). Both the [`ClaudeTranscriptInspector`] (which
|
|
//! reads one known transcript) and the [`ClaudeTranscriptTurnWatcher`] (which watches the
|
|
//! whole per-agent project dir) need to derive these paths, so the convention lives in
|
|
//! **one** small, tested place: if Claude's exact encoding ever differs, only this seam
|
|
//! changes.
|
|
//!
|
|
//! [`ClaudeTranscriptInspector`]: super::ClaudeTranscriptInspector
|
|
//! [`ClaudeTranscriptTurnWatcher`]: super::ClaudeTranscriptTurnWatcher
|
|
|
|
use domain::ports::RemotePath;
|
|
use domain::profile::{AgentProfile, ContextInjection};
|
|
use domain::project::ProjectPath;
|
|
|
|
/// Encodes a cwd the way Claude Code names its per-project transcript 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.is_ascii_alphanumeric() { c } else { '-' })
|
|
.collect()
|
|
}
|
|
|
|
/// `<home>/.claude/projects/<encoded-cwd>` — the directory holding **all** of one
|
|
/// agent's conversation transcripts for `cwd` (its isolated run dir). Trailing path
|
|
/// separators on `home_dir` are trimmed first.
|
|
pub(crate) fn claude_project_dir(home_dir: &str, cwd: &ProjectPath) -> RemotePath {
|
|
let base = home_dir.trim_end_matches(['/', '\\']);
|
|
let encoded = encode_cwd(cwd.as_str());
|
|
RemotePath::new(format!("{base}/.claude/projects/{encoded}"))
|
|
}
|
|
|
|
/// `<home>/.claude/projects/<encoded-cwd>/<conversation-id>.jsonl`.
|
|
pub(crate) fn claude_transcript_path(
|
|
home_dir: &str,
|
|
conversation_id: &str,
|
|
cwd: &ProjectPath,
|
|
) -> RemotePath {
|
|
let dir = claude_project_dir(home_dir, cwd);
|
|
RemotePath::new(format!("{}/{conversation_id}.jsonl", dir.0))
|
|
}
|
|
|
|
/// Recognises a Claude profile by its conventional context file `CLAUDE.md`, mirroring
|
|
/// the detection in `application::agent::lifecycle`. Shared by the inspector and the
|
|
/// turn watcher so both agree on what they can read.
|
|
pub(crate) fn supports_claude(profile: &AgentProfile) -> bool {
|
|
matches!(
|
|
&profile.context_injection,
|
|
ContextInjection::ConventionFile { target }
|
|
if target
|
|
.rsplit(['/', '\\'])
|
|
.next()
|
|
.unwrap_or(target)
|
|
.eq_ignore_ascii_case("CLAUDE.md")
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn encode_cwd_flattens_separators_to_dash() {
|
|
assert_eq!(
|
|
encode_cwd("/home/anthony/Documents/Projects/IdeA"),
|
|
"-home-anthony-Documents-Projects-IdeA"
|
|
);
|
|
assert_eq!(encode_cwd("/a/b"), "-a-b");
|
|
// 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]
|
|
fn project_dir_and_transcript_path_compose() {
|
|
let cwd = ProjectPath::new("/a/b").expect("valid path");
|
|
assert_eq!(
|
|
claude_project_dir("/home/me/", &cwd).0,
|
|
"/home/me/.claude/projects/-a-b"
|
|
);
|
|
assert_eq!(
|
|
claude_transcript_path("/home/me", "conv-1", &cwd).0,
|
|
"/home/me/.claude/projects/-a-b/conv-1.jsonl"
|
|
);
|
|
}
|
|
}
|