feat(orchestrator): durcir le rendez-vous inter-agents + backstop no-reply (Finding A)
Corrige le wedge survenant lorsqu'un agent délégué ne rappelle pas idea_reply :
le rendez-vous ask_agent ne reste plus bloqué indéfiniment.
Lot 1 — durcissement du préambule de délégation :
- input/mod.rs : delegation_preamble renforcé (obligation explicite idea_reply).
- agent/lifecycle.rs : préambule injecté conditionné à mcp_enabled.
Lot 2 — peuplement du prompt_ready_pattern :
- agent/catalogue.rs : .with_prompt_ready_pattern("? for shortcuts") sur le
profil Claude + tests de seed, pour calibrer la détection de grâce.
Lot 3 — backstop timeout serveur :
- mcp/jsonrpc.rs : code d'erreur typé RENDEZVOUS_NO_REPLY (-32001).
- mcp/server.rs : mapping timeout → erreur typée, with_ask_rendezvous_timeout
promu + resolve_ask_rendezvous_timeout (configurable).
- app-tauri/state.rs : lecture env IDEA_ASK_RENDEZVOUS_TIMEOUT_MS.
- tests/mcp_server.rs mis à jour, fix d'un test input flaky.
Propagation overlay (référence des profils) :
- agent/catalogue.rs : overlay_reference_defaults + reference_index + tests.
- store/profile.rs : ReferenceBackfillProfileStore + tests.
- app-tauri/state.rs : wrap au composition root.
- exports mod.rs / lib.rs (application + infrastructure).
Tests réels verts : application 494, app-tauri 235, infrastructure 248,
mcp_server 22/22, 0 échec. Validation e2e LIVE (rebuild AppImage) en attente
avant merge vers develop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -28,7 +28,7 @@ pub use embedder::{
|
||||
pub use live_state::FsLiveStateStore;
|
||||
pub use memory::{index_token_size, FsMemoryStore, NaiveMemoryRecall};
|
||||
pub use permission::FsPermissionStore;
|
||||
pub use profile::{FsEmbedderProfileStore, FsProfileStore};
|
||||
pub use profile::{FsEmbedderProfileStore, FsProfileStore, ReferenceBackfillProfileStore};
|
||||
pub use project::FsProjectStore;
|
||||
pub use skill::FsSkillStore;
|
||||
pub use template::FsTemplateStore;
|
||||
|
||||
@ -148,6 +148,59 @@ impl ProfileStore for FsProfileStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Merge-on-read decorator** over any [`ProfileStore`]: backfills *internal*
|
||||
/// reference defaults that a stored profile is missing, without ever persisting
|
||||
/// anything (finding A — propagate the measured `prompt_ready_pattern` to profiles
|
||||
/// saved before that field existed).
|
||||
///
|
||||
/// Only [`list`](ProfileStore::list) is transformed — each returned profile is run
|
||||
/// through [`application::overlay_reference_defaults`] (pure, allowlisted, never
|
||||
/// clobbers a user value, idempotent). `save`/`delete`/`is_configured`/
|
||||
/// `mark_configured` **delegate verbatim** so persistence and first-run detection are
|
||||
/// byte-for-byte unchanged. Wrap the concrete [`FsProfileStore`] once at the
|
||||
/// composition root, before injecting the store into use cases and the orchestrator,
|
||||
/// so every reader (including direct `.list()` calls) sees the overlaid reads.
|
||||
pub struct ReferenceBackfillProfileStore {
|
||||
inner: Arc<dyn ProfileStore>,
|
||||
}
|
||||
|
||||
impl ReferenceBackfillProfileStore {
|
||||
/// Wraps an inner [`ProfileStore`]; reads are overlaid, writes pass through.
|
||||
#[must_use]
|
||||
pub fn new(inner: Arc<dyn ProfileStore>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ProfileStore for ReferenceBackfillProfileStore {
|
||||
async fn list(&self) -> Result<Vec<AgentProfile>, StoreError> {
|
||||
Ok(self
|
||||
.inner
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|p| application::overlay_reference_defaults(&p))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn save(&self, profile: &AgentProfile) -> Result<(), StoreError> {
|
||||
self.inner.save(profile).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: ProfileId) -> Result<(), StoreError> {
|
||||
self.inner.delete(id).await
|
||||
}
|
||||
|
||||
async fn is_configured(&self) -> Result<bool, StoreError> {
|
||||
self.inner.is_configured().await
|
||||
}
|
||||
|
||||
async fn mark_configured(&self) -> Result<(), StoreError> {
|
||||
self.inner.mark_configured().await
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FsEmbedderProfileStore — declarative embedder profiles (`embedder.json`, LOT C).
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -286,3 +339,88 @@ impl EmbedderProfileStore for FsEmbedderProfileStore {
|
||||
FsEmbedderProfileStore::delete(self, id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod backfill_tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// In-memory [`ProfileStore`] fake recording mutating/query calls, so we can prove
|
||||
/// the decorator transforms ONLY `list` and delegates the rest verbatim.
|
||||
#[derive(Default)]
|
||||
struct FakeStore {
|
||||
profiles: Mutex<Vec<AgentProfile>>,
|
||||
saved: Mutex<Vec<AgentProfile>>,
|
||||
deleted: Mutex<Vec<ProfileId>>,
|
||||
is_configured_calls: Mutex<u32>,
|
||||
mark_configured_calls: Mutex<u32>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ProfileStore for FakeStore {
|
||||
async fn list(&self) -> Result<Vec<AgentProfile>, StoreError> {
|
||||
Ok(self.profiles.lock().unwrap().clone())
|
||||
}
|
||||
async fn save(&self, profile: &AgentProfile) -> Result<(), StoreError> {
|
||||
self.saved.lock().unwrap().push(profile.clone());
|
||||
Ok(())
|
||||
}
|
||||
async fn delete(&self, id: ProfileId) -> Result<(), StoreError> {
|
||||
self.deleted.lock().unwrap().push(id);
|
||||
Ok(())
|
||||
}
|
||||
async fn is_configured(&self) -> Result<bool, StoreError> {
|
||||
*self.is_configured_calls.lock().unwrap() += 1;
|
||||
Ok(true)
|
||||
}
|
||||
async fn mark_configured(&self) -> Result<(), StoreError> {
|
||||
*self.mark_configured_calls.lock().unwrap() += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude reference profile with the internal field cleared — the persisted-before
|
||||
/// shape the overlay backfills.
|
||||
fn claude_without_pattern() -> AgentProfile {
|
||||
let id = application::reference_profile_id("claude");
|
||||
let mut p = application::reference_profiles()
|
||||
.into_iter()
|
||||
.find(|p| p.id == id)
|
||||
.expect("claude reference exists");
|
||||
p.prompt_ready_pattern = None;
|
||||
p
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_overlays_reference_defaults() {
|
||||
let fake = Arc::new(FakeStore::default());
|
||||
fake.profiles.lock().unwrap().push(claude_without_pattern());
|
||||
let store = ReferenceBackfillProfileStore::new(Arc::clone(&fake) as Arc<dyn ProfileStore>);
|
||||
|
||||
let listed = store.list().await.expect("list ok");
|
||||
assert_eq!(listed.len(), 1);
|
||||
assert_eq!(
|
||||
listed[0].prompt_ready_pattern.as_deref(),
|
||||
Some("? for shortcuts"),
|
||||
"a stored Claude profile WITHOUT the pattern is backfilled on read"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn save_delete_and_config_delegate_verbatim() {
|
||||
let fake = Arc::new(FakeStore::default());
|
||||
let store = ReferenceBackfillProfileStore::new(Arc::clone(&fake) as Arc<dyn ProfileStore>);
|
||||
|
||||
let p = claude_without_pattern();
|
||||
store.save(&p).await.expect("save ok");
|
||||
store.delete(p.id).await.expect("delete ok");
|
||||
assert!(store.is_configured().await.expect("is_configured ok"));
|
||||
store.mark_configured().await.expect("mark_configured ok");
|
||||
|
||||
// save passes the profile through UNCHANGED (no overlay on the write path).
|
||||
assert_eq!(*fake.saved.lock().unwrap(), vec![p.clone()]);
|
||||
assert_eq!(*fake.deleted.lock().unwrap(), vec![p.id]);
|
||||
assert_eq!(*fake.is_configured_calls.lock().unwrap(), 1);
|
||||
assert_eq!(*fake.mark_configured_calls.lock().unwrap(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user