--- name: background-tasks-first-class-design description: memory note background-tasks-first-class-design metadata: type: project --- --- name: background-tasks-first-class-design description: Cadrage hexagonal du modèle BackgroundTask + mailbox bornée par agent + wake owner après complétion post-tour. Fait autorité pour les lots B1-B7 / F1-F4. metadata: type: reference --- # Tâches de fond de 1re classe — CADRAGE FIGÉ (Architect, 2026-07-02) Base : `feature/background-tasks-first-class` (@ fccc1e2, empilée sur v2 `62915ee`+`fccc1e2`, au-dessus de develop `a9653bc`, ligne CLI/PTY « toujours headless »). ## Objectif (2 défauts à corriger) 1. **Complétion post-tour perdue** : une tâche de fond (ex. build `run_in_background`) finit après la fin du tour de l'agent → IdeA ne ré-invoque pas le propriétaire avec le résultat. 2. **Pas de mailbox** : message concurrent pendant working/waiting rejeté « still busy » au lieu d'être mis en file et drainé au tour suivant. ## Modèle `BackgroundTask` { task_id, owner_agent_id, project_id, kind, state, started/updated_at_ms, deadline_ms?, correlation, result, wake_policy }. - kind : `Command | HeadlessRendezvous | SessionResume | Maintenance` - state : `Queued | Running | Waiting | Completed | Failed | Cancelled | Expired` - result : `None | Success(payload) | Failure(error) | Cancelled(reason)` - wake_policy : `WakeOwner | RecordOnly` Règle centrale : la complétion n'est JAMAIS seulement un retour de future local ; elle est persistée/observée comme événement de tâche, puis transformée en message mailbox pour le propriétaire. ## Réutilisé vs nouveau Réutilisé : `InputMediator`/`AgentMailbox` (FIFO par agent), `TicketId`, `AgentBusyState`, `run_ask_with_watchdog`+plafond+liveness, `live-state.json` (projection maigre), `ReplyEvent::Final`/`Announcement`, session-limit scheduler (réveil différé annulable). Nouveau : `BackgroundTask` persistant, store/registry, completion sink durable, `AgentWakePort`, mailbox entrante bornée par agent (couvre user/agents/complétions système), reconcile au boot. ## Ports - `BackgroundTaskStore` : create/get/save/list_open_for_agent/list_undelivered_completions/ mark_completion_delivered. **B1 figé** (async_trait, `BackgroundTaskPortError`). - `BackgroundTaskRunner` : spawn(spec)->handle / cancel / subscribe_completions()->stream. **B1 figé.** - `AgentInbox` (façade au-dessus d'`InputMediator`) : enqueue_message / dequeue_next / snapshot. **Lot B4.** - `AgentWakePort` : wake_agent(project, agent, reason) — ne connaît PAS Tauri ; adapter lance/rattache session structured/headless. **Lot B5.** ## Contrats/DTO `InboxItem` { id, agent_id, source: Human|Agent{id}|BackgroundTask{task_id}|System, kind: UserMessage|AgentDelegation|BackgroundCompletion|ResumeNotice, body, created_at_ms, correlation_id?, priority (FIFO défaut, pas de priorité cachée V1) }. `BackgroundCompletionDto` camelCase { taskId, ownerAgentId, projectId, kind, status, exitCode, stdoutTail, stderrTail, finishedAtMs }. Events `DomainEvent` : BackgroundTaskStarted/Progress(borné)/Completed/Failed/Cancelled, AgentInboxQueued/Drained, AgentWakeScheduled/Started/Failed. (Events = observabilité/UI ; store+mailbox = autorité.) ## Flux nominal run_in_background 1 agent lance commande → 2 crée BackgroundTask{owner,Running} → 3 adapter démarre → 4 tour peut finir → 5 commande finit plus tard → 6 completion sink reçoit exit/stdout/stderr → 7 écrit Completed/Failed dans store → 8 enqueue InboxItem::BackgroundCompletion → 9 si owner Idle: AgentWakePort.wake_agent démarre nouveau tour headless avec résultat → 10 si Busy/Waiting: FIFO, drainé au prochain tour libre. **Idempotence : 1 seule completion livrée par task_id ; crash entre store et wake réparé par reconcile.** ## Mailbox bornée 1 par agent, FIFO stricte, capacité configurable (`IDEA_AGENT_INBOX_CAPACITY`, défaut 100). Enqueue pendant Working/Waiting → `queued` (plus jamais `busy`). Overflow : messages humains/agents → `InboxFull` typée ; complétions de tâches → persistées delivery_pending, JAMAIS perdues. « Busy » = tour en cours, pas entrée refusée. ## Lots backend - **B1** domaine : types, ports Store/Runner, events, invariants purs. ✅ FAIT (12 tests). (AgentInbox/AgentWakePort reportés à B4/B5.) - **B2** store+registry : adapter FS/sqlite-like simple, écriture atomique, reconcile boot (Running sans handle vivant → Unknown/Failed ou delivery_pending). - **B3** completion sink : runner publie complétions sur canal interne ; sink persiste AVANT tout wake ; tests idempotence double completion. - **B4** mailbox unifiée : étendre `InputMediator`/`AgentMailbox` (pas de 2e FIFO concurrente) ; enqueue_message, snapshots workstate ; remplacer refus « still busy » par `queued`. - **B5** wake owner : adapter `AgentWakePort` au-dessus de `StructuredSessions`/ `AgentSession::send` ; wake seulement si idle ; sinon launch/rattach headless ; résultat injecté comme message système explicite. - **B6** rendezvous-as-task : `idea_ask_agent` reste synchrone pour l'appelant mais son exécution interne = BackgroundTask{HeadlessRendezvous} ; timeouts/backstops → résultats de tâche, plus des états invisibles. - **B7** reconcile boot : lire store, ré-enqueue complétions non livrées, recalculer live-state. ## Lots frontend - **F1** workstate : queue depth par agent, « Queued » au lieu de « busy rejected ». - **F2** panel tâches de fond : liste par agent running/completed/failed ; cancel/open output/retry. - **F3** agent cell : badge « messages en attente » + « tâche de fond terminée » ; pas de transcript brut auto-injecté. - **F4** notifications : toast sobre à la fin d'une tâche longue ; clic ouvre owner/détail. ## Invariants completion persistée avant wake ; livrée au plus une fois ; aucun message vers agent connu rejeté pour Busy ; mailbox ne dépasse jamais capacité ; overflow ne perd jamais une completion système ; 1 item traité à la fois ; live-state = projection jamais autorité ; `Final` reste le seul terminal normal headless ; un redémarrage n'oublie pas les complétions persistées non drainées. ## Critères QA (backend) commande background finissant après le tour → owner réveillé avec exit code + résumé ; idem avec IdeA redémarré entre fin et wake → completion retrouvée ; message user à agent busy → `queued` ; deux agents vers même agent busy → FIFO ; mailbox pleine → `InboxFull` sur message normal, completion système en pending ; double event même task_id → 1 livraison ; annulation → `Cancelled`, pas de wake succès ; rendezvous silencieux → backstop = tâche Failed/NoReply, libère la queue ; session-limit resume continue et ne contourne pas la mailbox. Liens : [[checkpoint-b2-bootstrap-applied-await-codex-reset-1430]], [[rendezvous-no-reply-backstop-design]], [[session-limit-handling-design]], [[headless-interagent-conversation-objective]], [[inter-agent-live-context-shared-per-agent]].