Tests LS4 — couche application de la feature « limites de session » : `SessionLimitService` (crates/application/src/agent/session_limit.rs) + réconciliation T4 dans crates/application/src/agent/structured.rs. Branche feature/agent-session-limits. DevBackend a livré (cargo build -p application OK, 0 warning ; tests compilent). Écris et exécute les tests, avec des FAKES des ports (Clock fixe, Scheduler, EventBus, AgentResumer), dans le style des tests applicatifs existants (regarde comment structured.rs et les autres use cases sont testés, réutilise les fakes existants si présents).

SessionLimitService — (a) détection→planification :
- `on_rate_limited(agent, node, conv_id, Some(reset_futur))` → le fake Scheduler reçoit exactement 1 `arm(fire_at_ms, ScheduledTask::ResumeAgent{agent_id, node_id, conversation_id})` avec fire_at_ms cohérent (= resets_at, ou now si passé via plan_resume) ; le fake EventBus reçoit `AgentRateLimited{agent_id, resets_at_ms}` PUIS `AgentResumeScheduled{agent_id, fire_at_ms}` DANS CET ORDRE ;
- `on_rate_limited(..., None)` → AUCUN arm ; events `AgentRateLimited{None}` puis `AgentRateLimitSuspected{None}` ;
- dédoublonnage (§21.10-4) : deux `on_rate_limited` successifs pour le MÊME agent → l'ancien ScheduleId est cancel-é sur le Scheduler (sans émettre d'AgentResumeCancelled pour le dédoublonnage interne).

(b) exécution de la reprise :
- `execute_resume(ScheduledTask::ResumeAgent{...})` → le fake AgentResumer voit `resume(agent_id, node_id, conversation_id, RESUME_PROMPT)` (vérifie que le prompt passé == la const RESUME_PROMPT) ; EventBus reçoit `AgentResumed{agent_id}` ; l'entrée interne est retirée (un cancel_resume ultérieur → false) ;
- AgentResumer qui retourne Err → l'erreur est propagée ET `AgentResumed` n'est PAS publié.

(c) annulation :
- `cancel_resume(agent)` après un `on_rate_limited` armé, fake Scheduler renvoyant true → retourne true, EventBus reçoit `AgentResumeCancelled{agent_id}` ;
- `cancel_resume(agent)` sans armement préalable → false, aucun event ;
- fake Scheduler renvoyant false (cas « déjà tiré ») → cancel_resume renvoie false ET n'émet PAS AgentResumeCancelled (la reprise suit son cours) — c'est le contrat anti-course important.

