fix(runtime): isolate agent state by project (#101)
This commit is contained in:
@ -17,6 +17,7 @@ use domain::conversation::{
|
||||
Conversation, ConversationId, ConversationParty, ConversationRegistry, ConversationSession,
|
||||
SessionRef,
|
||||
};
|
||||
use domain::ids::ProjectId;
|
||||
|
||||
/// Order-insensitive key for a conversation pair `{a, b}`.
|
||||
///
|
||||
@ -47,7 +48,7 @@ pub struct InMemoryConversationRegistry {
|
||||
#[derive(Default)]
|
||||
struct Inner {
|
||||
by_id: HashMap<ConversationId, Conversation>,
|
||||
by_pair: HashMap<(ConversationParty, ConversationParty), ConversationId>,
|
||||
by_pair: HashMap<(ProjectId, ConversationParty, ConversationParty), ConversationId>,
|
||||
}
|
||||
|
||||
impl InMemoryConversationRegistry {
|
||||
@ -79,10 +80,16 @@ impl InMemoryConversationRegistry {
|
||||
}
|
||||
|
||||
impl ConversationRegistry for InMemoryConversationRegistry {
|
||||
fn resolve(&self, a: ConversationParty, b: ConversationParty) -> Conversation {
|
||||
fn resolve(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
a: ConversationParty,
|
||||
b: ConversationParty,
|
||||
) -> Conversation {
|
||||
let key = pair_key(a, b);
|
||||
let scoped_key = (project_id, key.0, key.1);
|
||||
let mut inner = self.lock();
|
||||
if let Some(id) = inner.by_pair.get(&key).copied() {
|
||||
if let Some(id) = inner.by_pair.get(&scoped_key).copied() {
|
||||
// Existing thread for this pair — return its current snapshot.
|
||||
return inner
|
||||
.by_id
|
||||
@ -97,10 +104,10 @@ impl ConversationRegistry for InMemoryConversationRegistry {
|
||||
// the persistence key stable and aligned with `LaunchAgent` (P8a) / the
|
||||
// `resolve_conversation` fallback. The pair is valid by construction at call
|
||||
// sites; `try_new` still guards the invariants.
|
||||
let id = ConversationId::for_pair(key.0, key.1);
|
||||
let id = ConversationId::for_project_pair(project_id, key.0, key.1);
|
||||
let conv = Conversation::try_new(id, key.0, key.1)
|
||||
.expect("pair_key yields a valid distinct/≤1-user pair");
|
||||
inner.by_pair.insert(key, id);
|
||||
inner.by_pair.insert(scoped_key, id);
|
||||
inner.by_id.insert(id, conv.clone());
|
||||
conv
|
||||
}
|
||||
@ -136,14 +143,18 @@ mod tests {
|
||||
ConversationParty::agent(AgentId::from_uuid(uuid::Uuid::from_u128(n)))
|
||||
}
|
||||
|
||||
fn project(n: u128) -> ProjectId {
|
||||
ProjectId::from_uuid(uuid::Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_is_lazy_get_or_create() {
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
assert!(reg.is_empty());
|
||||
let c = reg.resolve(ConversationParty::User, agent(1));
|
||||
let c = reg.resolve(project(1), ConversationParty::User, agent(1));
|
||||
assert_eq!(reg.len(), 1);
|
||||
// Same pair ⇒ same id, no new conversation created.
|
||||
let c2 = reg.resolve(ConversationParty::User, agent(1));
|
||||
let c2 = reg.resolve(project(1), ConversationParty::User, agent(1));
|
||||
assert_eq!(c.id, c2.id);
|
||||
assert_eq!(reg.len(), 1);
|
||||
}
|
||||
@ -151,8 +162,8 @@ mod tests {
|
||||
#[test]
|
||||
fn same_pair_unordered_yields_same_id() {
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let c1 = reg.resolve(agent(1), agent(2));
|
||||
let c2 = reg.resolve(agent(2), agent(1)); // swapped order
|
||||
let c1 = reg.resolve(project(1), agent(1), agent(2));
|
||||
let c2 = reg.resolve(project(1), agent(2), agent(1)); // swapped order
|
||||
assert_eq!(c1.id, c2.id, "unordered pair identity");
|
||||
assert_eq!(reg.len(), 1);
|
||||
}
|
||||
@ -160,8 +171,8 @@ mod tests {
|
||||
#[test]
|
||||
fn distinct_pairs_get_distinct_ids() {
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let user_b = reg.resolve(ConversationParty::User, agent(2));
|
||||
let a_b = reg.resolve(agent(1), agent(2));
|
||||
let user_b = reg.resolve(project(1), ConversationParty::User, agent(2));
|
||||
let a_b = reg.resolve(project(1), agent(1), agent(2));
|
||||
assert_ne!(user_b.id, a_b.id, "User↔B and A↔B are different threads");
|
||||
assert_eq!(reg.len(), 2);
|
||||
}
|
||||
@ -169,14 +180,14 @@ mod tests {
|
||||
#[test]
|
||||
fn fresh_resolve_is_dormant() {
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let c = reg.resolve(ConversationParty::User, agent(1));
|
||||
let c = reg.resolve(project(1), ConversationParty::User, agent(1));
|
||||
assert_eq!(c.session, ConversationSession::Dormant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_session_makes_it_live_then_suspend_restores_dormant() {
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let c = reg.resolve(ConversationParty::User, agent(1));
|
||||
let c = reg.resolve(project(1), ConversationParty::User, agent(1));
|
||||
let sref = SessionRef::new(SessionId::from_uuid(uuid::Uuid::from_u128(99)));
|
||||
reg.bind_session(c.id, sref);
|
||||
let live = reg.get(c.id).unwrap();
|
||||
@ -210,10 +221,10 @@ mod tests {
|
||||
// l'IDE : la même paire User↔Agent doit produire le **même** id.
|
||||
let a = agent(7);
|
||||
let first = InMemoryConversationRegistry::new()
|
||||
.resolve(ConversationParty::User, a)
|
||||
.resolve(project(1), ConversationParty::User, a)
|
||||
.id;
|
||||
let second = InMemoryConversationRegistry::new()
|
||||
.resolve(ConversationParty::User, a)
|
||||
.resolve(project(1), ConversationParty::User, a)
|
||||
.id;
|
||||
assert_eq!(
|
||||
first, second,
|
||||
@ -226,8 +237,12 @@ mod tests {
|
||||
// Même garantie pour une paire Agent↔Agent (dérivation XOR commutative).
|
||||
let x = agent(11);
|
||||
let y = agent(13);
|
||||
let first = InMemoryConversationRegistry::new().resolve(x, y).id;
|
||||
let second = InMemoryConversationRegistry::new().resolve(y, x).id;
|
||||
let first = InMemoryConversationRegistry::new()
|
||||
.resolve(project(1), x, y)
|
||||
.id;
|
||||
let second = InMemoryConversationRegistry::new()
|
||||
.resolve(project(1), y, x)
|
||||
.id;
|
||||
assert_eq!(
|
||||
first, second,
|
||||
"id de paire Agent↔Agent stable au redémarrage, insensible à l'ordre"
|
||||
@ -242,18 +257,13 @@ mod tests {
|
||||
let agent_id = AgentId::from_uuid(uuid::Uuid::from_u128(42));
|
||||
let party = ConversationParty::agent(agent_id);
|
||||
let resolved = InMemoryConversationRegistry::new()
|
||||
.resolve(ConversationParty::User, party)
|
||||
.resolve(project(1), ConversationParty::User, party)
|
||||
.id;
|
||||
assert_eq!(
|
||||
resolved,
|
||||
ConversationId::for_pair(ConversationParty::User, party),
|
||||
ConversationId::for_project_pair(project(1), ConversationParty::User, party),
|
||||
"resolve == for_pair (alignement de clé)"
|
||||
);
|
||||
assert_eq!(
|
||||
resolved,
|
||||
ConversationId::from_uuid(agent_id.as_uuid()),
|
||||
"User↔Agent ⇒ id == uuid de l'agent (repli resolve_conversation)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -262,12 +272,12 @@ mod tests {
|
||||
let x = agent(101);
|
||||
let y = agent(202);
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let id_xy = reg.resolve(x, y).id;
|
||||
let id_yx = reg.resolve(y, x).id;
|
||||
let id_xy = reg.resolve(project(1), x, y).id;
|
||||
let id_yx = reg.resolve(project(1), y, x).id;
|
||||
assert_eq!(id_xy, id_yx, "resolve(a,b) == resolve(b,a)");
|
||||
assert_eq!(
|
||||
id_xy,
|
||||
ConversationId::for_pair(x, y),
|
||||
ConversationId::for_project_pair(project(1), x, y),
|
||||
"resolve == for_pair (Agent↔Agent)"
|
||||
);
|
||||
assert_eq!(reg.len(), 1, "une seule conversation pour la paire {{a,b}}");
|
||||
@ -277,11 +287,25 @@ mod tests {
|
||||
fn distinct_pairs_yield_distinct_ids_across_kinds() {
|
||||
// Deux paires distinctes ⇒ deux ids distincts (pas de collision de clé).
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let user_a = reg.resolve(ConversationParty::User, agent(1)).id;
|
||||
let user_b = reg.resolve(ConversationParty::User, agent(2)).id;
|
||||
let a_b = reg.resolve(agent(1), agent(2)).id;
|
||||
let user_a = reg
|
||||
.resolve(project(1), ConversationParty::User, agent(1))
|
||||
.id;
|
||||
let user_b = reg
|
||||
.resolve(project(1), ConversationParty::User, agent(2))
|
||||
.id;
|
||||
let a_b = reg.resolve(project(1), agent(1), agent(2)).id;
|
||||
assert_ne!(user_a, user_b, "User↔A ≠ User↔B");
|
||||
assert_ne!(user_a, a_b, "User↔A ≠ A↔B");
|
||||
assert_ne!(user_b, a_b, "User↔B ≠ A↔B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_agent_ids_in_distinct_projects_get_distinct_threads() {
|
||||
let reg = InMemoryConversationRegistry::new();
|
||||
let p1 = reg.resolve(project(1), ConversationParty::User, agent(7));
|
||||
let p2 = reg.resolve(project(2), ConversationParty::User, agent(7));
|
||||
|
||||
assert_ne!(p1.id, p2.id);
|
||||
assert_eq!(reg.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user