bots-garden/mini-mepublic Fork
⑂ v0.1.8
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 8de2ad8 · on v0.1.8 · k33g · Sep 18, 2026
persistent-sessions.md · 50 lines · 7.8 KBmarkdown
Blame HistoryOpen raw

Les sessions persistantes — explication

De quoi s'agit-il ?

Une session est une conversation : le prompt système, les questions, les réponses et les appels d'outils entre les deux. Jusqu'au 16 septembre 2026 elle vivait en mémoire et mourait avec le processus ; un éditeur qui relançait mm ne pouvait qu'ouvrir un nouveau fil, ce que Zed exprimait par « Loading or resuming sessions is not supported by this agent ». Les sessions persistantes (sessions.enabled) écrivent chaque conversation dans un fichier et permettent aux deux façades de la retrouver.

Pourquoi des fichiers, par projet

Les sessions vivent sous le projet, dans .mm/sessions/, un fichier JSON chacune, réécrit en entier après chaque tour. Trois raisons. Une conversation porte sur un projet — ses fichiers, ses commandes — et a sa place à côté d'eux, pas dans une base de données globale où les projets se mélangent. Les fichiers se lisent : jq répond à « qu'a lu le modèle ? » sans outil. Et la réécriture complète est ce dont la compression a besoin : /compact remplace les vieux tours par un résumé, et le fichier doit dire la même chose que la mémoire, pas la mémoire plus tout ce qu'elle contenait avant.

L'identifiant est l'heure de création suivie d'un suffixe aléatoire (20260916-101530-4f2a9c1e) : une liste se trie par date, et deux façades ne se marchent jamais dessus.

Pourquoi un lancement est une nouvelle session

Quand mm démarre, il ouvre une nouvelle session et indique comment retrouver les anciennes, plutôt que de reprendre la dernière de lui-même. C'est le comportement de docker agent (cagent) : sa commande run ne reprend jamais seule — --session -1 ou le navigateur /sessions de son interface le font — et c'est le défaut le plus sûr. La dernière conversation peut porter sur autre chose ; la reprendre en silence donnerait au modèle un contexte périmé, et l'utilisateur ne s'en apercevrait qu'aux réponses de travers. Un /resume explicite coûte une ligne et évite la surprise.

Le terminal écrit le fichier à la première question, pas au lancement : un lancement sans question ne laisse aucune trace, et /sessions liste des conversations, pas des lancements.

Pourquoi session/load et un rejeu complet

L'Agent Client Protocol offre deux retours vers une session. session/load fait partie de la spécification stable : l'agent rejoue la conversation sous forme de notifications session/update et l'éditeur reconstruit le fil à partir d'elles. session/list et session/resume sont des extensions instables : l'agent ne fait que réenregistrer la session, et l'éditeur garde ce qu'il affichait. docker agent a choisi la seconde voie (loadSession: false, capacité Resume, base SQLite dans ~/.cagent/session.db). mini-me a choisi la première, parce qu'elle est stable et parce qu'un rejeu remet le fil en place même dans un éditeur qui n'avait rien gardé.

« Rejouer » veut dire renvoyer des enregistrements. Chaque appel d'outil revient comme un seul tool_call déjà terminé, portant l'entrée donnée par le modèle et la sortie renvoyée par l'outil à l'époque. Rien n'est réexécuté : recharger un fil qui a un jour lancé rm ne lance rien.

Une session rechargée ou reprise continue sous le prompt système actuel, pas celui enregistré avec elle : le prompt appartient à l'agent, et un changement d'agent.yaml doit atteindre aussi les anciennes conversations.

/new dans les deux façades

Dans le terminal, /new laisse simplement l'ancien fichier tel quel et la question suivante en ouvre un nouveau. En ACP, l'éditeur possède l'identifiant de session et continue de l'utiliser : le fichier de cet identifiant doit devenir la session neuve, et la conversation écoulée est d'abord déplacée sous un nouvel identifiant. Dans les deux cas rien n'est perdu sur disque, et un rechargement ultérieur du fil rend la conversation courante, pas celle que /new a fermée.

