feat(domain,infra,app): rotation/rétention log.jsonl + lecture paginée (LS6)

Backend uniquement (UI React repoussée à LS7) :
- domain : port ConversationArchive + structs SegmentStats/PageCursor/PageDirection/
  TurnSlice/RotationDecision/RotationThresholds, fn pures rotation_plan/clamp_page_limit
  + consts ; re-exports lib.
- infrastructure : impl ConversationArchive pour FsConversationLog (stats/rotate/page
  + helpers), archive segmentée hors chemin chaud.
- application : ReadConversationPage + DTO + ConversationArchiveProvider (conversation/paginate),
  RotateConversationLog (conversation/rotate), exports mod/lib.
- app-tauri : AppConversationArchiveProvider + wiring (state), rotation détachée dans
  launch_agent + commande read_conversation_page (commands), DTOs (dto),
  commande enregistrée (generate_handler!).
- tests (QA, verts) : conversation_log, conversation_rotate_paginate (nouveau),
  dto (module test). Pivots INV-LS6 et cohérence fold-après-rotation verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 12:58:40 +02:00
parent f9231422fe
commit 40ca3e522f
13 changed files with 2095 additions and 43 deletions

View File

@ -26,14 +26,15 @@ use application::{
LiveSessions, LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout,
McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
ReadContext, ReadMemory, ReadMemoryIndex, ReadProjectContext, ReadSkill, RecallMemory,
ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout,
ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks, SaveEmbedderProfile, SaveProfile,
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, StopLiveAgent, StructuredSessions,
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent,
UpdateAgentContext, UpdateAgentPermissions, UpdateLiveState, UpdateMemory,
UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate, WriteMemory,
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
ReadContext, ReadConversationPage, ReadMemory, ReadMemoryIndex, ReadProjectContext, ReadSkill,
RecallMemory, ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles,
RenameLayout, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
RotateConversationLog, SaveEmbedderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
SnapshotRunningAgents, StopLiveAgent, StructuredSessions, SuggestedThisSession,
SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent, UpdateAgentContext,
UpdateAgentPermissions, UpdateLiveState, UpdateMemory, UpdateProjectContext,
UpdateProjectPermissions, UpdateSkill, UpdateTemplate, WriteMemory, WriteToTerminal,
AGENT_MEMORY_RECALL_BUDGET,
};
use domain::ports::{
AgentContextStore, AgentRuntime, AgentSessionFactory, Clock, Embedder, EmbedderEnvInspector,
@ -93,6 +94,24 @@ impl RecordTurnProvider for AppRecordTurnProvider {
}
}
/// Implémente [`ConversationArchiveProvider`](application::ConversationArchiveProvider)
/// (lot LS6) en matérialisant un [`FsConversationLog`] (qui implémente aussi
/// [`domain::ConversationArchive`]) ciblant le **project root** courant.
///
/// Même raison d'être que [`AppRecordTurnProvider`] : les use cases d'archivage/pagination
/// sont uniques pour tous les projets, alors que les logs sont **par project root**. On
/// construit donc un archive frais par appel, ciblant le bon dossier. Sans état.
struct AppConversationArchiveProvider;
impl application::ConversationArchiveProvider for AppConversationArchiveProvider {
fn conversation_archive_for(
&self,
root: &domain::project::ProjectPath,
) -> Option<Arc<dyn domain::ConversationArchive>> {
Some(Arc::new(FsConversationLog::new(root)) as Arc<dyn domain::ConversationArchive>)
}
}
/// Implémente [`HandoffProvider`](application::HandoffProvider) (lot P7) en
/// matérialisant un [`FsHandoffStore`] ciblant le **project root** du lancement en
/// cours.
@ -414,6 +433,10 @@ pub struct AppState {
pub list_resumable_agents: Arc<ListResumableAgents>,
/// Read-only live/busy state for the project's manifest agents.
pub get_project_work_state: Arc<GetProjectWorkState>,
/// Human paginated read of a conversation's full transcript (lot LS6).
pub read_conversation_page: Arc<ReadConversationPage>,
/// Best-effort log rotation, triggered off the hot path at thread resume/open (lot LS6).
pub rotate_conversation_log: Arc<RotateConversationLog>,
/// Rebinds an already-running agent's live session to a visible cell (Lot D).
pub attach_live_agent: Arc<AttachLiveAgent>,
/// Tears down an already-running agent's live session by agent id (Lot D).
@ -1143,6 +1166,17 @@ impl AppState {
as Arc<dyn application::ConversationLogProvider>,
),
);
// Lot LS6 — rotation (hors chemin chaud) + lecture humaine paginée. Tous deux
// composent le provider d'archive par root ; la rotation lit aussi le handoff
// (plancher `up_to`, INV-LS6). Aucune persistance déclenchée par un `append`.
let archive_provider = Arc::new(AppConversationArchiveProvider)
as Arc<dyn application::ConversationArchiveProvider>;
let read_conversation_page =
Arc::new(ReadConversationPage::new(Arc::clone(&archive_provider)));
let rotate_conversation_log = Arc::new(RotateConversationLog::new(
Arc::clone(&archive_provider),
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>,
));
// Lot D — actions contrôlées agent-level sur les sessions vivantes : attach
// (rebind de la cellule-vue, zéro spawn) et stop (kill PTY via la primitive
// `CloseTerminal` existante / shutdown structuré). Aucune création de session.
@ -1343,6 +1377,8 @@ impl AppState {
change_agent_profile,
list_resumable_agents,
get_project_work_state,
read_conversation_page,
rotate_conversation_log,
attach_live_agent,
stop_live_agent,
inspect_conversation,