How to Implement Persistent Session Resume via Reconnect in Open Agents

You can resume a disconnected agent session by storing the sandbox's VercelState (including its durable sandboxName) in your session database, then calling VercelSandbox.connect() with that state to reattach to the running cloud VM, verify liveness with a probe, and refresh the timeout.

Open Agents uses the Vercel Sandbox SDK to provide persistent, cloud-based execution environments that survive page reloads and network drops. Understanding how to implement persistent session resume via reconnect in Open Agents ensures your users never lose work when connections flake, as the underlying VM continues running in the cloud and can be reattached using its unique name.

Architecture Overview

The reconnect flow operates in three distinct phases orchestrated between your client, API layer, and the Vercel Sandbox infrastructure.

First, after initial sandbox creation, you persist the minimal state required for reconnection. Second, when a user returns or reconnects, you probe the existing VM to verify it is still alive. Third, you refresh the database with any updated timeout information and return a status payload to the client.

This design ensures that even if a user's browser closes completely, the agent's execution context—including variables, filesystem state, and running processes—remains intact in the cloud sandbox.

Persisting Sandbox State

Before you can reconnect, you must store the sandbox's identifying metadata. Open Agents defines this through the VercelState interface.

// packages/sandbox/vercel/state.ts
export interface VercelState {
  /** Where to clone from (omit for empty sandbox or when reconnecting/restoring) */
  source?: Source;
  /** Durable persistent sandbox name used for reconnecting/resuming sessions */
  sandboxName?: string;
  /** Legacy runtime sandbox ID from the stable SDK. */
  sandboxId?: string;
  /** Snapshot ID used only for legacy restore/migration flows */
  snapshotId?: string;
  /** Timestamp (ms) when the current runtime session expires */
  expiresAt?: number;
}

When you create a sandbox via VercelSandbox.create, the SDK either accepts a user-provided name or generates one automatically. This name is stored on the instance and retrieved later via getState().

// After creation, persist to your session DB
const state = sandbox.getState(); // Returns VercelState
await updateSession(sessionId, { sandboxState: state });

The sandboxName acts as the durable identifier that allows the SDK to locate the specific VM instance across disconnections.

The Reconnect API Endpoint

The public interface for resuming sessions lives in apps/web/app/api/sandbox/reconnect/route.ts. This handler accepts a sessionId query parameter, validates the stored state, and attempts to reattach to the sandbox.

// apps/web/app/api/sandbox/reconnect/route.ts
export async function GET(req: Request): Promise<Response> {
  // 1️⃣ Load the session and verify runtime state exists
  if (!hasRuntimeSandboxState(sessionRecord.sandboxState)) {
    return Response.json({ status: "no_sandbox" });
  }

  // 2️⃣ Attempt to connect and probe the VM
  const sandbox = await connectSandbox(state as SandboxState);
  const probe = await sandbox.exec("pwd", sandbox.workingDirectory, 15_000);
  
  // 3️⃣ Refresh database with latest state (including new expiresAt)
  const refreshedState = sandbox.getState?.() ?? {
    ...state,
    ...(sandbox.expiresAt ? { expiresAt: sandbox.expiresAt } : {})
  };
  
  await updateSession(sessionId, {
    sandboxState: refreshedState,
    sandboxExpiresAt: getSandboxExpiresAtDate(refreshedState),
  });

  // 4️⃣ Return payload for UI synchronization
  return Response.json({
    status: "connected",
    hasSnapshot: hasPausedState,
    expiresAt: sandbox.expiresAt,
    lifecycle: buildLifecyclePayload(updatedSession ?? sessionRecord),
  });
}

The endpoint uses hasRuntimeSandboxState from apps/web/lib/sandbox/utils.ts to distinguish between active sandboxes, paused snapshots, and unavailable states. If the probe succeeds, the session continues; if it fails with an unavailable error, the handler clears the stale state and returns status: "expired".

Connecting to Existing Sandboxes

The core reconnection logic resides in the VercelSandbox.connect static method. This function attaches to an existing VM by name and reconstructs the sandbox instance with the appropriate timeout and environment context.

// packages/sandbox/vercel/sandbox.ts
static async connect(
  sandboxName: string,
  options: {
    env?: Record<string, string>;
    githubToken?: string;
    hooks?: SandboxHooks;
    /** Remaining timeout in ms for this sandbox session. */
    remainingTimeout?: number;
    /** Ports that were declared at creation time */
    ports?: number[];
    /** Whether to explicitly resume a stopped sandbox */
    resume?: boolean;
  } = {},
): Promise<VercelSandbox> {
  const sdk = await VercelSandboxSDK.get({
    name: sandboxName,
    resume: options.resume ?? false,
  });
  
  await syncGitHubCredentialBrokering(sdk, options.githubToken);
  const session = sdk.currentSession();

  // Derive remaining timeout from live session metadata
  const remainingTimeout =
    options.remainingTimeout ??
    getRemainingTimeoutFromSession(session) ??
    (isStoppedSessionStatus(session.status) ? undefined : DEFAULT_RECONNECT_TIMEOUT_MS);
    
  const startTime = remainingTimeout !== undefined ? Date.now() : undefined;

  return new VercelSandbox(
    sdk,
    session,
    sandboxName,
    session.sessionId,
    DEFAULT_WORKING_DIRECTORY,
    options.env,
    undefined,
    options.hooks,
    remainingTimeout,
    startTime,
    options.ports,
  );
}

