Le Contrat d'Observation

Comment Cortex répartit la formation des souvenirs sur deux services — encodage dans le lac, consolidation dans l'entrepôt — via un contrat Observation typé.

Version 1.0 · Mis à jour

01

Pourquoi la formation des souvenirs ne peut pas vivre dans un seul service

La formation des souvenirs repose sur deux compétences distinctes qu'on ne peut pas co-localiser sans couplage. La première est la découverte : parser les données brutes, les typer, extraire leur signification depuis une source. Cela exige une connaissance du domaine source — ce qu'est un e-mail, un appel d'outil, un message Telegram. La seconde est l'intégration : déduplication, détection de conflits, calcul du schema fit, décroissance, écriture dans le graphe d'entités. Cela exige une connaissance de l'état courant du graphe.

Si découverte et intégration vivent toutes deux dans cortex-insight, insight doit suivre l'état du graphe — et chaque pipeline se couple au schéma de l'entrepôt. La mémoire perd sa capacité à arbitrer la déduplication. Si elles vivent toutes deux dans cortex-memory, memory doit contenir les parsers de chaque domaine source — le schéma générique de l'entrepôt est contaminé par des structures spécifiques ; memory devient massif. Aucune attribution pure ne produit une coupe propre. La solution est de répartir sur deux services avec un contrat typé explicite à la frontière.

Le problème de couplage n'est pas organisationnel. Il est épistémique : la connaissance nécessaire à l'encodage est source-spécifique ; la connaissance nécessaire à l'intégration est graphe-spécifique. Aucun service ne peut détenir les deux sans contamination.

02

Le découpage lac / entrepôt

C'est le schéma classique de l'ingénierie des données — lac pour le stockage brut, append-heavy, schema-on-read ; entrepôt pour la vérité structurée, schema-on-write, orientée consommateurs — appliqué à l'échelle embarquée.

cortex-insight (lac) est append-heavy et schema-on-read. Il stocke les événements bruts, exécute des pipelines ETL spécifiques à chaque source et publie des enregistrements Observation typés. Les réécritures sont peu coûteuses : le lac est une zone de transit, pas un actif long terme.

cortex-memory (entrepôt) est schema-on-write et générique. Il reçoit les Observation, les consolide dans le graphe d'entités, et gère la décroissance, la déduplication, la résolution des conflits, la vérité compilée, l'assemblage et l'actuation. Les changements de schéma sont versionnés et protégés, car c'est l'actif long terme.

Deux règles de frontière en découlent directement. Insight ne stocke jamais le graphe. Memory ne parse jamais de données source brutes. Ces règles sont imposées structurellement : les imports croisés entre les deux services ne sont pas déclarés dans pyproject.toml et échouent à l'import.

Les conséquences pratiques se composent. Ajouter une nouvelle source revient à écrire un nouveau parser et un nouveau pipeline dans cortex-insight uniquement — cortex-memory reste inchangé. Le service insight peut être entièrement réécrit sans toucher à la connaissance accumulée. L'asymétrie des dépendances est aussi explicite : insight est lourd (parsers, regex, appels LLM, consumers NATS, traitement tabulaire) ; memory est minimal (store de graphe, NATS).

PRODUCTEURS Claude Code JSONL Mail / Telegram Événements NATS cortex-insight (lac) événements bruts schema-on-read parsers source-spécifiques / ETL Observation roxabi.memory .observations.publish frontière cortex-memory (entrepôt) Retain Job graphe d'entités + relations décroissance · dedup · vérité compilée Consommateurs (assemble / actuate)
Flux lac / entrepôt : producteurs → cortex-insight (lac) → contrat Observation → cortex-memory (entrepôt) → consommateurs. La ligne amber en pointillés marque la frontière : insight ne stocke jamais le graphe ; memory ne parse jamais les données brutes.
03

L'analogie biologique : hippocampe → cortex

