Bläddra i källkod

Mémoire des membres : sans réaction 📝, documentation finalisée

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 3 dagar sedan
förälder
incheckning
4fb058562d
5 ändrade filer med 65 tillägg och 74 borttagningar
  1. 6 4
      README.md
  2. 7 11
      bot/claude_client.py
  3. 0 13
      bot/discord_bot.py
  4. 1 2
      bot/memory.py
  5. 51 44
      changelog.txt

+ 6 - 4
README.md

@@ -63,11 +63,11 @@ Il te faut :
    - dans **Privileged Gateway Intents**, active **Message Content Intent**. Les intents « Presence » et « Server Members » ne servent pas.
    - dans **Privileged Gateway Intents**, active **Message Content Intent**. Les intents « Presence » et « Server Members » ne servent pas.
 3. Onglet **OAuth2 > URL Generator** :
 3. Onglet **OAuth2 > URL Generator** :
    - coche les scopes **bot** et **applications.commands** (ce dernier pour les commandes comme `/reset` et `/memoire`) ;
    - coche les scopes **bot** et **applications.commands** (ce dernier pour les commandes comme `/reset` et `/memoire`) ;
-   - dans les permissions, coche **View Channels**, **Send Messages** et **Read Message History**, plus **Add Reactions** avec `MEMORY=on` ;
+   - dans les permissions, coche **View Channels**, **Send Messages** et **Read Message History** ;
    - ouvre l'adresse générée et invite le bot sur ton serveur.
    - ouvre l'adresse générée et invite le bot sur ton serveur.
 4. Dans Discord, active le mode développeur (**Paramètres utilisateur > Avancés > Mode développeur**), puis fais un clic droit sur le salon du bot > **Copier l'identifiant du salon** : c'est le `CHANNEL_ID`.
 4. Dans Discord, active le mode développeur (**Paramètres utilisateur > Avancés > Mode développeur**), puis fais un clic droit sur le salon du bot > **Copier l'identifiant du salon** : c'est le `CHANNEL_ID`.
 
 
-Le bot n'a besoin d'aucune autre permission : il n'utilise que les emojis et stickers du serveur. Pour un bot déjà invité, **Add Reactions** s'ajoute à son rôle dans les paramètres du serveur.
+Le bot n'a besoin d'aucune autre permission : il n'utilise que les emojis et stickers du serveur.
 
 
 ## Installation
 ## Installation
 
 
