Système mémoire

Trois niveaux de mémoire, cinq couches de stockage. La séparation stricte des rôles est ce qui empêche un système agentique long-running de s'effondrer sous son propre poids.

Pourquoi trois niveaux

Un agent qui n’a qu’une seule mémoire — sa fenêtre de contexte — ne peut pas tenir une conversation au-delà de quelques heures. Un agent qui dispose d’une seule mémoire externe (un vector store, par exemple) ne sait pas distinguer ce qui s’est passé maintenant de ce qu’on s’était dit la semaine dernière.

Nika OS pose trois niveaux pour cette raison :

  1. Working Memory — la fenêtre de contexte de l’instance actuelle. Limitée (~200 kB de tokens). Compactée automatiquement à 61 %.
  2. Mémoire épisodique — l’historique opérationnel : sessions, jobs, tournois, décisions, événements de pods. Stockée en Qdrant + JSONL.
  3. Mémoire sémantique — la connaissance générale : documents ingérés, patterns, snippets de code, articles, réponses à des questions clientes. Stockée en Qdrant (nika_vault, 413 000 + points).

Les cinq couches de stockage

CoucheStoreContenuBon pourMauvais pour
SemanticQdrantRésumés, décisions, docs, patterns, préférencesRappel sémantique, similaritéÉtat exact, locks, ownership
Workflowhierarchy.py + YAMLStatut entités, parent/child, deadlines, assignationSource de vérité d’exécutionRecherche sémantique
IPCRedis Streams + HashesEntity feed, working memory, signaling, contratsCommunication temps réel inter-podHistorique long terme
TransactionalRedisSémaphore, heartbeat, métriques, TTLÉtat temps réel, concurrenceHistorique long terme
CommsBus JSONLMessages inter-agents, review requests, dispatchesAudit trail, coordinationRecherche complexe

La règle d’or : le RAG sémantique n’est jamais la source de vérité pour l’état d’exécution. Qdrant rappelle ce qui a été. La hiérarchie YAML dit ce qui est maintenant. Redis IPC porte ce qui se passe à l’instant T.

Mélanger ces rôles produit des bugs subtils : un pod qui croit qu’un job est terminé parce que Qdrant a un résumé, alors que le YAML dit toujours in_progress, va prendre les mauvaises décisions.

L’enveloppe NIKA_META

Chaque point Qdrant, chaque message bus, chaque chunk de mémoire porte une enveloppe NIKA_META standardisée. Cette enveloppe répond aux questions : qui ? où ? quand ? quoi ? pourquoi ? comment lié à quoi ?

GroupeChampsExemples
Identity (WHO)session_id, tmux, agent_type, triggersession_id=abc, agent=alpha, trigger=user
Hierarchy (WHERE)project_id, job_id, task_idPROJ-NIKA-CORE, JOB-HOOKS-V3, TASK-xxx
Temporal (WHEN)timestamp, duration_s2026-05-22T10:48:25Z, 120
Classification (WHAT/WHY)action_type, domain, intentcode, hooks, feature
Graph (LINKS)entity_ids[], files[], tools[], parent_id, produces[][JOB-xxx], [on_stop.py], [Edit, Bash]

Les valeurs sont contraintes par des énumérations contrôlées :

  • action_type : code, research, debug, deploy, config, review, doc, system, comms
  • domain : hooks, infra, rag, hierarchy, mcp, ui, finance, browser, agent, memory
  • intent : feature, fix, refactor, optimize, explore, maintain, migrate, test
  • trigger : user, cron, autonomous, hook, pod, daemon, system

Cette discipline d’enveloppe permet trois choses :

  1. Filtrer un RAG search par projet, domaine, ou type d’action.
  2. Reconstruire un graphe d’événements liés (fichier édité par tel pod, qui découle de tel job).
  3. Auditer rétrospectivement pourquoi le système a pris une décision.

Compaction automatique

Nika OS compacte automatiquement la fenêtre de contexte à 61 % de remplissage (seuil arbitraire choisi pour préserver la marge de manœuvre). La compaction déclenche le hook PreCompact, qui produit un handoff packet :

PreCompact handoff packet
├── Décisions prises (avec timestamps)
├── Fichiers modifiés (avec lignes/diff résumés)
├── Tâches pendantes (avec contexte minimum pour reprise)
├── État RAG (queries récentes, résultats)
└── Lien vers les hooks lifecycle déclenchés