Le design à deux phases reflète la neurologie établie de la consolidation mnésique. L'hippocampe encode vite et avec perte — il capture l'épisode sans le résoudre contre la connaissance long terme. Le cortex sémantique consolide lentement, intégrant et dédupliquant à travers le graphe sémantique au fil du temps. Le cortex préfrontal assure la récupération conditionnée par l'objectif depuis le store consolidé.

Le système se projette précisément sur ce modèle. Les producteurs externes correspondent à la perception. cortex-insight est la couche hippocampique : rapide, source-spécifique, épisodique. cortex-memory est le cortex sémantique : lent, générique, conscient du graphe. La fonction memory.assemble et la pile d'objectifs correspondent à la récupération préfrontale.

Observation est l'unité de mémoire épisodique : typée, encodée, non encore résolue contre le graphe. Elle se situe exactement à la frontière hippocampique. Ceci est cohérent avec la formule de force mémorielle hippocampique adoptée pour le modèle de décroissance memory_strength : écriture rapide, intégration lente, récupération durable.

BIOLOGIQUE SYSTÈME Perception (sens) Hippocampe encode épisodique (rapide) Cortex sémantique intégration durable Cortex préfrontal récupération par objectif encode consolide Producteurs externes cortex-insight obs cortex-memory memory.assemble + pile d'objectifs
Analogie biologique projetée sur le système. Les connecteurs amber marquent la frontière encode (hippocampe → cortex-insight) et la frontière consolide (cortex sémantique → cortex-memory). La pilule Observation se situe au passage entre les deux services.
04

Le contrat Observation

Observation est le contrat de publication typé entre cortex-insight et cortex-memory — le seul artefact qui traverse la frontière de service. Il est publié sur le sujet NATS roxabi.memory.observations.publish.

python · Observation
class Observation(BaseModel):
    id: ULID
    source: str           # "claude-code-jsonl" | "mail" | "telegram" | …
    source_ref: str       # identifiant naturel dans la source
    timestamp: int        # epoch ms
    category: ObservationCategory  # Interaction | Finding | Decision | Artifact | …
    actors: list[ActorRef]         # références d'entités (non résolues)
    topic: list[str] | None        # concepts extraits
    sentiment: str | None
    payload_typed: dict            # payload structuré spécifique à la catégorie
    correlation: dict              # trace_id, parent_obs_id, episode_id, …

Quatre choix de conception sont structurants. D'abord, category est l'ancre taxonomique stable (ObservationCategory) : Interaction, Finding, Decision, Artifact, Statement. Le jeu de catégories doit être exhaustif et stable — c'est la surface de schéma sur laquelle le Retain Job de cortex-memory dispatche. Ensuite, actors porte des références — e-mail, nom, slug — mais la résolution contre le graphe est délibérément différée à memory. Insight ne sait pas si un acteur existe déjà dans le graphe. Puis, payload_typed est un contenu structuré spécifique à la catégorie, typé par insight et consommé par le Retain Job — pas du texte libre générique. Enfin, correlation (trace_id, parent_obs_id, episode_id) permet de tracer la causalité à travers l'ensemble du pipeline.

Observation n'est explicitement pas un EntityProposal. La résolution d'entités est la responsabilité de memory. Insight produit un enregistrement épisodique bien typé et source-spécifique ; memory le résout dans le graphe durable.

id: ULID identité source + source_ref + timestamp provenance category: ObservationCategory Interaction | Finding | Decision | Artifact | Statement ancre taxonomique actors: list[ActorRef] références d'entités (non résolues) graphe Retain Job topic: list[str] | None sentiment: str | None indices sémantiques payload_typed: dict payload spécifique à la catégorie typé par insight correlation: dict trace_id · parent_obs_id · episode_id traçage causal encodé par cortex-insight
Anatomie du schéma Observation. Le cadre amber délimite la struct complète. L'accolade gauche couvre tous les champs encodés par cortex-insight. La flèche pointillée depuis actors indique la résolution différée : l'identité des entités est établie par le Retain Job dans cortex-memory, jamais par insight.
05

