> Source: Onevium official documentation
> Article: Conversations: continuation, history, events, cancel, and reset
> Original URL: https://onevium.com/docs/developers/conversations
> Language: English
> Updated: 2026-09-09
> Applies to: 1.1.23+

---

# Conversations: continuation, history, events, cancel, and reset

Create and continue conversations through the Gateway, read history and SSE, and manage cancellation/reset with generations, idempotency, and operation records.

## Conversations, runs, and control operations

Publish a service and enable access using the [external-access overview](https://onevium.com/docs/developers/external-access), then call these Gateway APIs from your backend.

| Object            | Purpose                                                                  |
| ----------------- | ------------------------------------------------------------------------ |
| `conversation_id` | A durable business conversation reused for subsequent messages           |
| `generation`      | The current context generation, starting at 1 and increasing after reset |
| `run_id`          | One message's task, used for results, events, or cancellation            |
| `operation_id`    | A cancel/reset control operation whose completion must be queried        |

Creating a conversation does not stop work in an older one. Reset keeps the conversation ID and business association while advancing its context. It does not undo file changes or external actions already performed.

## Prepare the example environment

These commands use **Bash, curl with `--fail-with-body`, and jq**. Sample responses illustrate the response format; they are not real task results. Replace their IDs with those returned by your requests.

Inject `APP_KEY` from your credential store into the backend or test shell, without printing it. Fill the other variables from Onevium. Keep the operation identifiers for this exercise; change `FLOW_ID` only for an intentionally new exercise, not a network retry.

**Set `GATEWAY_URL` to the gateway origin only, without a trailing `/api/v1`.** For example, if the UI displays `http://127.0.0.1:48541/api/v1`, use `http://127.0.0.1:48541`; the commands below append `/api/v1` themselves.

```bash
export GATEWAY_URL='http://127.0.0.1:REPLACE_WITH_API_PORT'
export ENDPOINT_ID='YOUR_PUBLISHED_ENDPOINT_ID'
export SUBJECT='docs-test-user'
: "${APP_KEY:?Load APP_KEY from your credential store first}"
FLOW_ID='docs-demo-001'
BUSINESS_KEY="case-$FLOW_ID"
CREATE_KEY="$FLOW_ID-create"
```

The full walkthrough needs `conversations:read`, `conversations:write`, `runs:read`, and `runs:write`. See [authentication and scopes](https://onevium.com/docs/developers/authentication). Use the displayed **API address** locally or your configured HTTPS Gateway remotely.

## 1. Create a business conversation

`POST /api/v1/conversations` returns **201** on success.

| Field/header       | Requirement                                                                                                                  |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `endpoint_id`      | Required published service ID belonging to this application                                                                  |
| `conversation_key` | Optional business association, 1–256 characters without surrounding whitespace, such as a ticket ID; omit for no association |
| `Idempotency-Key`  | Required, 1–128 visible ASCII characters without spaces; identifies this creation operation                                  |

The business key is not an idempotency key. Creating the same business key again under the same app, subject, and service with another creation identifier returns `409 BUSY`. Store and continue the original conversation instead.

```bash
CREATE_BODY=$(jq -nc --arg endpoint "$ENDPOINT_ID" --arg key "$BUSINESS_KEY" \
  '{endpoint_id:$endpoint,conversation_key:$key}')
CREATE_RESPONSE=$(curl --fail-with-body -sS "$GATEWAY_URL/api/v1/conversations" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Idempotency-Key: $CREATE_KEY" \
  -H 'Content-Type: application/json' --data "$CREATE_BODY")
printf '%s\n' "$CREATE_RESPONSE" | jq .
CONVERSATION_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -er '.conversation_id')
GENERATION=$(printf '%s' "$CREATE_RESPONSE" | jq -er '.generation')
```

Continue only after a successful request supplies both values. **Example response:**

```json
{
  "conversation_id": "conv_0123456789abcdef_0000000000000001",
  "owner_id": "example-owner",
  "app_id": "11111111111111111111111111111111",
  "subject": "docs-test-user",
  "endpoint_id": "22222222222222222222222222222222",
  "binding_version": 1,
  "generation": 1,
  "business_key": "case-docs-demo-001",
  "state": "active",
  "created_at": "2026-09-09T08:00:00.000Z"
}
```

The request's `conversation_key` is named `business_key` in the record. `binding_version` identifies the published service version used by this conversation; it is separate from `generation`.

## 2. Understand message fields

`POST /api/v1/conversations/{conversation_id}/messages` accepts these fields. Unknown fields are rejected.

| Field                 | Requirement and meaning                                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `expected_generation` | Required positive integer matching the current context; do not send the string `"1"`                                                                   |
| `input`               | Required object with at least one nonempty input below                                                                                                 |
| `input.text`          | Optional text, at most 64 KiB UTF-8; omission is treated as empty text                                                                                 |
| `input.data`          | Optional plain JSON object containing structured business material                                                                                     |
| `input.resource_ids`  | Optional, at most five unique ready attachment IDs owned by this app and subject; the service must support their types                                 |
| `start_before`        | Optional RFC 3339 timestamp with timezone; must be later than now and at most five minutes after admission for this endpoint; defaults to five minutes |

`start_before` is the **latest start time**, not an execution timeout. Total request JSON is limited to 1 MiB. Do not add a model, working directory, tool permissions, or `busy_policy`; the published service defines model/capabilities, and busy policy belongs only to reset.

## 3. Send the first message

```bash
MESSAGE_1_KEY="$FLOW_ID-message-1"
MESSAGE_1_BODY=$(jq -nc --argjson generation "$GENERATION" \
  '{expected_generation:$generation,input:{text:"Remember the demo code Orion. Reply only: Received.",data:{case_id:"DEMO-001"}}}')
ADMISSION=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Idempotency-Key: $MESSAGE_1_KEY" \
  -H 'Content-Type: application/json' --data "$MESSAGE_1_BODY")
printf '%s\n' "$ADMISSION" | jq .
RUN_ID=$(printf '%s' "$ADMISSION" | jq -er '.run_id')
```

**Example 202 admission response:**

```json
{
  "conversation_id": "conv_0123456789abcdef_0000000000000001",
  "generation": 1,
  "message_id": "msg_0123456789abcdef_0000000000000003",
  "run_id": "run_0123456789abcdef_0000000000000002",
  "state": "queued",
  "event_cursor": 2
}
```

Store `run_id`; 202 is not a model reply. `event_cursor` is the conversation event sequence at admission. An idempotent replay can still return this original `queued` response; use GET for current state.

## 4. Query task state and output

```bash
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/runs/$RUN_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" | jq .
```

**Illustrative successful terminal response:**

```json
{
  "run_id": "run_0123456789abcdef_0000000000000002",
  "message_id": "msg_0123456789abcdef_0000000000000003",
  "conversation_id": "conv_0123456789abcdef_0000000000000001",
  "generation": 1,
  "state": "succeeded",
  "delivery_state": "device_received",
  "created_at": "2026-09-09T08:00:01.000Z",
  "updated_at": "2026-09-09T08:00:03.000Z",
  "result": { "text": "Received." }
}
```

| State                                 | Client interpretation                                             |
| ------------------------------------- | ----------------------------------------------------------------- |
| `queued`                              | Awaiting execution; inspect delivery state and the start deadline |
| `running`                             | Execution is active                                               |
| `waiting_approval`                    | Approval is needed; not complete                                  |
| `cancelling`                          | Stop requested but not yet confirmed                              |
| `reconciling`                         | Execution state is being reconciled; do not infer success         |
| `succeeded`                           | Successful terminal state; inspect `result`                       |
| `failed`                              | Failed terminal state; inspect `result.error` when present        |
| `cancelled`, `interrupted`, `expired` | Terminal, but not successful                                      |

`waiting_approval` requires the existing approval workflow; these public business routes do not include an endpoint for approving arbitrary tool actions.

`delivery_state` is either `waiting_device` or `device_received`, independently of run state. `result` can be `null`; otherwise it may contain `text`, `resource_ids`, or `error`. A failed run may retain partial output.

## 5. Continue the same conversation

Check the previous result, then read the snapshot for the actual generation:

```bash
SNAPSHOT=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT")
GENERATION=$(printf '%s' "$SNAPSHOT" | jq -er '.generation')
MESSAGE_2_KEY="$FLOW_ID-message-2"
MESSAGE_2_BODY=$(jq -nc --argjson generation "$GENERATION" \
  '{expected_generation:$generation,input:{text:"What was the earlier demo code?"}}')
ADMISSION_2=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Idempotency-Key: $MESSAGE_2_KEY" \
  -H 'Content-Type: application/json' --data "$MESSAGE_2_BODY")
printf '%s\n' "$ADMISSION_2" | jq .
RUN_ID=$(printf '%s' "$ADMISSION_2" | jq -er '.run_id')
```

This returns new message and run IDs. Store the second run ID and query it. The intended example answer is Orion; verify the actual model response.

**Messages can queue within a conversation.** A later message can be admitted as `queued` while an earlier run executes. Execution is serialized per conversation. The current per-conversation queued limit is 20; exceeding it returns 429. A resetting or otherwise non-admitting conversation returns `BUSY`. App and device pending-task limits also apply.

## 6. Read snapshots and paginated history

`GET /conversations/{id}` returns `conversation_id`, current `generation`, `watermark`, `messages`, and `runs`, rather than the creation endpoint's full conversation record. A snapshot takes up to 200 recent messages and 100 runs, with response-size budgets. Use message history for earlier records.

Each snapshot run contains `run_id`, `generation`, `state`, boolean `device_received`, `result`, and `result_available`. A result record may exist while the snapshot budget omits its body, giving `result_available: true` with `result: null`; query that run separately. `watermark` is for SSE resumption, not history pagination.

```bash
HISTORY=$(curl --fail-with-body -sS --get \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  --data-urlencode "generation=$GENERATION" --data-urlencode 'limit=50')
printf '%s\n' "$HISTORY" | jq .
```

| Query        | Meaning                                                                                                                    |
| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `generation` | Optional positive integer; omit to include retained messages across generations                                            |
| `limit`      | Defaults to 50; HTTP validation accepts 1–200, but the current storage layer returns at most 100 per page; use 100 or less |
| `before`     | Optional positive integer; use the previous page's minimum `cursor` for older records, not a message ID or SSE sequence    |

When the current page is nonempty, fetch the next older page:

```bash
BEFORE=$(printf '%s' "$HISTORY" | jq -er '.messages | map(.cursor) | min // empty')
curl --fail-with-body -sS --get \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  --data-urlencode "generation=$GENERATION" \
  --data-urlencode "before=$BEFORE" --data-urlencode 'limit=50' | jq .
```

**Example history containing one message:**

```json
{
  "messages": [
    {
      "cursor": 1,
      "message_id": "msg_0123456789abcdef_0000000000000003",
      "conversation_id": "conv_0123456789abcdef_0000000000000001",
      "generation": 1,
      "run_id": "run_0123456789abcdef_0000000000000002",
      "role": "user",
      "created_at": "2026-09-09T08:00:01.000Z",
      "content": { "text": "Remember the demo code Orion. Reply only: Received.", "data": { "case_id": "DEMO-001" } }
    }
  ]
}
```

Each page is returned oldest-to-newest; use its minimum cursor for the next older page and stop on an empty array. Snapshots may include unfinished assistant messages with `content.partial: true`; query the run for its final result. Events currently retain seven days, and terminal message/result bodies approximately 30 days. Keep durable business records outside the Gateway.

To list conversations, use `GET /api/v1/conversations?endpoint_id=SERVICE_ID&limit=50`, with a conversation ID as optional `before`. Here `endpoint_id` selects the device; the response contains that device's conversations for this app/subject, not necessarily only that service.

```bash
curl --fail-with-body -sS --get "$GATEWAY_URL/api/v1/conversations" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  --data-urlencode "endpoint_id=$ENDPOINT_ID" --data-urlencode 'limit=50' | jq .
```

## 7. Observe SSE and resume after disconnection

When initializing a business UI, load a snapshot first and subscribe from its watermark to avoid duplicating historical content:

```bash
SNAPSHOT=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT")
LAST_SEQ=$(printf '%s' "$SNAPSHOT" | jq -er '.watermark')
curl --fail-with-body -sS -N \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/events" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Last-Event-ID: $LAST_SEQ" \
  -H 'Accept: text/event-stream'
```

**Illustrative event, not a live capture:**

```text
id: 3
event: run.started
data: {"conversation_id":"conv_0123456789abcdef_0000000000000001","generation":1,"event_id":"evt_0123456789abcdef_0000000000000004","seq":3,"run_id":"run_0123456789abcdef_0000000000000002","message_id":null,"type":"run.started","payload":{"state":"running"}}
```

- `Last-Event-ID` is the **numeric seq**, not `event_id`. Alternatively use `?after=3`; the header wins when both are supplied.
- Save the last processed sequence and reconnect from it; only events with a greater sequence return. Persist processing before advancing your cursor.
- `GET /api/v1/runs/{run_id}/events` filters one run but retains conversation-wide sequences, so gaps are normal. Use the conversation stream for reset events.
- `assistant.delta` is incremental; `assistant.message` contains a complete message. Update by message ID instead of appending both as separate final answers.
- On `410 CURSOR_EXPIRED`, reload the snapshot and use its new watermark. `: ping` is a keepalive comment.

Close observation when your client has handled a terminal result; SSE does not promise to close automatically. Disconnecting does not cancel execution. Current concurrent limits per device/app/subject are 32/16/4, so reuse observation connections.

## 8. Stop one run

The endpoint is **`POST /api/v1/runs/{run_id}/cancel`**, with an empty JSON object. It is not `/stop` and does not accept `expected_generation`.

For the actual run you intend to stop:

```bash
CANCEL_KEY="$FLOW_ID-cancel-1"
CONTROL=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/runs/$RUN_ID/cancel" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Idempotency-Key: $CANCEL_KEY" \
  -H 'Content-Type: application/json' --data '{}')
printf '%s\n' "$CONTROL" | jq .
OPERATION_ID=$(printf '%s' "$CONTROL" | jq -er '.operation_id')
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/operations/$OPERATION_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" | jq .
```

**Example 202 control response:**

```json
{
  "operation_id": "op_0123456789abcdef_0000000000000005",
  "state": "stopping",
  "ref": { "conversation_id": "conv_0123456789abcdef_0000000000000001", "generation": 1 }
}
```

`pending`, `waiting_device`, and `stopping` are incomplete; `succeeded` confirms the control completed, while `failed` does not. Cancel/reset operations usually return `stopping` or `succeeded`; clients should still recognize the full protocol enum.

A queued run without execution can cancel immediately. A started run needs an actual terminal outcome. Cancelling an already finished run can produce a successful operation while preserving the original run's `succeeded` or other terminal state. Query the run afterwards; 202 or `cancelling` alone is not stop confirmation.

## 9. Reset the context

`POST /api/v1/conversations/{id}/reset` accepts:

| Field                 | Meaning                                                                                                                                                                          |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `expected_generation` | Required current generation                                                                                                                                                      |
| `busy_policy`         | Optional, defaults to `reject`; unfinished work yields `409 BUSY`. Explicit `cancel_and_reset` cancels this generation's unfinished work and waits for stopping before advancing |

Read the snapshot for the current generation first. With no unfinished work:

```bash
GENERATION=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" | jq -er '.generation')
RESET_KEY="$FLOW_ID-reset-idle-1"
RESET_BODY=$(jq -nc --argjson generation "$GENERATION" \
  '{expected_generation:$generation,busy_policy:"reject"}')
RESET_RESPONSE=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/reset" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Idempotency-Key: $RESET_KEY" \
  -H 'Content-Type: application/json' --data "$RESET_BODY")
printf '%s\n' "$RESET_RESPONSE" | jq .
OPERATION_ID=$(printf '%s' "$RESET_RESPONSE" | jq -er '.operation_id')
RESET_STATUS=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/operations/$OPERATION_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT")
printf '%s\n' "$RESET_STATUS" | jq .
```

If you intentionally want to cancel unfinished work and reset, use a different control operation and key:

```bash
RESET_KEY="$FLOW_ID-reset-busy-1"
RESET_BODY=$(jq -nc --argjson generation "$GENERATION" \
  '{expected_generation:$generation,busy_policy:"cancel_and_reset"}')
RESET_RESPONSE=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/reset" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" \
  -H "Idempotency-Key: $RESET_KEY" \
  -H 'Content-Type: application/json' --data "$RESET_BODY")
printf '%s\n' "$RESET_RESPONSE" | jq .
OPERATION_ID=$(printf '%s' "$RESET_RESPONSE" | jq -er '.operation_id')
```

Choose one path rather than blindly executing both. Query its returned operation ID until `succeeded`. **Example completed response:**

```json
{
  "operation_id": "op_0123456789abcdef_0000000000000006",
  "state": "succeeded",
  "ref": { "conversation_id": "conv_0123456789abcdef_0000000000000001", "generation": 2 }
}
```

Use `ref.generation` or a fresh snapshot for the next message; the old generation returns 409. Reset also adopts the service's current published binding version. After a service publication causes a version conflict, decide whether to reset. Old messages are not immediately deleted: query retained records with `generation=1`.

Update the local generation only after the operation succeeds:

```bash
RESET_STATUS=$(curl --fail-with-body -sS \
  "$GATEWAY_URL/api/v1/operations/$OPERATION_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT")
GENERATION=$(printf '%s' "$RESET_STATUS" | jq -er \
  'select(.state == "succeeded") | .ref.generation')
```

## Idempotency, retries, and concurrency

Use a new key for each intended creation, message, cancel, or reset. When a network failure leaves the response uncertain, retain the original **app, subject, service, target, request key, and body**, replay the operation, then query current resource state.

- Same key, changed content: `409 IDEMPOTENCY_CONFLICT`; changing keys repeatedly can hide duplicate execution.
- Generation conflict: load a snapshot and review whether the pending content is still appropriate; a newly intended execution uses a new key.
- Duplicate business key at creation: recover the stored conversation ID instead of treating it as a network retry.
- Missing cancel/reset response: replay with the original key to recover the operation, then keep querying it.
- 429: honor `Retry-After`; queue or storage conditions must also recover.

## How direct run creation differs

The overview's `POST /api/v1/agent-endpoints/{endpoint_id}/runs` is a task-oriented entry. It accepts `input`, optional `conversation_key`, optional `expected_generation`, and optional `start_before`.

Without a business key, each new operation creates a conversation. With a key, it finds or creates the associated conversation. Supplying `expected_generation` requires a `conversation_key` and an already-existing association. Its latest-start window is **24 hours**, compared with five minutes for conversation messages. Prefer the explicit conversation endpoints when an interactive client needs generation control.

## Acceptance checks and errors

Check creation, a second turn, history pagination, SSE reconnect, cancellation before/during execution, both reset policies, stale-generation rejection, repeated keys, and cross-subject 404 in your own environment.

| Error                                   | Next action                                                                            |
| --------------------------------------- | -------------------------------------------------------------------------------------- |
| 400 `UNKNOWN_FIELD` / `INVALID_REQUEST` | Use the exact message, cancel, or reset request shape                                  |
| 401 / 403                               | Check key, scope, and app/device status                                                |
| 404                                     | Check actual ID, app, and subject; knowing an ID does not grant access                 |
| 409 `GENERATION_CONFLICT`               | Reload the snapshot and check context or publication changes                           |
| 409 `BUSY`                              | Distinguish duplicate business keys, resetting state, and unfinished work during reset |
| 410 `CURSOR_EXPIRED`                    | Reload the snapshot and update the SSE cursor                                          |
| 503 `CAPABILITY_UNAVAILABLE`            | Check paused admission, Gateway availability, and device availability                  |

## Next steps

Configure [application keys and scopes](https://onevium.com/docs/developers/authentication), learn [Webhooks and callbacks](https://onevium.com/docs/developers/webhooks), or deploy a [remote gateway](https://onevium.com/docs/developers/remote-gateway).
