feat(orchestrator): backstop no-reply du rendez-vous inter-agents
Remédiation du wedge persistant après échec live du fix Finding A (77e62e5).
Détecte la fin de tour d'un agent sollicité qui n'a pas appelé idea_reply et
débloque l'agent demandeur au lieu de le laisser en attente indéfinie.
Ajoute le suivi de tour côté inspector Claude (claude_turn_watcher) et la
résolution des chemins de session (claude_paths), câblés dans le rendez-vous
idea_ask_agent ⇄ idea_reply.
Build workspace vert, suite complète verte, zéro warning.
Backstop NON prouvé levé en live : merge develop interdit tant que la levée
du wedge n'est pas validée en conditions réelles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -25,11 +25,13 @@ use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
|
||||
use domain::ports::{
|
||||
ConversationDetails, FileSystem, FsError, InspectError, RemotePath, SessionInspector,
|
||||
ConversationDetails, FileSystem, FsError, InspectError, SessionInspector,
|
||||
};
|
||||
use domain::profile::{AgentProfile, ContextInjection};
|
||||
use domain::profile::AgentProfile;
|
||||
use domain::project::ProjectPath;
|
||||
|
||||
use super::claude_paths::{claude_transcript_path, supports_claude};
|
||||
|
||||
/// Max byte length of the extracted `last_topic` before it is truncated.
|
||||
///
|
||||
/// Kept small: the topic only labels a resume popup, it is not the message.
|
||||
@ -67,32 +69,6 @@ impl ClaudeTranscriptInspector {
|
||||
pub fn with_base_dir(fs: Arc<dyn FileSystem>, home_dir: impl Into<String>) -> Self {
|
||||
Self::new(fs, home_dir)
|
||||
}
|
||||
|
||||
/// `<home>/.claude/projects/<encoded-cwd>/<conversation-id>.jsonl`.
|
||||
fn transcript_path(&self, conversation_id: &str, cwd: &ProjectPath) -> RemotePath {
|
||||
let base = self.home_dir.trim_end_matches(['/', '\\']);
|
||||
let encoded = encode_cwd(cwd.as_str());
|
||||
RemotePath::new(format!(
|
||||
"{base}/.claude/projects/{encoded}/{conversation_id}.jsonl"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn encode_cwd(cwd: &str) -> String {
|
||||
cwd.chars()
|
||||
.map(|c| if c == '/' || c == '\\' { '-' } else { c })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One transcript line, as far as we care about it. Everything is optional so
|
||||
@ -251,17 +227,7 @@ fn truncate_topic(s: &str) -> String {
|
||||
#[async_trait]
|
||||
impl SessionInspector for ClaudeTranscriptInspector {
|
||||
fn supports(&self, profile: &AgentProfile) -> bool {
|
||||
// Recognise Claude by its conventional context file `CLAUDE.md`,
|
||||
// mirroring the detection in `application::agent::lifecycle`.
|
||||
matches!(
|
||||
&profile.context_injection,
|
||||
ContextInjection::ConventionFile { target }
|
||||
if target
|
||||
.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.unwrap_or(target)
|
||||
.eq_ignore_ascii_case("CLAUDE.md")
|
||||
)
|
||||
supports_claude(profile)
|
||||
}
|
||||
|
||||
async fn details(
|
||||
@ -270,7 +236,7 @@ impl SessionInspector for ClaudeTranscriptInspector {
|
||||
conversation_id: &str,
|
||||
cwd: &ProjectPath,
|
||||
) -> Result<ConversationDetails, InspectError> {
|
||||
let path = self.transcript_path(conversation_id, cwd);
|
||||
let path = claude_transcript_path(&self.home_dir, conversation_id, cwd);
|
||||
match self.fs.read(&path).await {
|
||||
Ok(bytes) => Ok(parse_transcript(&bytes)),
|
||||
Err(FsError::NotFound(_)) => Err(InspectError::NotFound),
|
||||
@ -283,17 +249,6 @@ impl SessionInspector for ClaudeTranscriptInspector {
|
||||
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");
|
||||
// Windows-style separators flatten too.
|
||||
assert_eq!(encode_cwd("C:\\Users\\me"), "C:-Users-me");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_extracts_last_user_topic_and_token_sum() {
|
||||
let body = concat!(
|
||||
|
||||
Reference in New Issue
Block a user