feat(persistence): P6b — câblage live du checkpoint conversationnel (best-effort)

ask_agent persiste désormais chaque paire dans son conversationId : tour Prompt
à l'enqueue, tour Response au succès — best-effort (un échec de persistance ne
dégrade jamais la délégation).

- application : port RecordTurnProvider (matérialise un RecordTurn sur le bon
  project root — OrchestratorService est mono-instance multi-projets, le log est
  par root) ; wither with_record_turn(provider, Clock) ; helper
  record_turn_best_effort ; horodatage via domain::ports::Clock (pas d'horloge
  infra dans application)
- app-tauri : AppRecordTurnProvider (Fs* sur le root du projet) câblé au
  composition root avec le SystemClock partagé
- tests : 5 cas (paire Prompt→Response, fil A↔B vs User↔B, no-op sans provider,
  ask Ok même si record échoue) ; orchestrator_service 40, application +
  app-tauri verts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:31:13 +02:00
parent 2a5873dcf0
commit 0bf7a5c43c
5 changed files with 546 additions and 7 deletions

View File

@ -22,7 +22,8 @@ use application::{
ListTemplates,
LiveAgentRegistry, LoadLayout, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
OpenTerminal, OrchestratorService, ReadAgentContext, ReadMemoryIndex, ReadProjectContext,
RecallMemory, ReconcileLayouts, ReferenceProfiles, RenameLayout, ResizeTerminal,
RecallMemory, ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles,
RenameLayout, ResizeTerminal,
ResolveMemoryLinks,
SaveEmbedderProfile, SaveProfile, SetActiveLayout, SnapshotRunningAgents, StructuredSessions,
SuggestedThisSession,
@ -40,7 +41,8 @@ use domain::{DomainEvent, EmbedderProfile, Project, ProjectId};
use infrastructure::{
embedder_from_profile, AdaptiveMemoryRecall, ClaudeTranscriptInspector, CliAgentRuntime,
EmbedderEnvProbe, FsEmbedderProfileStore, FsEmbedderPromptStore, FsMemoryStore,
EmbedderEnvProbe, FsConversationLog, FsEmbedderProfileStore, FsEmbedderPromptStore,
FsHandoffStore, FsMemoryStore, HeuristicHandoffSummarizer,
FsOrchestratorWatcher, FsProfileStore, FsProjectStore, FsSkillStore, FsTemplateStore,
Git2Repository, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
LocalFileSystem, LocalProcessSpawner, McpServer, MediatedInbox, SystemMillisClock,
@ -61,6 +63,25 @@ use interprocess::local_socket::traits::tokio::Listener as _;
use interprocess::local_socket::{GenericFilePath, ListenerOptions, ToFsName};
use tokio::io::{AsyncBufReadExt, BufReader};
/// Implémente [`RecordTurnProvider`] (lot P6b) en matérialisant un [`RecordTurn`]
/// ciblant le **project root** du tour en cours.
///
/// L'[`OrchestratorService`] est unique pour tous les projets, alors que le log/handoff
/// conversationnel est **par project root** (`<root>/.ideai/conversations/`). Les
/// adapters `Fs*` fixent leur racine à la construction et ne sont que des jointures de
/// chemin : on en construit donc un jeu frais par tour, ciblant le bon dossier. Sans
/// état (zéro champ), partagé via un simple `Arc`.
struct AppRecordTurnProvider;
impl RecordTurnProvider for AppRecordTurnProvider {
fn record_turn_for(&self, root: &domain::project::ProjectPath) -> Option<Arc<RecordTurn>> {
let log = Arc::new(FsConversationLog::new(root));
let handoffs = Arc::new(FsHandoffStore::new(root));
let summarizer = Arc::new(HeuristicHandoffSummarizer::new());
Some(Arc::new(RecordTurn::new(log, handoffs, summarizer)))
}
}
/// Everything the IPC layer needs at runtime, managed by Tauri.
///
/// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete
@ -785,6 +806,14 @@ impl AppState {
// réelle ⇒ le pont MCP de la cible se spawne et `idea_reply` débloque le round-trip.
.with_mcp_runtime_provider(
Arc::new(AppMcpRuntimeProvider) as Arc<dyn application::McpRuntimeProvider>
)
// Persistance conversationnelle best-effort (lot P6b) : Prompt + Response de
// chaque paire déléguée écrits dans `<root>/.ideai/conversations/<conv>` via
// le provider per-root + l'horloge millis (port Clock) déjà câblée. Un échec
// n'affecte jamais la délégation live.
.with_record_turn(
Arc::new(AppRecordTurnProvider) as Arc<dyn RecordTurnProvider>,
Arc::clone(&clock) as Arc<dyn Clock>,
),
);