Réconciliation T4 (drain_with_readiness_outcome / drain_to_final) :
- flux `[RateLimited{Some(t)}]` sans Final → `Ok(TurnOutcome::RateLimited{Some(t)})` (fin gracieuse, PAS d'Err) ;
- flux `[RateLimited{None}]` sans Final → `Ok(TurnOutcome::RateLimited{None})` ;
- flux `[..., RateLimited, Final]` → `Ok(TurnOutcome::Completed(contenu))` (Final l'emporte) ;
- flux `[TextDelta]` seul (ni Final ni RateLimited) → `Err(Io)` INCHANGÉ (non-régression critique : un vrai flux tronqué reste une erreur) ;
- NON-RÉGRESSION : `send_blocking` et `drain_with_readiness` (signatures historiques) gardent leur comportement (RateLimited→Err(Io) côté send_blocking ; flux sans Final → Io). Vérifie que les tests existants (drain_with_readiness_lot1, send_blocking_d1, drain_marks_alive) restent verts.

Exécute `cargo test -p application`. Rends : rapport complet (nb tests, pass/fail), confirmation explicite que les tests applicatifs préexistants restent verts (zéro régression), et en cas d'échec un rapport CLAIR (attendu vs obtenu) pour DevBackend. Ne modifie QUE des tests ; signale tout vrai bug sans corriger la prod.


✅ LS4 — Tests couche application « limites de session » : TOUT VERT, zéro régression.

Commande : `cargo test -p application`
Résultat : **tous les binaires de test verts, 0 failed** sur l'ensemble du crate. 16 nouveaux tests LS4 ajoutés, répartis en 2 fichiers d'intégration (style des tests applicatifs existants, 100 % fakes des ports).

Confirmation explicite NON-RÉGRESSION : les suites préexistantes citées restent vertes —
- `drain_with_readiness_lot1.rs` : **7/7** ✓
- `send_blocking_d1.rs` : **9/9** ✓
- (et tout le reste du crate : project/agent_lifecycle/orchestrator/structured_launch/etc., 0 échec).

=== Nouveau fichier `tests/session_limit_service.rs` (9 tests) — fakes Clock fixe / Scheduler enregistreur-contrôlable / EventBus espion / AgentResumer espion-contrôlable ===
(a) détection→planification :
- `on_rate_limited_future_arms_and_emits_in_order` : Some(reset futur) ⇒ EXACTEMENT 1 `arm(fire_at_ms==reset, ResumeAgent{agent,node,conv})` + events `AgentRateLimited` PUIS `AgentResumeScheduled` dans cet ordre.
- `on_rate_limited_past_reset_clamps_fire_at_to_now` : reset passé ⇒ `fire_at_ms==now` (clamp anti-passé) ; l'event RateLimited garde l'heure brute passée, ResumeScheduled porte le now clampé.
- `on_rate_limited_without_reset_is_human_fallback_no_arm` : None ⇒ AUCUN arm + events `AgentRateLimited{None}` puis `AgentRateLimitSuspected{None}`.
- `on_rate_limited_twice_same_agent_dedups_cancelling_previous` : 2 signaux même agent ⇒ l'ancien ScheduleId est cancel-é avant réarmement, et AUCUN `AgentResumeCancelled` émis (dédoublonnage interne silencieux, §21.10-4).
(b) exécution :
- `execute_resume_calls_resumer_with_prompt_and_emits_resumed` : resume(agent,node,conv, prompt==RESUME_PROMPT vérifié) + `AgentResumed` publié + entrée retirée (cancel_resume ultérieur ⇒ false).
- `execute_resume_propagates_error_without_emitting_resumed` : Resumer Err ⇒ erreur propagée ET `AgentResumed` NON publié.
(c) annulation :
- `cancel_resume_after_arm_returns_true_and_emits_cancelled` : cancel renvoyant true ⇒ true + bon ScheduleId passé + `AgentResumeCancelled` publié.
- `cancel_resume_without_arm_is_false_no_event` : sans armement ⇒ false, Scheduler non sollicité, aucun event.
- `cancel_resume_when_scheduler_already_fired_is_false_no_event` : Scheduler renvoyant false (« déjà tiré ») ⇒ false ET PAS d'`AgentResumeCancelled` (contrat anti-course important).

=== Nouveau fichier `tests/session_limit_t4.rs` (7 tests) — réconciliation §21.2-T4, fakes AgentSession + InputMediator ===
- `outcome_rate_limited_some_without_final_is_graceful` : `[RateLimited{Some(t)}]` ⇒ `Ok(TurnOutcome::RateLimited{Some(t)})` (PAS d'Err) + vérifie qu'un RateLimited ne marque PAS Idle.
- `outcome_rate_limited_none_without_final_is_graceful` : `[RateLimited{None}]` ⇒ `Ok(RateLimited{None})`.
- `outcome_rate_limited_then_final_is_completed` : `[.., RateLimited, Final]` ⇒ `Completed(contenu)` (Final l'emporte) + Idle marqué.
- `outcome_truncated_stream_without_final_or_ratelimit_is_io_error` : `[TextDelta]` seul ⇒ `Err(Io)` INCHANGÉ (non-régression critique du flux tronqué).
- non-régression signatures historiques : `drain_with_readiness_rate_limited_is_io_error`, `send_blocking_rate_limited_is_io_error` (limite ⇒ Io), `drain_with_readiness_nominal_still_completes` (cas nominal Final intact).

Aucun bug détecté : le code DevBackend est conforme au cadrage §21.5 sur tous les axes (ordre des events, clamp via plan_resume, dédoublonnage silencieux, prompt constant, propagation d'erreur sans event, contrat anti-course du cancel). Je n'ai modifié AUCUNE ligne de prod (uniquement ajouté 2 fichiers de tests).

Feature LS4 validée côté tests application. Prêt pour la suite du cycle (Git : merge éventuel).


Tests LS5 — parser regex niveau 2 (infra) : `RateLimitParser` (crates/infrastructure/src/ratelimit/mod.rs) + le module d'heure partagé `timeparse` (crates/infrastructure/src/timeparse.rs). Branche feature/agent-session-limits. DevBackend a livré (cargo build -p infrastructure OK, 0 warning ; tests LS2 toujours verts). Tout est pur (now_ms injecté), testable sans I/O. Écris et exécute les tests dans le style existant.

RateLimitParser (new + detect + applies) :
- `new` sur regex INVALIDE → None (jamais de panique) ;
- pattern qui matche SANS reset_capture → `Some(SessionLimit{ resets_at_ms: None, source: Pattern, detected_at_ms == now_ms })` ;
- capture nommée `(?P<reset>...)` + time_format ABSOLU : `epoch_s` (secondes→×1000), `epoch_ms` (tel quel), `iso8601`/`rfc3339` (`...Z` → ms attendus) → resets_at_ms corrects ;
- time_format RELATIF (`relative_s`, capture « 600 », now=T) → `Some(resets_at_ms == T + 600_000)` ;
- time_format MURAL (`wall`, capture « 3pm ») : now correspondant à 10h du jour → 15h AUJOURD'HUI (même jour UTC) ; now correspondant à 16h → 15h DEMAIN (passage de minuit, +24h). Choisis des now_ms calculés proprement (epoch connu) et calcule l'attendu à la main ;
- pattern NE matche PAS → detect → None ;
- pattern matche mais capture absente/valeur pourrie/non parsable → `Some(SessionLimit{ resets_at_ms: None })` (détection utile sans heure) ;
- vérifie que `source == RateLimitSource::Pattern` dans tous les cas détectés ;
- compilation du regex faite une seule fois (à new) — au minimum vérifie que detect peut être appelé plusieurs fois sans souci.

applies(profile) :
- profil avec structured_adapter (structuré) → false (même s'il a un rate_limit_pattern) ;
- profil PTY (sans structured_adapter) AVEC rate_limit_pattern → true ;
- profil PTY SANS rate_limit_pattern → false.

timeparse (fonctions réexportées) :
- `days_from_civil` : 1970-01-01 → 0 ; une année bissextile (2024-02-29) cohérente ;
- `parse_wall_clock` : « 3pm » → (15,0,0) ; « 15:00:30 » → (15,0,30) ; format invalide → None ;
- `wall_clock_to_ms` : même jour vs lendemain selon now ;
- `parse_absolute_ms` : epoch s/ms/ISO cohérents (recoupe avec LS2 si utile).
- NON-RÉGRESSION : confirme que les tests LS2 (parse_reset_ms/parse_event dans session/mod.rs) restent verts après le refactor qui a déplacé les helpers vers timeparse.

Exécute `cargo test -p infrastructure`. Rends : rapport complet (nb tests, pass/fail), confirmation explicite que LS2/LS3 restent verts, et en cas d'échec rapport CLAIR (attendu vs obtenu) pour DevBackend.

EN PLUS (petit nettoyage que DevBackend t'a signalé) : tes tests scheduler LS3 (crates/infrastructure/src/scheduler/mod.rs, ~lignes 253/285) déclenchent 2 warnings `irrefutable if let` car ScheduledTask est mono-variante aujourd'hui. Corrige ces 2 warnings dans le code de TEST (ex. déstructuration directe au lieu de `if let`) pour garder la suite sans warning — c'est du test, tu peux le modifier. Ne touche à aucun code de prod ; signale tout vrai bug sans le corriger.


✅ LS5 — Tests parser regex niveau 2 (`RateLimitParser`) + module `timeparse` : TOUT VERT, 0 warning, zéro régression.

Commande : `cargo test -p infrastructure`
Résultat lib : **221 passed; 0 failed; 0 ignored** (195 antérieurs + **26 nouveaux LS5**). Tous les fichiers d'intégration + doctests verts. **Build de test : 0 warning** (vérifié `--no-run | grep -c warning` = 0).

Confirmation explicite NON-RÉGRESSION :
- **LS2** (`session::tests::parse_reset_ms*` / `parse_event*` / `send_emits_rate*`) : tous verts APRÈS le refactor qui a déplacé les helpers vers `timeparse` ✓
- **LS3** (`scheduler::tests::*`) : 7/7 verts ✓ (+ les 2 warnings `irrefutable if let` corrigés — voir plus bas)

=== `ratelimit/mod.rs` — 15 tests (in-file `#[cfg(test)] mod tests`) ===
new + detect :
- `new_returns_none_on_invalid_regex` : regex invalide `"rate limit ("` ⇒ None (jamais de panique).
- `detect_returns_none_when_pattern_does_not_match` : pas de match ⇒ None.
- `detect_match_without_reset_capture_has_no_time` : match sans reset_capture ⇒ `SessionLimit{resets_at_ms:None, source:Pattern, detected_at_ms==now}`.
- formats ABSOLUS : `detect_epoch_seconds_format` (×1000), `detect_epoch_millis_format` (tel quel), `detect_iso8601_format` (`2023-11-14T22:13:20Z`→1_700_000_000_000).
- format RELATIF : `detect_relative_seconds_format_uses_now` (capture « 600 », now=T ⇒ T+600_000).
- format MURAL (passage de minuit, math calculée à la main sur DAY_START=1_699_920_000_000 = 2023-11-14T00:00Z) : `detect_wall_clock_same_day_when_future` (now=10h, « 3pm » ⇒ 15h même jour) ; `detect_wall_clock_next_day_when_past` (now=16h ⇒ 15h DEMAIN, +24h).
- capture inexploitable ⇒ détection sans heure : `detect_match_with_missing_capture_group_has_no_time`, `detect_match_with_unparsable_value_has_no_time` (⇒ `resets_at_ms:None`).
- `detect_can_be_called_multiple_times` : regex compilé une seule fois, detect appelé 3× sans souci.
- `source == RateLimitSource::Pattern` asserté dans tous les cas détectés.
applies(profile) :
- `applies_false_for_structured_profile_even_with_pattern` (structuré ⇒ false, niveau 1 prend la main) ;
- `applies_true_for_pty_profile_with_pattern` (PTY + pattern ⇒ true) ;
- `applies_false_for_pty_profile_without_pattern` (PTY sans pattern ⇒ false).

=== `timeparse.rs` — 11 tests ===
- `days_from_civil_epoch_is_zero` (1970-01-01→0) ; `days_from_civil_leap_day_2024` (2024-02-29→19782, + cohérence avec le 1ᵉʳ mars).
- `parse_wall_clock_variants` (« 3pm »→(15,0,0), « 15:00:30 »→(15,0,30), « 3:30 pm »→(15,30,0), 12am→0h, 12pm→12h) ; `parse_wall_clock_rejects_invalid` (texte, 25:00, 10:75, trop de composantes ⇒ None).
- `wall_clock_to_ms_*` : même jour / lendemain / cas-limite « cible == now » (⇒ lendemain, jamais le présent).
- `parse_absolute_ms_*` : epoch s/ms, ISO8601, garbage→None (recoupe LS2).
- `parse_rfc3339_offsets_convert_to_utc` : +01:00 et −01:00 ⇒ même instant UTC.

=== Nettoyage LS3 demandé ===
Les 2 warnings `irrefutable if let` (scheduler/mod.rs ~253/285, `ScheduledTask` mono-variante) sont corrigés : remplacés par une **déstructuration directe** `let ScheduledTask::ResumeAgent { conversation_id, .. } = task;` (code de TEST uniquement). Suite désormais 0 warning.

Aucun bug détecté : le code DevBackend est conforme au cadrage §21 niveau 2 sur tous les axes (robustesse regex, stratégies de format absolu/relatif/mural, passage de minuit UTC, règle anti-double-détection `applies`, factorisation `timeparse` sans casser LS2). Je n'ai modifié AUCUN code de prod (uniquement ajouté des tests + corrigé mes propres tests LS3).

Feature LS5 validée côté tests infra. Prêt pour la suite du cycle.


Tâche QA — tests unitaires de la slice LS7-front (UI limites de session des agents), branche `feature/agent-session-limits`. DevFrontend vient d'implémenter (typecheck `tsc --noEmit` propre, 39 tests agents existants toujours verts, non commité).

Périmètre à couvrir (frontend/) :
1. `src/features/agents/useAgents.ts` — nouvel état `limitByAgent: Record<string, AgentLimitState>` (`{ limitedUntil?, resumeFireAt?, suspected? }`) peuplé depuis 5 DOMAIN_EVENT dans l'abonnement `onDomainEvent`. À tester (via le mock gateway qui émet des events) :
   - `agentRateLimited` {agentId, resetsAtMs} → entrée `{ limitedUntil: resetsAtMs, suspected: false }`.
   - `agentResumeScheduled` {agentId, fireAtMs} → arme `resumeFireAt`.
   - `agentResumeCancelled` {agentId} → retire `resumeFireAt`, reste limité.
   - `agentResumed` {agentId} → supprime l'entrée (état effacé).
   - `agentRateLimitSuspected` {agentId, resetsAtMs?} → `{ ..., suspected: true }`, y compris le cas SANS `resetsAtMs` (heure inconnue).
   - séquence réaliste : rateLimited → resumeScheduled → cancelResume (action) → vérifier retrait optimiste + appel `input.cancelResume` (mock `cancelledResumes` / `cancelResumeResult`).
   - Action `cancelResume(agentId)` exposée par le hook : retrait optimiste + verdict backend renvoyé (teste les deux verdicts via `cancelResumeResult`).
2. `src/features/agents/AgentLimitBadge.tsx` — helpers purs exportés `formatResetTime(epochMs)` (→ HH:MM) et `formatCountdown(remainingMs)` (→ « Xm Ys »/« Ys », clampé à 0 pour négatif). Teste ces helpers purs directement (cas limites : 0, négatif, < 60 s, > 60 s, minuit). Teste le rendu du badge (limité jusqu'à HH:MM vs « limité » sans heure vs « heure inconnue » pour suspected sans resetsAtMs) et que le bouton « Annuler la reprise » appelle l'action quand `resumeFireAt` est armé.
3. Adapter mock `src/adapters/mock/index.ts` — `MockInputGateway.cancelResume` (`cancelledResumes`, `cancelResumeResult`) : couvre via les tests du hook.

Suis le style des tests existants (cf. `singletonAgent.test.tsx`, `mock.test.ts`, les tests agents). Exécute la suite (`npm test` ou l'outil du repo) et RENDS-MOI via idea_reply : la commande exacte lancée, le résultat réel (nb tests passés/échoués), et le rapport d'erreurs complet si rouge. Si un test révèle un bug d'implémentation, décris-le précisément (fichier:ligne, attendu vs obtenu) — je relaierai à DevFrontend pour correction. Ne commit rien.


QA LS7-front — VERT. Aucun bug d'implémentation trouvé. Rien commité.

## Fichiers de test ajoutés (2)
- `frontend/src/features/agents/useAgentsLimits.test.tsx` (13 tests) — état `limitByAgent` du hook via le `MockSystemGateway` qui émet les events + `MockInputGateway`.
- `frontend/src/features/agents/AgentLimitBadge.test.tsx` (11 tests) — helpers purs + rendu du badge.

## Commandes exactes lancées (cwd = frontend/)
1. `npx vitest run src/features/agents/useAgentsLimits.test.tsx src/features/agents/AgentLimitBadge.test.tsx`
   → **Test Files 2 passed (2) · Tests 24 passed (24)**
2. `npx vitest run src/features/agents/` (suite agents complète, régression)
   → **Test Files 4 passed (4) · Tests 63 passed (63)** (39 existants + 24 nouveaux, 0 régression)

## Couverture livrée
useAgents (§21), tous via events réels :
- `agentRateLimited{resetsAtMs}` → `{limitedUntil, resumeFireAt:undefined, suspected:false}` ✓
- `agentResumeScheduled{fireAtMs}` → arme `resumeFireAt` par-dessus l'état limité ✓
- `agentResumeCancelled` → retire `resumeFireAt`, reste limité ✓ ; + no-op sur agent inconnu (aucune entrée créée) ✓
- `agentResumed` → entrée supprimée (`agentId in map === false`) ✓
- `agentRateLimitSuspected` AVEC et SANS `resetsAtMs` → `suspected:true`, `limitedUntil` undefined dans le cas sans heure ✓
- séquence réaliste rateLimited→scheduled→`cancelResume(action)` : retrait optimiste du countdown + agent toujours limité + `input.cancelledResumes === [agentId]` + verdict ✓
- action `cancelResume` : verdict `true` ET `false` (via `cancelResumeResult`), + appel du port même sans reprise armée ✓
- deux agents foldés indépendamment ✓

AgentLimitBadge :
- `formatCountdown` : 0→"0s", négatif→"0s" (clamp), <60s→"Xs", ceil partiel (4.2s→"5s"), ≥60s→"Xm Ys" (60000→"1m 0s", 125000→"2m 5s") ✓
- `formatResetTime` : HH:MM sans secondes (même minute +30s = même libellé), minuit stable ✓
- rendu : "limité jusqu'à HH:MM" (heure connue) / "limité" (sans heure) / note "heure inconnue — reprise à préciser" (suspected sans heure) + absence de la note quand suspected A une heure ✓
- bouton "Annuler la reprise" : appelle `onCancelResume` au clic quand `resumeFireAt` armé, countdown rendu, bouton désactivé si `busy` ✓

MockInputGateway.cancelResume (`cancelledResumes`/`cancelResumeResult`) : couvert via les tests du hook.

## Note hors-périmètre (PRÉEXISTANTE, pas causée par LS7-front)
`src/adapters/mock/mock.test.ts:16` échoue (1 test) : il assert « thirteen gateways » mais le `permission` gateway porte le compte à 14. Vérifié : la gateway `permission` existe déjà dans HEAD committé (eca2ba9) — l'assertion de comptage est simplement périmée, sans rapport avec les limites de session. Mes deux fichiers ne touchent pas ce test. Aucune action de ma part (hors scope) ; à relayer pour une mise à jour du compteur si tu veux la suite `adapters/mock` 100% verte.


Tâche QA — tests unitaires LS8-backend (filet humain niveau 3), branche `feature/agent-session-limits`. DevBackend a implémenté (build 0 erreur/0 warning, suites domain/application/app-tauri déjà vertes, non commité).

À COUVRIR — nouvelle méthode `SessionLimitService::confirm_human_resume(agent_id, node_id, conversation_id, resets_at_ms: i64)` dans `crates/application/src/agent/session_limit.rs`. Teste via les fakes existants (Clock/Scheduler/EventBus) du fichier `crates/application/tests/session_limit_service.rs` (suis ce style). Cas (cadrage Architect) :
(a) heure FUTURE ⇒ publie `AgentRateLimited{Some(resets_at_ms)}` puis `AgentResumeScheduled{fire_at_ms == resets_at_ms}` + un ScheduleId armé sur le scheduler.
(b) heure PASSÉE (resets_at_ms < now) ⇒ clamp : `fire_at_ms == now` (reprise quasi-immédiate). Vérifie l'événement `AgentResumeScheduled` avec fire_at_ms == now.
(c) DÉDOUBLONNAGE croisé : `confirm_human_resume` APRÈS un `on_rate_limited` déjà armé pour le même agent ⇒ le précédent réveil est désarmé, une seule entrée armée (et l'inverse : `on_rate_limited` après `confirm_human_resume`). Vérifie qu'il n'y a qu'un seul armement actif.
(d) ANNULABILITÉ : `cancel_resume(agent_id)` après `confirm_human_resume` ⇒ publie `AgentResumeCancelled` et renvoie true (l'armement humain s'annule par la même voie).
(e) PARITÉ auto/humain : même séquence d'événements qu'`on_rate_limited` cas Scheduled (à reset identique).

À COUVRIR — commande `set_resume_at` (app-tauri) si testable au niveau intégration comme `session_limit_wiring.rs` : au minimum le cas `node_for_agent == None` ⇒ erreur NOT_FOUND (pas d'armement orphelin). Si le câblage rend ce test trop lourd (besoin d'une cellule vivante), note-le et concentre-toi sur la couverture application (a–e) qui est l'essentiel.

Exécute (`cargo test -p application` + `-p app-tauri` si pertinent). RENDS-MOI via idea_reply : commande exacte, résultat réel (passés/échoués), rapport d'erreurs complet si rouge. Si un test révèle un bug (notamment sur le dédoublonnage croisé ou le clamp passé), décris-le précisément (attendu vs obtenu) — je relaierai à DevBackend. Ne commit rien.


QA LS8-backend (filet humain niveau 3) — VERT. Aucun bug d'implémentation trouvé (ni sur le dédoublonnage croisé, ni sur le clamp passé). Rien commité.

## Fichiers de test modifiés (2 — ajouts only, fakes existants réutilisés)
- `crates/application/tests/session_limit_service.rs` : +6 tests `confirm_human_resume` (style des fakes Clock/Scheduler/EventBus existants).
- `crates/app-tauri/tests/session_limit_wiring.rs` : +2 tests (précondition NOT_FOUND + parité runtime humaine sur le vrai bus).

## Commandes exactes lancées + résultats réels
1. `cargo test -p application --test session_limit_service`
   → **15 passed; 0 failed** (10 existants + 5 nouveaux).
2. `cargo test -p app-tauri --test session_limit_wiring`
   → **4 passed; 0 failed** (2 existants + 2 nouveaux).
3. `cargo test -p application` (régression complète) → **toutes suites vertes, 0 failed**.
4. `cargo test -p app-tauri` (régression complète) → **toutes suites vertes, 0 failed**.

## Couverture application (a–e) — l'essentiel
- **(a) heure FUTURE** `confirm_human_resume_future_arms_and_emits_in_order` : exactement 1 `arm(fire_at_ms==resets_at_ms, ResumeAgent{agent,node,conv})`, 1 ScheduleId émis, events `AgentRateLimited{Some(reset)}` PUIS `AgentResumeScheduled{fire_at_ms==reset}` dans cet ordre. ✓
- **(b) heure PASSÉE** `confirm_human_resume_past_reset_clamps_fire_at_to_now` : `fire_at_ms == now` (clamp anti-passé) ; `AgentRateLimited` garde l'heure brute passée, `AgentResumeScheduled` porte le `now` clampé. ✓
- **(c) dédoublonnage CROISÉ** (les deux sens) :
  - `confirm_human_resume_after_auto_dedups_single_active_arm` : confirm APRÈS on_rate_limited → l'ancien ScheduleId auto est cancel-é (`cancels == [issued[0]]`), pas d'`AgentResumeCancelled` (silencieux). Unicité prouvée : 1er `cancel_resume`→true, 2e→false. ✓
  - `auto_after_confirm_human_resume_dedups_single_active_arm` : on_rate_limited APRÈS confirm → symétrique, même invariant d'unicité. ✓
- **(d) ANNULABILITÉ** `cancel_resume_after_confirm_human_resume_returns_true_and_emits_cancelled` : renvoie `true` + publie `AgentResumeCancelled` (même voie que l'auto). ✓
- **(e) PARITÉ auto/humain** `confirm_human_resume_is_event_for_event_identical_to_auto_scheduled` : à reset futur identique, `bus.events()` ET `scheduler.armed()` strictement égaux entre `confirm_human_resume` et `on_rate_limited`. La source Human vs Structured n'a aucun effet observable. ✓

## Couverture app-tauri `set_resume_at`
- **NOT_FOUND** `set_resume_at_resolves_no_cell_for_an_agent_without_a_live_session` : ✓ couvert au niveau précondition. NOTE : la commande `#[tauri::command] set_resume_at` exige `State<AppState>`, non constructible hors runtime Tauri — le harnais wiring existant n'invoque jamais les commandes, seulement le service. J'ai donc testé la **précondition exacte** qui produit le NOT_FOUND : sur un `AppState::build` neuf, `structured_sessions.node_for_agent(unknown)` ET `terminal_sessions.node_for_agent(unknown)` renvoient `None` → la branche `ok_or_else(NotFound)` (commands.rs:1420-1428) est prise → aucun armement orphelin. C'est la couverture maximale réaliste sans faire tourner Tauri.
- **Parité runtime** `confirm_human_resume_arms_a_cancellable_resume_over_the_real_bus` : sur le vrai `TokioBroadcastEventBus`, `confirm_human_resume` publie `AgentRateLimited` puis `AgentResumeScheduled` et l'armement est annulable (`cancel_resume`→true), exactement comme la branche auto déjà testée. ✓

## Observation (non bloquante, pas un bug)
`confirm_human_resume` est total et défensif : le cas `ResumePlan::HumanFallback` y est inatteignable (`resets_at_ms` toujours `Some`) → traité en no-op via `if let Scheduled`. Conforme au cadrage Architect ; pas de test dédié (branche morte par construction).

Build app-tauri OK (0 erreur). Rien n'a été commité.


Tâche QA — validation LS8-front (formulaire de saisie d'heure, filet humain niveau 3) sur `feature/agent-session-limits`. DevFrontend a implémenté ET ajusté/ajouté des tests (typecheck propre, `npx vitest run src/features/agents/ src/adapters/mock/` = 109 tests verts). Ton rôle : valider la couverture, exécuter, et signaler tout trou ou bug.

Points à vérifier/exécuter :
1. Helper pur `timeInputToEpochMs(value, now)` (AgentLimitBadge.tsx) : cas couverts ? « HH:MM » valide → aujourd'hui à cette heure ; instant passé dans la journée → epoch < now (le backend clampe, pas le front) ; malformé/vide/`25:00`/`12:60` → `null`. Ajoute les cas manquants si besoin.
2. Soumission du formulaire (état suspected SANS heure) ⇒ `onSetResumeAt` / action hook `setResumeAt` appelée avec l'epoch attendu ; bouton désactivé si saisie vide ou `busy` ; saisie malformée (`null`) ne déclenche PAS d'appel.
3. Action hook `useAgents.setResumeAt(agentId, resetsAtMs)` : délègue bien au port `input.setResumeAt`, aucune mutation optimiste (vérifie que l'état ne bascule QUE sur réception de `agentResumeScheduled`, pas avant). Mock instrumenté `resumeArmings`.
4. Bascule complète : agent suspected sans heure → submit form → émettre `agentResumeScheduled` (via mock) → le badge passe à l'état nominal (countdown + bouton Annuler), le formulaire disparaît.
5. Non-régression : tout `src/features/agents/` + `src/adapters/mock/` vert. Confirme le compteur gateways corrigé (14) dans mock.test.ts.

Exécute `npx vitest run src/features/agents/ src/adapters/mock/` (+ `tsc --noEmit` si tu veux). RENDS-MOI via idea_reply : commande exacte, résultat réel (passés/échoués), trous comblés (fichiers/tests ajoutés), et tout bug trouvé (attendu vs obtenu, fichier:ligne) — je relaierai à DevFrontend. Ne commit rien.