Save and resume a session

A session runs commands and records saved states in a Store. The Store keeps the session’s history and references to its filesystem images. ConTree keeps the images on the server. To continue later, open the same Store and select the same session.

This guide starts with memory storage, then saves work in SQLite and opens it again. Configure your connection as described in Connection sources. Async SQLite examples require the contree-sdk[async] extra.

1. Choose where to keep history

Store is a storage interface. The SDK includes two implementations:

Store

Where history lives

When to use it

SyncMemoryStore / AsyncMemoryStore

In memory, while that Store remains open

Work within one Python program.

SyncSQLiteStore / AsyncSQLiteStore

In a local SQLite file

Continue work after Python exits.

Without store=, create_sandbox_session() creates a memory Store for that context. To share history between contexts, create a Store yourself and pass it through store=. The outer context below keeps that Store open until both sessions have finished.

Both sessions use session_id="demo". The second context opens the first session’s saved state and reads its file.

from contree_sdk import create_sandbox_session
from contree_sdk.store import SyncMemoryStore

with SyncMemoryStore() as store:
    with create_sandbox_session(image="tag:alpine:latest", store=store, session_id="demo") as session:
        session.run(shell="echo saved > /saved.txt")

    with create_sandbox_session(store=store, session_id="demo") as session:
        content = session.read_file("/saved.txt")
        print(content.decode())
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.store import AsyncMemoryStore


async def main() -> None:
    async with AsyncMemoryStore() as store:
        async with create_sandbox_session(image="tag:alpine:latest", store=store, session_id="demo") as session:
            await session.run(shell="echo saved > /saved.txt")

        async with create_sandbox_session(store=store, session_id="demo") as session:
            content = await session.read_file("/saved.txt")
            print(content.decode())


asyncio.run(main())

Output:

saved

Closing the memory Store discards its history. A new memory Store starts empty, even if you reuse the same session ID. Use SQLite to keep history between runs.

2. Save work in a SQLite file

Replace the memory Store with a SQLite Store. This program creates sessions.db in the current directory and records a session named my-project.

Run this program first. Normal exit from the session context saves the filesystem changes and records the saved image in the Store. Then the outer context closes the database.

from contree_sdk import create_sandbox_session
from contree_sdk.store import SyncSQLiteStore

with SyncSQLiteStore("sessions.db") as store:
    with create_sandbox_session(image="tag:alpine:latest", store=store, session_id="my-project") as session:
        session.run(shell="echo saved > /saved.txt")
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.store import AsyncSQLiteStore


async def main() -> None:
    async with AsyncSQLiteStore("sessions.db") as store:
        async with create_sandbox_session(image="tag:alpine:latest", store=store, session_id="my-project") as session:
            await session.run(shell="echo saved > /saved.txt")


asyncio.run(main())

There are now two different files: sessions.db is local history, and /saved.txt is inside the filesystem image stored by ConTree. The SQLite file does not contain a copy of that filesystem.

3. Continue in another program

Run this second program from the same directory. It opens the existing database and selects my-project. No image= argument is needed: the session gets its saved image from the Store.

The command reads the file created by the first program. You can run more commands in this context; normal exit saves their filesystem changes too.

from contree_sdk import create_sandbox_session
from contree_sdk.store import SyncSQLiteStore

with SyncSQLiteStore("sessions.db") as store:
    with create_sandbox_session(store=store, session_id="my-project") as session:
        result = session.run("cat", args=["/saved.txt"])
        print(result.stdout)
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.store import AsyncSQLiteStore


async def main() -> None:
    async with AsyncSQLiteStore("sessions.db") as store:
        async with create_sandbox_session(store=store, session_id="my-project") as session:
            result = await session.run("cat", args=["/saved.txt"])
            print(result.stdout)


asyncio.run(main())

Output:

saved

Keep the same database path, session ID, and access to the same ConTree server and project. A different working directory can make "sessions.db" refer to a different file. Use an absolute path when programs run from different directories.

An unknown session ID without image= raises ValueError. If you provide an image and an unused ID, the SDK creates a new session. An existing ID resumes its saved state even when you pass image=. Use a new ID for independent work.

4. Keep several sessions in one file

One Store can hold many sessions. Each session_id identifies a separate history within that Store. These two sessions start from the same base image, then save different contents at the same path.

Run this program to create a separate projects.db for the next steps.

from contree_sdk import create_sandbox_session
from contree_sdk.store import SyncSQLiteStore

