Zumik
Guides

Sessions and branching

Create a session, append events with optimistic concurrency, handle the 409 branch_version_conflict, fork branches, and merge explicitly with the four supported strategies.

A session is a causal state container, not a cache entry. It holds an append-only event line per branch, 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

POST /v2/sessions. base_bundle_ids is an optional ordered list of 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.

curl https://api.zumik.ai/v2/sessions \
  -H "Authorization: Bearer zk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"base_bundle_ids": ["bun_agent_prefix"]}'
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

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.

FieldTypeNotes
expected_versionintegerThe branch version you believe is current. Must match exactly.
expected_head_event_idstring | nullThe current head event id (null if the branch is empty). Must match.
event.event_typeenumuser_message, assistant_message, tool_result, retrieval_result, checkpoint, or note.
event.payload_refstring | nullAn artifact id holding the event payload.
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…" }
  }'
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

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:

{
  "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:

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.

Fork instead

If the concurrent event conflicts with your intent, fork a branch from the event you meant to extend and append there. Both paths now coexist without either clobbering the other.

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.

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.

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"
  }'
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

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:

StrategyHow
Append a checkpointWrite a human-selected checkpoint event (event_type: "checkpoint") whose payload_ref is a chosen artifact, marking the agreed state.
Compact to a summarySummarize a branch into a single summary artifact and append it as a checkpoint, collapsing a long line into one reusable block.
Rebase onto an eventCreate a new branch forked at a selected event, then re-append the events you want to carry forward.
New branch with chosen referencesFork a fresh branch and append events whose payload_refs 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.

On this page