feat(persistence): P7 — reprise par injection du handoff au lancement

Au (re)lancement, l'agent retrouve sur quoi il travaillait : si la cellule a une
conversation_id avec un handoff, son objectif + résumé sont injectés en dernière
section du convention file (# Reprise de la conversation), best-effort et
provider-indépendant.

- application : port HandoffProvider (root par appel, comme MemoryRecall) ;
  LaunchAgent.with_handoff_provider + resolve_handoff (parse uuid + load,
  jamais bloquant) ; section rendue dans compose_convention_file après la
  mémoire projet
- app-tauri : AppHandoffProvider (FsHandoffStore sur le root du projet) câblé
- tests : 4 purs (objectif présent/None/blanc/absent) + 5 intégration
  (happy path, provider absent, conversation_id None, uuid invalide, sans
  handoff ⇒ lancement OK sans section) ; application 311 + app-tauri 204 verts

Volet --resume/providers.json = P8 (swap cross-profile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:42:19 +02:00
parent 0bf7a5c43c
commit c1411d3b69
5 changed files with 484 additions and 23 deletions

View File

@ -82,6 +82,25 @@ impl RecordTurnProvider for AppRecordTurnProvider {
}
}
/// Implémente [`HandoffProvider`](application::HandoffProvider) (lot P7) en
/// matérialisant un [`FsHandoffStore`] ciblant le **project root** du lancement en
/// cours.
///
/// Même raison d'être que [`AppRecordTurnProvider`] : [`LaunchAgent`] est unique pour
/// tous les projets, alors que le handoff est **par project root**
/// (`<root>/.ideai/conversations/`). On construit donc un store frais par lancement,
/// ciblant le bon dossier. Sans état (zéro champ), partagé via un simple `Arc`.
struct AppHandoffProvider;
impl application::HandoffProvider for AppHandoffProvider {
fn handoff_store_for(
&self,
root: &domain::project::ProjectPath,
) -> Option<Arc<dyn domain::HandoffStore>> {
Some(Arc::new(FsHandoffStore::new(root)) as Arc<dyn domain::HandoffStore>)
}
}
/// Everything the IPC layer needs at runtime, managed by Tauri.
///
/// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete
@ -590,19 +609,26 @@ impl AppState {
// PTH : tout profil (Claude/Codex inclus) ouvre une cellule terminal. Le code
// `launch_structured` reste en place (mort-code retiré au lot de nettoyage B-6).
let _session_factory = session_factory; // décâblé en B-2 (nettoyage B-6)
let launch_agent = Arc::new(LaunchAgent::new(
Arc::clone(&contexts_port),
Arc::clone(&profile_store_port),
Arc::clone(&runtime_port),
Arc::clone(&fs_port),
Arc::clone(&pty_port),
Arc::clone(&skill_store_port),
Arc::clone(&terminal_sessions),
Arc::clone(&events_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&memory_recall_port),
Some(Arc::clone(&check_embedder_suggestion)),
));
let launch_agent = Arc::new(
LaunchAgent::new(
Arc::clone(&contexts_port),
Arc::clone(&profile_store_port),
Arc::clone(&runtime_port),
Arc::clone(&fs_port),
Arc::clone(&pty_port),
Arc::clone(&skill_store_port),
Arc::clone(&terminal_sessions),
Arc::clone(&events_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
Arc::clone(&memory_recall_port),
Some(Arc::clone(&check_embedder_suggestion)),
)
// Reprise conversationnelle (lot P7) : à chaque (re)lancement, si la cellule
// 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>),
);
// Hot-swap an agent's runtime profile (§15.1). Reuses the shared context/
// profile/project/fs stores, the live-session registry and PTY port, and