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"]}'{
"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.
| 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 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…" }
}'{
"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"
}'{
"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:
| 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_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.
Prompt layout
The recommended block ordering that preserves a reusable prefix, the reuse-killers that quietly defeat provider caching, and the zumik lint CLI and web prompt-linter that catch them.
Snapshots and reproducibility
Pin a snapshot to freeze a branch head, ordering, and prompt-compiler revision, understand exactly what a snapshot fixes, and replay recorded traffic against a pinned snapshot and alias release.