feat(orchestrator): watchdog d'inactivité réarmable + plafond du rendez-vous inter-agents
Redessine la borne de fin de tour du rendez-vous `idea_ask_agent` ⇄ `idea_reply` : au lieu d'un timeout plat (qui coupait à 600 s un seul long tour de la cible, cf. T7), la borne devient une **fenêtre d'inactivité réarmée** à chaque progrès observé de la cible, sous un **plafond absolu** (défaut 4 h, réglable via `IDEA_ASK_RENDEZVOUS_CEILING_MS`). - Sonde d'activité (`transcript_activity_token`, inspector) : jeton monotone = octets cumulés des `.jsonl` de la cible. Croît même pendant un seul long tour sans `turn_duration` ⇒ détecte « vivant et au travail » mi-tour. Best-effort, sans effet de bord ; folder absent/illisible ⇒ « pas de progrès ». - Watchdog (`run_inactivity_watchdog`, nouveau module `orchestrator/rendezvous`) : fenêtre réarmable + plafond, fallback timeout plat si aucune sonde (zéro régression). - Issue typée distincte `TargetCeilingActive` (code `RENDEZVOUS_CEILING_ACTIVE`) : une cible **active** stoppée au plafond n'est jamais confondue avec un `TargetReturnedNoReply` (silence) ; le message guide « ne pas retenter à l'aveugle ». - Câblage composition-root (`state.rs`) : sonde résolue nom→AgentId→run-dir transcript, branchée sur le service et sur l'McpServer (`AskActivityProbe`, `with_ask_ceiling`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -17,7 +17,7 @@
|
||||
//! [`ClaudeTranscriptInspector`]: super::ClaudeTranscriptInspector
|
||||
//! [`ClaudeTranscriptTurnWatcher`]: super::ClaudeTranscriptTurnWatcher
|
||||
|
||||
use domain::ports::RemotePath;
|
||||
use domain::ports::{FileSystem, RemotePath};
|
||||
use domain::profile::{AgentProfile, ContextInjection};
|
||||
use domain::project::ProjectPath;
|
||||
|
||||
@ -74,6 +74,38 @@ pub(crate) fn supports_claude(profile: &AgentProfile) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Cumulative byte size of **all** `.jsonl` transcripts in a Claude agent's per-run-dir
|
||||
/// folder (`<home>/.claude/projects/<encoded-cwd>/`) — a monotonic **activity token** for
|
||||
/// the `idea_ask_agent` rendezvous inactivity watchdog (server-side safety net).
|
||||
///
|
||||
/// Why bytes, not the `turn_duration` count: a target working in **one long turn** never
|
||||
/// emits a `turn_duration` record until that turn ends, yet its transcript file keeps
|
||||
/// **growing** as the model streams output. Summing transcript bytes therefore detects
|
||||
/// "alive and working" even mid-turn — exactly the wedge the flat timeout mis-handled.
|
||||
/// Best-effort and side-effect-free: a missing folder (cold start) or any unreadable file
|
||||
/// contributes `0`; `None` only when the folder cannot be listed at all (treated by the
|
||||
/// watchdog as "no observable progress"). Lives beside the path encoder so the transcript
|
||||
/// layout knowledge stays in one tested seam.
|
||||
pub async fn transcript_activity_token(
|
||||
fs: &dyn FileSystem,
|
||||
home_dir: &str,
|
||||
cwd: &ProjectPath,
|
||||
) -> Option<u64> {
|
||||
let dir = claude_project_dir(home_dir, cwd);
|
||||
let entries = fs.list(&dir).await.ok()?;
|
||||
let mut total: u64 = 0;
|
||||
for entry in entries {
|
||||
if entry.is_dir || !entry.name.ends_with(".jsonl") {
|
||||
continue;
|
||||
}
|
||||
let path = RemotePath::new(format!("{}/{}", dir.0, entry.name));
|
||||
if let Ok(bytes) = fs.read(&path).await {
|
||||
total = total.saturating_add(bytes.len() as u64);
|
||||
}
|
||||
}
|
||||
Some(total)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -12,4 +12,5 @@ mod claude_paths;
|
||||
mod claude_turn_watcher;
|
||||
|
||||
pub use claude::ClaudeTranscriptInspector;
|
||||
pub use claude_paths::transcript_activity_token;
|
||||
pub use claude_turn_watcher::ClaudeTranscriptTurnWatcher;
|
||||
|
||||
Reference in New Issue
Block a user