Fédération multi-kernel

Au-delà d'un seul tenant, un seul kernel ne suffit plus. La fédération expose N kernels Nika OS par /goal, routés via un triage RAG sémantique et fédérés via Redis pub/sub.

Pourquoi un seul kernel ne suffit pas

Un kernel Nika OS bien réglé peut absorber un usage soutenu pendant plusieurs mois. Mais une fois passé le seuil mono-tenant — plusieurs périmètres métier distincts, plusieurs équipes, plusieurs jeux de doctrines — les limites intrinsèques d’une instance unique apparaissent :

  1. Saturation de la fenêtre de contexte — même avec handover à 41 %, un kernel partagé entre R&D, opérations client, et veille marchés publics accumule trop de signal hétérogène. La compression RAG ne suit plus.
  2. Conflits de doctrine — deux périmètres peuvent exiger des invariants contradictoires (ex. tolérance aux écritures externes en R&D vs. validation systématique sur un dossier client). Une seule politique kernel ne peut pas honorer les deux à la fois.
  3. Isolation des credentials — un kernel BCUB3-IT et un kernel client Pulsa ne doivent pas pouvoir lire les credentials l’un de l’autre, même par accident.
  4. Cycle de mutation découplé — les mutations GEPA utiles à un périmètre ne le sont pas forcément à un autre. Un tournament unifié dilue les signaux.

La réponse architecturale est de spawner N kernels avec chacun leur /goal, leurs primitives propres, et leur mémoire isolée — puis de les fédérer via une couche partagée.

Le pattern : un kernel par /goal

Chaque kernel est une instance Nika OS isolée :

ChampContenu propre au kernel
/goalMission stratégique distincte (string courte)
Espace de podstmux nika-os-{kernel_id} ou namespace K3s dédié
Memory MDRépertoire ~/.claude/projects/{kernel_id}/ séparé
settings.jsonSkills, hooks et permissions taillés sur mesure
Autonomie localeDécisions internes sans consulter les autres kernels

Quelques /goal typiques :

KernelPérimètre
kernel-alpha-bcub3-itOpérateur IT BCUB3 (référence quotidienne)
kernel-beta-bcub3-labR&D BCUB3 Lab — brevets KTW, sWELU, benchmarks
kernel-gamma-client-missionMissions client : mails, rapports, livrables
kernel-delta-iot-edgeContrôleur embarqué + données capteurs
kernel-epsilon-veille-aoVeille marchés publics et benchmarks sectoriels

Chaque kernel possède son propre tmux, son contexte, ses skills tailored. Aucun n’a accès direct au contexte de l’autre ; ils communiquent uniquement via la couche fédération.

Le triage RAG sémantique

Un prompt utilisateur arrivant sur la fédération doit être routé vers le bon kernel. C’est le rôle du triage layer :

  1. Le prompt est embedé (modèle bge-small ou équivalent, 384 dimensions).
  2. Une recherche cosine est lancée sur la collection nika_federation_goals (1 chunk = 1 kernel /goal + ses tags sémantiques).
  3. Le top-1 est sélectionné si son score dépasse un seuil de confiance.
  4. Sinon, le prompt est routé au kernel généraliste par défaut, qui peut demander une clarification ou créer un nouveau kernel via la primitive kernel_spawn.

Le triage est rapide (< 100 ms) et déterministe sur des prompts non ambigus. Pour les prompts hybrides (ex. “génère un plan technique ISO pour le client X”), le triage peut produire un split en plusieurs sous-prompts adressés à plusieurs kernels.

Fédération via Redis pub/sub

Les kernels échangent des signaux faibles via un bus partagé Redis :

ChannelDirectionUsage
nika:federation:directivestout kernelSignal kernel A → kernel B : voici un événement qui te concerne
nika:federation:work_stealingtout kernelAnnonce qu’un kernel a de la capacité libre et peut prendre du travail en attente
nika:federation:healthtout kernelHeartbeat + métriques agrégées (TTFL, error_rate, KTW Y)
nika:federation:goalstriageMise à jour du registre des /goal actifs

Aucun kernel ne partage sa fenêtre de contexte directement avec un autre. Le pub/sub transporte uniquement des signaux (références d’entité, IDs de tâche, scores, alertes). Pour transférer du contenu, on passe par le RAG partagé.

Le RAG partagé et la couche de triage

Le store Qdrant nika_vault est partagé entre tous les kernels, avec un champ kernel_id filtré par défaut dans chaque requête. Un kernel ne lit pas la mémoire d’un autre par défaut. Mais le triage maintient deux collections complémentaires :

  • nika_federation_summaries — un résumé compact par kernel et par jour (1 chunk / kernel / 24h). Permet à un kernel de comprendre ce que font les autres sans lire leurs transcripts.
  • nika_federation_goals — la base de connaissance qui pilote le triage. Chaque entrée décrit un kernel par son /goal + ses domaines + ses exemples de prompts canoniques.

Le triage continu déduplique (cosine sim > 0.95 = drop), résume (chunks fréquents → super-chunk), et hiérarchise (strategy → project → job → task → atomic) pour garder le RAG actionnable.

Work-stealing entre kernels

Quand un kernel A est silencieux (pas de prompt en cours, file vide), il peut souscrire à nika:federation:work_stealing et proposer ses ressources :

  1. Kernel B publie un signal task_available avec un score de priorité.
  2. Kernel A (capacité libre) répond task_claim avec son score d’aptitude (calculé sur la similarité de son /goal avec la tâche).
  3. Si A est le seul candidat et que sa similarité dépasse un seuil, il prend la tâche.
  4. Sinon, B garde la tâche dans sa propre file.

