fix(domain,application,infrastructure,backend,web-server,app-tauri,frontend): statut/LED projet indexé par projectId, source LiveSessions/LiveStateStore/ReconcileLiveState — la fin d'un agent dans un projet non actif repasse désormais la LED du projet au repos — domain: DomainEvent::AgentBusyChanged porte project_id ; infrastructure input: busy émis au démarrage du tour et idle au mark_idle avec le projet possesseur ; backend: DTO agentBusyChanged sérialise project_id (busy_changed_keeps_owning_project_on_the_wire) ; application workstate: ReconcileLiveState exécuté aussi avant les lectures de projection (plus seulement à l'ouverture) et liveness évaluée par (projectId, agentId) — un même agent_id vivant dans un autre projet n'épargne plus un orphelin ; web-server: ws_send_chat libère le busy avant le Final visible et la route work-state projet reste au contrat tauri ; app-tauri: commandes work-state/busy alignées sur le projectId possesseur ; frontend: useProjectActivity agrège l'activité par projectId (busy_changed filtré par projet possesseur, plus le seul projet courant), wsLiveClient/wsLiveClientReconnect propagant project_id, AnnouncementsProvider/CustomAgentChatView/WebWorkspaceLive/LayoutGrid.cellControlsLayering alignés — #373 (QA verte: cargo check --tests -p domain -p application -p infrastructure -p backend -p app-tauri -p web-server, cargo test -p application workstate::reconcile::tests, cargo test -p infrastructure input::tests::busy_event_fires_on_turn_start_and_idle_on_mark_idle, cargo test -p backend events::tests::busy_changed_keeps_owning_project_on_the_wire, cargo test -p web-server websocket_chat_send_releases_busy_before_visible_final + authorized_get_project_work_state_returns_tauri_contract, npm test -- useProjectActivity wsLiveClient wsLiveClientReconnect workstate AnnouncementsProvider CustomAgentChatView WebWorkspaceLive LayoutGrid.cellControlsLayering, npm run typecheck, cargo fmt --all -- --check, git diff --check ; fichiers .ideai/* et scripts/ hors périmètre ; aucun push)
This commit is contained in:
@ -433,6 +433,7 @@ impl BusyTracker {
|
||||
application::diag!("[input-mediator] mark_idle turn ended agent={agent}");
|
||||
if let Some(events) = &self.events {
|
||||
events.publish(DomainEvent::AgentBusyChanged {
|
||||
project_id: agent.project_id,
|
||||
agent_id: agent.agent_id,
|
||||
busy: false,
|
||||
});
|
||||
@ -877,6 +878,7 @@ impl InputMediator for MediatedInbox {
|
||||
if started_turn {
|
||||
if let Some(events) = &self.tracker.events {
|
||||
events.publish(DomainEvent::AgentBusyChanged {
|
||||
project_id: agent.project_id,
|
||||
agent_id: agent.agent_id,
|
||||
busy: true,
|
||||
});
|
||||
@ -942,6 +944,7 @@ impl InputMediator for MediatedInbox {
|
||||
self.tracker.arm_liveness(agent, stall_after_ms, now_ms);
|
||||
if let Some(events) = &self.tracker.events {
|
||||
events.publish(DomainEvent::AgentBusyChanged {
|
||||
project_id: agent.project_id,
|
||||
agent_id: agent.agent_id,
|
||||
busy: true,
|
||||
});
|
||||
@ -1218,13 +1221,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
impl RecordingBus {
|
||||
fn busy_events(&self) -> Vec<(AgentId, bool)> {
|
||||
fn busy_events(&self) -> Vec<(domain::ProjectId, AgentId, bool)> {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
DomainEvent::AgentBusyChanged { agent_id, busy } => Some((*agent_id, *busy)),
|
||||
DomainEvent::AgentBusyChanged {
|
||||
project_id,
|
||||
agent_id,
|
||||
busy,
|
||||
} => Some((*project_id, *agent_id, *busy)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
@ -1266,13 +1273,13 @@ mod tests {
|
||||
|
||||
// First enqueue starts a turn ⇒ exactly one Busy(true) event.
|
||||
inbox.enqueue(a, ticket(10, "first"));
|
||||
assert_eq!(bus.busy_events(), vec![(a.agent_id, true)]);
|
||||
assert_eq!(bus.busy_events(), vec![(a.project_id, a.agent_id, true)]);
|
||||
|
||||
// Second enqueue while Busy queues behind ⇒ NO new busy event.
|
||||
inbox.enqueue(a, ticket(11, "second"));
|
||||
assert_eq!(
|
||||
bus.busy_events(),
|
||||
vec![(a.agent_id, true)],
|
||||
vec![(a.project_id, a.agent_id, true)],
|
||||
"no re-announce while busy"
|
||||
);
|
||||
|
||||
@ -1280,14 +1287,20 @@ mod tests {
|
||||
inbox.mark_idle(a);
|
||||
assert_eq!(
|
||||
bus.busy_events(),
|
||||
vec![(a.agent_id, true), (a.agent_id, false)]
|
||||
vec![
|
||||
(a.project_id, a.agent_id, true),
|
||||
(a.project_id, a.agent_id, false),
|
||||
]
|
||||
);
|
||||
|
||||
// mark_idle on an already-idle agent ⇒ no spurious event.
|
||||
inbox.mark_idle(a);
|
||||
assert_eq!(
|
||||
bus.busy_events(),
|
||||
vec![(a.agent_id, true), (a.agent_id, false)]
|
||||
vec![
|
||||
(a.project_id, a.agent_id, true),
|
||||
(a.project_id, a.agent_id, false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@ -1300,7 +1313,7 @@ mod tests {
|
||||
inbox.enqueue(a, ticket(10, "t"));
|
||||
inbox.preempt(a);
|
||||
// Only the enqueue's Busy(true); preempt does not toggle busy state.
|
||||
assert_eq!(bus.busy_events(), vec![(a.agent_id, true)]);
|
||||
assert_eq!(bus.busy_events(), vec![(a.project_id, a.agent_id, true)]);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
@ -1360,7 +1373,7 @@ mod tests {
|
||||
inbox.enqueue_silent(a, ticket(10, "headless turn"));
|
||||
|
||||
assert!(inbox.busy_state(a).is_busy());
|
||||
assert_eq!(bus.busy_events(), vec![(a.agent_id, true)]);
|
||||
assert_eq!(bus.busy_events(), vec![(a.project_id, a.agent_id, true)]);
|
||||
assert!(
|
||||
bus.delegation_ready().is_empty(),
|
||||
"headless bookkeeping must not leak a prompt to the terminal"
|
||||
|
||||
Reference in New Issue
Block a user