feat(diag): instrumente les blocages de délégation idea_ask_agent

Ajoute des logs diag! sans changement sémantique le long du canal de
délégation inter-agents (application/orchestrator, infrastructure
input, mailbox, mcp server & tools) pour diagnostiquer les blocages
récurrents de idea_ask_agent. Couvert par les tests existants étendus
(mcp_server, mailbox, input, tools, orchestrator_service).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 09:41:02 +02:00
parent 61c330a54e
commit e5e412bdf2
6 changed files with 426 additions and 43 deletions

View File

@ -147,7 +147,7 @@ impl BusyTracker {
/// appel, l'`enqueue` publie la `DelegationReady` immédiatement (chemin chaud,
/// zéro régression).
fn mark_starting(&self, agent: AgentId) {
eprintln!("[input-mediator] cold-start gate armed agent={agent}");
application::diag!("[input-mediator] mark_starting cold-start gate armed agent={agent}");
self.lock_starting().insert(agent);
}
@ -235,6 +235,10 @@ impl BusyTracker {
.collect()
};
for agent in newly_stalled {
// Transition de vivacité Alive→Stalled : le tour de l'agent n'a pas battu
// depuis plus que son seuil. Beacon événementiel (une fois par transition,
// jamais en boucle) — utile pour repérer une cible silencieuse pendant un ask.
application::diag!("[input-mediator] liveness Alive->Stalled agent={agent}");
self.publish_liveness(agent, AgentLiveness::Stalled);
}
}
@ -264,10 +268,10 @@ impl BusyTracker {
let mut busy = self.lock();
let entry = busy.entry(agent).or_insert(AgentBusyState::Idle);
if entry.is_busy() {
eprintln!("[input-mediator] enqueue queued behind busy agent={agent}");
application::diag!("[input-mediator] start_turn queued behind busy agent={agent}");
false
} else {
eprintln!("[input-mediator] turn started agent={agent} state={state:?}");
application::diag!("[input-mediator] start_turn started agent={agent} state={state:?}");
*entry = state;
true
}
@ -285,15 +289,17 @@ impl BusyTracker {
Some(sink) => match sink(agent, d) {
Some(d) => d,
None => {
eprintln!("[input-mediator] delegation handled by headless sink agent={agent}");
application::diag!(
"[input-mediator] delegationReady handled by headless sink agent={agent}"
);
return;
}
},
None => d,
};
if let Some(events) = &self.events {
eprintln!(
"[input-mediator] publishing DelegationReady agent={agent} ticket={} text_bytes={} submit_sequence={} submit_delay_ms={:?}",
application::diag!(
"[input-mediator] delegationReady -> write-portal agent={agent} ticket={} text_bytes={} submit_sequence={} submit_delay_ms={:?}",
d.ticket,
d.text.as_bytes().len(),
describe_submit_sequence(d.submit_sequence.as_deref()),
@ -323,13 +329,24 @@ impl BusyTracker {
let deferred = self.lock_deferred().remove(&agent);
match deferred {
Some(d) => {
eprintln!(
"[input-mediator] prompt-ready released deferred delegation agent={agent} ticket={}",
application::diag!(
"[input-mediator] prompt_ready released deferred delegation agent={agent} ticket={}",
d.ticket
);
self.publish_deferred(agent, d);
}
None => self.mark_idle(agent),
None => {
// Pas de tour différé : le prompt-ready ne fait que **libérer** le busy
// state / la FIFO (mark_idle). Il ne porte AUCUNE réponse pour réveiller
// un appelant en attente — d'où `no_reply_payload`. C'est exactement le
// scénario où une cible revient prompt-ready sans `idea_reply` : l'ask
// ne se résout pas par ce chemin, seulement par l'`idea_reply` ou le
// timeout côté appelant.
application::diag!(
"[input-mediator] prompt_ready agent={agent} no_reply_payload -> mark_idle"
);
self.mark_idle(agent);
}
}
}
@ -341,8 +358,8 @@ impl BusyTracker {
fn release_cold_start(&self, agent: AgentId) {
self.lock_starting().remove(&agent);
if let Some(d) = self.lock_deferred().remove(&agent) {
eprintln!(
"[input-mediator] mcp-ready released deferred delegation agent={agent} ticket={}",
application::diag!(
"[input-mediator] release_cold_start (mcp-ready) released deferred delegation agent={agent} ticket={}",
d.ticket
);
self.publish_deferred(agent, d);
@ -359,7 +376,7 @@ impl BusyTracker {
.is_some_and(|s| s.is_busy())
};
if was_busy {
eprintln!("[input-mediator] turn marked idle agent={agent}");
application::diag!("[input-mediator] mark_idle turn ended agent={agent}");
if let Some(events) = &self.events {
events.publish(DomainEvent::AgentBusyChanged {
agent_id: agent,
@ -837,6 +854,9 @@ impl InputMediator for MediatedInbox {
submit_delay_ms: submit.delay_ms,
};
if self.tracker.lock_starting().contains(&agent) {
application::diag!(
"[input-mediator] enqueue deferred (cold-start gate) agent={agent} ticket={ticket_id}"
);
self.tracker.lock_deferred().insert(agent, deferred);
} else {
self.tracker.publish_deferred(agent, deferred);
@ -918,6 +938,7 @@ impl InputMediator for MediatedInbox {
// CLI agents honour. A missing handle/port is a no-op (the agent simply has no
// live stream to interrupt). The busy state is left untouched: it returns to
// Idle through `mark_idle` (prompt-ready / explicit signal), not here.
application::diag!("[input-mediator] preempt agent={agent} (interrupt byte, no ticket)");
if let Some(pty) = &self.pty {
if let Some(handle) = self.handles().get(&agent).cloned() {
let _ = pty.write(&handle, b"\x1b");