feat(persistence): P5 — ProviderSessionStore (resumable_id par provider)
Range le resumable_id du moteur par (conversation, provider) dans providers.json, support de reprise non exclusif (le handoff reste la source de fidélité au swap cross-profile) — §19.2/§19.3. - domaine : port ProviderSessionStore (get/set par provider) - infra : FsProviderSessionStore — providers.json map plate, set en read-modify-write atomique (tmp+rename) sérialisé par conversation (coexistence multi-providers garantie), get absent ⇒ None, corrompu ⇒ StoreError::Serialization Tests : 6 cas P5 (coexistence claude+codex, 12 set concurrents sans perte, corruption, isolation) ; cible conversation_log 30 verts, suites complètes sans régression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -278,6 +278,54 @@ pub trait HandoffSummarizer: Send + Sync {
|
||||
async fn fold(&self, prev: Option<Handoff>, new_turns: &[ConversationTurn]) -> Handoff;
|
||||
}
|
||||
|
||||
/// Le store des `resumable_id` CLI, rangés **par (conversation, provider)** (port
|
||||
/// driven, lot P5).
|
||||
///
|
||||
/// Le `resumable_id` est l'identifiant **propre au moteur** (le `--resume`/`--continue`
|
||||
/// d'une CLI : `"claude"`, `"codex"`…) qui permet de **réattacher** la session native du
|
||||
/// provider à la reprise (ARCHITECTURE §19.2/§19.3). Contrairement au [`Handoff`]
|
||||
/// (résumé **portable**, indépendant du provider, source de vérité de la continuité),
|
||||
/// ce store garde l'optimisation **non portable** : chaque provider a **son** id, et
|
||||
/// plusieurs providers peuvent coexister pour une même conversation (après un swap
|
||||
/// Claude→Codex, on garde l'id de chacun). La clé est donc bien le couple
|
||||
/// `(conversation, provider_id)`.
|
||||
///
|
||||
/// `#[async_trait]` et erreur [`StoreError`] comme les ports voisins
|
||||
/// ([`ConversationLog`], [`HandoffStore`]) : injecté en `Arc<dyn ProviderSessionStore>`
|
||||
/// au composition root, gardé object-safe. La persistance réelle (`providers.json`) est
|
||||
/// une affaire d'infrastructure (l'adapter `FsProviderSessionStore`, lot P5).
|
||||
#[async_trait::async_trait]
|
||||
pub trait ProviderSessionStore: Send + Sync {
|
||||
/// Charge le `resumable_id` du provider `provider_id` pour `conversation`.
|
||||
///
|
||||
/// Renvoie `Ok(Some(id))` si un id a été rangé pour ce couple, `Ok(None)` si le
|
||||
/// provider (ou la conversation) n'a encore rien rangé. L'absence n'est **jamais**
|
||||
/// une erreur.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`StoreError`] en cas d'échec de lecture ou de désérialisation.
|
||||
async fn get(
|
||||
&self,
|
||||
conversation: ConversationId,
|
||||
provider_id: &str,
|
||||
) -> Result<Option<String>, StoreError>;
|
||||
|
||||
/// Range (en écrasant) le `resumable_id` du provider `provider_id` pour
|
||||
/// `conversation`.
|
||||
///
|
||||
/// Les autres providers déjà rangés pour la même conversation **coexistent** : seul
|
||||
/// l'enregistrement de `provider_id` est créé/écrasé.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`StoreError`] en cas d'échec d'écriture ou de sérialisation.
|
||||
async fn set(
|
||||
&self,
|
||||
conversation: ConversationId,
|
||||
provider_id: &str,
|
||||
resumable_id: &str,
|
||||
) -> Result<(), StoreError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user