--- name: checkpoint-b8-command-runner-loop-closed description: memory note checkpoint-b8-command-runner-loop-closed metadata: type: project --- # B8 — Runner de commandes + boucle sink fermée (DevBackend, 2026-07-03) Base `feature/background-tasks-first-class`. Suite de [[b8-command-runner-pty-framing]]. **Build workspace vert** ; sink tests (6) + tail units (5) + reconcile B7 verts. **Non committé** (Git tranche). QA à suivre. ## Livré (sous-tâches 1-5) - **Domaine** : nouveau port `BackgroundCommandArchive { spec_for(TaskId)->Option }` (ports.rs, exporté lib). Ajouté car le retry a besoin de la commande d'origine, qui n'est PAS un champ persisté du `BackgroundTask`. **À faire bénir par Architect** (ajout hors port figé). - **Infra** : `background_task.rs` → module dir (`sink.rs` déplacé verbatim, `tail.rs`, `runner.rs`). - `tail.rs` : `BoundedTail` ring UTF-8-safe + `bounded_tail()` + `tail_cap_bytes()` (env `IDEA_BG_TAIL_BYTES`, défaut 8 KiB, clamp [1 KiB, domaine 16 KiB]). - `runner.rs` : `CommandBackgroundRunner` compose `Arc` + `Arc`. `spawn` → pty.spawn(24x80) + worker détaché ; détection de fin = EOF du stream single-consumer ; tail lu du **scrollback** (pas du stream, robuste à la course subscribe) ; exit code via `kill()` (wait après EOF naturel) ; `cancel` = flag+Notify (la branche cancel gagne la course EOF) ; deadline→Expired via select. Implémente `BackgroundTaskRunner` + `BackgroundCommandArchive`. Émet 1 complétion/tâche, n'écrit ni store ni inbox. - **Application** : module `background` : `SpawnBackgroundCommand` (alloue TaskId, create Queued→save Running, runner.spawn ; si spawn KO → task Failed best-effort), `CancelBackgroundTask`, `RetryBackgroundTask` (nouveau TaskId, deadline **droppée** pour éviter l'expiry instantané ; réutilise `create_and_run`). - **Composition root** (state.rs ~1545) : runner + `BackgroundCompletionSink::new(store, ready_tx.clone())` + `sink.start_from_runner(runner)` DANS `tauri::async_runtime::spawn` (Handle::current requis) ; JoinHandle du drain gardé vivant via `.await`. 4 champs AppState + use cases câblés. **Boucle B2 fermée** (avant : sink/runner uniquement en tests). - **app-tauri** : `BackgroundTaskDto` camelCase (aplati result→exitCode/summary/tails), `parse_task_id`, commandes `spawn_background_command` (AJOUTÉE hors liste framing pour rendre la boucle atteignable/testable), `cancel_background_task`, `retry_background_task`, `list_background_tasks` — enregistrées dans invoke_handler. ## Écarts au cadrage (à arbitrer) 1. **Tee PTY live (sous-tâche 6) NON câblé** : le `Broadcast` de `PortablePtyAdapter` est **single-consumer** (chaque `subscribe_output` supersede le précédent). Un tee UI re-souscrirait et **casserait la détection EOF du runner**. Un vrai tee live exige un broadcast multi-consommateur OU un port `wait/try_wait` sur PtyPort → **décision Architect** (PtyPort figé). Documenté dans `runner.rs`. 2. **stderr_tail toujours None** : le PTY fusionne stdout/stderr → sortie fusionnée dans `stdout_tail`. 3. Port `BackgroundCommandArchive` ajouté (voir ci-dessus). 4. `list_background_tasks` sans `agentId` ne renvoie que les complétions non livrées du projet (le store ne sait pas énumérer les tâches ouvertes par projet).