Ce packet est ingéré en Qdrant. Le pod redémarre avec un contexte propre, et peut rapatrier le packet par un seul RAG search au boot.

PALACE PROTOCOL : le hard-gate factuel

Avant de répondre à une question factuelle sur un client, un projet, une décision passée, ou une entité mémorisée, le système doit d’abord interroger le RAG (nika_rag_search ou qdrant-find).

Si le retrieval retourne zéro hit pertinent, la réponse correcte est : « aucun souvenir en RAG pour {X} ». Pas une fabrication. Pas une approximation. Pas une réponse « générale ».

Cette règle s’appelle PALACE PROTOCOL, en référence au projet mempalace (le système de mémoire le mieux noté sur les benchmarks publics, libre sous MIT). Le principe sous-jacent : la RAG est la seule source de vérité factuelle sur le passé.

Auto-memory (compatible l’agent CLI, autres CLI au cas par cas)

En complément du RAG, le pod Alpha exploite la fonctionnalité native auto-memory native (l’agent CLI aujourd’hui, autres CLI selon support) : un répertoire persistant ~/.claude/projects/-home-nika-vault/memory/ qui contient :

  • un fichier index MEMORY.md (toujours chargé, max 200 lignes) ;
  • des fichiers topic-spécifiques (one-line description en frontmatter).

Les entrées y sont sauvegardées quand l’utilisateur donne un feedback non trivial, partage un projet en cours, ou corrige une approche. Le tri se fait sur quatre types : user, feedback, project, reference. Le système ne sauvegarde pas ce qui peut être dérivé du code actuel ou de git log.

Dette technique de la mémoire — hyperparamètres à traquer

À mesure que Nika OS accumule des connaissances, la mémoire peut dégrader la qualité du raisonnement au lieu de l’aider. Quand le « second cerveau » (RAG + working memory + audit JSONL) n’arrive plus à suivre, c’est de la dette technique mémoire — observable, mesurable, et corrigeable.

Symptômes

  • Le RAG retourne des chunks hors-sujet par rapport au projet / job en cours (mauvais classement de similarité, doublons, anciennes décisions désormais contradictoires).
  • La compaction du contexte est déclenchée plus tôt qu’attendu, signe de bruit accumulé.
  • Le temps de réponse moyen augmente alors que la charge ne bouge pas.
  • Les pods spawnés produisent des livrables qui re-traitent des problèmes déjà résolus dans l’historique.

Hyperparamètres tracables

HyperparamDéfinitionSeuil d’alerteAction automatique
rag_offtopic_rate% de chunks RAG retournés non liés au scope projet / job / tâche en cours (mesuré par cosine drift)> 30 % sur 50 requêtesRe-tagging du payload Qdrant + archive contradictoires
qdrant_growth_ratePoints ajoutés / jour vs supprimés> 10 000 points/jour netTrigger compaction GEPA + déduplication cosine > 0.92
context_compaction_ageDélai moyen entre 2 compactions du contexte< 30 minDiagnostic working memory bruit → ingester en RAG + trim
duplicate_rateDoublons sémantiques (cosine > 0.95) dans nika_vault> 5 % du corpusCleanup cron + alerte fleet
staleness_scoreÂge moyen des chunks retournés par les top-5 dernières requêtes> 90 jours sur queries opérationnellesBoost recency dans le scoring
cross_project_leakage% de chunks d’un autre projet remontés sur une query d’un projet donné> 15 %Renforcer les filtres scope / project_id dans la requête
judge_disagreementÉcart entre 2 verdicts LLM-as-judge sur le même output> 30 % désaccordAudit prompt de jugement + soumettre au tournoi GEPA
audit_log_growth_rateTaille du bus JSONL ajoutée / jour> 100 MB/jourArchivage S3 + rotation logs locaux
pod_ttfl_p9595e percentile du time-to-first-log d’un pod> 30 sDiagnostic spawn primitive (race condition Enter)

Doctrine de purge

Pas de purge sans remplacement. Avant d’archiver un chunk :

  1. Indexer la nouvelle version (si remplacement) avant de supprimer l’ancien (évite l’effet « trou » dans une requête en cours).
  2. Vérifier qu’aucune session active ne référence le chunk via son ID (consulter nika:rag:active_refs Redis).
  3. Logguer la purge dans le bus JSONL avec raison + timestamp + ID remplaçant éventuel — la dette technique mémoire est elle-même tracée.