Key implementation details include:

  • Name-based attachment: VercelSandboxSDK.get locates the VM using the durable sandboxName.
  • Timeout preservation: The method calculates remainingTimeout from live session metadata, ensuring the countdown continues from where it left off rather than resetting.
  • Resume flexibility: The resume parameter controls whether to wake a stopped sandbox or simply attach to a running one.

Handling Timeouts and Lifecycle

Persistent sessions require careful timeout management to prevent resource leaks while allowing reasonable reconnection windows.

When you initially create a sandbox, VercelSandbox.create stores the effectiveTimeout and schedules a proactive stop. Upon reconnection, VercelSandbox.connect accepts a remainingTimeout value derived from the live SDK session via getRemainingTimeoutFromSession.

If the SDK does not expose a remaining timeout (for example, if the session metadata is stale), the system falls back to DEFAULT_RECONNECT_TIMEOUT_MS (5 minutes). This ensures the UI can still display a valid countdown even when exact session metadata is unavailable.

// Calculate expiry for database storage
const expiresAt = startTime ? startTime + remainingTimeout : undefined;

This approach maintains continuity—if a user disconnects with 10 minutes remaining, they reconnect with approximately 10 minutes left, not a full reset.

Error Handling and Cleanup

Network partitions and VM terminations require graceful degradation. The reconnect route uses isSandboxUnavailableError from apps/web/lib/sandbox/utils.ts to detect transient failures such as HTTP 410 (Gone) or 404 (Not Found) responses from the sandbox probe.

When an unavailable error occurs:

  1. The handler calls clearUnavailableSandboxState to remove stale references from the session record.
  2. The session lifecycle updates to hibernated via buildHibernatedLifecycleUpdate.
  3. The API returns status: "expired" so the UI can prompt for a fresh sandbox or snapshot restoration.
// Error handling pattern in route.ts
try {
  const probe = await sandbox.exec("pwd", sandbox.workingDirectory, 15_000);
} catch (error) {
  if (isSandboxUnavailableError(error.message)) {
    await clearUnavailableSandboxState(sessionId);
    return Response.json({ status: "expired" });
  }
  throw error;
}

This pattern prevents "ghost" sessions where the database claims a sandbox exists but the underlying VM has been garbage collected.

Client-Side Implementation

To implement reconnection in your frontend, call the API endpoint when the component mounts or when the user explicitly requests to resume.

// Example: Reconnect from a React component
async function reconnect(sessionId: string) {
  const resp = await fetch(`/api/sandbox/reconnect?sessionId=${sessionId}`);
  const data = await resp.json() as {
    status: "connected" | "no_sandbox" | "expired";
    expiresAt?: number;
    hasSnapshot: boolean;
    lifecycle: LifecyclePayload;
  };

  switch (data.status) {
    case "connected":
      // Sandbox is alive - store the expiresAt for your countdown UI
      console.log(`Reconnected. Expires at: ${new Date(data.expiresAt!)}`);
      break;
      
    case "no_sandbox":
      // No runtime VM available - offer to create new or restore from snapshot
      break;
      
    case "expired":
      // VM terminated while offline - show warning and recovery options
      break;
  }
  
  return data;
}

The expiresAt field enables you to synchronize countdown timers across reconnections, while the lifecycle object provides metadata for displaying hibernation status or error states.

Summary

  • Persist state early: Store the VercelState object (containing sandboxName and expiresAt) in your session database immediately after sandbox creation.
  • Use the connect method: Reattach to existing VMs using VercelSandbox.connect(sandboxName, { remainingTimeout }) rather than creating new instances.
  • Probe for liveness: Execute a lightweight command like pwd to verify the VM is responsive before declaring the session resumed.
  • Handle expiration gracefully: Implement isSandboxUnavailableError checks to clear stale state and return clear status codes (expired, no_sandbox) to the client.
  • Preserve timeouts: Pass the remainingTimeout from the previous session to maintain consistent resource limits across disconnections.

Frequently Asked Questions

What happens if the sandbox expired while the user was offline?

If the underlying VM was terminated due to timeout or infrastructure cleanup, the probe in apps/web/app/api/sandbox/reconnect/route.ts will fail with an unavailable error. The handler catches this, clears the stale sandboxState from the database using clearUnavailableSandboxState, and returns status: "expired". Your frontend should detect this status and offer to create a new sandbox or restore from a snapshot.

How long can a session remain disconnected before it expires?

The maximum disconnection time depends on the remainingTimeout calculated when the sandbox was created or last reconnected. The fallback DEFAULT_RECONNECT_TIMEOUT_MS is 5 minutes, but your specific implementation may configure longer windows through the Vercel Sandbox SDK limits. The expiresAt timestamp returned by the reconnect API reflects the exact millisecond when the session will terminate.

Can I reconnect to a sandbox from a different device or browser?

Yes. Since the sandboxName is the only required identifier and it is stored in your centralized session database (not browser local storage), any client that authenticates as the same user and session can retrieve the VercelState and call VercelSandbox.connect. The VM runs in the cloud, independent of the client device.

What is the difference between the resume and reconnect options?

Reconnect (VercelSandbox.connect) attaches to an existing running or stopped sandbox by name, optionally calculating a new timeout. Resume specifically refers to waking a stopped sandbox from hibernation. In the connect method options, setting resume: true instructs the SDK to explicitly restart a stopped VM, whereas the default resume: false simply attaches to whatever state exists (running or stopped) without modification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →