- Inicio
- Documentation
- Sesiones
- Almacenamiento de Sesiones y Modelo de Entradas
Almacenamiento de Sesiones y Modelo de Entradas
Este documento es la fuente de verdad sobre cómo las sesiones del agente de codificación se representan, persisten, migran y reconstruyen en tiempo de ejecución.
Alcance
Sección titulada «Alcance»Cubre:
- Formato JSONL de sesión y versionado
- Taxonomía de entradas y semántica de árbol (
id/parentId+ puntero de hoja) - Comportamiento de migración/compatibilidad al cargar archivos antiguos o mal formados
- Reconstrucción de contexto (
buildSessionContext) - Garantías de persistencia, comportamiento ante fallos, truncamiento/externalización de blobs
- Abstracciones de almacenamiento (
FileSessionStorage,MemorySessionStorage) y utilidades relacionadas
No cubre el comportamiento de renderizado de la UI /tree más allá de la semántica que afecta los datos de sesión.
Archivos de Implementación
Sección titulada «Archivos de Implementación»src/session/session-manager.tssrc/session/messages.tssrc/session/session-storage.tssrc/session/history-storage.tssrc/session/blob-store.ts
Estructura en Disco
Sección titulada «Estructura en Disco»Ubicación predeterminada del archivo de sesión:
~/.xcsh/agent/sessions/--<cwd-encoded>--/<timestamp>_<sessionId>.jsonl<cwd-encoded> se deriva del directorio de trabajo eliminando la barra inicial y reemplazando /, \\ y : por -.
Ubicación del almacén de blobs:
~/.xcsh/agent/blobs/<sha256>Los archivos de referencia de terminal se escriben en:
~/.xcsh/agent/terminal-sessions/<terminal-id>El contenido de la referencia son dos líneas: el cwd original, seguido de la ruta del archivo de sesión. continueRecent() prefiere este puntero con alcance de terminal antes de buscar el mtime más reciente.
Formato de Archivo
Sección titulada «Formato de Archivo»Los archivos de sesión son JSONL: un objeto JSON por línea.
- La línea 1 es siempre el encabezado de sesión (
type: "session"). - Las líneas restantes son valores
SessionEntry. - Las entradas son de solo escritura (append-only) en tiempo de ejecución; la navegación entre ramas mueve un puntero (
leafId) en lugar de mutar entradas existentes.
Encabezado (SessionHeader)
Sección titulada «Encabezado (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"}Notas:
versiones opcional en archivos v1; su ausencia significa v1.parentSessiones una cadena opaca de linaje. El código actual escribe ya sea un id de sesión o una ruta de sesión dependiendo del flujo (fork,forkFrom,createBranchedSession, onewSession({ parentSession })explícito). Trátelo como metadatos, no como una clave foránea tipada.
Base de Entrada (SessionEntryBase)
Sección titulada «Base de Entrada (SessionEntryBase)»Todas las entradas que no son encabezado incluyen:
{ "type": "...", "id": "8-char-id", "parentId": "previous-or-branch-parent", "timestamp": "2026-02-16T10:20:30.000Z"}parentId puede ser null para una entrada raíz (primer append, o después de resetLeaf()).
Taxonomía de Entradas
Sección titulada «Taxonomía de Entradas»SessionEntry es la unión de:
messagethinking_level_changemodel_changecompactionbranch_summarycustomcustom_messagelabelttsr_injectionsession_initmode_change
message
Sección titulada «message»Almacena un AgentMessage directamente.
{ "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
Sección titulada «model_change»{ "type": "model_change", "id": "b1c2d3e4", "parentId": "a1b2c3d4", "timestamp": "2026-02-16T10:21:30.000Z", "model": "openai/gpt-4o", "role": "default"}role es opcional; su ausencia se trata como default en la reconstrucción de contexto.
thinking_level_change
Sección titulada «thinking_level_change»{ "type": "thinking_level_change", "id": "c1d2e3f4", "parentId": "b1c2d3e4", "timestamp": "2026-02-16T10:22:00.000Z", "thinkingLevel": "high"}compaction
Sección titulada «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
Sección titulada «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 se ramifica desde la raíz (branchFromId === null), fromId es la cadena literal "root".
Persistencia de estado de extensiones; ignorado por buildSessionContext.
{ "type": "custom", "id": "f1a2b3c4", "parentId": "e1f2a3b4", "timestamp": "2026-02-16T10:25:00.000Z", "customType": "my-extension", "data": { "state": 1 }}custom_message
Sección titulada «custom_message»Mensaje proporcionado por una extensión que sí participa en el contexto del 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 elimina una etiqueta para targetId.
ttsr_injection
Sección titulada «ttsr_injection»{ "type": "ttsr_injection", "id": "c2d3e4f5", "parentId": "b2c3d4e5", "timestamp": "2026-02-16T10:28:00.000Z", "injectedRules": ["ruleA", "ruleB"]}session_init
Sección titulada «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
Sección titulada «mode_change»{ "type": "mode_change", "id": "e2f3a4b5", "parentId": "d2e3f4a5", "timestamp": "2026-02-16T10:30:00.000Z", "mode": "plan", "data": { "planFile": "/tmp/plan.md" }}Versionado y Migración
Sección titulada «Versionado y Migración»Versión actual de sesión: 3.
v1 -> v2
Sección titulada «v1 -> v2»Se aplica cuando version del encabezado está ausente o es < 2:
- Añade
idyparentIda cada entrada que no sea encabezado. - Reconstruye una cadena de padres lineal usando el orden del archivo.
- Migra el campo de compactación
firstKeptEntryIndex->firstKeptEntryIdcuando está presente. - Establece
version = 2en el encabezado.
v2 -> v3
Sección titulada «v2 -> v3»Se aplica cuando version del encabezado es < 3:
- Para entradas
message: reescribe el legadomessage.role === "hookMessage"a"custom". - Establece
version = 3en el encabezado.
Activación de Migración y Persistencia
Sección titulada «Activación de Migración y Persistencia»- Las migraciones se ejecutan durante la carga de la sesión (
setSessionFile). - Si alguna migración se ejecutó, el archivo completo se reescribe en disco inmediatamente.
- La migración muta las entradas en memoria primero, luego persiste el JSONL reescrito.
Comportamiento de Carga y Compatibilidad
Sección titulada «Comportamiento de Carga y Compatibilidad»Comportamiento de loadEntriesFromFile(path):
- Archivo faltante (
ENOENT) -> retorna[]. - Las líneas no parseables son manejadas por el parser JSONL tolerante (
parseJsonlLenient). - Si la primera entrada parseada no es un encabezado de sesión válido (
type !== "session"o faltaidcomo cadena) -> retorna[].
Comportamiento de SessionManager.setSessionFile():
[]del cargador se trata como sesión vacía/inexistente y se reemplaza con un nuevo archivo de sesión inicializado en esa ruta.- Los archivos válidos se cargan, migran si es necesario, se resuelven las referencias de blobs y luego se indexan.
Semántica de Árbol y Hoja
Sección titulada «Semántica de Árbol y Hoja»El modelo subyacente es un árbol de solo escritura + puntero de hoja mutable:
- Cada método de append crea exactamente una nueva entrada cuyo
parentIdes elleafIdactual. - La nueva entrada se convierte en el nuevo
leafId. branch(entryId)mueve sololeafId; las entradas existentes permanecen sin cambios.resetLeaf()estableceleafId = null; el siguiente append crea una nueva entrada raíz (parentId: null).branchWithSummary()establece la hoja en el destino de la rama y añade una entradabranch_summary.
getEntries() retorna todas las entradas que no son encabezado en orden de inserción. Las entradas existentes no se eliminan en operación normal; las reescrituras preservan el historial lógico mientras actualizan la representación (migraciones, movimiento, ayudantes de reescritura dirigida).
Reconstrucción de Contexto (buildSessionContext)
Sección titulada «Reconstrucción de Contexto (buildSessionContext)»buildSessionContext(entries, leafId, byId?) resuelve lo que se envía al modelo.
Algoritmo:
- Determinar la hoja:
leafId === null-> retorna contexto vacío.leafIdexplícito -> usa esa entrada si se encuentra.- de lo contrario, recurre a la última entrada.
- Recorre la cadena de
parentIddesde la hoja hasta la raíz e invierte al camino raíz->hoja. - Deriva el estado en tiempo de ejecución a lo largo del camino:
thinkingLeveldelthinking_level_changemás reciente (por defecto"off")- mapa de modelos de las entradas
model_change(role ?? "default") models.defaultde respaldo del proveedor/modelo del mensaje del asistente si no hay cambio de modelo explícitoinjectedTtsrRulesdeduplicadas de todas las entradasttsr_injection- modo/modeData del
mode_changemás reciente (modo por defecto"none")
- Construir la lista de mensajes:
- Las entradas
messagepasan directamente - Las entradas
custom_messagese convierten enAgentMessagesde tipocustomvíacreateCustomMessage - Las entradas
branch_summaryse convierten enAgentMessagesde tipobranchSummaryvíacreateBranchSummaryMessage - Si existe una
compactionen el camino:- emite primero el resumen de compactación (
createCompactionSummaryMessage) - emite las entradas del camino comenzando en
firstKeptEntryIdhasta el límite de compactación - emite las entradas después del límite de compactación
- emite primero el resumen de compactación (
- Las entradas
Las entradas custom y session_init no inyectan contexto del modelo directamente.
Garantías de Persistencia y Modelo de Fallos
Sección titulada «Garantías de Persistencia y Modelo de Fallos»Persistencia vs en memoria
Sección titulada «Persistencia vs en memoria»SessionManager.create/open/continueRecent/forkFrom-> modo persistente (persist = true).SessionManager.inMemory-> modo no persistente (persist = false) conMemorySessionStorage.
Pipeline de escritura
Sección titulada «Pipeline de escritura»Las escrituras se serializan a través de una cadena de promesas interna (#persistChain) y NdjsonFileWriter.
append*actualiza el estado en memoria inmediatamente.- La persistencia se difiere hasta que exista al menos un mensaje del asistente.
- Antes del primer asistente: las entradas se retienen en memoria; no se realiza ningún append al archivo.
- Cuando existe el primer asistente: toda la sesión en memoria se vuelca al archivo.
- Después: las nuevas entradas se añaden incrementalmente.
Justificación en el código: evitar persistir sesiones que nunca produjeron una respuesta del asistente.
Operaciones de durabilidad
Sección titulada «Operaciones de durabilidad»flush()vuelca el escritor y llama afsync().- Las reescrituras completas atómicas (
#rewriteFile) escriben en un archivo temporal, realizan flush+fsync, cierran y luego renombran sobre el destino. - Se usan para migraciones,
setSessionName,rewriteEntries, operaciones de movimiento y reescrituras de argumentos de llamadas a herramientas.
Comportamiento ante errores
Sección titulada «Comportamiento ante errores»- Los errores de persistencia se enganchan (
#persistError) y se relanzan en operaciones subsiguientes. - El primer error se registra una vez con el contexto del archivo de sesión.
- El cierre del escritor es de mejor esfuerzo pero propaga el primer error significativo.
Controles de Tamaño de Datos y Externalización de Blobs
Sección titulada «Controles de Tamaño de Datos y Externalización de Blobs»Antes de persistir las entradas:
- Las cadenas grandes se truncan a
MAX_PERSIST_CHARS(500.000 caracteres) con aviso:"[Session persistence truncated large content]"
- Los campos transitorios
partialJsonyjsonlEventsse eliminan. - Si el objeto tiene tanto
contentcomolineCount, el conteo de líneas se recalcula después del truncamiento. - Los bloques de imagen en arrays
contentcon longitud base64 >= 1024 se externalizan a referencias de blob:- almacenados como
blob:sha256:<hash> - los bytes crudos se escriben en el almacén de blobs (
BlobStore.put)
- almacenados como
Al cargar, las referencias de blob se resuelven de vuelta a base64 para los bloques de imagen de message/custom_message.
Abstracciones de Almacenamiento
Sección titulada «Abstracciones de Almacenamiento»La interfaz SessionStorage proporciona todas las operaciones de sistema de archivos utilizadas por SessionManager:
- síncronas:
ensureDirSync,existsSync,writeTextSync,statSync,listFilesSync - asíncronas:
exists,readText,readTextPrefix,writeText,rename,unlink,openWriter
Implementaciones:
FileSessionStorage: sistema de archivos real (Bun + node fs)MemorySessionStorage: implementación en memoria respaldada por mapa para pruebas/sesiones no persistentes
SessionStorageWriter expone writeLine, flush, fsync, close, getError.
Utilidades de Descubrimiento de Sesiones
Sección titulada «Utilidades de Descubrimiento de Sesiones»Definidas en session-manager.ts:
getRecentSessions(sessionDir, limit)-> metadatos ligeros para la UI/selector de sesionesfindMostRecentSession(sessionDir)-> la más reciente por mtimelist(cwd, sessionDir?)-> sesiones en el alcance de un proyectolistAll()-> sesiones en todos los alcances de proyecto bajo~/.xcsh/agent/sessions
La extracción de metadatos lee solo un prefijo (readTextPrefix(..., 4096)) cuando es posible.
Relacionado pero Distinto: Almacenamiento de Historial de Prompts
Sección titulada «Relacionado pero Distinto: Almacenamiento de Historial de Prompts»HistoryStorage (history-storage.ts) es un subsistema SQLite separado para la recuperación/búsqueda de prompts, no para la reproducción de sesiones.
- BD:
~/.xcsh/agent/history.db - Tabla:
history(id, prompt, created_at, cwd) - Índice FTS5:
history_ftscon sincronización mantenida por triggers - Deduplica prompts idénticos consecutivos usando una caché en memoria del último prompt
- Inserción asíncrona (
setImmediate) para que la captura de prompts no bloquee la ejecución del turno
Use los archivos de sesión para la reproducción del grafo/estado de la conversación; use HistoryStorage para la UX del historial de prompts.