# Advanced: resume a background operation For ordinary commands, start with {doc}`getting-started` and its sandbox factory. This guide exposes lower-level controls. See {doc}`manual-sandbox` for manual component assembly and resource ownership. Use `spawn_detached()` when a command must continue after the current Python process exits. The session records its UUID, source image, parent entry, branch, command options, and uploaded attachment metadata. It does not start an event reader. The server still applies the operation's configured timeout. Use a SQLite store for persistence across processes. Memory stores implement the same API but retain records only while that store instance exists. Keep the same session ID, database, and endpoint when resuming. ## Submit work The first block submits work and closes the local store and client. It leaves the remote operation running. Store the operation UUID in your application or retrieve it through `session.list_operations()` after restarting Python. The async variant uses native transport and storage. No event loop or background reader must remain running between submission and collection. Run async blocks inside an async function or a compatible notebook. ::::{tab} Sync ```python from contree_client import profiles from contree_client.sync import ContreeClient from contree_sdk import ContreeSession from contree_sdk.store import SyncSQLiteStore with ( ContreeClient.from_profile(profiles.from_env() or profiles.resolve_profile()) as client, SyncSQLiteStore("session.db") as store, ): session = ContreeSession(client, image="tag:alpine:latest", session_id="background", store=store) record = session.spawn_detached(shell="printf saved > /result.txt; cat /result.txt") operation_uuid = record.uuid ``` :::: ::::{tab} Async ```python from contree_client import profiles from contree_client.asyncio import ContreeAsyncClient from contree_sdk import ContreeAsyncSession from contree_sdk.store import AsyncSQLiteStore async with ( ContreeAsyncClient.from_profile(profiles.from_env() or profiles.resolve_profile()) as client, AsyncSQLiteStore("async.db") as store, ): session = ContreeAsyncSession(client, image="tag:alpine:latest", session_id="background", store=store) record = await session.spawn_detached(shell="printf saved > /result.txt; cat /result.txt") operation_uuid = record.uuid ``` :::: ## Collect the result Run the next block in the process that collects the result. `wait_operation()` uses the session's commit policy and records completion atomically with history. It returns the process result, including a nonzero exit code. The consumer needs the same working directory and connection profile. It obtains the UUID from the store, so no Python variables from the submitting process are needed. This example has exactly one pending operation; applications with several operations must select the intended record or accept a UUID as an argument. ::::{tab} Sync ```python from contree_client import profiles from contree_client.sync import ContreeClient from contree_sdk import ContreeSession from contree_sdk.store import SyncSQLiteStore with ( ContreeClient.from_profile(profiles.from_env() or profiles.resolve_profile()) as client, SyncSQLiteStore("session.db") as store, ): session = ContreeSession(client, session_id="background", store=store) pending = session.list_operations() (record,) = pending operation_uuid = record.uuid result = session.wait_operation(operation_uuid) repeated_result = session.wait_operation(operation_uuid) assert result == repeated_result assert session.list_operations() == () assert len(session.history()[0]) == 2 assert session.list_operations(pending_only=False)[0].history_id is not None ``` :::: ::::{tab} Async ```python from contree_client import profiles from contree_client.asyncio import ContreeAsyncClient from contree_sdk import ContreeAsyncSession from contree_sdk.store import AsyncSQLiteStore async with ( ContreeAsyncClient.from_profile(profiles.from_env() or profiles.resolve_profile()) as client, AsyncSQLiteStore("async.db") as store, ): session = ContreeAsyncSession(client, session_id="background", store=store) pending = await session.list_operations() (record,) = pending operation_uuid = record.uuid result = await session.wait_operation(operation_uuid) repeated_result = await session.wait_operation(operation_uuid) assert result == repeated_result assert await session.list_operations() == () assert len((await session.history())[0]) == 2 assert (await session.list_operations(pending_only=False))[0].history_id is not None ``` :::: ## Branches and commit policy Completion keeps the parent captured at spawn time. Switching branches or continuing other work does not redirect or reject the result. The completed entry is another child of its original parent. Obtain its ID from `session.get_operation(uuid).history_id` and use `navigate()` to select it. Waiting can advance the local position when it still matches the source; replaying an already completed result does not undo a later rollback. The default `ApiSuccessCommitPolicy` retains every successful non-disposable result, including nonzero exit codes. Pass `commit_policy=ZeroExitCommitPolicy()` when resuming to retain only exit code zero. See {doc}`customization` for custom policies. Concurrent completion calls share the first stored decision. A later policy change or rollback cannot cause a second commit. Disposable and policy-rejected results remain readable without a history entry. Final API failures and cancellation remain readable as the same exceptions. Completed `wait_operation()` calls use the stored response without remote I/O. Transport errors remain pending. A timeout or cancellation of a live wait follows `Operation.wait()` semantics and requests remote cancellation; it does not merely stop observing the operation. `wait_operation()` closes its internal handle before returning or raising. Async cleanup completes even after repeated caller cancellation. Synchronous cleanup uses a bounded reader join; a blocked transport can keep its reader thread alive until the stream releases it. See {doc}`operation` for lifecycle details. ## Control and inspect registered work `detach(operation)` registers an existing session operation without closing its handle. `restore_operation(uuid)` reconstructs a handle through `create_operation()` with the original context. Override `create_operation_record()` and `restore_operation_context()` together to persist application-specific request fields. Both restoration and commit policies use these hooks. Use that handle to send signals, cancel the operation, or read events. After `wait()`, `commit_result()` on a restored handle uses the same atomic completion path. Use `wait_operation()` to replay a stored response. `list_operations()` returns pending records. Set `pending_only=False` to include completed records. `OperationRecord.history_id` identifies a retained result; `response` stores the final API response as a JSON object. Records include the effective command environment and output, but not client credentials. A different client endpoint is rejected before the UUID is used. Local file and stdin sources are not serialized or reopened. Attachments retain their uploaded UUIDs and destination permissions. Restored `OperationContext` values have `request.files=None` and `request.stdin=None`; inspect `attachments` and `files` for the submitted inputs. Commit policies must use this persisted context, rather than source objects. Detached titles and file metadata are fixed at registration; `commit_result()` cannot override them. ## Failure boundaries and custom stores Remote submission and local registration are separate operations. If registration raises normally, `spawn_detached()` requests cancellation and preserves the registration error. A process crash between remote submission and registration can leave an unregistered remote operation. Registration does not promise exactly one remote submission. Retrying submission can start another operation. A pending node stores the original context and the identity reserved for its result. Completion appends one immutable node containing the final response and optional retained image. An identical write can be retried. If concurrent collectors disagree on commit policy, the first saved completion is retained. Deleting the tree removes both records; it does not cancel the remote operation. Custom stores need only the tree contract, including atomic and idempotent `append()`. They do not implement a second operation registry API. See {doc}`reference/store` for the guarantees required across processes.