Automatisation

Le cron vault/scripts/cron/memory_debt_audit.py (à étendre) calcule ces hyperparamètres toutes les 6 heures. Au-delà d’un seuil critique, il :

  • Ouvre une review request sur le bus JSONL avec un rapport structuré.
  • Bloque les ingestions massives temporairement pour éviter d’aggraver le bruit.
  • Notifie l’opérateur via WhatsApp si l’incident dépasse 24 heures.

Cette discipline est cohérente avec la doctrine d’antifragilité : on préfère une mémoire petite et propre à une mémoire grosse et bruyante. Une dette mémoire ignorée dégrade silencieusement toutes les décisions du système.

Compression vers « Nika OS en prod »

À mesure que la dette technique mémoire s’accumule (même bien gérée), il arrive un moment où le système gagne plus à redémarrer frais qu’à continuer à porter sa propre histoire. Nika OS prévoit deux stratégies complémentaires pour ce passage à l’échelle suivant.

Stratégie A — Compression dans le repo (le kernel devient lisible)

L’idée : transformer l’état accumulé dans Qdrant + bus JSONL + memory MD en artefacts versionnés dans le repo Nika OS, puis redémarrer un kernel neuf qui retrouvera son contexte par RAG sur le repo lui-même.

Le processus :

  1. Snapshot Qdrant — export complet de nika_vault + nika_multimodal en format réutilisable (JSONL + binaires).
  2. Synthèse hiérarchique — méta-pod déterministe résume chaque niveau (strategy → project → job → task → atomic) en condensés vectorisables.
  3. Décomposition par thème — clustering sémantique sur l’historique pour générer un dossier par thème (/knowledge/{topic}/{date}.md) avec metadata structurée (entity_type, scope, recency, confidence).
  4. Publication versionnée — push dans le repo nika-os ou un sub-repo public selon la sensibilité du contenu.
  5. Re-ingestion fraîche — au boot du kernel suivant, ingestion du repo compressé dans un Qdrant vierge. Le système retrouve son passé par recherche, pas par accumulation.

L’effet : on découple la mémoire opérationnelle (Qdrant en cours d’usage) de la mémoire archivée (repo lisible par un humain et par n’importe quel nouveau kernel). On peut wiper l’opérationnel sans perdre la connaissance.

Stratégie B — Scaling horizontal multi-VPS

Si la charge croît plus vite que la dette mémoire, l’autre voie est de scaler en parallèle. Le wizard d’install inclut une option auto-scale multi-cloud avec clés API pour les fournisseurs courants :

ProviderUsagePricing typique
ScalewayVPS EU souverain, GPU L4/H100À l’heure ou mensuel
ContaboVPS haute RAM/CPU à faible coûtMensuel
Microsoft AzureCompute + ML, Active Directory tenantÀ la minute
Google CloudVertex AI, compute, TPUÀ la seconde
AWSEC2 spot, SageMakerÀ l’heure
RunPodGPU spot à la minute, serverlessTrès flexible

Le scheduler Nika détecte la saturation (CPU > 80 % sur 10 min, RAM > 75 %, queue Redis > N tâches) et spawne automatiquement un VPS worker via l’API du provider configuré. Le nouveau VPS rejoint le mesh Tailscale, télécharge l’image Nika OS, et commence à drainer la queue. Quand la charge redescend, les workers excédentaires se decommissionnent.

Combinaison des deux : Stratégie A pour la qualité (mémoire toujours fraîche), Stratégie B pour la capacité (compute à la demande). Les deux s’alimentent : un kernel qui scale horizontalement génère plus de données, donc plus de dette mémoire, donc plus de raisons de compresser régulièrement.

Indicateurs déclencheurs

IndicateurStratégie suggéréeSeuil
judge_disagreement élevéCompression A (mémoire incohérente)> 30 % sur 50 outputs
cross_project_leakage élevéCompression A (cloisonnement perdu)> 15 %
qdrant_growth_rate > 10 000/jour soutenuCompression A (purge)7 jours consécutifs
pod_ttfl_p95 > 30 sScaling B (compute saturé)sur 30 min
redis_queue_depth > 50Scaling B (worker spawn)sur 10 min
vps_load_avg > 0.85 × coresScaling B + Compression Asur 15 min
disk_usage_qdrant > 80 %Compression A urgenteatteint

