- Accueil
- Documentation
- Sessions
- Stockage de session et modèle d'entrée
Stockage de session et modèle d'entrée
Ce document est la source de vérité concernant la manière dont les sessions de l’agent de codage sont représentées, persistées, migrées et reconstruites à l’exécution.
Couvre :
- Le format JSONL des sessions et le versionnage
- La taxonomie des entrées et la sémantique arborescente (
id/parentId+ pointeur de feuille) - Le comportement de migration/compatibilité lors du chargement de fichiers anciens ou malformés
- La reconstruction du contexte (
buildSessionContext) - Les garanties de persistance, le comportement en cas d’échec, la troncature/externalisation des blobs
- Les abstractions de stockage (
FileSessionStorage,MemorySessionStorage) et les utilitaires associés
Ne couvre pas le comportement de rendu de l’interface /tree au-delà des sémantiques qui affectent les données de session.
Fichiers d’implémentation
Section intitulée « Fichiers d’implémentation »src/session/session-manager.tssrc/session/messages.tssrc/session/session-storage.tssrc/session/history-storage.tssrc/session/blob-store.ts
Organisation sur le disque
Section intitulée « Organisation sur le disque »Emplacement par défaut du fichier de session :
~/.xcsh/agent/sessions/--<cwd-encoded>--/<timestamp>_<sessionId>.jsonl<cwd-encoded> est dérivé du répertoire de travail en supprimant le slash initial et en remplaçant /, \\ et : par -.
Emplacement du magasin de blobs :
~/.xcsh/agent/blobs/<sha256>Les fichiers de fil d’Ariane du terminal sont écrits sous :
~/.xcsh/agent/terminal-sessions/<terminal-id>Le contenu du fil d’Ariane se compose de deux lignes : le répertoire de travail original, puis le chemin du fichier de session. continueRecent() privilégie ce pointeur limité au terminal avant de scanner le mtime le plus récent.
Format de fichier
Section intitulée « Format de fichier »Les fichiers de session sont en JSONL : un objet JSON par ligne.
- La ligne 1 est toujours l’en-tête de session (
type: "session"). - Les lignes restantes sont des valeurs
SessionEntry. - Les entrées sont en ajout uniquement à l’exécution ; la navigation entre branches déplace un pointeur (
leafId) plutôt que de modifier les entrées existantes.
En-tête (SessionHeader)
Section intitulée « En-tête (SessionHeader) »{ "type": "session", "version": 3, "id": "1f9d2a6b9c0d1234", "timestamp": "2026-02-16T10:20:30.000Z", "cwd": "/work/pi", "title": "optional session title", "parentSession": "optional lineage marker"}Notes :
versionest optionnel dans les fichiers v1 ; son absence signifie v1.parentSessionest une chaîne de lignage opaque. Le code actuel écrit soit un identifiant de session, soit un chemin de session selon le flux (fork,forkFrom,createBranchedSession, ounewSession({ parentSession })explicite). À traiter comme des métadonnées, pas comme une clé étrangère typée.
Base d’entrée (SessionEntryBase)
Section intitulée « Base d’entrée (SessionEntryBase) »Toutes les entrées non-en-tête incluent :
{ "type": "...", "id": "8-char-id", "parentId": "previous-or-branch-parent", "timestamp": "2026-02-16T10:20:30.000Z"}parentId peut être null pour une entrée racine (premier ajout, ou après resetLeaf()).
Taxonomie des entrées
Section intitulée « Taxonomie des entrées »SessionEntry est l’union de :
messagethinking_level_changemodel_changecompactionbranch_summarycustomcustom_messagelabelttsr_injectionsession_initmode_change
Stocke directement un AgentMessage.
{ "type": "message", "id": "a1b2c3d4", "parentId": null, "timestamp": "2026-02-16T10:21:00.000Z", "message": { "role": "assistant", "provider": "anthropic", "model": "claude-sonnet-4-5", "content": [{ "type": "text", "text": "Done." }], "usage": { "input": 100, "output": 20, "cacheRead": 0, "cacheWrite": 0, "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "total": 0 } }, "timestamp": 1760000000000 }}model_change
Section intitulée « model_change »{ "type": "model_change", "id": "b1c2d3e4", "parentId": "a1b2c3d4", "timestamp": "2026-02-16T10:21:30.000Z", "model": "openai/gpt-4o", "role": "default"}role est optionnel ; son absence est traitée comme default lors de la reconstruction du contexte.
thinking_level_change
Section intitulée « thinking_level_change »{ "type": "thinking_level_change", "id": "c1d2e3f4", "parentId": "b1c2d3e4", "timestamp": "2026-02-16T10:22:00.000Z", "thinkingLevel": "high"}compaction
Section intitulée « compaction »{ "type": "compaction", "id": "d1e2f3a4", "parentId": "c1d2e3f4", "timestamp": "2026-02-16T10:23:00.000Z", "summary": "Conversation summary", "shortSummary": "Short recap", "firstKeptEntryId": "a1b2c3d4", "tokensBefore": 42000, "details": { "readFiles": ["src/a.ts"] }, "preserveData": { "hookState": true }, "fromExtension": false}branch_summary
Section intitulée « branch_summary »{ "type": "branch_summary", "id": "e1f2a3b4", "parentId": "a1b2c3d4", "timestamp": "2026-02-16T10:24:00.000Z", "fromId": "a1b2c3d4", "summary": "Summary of abandoned path", "details": { "note": "optional" }, "fromExtension": true}Si le branchement se fait depuis la racine (branchFromId === null), fromId est la chaîne littérale "root".
Persistance de l’état des extensions ; ignoré par buildSessionContext.
{ "type": "custom", "id": "f1a2b3c4", "parentId": "e1f2a3b4", "timestamp": "2026-02-16T10:25:00.000Z", "customType": "my-extension", "data": { "state": 1 }}custom_message
Section intitulée « custom_message »Message fourni par une extension qui participe au contexte LLM.
{ "type": "custom_message", "id": "a2b3c4d5", "parentId": "f1a2b3c4", "timestamp": "2026-02-16T10:26:00.000Z", "customType": "my-extension", "content": "Injected context", "display": true, "details": { "debug": false }}{ "type": "label", "id": "b2c3d4e5", "parentId": "a2b3c4d5", "timestamp": "2026-02-16T10:27:00.000Z", "targetId": "a1b2c3d4", "label": "checkpoint"}label: undefined efface un label pour targetId.
ttsr_injection
Section intitulée « ttsr_injection »{ "type": "ttsr_injection", "id": "c2d3e4f5", "parentId": "b2c3d4e5", "timestamp": "2026-02-16T10:28:00.000Z", "injectedRules": ["ruleA", "ruleB"]}session_init
Section intitulée « session_init »{ "type": "session_init", "id": "d2e3f4a5", "parentId": "c2d3e4f5", "timestamp": "2026-02-16T10:29:00.000Z", "systemPrompt": "...", "task": "...", "tools": ["read", "edit"], "outputSchema": { "type": "object" }}mode_change
Section intitulée « mode_change »{ "type": "mode_change", "id": "e2f3a4b5", "parentId": "d2e3f4a5", "timestamp": "2026-02-16T10:30:00.000Z", "mode": "plan", "data": { "planFile": "/tmp/plan.md" }}Versionnage et migration
Section intitulée « Versionnage et migration »Version actuelle de session : 3.
v1 -> v2
Section intitulée « v1 -> v2 »Appliquée lorsque le version de l’en-tête est absent ou < 2 :
- Ajoute
idetparentIdà chaque entrée non-en-tête. - Reconstruit une chaîne parentale linéaire en utilisant l’ordre du fichier.
- Migre le champ de compaction
firstKeptEntryIndex->firstKeptEntryIdlorsqu’il est présent. - Définit
version = 2dans l’en-tête.
v2 -> v3
Section intitulée « v2 -> v3 »Appliquée lorsque le version de l’en-tête est < 3 :
- Pour les entrées
message: réécrit l’ancienmessage.role === "hookMessage"en"custom". - Définit
version = 3dans l’en-tête.
Déclencheur de migration et persistance
Section intitulée « Déclencheur de migration et persistance »- Les migrations s’exécutent lors du chargement de session (
setSessionFile). - Si une migration a été exécutée, le fichier entier est réécrit sur le disque immédiatement.
- La migration modifie d’abord les entrées en mémoire, puis persiste le JSONL réécrit.
Comportement de chargement et compatibilité
Section intitulée « Comportement de chargement et compatibilité »Comportement de loadEntriesFromFile(path) :
- Fichier manquant (
ENOENT) -> retourne[]. - Les lignes non analysables sont gérées par le parseur JSONL tolérant (
parseJsonlLenient). - Si la première entrée analysée n’est pas un en-tête de session valide (
type !== "session"ouidde type chaîne manquant) -> retourne[].
Comportement de SessionManager.setSessionFile() :
[]retourné par le chargeur est traité comme une session vide/inexistante et remplacé par un nouveau fichier de session initialisé à ce chemin.- Les fichiers valides sont chargés, migrés si nécessaire, les références de blobs sont résolues, puis indexés.
Sémantique de l’arbre et des feuilles
Section intitulée « Sémantique de l’arbre et des feuilles »Le modèle sous-jacent est un arbre en ajout uniquement + un pointeur de feuille mutable :
- Chaque méthode d’ajout crée exactement une nouvelle entrée dont le
parentIdest leleafIdactuel. - La nouvelle entrée devient le nouveau
leafId. branch(entryId)déplace uniquementleafId; les entrées existantes restent inchangées.resetLeaf()définitleafId = null; le prochain ajout crée une nouvelle entrée racine (parentId: null).branchWithSummary()définit la feuille sur la cible de branchement et ajoute une entréebranch_summary.
getEntries() retourne toutes les entrées non-en-tête dans l’ordre d’insertion. Les entrées existantes ne sont pas supprimées en fonctionnement normal ; les réécritures préservent l’historique logique tout en mettant à jour la représentation (migrations, déplacements, utilitaires de réécriture ciblée).
Reconstruction du contexte (buildSessionContext)
Section intitulée « Reconstruction du contexte (buildSessionContext) »buildSessionContext(entries, leafId, byId?) détermine ce qui est envoyé au modèle.
Algorithme :
- Déterminer la feuille :
leafId === null-> retourner un contexte vide.leafIdexplicite -> utiliser cette entrée si trouvée.- sinon, repli sur la dernière entrée.
- Remonter la chaîne
parentIdde la feuille à la racine et inverser pour obtenir le chemin racine->feuille. - Dériver l’état à l’exécution à travers le chemin :
thinkingLeveldepuis le dernierthinking_level_change(par défaut"off")- carte des modèles depuis les entrées
model_change(role ?? "default") models.defaultde repli depuis le provider/modèle du message assistant s’il n’y a pas de changement de modèle expliciteinjectedTtsrRulesdédupliquées depuis toutes les entréesttsr_injection- mode/modeData depuis le dernier
mode_change(mode par défaut"none")
- Construire la liste des messages :
- Les entrées
messagepassent directement - Les entrées
custom_messagedeviennent des AgentMessagescustomviacreateCustomMessage - Les entrées
branch_summarydeviennent des AgentMessagesbranchSummaryviacreateBranchSummaryMessage - Si une
compactionexiste sur le chemin :- Émettre d’abord le résumé de compaction (
createCompactionSummaryMessage) - Émettre les entrées du chemin à partir de
firstKeptEntryIdjusqu’à la limite de compaction - Émettre les entrées après la limite de compaction
- Émettre d’abord le résumé de compaction (
- Les entrées
Les entrées custom et session_init n’injectent pas directement de contexte dans le modèle.
Garanties de persistance et modèle de défaillance
Section intitulée « Garanties de persistance et modèle de défaillance »Persistance vs en mémoire
Section intitulée « Persistance vs en mémoire »SessionManager.create/open/continueRecent/forkFrom-> mode persistant (persist = true).SessionManager.inMemory-> mode non persistant (persist = false) avecMemorySessionStorage.
Pipeline d’écriture
Section intitulée « Pipeline d’écriture »Les écritures sont sérialisées via une chaîne de promesses interne (#persistChain) et NdjsonFileWriter.
append*met à jour l’état en mémoire immédiatement.- La persistance est différée jusqu’à ce qu’au moins un message assistant existe.
- Avant le premier assistant : les entrées sont conservées en mémoire ; aucun ajout au fichier ne se produit.
- Lorsque le premier assistant existe : l’intégralité de la session en mémoire est vidée dans le fichier.
- Par la suite : les nouvelles entrées sont ajoutées de manière incrémentale.
Justification dans le code : éviter de persister des sessions qui n’ont jamais produit de réponse assistant.
Opérations de durabilité
Section intitulée « Opérations de durabilité »flush()vide l’écrivain et appellefsync().- Les réécritures atomiques complètes (
#rewriteFile) écrivent dans un fichier temporaire, effectuent flush+fsync, ferment, puis renomment par-dessus la cible. - Utilisées pour les migrations,
setSessionName,rewriteEntries, les opérations de déplacement et les réécritures d’arguments d’appels d’outils.
Comportement en cas d’erreur
Section intitulée « Comportement en cas d’erreur »- Les erreurs de persistance sont verrouillées (
#persistError) et relancées lors des opérations suivantes. - La première erreur est journalisée une seule fois avec le contexte du fichier de session.
- La fermeture de l’écrivain est en meilleur effort mais propage la première erreur significative.
Contrôles de taille des données et externalisation des blobs
Section intitulée « Contrôles de taille des données et externalisation des blobs »Avant de persister les entrées :
- Les grandes chaînes sont tronquées à
MAX_PERSIST_CHARS(500 000 caractères) avec notification :"[Session persistence truncated large content]"
- Les champs transitoires
partialJsonetjsonlEventssont supprimés. - Si l’objet a à la fois
contentetlineCount, le nombre de lignes est recalculé après troncature. - Les blocs d’images dans les tableaux
contentavec une longueur base64 >= 1024 sont externalisés en références de blob :- stockés sous la forme
blob:sha256:<hash> - les octets bruts sont écrits dans le magasin de blobs (
BlobStore.put)
- stockés sous la forme
Au chargement, les références de blob sont résolues en base64 pour les blocs d’images des message/custom_message.
Abstractions de stockage
Section intitulée « Abstractions de stockage »L’interface SessionStorage fournit toutes les opérations du système de fichiers utilisées par SessionManager :
- synchrones :
ensureDirSync,existsSync,writeTextSync,statSync,listFilesSync - asynchrones :
exists,readText,readTextPrefix,writeText,rename,unlink,openWriter
Implémentations :
FileSessionStorage: système de fichiers réel (Bun + node fs)MemorySessionStorage: implémentation en mémoire basée sur une map pour les tests/sessions non persistantes
SessionStorageWriter expose writeLine, flush, fsync, close, getError.
Utilitaires de découverte de sessions
Section intitulée « Utilitaires de découverte de sessions »Définis dans session-manager.ts :
getRecentSessions(sessionDir, limit)-> métadonnées légères pour l’interface/sélecteur de sessionfindMostRecentSession(sessionDir)-> la plus récente par mtimelist(cwd, sessionDir?)-> sessions dans le périmètre d’un projetlistAll()-> sessions à travers tous les périmètres de projets sous~/.xcsh/agent/sessions
L’extraction des métadonnées ne lit qu’un préfixe (readTextPrefix(..., 4096)) lorsque c’est possible.
Connexe mais distinct : stockage de l’historique des prompts
Section intitulée « Connexe mais distinct : stockage de l’historique des prompts »HistoryStorage (history-storage.ts) est un sous-système SQLite séparé pour le rappel/la recherche de prompts, pas pour la relecture de sessions.
- Base de données :
~/.xcsh/agent/history.db - Table :
history(id, prompt, created_at, cwd) - Index FTS5 :
history_ftsavec synchronisation maintenue par déclencheurs - Déduplique les prompts identiques consécutifs en utilisant un cache en mémoire du dernier prompt
- Insertion asynchrone (
setImmediate) pour que la capture de prompt ne bloque pas l’exécution du tour
Utilisez les fichiers de session pour le graphe de conversation/la relecture d’état ; utilisez HistoryStorage pour l’expérience utilisateur de l’historique des prompts.