Skip to main content
SyncStore and AsyncStore store immutable trees. Sessions, Docker builds, and upload reuse share one Store. Helpers and direct constructors accept it through store=. The Store holds references; images and files remain on the ConTree server.

Nodes and tree kinds

A Node contains id, parent_id, kind, and JSON-object data. Its generated ID identifies a record, not a command, image, timestamp, or execution order. Retain the same Node when retrying a write. Two executions can have equal inputs and still produce different nodes. A tree has one root and an immutable string kind. Standard consumers use session, docker-build, upload-cache, and file-sources. Applications can supply their own kinds. A node’s kind describes its step, such as run, stage, or config. Tree kinds let a CLI exclude service histories before pagination.

Contract

  • create_session() creates an empty tree. Repeating it with the same kind returns the existing tree without renewing its TTL. A different kind raises ValueError.
  • append() requires a live tree and a parent in that tree, except for its root. It atomically inserts one node. Equal identity and contents succeed on retry; changed contents or a second root raise NodeConflictError without overwriting.
  • Reads return independent JSON values. Mutating a returned dictionary does not modify storage. Updating a saved node requires a new node.
  • Calls are atomic across clients sharing a backend. A successful write is visible to subsequent reads from any client, unless deletion or expiry removes the tree. Concurrent calls may take either order. Separate calls are not a transaction. A lost response leaves the write outcome unknown; retry an append with the same prepared Node.
  • get_session() returns None for a missing or expired tree. Reading a missing node or appending to a missing tree raises ValueError.
  • get_nodes(session_id, node_ids) reads up to 1000 identities in one atomic call. The returned tuple preserves input order and duplicates. Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Each returned node contains independent JSON values.
  • list_sessions(), list_nodes(), and list_children() filter before pagination and order by identity. list_history() follows parents from the specified node. Pass next_cursor with the same selection; None means the end. Pages are live reads and do not share a snapshot across calls.
  • List methods request 1000 items by default. A backend can return fewer items with a continuation cursor to bound response size. Always follow next_cursor; a short page does not imply the end. Treat cursors as opaque positions. They are not authorization tokens; a remote backend must check access separately.
  • TTL belongs to the whole tree. set_session_ttl() sets a new expiry from now. Reads and append do not renew it. ttl=None uses the implementation policy; the supplied Memory and SQLite implementations retain the tree until deletion. Zero expires immediately; negative or nonfinite TTL raises ValueError.
  • delete_session() removes the tree and returns whether stored data existed, including expired data. It does not delete remote images or cancel operations. A late append cannot recreate a deleted tree. Explicit creation can reuse its name; use a new session ID when a new lifetime must remain distinguishable.

Execution belongs to Session

The Store has no selected branch or shared head. Each Session keeps its own position and local branch names. Two actors executing from the same parent add two children. A late result keeps its captured parent even after local navigation. Use separate Session instances for independent actors. Serialize navigation when sharing one Session across threads. See Branch, switch, and roll back and Save and resume a session for selection and explicit resume. For a live tree with root R, actors can append R → A and R → B in either order. Both nodes remain available. Each actor can then extend its own node or explicitly select a node written by another actor. Repeating the same append adds no duplicate. Changing a node’s parent or data under the same ID raises NodeConflictError. This holds for any number of actors because each successful append adds one node without changing existing nodes. Each actor must retain its selected (session_id, node_id) and the prepared Node for a retry. Use explicit node_id when opening a shared session; automatic selection is not a durable actor position. Store does not merge filesystem changes or make VM execution exactly once. An identity cursor is not an event subscription. Concurrent inserts with IDs before that cursor will not appear in the remaining pages. Restart enumeration to discover them, or exchange explicit node IDs between actors. The ancestry of a known node is stable while other actors append, so list_history() can page that path without mixing branches. Deletion and expiry can still interrupt it. Pending operations and their completions are immutable nodes too. A completion node contains the response and retained image, so atomic append also protects completion from partial writes. See Advanced: resume a background operation. Session lists operation registrations by page and reads their completion nodes with get_nodes(). It does not fetch each registration again. Successful image commits record a staging_complete marker in the SDK payload. Staging recovery stops there because that image contains all staged inputs captured for the operation. Older records remain readable through ancestry traversal. Failed or discarded operations do not consume their source staging. Docker searches run children of the selected parent and use nodes for FROM. It still checks Docker hashes in the client. Large sibling sets and accumulated FROM records can require multiple pages. Opening a session without node_id still enumerates its tree to detect ambiguous continuation; pass node_id when resuming a known position. Custom backends implement this contract through SyncStore or AsyncStore. Atomicity must cover all clients, including separate processes and connections. An SDK-side lock alone does not meet that requirement. Memory uses SQLite’s in-memory engine; SQLite stores persist to a file. AsyncSQLiteStore uses aiosqlite. Existing databases from earlier experimental contracts are not migrated into these trees.

class AsyncMemoryStore

Methods

append()

required
required
None

close()

None

create_session()

required
default:"'session'"
default:"None"
SessionInfo

delete_session()

required
bool

get_node()

required
required
Node

get_nodes()

Read up to 1000 identities atomically, preserving input order and duplicates.Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Returned nodes contain independent JSON.
required
required
tuple[Node | None, ...]
tuple[Node | None, …]

get_session()

required
SessionInfo | None

list_children()

