Resuming & Reconnecting AI Agent Sessions
Here's a scenario every agent builder hits eventually. A user kicks off an agent session, watches it work for a minute, then closes the laptop and goes to lunch. An hour later they come back and expect to pick up exactly where they left off — same working directory, same half-built project, same context. Or it's the other direction: a long agent task needs to pause overnight and resume in the morning. Or, less gracefully, your orchestrator process crashes at 2am and you need to reattach to a sandbox that's still running just fine without it.
I'm Ajay — I build PandaStack, an open-source Firecracker microVM platform — and "how do I resume a session" is one of the most common questions I get, usually phrased as a panic: "my process restarted, is the work gone?" Almost always the answer is no, the work is fine, you just need to look the sandbox back up and reconnect to it. This post is the practical playbook: what a session actually is, how to pause an idle one so it costs nothing, how to reattach a client after a disconnect, and — the part everyone forgets — what survives a resume and what you have to rebuild yourself.
The session IS the sandbox id
The single most useful thing to internalize is this: an agent session is not a live TCP connection to a process you must keep alive. A session is a sandbox, and the sandbox has a stable id. That id is the handle to the entire session — its filesystem, its installed packages, its running processes, its snapshotted memory. As long as you hold the id (or can look it up), you own the session, and any client on any machine can re-attach to it.
This is the mental shift that makes everything else easy. Your orchestrator isn't the session — it's just one client currently talking to the session. If it dies, the sandbox keeps running. When a new orchestrator (or the same one, restarted) comes up, it does a `GET` by id and it's back in business. The connection between client and sandbox is disposable; the sandbox is the durable thing.
from pandastack import Sandbox
# You started a session earlier and stored this id somewhere durable
# (your DB, a KV store, a cookie) — NOT in the memory of a process that
# might crash.
session_id = "sbx_9f2a..." # the handle to the whole session
# An hour later, a different process, a fresh boot — doesn't matter.
# Look the session back up by id:
sbx = Sandbox.get(session_id)
print(sbx.id, sbx.status) # it's still here; the work is intact
# Reattach and keep going — the filesystem is exactly as you left it.
result = sbx.exec("cat /workspace/progress.json")
print(result.stdout)Idle for an hour? Hibernate it, wake it on return
Keeping a sandbox running for the hour your user is at lunch works — but it's paying for CPU and memory to do absolutely nothing. The better move is to hibernate the session when it goes idle and wake it when the user comes back. Hibernate is snapshot-plus-pause: PandaStack captures the VM's memory and disk state and then stops the VM. A hibernated session consumes no CPU and no live memory — it's parked as a snapshot. When the user returns, you wake it, which resumes the VM from that snapshot with its full state intact.
The reason this is even possible without losing anything is the same snapshot-restore machinery every PandaStack create uses. A warm resume — restoring a snapshot that's already staged locally — is fast, in the same neighborhood as a normal snapshot restore (the restore step itself is around 49ms, end-to-end create is p50 179ms / p99 ~203ms). A full cold wake, where the VM has to be rebuilt and its memory downloaded before it can run, is slower — tens of seconds — depending on the path. The point is that an idle session trends toward zero cost while parked, and a returning user gets their exact machine back, not a fresh one.
- Hibernate on idle — when a session has no activity for N minutes, snapshot-and-pause it. Now it costs nothing to keep around, so you can afford to keep it around for a long time instead of tearing it down and losing the work.
- Wake on return — when the next request for that session arrives, wake the sandbox before you route the request to it. A warm resume is fast; the user perceives it as "my session was just sitting there."
- TTL as a backstop — set a generous time-to-live so a session that's abandoned for good eventually reaps itself, but long enough that a normal walk-away never trips it.
This is what makes long-lived sessions economically sane. You're not choosing between "pay to keep every idle session warm" and "tear everything down and lose state." Hibernate gives you a third option: keep the state, drop the cost. (There's a whole post on the tradeoffs of holding sessions open for hours at /blog/long-running-sandboxes-for-ai-agents.)
Reconnecting exec, PTY, and logs to a live sandbox
Reattaching a client is the flip side of "the id is the session." Because operations like exec, PTY (terminal), and log streaming are addressed by sandbox id rather than by a session you have to keep open, a disconnect on the client side doesn't destroy anything on the sandbox side. Your websocket dropped; the sandbox didn't notice. You reconnect by opening a fresh exec/PTY/log stream against the same id.
from pandastack import Sandbox
# Your terminal UI lost its connection mid-session. The sandbox is fine.
# Reattach by looking it up and opening a new exec/stream against the id.
sbx = Sandbox.get(session_id)
# A one-shot command against the existing session — picks up right where
# the filesystem was left:
print(sbx.exec("tail -n 20 /var/log/agent-run.log").stdout)
# Reattach a live PTY (xterm.js-style terminal) to the SAME sandbox.
# The old socket is gone; this is a brand-new stream to the same machine.
with sbx.terminal() as pty:
pty.send("ps aux | grep my-agent\n")
for chunk in pty.stream():
print(chunk, end="")
# Re-follow logs after a dropped stream — same idea, new stream, same id.
for line in sbx.logs(follow=True):
print(line)The important nuance: a long-running process you started inside the sandbox keeps running whether or not any client is attached. If you launched an agent as a detached process (`setsid ... &`, or a systemd-style service inside the guest), it doesn't care that your exec stream disconnected — it's chugging along, writing to a log file. When you reconnect, you don't restart it; you tail its log and check its state. The disconnect was a client event, never a sandbox event. Attach a client to watch; detach and the work continues; reattach and you're watching again.
What survives a resume — and what you must rebuild
This is the part that trips people up, and I want to be completely honest about it. A resume restores the machine, not the moment. The filesystem and the snapshotted RAM come back exactly as they were. The network does not — a resumed VM's previously-open TCP connections are dead, and its clock was frozen at snapshot time until the platform syncs it on wake. Treat a resume as "same machine, new network moment," and you'll never be surprised.
- Filesystem — fully survives. Every file, every installed package, every checkpoint you wrote to disk is exactly as you left it. This is your most reliable place to stash progress: write it to disk and it's there after a resume, a hibernate, even an orchestrator crash.
- RAM / process state — survives via the snapshot. The VM's memory is captured and restored, so in-memory variables, a paused Python process, a loaded model in RAM — all come back. This is the magic of snapshot-restore: you resume into the exact memory state, not a re-run from scratch.
- Open sockets / network connections — do NOT survive. A snapshot freezes the guest's TCP state, but the other end of every connection (a database, an upstream API, an SSH peer) moved on long ago. Those file descriptors point at dead connections. You must re-establish them on resume: reconnect your DB pool, re-open your HTTP clients, re-auth anything with a session token that may have expired.
- Wall clock — frozen at snapshot time until synced. A restored guest wakes believing it's still the instant it was hibernated. PandaStack syncs the guest clock on wake so time-sensitive code (TLS cert validation, token expiry, timestamps) sees the real current time — but any wall-clock assumption your code cached before hibernation is stale until that sync lands.
- In-flight requests — do NOT survive. An HTTP request that was mid-flight when you hibernated didn't pause and resume; it failed the moment the connection died. Anything in-flight at snapshot time must be retried, not awaited, after a resume.
The practical consequence is a small "on-resume" ritual in your agent code: after a wake, re-establish connections before you trust them. Reconnect the database pool, re-create HTTP clients, refresh any token that might have expired while the session was parked, and retry any request that was in flight. Everything on disk and in memory is already correct; it's only the wires to the outside world you have to plug back in. (For the deeper story on persisting agent memory and state across sessions, see /blog/ai-agent-persistent-memory-state.)
The idempotent get-or-create pattern
Put it all together and you get a single helper every agent app should have: given a session id, hand me back a ready-to-use sandbox — whether that means waking a hibernated one, reattaching to a running one, or creating a fresh one if this is the very first turn. It's idempotent by session id: call it a hundred times for the same session and you get the same sandbox, not a hundred VMs. This is the function your orchestrator calls at the top of every turn, and it makes crashes and reconnects boring.
from pandastack import Sandbox
def get_or_create_session(session_id: str) -> Sandbox:
"""Return a ready sandbox for this session, idempotently.
Wake it if hibernated, reattach if running, create it if it's new.
Safe to call at the top of every agent turn."""
# 1. Do we already have a sandbox for this session? We keep the mapping
# in OUR database, keyed by session_id, so it survives our restarts.
existing_id = db.get_sandbox_id(session_id)
if existing_id:
sbx = Sandbox.get(existing_id)
# 2. If it's hibernated, wake it (warm resume: fast). If it's already
# running, this is a no-op and we just reattach.
if sbx.status == "hibernated":
sbx.wake()
# 3. On resume, the machine is back but the wires aren't. Re-establish
# outbound connections before we trust them.
reconnect_outbound(sbx) # DB pool, HTTP clients, refresh tokens
return sbx
# 4. First turn for this session: create fresh (snapshot-restore,
# ~179ms p50), then remember the id so next time we reattach.
sbx = Sandbox.create(
template="agent",
metadata={"session_id": session_id}, # lets you find it by session too
)
db.set_sandbox_id(session_id, sbx.id)
return sbx
def reconnect_outbound(sbx: Sandbox) -> None:
"""After a wake, the guest's old TCP connections are dead. Re-open them."""
sbx.exec("systemctl restart my-agent-db-pool || true")
# ...refresh API tokens, re-auth clients, retry any in-flight work.
# ---- one agent turn, from anywhere, after any crash ----
def run_turn(session_id: str, user_message: str) -> str:
sbx = get_or_create_session(session_id)
result = sbx.exec(f"python3 /workspace/agent.py {shlex.quote(user_message)}")
# 5. Done for now — the user might walk away. Hibernate so an idle
# session costs ~nothing, but keeps ALL its state for next time.
sbx.hibernate()
return result.stdoutTwo details make this robust. First, the source of truth for "which sandbox belongs to this session" lives in your database, not in your process memory — so a restart re-derives it instantly. (You can also stash `session_id` in the sandbox `metadata` as a secondary lookup path, so you can find orphaned sandboxes even if your mapping row is lost.) Second, `hibernate()` at the end of a turn is what turns this from an expensive always-warm pool into a park-it-cheaply model: the session keeps every byte of its state, but stops billing you for idle CPU and RAM the moment the user's attention wanders.
Putting it together
Resuming an agent session comes down to a few honest truths. The session is a sandbox with a stable id; hold the id somewhere durable and you own the session no matter how many times your orchestrator restarts. An idle session should hibernate — snapshot-and-pause — so it costs ~nothing while parked, and wake on return with its full filesystem and RAM intact. Reconnecting a client is just opening a fresh exec, PTY, or log stream against the same id; the work inside kept running the whole time. And on resume, remember that the machine came back but the network didn't: the disk and memory are exactly as you left them, but every open socket is stale and every in-flight request failed, so re-establish your connections and retry your requests. Wrap all of it in an idempotent get-or-create-by-session-id helper, and walking away, coming back, and even crashing become non-events. The user closes their laptop at lunch and, an hour later, opens it to the exact machine they left — because that's precisely what it is.
Frequently asked questions
How do I reconnect to an agent sandbox after my process crashes?
Look it up by id. A session is a sandbox with a stable id, and that id is the handle to the whole session — filesystem, processes, and snapshotted memory. Store the id in a durable place (your database, keyed by user or thread), not in the memory of a process that might die. When a new or restarted orchestrator comes up, it does a GET by id and reattaches — the sandbox kept running the entire time your process was down. If you also stash the session id in the sandbox metadata, you can find an orphaned sandbox even if your own mapping row is lost.
Does an idle agent session keep costing money?
Not if you hibernate it. Hibernate is snapshot-plus-pause: PandaStack captures the VM's memory and disk state and stops the VM, so a parked session consumes no CPU and no live memory. When the user returns, you wake it and the VM resumes from that snapshot with full state intact. A warm resume (snapshot already staged locally) is fast — in the neighborhood of a normal snapshot restore; a full cold wake that has to rebuild the VM and download its memory is slower, on the order of tens of seconds depending on the path. Either way, an idle session trends toward zero cost while keeping all its state.
What survives when I resume a hibernated sandbox, and what doesn't?
The filesystem fully survives — every file, package, and checkpoint on disk is exactly as you left it. The RAM and process state survive via the snapshot, so in-memory variables and paused processes come back. What does NOT survive: open TCP connections and in-flight network requests. A snapshot freezes your side of a socket, but the peer (a database, an API) closed the connection long ago, so those file descriptors are dead — you must reconnect on resume. The guest clock is also frozen at snapshot time until the platform syncs it on wake. Treat a resume as 'same machine, new network moment.'
Do the processes inside my sandbox keep running if my client disconnects?
Yes, as long as you ran them as detached processes inside the guest rather than tying them to your exec stream. A disconnect is a client event — the sandbox never notices. If you launched an agent with setsid (or as a guest service) that writes progress to a log file, it keeps running while no client is attached. To reconnect, you open a fresh exec, PTY, or log stream against the same sandbox id and tail the log; you don't restart the work. The rule of thumb: if the work dies when your socket drops, run it detached instead.
What's the idempotent get-or-create-by-session-id pattern?
It's a single helper you call at the top of every agent turn: given a session id, it returns a ready sandbox — waking a hibernated one, reattaching to a running one, or creating a fresh one if it's the first turn. It's idempotent by session id, so calling it repeatedly for the same session yields the same sandbox, not many VMs. Keep the session-id-to-sandbox-id mapping in your database so it survives your restarts, wake the sandbox if it's hibernated, re-establish outbound connections after the wake, and hibernate at the end of the turn so an idle session costs almost nothing while retaining every byte of state.
49ms p50 cold start. Fork, snapshot, and scale to zero.