Skip to content

Resuming sessions

caw can resume a conversation later — in the same process, after a restart, or in a completely different process. Grab a resume_handle (a string), store it anywhere (a database, a file, a queue), and resume from it.

# Process 1: start, communicate, persist the handle.
agent = Agent(provider="claude_code")
session = agent.start_session()
session.send("My deploy target is staging-eu. Remember that.")
handle = session.resume_handle          # store this string
session.end()

# Process 2 (later, after a restart): resume by handle.
agent = Agent(provider="claude_code")
session = agent.resume_session(handle)
print(session.send("Where am I deploying?").result)   # -> "staging-eu"
session.end()

The handle is self-contained

The handle is a JSON string carrying the backend's own resume key, so resuming works even with no data_dir — the underlying CLI still has the conversation:

{"version": 1, "provider": "claude_code", "session_id": "bd260210-…", "resume_key": "bd260210-…"}

resume_key is Claude's session id, Codex's thread_id, or opencode's session id — for codex/opencode it differs from session_id. Resuming works across all three providers.

Treat the handle like a secret

The handle grants resume access to the conversation — it is not an opaque random id. Store it with the same care as a credential.

Send before reading the handle

The backend assigns its resume key on the first exchange, so send at least one message before reading session.resume_handle. Reading it earlier raises.

data_dir is optional and additive

Whether you resume with or without the original data_dir changes only how much caw-side history you get back — the backend conversation resumes either way:

without data_dir with the original data_dir
backend conversation resumed resumed
caw trajectory starts empty full history restored
new turns not persisted appended to the original session dir

A bare session id is also accepted in place of a full handle, but only when data_dir is set (the resume key is then read from disk).

Full example

examples/resume.py shows both the with- and without-data_dir paths:

"""Resuming a session across processes.

Start a session, store its ``resume_handle`` (a string), then resume the
conversation later with a brand-new Agent — as if a new process picked it up.

The handle is self-contained, so resume works even without a ``data_dir``
(the backend CLI still has the conversation). With a ``data_dir`` you also get
the full trajectory restored and new turns appended. This example shows both.
"""

import os

os.environ["CAW_LOG"] = "full"

from caw import Agent

DATA_DIR = "caw_data"


def main():
    # --- "Process 1": start a session and grab a handle to store somewhere. ---
    agent = Agent(data_dir=DATA_DIR)
    session = agent.start_session()
    session.send("My favorite number is 42. Acknowledge it.")
    handle = session.resume_handle
    session.end()

    print(f"\nStored resume_handle: {handle}\n")
    # In a real app you'd persist `handle` (DB, file, queue) and exit here.

    # --- "Process 2a": a fresh Agent WITH the same data_dir restores history. ---
    agent2 = Agent(data_dir=DATA_DIR)
    resumed = agent2.resume_session(handle)
    turn = resumed.send("What is my favorite number?")
    resumed.end()
    print(f"\n[with data_dir]    recalled: {turn.result!r}")
    print(f"[with data_dir]    turns in trajectory: {resumed.trajectory.num_turns}")

    # --- "Process 2b": a fresh Agent with NO data_dir still resumes the chat. ---
    agent3 = Agent(data_dir=None)
    resumed2 = agent3.resume_session(handle)
    turn2 = resumed2.send("And what is my favorite number again?")
    resumed2.end()
    print(f"\n[without data_dir] recalled: {turn2.result!r}")
    # Trajectory starts empty here, so only this turn is recorded.
    print(f"[without data_dir] turns in trajectory: {resumed2.trajectory.num_turns}")


if __name__ == "__main__":
    main()