# Sessions and branching (/guides/sessions-and-branching)



A [session](/concepts/sessions) is a causal state container, not a cache entry. It holds an
append-only event line per [branch](/concepts/branches), and every append uses compare-and-swap so two
writers can never silently clobber each other. Branches let you explore alternative paths from any
point in history without copying state. Merges are always explicit - Zumik never auto-merges and never
silently reorders semantically meaningful events.

## Create a session [#create-a-session]

`POST /v2/sessions`. `base_bundle_ids` is an optional ordered list of [bundles](/concepts/bundles) that
form the stable prefix every turn shares. Each id must reference a bundle in your project. A default
branch is created for you.

```bash
curl https://api.zumik.ai/v2/sessions \
  -H "Authorization: Bearer zk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"base_bundle_ids": ["bun_agent_prefix"]}'
```

```json title="Response"
{
  "id": "ses_01jy…",
  "object": "session",
  "project_id": "prj_01jy…",
  "default_branch_id": "br_01jy…",
  "status": "active",
  "base_bundle_ids": ["bun_agent_prefix"],
  "created_at": "2026-06-15T12:00:00Z"
}
```

`status` is `active`, `archived`, or `tombstoned`. Retrieve with `GET /v2/sessions/{id}`; delete with
`DELETE /v2/sessions/{id}` (returns `{ "object": "session.deleted", "deleted": true }`).

## Append events with optimistic concurrency [#append-events-with-optimistic-concurrency]

`POST /v2/sessions/{session_id}/branches/{branch_id}/events`. Every append must carry the version and
head it expects to extend - this is the compare-and-swap that makes concurrent writers safe.

| Field                    | Type           | Notes                                                                                            |
| ------------------------ | -------------- | ------------------------------------------------------------------------------------------------ |
| `expected_version`       | integer        | The branch version you believe is current. Must match exactly.                                   |
| `expected_head_event_id` | string \| null | The current head event id (null if the branch is empty). Must match.                             |
| `event.event_type`       | enum           | `user_message`, `assistant_message`, `tool_result`, `retrieval_result`, `checkpoint`, or `note`. |
| `event.payload_ref`      | string \| null | An [artifact](/concepts/artifacts) id holding the event payload.                                 |

```bash
curl https://api.zumik.ai/v2/sessions/ses_01jy…/branches/br_01jy…/events \
  -H "Authorization: Bearer zk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "expected_version": 17,
    "expected_head_event_id": "evt_017",
    "event": { "event_type": "user_message", "payload_ref": "art_01jy…" }
  }'
```

```json title="Response"
{
  "id": "evt_018",
  "object": "session_event",
  "session_id": "ses_01jy…",
  "branch_id": "br_01jy…",
  "sequence": 18,
  "event_type": "user_message",
  "parent_event_id": "evt_017",
  "payload_ref": "art_01jy…",
  "created_at": "2026-06-15T12:00:01Z"
}
```

On success the branch `version` increments by one and `head_event_id` advances to the new event. The
new event's `sequence` equals `expected_version + 1` and its `parent_event_id` is the prior head.

## Handle the 409 branch\_version\_conflict [#handle-the-409-branch_version_conflict]

If the branch moved since you read it - because another writer appended first - the version or head you
sent no longer matches, and the append is rejected with `409`:

```json
{
  "error": {
    "message": "Branch 'br_01jy…' is at version 18 with head evt_018, not the expected version/head.",
    "type": "invalid_request_error",
    "code": "branch_version_conflict"
  }
}
```

The fix is not to retry blindly - the head you wanted to extend is gone. Re-read the branch with
`GET /v2/sessions/{session_id}/branches/{branch_id}`, decide whether the new head is compatible with
what you intended, and either:

<Steps>
  <Step title="Re-base your append">
    If the concurrent event is compatible, re-issue with the new `expected_version` and
    `expected_head_event_id` so your event extends the updated head.
  </Step>

  <Step title="Fork instead">
    If the concurrent event conflicts with your intent, [fork a branch](#fork-a-branch) from the event
    you meant to extend and append there. Both paths now coexist without either clobbering the other.
  </Step>
</Steps>

<Warning>
  Never resolve a `branch_version_conflict` by dropping `expected_head_event_id` to force the write.
  The conflict is the system protecting causal order; bypassing it corrupts the event line.
</Warning>

## Fork a branch [#fork-a-branch]

`POST /v2/sessions/{session_id}/branches`. Fork from a branch, optionally pinning the exact event to
fork at. Omit `fork_from_event_id` to fork at the parent's current head.

```bash
curl https://api.zumik.ai/v2/sessions/ses_01jy…/branches \
  -H "Authorization: Bearer zk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "fork_from_branch_id": "br_01jy…",
    "fork_from_event_id": "evt_017"
  }'
```

```json title="Response"
{
  "id": "br_02jz…",
  "object": "session_branch",
  "session_id": "ses_01jy…",
  "parent_branch_id": "br_01jy…",
  "forked_from_event_id": "evt_017",
  "head_event_id": "evt_017",
  "version": 17
}
```

The new branch inherits the parent's version at the fork point and starts at the forked event as its
head. From here you append independently - an `alternative-debug-path` that does not disturb the
original line.

## Merge explicitly [#merge-explicitly]

There are no automatic branch merges. Combining branches is always a deliberate act you perform with
the same append and fork primitives, choosing one of four strategies:

| Strategy                          | How                                                                                                                                        |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Append a checkpoint               | Write a human-selected `checkpoint` event (`event_type: "checkpoint"`) whose `payload_ref` is a chosen artifact, marking the agreed state. |
| Compact to a summary              | Summarize a branch into a single summary artifact and append it as a `checkpoint`, collapsing a long line into one reusable block.         |
| Rebase onto an event              | Create a new branch forked at a selected event, then re-append the events you want to carry forward.                                       |
| New branch with chosen references | Fork a fresh branch and append events whose `payload_ref`s point at the artifacts you want, hand-picking what comes across.                |

Because every step is an ordinary append under compare-and-swap, a merge can never silently lose or
reorder events - the result is a new, auditable line you constructed on purpose.

<CardGroup cols="2">
  <Card title="Pin state with a snapshot" icon="lock" href="/guides/snapshots-and-reproducibility">
    Capture a branch head and ordering so a response can be replayed against stable state.
  </Card>

  <Card title="Order the prompt for reuse" icon="layer-group" href="/guides/prompt-layout">
    The bundle is the stable prefix, the branch history is the ordered middle, the latest turn is last.
  </Card>
</CardGroup>
