feat(background): livraison auto au propriétaire + projection des tâches de fond dans le work-state (T1+T3)

T1 — Wake automatique du propriétaire à la complétion.
La complétion d'une tâche de fond est désormais livrée à l'agent
propriétaire dès que la session accepte l'envoi (wake.rs :
mark_completion_delivered au send accepté), via un drain de flux dédié
(structured.rs : drain_reply_stream_with_readiness). L'inbox médiée
enfile l'item sans démarrer de tour ni marquer l'agent busy
(input/mod.rs : enqueue FIFO silencieux). Régression couverte
(tests/agent_wake.rs, tests input/mod.rs).

T3 — Tâches de fond projetées dans le read-model du panneau Work.
AgentWorkState porte désormais background_tasks
(VO AgentBackgroundTaskState), alimenté par un builder best-effort
with_background_tasks(store) : union list_open_for_agent + dispatch des
completions non livrées par owner_agent_id, erreur store => Vec vide
(aucune régression live/busy/tickets). DTO Tauri backgroundTasks et
wiring du BackgroundTaskStore côté state.rs. Le frontend, déjà câblé,
affiche Cancel/Retry (mapping queued/waiting -> pending, tri sur
updatedAtMs). Borne V1 : une tâche terminale déjà livrée n'est plus
énumérable (Retry limité à la fenêtre non livrée).

Tests : cargo build --workspace OK ; cargo test -p application /
-p app-tauri / -p infrastructure verts ; frontend build + vitest verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:21:33 +02:00
parent f08dae62eb
commit eb9cc16181
21 changed files with 946 additions and 148 deletions

View File

@ -503,6 +503,48 @@ impl AppReconcileBackgroundTasks {
}
}
fn schedule_background_ready_retry(
ready_tx: tokio::sync::mpsc::UnboundedSender<BackgroundTaskReadyToDeliver>,
ready: BackgroundTaskReadyToDeliver,
) {
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let _ = ready_tx.send(ready);
});
}
fn schedule_background_wake_retry(
wake: Arc<dyn AgentWakePort>,
project: Project,
owner_agent_id: AgentId,
task_id: TaskId,
) {
tauri::async_runtime::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
match wake
.wake_agent(
&project,
owner_agent_id,
WakeReason::BackgroundCompletion { task_id },
)
.await
{
Ok(()) => break,
Err(WakeError::AgentBusy { .. }) => continue,
Err(err) => {
application::diag!(
"[background-task] completion wake retry failed: task={} owner={} err={err}",
task_id,
owner_agent_id
);
break;
}
}
}
});
}
struct AppWakeSessionProvider {
launch_agent: Arc<LaunchAgent>,
structured_sessions: Arc<StructuredSessions>,
@ -1690,6 +1732,7 @@ impl AppState {
let wake = Arc::clone(&background_wake);
let projects = Arc::clone(&store_port);
let clock_for_items = Arc::clone(&clock) as Arc<dyn Clock>;
let retry_ready = background_ready_tx.clone();
tauri::async_runtime::spawn(async move {
while let Some(ready) = background_ready_rx.recv().await {
let item = InboxItem {
@ -1712,6 +1755,7 @@ impl AppState {
ready.owner_agent_id,
receipt.depth
);
schedule_background_ready_retry(retry_ready.clone(), ready);
continue;
}
Ok(_) => {}
@ -1722,6 +1766,7 @@ impl AppState {
ready.task_id,
ready.owner_agent_id
);
schedule_background_ready_retry(retry_ready.clone(), ready);
continue;
}
Err(err) => {
@ -1758,6 +1803,20 @@ impl AppState {
)
.await
{
if matches!(err, WakeError::AgentBusy { .. }) {
application::diag!(
"[background-task] completion wake postponed: task={} owner={}",
ready.task_id,
ready.owner_agent_id
);
schedule_background_wake_retry(
Arc::clone(&wake),
project,
ready.owner_agent_id,
ready.task_id,
);
continue;
}
application::diag!(
"[background-task] completion wake failed: task={} owner={} err={err}",
ready.task_id,
@ -1801,7 +1860,8 @@ impl AppState {
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>,
Arc::new(AppConversationLogProvider)
as Arc<dyn application::ConversationLogProvider>,
),
)
.with_background_tasks(Arc::clone(&background_tasks_port)),
);
// 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