Глава 06 · Преемственность
Sessions — как агент помнит вчерашний разговор
Сессия — это обёртка над разговором, у которой есть имя. Если вы знаете имя — можно вернуться через час, день, неделю — и продолжить с того же места.
Одна сессия — три «жизни». Главный session_id рождается при первом запуске, transcript сохраняется в JSONL после каждого хода. Resume — это тот же session_id, новый QueryEngine, прочитанный с диска transcript и следующий ход поверх.
Зачем нужна сессия
Без сессии у вас просто история сообщений — массив messages в RAM процесса, который умирает вместе с ним. Закрыли терминал — массив выброшен. Открыли заново — пустой контекст. Это нормально для чата, но не для агента, который правит ваш репозиторий и отлаживается в три захода: половина работы — это контекст, который вы собрали к шагу №N, и терять его на каждом запуске абсурдно.
Сессия решает ровно эту задачу. Это обёртка над разговором, у которой есть имя (session_id), метаданные (когда создана, в каком режиме) и ссылка для возврата. История сообщений получает идентификатор, под которым лежит на диске. Архитектурно сессия — это единица продолжаемости: минимальная сущность, которую можно «закрыть» сегодня и «открыть» завтра, не теряя ничего.
Жизненный цикл сессии
В claude-cli жизнь сессии разложена на три стадии. Bootstrap: argv-парсинг в entrypoints/cli.tsx решает, в каком режиме запускаться; мемоизированный init() готовит конфиги, телеметрию, cleanup-регистр. Здесь же генерится session_id — UUID, попадающий в init message (system-сообщение с subtype: "init") — первое, что видит SDK-клиент в потоке. Anthropic документирует это явно: «capture the session ID from the system/init message, pass to options.resume to resume».
Live: каждый turn прогоняется через единственный QueryEngine. Метод submitMessage пишет transcript через recordTranscript до первого вызова модели — сознательный инвариант: «kill-mid-request leaves a resumable session». Serialize: после каждого хода transcript дописывается append-only. Когда процесс завершается — нормально или аварийно — последний снимок остаётся на диске.
Resume — симметричная операция: при запуске с --resume <id> (или options.resume в SDK) harness читает JSONL, восстанавливает messages, создаёт новый QueryEngine с тем же session_id, и цикл продолжает работу. Также восстанавливается режим: matchSessionMode перед первым чтением переключает env-переменную coordinator/normal в значение, с которым сессия была сохранена. Это не «новый разговор с подгруженным контекстом», это тот же разговор, продолженный позже.
Где хранится transcript
Здесь важно различать два понятия «сессии», которые часто смешивают. Client-side session — то, что живёт у вас на машине: транскрипт в JSONL, по одной записи на строку (user msg, assistant msg, tool_use, tool_result), файлы в ~/.claude/sessions/<id>.jsonl. Anthropic формулирует коротко: «Sessions are JSONL on the local filesystem». SDK Session API (listSessions, renameSession, forkSession) работает именно с этими файлами: rename и tag добавляют новые JSON-записи, fork — копирует JSONL и перегенерит UUID-цепочки, сохраняя parentUuid-связи. Транскрипт — source of truth, отдельного индекса нет.
Server-side session — то, что живёт у Anthropic: внутренний идентификатор в Managed Agents или в bridge-режиме на claude.ai. У клиента к нему доступ только через REST API. В сравнительной таблице Agent SDK vs Managed Agents Anthropic разводит эти миры: «Session state: JSONL on your filesystem» против «Anthropic-hosted event log». Для локальных агентов важна client-side ветка — это просто файл, который вы можете прочитать, переименовать, форкнуть, забэкапить обычными средствами ФС.
QueryEngine — что это
Внутри claude-cli сессия имеет конкретное воплощение — класс QueryEngine. Это per-conversation envelope: один QueryEngine = одна сессия, никаких глобальных. Внутри живёт ссылка на текущий transcript, текущий context (сжатые блоки, активные tool-use'ы, thinking-блоки в обработке), текущее состояние loop. Именно QueryEngine.submitMessage крутит ту queryLoop из главы 2 — вечный while по stop_reason.
Когда вы делаете resume, движок не «продолжается» — создаётся новый QueryEngine с тем же session_id и transcript'ом, прочитанным с диска. Прежний инстанс к этому моменту мёртв вместе со своим процессом. QueryEngine не хранит состояние между запусками — он его восстанавливает из transcript'а. Всё, что не записано в transcript до завершения, для нового движка не существует: если процесс упал между submit и write, следующий resume увидит сессию до момента последней успешной записи.
Mode dispatch — три способа войти в loop
Сессия — не способ запуска, это объект, с которым работает любой запуск. Способов войти в loop обычно три, и каждый создаёт свой QueryEngine. Интерактивный REPL: claude без аргументов, терминальный UI, новая сессия с новым session_id в init-сообщении. SDK: query({ prompt }) в Python или TypeScript — программный вызов, который тоже породит сессию (или подцепится через options.resume) и завершится. Headless CLI: claude -p "prompt" — без REPL, prompt аргументом, результат в stdout. Все три диспатчатся в entrypoints/cli.tsx через argv-инспекцию до загрузки тяжёлых модулей.
Anthropic в Agent SDK документирует две вариации входа в query: streaming input mode (async-генератор, рекомендуемый, поддерживает images, очередь, mid-flight interrupt, hooks) и single message input (строковый prompt, одноразовый, без hook-интеграции). Это та же дихотомия «интерактивно vs одноразово» на уровне SDK. Под всеми режимами лежит один и тот же QueryEngine и один и тот же transcript-формат: переключение режима меняет, как разговор начинается и заканчивается, но не как он сохраняется.
Что НЕ делает session
Сессия и память (глава 5) — соседние, но разные сущности. Сессия помнит то, что было в разговоре — буквальный transcript: ваши сообщения, ответы агента, tool-use'ы и их результаты, thinking-блоки. Это сырая последовательность; её предел — лимит модели на длину контекста (плюс политика компакции, глава 4).
Память — то, что вы решили вытащить из разговора в отдельный носитель. Memdir, MEMORY.md, файлы с фронтматтером — это уже не transcript, а перегон знаний в durable хранилище, которое переживёт любую сессию. Сессия закроется — память останется, потому что лежит в другом месте и не привязана к session_id. Симметрично: потеряете memdir — сессия не пострадает; потеряете transcript — память не пострадает. Две оси персистентности живут параллельно: сессия про «продолжить тот же разговор», память про «помнить факт независимо от разговора».
Inside a conversation, each turn is driven by a single QueryEngine instance; the engine's submitMessage writes the transcript via recordTranscript BEFORE the first model call so a kill-mid-request leaves a resumable session.
— claude-cli/02-session-lifecycle, инвариант «one QueryEngine = one conversation»Источники главы
- [ant] Agent SDK overview — Anthropic Docs (init message с
session_id,options.resume, JSONL on local filesystem, Agent SDK vs Managed Agents) - [ant] Streaming vs single mode — Anthropic Docs (два режима входа в
query, mid-flight interrupt, hook integration) - [cli] Конспект: Session Lifecycle (bootstrap → init → live → serialize,
QueryEngineper-conversation, mode dispatch,matchSessionModeна resume) - [cli] Конспект: Anthropic API distill (Agent SDK section, C-12 session continuity, transcript pre-write invariant)