Ce pattern permet d’absorber les pics de charge sans avoir à pré-allouer de la capacité sur chaque kernel.

Architecture en diagramme

flowchart TB
    U["Prompt utilisateur"] --> T["Triage RAG<br/>cosine sur /goal"]
    T -->|"match BCUB3-IT"| KA["Kernel Alpha<br/>BCUB3-IT"]
    T -->|"match BCUB3-LAB"| KB["Kernel Beta<br/>BCUB3-LAB"]
    T -->|"match client"| KC["Kernel Gamma<br/>Client mission"]

    KA <-->|"directives + signaux"| BUS[("Redis pub/sub<br/>nika:federation:*")]
    KB <-->|"directives + signaux"| BUS
    KC <-->|"directives + signaux"| BUS

    KA -->|"read/write scopé"| RAG[("Qdrant nika_vault<br/>+ federation_summaries")]
    KB -->|"read/write scopé"| RAG
    KC -->|"read/write scopé"| RAG

    T --> RAG

    classDef prompt fill:#F5F1E8,color:#2C3E42,stroke:#7DB5A5,stroke-width:2px;
    classDef triage fill:#E99971,color:#FDFBF8,stroke:#C97A55,stroke-width:2px;
    classDef kernel fill:#7DB5A5,color:#FDFBF8,stroke:#5E9384,stroke-width:2px;
    classDef bus fill:#2C3E42,color:#F5F1E8,stroke:#1A262A;
    classDef store fill:#F5F1E8,color:#2C3E42,stroke:#A86640;

    class U prompt;
    class T triage;
    class KA,KB,KC kernel;
    class BUS bus;
    class RAG store;

Les flèches pleines portent du contenu (prompts, embeddings, vecteurs). Les flèches pointillées portent des signaux faibles (alertes, scores, références).

Auto-handover via session_registry

Quand un kernel atteint son seuil de contexte (typiquement 41 % par défaut), il déclenche un handover automatique vers un kernel successeur :

  1. Le kernel courant sérialise son état essentiel dans nika_federation_summaries (résumé) + session_registry (table des sessions actives ↔ entités).
  2. Un kernel successeur est spawné avec /goal identique et l’entité parente injectée au boot.
  3. Le session_id du nouveau kernel est lié à l’entity_id racine via session_registry, ce qui permet aux pods enfants de retrouver leur lignée sans lire les transcripts.

Cette mécanique est décrite plus en détail dans la doctrine session ↔ hierarchy — le registry est le point de vérité.

Conditions de spawn d’un nouveau kernel

Quatre déclencheurs algorithmiques peuvent provoquer la création d’un kernel supplémentaire :

TriggerMesureSeuil par défaut
Saturation contexteMoyenne du % contexte utilisé sur 24h> 70 %
Diversité topiqueNombre de clusters disjoints dans le RAG du kernel> 5
Conflit de doctrineDétection de deux doctrines mutuellement contradictoires activesbinaire
Demande expliciteCommande utilisateur /kernel new <goal>

Aucun de ces triggers ne spawne un kernel sans validation humaine explicite — le coût (RAM, embeddings, mutation tournaments) est élevé et mérite un arbitrage.

Lifecycle : cinq verbes

Comme pour les pods, un kernel suit cinq verbes :

spawn       → kernel_spawn(goal, scope, resource_limits)
invoke      → kernel_invoke(kernel_id, prompt)
readjust    → kernel_readjust(kernel_id, new_directive)
observe     → kernel_observe(kernel_id, since, query?)
kill        → kernel_kill(kernel_id, reason)

readjust permet d’injecter une directive en cours de session sans tuer ni respawn le kernel — l’équivalent d’un patch sur le contexte. Le verbe est utile quand le périmètre métier glisse légèrement sans changer le /goal.

Quand orchestrer plusieurs kernels en parallèle

Trois cas d’usage canoniques :

  • Burst de charge — ingestion massive (centaines de PDF, vidéos, data sets). Spawn d’un kernel worker temporaire le temps du burst, puis decommission.
  • Diversification de doctrine — un kernel R&D peut tolérer l’exploration agressive, un kernel client exige la validation systématique. Les séparer permet d’optimiser chacun pour son métier.
  • Région data — un kernel hébergé en France pour les données client sensibles, un autre en zone neutre pour la veille publique. Le triage route selon la nature du prompt.

Coût et bottlenecks

Chaque kernel a un coût fixe (RAM ~512 Mo – 2 Go selon les skills chargées, CPU pendant les bursts d’embedding) et un coût variable (tokens API ou GPU à la décision). Les bottlenecks observés sur un VPS mutualisé :

  • RAM — limite primaire. Sur un VPS 29 Go, 14 à 58 kernels parallèles selon l’enveloppe par kernel.
  • CPU — surtout sur les bursts Qdrant et les hooks Python lourds.
  • Network — rate limits provider (Anthropic, OpenRouter, Mistral).
  • Disk — croissance du Qdrant. La rétention TTL 24h sur nika:pod_streams mitige.

L’isolation par pod K3s ou par namespace permet de poser des limits durs (cgroups Linux) et d’éviter qu’un kernel sature la machine entière.

Voir aussi

  • Kernel et pods — le contrat de spawn d’un pod s’applique aussi à un kernel.
  • IPC et bus — Redis Streams, consumer groups, JSONL.
  • Observabilité et contrôleurs — métriques agrégées cross-kernel via nika:federation:health.
  • Doctrines — antifragilité, kernel/pod split, agnosticisme multi-CLI s’appliquent à la fédération.