@@ -104,6 +104,7 @@ Tous les réglages se trouvent dans le `.env`, et chacun est expliqué dans `.en
 | `CONTEXT_MESSAGE_LIMIT` | Messages lus autour d'une mention | `20` |
 | `CONTEXT_MESSAGE_LIMIT` | Messages lus autour d'une mention | `20` |
 | `WEB_SEARCH` | Recherche web et lecture de liens (`on` ou `off`) ; prévois alors un `API_TIMEOUT` d'au moins 90 secondes | `off` |
 | `WEB_SEARCH` | Recherche web et lecture de liens (`on` ou `off`) ; prévois alors un `API_TIMEOUT` d'au moins 90 secondes | `off` |
 | `PRESENCE` | Statut Discord et absences (`on` ou `off`) ; les `PRESENCE_*` règlent les chances de chaque statut et la nuit | `on` |
 | `PRESENCE` | Statut Discord et absences (`on` ou `off`) ; les `PRESENCE_*` règlent les chances de chaque statut et la nuit | `on` |
+| `MEMORY` | Mémoire des membres (`on` ou `off`) ; `MEMORY_MEMBER_LIMIT` et `MEMORY_SERVER_LIMIT` règlent la taille des fiches et du fichier général (3000 et 5000 caractères) | `off` |
 | `PDF_MAX_PAGES` | Pages maximales d'un PDF (0 = PDF refusés) | `20` |
 | `PDF_MAX_PAGES` | Pages maximales d'un PDF (0 = PDF refusés) | `20` |
 | `PERSONALITY_FILE` | Fichier de la personnalité | `personality_prompt.txt` |
 | `PERSONALITY_FILE` | Fichier de la personnalité | `personality_prompt.txt` |
 | `TIMEZONE` | Fuseau horaire de l'heure donnée à Claude | `Europe/Paris` |
 | `TIMEZONE` | Fuseau horaire de l'heure donnée à Claude | `Europe/Paris` |
@@ -122,7 +123,7 @@ Pas besoin d'y recopier la liste des emojis du serveur : le bot la donne lui-mê
 
 
 Le `Dockerfile` construit une image prête à lancer. À chaque push sur `main`, GitHub Actions la construit et la publie sur Docker Hub (`<compte>/chatbot-maisonnette`, avec les secrets `DOCKER_USERNAME` et `DOCKER_PASSWORD` du dépôt).
 Le `Dockerfile` construit une image prête à lancer. À chaque push sur `main`, GitHub Actions la construit et la publie sur Docker Hub (`<compte>/chatbot-maisonnette`, avec les secrets `DOCKER_USERNAME` et `DOCKER_PASSWORD` du dépôt).
 
 
-L'image ne contient ni `.env`, ni personnalité `*.local.txt`, ni historique. Les réglages se passent en variables d'environnement, et les fichiers à garder entre deux redémarrages doivent être sur un volume, par exemple :
+L'image ne contient ni `.env`, ni personnalité `*.local.txt`, ni historique, ni mémoire des membres. Les réglages se passent en variables d'environnement, et les fichiers à garder entre deux redémarrages doivent être sur un volume. Le dossier `memories/` se crée à côté de `HISTORY_FILE`, donc sur le même volume. Par exemple :
 
 
 ```
 ```
 docker run -d --name chatbot \
 docker run -d --name chatbot \
@@ -142,7 +143,8 @@ Chaque message envoyé à Claude est facturé selon le nombre de tokens lus (per
 - la personnalité et la mémoire sont mises en cache : relues dans les 5 minutes, elles coûtent 10 % du prix normal ;
 - la personnalité et la mémoire sont mises en cache : relues dans les 5 minutes, elles coûtent 10 % du prix normal ;
 - les images, PDF et fichiers ne sont payés qu'une fois, au message qui les contient ;
 - les images, PDF et fichiers ne sont payés qu'une fois, au message qui les contient ;
 - `claude-haiku-5-5` coûte environ 20 fois moins cher que `claude-sonnet-5-5` ; `THINKING=off` et un `EFFORT` bas réduisent aussi les réponses ;
 - `claude-haiku-5-5` coûte environ 20 fois moins cher que `claude-sonnet-5-5` ; `THINKING=off` et un `EFFORT` bas réduisent aussi les réponses ;
-- chaque page de PDF coûte souvent plusieurs milliers de tokens, et chaque recherche web est payante en plus.
+- chaque page de PDF coûte souvent plusieurs milliers de tokens, et chaque recherche web est payante en plus ;
+- avec `MEMORY=on`, les fiches jointes sont relues à chaque message, hors cache : environ 850 tokens par fiche pleine de 3000 caractères. Chaque note demande en plus un court appel, la conversation étant alors relue en cache.
 
 
 Le fichier `bot.log` (`LOG_FILE`) garde l'activité du bot, avec pour chaque appel les tokens utilisés et le coût estimé en dollars. Les tarifs de Sonnet, Haiku et Opus 5.5 sont connus du bot ; pour un autre modèle, renseigne `PRICE_INPUT_PER_MTOK` et `PRICE_OUTPUT_PER_MTOK`.
 Le fichier `bot.log` (`LOG_FILE`) garde l'activité du bot, avec pour chaque appel les tokens utilisés et le coût estimé en dollars. Les tarifs de Sonnet, Haiku et Opus 5.5 sont connus du bot ; pour un autre modèle, renseigne `PRICE_INPUT_PER_MTOK` et `PRICE_OUTPUT_PER_MTOK`.
 
 

+ 7 - 11
bot/claude_client.py

@@ -70,7 +70,8 @@ MEMBER_MEMORY_NOTE = (
     "général sur le serveur (blagues récurrentes, événements, habitudes). Quand tu apprends sur quelqu'un "
     "général sur le serveur (blagues récurrentes, événements, habitudes). Quand tu apprends sur quelqu'un "
     "quelque chose qui vaut la peine d'être retenu, note-le dans sa fiche, quelle que soit la personne qui "
     "quelque chose qui vaut la peine d'être retenu, note-le dans sa fiche, quelle que soit la personne qui "
     "te l'apprend ; une info donnée par quelqu'un d'autre que l'intéressé se note avec sa source "
     "te l'apprend ; une info donnée par quelqu'un d'autre que l'intéressé se note avec sa source "
-    "(« d'après Toto »). Inutile de dire dans ta réponse tout ce que tu retiens. "
+    "(« d'après Toto »). Ne dis pas que tu notes quelque chose : c'est plus tard, quand tu t'en souviens, qu'on doit "
+    "découvrir que tu l'as retenu. "
     "Écris en style télégraphique, une ligne par fait, sans doublon, et mets à jour la ligne existante plutôt "
     "Écris en style télégraphique, une ligne par fait, sans doublon, et mets à jour la ligne existante plutôt "
     "que d'en ajouter une (« jeu préféré : Zelda » devient « jeu préféré : Elden Ring »). La première ligne "
     "que d'en ajouter une (« jeu préféré : Zelda » devient « jeu préféré : Elden Ring »). La première ligne "
     "d'une fiche est « Surnoms : … » : complète-la dès que tu comprends qu'un surnom désigne ce membre. "
     "d'une fiche est « Surnoms : … » : complète-la dès que tu comprends qu'un surnom désigne ce membre. "
@@ -160,7 +161,6 @@ class ImageData:
 class Answer:
 class Answer:
     text: str
     text: str
     web_used: bool  # Claude a fait au moins une recherche web ou lu une page
     web_used: bool  # Claude a fait au moins une recherche web ou lu une page
-    memory_written: bool = False  # Claude a noté quelque chose dans sa mémoire
 
 
 
 
 class ClaudeClient:
 class ClaudeClient:
@@ -293,7 +293,6 @@ class ClaudeClient:
 
 
         responses = [await self._call(params, attachments)]
         responses = [await self._call(params, attachments)]
         pauses = operations = 0
         pauses = operations = 0
-        memory_written = False
         while True:
         while True:
             last = responses[-1]
             last = responses[-1]
             # Lors de longues recherches, l'API peut rendre la main avant la fin (pause_turn) :
             # Lors de longues recherches, l'API peut rendre la main avant la fin (pause_turn) :
@@ -304,9 +303,8 @@ class ClaudeClient:
             # Appel à l'outil « memoire » : le bot l'exécute et renvoie le résultat. Une fois la limite
             # Appel à l'outil « memoire » : le bot l'exécute et renvoie le résultat. Une fois la limite
             # d'opérations atteinte, un dernier tour lui demande de répondre, puis la boucle s'arrête.
             # d'opérations atteinte, un dernier tour lui demande de répondre, puis la boucle s'arrête.
             elif last.stop_reason == "tool_use" and self.memory and operations <= self.settings.memory_max_operations:
             elif last.stop_reason == "tool_use" and self.memory and operations <= self.settings.memory_max_operations:
-                results, written = await self._run_tools(last, operations)
+                results = await self._run_tools(last, operations)
                 follow_up = [{"role": "user", "content": results}]
                 follow_up = [{"role": "user", "content": results}]
-                memory_written = memory_written or written
                 operations += len(results)
                 operations += len(results)
             else:
             else:
                 break
                 break
@@ -331,14 +329,13 @@ class ClaudeClient:
         if not text:
         if not text:
             logger.warning("Réponse vide de Claude (stop_reason=%s).", stop_reason)
             logger.warning("Réponse vide de Claude (stop_reason=%s).", stop_reason)
             raise ClaudeError("Je n'ai rien trouvé à répondre à ça.")
             raise ClaudeError("Je n'ai rien trouvé à répondre à ça.")
-        return Answer(text, web_used, memory_written)
+        return Answer(text, web_used)
 
 
-    async def _run_tools(self, response, done: int) -> tuple[list[dict], bool]:
+    async def _run_tools(self, response, done: int) -> list[dict]:
         """Exécute les appels à l'outil « memoire » d'une réponse. Renvoie leurs résultats, à envoyer
         """Exécute les appels à l'outil « memoire » d'une réponse. Renvoie leurs résultats, à envoyer
-        tous dans un même message comme l'exige l'API, et si un fichier a été modifié.
+        tous dans un même message comme l'exige l'API.
         done : opérations déjà faites pour cette réponse."""
         done : opérations déjà faites pour cette réponse."""
         results = []
         results = []
-        written = False
         for block in response.content:
         for block in response.content:
             if block.type != "tool_use":
             if block.type != "tool_use":
                 continue
                 continue
@@ -350,10 +347,9 @@ class ClaudeClient:
             else:
             else:
                 result = await self.memory.run(block.input)
                 result = await self.memory.run(block.input)
             done += 1
             done += 1
-            written = written or result.written
             results.append({"type": "tool_result", "tool_use_id": block.id, "content": result.content,
             results.append({"type": "tool_result", "tool_use_id": block.id, "content": result.content,
                             "is_error": result.is_error})
                             "is_error": result.is_error})
-        return results, written
+        return results
 
 
     async def write_custom_status(self, kind: str, now: str, duration: str, recent: list[str],
     async def write_custom_status(self, kind: str, now: str, duration: str, recent: list[str],
                                   max_length: int) -> str | None:
                                   max_length: int) -> str | None:

+ 0 - 13
bot/discord_bot.py

@@ -42,8 +42,6 @@ QUOTE_MAX_LENGTH = 500
 USER_MESSAGE_TYPES = (discord.MessageType.default, discord.MessageType.reply)
 USER_MESSAGE_TYPES = (discord.MessageType.default, discord.MessageType.reply)
 # En passant absent ou en « Ne pas déranger », le bot prévient son salon si on y a parlé depuis moins de :
 # En passant absent ou en « Ne pas déranger », le bot prévient son salon si on y a parlé depuis moins de :
 DEPARTURE_WINDOW = timedelta(minutes=3)
 DEPARTURE_WINDOW = timedelta(minutes=3)
-# Ajoutée au message auquel le bot répond quand il a noté quelque chose dans sa mémoire.
-NOTED_REACTION = "📝"
 # Envoyé une seule fois à chaque membre, à son premier message, avec MEMORY=on.
 # Envoyé une seule fois à chaque membre, à son premier message, avec MEMORY=on.
 WELCOME = ("{name} peut retenir des choses sur toi au fil des conversations (tes goûts, tes projets…) "
 WELCOME = ("{name} peut retenir des choses sur toi au fil des conversations (tes goûts, tes projets…) "
            "pour mieux te connaître. Tape /memoire sur le serveur pour voir ce qui a été retenu, le modifier "
            "pour mieux te connaître. Tape /memoire sur le serveur pour voir ce qui a été retenu, le modifier "
@@ -406,8 +404,6 @@ class ChatBot(commands.Bot):
                 self.forget_waiting(*(p.message for p in batch))
                 self.forget_waiting(*(p.message for p in batch))
             await self.history.add_exchange(channel.id, lines, short_emojis(answer.text), answer.web_used)
             await self.history.add_exchange(channel.id, lines, short_emojis(answer.text), answer.web_used)
         await self.send_answer(last, answer.text)
         await self.send_answer(last, answer.text)
-        if answer.memory_written:
-            await self.mark_noted(last)
 
 
     async def handle_mention(self, message: discord.Message) -> None:
     async def handle_mention(self, message: discord.Message) -> None:
         if not self.available() or message.channel.id in self._waiters:
         if not self.available() or message.channel.id in self._waiters:
@@ -493,8 +489,6 @@ class ChatBot(commands.Bot):
                 return
                 return
         # En retard, la réponse cite la mention : on sait à quoi elle répond.
         # En retard, la réponse cite la mention : on sait à quoi elle répond.
         await self.send_answer(message, answer.text, reply=late is not None)
         await self.send_answer(message, answer.text, reply=late is not None)
-        if answer.memory_written:
-            await self.mark_noted(message)
 
 
     # ------------------------------------------------------ réponses différées
     # ------------------------------------------------------ réponses différées
 
 
@@ -831,13 +825,6 @@ class ChatBot(commands.Bot):
                 logger.error("Envoi de la réponse impossible : %s", exc)
                 logger.error("Envoi de la réponse impossible : %s", exc)
                 return
                 return
 
 
-    async def mark_noted(self, message: discord.Message) -> None:
-        """Réaction 📝 sur le message auquel le bot vient de répondre : il a noté quelque chose."""
-        try:
-            await message.add_reaction(NOTED_REACTION)
-        except discord.HTTPException as exc:  # permission « Ajouter des réactions » absente, message supprimé…
-            logger.warning("Réaction %s impossible : %s", NOTED_REACTION, exc)
-
     async def safe_reply(self, message: discord.Message, text: str) -> None:
     async def safe_reply(self, message: discord.Message, text: str) -> None:
         try:
         try:
             await message.reply(text, mention_author=False)
             await message.reply(text, mention_author=False)

+ 1 - 2
bot/memory.py

@@ -51,7 +51,6 @@ MEMORY_TOOL = {
 class ToolResult:
 class ToolResult:
     content: str
     content: str
     is_error: bool = False
     is_error: bool = False
-    written: bool = False  # un fichier a été modifié
 
 
 
 
 class MemoryStore:
 class MemoryStore:
@@ -184,7 +183,7 @@ class MemoryStore:
                 logger.error("Mémoire : impossible d'écrire %s : %s", name, exc)
                 logger.error("Mémoire : impossible d'écrire %s : %s", name, exc)
                 return ToolResult("Écriture impossible pour l'instant, à cause d'une erreur du bot.", is_error=True)
                 return ToolResult("Écriture impossible pour l'instant, à cause d'une erreur du bot.", is_error=True)
         logger.info("Mémoire : %s sur %s (%s), %d → %d caractères", action, name, label, len(before), len(after))
         logger.info("Mémoire : %s sur %s (%s), %d → %d caractères", action, name, label, len(before), len(after))
-        return ToolResult(f"C'est fait. {name} : {len(after)}/{limit} caractères.", written=True)
+        return ToolResult(f"C'est fait. {name} : {len(after)}/{limit} caractères.")
 
 
     def limit(self, name: str) -> int:
     def limit(self, name: str) -> int:
         return self.server_limit if name == SERVER_FILE else self.member_limit
         return self.server_limit if name == SERVER_FILE else self.member_limit

+ 51 - 44
changelog.txt

@@ -6,55 +6,62 @@ Version 5.10
 ------------
 ------------
 
 
 Nouveautés
 Nouveautés
-- Mémoire des membres (MEMORY=on, en construction) : le bot tient un
-  registre des membres qu'il voit : auteurs des messages qu'il lit,
-  membres mentionnés et auteurs des messages cités, sans les bots. Pour chacun, il garde le
-  nom d'utilisateur, le nom d'affichage, le pseudo sur le serveur et les
-  5 derniers anciens pseudos, mis à jour à chaque message. Le registre
-  est dans memories/registre.json, à côté de l'historique (MEMORY_DIR
-  pour un autre dossier), et n'est réécrit que si quelque chose change.
-  Rika tient une fiche par membre (memories/membres/<id>.md) et un
-  fichier général du serveur (memories/serveur.md), avec un outil
-  « memoire » exécuté par le bot : lire, ajouter, remplacer ou réécrire,
-  dans ces seuls fichiers. Elle note en style télégraphique, une ligne
-  par fait, avec la source quand l'info vient d'un autre membre. Une
-  fiche est limitée à 3000 caractères (MEMORY_MEMBER_LIMIT) et le
-  fichier général à 5000 (MEMORY_SERVER_LIMIT) : une note de trop est
-  refusée, et Rika réécrit alors le fichier en plus court. Au plus 5
-  lectures ou écritures par réponse (MEMORY_MAX_OPERATIONS). Quand elle
-  a noté quelque chose, elle ajoute 📝 au message auquel elle répond
-  (permission « Ajouter des réactions » nécessaire). Outil maison plutôt
-  que celui d'Anthropic, qui lui ferait relire tout le dossier avant
-  chaque réponse (un appel de plus à chaque message).
-  Le bot joint lui-même à chaque message les fiches des membres
-  concernés (auteurs, membres mentionnés, auteur du message cité), sans
-  appel supplémentaire ; elles ne sont pas enregistrées dans
-  l'historique, et le cache s'arrête juste avant elles pour que
-  l'historique reste relu en cache au message suivant. Le prompt
-  contient aussi la liste des membres, avec tous leurs noms (pseudo,
-  nom d'utilisateur, surnoms de la fiche, anciens pseudos) et le chemin
-  de leur fiche, ce qui permet à Rika de noter une info sur quelqu'un
-  cité par son seul nom ou surnom ; ainsi que le fichier général. Les
-  membres qui ont désactivé la mémoire n'y figurent pas.
+- Mémoire des membres (MEMORY=on) : Rika tient une fiche sur chaque
+  membre et un fichier général du serveur (blagues récurrentes,
+  événements, habitudes). Elle décide seule de ce qui mérite d'être
+  retenu, quelle que soit la personne qui le lui apprend, et note la
+  source quand l'info vient d'un autre membre (« d'après Toto »). Elle
+  écrit en style télégraphique, une ligne par fait, sans doublon, et met
+  à jour une ligne plutôt que d'en ajouter une. Elle ne signale pas ce
+  qu'elle note : on le découvre plus tard, quand elle s'en souvient.
+- Ce que Rika voit : le bot joint lui-même à chaque message les fiches
+  des membres concernés (auteurs, membres mentionnés, auteur du message
+  cité), sans appel supplémentaire. Son prompt contient aussi le fichier
+  général et la liste des membres, avec tous leurs noms (pseudo, nom
+  d'utilisateur, surnoms notés en tête de fiche, anciens pseudos) et le
+  chemin de leur fiche : elle peut ainsi noter une info sur quelqu'un
+  cité par son seul nom ou surnom, et lire sa fiche si besoin. Les
+  fiches jointes ne sont pas enregistrées dans l'historique, et le cache
+  s'arrête juste avant elles : l'historique reste relu en cache.
+- Comment elle écrit : avec un outil « memoire » exécuté par le bot
+  (lire, ajouter, remplacer, réécrire), limité à ces seuls fichiers. Une
+  fiche fait au plus 3000 caractères (MEMORY_MEMBER_LIMIT) et le fichier
+  général 5000 (MEMORY_SERVER_LIMIT) : une note de trop est refusée, et
+  Rika réécrit alors le fichier en plus court, en fusionnant et en
+  retirant le moins important. Au plus 5 lectures ou écritures par
+  réponse (MEMORY_MAX_OPERATIONS). Outil maison plutôt que celui
+  d'Anthropic, qui lui ferait relire tout le dossier avant chaque
+  réponse, soit un appel de plus à chaque message.
 - /memoire : chaque membre voit sa fiche, visible par lui seul, et peut
 - /memoire : chaque membre voit sa fiche, visible par lui seul, et peut
   la modifier dans une fenêtre déjà remplie, tout effacer ou désactiver
   la modifier dans une fenêtre déjà remplie, tout effacer ou désactiver
-  la mémoire (avec confirmation : sa fiche est alors effacée, plus rien
-  n'est retenu sur lui ni joint à ses messages). Il peut la réactiver au
-  même endroit. Les administrateurs (permission Administrateur) voient,
+  la mémoire (avec confirmation : sa fiche est effacée, plus rien n'est
+  retenu sur lui ni joint à ses messages). Il peut la réactiver au même
+  endroit. Les administrateurs (permission Administrateur) voient,
   modifient et effacent la fiche de n'importe qui avec
   modifient et effacent la fiche de n'importe qui avec
   /memoire membre:@… ; seul le membre lui-même peut désactiver sa
   /memoire membre:@… ; seul le membre lui-même peut désactiver sa
   mémoire. /memoire-serveur, réservé aux administrateurs, fait de même
   mémoire. /memoire-serveur, réservé aux administrateurs, fait de même
-  pour le fichier général. Avec MEMORY=off, les deux commandes le
-  signalent. MEMORY_MEMBER_LIMIT et MEMORY_SERVER_LIMIT ne peuvent pas
-  dépasser 20000 (taille maximale de la fenêtre de modification).
-- Présentation de la mémoire : au premier message d'un membre, le bot
-  lui envoie une seule fois un message privé qui explique ce qu'il peut
-  retenir et comment le voir, le modifier ou le désactiver avec
-  /memoire. Si ses messages privés sont fermés, le message est posté
-  dans le salon, avec son nom en mention mais sans notification, et
-  s'efface au bout d'une minute. Les membres déjà actifs sont prévenus
-  à leur premier message après le passage à la 5.10 ; un membre qui a
-  déjà utilisé /memoire ne l'est pas.
+  pour le fichier général.
+- Présentation : au premier message d'un membre, le bot lui envoie une
+  seule fois un message privé qui explique ce qu'il peut retenir et
+  comment le voir, le modifier ou le désactiver avec /memoire. Si ses
+  messages privés sont fermés, le message est posté dans le salon, avec
+  son nom en mention mais sans notification, et s'efface au bout d'une
+  minute. Les membres déjà actifs sont prévenus à leur premier message
+  après le passage à la 5.10 ; un membre qui a déjà utilisé /memoire
+  ne l'est pas.
+- Registre des membres : le bot garde, pour chaque membre vu, son nom
+  d'utilisateur, son nom d'affichage, son pseudo sur le serveur et ses
+  5 derniers anciens pseudos, mis à jour à chaque message, ainsi que ses
+  réglages de la mémoire. Tout est dans le dossier memories/, à côté de
+  l'historique (MEMORY_DIR pour un autre dossier) : registre.json,
+  serveur.md et membres/<id>.md. Avec Docker, il est donc sur le même
+  volume que l'historique.
+- Mise en service : MEMORY=on dans le .env (ou la ConfigMap), sans
+  nouvelle permission Discord. Avec MEMORY=off (par défaut), rien ne
+  change, et /memoire et /memoire-serveur indiquent que la mémoire est
+  coupée.
+  MEMORY_MEMBER_LIMIT et MEMORY_SERVER_LIMIT ne peuvent pas dépasser
+  20000 (taille maximale de la fenêtre de modification).
 
 
 Divers
 Divers
 - /reset et /skip sont réservés aux membres qui ont la permission
 - /reset et /skip sont réservés aux membres qui ont la permission