Ces indicateurs sont remontés par le cron memory_debt_audit.py et par le watcher tour_de_terrain.py. Les actions automatiques restent opt-in : le wizard d’install demande à l’utilisateur s’il veut que Nika OS prenne ces décisions de scale tout seul, ou qu’il escalade chaque fois.

Optimisation récursive des hyperparams — la poupée russe complète

L’optimisation dans Nika OS n’est pas plate. Chaque niveau optimise le niveau d’en-dessous, et chaque niveau a ses propres hyperparamètres qui peuvent eux-mêmes être optimisés. C’est l’expression mathématique de la récursion fractale.

Les 4 niveaux observables aujourd’hui

NiveauQuoi optimiserAvec quel optimiseur
0 — Action atomiqueTool call → résultatLLM lui-même (raisonne, choisit l’outil)
1 — Skill / promptPrompt formulationGEPA (mutation + tournoi multi-objectifs)
2 — RAG retrievalHyperparams Qdrant searchMoE / KTW / petit MLP / XGBoost / DOE
3 — Meta-optimiseurQuel optimiseur utiliser pour 2Autoresearch + plan d’expérience
4 — Architecture optimiseurDesign du modèle (couches, neurones, ratio data/params)Neural Architecture Search + Bayesian opt

Niveau 4 — l’architecture est elle-même un hyperparamètre

Quand on choisit un MLP 3 couches × 64 neurones pour apprendre à mapper (query_features → optimal_RAG_hyperparams), ce choix repose sur des hypothèses :

  • Combien de paramètres total ? Trop = surapprentissage, pas assez = sous- apprentissage. La règle empirique params ≈ 10 × N_samples est un point de départ — mais c’est aussi un hyperparamètre.
  • Combien de couches ? Combien de neurones par couche ? Quelle activation (ReLU, GELU, sWELU si on utilise notre brevet) ?
  • Quel optimizer (Adam, SGD, AdamW) ? Quel learning rate ? Quel schedule ?
  • Quelle initialisation des poids (Xavier, He, custom) ?

Tous ces choix sont des hyperparams du modèle qui optimise les hyperparams du RAG. Ils sont eux-mêmes optimisables via :

  • Neural Architecture Search (NAS) — recherche automatique de l’architecture optimale (DARTS, ENAS, evolutionary NAS).
  • Bayesian optimization (Optuna, scikit-optimize) — modélise la fonction architecture → performance et explore efficacement.
  • Plans d’expérience Taguchi — orthogonal arrays L9/L18/L27 pour explorer les facteurs principaux sans explosion combinatoire.

La récursion s’arrête où ?

En théorie, on pourrait continuer : optimiser le choix entre NAS et Bayesian opt, puis optimiser le choix de cet optimiseur, etc. En pratique, on s’arrête à 4-5 niveaux pour deux raisons :

  1. Coût computationnel : chaque niveau supplémentaire multiplie le coût par ~10x. Au-delà de 5 niveaux, c’est intractable sur infra modeste.
  2. Bruit > signal : à mesure qu’on monte, le signal devient noyé dans le bruit. Au-delà du niveau 4, les gains mesurés sont indistinguables du bruit statistique.

La règle Nika OS : on optimise jusqu’au niveau où le gain mesuré dépasse 2× l’écart-type du bruit. Au-delà, on gèle et on documente la décision.

Implications pratiques

  • Toute primitive d’optimisation dans Nika OS doit logger ses hyperparams
    • ses résultats en JSONL pour permettre une analyse à posteriori.
  • Le cron autoresearch_orchestrator.py (cf. swelu_autoresearch existant) peut être étendu pour piloter les niveaux 2-4.
  • Les brevets KTW de Paul s’appliquent à tous les niveaux : Kelly pour le dosage de mutation, Taguchi pour la perte mesurée, Weibull pour la détection de dérive — peu importe le niveau.

C’est cette récursion qui rend Nika OS antifragile au sens mathématique : à chaque incident (loss qui monte, retrieval qui rate, NAS qui diverge), le système peut monter d’un niveau et optimiser l’optimiseur. Plus le système est stressé, plus il a d’occasions d’apprendre.

