--- icon: database --- # 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 {ref}`sandbox-connection`. 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. ::::{tab} Sync ```python 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()) ``` :::: ::::{tab} Async ```python 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: ```text 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. ::::{tab} Sync ```python 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") ``` :::: ::::{tab} Async ```python 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. ::::{tab} Sync ```python 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) ``` :::: ::::{tab} Async ```python 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: ```text 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. ::::{tab} Sync ```python 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") ``` :::: ::::{tab} Async ```python 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. ::::{tab} Sync ```python 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 ``` :::: ::::{tab} Async ```python 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: ```text 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. ::::{tab} Sync ```python 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()) ``` :::: ::::{tab} Async ```python 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: ```text 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 {doc}`branching` 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](session-model.md#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 {doc}`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 {doc}`manual-sandbox` for explicit component construction and cleanup, and {doc}`reference/store` for the complete storage API.