Synchroniser la mémoire sans « dernière écriture gagnante »

· 5 min de lecture

Deux machines, un seul ensemble de notes, l'une modifiée pendant que l'autre dormait. Tout système de synchronisation répond à ce cas, et la plupart y répondent par une comparaison d'horodatages qui écarte silencieusement l'un des deux côtés. C'est acceptable pour un brouillon qu'on peut retaper. Ça ne l'est pas pour le fichier qu'un agent lit avant chaque tour, car ce qui est écarté, c'est la ligne que vous avez écrite pour qu'une chose ne se reproduise pas.

La synchronisation de la mémoire dans OHarness n'a donc aucun chemin « dernière écriture gagnante ». Il y a une réconciliation à trois voies et, là où elle ne peut pas trancher, un conflit qui vous attend.

Local d'abord, et uniquement local si vous le souhaitez

Les notes se trouvent dans <project>/.oharness/memory/*.md. Elles fonctionnent sans connexion à un compte, hors ligne, et ce sont de simples fichiers Markdown modifiables dans n'importe quel éditeur. Rien dans le cloud n'est porteur : la synchronisation est une fonctionnalité ajoutée par-dessus des fichiers déjà complets sans elle.

La connexion active la synchronisation automatique, détectée en une quinzaine de secondes environ pendant que le harnais tourne. La page Mémoire permet de la désactiver et de synchroniser immédiatement. La désactivation persiste après un redémarrage du processus et une reconnexion, et bloque les demandes automatiques et manuelles jusqu'à ce que vous la réactiviez — un interrupteur qui revient discrètement en arrière au lancement suivant n'est pas un interrupteur.

Une limite, énoncée honnêtement dans la documentation plutôt que découverte plus tard : la désactivation annule les requêtes en cours, mais les données que le cloud a déjà acceptées ne peuvent pas être retirées. Désactiver empêche le prochain envoi ; cela ne supprime pas ce qui s'y trouve déjà.

À trois voies, pas à deux

La réconciliation a besoin de trois états, et non de deux : ce qui est ici, ce qui est là-bas, et ce sur quoi les deux côtés se sont accordés en dernier. Ce dernier état est la référence, conservée dans ~/.oharness/memory-sync/, et c'est lui qui fait la différence entre « ces deux-là diffèrent » et « celui-ci a changé ».

À partir de là, les cas n'ont rien de remarquable, et c'est voulu :

  • inchangé des deux côtés — ignoré, ni lecture ni écriture
  • modifié d'un seul côté — copié vers l'autre
  • modifié des deux côtés — un conflit, affiché et laissé à votre décision

Les suppressions sont des marqueurs de suppression (tombstones) plutôt que des absences, car un fichier absent ne se distingue pas d'un fichier qui n'est pas encore arrivé, et confondre le second avec le premier, c'est ainsi qu'un système de synchronisation supprime votre travail sur un réseau lent.

Côté cloud, R2 conserve des révisions Markdown immuables ainsi qu'un manifeste remplacé de manière atomique :

users/<server-verified-user-id>/projects/<project-id>/memory/
  manifest.json
  objects/<note-id>/<revision-id>.md

Les écritures du manifeste utilisent un compare-and-swap sur l'ETag : une mise à jour concurrente périmée est donc rejetée plutôt qu'appliquée. Les révisions sont immuables, si bien que le côté perdant d'un conflit reste un objet qui existe. Et lorsque le manifeste n'a pas changé, seul le manifeste est récupéré — le cas courant, une machine qui tourne tout l'après-midi sans rien à faire, coûte une seule petite lecture.

L'identité d'un projet est le commit racine Git, ce qui permet au même projet, cloné deux fois à deux chemins différents, d'être un seul projet. Les projets hors Git se rabattent sur un id fondé sur le chemin absolu et ne sont donc pas mis en correspondance automatiquement d'un appareil à l'autre. C'est une vraie limite, pas une aspérité à masquer : l'alternative serait une heuristique qui fusionne de temps en temps deux répertoires sans rapport, et cet échec n'a pas de bonne version.

Chaque répertoire de mémoire local est lié au premier compte et à la première origine cloud qui l'ont synchronisé. Changer l'un ou l'autre met la synchronisation en pause et vous invite à utiliser plutôt un répertoire distinct. Il n'existe délibérément pas de réaffectation en un clic, car la version en un clic de cette opération est celle qui envoie les notes d'une personne dans le compte d'une autre.

Ce que ce n'est pas

Deux points sur lesquels il vaut mieux être direct.

Ce n'est pas chiffré de bout en bout. R2 est privé et tout passe par TLS, mais le cloud peut lire le contenu des notes. Si ce compromis ne convient pas à ce que vous écrivez, désactivez la synchronisation et gardez les fichiers en local : c'est une configuration prise en charge, pas une configuration dégradée.

Seules les notes de mémoire sont synchronisées. Pas les journaux de session, pas les fichiers, pas les identifiants, ni la trace d'audit des permissions — cette dernière reste sur la machine qui a pris les décisions et est supprimée avec sa session.

Il y a aussi des limites, du genre que l'on trouve dans une première version : 500 entrées de manifeste par projet, marqueurs de suppression compris, 128 Kio par corps de note, 2 Mio par instantané sérialisé. Les anciennes révisions sont conservées ; le ramasse-miettes des révisions et des marqueurs de suppression n'est pas encore implémenté. Les notes supprimées et les versions perdantes des conflits sont sauvegardées dans ~/.oharness/memory-backups/ et peuvent être restaurées depuis l'éditeur, même s'il n'existe pas encore de navigateur de sauvegardes.

Pourquoi un plugin

La mémoire est un plugin d'architecture intégré, pas un serveur MCP. Elle enregistre son outil, son index de prompt système à chaque tour, sa page et son synchroniseur en arrière-plan via le même PluginHost que les plugins de modèle, d'outil et d'audit — et { "id": "memory", "enabled": false } dans plugins.json retire les quatre.

C'est le test qu'un système de plugins réussit ou rate. Si les fonctionnalités livrées d'origine atteignent le runtime par une porte privée, la porte publique n'est que décorative, et tous ceux qui développent dessus le découvrent lentement. L'extension de page reste petite pour la même raison : des descripteurs enregistrés et des moteurs de rendu gérés par l'hôte, pas de JavaScript frontend arbitraire ni de SDK d'iframe. Une surface qui peut tout faire est une surface que personne ne peut plus modifier ensuite.


OHarness est un harnais d'agent open source, sous licence MIT. La référence mémoire et synchronisation documente en détail les règles de conflit, l'organisation du stockage et les limites de configuration, et la page synchronisation auto-hébergée explique comment faire tourner l'ensemble sur votre propre infrastructure.