Session Management for AI Agents From Scratch

Conversation persistence transforms a stateless model interaction into a resumable workflow. Without session saving, every engagement with an agent begins fresh. Notes capture conventions and preferences but cannot track what was asked, what was changed, or what remains undone. This article explains how to save conversations, resume previous work, and recover gracefully after interruption.

What Is a Session

A session is one conversation with the agent. It collects all user messages, model responses, and tool calls that occurred during a use of the agent. Previously, that conversation lived only in the harness memory. To make it savable and resumable, the session receives an ID and gets written to disk. Closing the program ends the current run but does not erase the conversation. Though sessions and memories share a directory, they are separate: memories collected during a session store separately into project and user files.

Saving Sessions

The memory directory now contains a sessions folder alongside the notes: project.md, user.md, and sessions/ with JSON files such as z7Pq3hM4kA.json. A session record includes an ID, creation and update timestamps, a summary, and the conversation messages. The harness creates this record when a new session starts. Messages from the OpenAI client are objects, not always dictionaries. Before writing them as JSON, the harness converts them with model_dump so structured tool calls and connecting IDs persist. Saving only visible assistant text would lose which tools the model requested and the IDs linking those calls to their results.

Working Context and Full Transcripts

Keeping only one message list creates a problem. Context sent to the model should shrink when a conversation grows long, but the original conversation must not be lost from disk. The SessionStore maintains two lists. The messages list is the working context sent to the model and restored on resume. Compaction can replace older turns with a summary. The transcript keeps the original persisted messages, including structured tool calls and results, even after compaction. The record also stores compactions, a list of summary events, and metadata such as title, archived flag, project_root, and planning state. When loading an older session created before these fields existed, defaults supply the missing records. This approach preserves compatibility with sessions from the previous implementation, though messages already trimmed before the upgrade cannot be recovered.

For normal updates, only new messages extend the transcript. The incoming context must match the saved context, otherwise a ValueError raised. This guard prevents session corruption from unexpected context shifts.

Preventing Session Corruption on Writes

Sessions update frequently, so overwriting the file directly risks incomplete JSON if the program stops mid-write. Instead, a temporary file is created and atomically replaces the session file. The temporary file lives in the same directory, so the replacement stays on the same filesystem. Contents flush and sync before replacement. Until the replacement happens, the previous file remains intact. This strategy protects against partial JSON writes that would lose the saved conversation.

Session Locks

Atomic replacement does not prevent two processes from overwriting each other's work. If the same session resumes in two terminals, each could save a different version into the same file. To avoid this, a separate .lock file and a Linux advisory lock coordinate access. The lock uses fcntl, a POSIX mechanism that works on Linux and macOS but not Windows. A second process attempting to acquire the lock fails immediately rather than waiting. The running agent holds the lease for its entire run. Short management operations also acquire the lock before changing a record. Nested calls from the same store can reuse the lease. The operating system releases the lock when the process exits, including after a crash. The lock file persists even after session deletion because removing it could allow different processes to lock different files under the same name. These are advisory locks: every writer must cooperate, so running an older implementation without those locks at the same time is not advisable.

Session ID Validation

Before using a session ID as a filename, the harness validates it matches a specific pattern. Session IDs must contain only letters, numbers, underscores, and hyphens. Invalid characters raise a ValueError. This validation prevents path traversal or unintended file placement.

Connecting Persistence to the Agent Loop

At the start of the agent loop, the system either uses the initial system message or shows messages loaded from a previous session. If a memory store exists and a session ID is provided, the harness creates the session with those messages. configure_memory gives host-side tools the store and the active session ID. Messages save at three points: after appending a new user message, after a model response, and after completing a batch of tool calls with results. At each point, the same code runs: if a memory store and session ID exist, save the messages. Because the system does not wait for the user to end the session, unexpected process termination still leaves the last successfully saved messages intact.

Saving Other Conversation State

Messages are not the only state the agent maintains. The scratchpad and to-do list live in Python memory, and resource controls track tool calls and cumulative model usage. Restoring a session requires restoring all of that state to avoid an incomplete session. A checkpoint helper saves state alongside messages: scratchpad content, todo items including retry counts, the session tool-call count, and cumulative API usage (tokens in, tokens out, cost in USD, and call count). This checkpoint runs around model requests, after tool batches, and after final replies. On resume, the scratchpad, all todo items, the session tool-call count, and cumulative API usage restore. The per-user-turn iteration counter starts again for the next request. Restarting the program does not reset recorded session spend or tool-call count — limits still come from current command-line options. Though each JSON write is atomic, messages and state save in two separate updates. A crash between them can leave newer messages with an older planning checkpoint. A crash during a tool or model call can also leave side effects or billing unrecorded.

Session Summaries

Selecting a previous session by session ID is not helpful. To make finding sessions easier, each session gets a short summary containing its goal, completed work, important decisions, and remaining work. The model can write this summary with a tool. The inherited MemoryStore method replaces the previous summary and updates the timestamp. SessionStore wraps this operation in the session lease so concurrent writes cannot corrupt the record.

Not every session receives a user-written summary. The agent might forget to call the summary tool, or the session might stop before the chance arises. As a fallback, the harness calls ensure_summary. This fallback takes the last six user and assistant messages with content and keeps up to 2000 characters. It is an excerpt, but at least it gives something to recognize the session by. The fallback only runs when the summary is empty.

Listing Saved Sessions

The base MemoryStore reads JSON files from the sessions directory and returns a small record for each one: the session ID, the last update timestamp, and the summary. The list orders sessions by recency, most recently updated first. Unreadable or malformed records are skipped and do not appear in the list.

SessionStore extends these records with a title, archive flag, and status, filtering archived sessions out by default. This makes it easy to discover active sessions without scanning archived ones.

Session management gives AI agents memory of unfinished work. Conversations persist across process exits. Compaction keeps the model context manageable while the full transcript preserves the original. Locks prevent concurrent writes from corrupting state. Summaries and listing make it practical to find and resume prior work. The implementation uses JSON files as the medium, atomic writes for safety, and POSIX advisory locks for concurrency control. Checkpointing captures additional state so a resumed session is complete. These techniques apply whether the agent runs locally or in a hosted environment where conversation persistence matters.