feat(persistence): P8b — écriture providers.json (resumable par provider)

Au lancement structuré, le resumable exposé par le moteur est rangé dans
providers.json sous (id de paire, provider) — best-effort, n'altère jamais le
lancement.

- domaine : StructuredAdapter::provider_key() ⇒ "claude"/"codex" (clé stable
  par famille, lisible, indépendante de l'uuid d'instance)
- application : port ProviderSessionProvider (root par appel) +
  with_provider_session_provider ; helper persist_provider_session dans
  launch_structured (skip si provider/adapter/id moteur absents ou set KO)
- app-tauri : AppProviderSessionProvider (FsProviderSessionStore sur le root)
- tests : 6 cas (nominal claude/codex, no-op sans provider/sans id moteur,
  best-effort, mapping provider_key) ; domain+application verts

Lecture du store pour --resume = P8c.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:36:19 +02:00
parent 09cc8f0902
commit 9b053216e3
6 changed files with 430 additions and 5 deletions

View File

@ -42,7 +42,7 @@ use domain::{DomainEvent, EmbedderProfile, Project, ProjectId};
use infrastructure::{
embedder_from_profile, AdaptiveMemoryRecall, ClaudeTranscriptInspector, CliAgentRuntime,
EmbedderEnvProbe, FsConversationLog, FsEmbedderProfileStore, FsEmbedderPromptStore,
FsHandoffStore, FsMemoryStore, HeuristicHandoffSummarizer,
FsHandoffStore, FsMemoryStore, FsProviderSessionStore, HeuristicHandoffSummarizer,
FsOrchestratorWatcher, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore,
Git2Repository, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
LocalFileSystem, LocalProcessSpawner, McpServer, MediatedInbox, SystemMillisClock,
@ -101,6 +101,25 @@ impl application::HandoffProvider for AppHandoffProvider {
}
}
/// Implémente [`ProviderSessionProvider`](application::ProviderSessionProvider) (lot
/// P8b) en matérialisant un [`FsProviderSessionStore`] ciblant le **project root** du
/// lancement en cours.
///
/// Jumeau stateless de [`AppHandoffProvider`] : [`LaunchAgent`] est unique pour tous
/// les projets, alors que `providers.json` est **par project root**
/// (`<root>/.ideai/conversations/providers.json`). On construit donc un store frais
/// par lancement, ciblant le bon dossier. Sans état (zéro champ), partagé via `Arc`.
struct AppProviderSessionProvider;
impl application::ProviderSessionProvider for AppProviderSessionProvider {
fn provider_session_store_for(
&self,
root: &domain::project::ProjectPath,
) -> Option<Arc<dyn domain::ProviderSessionStore>> {
Some(Arc::new(FsProviderSessionStore::new(root)) as Arc<dyn domain::ProviderSessionStore>)
}
}
/// Everything the IPC layer needs at runtime, managed by Tauri.
///
/// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete
@ -627,7 +646,14 @@ impl AppState {
// porte une conversation et qu'un handoff existe (`<root>/.ideai/conversations/`),
// son résumé est réinjecté dans le convention file. Best-effort, additif :
// un handoff absent/illisible ⇒ lancement normal sans section.
.with_handoff_provider(Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>),
.with_handoff_provider(Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>)
// Resumable moteur par provider (lot P8b) : après un lancement structuré
// exposant un id de session moteur, rangé sous la clé de paire dans
// `<root>/.ideai/conversations/providers.json`. Best-effort, additif : une
// écriture en échec / pas d'id moteur ⇒ lancement normal, aucune écriture.
.with_provider_session_provider(
Arc::new(AppProviderSessionProvider) as Arc<dyn application::ProviderSessionProvider>,
),
);
// Hot-swap an agent's runtime profile (§15.1). Reuses the shared context/