---
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]].