Agent infrastructure · Process lifecycle · 8 min read

Disconnect the terminal. Keep the agent running.

A terminal session and a sandbox have different lifetimes. Use that difference deliberately: start a detached process, persist its state in the workspace, reconnect from another process, and let one controller own final deletion.

01 · Three lifetimes

Do not make the terminal your scheduler.

A PTY exists until the WebSocket or shell exits. A child started in that foreground session may receive a hangup when the PTY disappears. The sandbox container is different: it remains running until an explicit delete, and processes properly detached from the PTY can remain with it.

Terminal

Human-scale and interactive. Safe to disconnect, but not a durable process supervisor.

Worker process

Detached from stdin, writing logs and checkpoints to /workspace.

Sandbox

The billing and isolation boundary. It survives both the launcher and terminal.

02 · A minimal worker

Put progress on disk, not only in memory.

The example consumes a newline-delimited task file and records an offset after every completed item. It is intentionally small. A production queue should provide atomic claims, retries, visibility timeouts, and idempotent task handlers; the sandbox itself is only the execution boundary.

worker.py
# worker.py
import json
import time
from pathlib import Path

queue = Path("/workspace/tasks.jsonl")
checkpoint = Path("/workspace/checkpoint.txt")

offset = int(checkpoint.read_text() or 0) if checkpoint.exists() else 0

while True:
    lines = queue.read_text().splitlines() if queue.exists() else []
    for line in lines[offset:]:
        task = json.loads(line)
        print(f"processing {task['id']}", flush=True)
        # Replace this with an agent invocation or finite task handler.
        time.sleep(2)
        offset += 1
        checkpoint.write_text(str(offset))
    time.sleep(5)

03 · Launch and leave

Detach every inherited stream.

nohup handles a hangup signal, but that is only part of detaching. Redirect stdin away from the API call and send stdout and stderr to a file. Record the PID so later controllers can distinguish “the sandbox exists” from “the worker is alive.”

launch.py
from boltzlabs import Sandbox

WORKER = open("worker.py").read()

# Do not use a context manager: this machine is meant to outlive this launcher.
sb = Sandbox(
    environment="python",
    machine="small",
    name="background-worker",
    internet=True,
)

# There is no upload primitive. Run a small Python snippet that writes the file.
sb.run(
    f"from pathlib import Path; Path('/workspace/worker.py').write_text({WORKER!r})",
    language="python",
).check()

sb.exec(
    "nohup python3 -u /workspace/worker.py "
    "</dev/null >/workspace/worker.log 2>&1 "
    "& echo $! >/workspace/worker.pid"
).check()

print(sb.id)
Why no with block? A context manager deletes the sandbox when the launcher exits. That is exactly right for a finite job and exactly wrong when the process is supposed to outlive its launcher.

04 · Operate it

Reconnect by name and inspect finite snapshots.

Sandbox exec captures output after a finite command completes; it is not a continuous log stream. Use short status and tail commands for polling, or connect a PTY when a human needs to observe the workspace directly.

terminal shell
bzlabs exec background-worker   'pid=$(cat /workspace/worker.pid); kill -0 "$pid" && echo running'

bzlabs exec background-worker 'tail -n 50 /workspace/worker.log'
bzlabs exec background-worker 'cat /workspace/checkpoint.txt'
enqueue.py
import shlex
from boltzlabs import sandbox

sb = sandbox("background-worker")
task = '{"id":"index-docs","repo":"https://github.com/example/docs.git"}'

sb.exec(
    f"printf '%s\n' {shlex.quote(task)} >> /workspace/tasks.jsonl"
).check()

The Python SDK can attach to the same named sandbox from a different process. Keep the name unique among running sandboxes and treat the workspace as the handoff point.

reconnect.py
import boltzlabs

sb = boltzlabs.sandbox("background-worker")
print(sb.status)
print(sb.exec("tail -n 20 /workspace/worker.log").stdout)

# Cleanup belongs in the controller that owns the job lifecycle.
sb.delete()

05 · Stop and clean up

Deletion is the terminal state.

Stopping a worker leaves its logs available for review and leaves the meter running. Deleting the sandbox removes the container and workspace. Export anything you need before that step; there is no snapshot or restore API.

terminal shell
# Ask the process to stop, wait briefly, then destroy the machine.
bzlabs exec background-worker   'kill -TERM "$(cat /workspace/worker.pid)" 2>/dev/null || true'

bzlabs rm background-worker

Current operational boundary

BoltzLabs does not currently supervise or retry the detached process, stream its stdout, or enforce the accepted idle-timeout and maximum-lifetime fields. Run an external reconciler if jobs must be restarted or garbage-collected without human intervention.