with SyncSQLiteStore("projects.db") as store:
    for name in ("project-a", "project-b"):
        with create_sandbox_session(image="tag:alpine:latest", store=store, session_id=name) as session:
            session.run(shell=f"echo {name} > /project.txt")
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.store import AsyncSQLiteStore


async def main() -> None:
    async with AsyncSQLiteStore("projects.db") as store:
        for name in ("project-a", "project-b"):
            async with create_sandbox_session(image="tag:alpine:latest", store=store, session_id=name) as session:
                await session.run(shell=f"echo {name} > /project.txt")


asyncio.run(main())

The shared database does not combine the files or histories of these sessions. Omit session_id to generate a new ID automatically; read session.session_id if you need to keep that ID.

5. Find a saved session

Open the database and call list_sessions(kind="session"). The filter selects user session histories and excludes records used for builds or upload reuse. Listing stored sessions does not start a VM or contact ConTree.

Results arrive in pages. Print each page, then pass next_cursor to read the next one. Stop when it is None. This example requests one item per page so you can see pagination with just two sessions; increase limit for larger lists.

from contree_sdk.store import SyncSQLiteStore

with SyncSQLiteStore("projects.db") as store:
    cursor = None
    while True:
        page = store.list_sessions(kind="session", limit=1, cursor=cursor)
        for saved in page.items:
            print(saved.session_id)
        cursor = page.next_cursor
        if cursor is None:
            break
import asyncio

from contree_sdk.store import AsyncSQLiteStore


async def main() -> None:
    async with AsyncSQLiteStore("projects.db") as store:
        cursor = None
        while True:
            page = await store.list_sessions(kind="session", limit=1, cursor=cursor)
            for saved in page.items:
                print(saved.session_id)
            cursor = page.next_cursor
            if cursor is None:
                break


asyncio.run(main())

Output:

project-a
project-b

The list is ordered by session ID, not by last activity. Each item also contains created_at and expires_at. Pass prefix="project-" to select IDs with that prefix.

6. Continue the session you chose

Copy an ID from the list into session_id. This program selects project-b and reads its saved file. To open project-a, change only that ID.

from contree_sdk import create_sandbox_session
from contree_sdk.store import SyncSQLiteStore

with SyncSQLiteStore("projects.db") as store:
    with create_sandbox_session(store=store, session_id="project-b") as session:
        content = session.read_file("/project.txt")
        print(content.decode())
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.store import AsyncSQLiteStore


async def main() -> None:
    async with AsyncSQLiteStore("projects.db") as store:
        async with create_sandbox_session(store=store, session_id="project-b") as session:
            content = await session.read_file("/project.txt")
            print(content.decode())


asyncio.run(main())

Output:

project-b

Resume a selected node

The examples above each have one continuation, so a session ID is enough. After branching, a history can contain several continuations. The SDK then raises ValueError unless you pass node_id= to select a saved state.

Keep the entry ID returned by snapshot() when you need an exact checkpoint. Pass it as node_id= together with session_id= when reopening. A session ID selects the history; a node ID selects a point within that history. See Branch, switch, and roll back for checkpoints and branches.

The Store does not retain the last selected position or local branch names. Even with one continuation, reopening can select a later saved state than the one you selected with rollback(). Keep the selected node ID for exact resume. Independent clients can resume the same node and create separate continuations.

What must remain available?

  • Keep the SQLite database and access to the same ConTree server and project.

  • Keep the referenced images on that server. SQLite cannot restore a deleted image.

  • Save filesystem changes before stopping Python. Normal context exit saves them; snapshot() saves an earlier checkpoint. An exception discards changes since the last completed snapshot. Abrupt process termination does not guarantee a save.

  • Snapshots save files, not running processes or VM memory. Continuing a session starts new processes from the saved filesystem.

An unchanged filesystem can produce the same image UUID at several checkpoints. History entry IDs still distinguish them. For automatic saving after each command, see Save after every command. A nonzero command exit code can still produce a saved image; inspect the result before continuing or rolling back.

Use another storage backend

For your own database or service, implement SyncStore or AsyncStore and pass the object through store=. See Implement a custom Store in Advanced Usage for an example and the implementation requirements.

The application closes any Store it supplies. Keep the Store context outside all session contexts that use it, as in these examples. See Advanced: assemble a sandbox yourself for explicit component construction and cleanup, and Tree stores for the complete storage API.