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:
@ -17,12 +17,13 @@ use application::{
|
||||
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
|
||||
LaunchAgentInput, ListAgentsInput, ListLayoutsInput, ListMemoriesInput,
|
||||
ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput, McpRuntime,
|
||||
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadMemoryIndexInput,
|
||||
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, RenameLayoutInput,
|
||||
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
|
||||
SnapshotRunningAgentsInput, StopLiveAgentInput, SyncAgentWithTemplateInput,
|
||||
UnassignSkillFromAgentInput, UpdateAgentContextInput, UpdateAgentPermissionsInput,
|
||||
UpdateMemoryInput, UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
||||
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadConversationPageInput,
|
||||
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput,
|
||||
RenameLayoutInput, ResolveAgentPermissionsInput, ResolveMemoryLinksInput,
|
||||
RotateConversationLogInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
|
||||
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
||||
UpdateAgentContextInput, UpdateAgentPermissionsInput, UpdateMemoryInput,
|
||||
UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
||||
};
|
||||
use domain::ports::PtyHandle;
|
||||
|
||||
@ -43,13 +44,14 @@ use crate::dto::{
|
||||
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
|
||||
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
||||
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||
ProjectPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto, ReattachChatDto,
|
||||
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
|
||||
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, ResumableAgentListDto,
|
||||
SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto,
|
||||
SkillListDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
|
||||
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
||||
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
||||
ProjectPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto,
|
||||
ReadConversationPageRequestDto, ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto,
|
||||
RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
||||
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
|
||||
SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto, SkillListDto,
|
||||
StopLiveAgentRequestDto, StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto,
|
||||
SyncResultDto, TemplateDto, TemplateListDto, TerminalClosedDto, TerminalSessionDto,
|
||||
TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
||||
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
||||
UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
||||
WriteTerminalRequestDto,
|
||||
@ -989,6 +991,39 @@ pub async fn get_project_work_state(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `read_conversation_page` — human, paginated read of a conversation's **full**
|
||||
/// transcript (lot LS6). Archive-aware (segments + active), text never truncated.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed project/conversation id,
|
||||
/// `NOT_FOUND` if the project is unknown, `STORE` on log I/O failure).
|
||||
#[tauri::command]
|
||||
pub async fn read_conversation_page(
|
||||
request: ReadConversationPageRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<TurnPageDto, ErrorDto> {
|
||||
let project = resolve_project(&request.project_id, &state).await?;
|
||||
let conversation = uuid::Uuid::parse_str(&request.conversation_id)
|
||||
.map(domain::ConversationId::from_uuid)
|
||||
.map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid conversation id: {}", request.conversation_id),
|
||||
})?;
|
||||
let cursor = request.cursor();
|
||||
let limit = request.limit.unwrap_or(0);
|
||||
state
|
||||
.read_conversation_page
|
||||
.execute(ReadConversationPageInput {
|
||||
project_root: project.root,
|
||||
conversation,
|
||||
cursor,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
.map(TurnPageDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_live_agents` — list every agent that currently owns a live session
|
||||
/// (raw PTY **or** structured/chat) and the cell hosting each, so the UI can
|
||||
/// disable an agent already running in another cell (the "one live session per
|
||||
@ -1222,6 +1257,9 @@ pub async fn launch_agent(
|
||||
// agent so an auto-resume (which only carries agent/node/conversation) can recompose a
|
||||
// full `LaunchAgentInput`. Cloned here — `project` is moved into the launch below.
|
||||
let resume_project = project.clone();
|
||||
// Lot LS6 — project root captured before `project` moves, for the off-hot-path log
|
||||
// rotation triggered at thread (re)open below.
|
||||
let rotation_root = project.root.clone();
|
||||
|
||||
let output = state
|
||||
.launch_agent
|
||||
@ -1250,6 +1288,28 @@ pub async fn launch_agent(
|
||||
);
|
||||
}
|
||||
|
||||
// Lot LS6 — rotation **best-effort** du log, déclenchée à la (re)ouverture du fil et
|
||||
// **hors chemin chaud** : détachée (`tokio::spawn`) pour n'ajouter aucune latence au
|
||||
// launch, erreurs **avalées** (la rotation ne doit jamais casser une reprise). Un
|
||||
// `append` ne déclenche JAMAIS la rotation. Skippée si la cellule ne porte pas une
|
||||
// `conversation_id` UUID (rien à roter).
|
||||
if let Some(conversation) = request
|
||||
.conversation_id
|
||||
.as_deref()
|
||||
.and_then(|raw| uuid::Uuid::parse_str(raw).ok())
|
||||
.map(domain::ConversationId::from_uuid)
|
||||
{
|
||||
let rotate = std::sync::Arc::clone(&state.rotate_conversation_log);
|
||||
tokio::spawn(async move {
|
||||
let _ = rotate
|
||||
.execute(RotateConversationLogInput {
|
||||
project_root: rotation_root,
|
||||
conversation,
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
let session_id = output.session.id;
|
||||
// Host cell of the freshly (re)launched session — the pivot the level-2 tap reports
|
||||
// to the service alongside the agent.
|
||||
|
||||
Reference in New Issue
Block a user