Pourquoi votre export /save perdait des tours — et le plafond derrière cela


Vous avez une session de longue durée, la compaction en a archivé plusieurs tours, et vous voulez l’exporter pour la conserver. /save json depuis la ligne de commande fonctionne parfaitement. Vous faites la même chose dans l’application de bureau, vous importez le résultat plus tard, et vous remarquez que des morceaux manquent — les tours archivés par la compaction ont tout simplement disparu. Vous commencez à vous demander si vous avez mal mémorisé, ou si un /undo les a supprimés.

Deux problèmes se superposent ici : le même /save passait par trois chemins de code différents (CLI, plateformes de messagerie et le RPC session.save du bureau/TUI), et les deux règles qui comptent — ce qu’il faut exporter et à partir de quand c’est trop volumineux — finissaient par diverger entre elles. Le correctif ramène tout sur un seul chemin.

Trois surfaces /save, auparavant trois réponses

/save prend en charge json (un instantané complet), md (une transcription lisible) et html (une page autonome à partager). Le bureau et le TUI appellent le RPC session.save, tandis que la CLI et les plateformes de messagerie utilisaient une autre implémentation. La différence apparaissait dans la projection de l’export :

  • Une transcription (md / html) représente « ce que l’utilisateur a vu », elle doit donc inclure les tours que la compaction sur place a archivés ;
  • Un instantané JSON représente « ce qu’un import restaure », il porte donc chaque ligne stockée avec ses indicateurs active/compacted. Exporter uniquement les lignes actives fait disparaître chaque portion archivée de la conversation ; exporter les indicateurs permet à l’import de les restaurer comme archivées plutôt que de les rejouer comme contexte actif.

Le chemin RPC ne suivait pas cette règle de manière cohérente, si bien qu’une même session s’exportait différemment selon la surface utilisée.

La règle désormais : un seul chemin partagé par les trois

Le correctif extrait la logique déjà correcte de la CLI dans une fonction partagée load_save_snapshot(db, session_id, fmt) dans hermes_cli/session_export.py, et les trois surfaces l’appellent. Elle fait trois choses :

  1. Choisit la projection selon le format : include_compacted=True pour les transcriptions, include_inactive=True pour le JSON ;
  2. Pour un instantané JSON, écarte les lignes annulées : seules les lignes active ou compacted survivent, tandis que les lignes supprimées par /undo, rembobinage (rewind) ou édition restent dehors — délibérément, car un import ne doit pas ressusciter ce que l’utilisateur a annulé ;
  3. Applique le plafond d’export (ci-dessous).

Deux choses plus modestes sont venues avec. L’instantané JSON utilise désormais la forme d’export canonique, alignée sur la CLI, au lieu des clés héritées de l’application de bureau — et quand une session n’a pas de ligne stockée (ou que la lecture échoue), le repli en mémoire écrit cette même forme (id, model, started_at, system_prompt, messages). Il lit aussi la ligne que l’agent actif est en train d’écrire, de sorte qu’une rotation de compression n’exporte pas un parent obsolète.

Le plafond d’export : sessions.max_export_messages

Matérialiser en mémoire une session de 100 000 lignes d’un seul coup peut épuiser le processus, d’où une garde :

sessions:
  max_export_messages: 20000   # la valeur par défaut ; 0 désactive la garde

Elle est vérifiée par session, de sorte qu’une sauvegarde de toute la base composée de nombreuses petites sessions n’est pas affectée — seule une session surdimensionnée est refusée. Après ce correctif, le /save json du bureau/TUI la respecte aussi, et :

  • le refus arrive sous forme de code d’erreur RPC 4131 (et non le générique 5011) avec le même message que celui affiché par la CLI ;
  • sur les sessions isolées par tour (compute-host), l’hôte transmet le code d’erreur du gestionnaire dans control.error et le parent le répercute, si bien que les sessions exécutées à distance signalent la vraie raison ;
  • le message vous indique de définir sessions.max_export_messages: 0 dans config.yaml pour désactiver la garde.

Le dernier détail est facile à négliger et compte : la limite est lue depuis la configuration du profil auquel appartient la session. L’application de bureau lit désormais la state.db propre à ce profil sous la configuration de ce profil, si bien que le plafond d’export d’un profil secondaire est le sien, et non celui de votre profil principal.

Choisir selon le scénario

Une fois les règles claires, le choix du format est simple :

Votre objectif À utiliser Pourquoi
Archiver / changer de machine /save json Instantané complet avec indicateurs d’archivage ; ré-importable tel quel
Envoyer à un collègue /save html ou /save md Transcription lisible, tours archivés par compaction inclus
Session trop volumineuse, export refusé hermes sessions export pour une seule session, ou augmenter/désactiver max_export_messages Le plafond est jugé par session
Publier quelque part de public N’importe quel format plus redact Nettoie les clés d’API, jetons et identifiants avant l’écriture

L’essentiel à retenir : « l’export » sert deux objectifs différents et les règles diffèrent délibérément. Une transcription est destinée aux humains, elle reste donc aussi proche que possible de ce que vous avez vu — tours archivés inclus. Le JSON est destiné aux machines, il préserve donc les indicateurs et retire les lignes annulées. Si vous aviez supposé que le manque en JSON signifiait une perte de données, cette règle l’explique.

Quand vous pouvez l’obtenir

Le correctif a été fusionné dans la branche main le 3 octobre 2026. La dernière version est v0.21.5 (tag v2026.9.24), donc il ne vit actuellement que sur main — bien que sessions.max_export_messages lui-même existe déjà dans les versions publiées et fonctionne aujourd’hui. Les installations qui suivent main deviennent cohérentes à la prochaine commande hermes update.

Pour une vue d’ensemble, le guide de sauvegarde et d’export de session compare /save à hermes sessions export, et le guide des limites de session explique comment les sessions sont délimitées. Si c’est le mécanisme de compression lui-même qui vous intéresse, l’intégrité de la compaction en cours de tour et l’article sur la compaction en un seul appel vont plus loin. Les détails au niveau des commandes se trouvent dans la référence de la commande sessions.

La version courte

La valeur ici n’est pas une nouvelle fonctionnalité mais la disparition d’une ambiguïté : auparavant, vous ne pouviez pas dire si un export JSON court du bureau était un bug ou un réglage. Désormais trois surfaces partagent une règle, les deux projections ont des justifications explicites, et une session surdimensionnée renvoie un code précis avec un conseil que vous pouvez appliquer directement. Si vous exportez des sessions de façon planifiée, une vérification vaut le coup aujourd’hui : quelle surface et quel format utilise votre pipeline de sauvegarde — et si ce qu’il produit se ré-importe vraiment proprement.