Le Retain Job : consolidation dans la mémoire

Le Retain Job réside entièrement dans cortex-memory. C'est le moteur de consolidation : pour chaque Observation reçue depuis le sujet NATS, il exécute une séquence déterministe.

Il commence par résoudre actors contre le graphe — chaque ActorRef s'associe soit à une entité existante, soit en crée une nouvelle. Ensuite, il déduplique contre le contenu existant du graphe. Il calcule le schema_fit de l'observation entrante par rapport à l'état courant du graphe. Il détecte les conflits. Il écrit les entités et les relations. Il met à jour memory_strength et applique le modèle de décroissance. Enfin, si l'impact de l'observation dépasse un seuil, il déclenche la régénération de compiled_truth.

Le Retain Job était initialement co-localisé avec les détecteurs d'insight. Il a été déplacé dans cortex-memory parce que la consolidation est structurellement un attribut de l'entrepôt : elle exige l'état du graphe, arbitre l'identité et contrôle l'actif long terme. Le placer dans le lac aurait contraint insight à suivre l'état du graphe — exactement le couplage que la séparation visait à prévenir.

Le Retain Job est l'arbitre de l'identité, de la déduplication et de la vérité. Insight est le producteur d'observations brutes bien typées. Cette asymétrie n'est pas une commodité opérationnelle — c'est un invariant structurel.

06

Alternatives écartées

Toute la génération dans insight, EntityProposal comme sortie finale. Insight doit alors connaître l'état du graphe ; chaque pipeline se couple au schéma de l'entrepôt ; memory ne peut plus arbitrer la déduplication. Écarté.

Toute la génération dans memory, insight comme lac passif. Memory accumule les parsers par domaine source ; le schéma générique de l'entrepôt est contaminé par des structures spécifiques ; memory devient massif. Écarté.

Pipeline cross-service unique via NATS streaming. Complexité prématurée. Les deux phases sont logiquement distinctes ; les nommer explicitement avec un contrat typé est plus durable que de les aplatir en un seul pipeline. Écarté.

Aucun découpage lac / entrepôt, statu quo mono-repo. Le schéma d'événements bruts est centré JSONL et inutilisable pour les e-mails ou Telegram. Le Retain Job couplerait le graphe à un seul domaine source. L'actif long terme du graphe vivrait dans le repo de l'analyseur — propriété inversée. Écarté.

Découpage en trois (sources / extraction / store). Trop décomposé pour l'échelle actuelle. Le couplage entre extraction et données brutes justifie de les co-localiser dans insight. Écarté.

07

Les contrats comme source unique de vérité du protocole

Les sujets NATS et les schémas de messages Pydantic — dont Observation — vivent dans le package partagé roxabi-contracts, et non à l'intérieur de l'un ou l'autre service. cortex-insight et cortex-memory déclarent tous deux roxabi-contracts comme dépendance versionnée explicite dans leur pyproject.toml.

La règle est inconditionnelle : ne jamais définir de sujets NATS ni de schémas de messages à l'intérieur d'un package de service. Les imports croisés entre insight et memory ne sont pas déclarés — tout import de ce type échoue à l'exécution. Le seul canal de communication cross-service valide est NATS, via les contrats.

Les tests d'intégration sont le seul site valide pour les assertions cross-service. Ils montent les deux services et un broker NATS local. Les tests unitaires au sein de chaque service testent ce service en isolation, contre le seul package de contrats. Cette frontière est imposée au niveau du graphe de dépendances, et non par convention.

roxabi-contracts est le SSoT du protocole. Le versionner indépendamment signifie qu'un changement de schéma est explicite, révisable, et visible simultanément par les deux consommateurs — pas un renommage interne silencieux dans l'un des services.