required
required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_history()

required
required
default:"1000"
default:"None"
Page[Node]

list_nodes()

required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_sessions()

default:"None"
default:"''"
default:"1000"
default:"None"
Page[SessionInfo]

set_session_ttl()

required
required
SessionInfo

class AsyncSQLiteStore

Native aiosqlite implementation of the same tree contract.

Methods

append()

required
required
None

close()

None

create_session()

required
default:"'session'"
default:"None"
SessionInfo

delete_session()

required
bool

get_node()

required
required
Node

get_nodes()

Read up to 1000 identities atomically, preserving input order and duplicates.Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Returned nodes contain independent JSON.
required
required
tuple[Node | None, ...]
tuple[Node | None, …]

get_session()

required
SessionInfo | None

list_children()

required
required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_history()

required
required
default:"1000"
default:"None"
Page[Node]

list_nodes()

required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_sessions()

default:"None"
default:"''"
default:"1000"
default:"None"
Page[SessionInfo]

set_session_ttl()

required
required
SessionInfo

class AsyncStore

Async equivalent of SyncStore with the same guarantees.

Methods

append()

required
required
None

close()

None

create_session()

required
default:"'session'"
default:"None"
SessionInfo

delete_session()

required
bool

get_node()

required
required
Node

get_nodes()

Read up to 1000 identities atomically, preserving input order and duplicates.Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Returned nodes contain independent JSON.
required
required
tuple[Node | None, ...]
tuple[Node | None, …]

get_session()

required
SessionInfo | None

list_children()

required
required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_history()

required
required
default:"1000"
default:"None"
Page[Node]

list_nodes()

required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_sessions()

default:"None"
default:"''"
default:"1000"
default:"None"
Page[SessionInfo]

set_session_ttl()

required
required
SessionInfo

class Node

Attributes

str | None
str
dict[str, str | int | float | bool | None | list[str | int | float | bool | None | list[JSONValue] | dict[str, JSONValue]] | dict[str, str | int | float | bool | None | list[JSONValue] | dict[str, JSONValue]]]
str

exception NodeConflictError

An identity already names a different node, or the tree already has a root.

Attributes

Methods

add_note()

Add a note to the exception

with_traceback()

Set self.traceback to tb and return self.
alias of str

class Page

Attributes

str | None
tuple[TypeVar(T), ...]

class SessionInfo

Attributes

str
str
float
float | None

class SyncMemoryStore

Methods

append()

required
required
None

close()

None

create_session()

required
default:"'session'"
default:"None"
SessionInfo

delete_session()

required
bool

get_node()

required
required
Node

get_nodes()

Read up to 1000 identities atomically, preserving input order and duplicates.Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Returned nodes contain independent JSON.
required
required
tuple[Node | None, ...]
tuple[Node | None, …]

get_session()

required
SessionInfo | None

list_children()

required
required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_history()

required
required
default:"1000"
default:"None"
Page[Node]

list_nodes()

required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_sessions()

default:"None"
default:"''"
default:"1000"
default:"None"
Page[SessionInfo]

set_session_ttl()

required
required
SessionInfo

class SyncSQLiteStore

Persist trees. None TTL means no expiry for this implementation.

Methods

append()

required
required
None

close()

None

create_session()

required
default:"'session'"
default:"None"
SessionInfo

delete_session()

required
bool

get_node()

required
required
Node

get_nodes()

Read up to 1000 identities atomically, preserving input order and duplicates.Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Returned nodes contain independent JSON.
required
required
tuple[Node | None, ...]
tuple[Node | None, …]

get_session()

required
SessionInfo | None

list_children()

required
required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_history()

required
required
default:"1000"
default:"None"
Page[Node]

list_nodes()

required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_sessions()

default:"None"
default:"''"
default:"1000"
default:"None"
Page[SessionInfo]

set_session_ttl()

required
required
SessionInfo

class SyncStore

Store independent trees with atomic, idempotent node insertion. Repeating create_session with the same kind returns the existing tree without renewing TTL. append requires a live tree and an existing parent, except for its single root. Repeating an identical node succeeds; changed data conflicts. Reads return detached JSON. Node IDs are identities, not execution order. Pages use stable identity order; they are live reads, not a shared snapshot. None TTL delegates retention to the implementation. Calls are atomic across clients sharing a backend. Once a write succeeds, subsequent reads through any client observe it unless the tree was deleted or expired. Concurrent calls may take either order; separate calls do not form a transaction. A lost response does not imply that the write failed. Retry append with the same node identity, parent, kind, and data.

Methods

append()

required
required
None

close()

None

create_session()

required
default:"'session'"
default:"None"
SessionInfo

delete_session()

required
bool

get_node()

required
required
Node

get_nodes()

Read up to 1000 identities atomically, preserving input order and duplicates.Missing nodes occupy None slots. A missing or expired tree raises ValueError, including for an empty batch. Returned nodes contain independent JSON.
required
required
tuple[Node | None, ...]
tuple[Node | None, …]

get_session()

required
SessionInfo | None

list_children()

required
required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_history()

required
required
default:"1000"
default:"None"
Page[Node]

list_nodes()

required
default:"None"
default:"1000"
default:"None"
Page[Node]

list_sessions()

default:"None"
default:"''"
default:"1000"
default:"None"
Page[SessionInfo]

set_session_ttl()

required
required
SessionInfo