fix(runtime): isolate agent state by project (#101)

This commit is contained in:
2026-07-25 22:14:02 +02:00
parent 6a87c4635f
commit 6e98fd89f7
52 changed files with 1894 additions and 805 deletions

View File

@ -22,7 +22,7 @@
use serde::{Deserialize, Serialize};
use crate::ids::{AgentId, SessionId};
use crate::ids::{AgentId, ProjectId, RuntimeAgentKey, SessionId};
/// Identifies one [`Conversation`].
///
@ -68,19 +68,38 @@ impl ConversationId {
/// Pour une paire `Agent↔Agent`, l'id est dérivé des deux UUID agent de façon
/// commutative (XOR), stable et déterministe.
#[must_use]
pub fn for_pair(a: ConversationParty, b: ConversationParty) -> Self {
pub fn for_project_pair(
project_id: ProjectId,
a: ConversationParty,
b: ConversationParty,
) -> Self {
match (a.as_agent(), b.as_agent()) {
// User↔Agent (un seul agent) : aligné sur le repli de `resolve_conversation`.
(Some(agent), None) | (None, Some(agent)) => Self::from_uuid(agent.as_uuid()),
(Some(agent), None) | (None, Some(agent)) => {
let key = RuntimeAgentKey::new(project_id, agent);
Self::from_uuid(uuid::Uuid::from_u128(
key.project_id.as_uuid().as_u128() ^ key.agent_id.as_uuid().as_u128(),
))
}
// Agent↔Agent : combinaison commutative des deux ids (insensible à l'ordre).
(Some(x), Some(y)) => {
let xor = x.as_uuid().as_u128() ^ y.as_uuid().as_u128();
let xor =
project_id.as_uuid().as_u128() ^ x.as_uuid().as_u128() ^ y.as_uuid().as_u128();
Self::from_uuid(uuid::Uuid::from_u128(xor))
}
// User↔User n'existe pas (invariant `Conversation`) : repli sûr non-panic.
(None, None) => Self::from_uuid(uuid::Uuid::nil()),
}
}
/// Legacy deterministic pair id without project scoping.
///
/// Runtime code must prefer [`Self::for_project_pair`]. Kept for persisted legacy
/// data and tests that intentionally exercise project-less values.
#[must_use]
pub fn for_pair(a: ConversationParty, b: ConversationParty) -> Self {
Self::for_project_pair(ProjectId::from_uuid(uuid::Uuid::nil()), a, b)
}
}
impl std::fmt::Display for ConversationId {
@ -322,7 +341,12 @@ pub trait ConversationRegistry: Send + Sync {
/// Get-or-create: returns the thread for the pair `{a, b}`, opening it
/// (`Dormant`) if it did not exist. Pure registry — opens **no** session.
/// The same unordered pair always yields the same [`ConversationId`].
fn resolve(&self, a: ConversationParty, b: ConversationParty) -> Conversation;
fn resolve(
&self,
project_id: ProjectId,
a: ConversationParty,
b: ConversationParty,
) -> Conversation;
/// Marks the conversation `id` `Live` with the given session reference.
fn bind_session(&self, id: ConversationId, session: SessionRef);