feat(agent): robustesse routage ask — fix registre session + concurrence (R0+A0) — §14.3/§15
Stabilise le routage de `ask` avant le bind transport MCP (v5). Invariant « 1 agent = 1 employé » durci ; un agent traite un tour à la fois. - R0a garde LaunchAgent : lève AgentAlreadyRunning pour un lancement neuf ciblant un agent déjà vivant sur un autre node (PTY + structuré) ; rebind seulement même-node ou réattache explicite (conversation_id). Idem spawn_agent. - R0b list_live_agents agrège PTY + structuré (LiveSessions) + dédup. - R0c réconciliation des layouts.json à doublons à l'ouverture (host déterministe, idempotent) — corrige « une cellule reset au retour d'onglet ». - R0d UI : option agent désactivée si vivant ailleurs + « aller à la cellule », mapping AGENT_ALREADY_RUNNING. - A0 sérialisation FIFO des tours par agent_id dans ask_agent (verrou tokio par agent ; agents différents en parallèle ; timeout tour 300s, cap attente 600s). Cadrage : .ideai/briefs/orchestration-v5-transport-bind-cadrage.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,7 @@
|
||||
//! `TerminalSessions` registry, keyed by that same `SessionId`.
|
||||
|
||||
mod management;
|
||||
mod reconcile;
|
||||
mod snapshot;
|
||||
mod store;
|
||||
mod usecases;
|
||||
@ -30,6 +31,7 @@ pub use management::{
|
||||
DeleteLayoutOutput, LayoutInfo, ListLayouts, ListLayoutsInput, ListLayoutsOutput, RenameLayout,
|
||||
RenameLayoutInput, SetActiveLayout, SetActiveLayoutInput,
|
||||
};
|
||||
pub use reconcile::{ReconcileLayouts, ReconcileLayoutsInput, ReconcileLayoutsOutput};
|
||||
pub use snapshot::{
|
||||
SnapshotRunningAgents, SnapshotRunningAgentsInput, SnapshotRunningAgentsOutput,
|
||||
};
|
||||
|
||||
101
crates/application/src/layout/reconcile.rs
Normal file
101
crates/application/src/layout/reconcile.rs
Normal file
@ -0,0 +1,101 @@
|
||||
//! [`ReconcileLayouts`] — réconcilie, à l'**ouverture** d'un projet, les
|
||||
//! `layouts.json` qui contiennent des feuilles en **doublon** sur un même agent
|
||||
//! (lot R0c du cadrage orchestration v5, §3.4 « Trou C »).
|
||||
//!
|
||||
//! C'est la **jumelle** de [`super::snapshot::SnapshotRunningAgents`] : là où le
|
||||
//! snapshot **gèle** `agent_was_running` à la *fermeture*, cette réconciliation
|
||||
//! **dé-doublonne** à l'*ouverture*. Un `layouts.json` persisté peut déjà porter
|
||||
//! deux feuilles sur le **même** `agent` id ; à la réouverture il ne faut **pas**
|
||||
//! relancer la 2ᵉ. On garde **une** feuille « hôte » (potentiellement vivante /
|
||||
//! reprenable) et on transforme les autres en **vues mortes** : leur
|
||||
//! `agent_was_running` repasse à `false` et leur `conversation_id` est retiré.
|
||||
//! C'est précisément ce qui éliminait le symptôme « une cellule reset au retour
|
||||
//! d'onglet ».
|
||||
//!
|
||||
//! Le use case est un mince orchestrateur au-dessus de :
|
||||
//! - le store des layouts persistés ([`super::store`]),
|
||||
//! - l'opération pure du domaine
|
||||
//! [`domain::LayoutTree::reconcile_duplicate_agents`] (qui porte la **règle
|
||||
//! déterministe de choix de l'hôte** et la garantie d'idempotence).
|
||||
//!
|
||||
//! **Idempotence / no-op** : un layout sans doublon (ou déjà réconcilié) ressort
|
||||
//! **identique** de l'opération pure ; on ne persiste alors **rien** (aucune
|
||||
//! écriture). Une 2ᵉ ouverture du même projet réconcilié est donc un no-op.
|
||||
//!
|
||||
//! **Ordre d'insertion** : à appeler à l'ouverture du projet, **après** le
|
||||
//! chargement/résolution des layouts et **avant** toute reprise
|
||||
//! ([`crate::ListResumableAgents`]) — qui relit la version persistée — de sorte
|
||||
//! qu'on ne propose / ne relance qu'**une** session par agent.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{FileSystem, ProjectStore};
|
||||
use domain::ProjectId;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::store::{persist_doc, resolve_doc};
|
||||
|
||||
/// Input de [`ReconcileLayouts::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ReconcileLayoutsInput {
|
||||
/// Le projet dont les layouts doivent être dé-doublonnés.
|
||||
pub project_id: ProjectId,
|
||||
}
|
||||
|
||||
/// Output de [`ReconcileLayouts::execute`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct ReconcileLayoutsOutput {
|
||||
/// `true` si au moins un layout a été modifié (et donc le doc persisté).
|
||||
/// `false` ⇒ aucun doublon : no-op, aucune écriture (idempotence).
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
/// Réconcilie les feuilles d'agent en doublon de tous les layouts d'un projet,
|
||||
/// puis persiste le doc **uniquement** s'il a changé.
|
||||
pub struct ReconcileLayouts {
|
||||
store: Arc<dyn ProjectStore>,
|
||||
fs: Arc<dyn FileSystem>,
|
||||
}
|
||||
|
||||
impl ReconcileLayouts {
|
||||
/// Construit le use case à partir de ses ports injectés.
|
||||
#[must_use]
|
||||
pub fn new(store: Arc<dyn ProjectStore>, fs: Arc<dyn FileSystem>) -> Self {
|
||||
Self { store, fs }
|
||||
}
|
||||
|
||||
/// Exécute la réconciliation pour un projet.
|
||||
///
|
||||
/// Pour chaque layout, applique l'opération pure
|
||||
/// [`domain::LayoutTree::reconcile_duplicate_agents`]. Si **aucun** arbre n'a
|
||||
/// changé, ne persiste **rien** (no-op idempotent). Sinon, persiste tout le
|
||||
/// doc une fois.
|
||||
///
|
||||
/// # Errors
|
||||
/// - [`AppError::NotFound`] si le projet est inconnu,
|
||||
/// - [`AppError::Store`] sur défaillance du registre,
|
||||
/// - [`AppError::FileSystem`] sur défaillance de persistance.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
input: ReconcileLayoutsInput,
|
||||
) -> Result<ReconcileLayoutsOutput, AppError> {
|
||||
let project = self.store.load_project(input.project_id).await?;
|
||||
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
|
||||
|
||||
let mut changed = false;
|
||||
for named in &mut doc.layouts {
|
||||
let reconciled = named.tree.reconcile_duplicate_agents();
|
||||
if reconciled != named.tree {
|
||||
named.tree = reconciled;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
persist_doc(self.fs.as_ref(), &project, &doc).await?;
|
||||
}
|
||||
|
||||
Ok(ReconcileLayoutsOutput { changed })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user