Branche le SessionLimitService dans l'application Tauri et expose la
surface de reprise/annulation au front.
- application/agent/lifecycle.rs : LaunchAgentOutput.profile exposé
(None sur réattache/idempotent, Some sur lancement effectif).
- application/terminal/registry.rs : StructuredSessions::meta_for_session()
(lookup agent/node par SessionId pour le tap niveau 1).
- app-tauri/state.rs : ResumeContext(s), AppAgentResumer (impl du port
AgentResumer au-dessus de LaunchAgent), instanciation + câblage du
SessionLimitService (TokioScheduler + drain des réveils) dans AppState::build.
- app-tauri/commands.rs : taps niveau 1 (agent_send) et niveau 2
(launch_agent, parser regex confiné), alimentation de resume_contexts,
commande cancel_resume.
- app-tauri/lib.rs : enregistrement de cancel_resume dans le handler.
- app-tauri/Cargo.toml : dépendance async-trait.
Tests : session_limit_wiring.rs (2 tests de composition) + meta_for_session
dans structured_registry_d1.rs ; fixtures dto_agents/dto_chat ajustées
(profile: None). Tout vert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Étend l'enforcement OS au chemin structuré (sessions Claude/Codex mode JSON),
jusqu'ici seulement advisory. Approche validée par l'Architecte : transposer la
technique du PTY plutôt qu'un pre_exec (rejeté — landlock alloue, deadlock malloc
post-fork en process multithreadé).
Mécanique (cfg(target_os=linux)) : run_turn_sandboxed/drain_sandboxed exécutent
enforce(plan) sur un thread jetable AVANT le spawn std, puis std::process::spawn
depuis ce thread ; l'enfant hérite le domaine Landlock via les credentials de la
tâche (garanti à travers fork/clone/execve, y compris posix_spawn — pas de pre_exec
nécessaire, forbid(unsafe_code) préservé). Fail-closed sur Err d'enforce (aucun
child). Timeout sous sandbox : oneshot killer + tokio::time::timeout → kill → EOF →
reap (pas de zombie/thread bloqué). Chemin non-sandboxé (plan None / pas d'enforcer /
non-Linux) = drain async tokio inchangé.
Contrat : SpawnLine.sandbox ; AgentSessionFactory::start(.., sandbox) ;
StructuredSessionFactory::with_sandbox_enforcer (jumeau du PTY) ; plan calculé en
step 5d de lifecycle relayé à launch_structured ; default_enforcer() injecté au
composition root.
Tests : 7 invariants e2e (parité, companion négatif, fail-closed, no-op natif,
confinement de l'irréversibilité entre tours, timeout, resume préservé) — zéro
token (sh/FakeCli). 80 suites vertes, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Composition root : PortablePtyAdapter::new() injecte désormais default_enforcer()
(Landlock sur Linux / Noop ailleurs) via with_sandbox_enforcer, dans state.rs et
dans LocalHost (remote/mod.rs).
Launch path : LaunchAgent::execute peuple spec.sandbox via le plan pur
compile_sandbox_plan(effective_permissions, SandboxContext{project_root, run_dir}),
en réutilisant l'EffectivePermissions déjà résolu pour la projection advisory LP3.
Invariant produit conservé : eff == None ⇒ plan None ⇒ comportement natif, aucune
projection OS.
Portée : seul le chemin PTY brut est OS-enforcé ; le chemin structuré (Claude/Codex
mode JSON) porte le plan mais ne l'enforce pas encore (lot ultérieur).
Tests : ajout d'un test d'intégration bout-en-bout (pty/mod.rs, sandbox_e2e_tests)
prouvant qu'un enfant spawné sous un plan RW restreint ne peut PAS écrire hors-grant
(écriture bloquée par le kernel) alors que l'écriture dans le grant passe, plus un
test compagnon anti faux-positif (sans plan ⇒ pas de sandbox). Skip propre si le
kernel n'a pas Landlock. domain+infrastructure+application+app-tauri verts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fondations pures (zéro I/O, zéro dépendance landlock, aucun câblage
runtime — SpawnSpec.sandbox posé mais jamais lu ⇒ zéro régression) de la
voie « airtight » des permissions, complément de la voie projection LP3.
- domain/sandbox.rs : SandboxPlan/PathGrant/PathAccess (RO|RW|EXEC),
SandboxContext, SandboxKind/Status/Error, port SandboxEnforcer, et la
fonction pure compile_sandbox_plan(EffectivePermissions → plan OS).
- domain/permission.rs : render_permission_summary (bloc Markdown injecté
plus tard ; mentionne explicitement fichiers OS-enforced vs commandes
advisory).
- domain/ports.rs : SpawnSpec.sandbox: Option<SandboxPlan> (None ⇒ natif),
propagé à tous les sites de construction.
Sémantique de compile_sandbox_plan :
- Invariant produit : eff == None ⇒ None (rien posé ⇒ CLI 100 % native).
- Borne Landlock : seules les capabilities fichier produisent des grants
(Read→RO, Write/Delete→RW) ; ExecuteBash reste advisory (non verrouillable
par chemin).
- Deny-wins PAR CLASSE D'ACCÈS (RO/RW), fail-closed intra-classe : un Deny
ne ferme que les grants de sa propre classe (un Deny Write n'ampute pas un
Allow Read). Choix retenu pour maximiser l'autonomie des agents : on
respecte exactement la politique pré-renseignée sans sur-restreindre, donc
moins de blocages qui forceraient l'agent à redemander l'utilisateur.
- Globs réduits à leur préfixe statique ; grant abandonné si une barrière de
même classe chevauche (égal/ancêtre/descendant) — sous-approximation
conservatrice (un sandbox additif ne peut pas carver un deny sous-arbre).
Tests : 16 tests purs sur sandbox + 3 sur render_permission_summary,
cargo test --workspace 100 % vert, 0 ignored.
Reste LP4 : LP4-1 adapter LandlockSandbox + pre_exec PTY (fail-open+warning
sauf posture Deny), LP4-2 câblage application, LP4-3 composition root.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Régression introduite par 0f8ba38 : le re-câblage de `.with_structured(...)`
sur `OrchestratorService` (pour activer `drain_with_readiness`, readiness lot 1)
faisait emprunter à `ask_agent` la branche structurée `ensure_structured_session`,
qui ne peut JAMAIS aboutir : depuis le lot B-2 (« Option 1 Terminal + MCP »,
eca2ba9) la fabrique structurée est décâblée de `LaunchAgent`, donc aucun agent
n'a de session structurée — tous tournent en PTY brut et la délégation passe par
les outils MCP. Symptôme : `idea_ask_agent` échouait systématiquement avec
« aucune session structurée vivante après lancement » à chaque relance d'IdeA.
Non détecté par les tests : les e2e loopback câblent leur propre service
structuré complet, jamais le composition root réel (gap tests ≠ composition).
Fix : retirer le `.with_structured` du composition root pour que
`self.structured = None` et que `ask_agent` retombe sur le chemin PTY+MCP
fonctionnel (état pré-0f8ba38). `drain_with_readiness` reste dormant tant que
la voie structurée n'est pas réactivée au composition root.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sauvegarde de l'arbre de travail en cours (persistance P8, conversations
C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le
support de la délégation inter-agents pour les profils Codex.
Le round-trip inter-agent question/réponse est couvert sans tokens par
les tests loopback existants (state::mcp_e2e_loopback_tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Au lancement structuré, le resumable exposé par le moteur est rangé dans
providers.json sous (id de paire, provider) — best-effort, n'altère jamais le
lancement.
- domaine : StructuredAdapter::provider_key() ⇒ "claude"/"codex" (clé stable
par famille, lisible, indépendante de l'uuid d'instance)
- application : port ProviderSessionProvider (root par appel) +
with_provider_session_provider ; helper persist_provider_session dans
launch_structured (skip si provider/adapter/id moteur absents ou set KO)
- app-tauri : AppProviderSessionProvider (FsProviderSessionStore sur le root)
- tests : 6 cas (nominal claude/codex, no-op sans provider/sans id moteur,
best-effort, mapping provider_key) ; domain+application verts
Lecture du store pour --resume = P8c.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Au (re)lancement, l'agent retrouve sur quoi il travaillait : si la cellule a une
conversation_id avec un handoff, son objectif + résumé sont injectés en dernière
section du convention file (# Reprise de la conversation), best-effort et
provider-indépendant.
- application : port HandoffProvider (root par appel, comme MemoryRecall) ;
LaunchAgent.with_handoff_provider + resolve_handoff (parse uuid + load,
jamais bloquant) ; section rendue dans compose_convention_file après la
mémoire projet
- app-tauri : AppHandoffProvider (FsHandoffStore sur le root du projet) câblé
- tests : 4 purs (objectif présent/None/blanc/absent) + 5 intégration
(happy path, provider absent, conversation_id None, uuid invalide, sans
handoff ⇒ lancement OK sans section) ; application 311 + app-tauri 204 verts
Volet --resume/providers.json = P8 (swap cross-profile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ask_agent persiste désormais chaque paire dans son conversationId : tour Prompt
à l'enqueue, tour Response au succès — best-effort (un échec de persistance ne
dégrade jamais la délégation).
- application : port RecordTurnProvider (matérialise un RecordTurn sur le bon
project root — OrchestratorService est mono-instance multi-projets, le log est
par root) ; wither with_record_turn(provider, Clock) ; helper
record_turn_best_effort ; horodatage via domain::ports::Clock (pas d'horloge
infra dans application)
- app-tauri : AppRecordTurnProvider (Fs* sur le root du projet) câblé au
composition root avec le SystemClock partagé
- tests : 5 cas (paire Prompt→Response, fil A↔B vs User↔B, no-op sans provider,
ask Ok même si record échoue) ; orchestrator_service 40, application +
app-tauri verts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dernier kilomètre de l'orchestration native : une CLI MCP réellement lancée
joint le serveur MCP du projet et ses outils idea_* aboutissent au vrai dispatch.
- M5a endpoint loopback par projet (interprocess UDS/named pipe, source unique
mcp_endpoint, cleanup au close) — zéro port réseau, AppImage/SSH-safe.
- M5b sous-commande `idea mcp-server` : pont stdio↔loopback headless (avant init
Tauri), handshake {"project","requester"}, endpoint-absent borné, EOF propre.
- M5c McpServerHandle boucle accept + serve_peer par pair ; requester réel
propagé jusqu'à OrchestratorRequestProcessed (fin du "mcp" figé) ; isolation
des pairs ; terminaison propre.
- M5d apply_mcp_config écrit la déclaration réelle (current_exe + --endpoint
mcp_endpoint + --project simple-uuid + --requester) ; McpRuntime injecté comme
donnée (application ne dépend pas de app-tauri).
- M5e smoke e2e sur vrai loopback : list/ask inline, cible PTY → erreur typée,
JSON malformé → pas de panic, requester propagé.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stabilise le routage de `ask` avant le bind transport MCP (v5). Invariant
« 1 agent = 1 employé » durci ; un agent traite un tour à la fois.
- R0a garde LaunchAgent : lève AgentAlreadyRunning pour un lancement neuf
ciblant un agent déjà vivant sur un autre node (PTY + structuré) ; rebind
seulement même-node ou réattache explicite (conversation_id). Idem spawn_agent.
- R0b list_live_agents agrège PTY + structuré (LiveSessions) + dédup.
- R0c réconciliation des layouts.json à doublons à l'ouverture (host déterministe,
idempotent) — corrige « une cellule reset au retour d'onglet ».
- R0d UI : option agent désactivée si vivant ailleurs + « aller à la cellule »,
mapping AGENT_ALREADY_RUNNING.
- A0 sérialisation FIFO des tours par agent_id dans ask_agent (verrou tokio par
agent ; agents différents en parallèle ; timeout tour 300s, cap attente 600s).
Cadrage : .ideai/briefs/orchestration-v5-transport-bind-cadrage.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose l'orchestration IdeA comme serveur MCP par-dessus le même
OrchestratorService::dispatch, avec repli fichier .ideai/requests pour les
CLI sans MCP. v3 réduite à la surface MCP : la messagerie inter-agents et la
corrélation requête↔réponse étaient déjà résolues par §17 (send_blocking).
- M0 capacité MCP sur le profil (McpCapability/McpConfigStrategy/McpTransport)
- M1 injection conf MCP au LaunchAgent + prose adaptée selon la surface
- M2 serveur/adapter MCP (JSON-RPC 2.0 maison ; outils idea_*) + ListAgents
- M3 câblage par projet (registre mcp_servers jumeau du watcher)
- M4 observabilité UI : OrchestratorRequestProcessed.source = file|mcp + badge
Trois portes d'entrée (fichier, MCP, UI) → un seul dispatch ; aucun nouveau
port applicatif ; MCP confiné à l'adapter infra. Tous lots verts (cycle §3).
Cadrage : .ideai/briefs/orchestration-v3-cadrage.md ; ARCHITECTURE.md §14.3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comble le trou historique : un agent qui en interroge un autre reçoit enfin
le CONTENU de la réponse, plus seulement un ACK de cycle de vie.
- domain : OrchestratorCommand::AskAgent { target, task } + action wire
agent.message (target+task requis) ; DomainEvent::AgentReplied
{ agent_id, reply_len } (bus I/O-free, le contenu remonte par l'outcome).
- application : OrchestratorOutcome gagne reply: Option<String>. Méthode
ask_agent (§17.4) — cible structurée vivante ⇒ send_blocking direct ;
cible morte ⇒ LaunchAgent structuré puis send ; agent vivant en PTY ou
profil sans structured_adapter ⇒ Invalid (non adressable, jamais d'ACK
trompeur) ; agent inconnu ⇒ NotFound ; service non câblé ⇒ Invalid.
Timeout (300s) ⇒ erreur typée SANS tuer la session. Succès ⇒ publie
AgentReplied + reply: Some(content). Injection additive via builders
with_structured / with_events (call sites legacy intacts).
- app-tauri : state câble with_structured/with_events ; DTO AgentReplied.
Tests (QA) : +14 verts (11 app + 3 domaine), invariants validés par
mutation test (reply!=None, timeout ne shutdown pas). cargo test
--workspace : 817 passed, 0 failed.
Suivi (D6b) : surfacer reply dans le writer wire .response.json
(infrastructure/orchestrator) pour la délégation par protocole fichier.
Reste D7 : menu restreint Claude/Codex + retrait custom.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- LOT C2 (§14.5.3) : use cases de configuration des embedders déclaratifs
(List/Save/Delete + DescribeEmbedderEngines : modèles ONNX recommandés,
environnement local détecté, stratégies compilées). UI EmbedderSettings.
- LOT C3 (§14.5.5) : suggestion contextuelle best-effort à l'activation quand la
mémoire dépasse le budget de recall sans embedder configuré (event
EmbedderSuggested, anti-spam 1×/session, « ne plus demander »).
- Contexte projet partagé .ideai/CONTEXT.md (model-agnostic) injecté à tous les
agents/profils au lancement, avant la persona. UI ProjectContextPanel.
Tests : backend workspace vert (0 échec) ; frontend 306/306.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remplace les StubEmbedder pour les stratégies localServer/api/localOnnx par de
vrais moteurs, chacun derrière une feature cargo off-by-default — la posture
fondatrice « rien d'imposé, zéro dépendance » (défaut none → rappel naïf) reste
byte-for-byte inchangée.
C1a (feature vector-http, reqwest rustls optional):
- HttpEmbedder couvrant localServer (Ollama/llama.cpp) et api (OpenAI/Voyage…),
payload OpenAI-compatible /v1/embeddings, ordre restauré par index, bearer
token lu via env var (jamais en clair), timeout client 30s.
- detect_ollama() pour la détection de l'existant (C3).
C1b (feature vector-onnx, fastembed v5 optional):
- OnnxEmbedder en-process (e5-small, dim 384), init paresseuse + spawn_blocking,
cache modèle sous <app_data>/embedders/onnx — aucun download au build ni au
first-run, uniquement à la demande au 1er embed.
- Catalogue RECOMMENDED_ONNX_MODELS + ONNX_CACHE_SUBDIR + onnx_model_is_cached
exposés (sans feature) pour la config (C2) et la popup (C3).
embedder_from_profile(profile, onnx_cache_dir) dispatche feature-gated ; sans la
feature, retombe sur StubEmbedder (Unsupported) → fallback naïf via
AdaptiveMemoryRecall. Composition root (build_memory_recall) propage le cache dir.
Tests: 10 HTTP + 6 ONNX (dont 2 #[ignore] download réel) + 26 vectoriels, verts
en défaut, --features vector-http et --features vector-onnx.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pièce 1 (backend) : state.rs câble AdaptiveMemoryRecall via build_memory_recall,
piloté par le profil embedder chargé depuis embedder.json (fallback none). Défaut
none ⇒ NaiveMemoryRecall nu (comportement inchangé, StubEmbedder jamais touché,
zéro dépendance lourde). Instance de recall partagée (RecallMemory + LaunchAgent).
Chargement du profil isolé sur un runtime dédié (évite le block_on imbriqué).
Pièce 2 (frontend) : feature mémoire complète en miroir de skills —
MemoryGateway (port) + TauriMemoryGateway + MockMemoryGateway, types domaine
Memory/MemoryIndexEntry/MemoryType, MemoryPanel/MemoryEditor/useMemory, onglet
sidebar « Memory » dans ProjectsView. CRUD par slug, liens [[slug]] résolus.
Le sujet mémoire est clos : CRUD .md + index + rappel adaptatif + injection à
l'activation des agents + UI de gestion. Embedder concret ONNX/HTTP reste un
follow-up (défaut none = pleinement fonctionnel sans dépendance).
Tests: backend 57 binaires verts, frontend 285 tests verts, typecheck OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
À l'activation d'un agent, LaunchAgent compose désormais une section
« # Mémoire projet » dans le convention file généré (CLAUDE.md/AGENTS.md…),
au même titre que les skills assignés (§14.2). Les agents lisent ainsi la
mémoire projet sans aucun flag ni mécanisme propre à une CLI.
- LaunchAgent reçoit le port MemoryRecall (Arc<dyn MemoryRecall>), résout le
rappel (budget AGENT_MEMORY_RECALL_BUDGET=2048, requête = persona de l'agent)
en best-effort : mémoire vide/absente ou erreur ⇒ section omise, le launch
n'est jamais bloqué (comme un SkillRef dangling).
- compose_convention_file gagne un argument `memory: &[MemoryIndexEntry]`
(reste pure) ; section injectée seulement pour la stratégie conventionFile.
- state.rs : une seule instance NaiveMemoryRecall partagée (recall + LaunchAgent).
Tests: 57 binaires verts. compose_convention_file (vide ⇒ inchangé, ordre,
cohabitation skills) + intégration LaunchAgent (section présente / absente /
best-effort sur erreur / pas d'injection en stratégie env).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base de connaissance persistante par projet, indépendante de tout modèle/CLU
et de git. Cadrage archi en §14.5 (ARCHITECTURE.md), cycle Archi→Dev→Test.
LOT A — étage 1 (.md, source de vérité)
- domaine: entité Memory (+ MemorySlug, MemoryType, MemoryFrontmatter,
MemoryLink, MemoryIndexEntry), liens [[slug]], index MEMORY.md dérivé
- port MemoryStore + MemoryError, adapter FsMemoryStore (.ideai/memory/)
- application: 7 use cases (Create/Update/List/Get/Delete/ReadIndex/
ResolveLinks), From<MemoryError> for AppError
- app-tauri: commandes + DTO, events MemorySaved/MemoryDeleted
- suppression de la variante morte DomainError::MalformedFrontmatter
LOT B — rappel adaptatif (étage 1)
- port MemoryRecall + MemoryQuery, adapter NaiveMemoryRecall (troncature
au budget de tokens, court-circuit budget-0), use case RecallMemory
LOT C — étage 2 vectoriel (structure complète, zéro dépendance lourde)
- port Embedder + EmbedderError, profils déclaratifs EmbedderProfile/
EmbedderStrategy (embedder.json)
- VectorMemoryRecall (cosinus, cache .ideai/memory/.index/ gitignoré)
- AdaptiveMemoryRecall (bascule pure should_use_vector), défaut none
- HashEmbedder (déterministe, tests), StubEmbedder (onnx/server/api)
Tests: 57 binaires verts, build + clippy --workspace sans warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Permet de recharger la conversation CLI précédente de chaque cellule à la
réouverture du projet, de façon universelle (indépendant du modèle/CLI).
- profil AgentRuntime: bloc déclaratif optionnel `session { assignFlag, resumeFlag }`
- LeafCell: `conversationId` (persistant, distinct du SessionId PTY) + `agentWasRunning`
- runtime: SessionPlan (None/Assign/Resume) + composition pure des args
- LaunchAgent: décide Assign vs Resume, génère l'UUID, remonte l'id assigné
(persistance par l'appelant via setCellConversation — découplage SRP)
- close: SnapshotRunningAgents fige `agentWasRunning` avant le kill-all
(statut clot/en cours universel, sans parsing CLI)
- SessionInspector: port optionnel best-effort + adapter ClaudeTranscriptInspector
- popup de reprise par cellule (statut + sujet/tokens si dispo), intercalée
avant le Resume auto, jamais sur le chemin reattach
fix(terminals): sérialise les écritures PTY (file FIFO par handle) — corrige
les caractères mélangés/accents dus au réordonnancement des invoke Tauri concurrents
fix(layout): l'opération `move` préservait mal les champs du leaf (perdait `agent`)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- domain: OrchestratorCommand::CreateSkill with optional scope field
(defaults to Project, case-insensitive, UnknownScope on bad value)
- application: dispatch CreateSkill through the CreateSkill use case
- app-tauri: build OrchestratorService and start/stop per-project request
watchers on open/create/close_project (ensure/stop_orchestrator_watch)
- tests: domain validation, service dispatch, infra watcher e2e,
app-tauri wiring lifecycle (idempotent, isolated, stop)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>