Stratégies conditionnelles + Multi-tenant

Les stratégies de compression (A) et de scaling (B) sont des primitives de Nika OS, mais leur disponibilité dépend de la configuration utilisateur. Sans clé API d’un provider (Contabo, Scaleway, GCP, etc.), la stratégie correspondante est tout simplement désactivée pour ce tenant — pas d’erreur, juste pas d’option proposée.

Conditionnalité par config

Le scheduler vérifie au boot et à chaque tick :

def available_strategies(tenant_config):
    strategies = []
    if tenant_config.has_api_key("scaleway") or tenant_config.has_api_key("contabo"):
        strategies.append("scale_horizontal_eu")
    if tenant_config.has_api_key("runpod") or tenant_config.has_api_key("gcp"):
        strategies.append("scale_horizontal_gpu")
    if tenant_config.has_repo("nika-os-knowledge"):
        strategies.append("compress_to_repo")
    return strategies

Si aucune stratégie n’est disponible et que la dette mémoire dépasse les seuils critiques, Nika OS notifie l’utilisateur avec une recommandation claire : « configurez une clé API Scaleway dans le wizard pour activer le scaling, ou autorisez la compression vers un repo Git pour reset fresh ».

Multi-tenant — un tmux par tenant

Cette conditionnalité ouvre naturellement la voie au multi-tenant : plusieurs utilisateurs partagent la même installation Nika OS, mais avec des configurations isolées et leurs propres providers.

L’architecture multi-tenant :

ComposantLogique
IdentificationLe bridge (WhatsApp Business numéro / Teams compte / Google Chat bot) attribue un tenant_id au message entrant.
IsolationUn tmux nika-tenant-{tenant_id} par tenant, avec son propre kernel, ses skills, sa mémoire.
ConfigurationChaque tenant a son fichier ~/.config/nika-os/tenants/{tenant_id}/config.yaml avec ses clés API, préférences CLI, niveau de plan (Free / Pro / Multimodal).
QuotasLimites par tenant (CPU%, RAM, tokens / jour) appliquées via cgroups K3s.
MémoireCollections Qdrant filtrées par tenant_id dans le payload. Pas de fuite cross-tenant sans configuration explicite.
FacturationChaque appel d’outil + chaque token consommé est logué avec tenant_id pour facturation séparée.

Bridge → tenant routing

Quand un message arrive sur un gateway :

sequenceDiagram
    participant U as 👤 Utilisateur
    participant G as 🌐 Gateway WA/Teams
    participant R as 🔀 Bridge Router
    participant T as 🪆 tmux nika-tenant-X
    participant Q as 🗄️ Qdrant (tenant_id=X)

    U->>G: message vocal/texte/image
    G->>R: webhook (msg + sender_id)
    R->>R: lookup tenant_id(sender_id)
    R->>T: route vers kernel du tenant
    T->>Q: RAG query filter tenant_id=X
    Q-->>T: contexte tenant uniquement
    T->>G: réponse formatée
    G->>U: livraison vocale/texte/image

Avantages

  • Démo facile — on peut faire tourner plusieurs démos clients en parallèle sur la même machine, sans interférence.
  • Onboarding parallèle — chaque nouveau tenant attaque son onboarding chatbot sans bloquer les autres.
  • Coût mutualisé — la même infrastructure de base (Redis, Qdrant, bridges) sert N tenants, chacun paye au prorata de son usage.
  • Sécurité par défaut — isolation cgroups K3s + filter Qdrant par tenant_id + clés API séparées rendent une fuite cross-tenant techniquement très improbable.

Limites

  • Le multi-tenant lourd (>50 tenants par VPS) saturera rapidement la mémoire d’un seul host. La voie de sortie est la stratégie B : spawner un VPS dédié par tenant lourd via Scaleway / Contabo / GCP / Azure (cf. wizard step 5bis).
  • Les performances baissent légèrement avec le nombre de tenants actifs simultanément (RAM partagée). Les fournisseurs Free Tier doivent accepter une latence un peu plus haute.

Cette architecture est l’aboutissement opérationnel du positionnement proto-OS : un seul Nika OS sert N utilisateurs distincts, avec leur config, leur mémoire, leur facturation — sans qu’aucun ne touche au terminal.