feat(backend,frontend): tâches planifiées projet — tool MCP idea_scheduled_task_* et menu utilisateur #305, fix transcript lié indisponible après reboot #314 (QA verte)
#305: nouvel agrégat durable ScheduledProjectTask dans domain (scheduled_task.rs, ids, ports ScheduledProjectTaskStore), source de vérité <app_data_dir>/scheduled-project-tasks/<projectId>.json, schedules bornés V1 interval/daily/weekly avec fuseau, cycle À approuver/Active/Suspendue avec approbation obligatoire des tâches demandées par agent; use cases application/scheduled_task (list/create/update/delete/run_now/reconcile/fire) réutilisant BackgroundTask pour les runs avec distinction planifié/manuel; store infrastructure dédié + scheduler reconcile au boot sans rattrapage en rafale, cwd confiné au projet; 7 tools MCP idea_scheduled_task_* (list/read lecture seule, create/update/delete/run_now/set_enabled write-actions soumises aux permissions natives); commandes Tauri + routes HTTP aux contrats DTO identiques (camelCase nextRunAtMs/lastRunAtMs/lastTaskId/createdBy/updatedBy); frontend: gateways Tauri/HTTP/mock + types ports/domain, surface projet 'Tâches planifiées' (liste utilisateur+agent unifiée, création/édition avec récapitulatif avant activation, approbation/refus, exécuter maintenant, suspendre/reprendre, suppression), catalogage surface web (webSurfaceCatalog/viewPlacement/ViewPanelBody/WebWorkspace); docs/scheduled-project-tasks.md et sdk docs/services.md (047c38b: pas un service plugin V1, pas d'émulation via cron système). #314: correction de 'Conversation liée — transcript indisponible' après reboot: restauration du transcript lié côté application (orchestrator service, lifecycle, session_limit, background sink, terminal registry) et côté frontend (adapters agent/http frames/stream/requestResponse, workStateNormalization, CustomAgentChatView avec état et action utile), tests dto_chat/agent_lifecycle/structured_registry_d1/background_completion_sink étendus. QA verte rapportée par QA sur feature/305-scheduled-project-tasks-mcp, sans bloquant, pour #305 et #314. Hors périmètre worktree: tickets non liés, attachments/, scripts/; aucun push.
This commit is contained in:
@ -3917,11 +3917,32 @@ pub enum ReplyChunk {
|
||||
},
|
||||
}
|
||||
|
||||
/// Reason why a successful structured attach has no replayable turns.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ConversationTranscriptUnavailableReasonDto {
|
||||
/// A linked conversation was resumed, but the volatile live transcript was absent.
|
||||
MissingTranscript,
|
||||
/// The session is fresh and has not produced a conversation turn yet.
|
||||
EmptyConversation,
|
||||
}
|
||||
|
||||
impl ConversationTranscriptUnavailableReasonDto {
|
||||
/// Returns an additive reason only when no replayable chunk exists.
|
||||
#[must_use]
|
||||
pub fn for_scrollback(is_empty: bool, transcript_restore_expected: bool) -> Option<Self> {
|
||||
is_empty.then_some(if transcript_restore_expected {
|
||||
Self::MissingTranscript
|
||||
} else {
|
||||
Self::EmptyConversation
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Response DTO for `reattach_agent_chat`: the retained **conversation
|
||||
/// scrollback** of a still-live structured session, replayed into the
|
||||
/// re-mounting `AgentChatView` before the new reply stream is wired (§17.6). The
|
||||
/// typed twin of [`ReattachResultDto`] (PTY scrollback bytes) — here the
|
||||
/// scrollback is the ordered list of [`ReplyChunk`]s already streamed.
|
||||
/// scrollback** of a still-live structured session, replayed before the new
|
||||
/// reply stream is wired. Unlike a transport error, an empty successful result
|
||||
/// carries a typed reason so the UI can offer an appropriate recovery action.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReattachChatDto {
|
||||
@ -3936,6 +3957,32 @@ pub struct ReattachChatDto {
|
||||
/// replays them to rebuild the visible turns, then receives subsequent chunks
|
||||
/// over the freshly-registered channel.
|
||||
pub scrollback: Vec<ReplyChunk>,
|
||||
/// Structured reason for an empty scrollback. Absent when replayable chunks exist.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transcript_unavailable_reason: Option<ConversationTranscriptUnavailableReasonDto>,
|
||||
}
|
||||
|
||||
impl ReattachChatDto {
|
||||
/// Builds the additive read/open contract from runtime transcript metadata.
|
||||
#[must_use]
|
||||
pub fn from_scrollback(
|
||||
session_id: String,
|
||||
conversation_id: Option<String>,
|
||||
scrollback: Vec<ReplyChunk>,
|
||||
transcript_restore_expected: bool,
|
||||
) -> Self {
|
||||
let transcript_unavailable_reason =
|
||||
ConversationTranscriptUnavailableReasonDto::for_scrollback(
|
||||
scrollback.is_empty(),
|
||||
transcript_restore_expected,
|
||||
);
|
||||
Self {
|
||||
session_id,
|
||||
conversation_id,
|
||||
scrollback,
|
||||
transcript_unavailable_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `inspect_conversation` (T7).
|
||||
@ -5751,7 +5798,7 @@ mod ls6_pagination_tests {
|
||||
turns: vec![
|
||||
TurnView {
|
||||
id: tid(1),
|
||||
at_ms: 1_700_000_000_000,
|
||||
at_ms: Some(1_700_000_000_000),
|
||||
role: TurnRole::Prompt,
|
||||
source: TurnSource::Human,
|
||||
text: big.clone(),
|
||||
@ -5759,7 +5806,7 @@ mod ls6_pagination_tests {
|
||||
},
|
||||
TurnView {
|
||||
id: tid(2),
|
||||
at_ms: 1_700_000_000_001,
|
||||
at_ms: Some(1_700_000_000_001),
|
||||
role: TurnRole::Response,
|
||||
source: TurnSource::Agent {
|
||||
agent_id: domain::AgentId::from_uuid(uuid::Uuid::from_u128(7)),
|
||||
@ -5822,7 +5869,8 @@ mod ls6_pagination_tests {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use domain::{
|
||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState, TaskId,
|
||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskOrigin, BackgroundTaskResult,
|
||||
BackgroundTaskState, TaskId,
|
||||
};
|
||||
|
||||
/// One first-class background task as seen by the frontend (camelCase wire shape).
|
||||
@ -5846,6 +5894,8 @@ pub struct BackgroundTaskDto {
|
||||
/// Kind discriminant (`"command"`, `"headlessRendezvous"`, `"sessionResume"`,
|
||||
/// `"maintenance"`).
|
||||
pub kind: String,
|
||||
/// Manual launch or scheduled occurrence provenance.
|
||||
pub origin: BackgroundTaskOrigin,
|
||||
/// Lifecycle state (`"queued"`, `"running"`, `"waiting"`, `"completed"`,
|
||||
/// `"failed"`, `"cancelled"`, `"expired"`).
|
||||
pub state: String,
|
||||
@ -5955,6 +6005,7 @@ impl From<BackgroundTask> for BackgroundTaskDto {
|
||||
project_id: task.project_id.to_string(),
|
||||
parent_task_id: task.parent_task_id.map(|id| id.to_string()),
|
||||
kind: background_kind_label(&task.kind).to_owned(),
|
||||
origin: task.origin.clone(),
|
||||
state: background_state_label(task.state).to_owned(),
|
||||
exit_code,
|
||||
summary,
|
||||
@ -6000,6 +6051,55 @@ pub fn parse_task_id(raw: &str) -> Result<TaskId, ErrorDto> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a durable scheduled-task id.
|
||||
pub fn parse_scheduled_task_id(raw: &str) -> Result<domain::ScheduledTaskId, ErrorDto> {
|
||||
uuid::Uuid::parse_str(raw)
|
||||
.map(domain::ScheduledTaskId::from_uuid)
|
||||
.map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid scheduled task id: {raw}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Stable scheduled task response; domain serialization already uses camelCase.
|
||||
pub type ScheduledProjectTaskDto = domain::ScheduledProjectTask;
|
||||
|
||||
/// Create request shared by Tauri and HTTP.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateScheduledProjectTaskRequestDto {
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
pub command: domain::ScheduledTaskCommand,
|
||||
pub schedule: domain::ScheduledTaskSchedule,
|
||||
pub timezone: String,
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
/// Full replacement request with optimistic audit revision.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateScheduledProjectTaskRequestDto {
|
||||
pub project_id: String,
|
||||
pub scheduled_task_id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
pub command: domain::ScheduledTaskCommand,
|
||||
pub schedule: domain::ScheduledTaskSchedule,
|
||||
pub timezone: String,
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub timeout_ms: Option<u64>,
|
||||
pub audit_revision: u64,
|
||||
}
|
||||
|
||||
/// Request DTO for `spawn_background_command`.
|
||||
///
|
||||
/// Builds a command-backed [`BackgroundTask`]: the command line runs under the
|
||||
|
||||
Reference in New Issue
Block a user