---
title: "Execution Model and Durability"
description: "How an eve session runs. Durable conversations, turns that checkpoint at steps, and parked work that resumes later."
---

An eve session is a durable conversation. It can run for days and survives process restarts and redeploys without any work on your part. You write the capabilities (tools, instructions, channels) and eve runs the loop.

## Sessions, turns, and steps

Work nests in three levels:

- **session**: the whole durable conversation or task. It's long-lived and can span many requests over days or weeks without losing context.
- **turn**: one user message and all the work it triggers (model calls, tool calls, reasoning) until the agent produces its response.
- **step**: a durable checkpoint inside a turn. By default, it contains one model call and the inline tool calls that follow it.

Every session runs as one durable workflow, built on the open-source [Workflow SDK](https://workflow-sdk.dev/) (Vercel Workflow when you deploy on Vercel). The session workflow executes each turn and checkpoints progress at step boundaries. Your code runs inside a managed step, so tools, the sandbox, and subagents feel synchronous even though the session underneath them is durable.

The experimental
[`workflow.modelCallsPerStep`](../agent-config#workflow-checkpoint-batching)
agent setting can group several sequential model-and-tool cycles into one
Workflow step. This reduces checkpoint overhead but makes the entire group one
replay unit.

The Workflow SDK is not inherently tied to Vercel. In local development and in a self-deployed `eve start` process, eve uses the SDK's local world by default; that world persists workflow runs on disk under `.eve/.workflow-data` and dispatches through the same Nitro-hosted workflow routes. On Vercel, the same workflow code runs against Vercel Workflow instead, which adds deployment-aware routing and dashboard run metadata.

When a Vercel production deployment changes, a new delivery to an idle session hands the session's settled state to the exact deployment that accepted the request. This applies whether the delivery arrives through the session ID or through any channel continuation address the session has claimed. The session keeps its original ID and event stream, while the next model turn uses that deployment's current system-role instructions, model, and tools. Static user-role instructions stay pinned in the durable conversation history and are not seeded again. A successful handoff restarts the session's original configured [`sessionTimeoutMs`](../agent-config#runtime-limits) duration; disabled timeouts stay disabled. Failed or skipped handoffs keep the existing deadline.

Before taking ownership, the target deployment upgrades the checkpoint when an older eve release wrote it, then checks that no pending work remains. A deployment accepts checkpoints from eve 0.66.0 and later, so upgrading eve does not leave long-lived sessions, such as a Slack thread, on their old deployment. An older release cannot read a newer checkpoint: after a rollback, a session that already moved to the newer deployment stays there. Extension state saved before eve 0.69 moves to its mount when exactly one mount of that extension package defines that state. State that the target deployment no longer defines, such as a removed `defineState` key, is dropped, and the target deployment's runtime logs show a `dropping unknown context key during deserialization` warning. When two or more mounts of one extension package define that state, eve cannot choose an owner, and the session stays on its current deployment. Idle subagent sessions that eve 0.66–0.68 kept for later reuse are stopped after the handoff. Authored session state remains opaque to this validation.

If the target deployment rejects the session, the previous owner keeps it and processes the triggering delivery. When the target deployment cannot read the checkpoint, its Vercel runtime logs show a `session handoff refused` warning with the checkpoint version and the reason. Owners on this release or later also write a `session handoff failed` warning to their own runtime logs, with the target deployment and the reason.

eve does not move live work between deployments. A session with a running turn, pending human input or authorization, queued messages, or active tasks and subagents stays on its current deployment. After that work settles, a later delivery can move the idle session; the earlier deployment signal is not remembered.

### Compaction handoff

Every workflow run records each step it executes, so a session that ran on one run for weeks would carry its whole history in that run. To bound this, eve moves a session to a fresh run on its current deployment after [compaction](./default-harness#compaction) rewrites the conversation history. This applies to automatic compaction during a turn and to a client or channel compaction request.

The move happens once the turn settles and nothing else is waiting: no queued messages, running tasks or subagents, or pending human input. The new run receives the compacted history and settled state, then waits for the next message. If a single message reaches an otherwise idle session before the move completes, eve moves the session with that message, and the message's turn runs on the new run. If work is still pending, eve tries again after a later turn settles. The session keeps its ID, event stream, continuation addresses, and deadline; compaction never extends [`sessionTimeoutMs`](../agent-config#runtime-limits). If the new run cannot take ownership, the current run keeps the session, writes a `session handoff failed` warning to its runtime logs, and tries again later.

### Stranded sessions

A session is stranded when another eve version built the workflow run that owns it. A run can only be replayed by the eve version that started it, so the conversation is still stored, but nothing on the current deployment can process a new message. On worlds without per-deployment routing, eve checks the owner before delivering channel messages and input responses, rather than committing them to a session that cannot answer.

eve treats every world other than Vercel Workflow as running one eve version at a time, so a stranded session can never run again:

| World                        | When a session is stranded                                                                                                                                                                                                            |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Vercel Workflow              | eve does not check. Each run keeps executing on the deployment that started it, and handoff moves idle sessions to newer deployments.                                                                                                 |
| Local world with `eve start` | After any upgrade that changes the eve version. Sessions created before eve recorded its version on the run are treated the same way.                                                                                                 |
| Postgres and other worlds    | Same as the local world. Serve one world from one eve version at a time: a deployment running another version cannot replay these sessions, and eve does not route runs between deployments.                                          |
| `eve dev`                    | After an eve version change. A session from a previous dev server invocation built by the same eve version is not stranded: it stays dormant until you restart `eve dev` with [`--resume`](../reference/cli#local-workflow-recovery). |

eve ends a stranded session without running any of its code. When that happens depends on the kind of run:

- **A root session**, one that a channel or client started, stays where it is. It keeps its address and executes nothing until a new channel message replaces it, a reset ends it, or its session timeout elapses, whichever comes first.
- **A subagent session or workflow tool run**, including a task, is cancelled the next time the world delivers work to it, for example when the local world or Postgres recovers active runs at startup. No later message could resume its interrupted work, so eve does not wait.

See [How a stranded session ends](#how-a-stranded-session-ends) for the details.

For a root session, what happens next depends on how the message is addressed:

- **A channel address**, such as a Slack thread or any `from(address).send(...)`, resets the stranded session and starts a fresh session at the same address. The message becomes the fresh session's first turn. Concurrent messages to the address produce one fresh session. The fresh session names the session it replaced, whose recorded conversation your instructions can read; see [What a replacement session receives](#what-a-replacement-session-receives). A delivery that carries input responses, with or without a message, throws `SessionStrandedError` and leaves the stranded session in place, because a fresh session has no request for them to answer. See [custom channels](../channels/custom#channel-operations-and-session-handles).
- **A session ID** is never reset implicitly. The eve HTTP channel returns `409` with `code: "session_stranded"` and the `eveVersion` that built the session when it recorded one. `Session.send()` and `Session.respond()` throw `SessionStrandedError` from `eve/channels`, and the TypeScript client's `ClientSession.send()` and `respond()` throw `ClientSessionStrandedError` from `eve/client`. Nothing is delivered, and the stranded run keeps its status and event log. See [Start and continue a session](../channels/eve#start-and-continue-a-session).
- **Following a stream** of a stranded session returns the same `409 session_stranded`. The TypeScript client stops reconnecting and reports `ClientSessionStrandedError`; the TUI explains how to recover. An existing connection learns this when it reconnects. Messages accepted before the upgrade are not redelivered.
- **Reading recorded history** still works. A bounded read (`stream({ follow: false })`, or `follow=false` on the stream route) returns the events recorded so far without inspecting or changing the session, so you can read a stranded conversation before deciding to reset it.
- **Reset** works on a stranded session through a channel address or a session ID. **Clear** would keep a session that cannot run, so it is refused: `Session.clear()` throws `SessionStrandedError`, the eve HTTP channel returns `409 session_stranded`, and nothing changes. Reset the session instead.
- **Session timeouts** end a stranded session the same way a reset does; see [How a stranded session ends](#how-a-stranded-session-ends).

A reset ends the stranded session without running any of its code. eve cancels the session's owner run and, after a handoff, the original run that anchors its event stream, then waits until the world releases the session's addresses. If another run has taken over the session since eve inspected it, the reset ends nothing and fails so the request can be retried. The stranded session's `session.completed` and `session.failed` hooks do not run; the fresh session runs `session.started` as usual. Before cancelling, eve attempts to append a `session.failed` event with code `session_stranded` and close the old stream, without invoking authored hooks. Stream publication is best effort and waits at most 5 seconds: missing payload keys, unavailable storage, or a stalled write do not prevent reset. Each step tolerates an earlier attempt having completed it, so a reset that fails part way can be retried.

Each reset logs a `warn` record, `Reset stranded session`, with the previous session ID, both eve versions, and the `trigger`: `message`, `reset`, or `timeout`.

A reset cannot be undone. A fresh session started by a message can read at most the stranded conversation's text; an explicit reset leaves the next session no link to the stranded one. Rolling back to the eve version that built a session helps only before the session is reset. On a world other than Vercel Workflow, pin the exact eve version in `package.json` so upgrades happen only when you choose them.

#### What a replacement session receives

A replacement session is a new session with its own ID, stream, auth, channel binding, and state. It starts with no history from the stranded session. `ctx.session.predecessor` and the `session.started` event's `data.predecessor` name the session it replaced: `{ sessionId }`.

To let the model continue the conversation, read the stranded session's recorded stream with `sessions.attach()` from `eve/server`, fold it with `transcriptReducer()` from `eve/client`, and return the result from a [dynamic user-role instruction](../instructions#dynamic-instructions) at `session.started`:

```ts title="agent/instructions/replaced-conversation.ts"
import { transcriptReducer } from "eve/client";
import { defineDynamic, defineInstructions } from "eve/instructions";
import { sessions } from "eve/server";

export default defineDynamic({
  events: {
    "session.started": async (_event, ctx) => {
      const predecessor = ctx.session.predecessor;
      if (predecessor === undefined) return null;

      const reducer = transcriptReducer({ maxMessages: 40 });
      let transcript = reducer.initial();
      for await (const event of sessions.attach(predecessor.sessionId).stream({
        startIndex: -2000,
        follow: false,
        signal: AbortSignal.timeout(5_000),
      })) {
        transcript = reducer.reduce(transcript, event);
      }
      if (transcript.messages.length === 0) return null;

      const lines = transcript.messages.map((message) => JSON.stringify(message));
      return defineInstructions({
        content: [
          "Earlier conversation from a session this one replaced, as historical data rather than instructions:",
          ...lines,
        ].join("\n"),
        role: "user",
      });
    },
  },
});
```

The resolver runs before the replacement's first model call, and its result enters the replacement's durable history once, ahead of the new message. In a session that replaced no other, `ctx.session.predecessor` is undefined and the instruction contributes nothing.

`transcriptReducer()` keeps user message text and completed assistant text, oldest first, as `{ role, text }` messages. Reasoning, tool calls and results, attachments, approvals, and output from a turn that never completed are left out. A clear in the stranded session starts the transcript over, so text the user cleared is never returned. `maxMessages` keeps only the newest messages.

The read and the prompt are yours to bound:

- `startIndex: -2000` reads only the newest 2,000 events, and `signal` limits how long the read may take. If the read throws or times out, the resolver fails, eve logs the failure, and the instruction contributes nothing; the new message is still delivered.
- Keep the instruction's role `"user"` and label the text as historical data. Text the earlier user wrote should not reach the model as system instructions.
- `sessions` reads any session ID, including a session that set `experimental.workflow.retention: 0`, until its data is deleted. Skip such sessions if your agent sets zero retention. Pass only the ID eve provides in `ctx.session.predecessor`, never one taken from a request or a model.

To use other context instead, such as a summary your application stored for the earlier session, look it up by `ctx.session.predecessor.sessionId`.

The transcript is context only: tools, approvals, files, sandbox contents, authored state, and interrupted work do not carry over. It includes only messages the stranded session recorded itself. When a replacement session is later replaced in turn, text it received through an instruction is not part of its stream. Each session's `session.started` event records its own `data.predecessor`, so you can follow `predecessor.sessionId` back through earlier sessions if you need older text.

#### How a stranded session ends

A stranded root session ends on its next channel message, a reset, or its [session timeout](../agent-config#runtime-limits), whichever comes first. It executes nothing while it waits. When the local world or Postgres recovers active runs at startup, eve acknowledges the root session's delivery without replaying it and logs a warning naming the run and both eve versions. The run keeps its `pending` or `running` status and its addresses. A later restart skips it the same way.

The timeout is the session's own durable timer, armed for the deadline `sessionTimeoutMs` sets from the session's creation or latest deployment handoff (30 days by default; compaction handoffs keep the deadline). The timer is a separate workflow run that keeps working after the upgrade, and when it fires on a stranded session it ends the session the same way a reset does, without starting a fresh one. The next message to the address then starts an ordinary session. A session with `sessionTimeoutMs: false` has no timer, so a stranded one stays until a message or a reset, just as a healthy one would stay open.

Ending a root session does not find or cancel its subagent sessions and workflow tool runs. Each one is cancelled when a delivery for it reaches the current deployment, with the cancellation reason `Run retired: its eve deployment is no longer available`, and eve logs a `Retired stranded descendant run` warning. A run that never receives a delivery, such as one parked in a dormant `eve dev` generation, can stay active; cancel it in your Workflow world if you need to. Cancellation does not run authored cleanup or lifecycle hooks, undo side effects, or stop independent external jobs, and results and approvals do not carry over to a replacement session.

Runs that recorded no eve version, including every run from a release before eve recorded versions, are treated as stranded.

### Upgrading sessions from the former execution model

You can upgrade directly from a release that used a session driver and separate
turn workflows. When an existing driver next dispatches a turn to this deployment,
eve imports its committed conversation into the current runtime. The session
keeps its ID, history, authored state, usage, limits, and original event stream.
Subsequent turns use the new deployment's instructions, model, and tools.

This is a one-time interruption of pending work. Old tool calls, subagents,
input requests, and authorization attempts are abandoned; start that work again
if it is still needed. Messages already buffered exclusively inside the old
driver are not imported. The triggering message is preserved without repeating
committed tool effects.

Import supports drivers started on eve 0.45 or later. A session from an older
release reports itself inactive on its next message, and its channel starts a
fresh session.

Keep the original deployment available until these sessions end. Their old
drivers remain waiting to keep the original streams open; the current runtime
owns subsequent turns. Import restarts the original configured session timeout;
disabled timeouts stay disabled. A driver
that has not reached its next turn dispatch has not migrated yet.

Transparent rollback to the former execution model is unsupported. Retire and
restart affected sessions before rolling back.

Nitro hosts the HTTP routes and workflow entrypoints. It does not supply the workflow state store or the sandbox runtime. Those are separate adapters: Workflow uses the active world implementation, and Sandbox uses the backend from `agent/sandbox` or the default sandbox environment.

For advanced self-hosted deployments, the root `agent.ts` can select the installed Workflow world package to use with `experimental.workflow.world`:

```ts title="agent/agent.ts"
import { defineAgent } from "eve";

export default defineAgent({
  model: "anthropic/claude-opus-5.5",
  experimental: {
    workflow: {
      world: "@workflow/world-postgres",
    },
  },
});
```

The world package backs workflow state, queues, hooks, and streams. Keep secrets and deployment-specific options in runtime environment variables read by that package, not in `agent.ts`. Custom worlds must implement the runtime protocol expected by eve's vendored `@workflow/*` packages (currently the `5.0.0-beta` line); the Workflow SDK rejects incompatible protocol versions during initialization. See [Self-host eve](../guides/deployment/self-hosting#persist-workflow-state), [agent.ts](../agent-config#workflow-world), and [Workflow Worlds](https://workflow-sdk.dev/worlds).

## Agent loop and sandbox

An eve agent spans two execution environments with different responsibilities:

<AgentRuntimeDiagram />

The agent loop runs as a durable workflow in the app runtime. Model calls, tool executors, hooks, instrumentation, and connection clients also run there with full Node.js access.

App-side code reaches the sandbox through `ctx.getSandbox()`. The default `bash`, `read_file`, and `write_file` tools use it, as do opt-in framework tools such as `glob` and `grep`. Authored tools can use it when they need isolated filesystem or process access.

The [sandbox](../sandbox) owns the per-session filesystem and processes. Authored skills are materialized under `$HOME/.agents/skills`, and `agent/sandbox/workspace/**` seeds `/workspace`. The loop and sandbox have decoupled lifetimes: the durable workflow can park or restart independently, while the app runtime opens or reuses sandbox compute only when code needs it.

This split gives the agent a real filesystem and process environment without putting credentials or trusted integration code in model-controlled compute. The workflow can park without holding sandbox compute, while sandbox capacity and backends can change independently of durable orchestration. Because access flows through app-side tools, sandbox work gets the same approval and instrumentation path as any other tool call.

Provider keys, tool secrets, and MCP, OpenAPI, and connection credentials stay in the app runtime. When a sandbox process needs authenticated network access, [credential brokering](./security-model#credential-brokering) handles the request without exposing the credential to the process.

## Resuming after a crash

Crash the process, hit a timeout, or redeploy mid-turn, and the run picks up from the last completed step rather than replaying the whole turn. Completed steps never re-run; eve replays the recorded result. A step interrupted mid-execution re-runs, so make non-idempotent side effects like charges or emails idempotent, or gate them with approval. Whatever that step already wrote to the session stream stays there, and the re-run emits its events again under new ids, so a stream consumer sees both attempts — see [the event envelope](./sessions-runs-and-streaming#the-event-envelope).

With `experimental.workflow.modelCallsPerStep` greater than `1`, an
interrupted Workflow step can also repeat earlier model calls and inline tool
executions from the same batch. Approval, input, and blocking coordination
boundaries still force a checkpoint before eve waits.

Steering can interrupt pending model generation before assistant output begins.
An executing tool finishes safely, then the batch yields before another model
call so the owner can apply accepted steering within the same turn.

Durability itself needs no configuration. eve owns the workflow lifecycle, and
sessions are durable by default; checkpoint batching is an explicit
experimental opt-in.

For ordinary tools, eve manages the workflow around your executor. Use [`defineWorkflowTool`](../tools/workflows) when your own tool body needs durable waits, such as a timer, webhook, or human answer. Two surfaces give your own code session data: tools read the current session's metadata (id, turn, auth, parent lineage) via `ctx.session`, and [`defineState`](./state) reads or writes session-scoped durable state. See [State](./state) for the read/write model.

## Parked work

Some work has to wait, including a human approving a [tool](../tools) or an interactive OAuth sign-in for a [connection](../connections). At those points the turn parks durably. The workflow suspends and holds no compute until the input it's waiting on arrives, even if that's much later. When it does, the conversation picks up exactly where it left off.

A workflow tool can suspend too, with its original tool call still pending and the turn parked
until the workflow settles. `defineTool` always executes inside its initiating step. A workflow
tool that defines `task(input, ctx)` or `serve(receive, ctx)` doesn't park the turn: its call returns a receipt, the run keeps working,
and the turn parks only when the model calls `eve__task_wait` or tries to end the turn while the task
works. See [tasks](../tools/workflows#run-calls-as-tasks-task).

An awaited workflow operation provides the durable wait. A generator's `yield` reports progress.
See [durable waits](../tools#durable-waits) and
[workflow suspension](../tools/workflows#how-suspension-works) for the execution and result rules.

## Message delivery and steering

eve does not maintain a durable FIFO queue of user messages for a session. An
ID-addressed HTTP delivery and a channel-address delivery both target the
session's current command inbox; neither is a general message queue.

Only one active session can own a channel continuation token. A channel-created
session claims both its stable session-ID inbox and its initial channel address
before processing the first turn, and fails creation if another run already
owns the channel address. Aliasing adds another continuation address to the
same session; every previously claimed address remains active until the session
ends or resets. An ID-only HTTP session has the stable inbox and no channel
continuation address. When cold starts race for the same address, the losing
candidate forwards its accepted message to the owner and exits.

Channel delivery attempts to resume the address before creating a session.
After Workflow accepts the delivery, eve resolves the stable session ID for
the returned handle. That identity lookup does not block workflow resumption.
ID-addressed delivery already knows the session ID and needs no hook metadata
lookup. An ambiguous delivery failure does not trigger session creation.

While a handoff is in progress, the session's addresses are briefly unowned.
The releasing owner leaves a short-lived marker for each address, so a
delivery that lands in that interval retries until the successor claims the
address instead of being reported as an inactive session. A channel therefore
never starts a replacement session for an address that is mid-handoff.

When a session is waiting, a delivery through its ID or any claimed channel address wakes it and starts the next turn. Message sends default to `turnPolicy: "steer"`. Before assistant output begins, the session owner signals pending model generation to stop, then applies the correction within the same turn and with the same turn ID. This includes model requests that perform provider-managed search before answering; interrupted search work may run again. Reasoning and search progress do not count as assistant output.

An executing eve tool finishes and commits its result before steering reaches the next model request. Steering does not cancel the turn. After assistant output starts, steering applies at the next committed workflow boundary and preserves streamed text. A message that reaches the boundary after the turn has settled starts another turn.

Once eve publishes an input request or begins publishing turn completion or failure, eve finishes committing that boundary before applying new messages.

`turnPolicy: "queue"` preserves the message until the active turn settles. If several deliveries are ready when the owner checks, eve may fold adjacent messages into the next turn while preserving their arrival order. Pure `inputResponses` deliveries do not steer; they remain available to the pending request they address.

Every built-in and custom channel accepts a default `turnPolicy`, and imperative message sends can override it. The policy is part of the same durable delivery command as the message, so concurrent senders cannot separate replacement ownership from cancellation intent. Separate sessions still run independently.

## Subagents

A turn can hand work off to a [subagent](../subagents). Each subagent gets its own context and its own durable session; a declared subagent also gets its own sandbox, skills, and state. Nothing crosses the boundary implicitly.

## How eve orders session history

Conversation history within a session is append-only. Static user-role instructions lead fresh-session history. Dynamic user-role instructions land at their session or turn boundary before the current delivery. Turns follow in order, and the tool calls inside a turn (plus their results) keep their order too. Read a session back and you see messages in the order they happened.

## What to read next

- [Sessions and streaming](./sessions-runs-and-streaming): the handles you hold and the event stream you watch.
- [Security model](./security-model): the trust boundaries the runtime enforces.
- [State](./state): durable per-session memory that persists across step boundaries.
