跪拜 Guibai
← Back to the summary

How Claude Code Rebuilds a Session from a JSONL Log

How Does Claude Code Resume a Session?

Author: candyTong Tags: Architecture, AI Programming, Backend

You're halfway through a Claude Code session, close the terminal, and later run claude --continue. Usually, you can pick up right where you left off. Old messages reappear in the terminal, and recoverable work states like agent configuration and Git worktree are brought back as well.

This looks like "re-reading the logs," but the actual recovery happens in two steps: Claude Code first reconstructs a resumable session from local records; only after you enter a new message does it assemble the context for the next model request. claude --resume <session-id> follows the same main path, just targeting a specific session.

Let's follow this path to see how a session is found, reconstructed, and put back into use:

continue-resume-b

Let's start with how Claude Code finds a session.

Selecting a Session to Resume

Claude Code provides three common entry points:

Entry Point Purpose
claude --continue Continue the most recent session in the current project
claude --resume <id or name> Return to a specified session; opens a selector when no argument is given
/resume Switch sessions within the current Claude Code process

Sessions are saved per project, so "most recent" means the most recent session within the current project. The selector can also expand to other worktrees of the same repository, or other projects on the machine.

--continue and --resume resume a session at startup; /resume switches sessions within the current process. Their entry points differ, but the subsequent steps of reading logs and restoring messages are largely the same.

Reconstructing Conversations from JSONL

JSONL (JSON Lines) is a text format that stores JSON data line by line. Each line is an independent JSON record, so a program can continuously append new content to the end of a file while a session is running.

Claude Code typically saves each session as a JSONL file, located by default at ~/.claude/projects/<project-directory>/<session-id>.jsonl. The session-id in the filename identifies the session.

Messages, tool calls, and state changes during a session are all written to this file sequentially. The top-level type of each line is the JSONL record type, which tells Claude Code how to read and restore that record. It is not a classification of the AI message content, nor should it be simply understood as "user is what the person said, assistant is the AI's text reply."

For user and assistant records, there is also an internal message.role. These usually share the same name as the top-level type, but they express the direction of model interaction: user indicates the side sent to the model, and assistant indicates the side returned by the model. What exactly is inside the message depends on message.content.

JSONL top-level user and assistant model interaction records

Therefore, when you see top-level type: "user", you cannot yet judge whether it was personally entered by the user. If the inside is text, it might be a user message; if the inside is tool_result, it is the result returned by a tool to the model.

After reading the JSONL, Claude Code organizes the recoverable conversation records into an in-memory messageList. A single tool call typically forms four records: the user asks a question, the model initiates a tool_use, the tool returns a tool_result, and the model continues its answer. On the interface, it looks like the model continuously completed "calling the tool" and "giving the conclusion," but between the two assistant records in the JSONL, there is also a user record storing the tool result.

Four conversation records corresponding to one complete tool call in messageList

These types of records are mainly linked by the following two fields:

Field Purpose
uuid Identifies the current message
parentUuid Points to the logically previous message, not necessarily the immediately preceding line in the JSONL

Besides user and assistant, the JSONL also stores session control and auxiliary records. They are not displayed as ordinary user messages or model answers, and are mainly used to restore control state, session information, run mode, and work environment.

type Content Recorded Role During Recovery
system Runtime control records written by Claude Code Restore corresponding session control state based on subtype
summary A brief overview for display in the session list Helps identify the session; not restored as normal conversation content
custom-title Session title set by the user Restore the title in the session list
tag Tags added to the session by the user Used for filtering and finding sessions
agent-name Display name of the agent Restore the name in the session list and terminal
agent-setting The agent type or name used in the current session Find and re-apply the corresponding agent configuration
mode The run mode of the session Restore the corresponding run mode
worktree-state Information such as worktree name, path, and branch If the worktree still exists, re-enter the original working directory
file-history-snapshot File backup index corresponding to a message node Rebuild file history for rewind to restore file content
pr-link PR information associated with the session Restore the association between the session and the PR

An example of a worktree state record:

A worktree state record

These records are usually attributed to a session via sessionId, and then handed off to the corresponding recovery logic based on type. system records may participate in the parentUuid message chain; metadata like titles, tags, and worktree typically do not.

A normal conversation usually has only one path. Forks typically appear when the user uses /rewind to go back to an earlier message and then enters new content.

This session log uses append-only writing: new messages are written directly to the end of the file after they are generated, without needing to rewrite the entire session each time. This makes writing simpler, and even if the process exits midway, it usually does not affect previously saved records; the uuid of messages can also remain unchanged.

Therefore, rewind only changes "where to continue from next," and does not delete conversations that have already happened. New messages are appended to the end of the file and point to earlier messages via parentUuid, leaving both the old and new paths in the JSONL simultaneously.

Below is a simplified example using real fields. For readability, irrelevant fields like session ID and timestamps are omitted:

Six records appended to JSONL, containing two conversation paths

The first four lines are a complete conversation: the user first asks to fix a login issue, then decides to switch to OAuth. Next, the user uses /rewind to go back after a1 and changes the request to "just fix password login." Therefore, although the fifth line is written at the end of the file, its parentUuid is still a1.

These 6 lines actually form two paths:

u1-a1-a1

Assuming we want to continue from a3 this time, Claude Code will trace backwards along parentUuid:

a3 → u3 → a1 → u1

Then reverse the order, and the reconstructed result is the following 4 messages arranged in normal reading order:

Fix login failure
→ I'll first check the authentication flow
→ Don't change the plan for now, just fix password login
→ The problem is in the password verification logic

After backtracking from a3 and reversing, Claude Code obtains the conversation history that needs to be restored this time. It will first use this set of messages to restore the session in the terminal; when the user continues to input, it will then use this as the basis to prepare the next model request.

This reconstruction process does not generate new JSONL, nor does it regenerate summaries. The 6 records remain on disk, but u2 and a2 belong to the OAuth path and are not in the current message chain. When restoring along this path, they will be skipped but will remain in the file, taking up space.

If the Conversation Was Previously Compacted

Compaction usually occurs during the session runtime, not during recovery. After earlier conversations are compacted, Claude Code also writes the compaction boundary and the generated summary into the JSONL.

Assume the conversation before compaction is:

u1 → a1 → u2 → a2

After compaction is complete, a compaction boundary B and a summary message S will be added to the end of the file. The S here is not the top-level type: summary record used for session list display mentioned earlier, but the compaction result that participates in the current message chain. Below is a simplified example using real fields:

Two JSON records for compaction boundary B and summary message S

The fields in the compaction boundary represent:

Field Meaning
subtype: compact_boundary This is not an ordinary system message, but a demarcation point of a compaction
parentUuid: null The new conversation chain starts from here, no longer reading earlier original messages along parent references
logicalParentUuid: a2 Records that processing last reached a2 before compaction, but will not trace back along it when rebuilding the message chain
compactMetadata Saves whether the compaction was triggered automatically or manually, and the approximate token count before compaction
isCompactSummary: true Indicates that S is a summary of the earlier conversation, not a new question input by the user

The parentUuid of the summary message S points to the compaction boundary B. Subsequent conversations continue from S:

Original messages: u1 → a1 → u2 → a2

Current path: B → S → u3 → a3

Old messages may still remain in the JSONL, but they are no longer in the current path.

During recovery, Claude Code traces backwards from a3 along parentUuid and will only get:

B → S → u3 → a3

The compaction boundary is an internal marker and will not be sent to the model as normal chat content. Therefore, what is actually used to prepare the model request subsequently is:

Early conversation summary S
→ New messages after compaction u3, a3
→ New messages the user will input next

This process does not re-call the model to generate a summary; it directly reuses the summary already saved in the JSONL.

Restoring Messages and Work State

After reconstructing the conversation history, turns that did not end normally last time must also be handled. For example:

These processes do not automatically re-run tools; they organize the messages into a structure where the conversation can continue.

Besides messages, Claude Code also restores "work state." Work state here refers to records that are not directly written in the chat body but affect the environment and manner in which work continues next. It is not a snapshot of the entire process, nor will it restore a command that has already stopped.

These work state records are also saved in the current session's JSONL. The example below only retains fields relevant to the explanation:

Agent, worktree, and file history state records in JSONL

During recovery, Claude Code processes messages and state records separately: messages are reconstructed according to parentUuid, while states are read according to their respective types and written back to the current process.

Work State Approximate Role Previously Stored Where What Happens During Recovery
Agent settings Determines which set of prompts, tool scope, and model configuration to use agent-setting in JSONL, only saves the agent's name or type Find the corresponding agent in current settings and apply its current configuration
Worktree Allows subsequent modifications to continue in the original isolated directory and branch, avoiding changes to the wrong code directory worktree-state in JSONL, saves information like name, path, and branch Check if the directory exists; if so, switch to it; if not, stay in the current directory
File read records Records which files have already been read, used for pre-edit validation by tools like Edit Not saved separately, but included in tool messages like Read, Write, Edit Scan the restored messages and rebuild the file read cache
File history Saves backups of files at different message nodes; when executing rewind, code can be restored to the state at that time JSONL saves file-history-snapshot, actual backups are stored in the file-history directory under the config directory Rebuild snapshots according to the current conversation chain and re-associate corresponding backup files
Session info Helps users identify and find sessions; does not itself determine how the model executes Records like title, tags, and agent name in JSONL Read the last saved value and restore to the session list and terminal

From an implementation perspective, the reading phase first obtains a temporary recovery result containing data like messages, agent settings, worktree, and file history. Subsequently, this data is written separately into the message list, runtime state, cache, and current working directory:

jsonl-b

After this data is written back, what Claude Code gets is not a new state file, but a process whose runtime environment has already been restored. The model context still waits for the user to continue inputting before being prepared.

For example, suppose the last session used a code review agent, read and modified src/auth.ts in the fix-login worktree, and then the terminal was closed while a test tool was halfway through execution. After recovery, Claude Code will clean up unfinished tool calls, re-enter the still-existing worktree, restore the agent, file read records, and file history, and then wait for the user to continue inputting.

Recovery is conditional: Claude Code will re-read the current settings, and command-line arguments can override settings saved in the session; if the original agent has been deleted, or the worktree directory no longer exists, it will fall back to the currently available configuration and directory.

Will the Model See All the Old Messages in the Terminal?

Not necessarily. After session recovery, the terminal displays the conversation on the current path. The content actually sent to the model next time is only assembled when you continue inputting.

Claude Code will read history starting from the position of the most recent compaction. If the content is still too much, the earlier parts will continue to be compacted, and the remaining messages will then be converted into the format required for the model request.

This creates a seemingly contradictory situation: you can still scroll back to very early conversations in the terminal, but Claude did not catch a certain detail from them. The record may not be lost; it's just that that segment of content did not fully enter this request, or has already become a summary. Of course, it's also possible that the content entered the context, but the model did not use it.

If you feel after recovery that the previous context has been forgotten, you can first check the history in the terminal. If a segment is already missing here, go back and check session selection, log reading, and the message chain; if the terminal records are still there, then look at the subsequent context trimming and compaction. Separating these two layers during troubleshooting makes it less likely to go in the wrong direction.

Conclusion

Claude Code's session recovery is more like picking up the previous work again, rather than returning the program to the moment before it was closed. JSONL leaves behind conversation and state records, parentUuid finds the current path, and still-valid work states return to the current process.

This implementation also provides a very practical engineering idea: logs don't have to be just a few lines of strings written for humans to read. JSONL is still text, but each line has a fixed structure; the program can append while running and can also re-parse later.

The same log can also store multiple types of records. Conversation messages, tool results, and work states each have their own type, and are handed off to different logic when read. Logs are therefore not only for troubleshooting but can also become a data source for restoring state.

Claude Code relies on these structured records to reconnect a session. Only after the user continues inputting does the preparation of a new model request begin; the content saved in the log, the session restored in the terminal, and the context finally received by the model are interrelated but not necessarily identical.

If you found this article helpful, feel free to like and bookmark it. You can also follow me.