Pourquoi les erreurs sont enregistrées à côté de l'historique, pas dedans

Un tour peut échouer — serveur arrêté, modèle non téléchargé, chien de garde qui renonce, résumé de compression revenu vide — et la façade le dit en une ligne rouge. Cette ligne était la seule trace : partie avec le défilement, absente du fichier, parce que le fichier contient ce que voit le modèle et qu'un tour raté ne lui laisse rien à voir (la question sans réponse est retirée). Depuis le 18 septembre 2026, le fichier de session porte aussi un tableau errors : un enregistrement par ligne affichée, avec l'heure, le nombre de tours, la première ligne de la question en cours, la ligne telle qu'affichée, et l'erreur brute quand elle en dit plus. Trois sortes sont enregistrées : generation (une réponse échouée), compaction (un résumé échoué, automatique ou /compact) et resume (un /resume qui n'a pas pu charger sa session). Un tour annulé n'est pas une erreur et n'est pas enregistré.

Le tableau est à côté de messages, pas dedans, à dessein. Le modèle ne le lit jamais : une erreur concerne l'exécution, pas le projet, et la lui renvoyer lui ferait expliquer son infrastructure au lieu de faire sa tâche. /resume et session/load ne le rejouent jamais : tous deux ne lisent que messages, donc une vieille session se charge exactement comme avant et une nouvelle avec des erreurs se charge comme si le champ n'existait pas. Et la compression n'y touche pas : /compact réécrit messages, la trace reste entière. L'enregistrement suit sa conversation : en ACP, /new le déplace dans le fichier archivé avec l'historique auquel il appartient.

À quoi cela sert : demander après coup, le fichier en main, pourquoi une exécution a mal tourné — jq .errors <fichier> pour un humain, ou le fichier confié à un agent de code à qui l'on demande d'expliquer les échecs et de proposer des changements de code ou de YAML (un watchdogTimeout plus long, un autre modèle, un maxOutput plus petit). La liste /sessions et le compte rendu de /resume affichent · ⚠️ N error(s) pour qu'une session à examiner se remarque. La trace est plafonnée à 200 enregistrements, les plus anciens sortent d'abord, pour qu'une exécution qui échoue à chaque tour ne fasse pas grossir son fichier sans fin.

Alternatives écartées

  • Reprendre automatiquement la dernière session au lancement. Écartée pour la raison du contexte périmé, et parce que docker agent ne le fait pas non plus.
  • Un journal en ajout seul par session. Écarté : la compression raccourcit l'historique, et un journal continuerait de dire ce que la mémoire ne contient plus.
  • SQLite, comme docker agent. Écarté pour un agent minimal : une dépendance de plus, des fichiers opaques, et une base par utilisateur là où mini-me veut des fichiers par projet.
  • session/resume (instable) plutôt que session/load. Écarté : hors de la spécification stable, et dépendant d'un éditeur qui aurait gardé le fil.
  • Ne rejouer que les messages texte. Écarté par l'utilisateur : les appels d'outils sont la partie d'une session de code qui vaut d'être revue.
  • Enregistrer les erreurs comme des messages de l'historique. Écarté : le modèle les lirait, session/load devrait les rejouer sous une forme ou une autre, et un vieux fichier se chargerait autrement qu'un nouveau. Un champ séparé ne coûte rien au chargement et ne change rien pour le modèle.

Liens avec le reste

internal/store est la couche fichiers, utilisée par internal/agent (un keeper qui enregistre après chaque tour et sert /sessions et /resume) et par internal/acp (un fichier par session, session/load, le rejeu dans replay.go). internal/session.WithSystem remplace le prompt enregistré par l'actuel. La compression (internal/compact) ignore la persistance : les façades enregistrent après son passage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Les sessions persistantes — explication

## De quoi s'agit-il ?

Une session est une conversation : le prompt système, les questions, les réponses et les appels d'outils entre les deux. Jusqu'au 16 septembre 2026 elle vivait en mémoire et mourait avec le processus ; un éditeur qui relançait `mm` ne pouvait qu'ouvrir un nouveau fil, ce que Zed exprimait par « Loading or resuming sessions is not supported by this agent ». Les sessions persistantes (`sessions.enabled`) écrivent chaque conversation dans un fichier et permettent aux deux façades de la retrouver.

## Pourquoi des fichiers, par projet

Les sessions vivent sous le projet, dans `.mm/sessions/`, un fichier JSON chacune, réécrit en entier après chaque tour. Trois raisons. Une conversation porte sur un projet — ses fichiers, ses commandes — et a sa place à côté d'eux, pas dans une base de données globale où les projets se mélangent. Les fichiers se lisent : `jq` répond à « qu'a lu le modèle ? » sans outil. Et la réécriture complète est ce dont la compression a besoin : `/compact` remplace les vieux tours par un résumé, et le fichier doit dire la même chose que la mémoire, pas la mémoire plus tout ce qu'elle contenait avant.

L'identifiant est l'heure de création suivie d'un suffixe aléatoire (`20260916-101530-4f2a9c1e`) : une liste se trie par date, et deux façades ne se marchent jamais dessus.

## Pourquoi un lancement est une nouvelle session

Quand `mm` démarre, il ouvre une nouvelle session et indique comment retrouver les anciennes, plutôt que de reprendre la dernière de lui-même. C'est le comportement de docker agent (cagent) : sa commande `run` ne reprend jamais seule — `--session -1` ou le navigateur `/sessions` de son interface le font — et c'est le défaut le plus sûr. La dernière conversation peut porter sur autre chose ; la reprendre en silence donnerait au modèle un contexte périmé, et l'utilisateur ne s'en apercevrait qu'aux réponses de travers. Un `/resume` explicite coûte une ligne et évite la surprise.

Le terminal écrit le fichier à la première question, pas au lancement : un lancement sans question ne laisse aucune trace, et `/sessions` liste des conversations, pas des lancements.

## Pourquoi `session/load` et un rejeu complet

L'Agent Client Protocol offre deux retours vers une session. `session/load` fait partie de la spécification stable : l'agent rejoue la conversation sous forme de notifications `session/update` et l'éditeur reconstruit le fil à partir d'elles. `session/list` et `session/resume` sont des extensions instables : l'agent ne fait que réenregistrer la session, et l'éditeur garde ce qu'il affichait. docker agent a choisi la seconde voie (`loadSession: false`, capacité `Resume`, base SQLite dans `~/.cagent/session.db`). mini-me a choisi la première, parce qu'elle est stable et parce qu'un rejeu remet le fil en place même dans un éditeur qui n'avait rien gardé.

« Rejouer » veut dire renvoyer des enregistrements. Chaque appel d'outil revient comme un seul `tool_call` déjà terminé, portant l'entrée donnée par le modèle et la sortie renvoyée par l'outil à l'époque. Rien n'est réexécuté : recharger un fil qui a un jour lancé `rm` ne lance rien.

Une session rechargée ou reprise continue sous le prompt système actuel, pas celui enregistré avec elle : le prompt appartient à l'agent, et un changement d'`agent.yaml` doit atteindre aussi les anciennes conversations.

## `/new` dans les deux façades

Dans le terminal, `/new` laisse simplement l'ancien fichier tel quel et la question suivante en ouvre un nouveau. En ACP, l'éditeur possède l'identifiant de session et continue de l'utiliser : le fichier de cet identifiant doit devenir la session neuve, et la conversation écoulée est d'abord déplacée sous un nouvel identifiant. Dans les deux cas rien n'est perdu sur disque, et un rechargement ultérieur du fil rend la conversation courante, pas celle que `/new` a fermée.

## Pourquoi les erreurs sont enregistrées à côté de l'historique, pas dedans

Un tour peut échouer — serveur arrêté, modèle non téléchargé, chien de garde qui renonce, résumé de compression revenu vide — et la façade le dit en une ligne rouge. Cette ligne était la seule trace : partie avec le défilement, absente du fichier, parce que le fichier contient ce que voit le modèle et qu'un tour raté ne lui laisse rien à voir (la question sans réponse est retirée). Depuis le 18 septembre 2026, le fichier de session porte aussi un tableau `errors` : un enregistrement par ligne affichée, avec l'heure, le nombre de tours, la première ligne de la question en cours, la ligne telle qu'affichée, et l'erreur brute quand elle en dit plus. Trois sortes sont enregistrées : `generation` (une réponse échouée), `compaction` (un résumé échoué, automatique ou `/compact`) et `resume` (un `/resume` qui n'a pas pu charger sa session). Un tour annulé n'est pas une erreur et n'est pas enregistré.

Le tableau est à côté de `messages`, pas dedans, à dessein. Le modèle ne le lit jamais : une erreur concerne l'exécution, pas le projet, et la lui renvoyer lui ferait expliquer son infrastructure au lieu de faire sa tâche. `/resume` et `session/load` ne le rejouent jamais : tous deux ne lisent que `messages`, donc une vieille session se charge exactement comme avant et une nouvelle avec des erreurs se charge comme si le champ n'existait pas. Et la compression n'y touche pas : `/compact` réécrit `messages`, la trace reste entière. L'enregistrement suit sa conversation : en ACP, `/new` le déplace dans le fichier archivé avec l'historique auquel il appartient.

À quoi cela sert : demander après coup, le fichier en main, pourquoi une exécution a mal tourné — `jq .errors <fichier>` pour un humain, ou le fichier confié à un agent de code à qui l'on demande d'expliquer les échecs et de proposer des changements de code ou de YAML (un `watchdogTimeout` plus long, un autre modèle, un `maxOutput` plus petit). La liste `/sessions` et le compte rendu de `/resume` affichent ` · ⚠️ N error(s)` pour qu'une session à examiner se remarque. La trace est plafonnée à 200 enregistrements, les plus anciens sortent d'abord, pour qu'une exécution qui échoue à chaque tour ne fasse pas grossir son fichier sans fin.

## Alternatives écartées

- **Reprendre automatiquement la dernière session au lancement.** Écartée pour la raison du contexte périmé, et parce que docker agent ne le fait pas non plus.
- **Un journal en ajout seul par session.** Écarté : la compression raccourcit l'historique, et un journal continuerait de dire ce que la mémoire ne contient plus.
- **SQLite, comme docker agent.** Écarté pour un agent minimal : une dépendance de plus, des fichiers opaques, et une base par utilisateur là où mini-me veut des fichiers par projet.
- **`session/resume` (instable) plutôt que `session/load`.** Écarté : hors de la spécification stable, et dépendant d'un éditeur qui aurait gardé le fil.
- **Ne rejouer que les messages texte.** Écarté par l'utilisateur : les appels d'outils sont la partie d'une session de code qui vaut d'être revue.
- **Enregistrer les erreurs comme des messages de l'historique.** Écarté : le modèle les lirait, `session/load` devrait les rejouer sous une forme ou une autre, et un vieux fichier se chargerait autrement qu'un nouveau. Un champ séparé ne coûte rien au chargement et ne change rien pour le modèle.

## Liens avec le reste

`internal/store` est la couche fichiers, utilisée par `internal/agent` (un `keeper` qui enregistre après chaque tour et sert `/sessions` et `/resume`) et par `internal/acp` (un fichier par session, `session/load`, le rejeu dans `replay.go`). `internal/session.WithSystem` remplace le prompt enregistré par l'actuel. La compression (`internal/compact`) ignore la persistance : les façades enregistrent après son passage.