# Sessions

> The Sessions operations of the Kabu API: a running unit of agent work; drives Live Activities.

A running unit of agent work; drives Live Activities.

Generated from the contract, version `2.0.0-draft.2` ([OpenAPI](/openapi.yaml)). Base URL `https://usekabu.com/api`. Errors are problem details, each [code](/docs/errors) with its own page.

## `POST /v2/sessions` · Get or create a session (agent), or kick one off (human)

Authorization depends on intent. Without `steer`, it creates or finds a running session and needs sessions:write. With `steer`, it is a human kickoff and needs a user token, or steer:write plus an `as_recipient` in the target audience; sessions:write is not required, and a key holding messages:write is refused (403 self_response_forbidden).

- **Auth:** an API key, or a signed-in human's token (the Kabu apps).
- **Scopes:** `sessions:write`, or `steer:write`.
- **Signed-in humans:** `audience`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string, at most 255 characters | no | Retry-safe key. For messages, sessions and responses it binds to the created object for its life; elsewhere for 24 h. |
| `Kabu-Org` | header | string | no | Select a live membership (default personal org); non-member is 404. Human tokens are live-only. Caller user id is distinct from billing owner. |

**Request body**: [SessionCreate](#schema-SessionCreate).

| Status | Means | Body |
|---|---|---|
| `201` | Created, or the existing open session for (project, agent, external_id). | [Session](#schema-Session) |
| `429` | A rate, concurrency or quota limit was reached | [Problem](/docs/api/discovery#schema-Problem) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X POST "https://usekabu.com/api/v2/sessions" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"title":"…"}'
```

## `GET /v2/sessions` · List sessions

With a user token, only sessions whose audience includes the caller.

- **Auth:** an API key, or a signed-in human's token (the Kabu apps).
- **Scopes:** `sessions:read`.
- **Signed-in humans:** `audience`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | integer, from 1 to 100 | no |  |
| `after` | query | string | no | Opaque cursor from next_cursor. |
| `project` | query | string | no |  |
| `agent` | query | string | no |  |
| `external_id` | query | string | no |  |
| `status` | query | [SessionStatusKnown](#schema-SessionStatusKnown) | no |  |
| `Kabu-Org` | header | string | no | Select a live membership (default personal org); non-member is 404. Human tokens are live-only. Caller user id is distinct from billing owner. |

| Status | Means | Body |
|---|---|---|
| `200` | OK | [SessionList](#schema-SessionList) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl "https://usekabu.com/api/v2/sessions" \
  -H "Authorization: Bearer $KABU_API_KEY"
```

## `GET /v2/sessions/{session_id}` · Get a session

- **Auth:** an API key, or a signed-in human's token (the Kabu apps).
- **Scopes:** `sessions:read`.
- **Signed-in humans:** `audience`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `session_id` | path | string | yes | A `ses_…` id. |
| `Kabu-Org` | header | string | no | Select a live membership (default personal org); non-member is 404. Human tokens are live-only. Caller user id is distinct from billing owner. |

| Status | Means | Body |
|---|---|---|
| `200` | OK | [Session](#schema-Session) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl "https://usekabu.com/api/v2/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $KABU_API_KEY"
```

## `PATCH /v2/sessions/{session_id}` · Update status, headline, progress or Live Activity

Drives the Live Activity. Device pushes are coalesced to at most one per 2 s.

- **Auth:** an API key.
- **Scopes:** `sessions:write`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `session_id` | path | string | yes | A `ses_…` id. |
| `If-Match` | header | string | no |  |

**Request body**: [SessionUpdate](#schema-SessionUpdate).

| Status | Means | Body |
|---|---|---|
| `200` | OK | [Session](#schema-Session) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X PATCH "https://usekabu.com/api/v2/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## `POST /v2/sessions/{session_id}/end` · End a session; pending asks are cancelled

- **Auth:** an API key.
- **Scopes:** `sessions:write`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `session_id` | path | string | yes | A `ses_…` id. |
| `Idempotency-Key` | header | string, at most 255 characters | no | Retry-safe key. For messages, sessions and responses it binds to the created object for its life; elsewhere for 24 h. |

**Request body** (optional): [SessionEnd](#schema-SessionEnd).

| Status | Means | Body |
|---|---|---|
| `200` | OK | [Session](#schema-Session) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X POST "https://usekabu.com/api/v2/sessions/$SESSION_ID/end" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## `PUT /v2/presence` · Report whether a machine's agents are live (a batch a minute, and each transition)

One reporter per machine, (its key, its installation), sends every session it runs, at most 20, as enums only. The request is one transaction: its (generation, seq) must be newer than the reporter's stored one, or every report is refused (stale) and nothing renews; taken, it is stored with the reporter's 150 s lease on the server's clock. A steady batch writes that one row; a session row is written only when it changes, and presence.updated goes out only then: a new state, a session the batch leaves out, and once when the lease lapses. Presence never changes Session.version, never pushes and writes no request log. 429 beyond 12 requests a minute per reporter, 60 requests and 600 reports a minute per key, or 1,200 requests a minute per org.

- **Auth:** an API key.
- **Scopes:** `sessions:write`.

**Request body**: [PresenceBeat](#schema-PresenceBeat).

| Status | Means | Body |
|---|---|---|
| `200` | OK | [PresenceAck](#schema-PresenceAck) |
| `429` | A rate, concurrency or quota limit was reached | [Problem](/docs/api/discovery#schema-Problem) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X PUT "https://usekabu.com/api/v2/presence" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"instance":"…","generation":0,"seq":1,"reports":[{"session":"…","lane":"…","runtime":"working","source":"hook"}]}'
```

## `GET /v2/presence` · List the presence of sessions

Newest sessions first. With a user token, only sessions whose audience includes the caller. A session that was never leased, or whose lease lapsed over 24 h ago, has no presence.

- **Auth:** an API key, or a signed-in human's token (the Kabu apps).
- **Scopes:** `sessions:read`.
- **Signed-in humans:** `audience`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | integer, from 1 to 100 | no |  |
| `after` | query | string | no | Opaque cursor from next_cursor. |
| `session` | query | string | no | Only this `ses_…` session. |
| `agent` | query | string | no | Only this agent's sessions: its `agt_…` id or slug. |
| `project` | query | string | no | Only this project's sessions: its `prj_…` id or slug. |
| `Kabu-Org` | header | string | no | Select a live membership (default personal org); non-member is 404. Human tokens are live-only. Caller user id is distinct from billing owner. |

| Status | Means | Body |
|---|---|---|
| `200` | OK | [RuntimePresenceList](#schema-RuntimePresenceList) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl "https://usekabu.com/api/v2/presence" \
  -H "Authorization: Bearer $KABU_API_KEY"
```

## Schemas

### SessionCreate

An agent key creates a running session. A human (app or steer:write) with `steer` creates a queued kickoff for a runtime to claim.

| Property | Type | Required | Description |
|---|---|---|---|
| `title` | string, 1 to 80 characters | yes |  |
| `project` | [ProjectRef](/docs/api/messages#schema-ProjectRef) | no | A project slug (auto-created on first use) or `prj_…` id. Omit for `default`. A slug that normalises (lowercase, letters and digits only) to an existing project's key names that project, so `My App`, `my-app` and `my_app` are one project. Automatic creation stops at 1,000 live projects per org and mode with `429 quota_exceeded`; existing projects stay usable. Nothing remembers a deleted project (projects.delete): the next message that names its slug creates a fresh one. |
| `agent` | string, at most 64 characters | no |  |
| `external_id` | string, at most 120 characters | no | Your run id. Create is get-or-create on (project, agent, external_id): the open run it finds comes back as it is, except that `live.enabled` turns its Live Activity on, never an automatic session's. |
| `to` | array of [RecipientRef](/docs/api/messages#schema-RecipientRef), 1 to 20 items | no | Audience: who sees this session and its Live Activity, and who may steer it. Default: the key's default_to; for a human kickoff, the creating human. Messages on the session default to it. |
| `live` | [Live](#schema-Live) | no | Live Activity / Dynamic Island / Android ongoing notification, shown to the session audience. |
| `steerable` | boolean | no |  |
| `automatic` | boolean | no | True for a session your integration opens on its own to group its messages, as Kabu's MCP server does: it claims no run, so it starts `idle`, returns to `idle` once an ask in it settles, and never has a Live Activity of its own (`live` is not turned on, here or by a later get-or-create); each ask in it still has its own. False makes a run, whatever the `external_id`. Not with `steer`: a kickoff is a run. Absent, a kickoff or a session with `live` on is a run, and otherwise an `external_id` starting with `mcp:` or `auto:` (as integrations made them before this field) makes the session automatic. |
| `steer` | [SteerInput](#schema-SteerInput) | no | `control: instruction` with a body, or a pause / resume / stop signal. |
| `metadata` | [Metadata](/docs/api/messages#schema-Metadata) | no | Your own key/value pairs (≤ 50 keys, values ≤ 500 chars), returned verbatim on every object and event. |

Unknown properties are refused.

### Live

Live Activity / Dynamic Island / Android ongoing notification, shown to the session audience.

| Property | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | no | On: the session's Live Activity shows it, unless the session is automatic, which never has one. Turning it off ends the activity at once. Turned on again, a new activity starts at the session's first change eight hours after the ended one started (iOS's limit), not before. |
| `style` | string, one of `"progress"`, `"steps"`, `"timer"` | no |  |

Unknown properties are refused.

### SteerInput

`control: instruction` with a body, or a pause / resume / stop signal.

Type: [SteerInstruction](#schema-SteerInstruction) or [SteerSignal](#schema-SteerSignal).

Chosen by `control`: `"instruction"`: [SteerInstruction](#schema-SteerInstruction), `"pause"`: [SteerSignal](#schema-SteerSignal), `"resume"`: [SteerSignal](#schema-SteerSignal), `"stop"`: [SteerSignal](#schema-SteerSignal).

### SteerInstruction

A free-text instruction to the running agent.

| Property | Type | Required | Description |
|---|---|---|---|
| `control` | `"instruction"` | yes |  |
| `body` | string, 1 to 8000 characters | yes |  |
| `attachments` | array of [AttachmentRef](/docs/api/messages#schema-AttachmentRef), at most 10 items | no |  |
| `as_recipient` | [RecipientRef](/docs/api/messages#schema-RecipientRef) | no | `rcp_…`, `ext:<your external id>`, or `me` (the key's `default_to`). |
| `metadata` | [Metadata](/docs/api/messages#schema-Metadata) | no | Your own key/value pairs (≤ 50 keys, values ≤ 500 chars), returned verbatim on every object and event. |

Unknown properties are refused.

### SteerSignal

A control signal: pause, resume or stop. Signals have their own lane: they are leasable even while an instruction holds the head, ordered only among themselves, and charged only to a per-human abuse cap, never to send or month. `stop` marks every still-queued instruction on the session cancelled.

| Property | Type | Required | Description |
|---|---|---|---|
| `control` | string, one of `"pause"`, `"resume"`, `"stop"` | yes |  |
| `body` | string, 1 to 8000 characters | no | Optional reason. |
| `attachments` | array of [AttachmentRef](/docs/api/messages#schema-AttachmentRef), at most 10 items | no |  |
| `as_recipient` | [RecipientRef](/docs/api/messages#schema-RecipientRef) | no | `rcp_…`, `ext:<your external id>`, or `me` (the key's `default_to`). |
| `metadata` | [Metadata](/docs/api/messages#schema-Metadata) | no | Your own key/value pairs (≤ 50 keys, values ≤ 500 chars), returned verbatim on every object and event. |

Unknown properties are refused.

### Session

| Property | Type | Required | Description |
|---|---|---|---|
| `object` | `"session"` | yes |  |
| `id` | string | yes |  |
| `livemode` | boolean | yes |  |
| `project` | string | yes |  |
| `version` | integer | yes |  |
| `status` | [SessionStatus](#schema-SessionStatus) | yes | Open enum. Known values are `SessionStatusKnown`; accept and ignore unknown values. |
| `title` | string | yes |  |
| `headline` | string or null | yes |  |
| `to` | array of string | yes | The session audience. |
| `agent` | string or null | yes |  |
| `external_id` | string or null | yes |  |
| `progress` | [ProgressView](#schema-ProgressView) or null | yes |  |
| `live` | [LiveView](#schema-LiveView) | yes | Live Activity / Dynamic Island / Android ongoing notification, shown to the session audience. |
| `steerable` | boolean | yes |  |
| `automatic` | boolean | no | Read-only. True for a session made to group messages (`SessionCreate.automatic`; a session made before the field whose `external_id` starts with `mcp:` or `auto:` counts too): it claims no run and never has a Live Activity of its own. |
| `listening` | boolean | yes | Read-only presence: a consumer holds a claim or stream. |
| `open_asks` | integer | yes |  |
| `queued_steers` | integer | yes |  |
| `steering` | [SteeringState](#schema-SteeringState) or null | yes |  |
| `outcome` | string or null | yes |  |
| `metadata` | [Metadata](/docs/api/messages#schema-Metadata) | yes | Your own key/value pairs (≤ 50 keys, values ≤ 500 chars), returned verbatim on every object and event. |
| `created_at` | [Timestamp](/docs/api/messages#schema-Timestamp) | yes |  |
| `updated_at` | [Timestamp](/docs/api/messages#schema-Timestamp) | yes |  |
| `ended_at` | [Timestamp](/docs/api/messages#schema-Timestamp) or null | yes |  |
| `presence` | [RuntimePresence](#schema-RuntimePresence) | no | Whether a session's agent is live: a lease its machine renews, merged with the session's asks and end. Read-only. A session never leased has none. Presence never changes Session.version and never pushes. |

### SessionStatus

Open enum. Known values are `SessionStatusKnown`; accept and ignore unknown values.

Type: [SessionStatusKnown](#schema-SessionStatusKnown) or string.

### SessionStatusKnown

`queued` until a runtime claims a kickoff; `waiting` while an ask on the session is pending.

Type: string, one of `"queued"`, `"running"`, `"waiting"`, `"idle"`, `"ended"`.

### ProgressView

Measured progress, shown exactly as sent: value of total with label naming what is counted (54 of 172 projects), or known steps, the one in state doing named. Report progress only when it is measurable (items done of a known total) or for known steps; never an estimated percentage; otherwise send only a headline.

| Property | Type | Required | Description |
|---|---|---|---|
| `value` | number, ≥ 0 | no |  |
| `total` | number, > 0 | no |  |
| `label` | string, at most 60 characters | no |  |
| `steps` | array of [StepView](#schema-StepView), at most 8 items | no |  |

### StepView

| Property | Type | Required | Description |
|---|---|---|---|
| `label` | string, at most 40 characters | yes |  |
| `state` | string, one of `"todo"`, `"doing"`, `"done"`, `"failed"` or string | yes |  |

### LiveView

Live Activity / Dynamic Island / Android ongoing notification, shown to the session audience.

| Property | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | no | On: the session's Live Activity shows it, unless the session is automatic, which never has one. Turning it off ends the activity at once. Turned on again, a new activity starts at the session's first change eight hours after the ended one started (iOS's limit), not before. |
| `style` | string, one of `"progress"`, `"steps"`, `"timer"` or string | no |  |

### SteeringState

Why steering is or is not moving, for the human.

| Property | Type | Required | Description |
|---|---|---|---|
| `head` | string or null | yes |  |
| `state` | string | yes | idle, leased, received or blocked (open). |
| `consumer` | string or null | yes |  |
| `since` | [Timestamp](/docs/api/messages#schema-Timestamp) or null | yes |  |
| `reason` | string or null | yes |  |

### RuntimePresence

Whether a session's agent is live: a lease its machine renews, merged with the session's asks and end. Read-only. A session never leased has none. Presence never changes Session.version and never pushes.

| Property | Type | Required | Description |
|---|---|---|---|
| `object` | `"presence"` | yes |  |
| `id` | string | yes | The session's `ses_…` id. |
| `version` | integer | yes | Monotonic per session's presence, apart from Session.version; increases on every change except a renewal. presence.updated events carry it. |
| `livemode` | boolean | yes |  |
| `project` | string | yes |  |
| `agent` | string or null | yes |  |
| `state` | [PresenceState](#schema-PresenceState) | yes | Open enum. Known values are `PresenceStateKnown`; accept and ignore unknown values. |
| `runtime` | [PresenceRuntime](#schema-PresenceRuntime) | yes | Open enum. Known values are `PresenceRuntimeKnown`; accept and ignore unknown values. |
| `attention` | [PresenceAttention](#schema-PresenceAttention) or null | yes |  |
| `source` | [PresenceSource](#schema-PresenceSource) | yes | Open enum. Known values are `PresenceSourceKnown`; accept and ignore unknown values. |
| `outcome` | string or null | yes |  |
| `since` | string (date-time) | yes | When `state` last changed, by the server's clock. |
| `last_seen_at` | string (date-time) | yes | The last batch the server took that held the session. |
| `expires_at` | string (date-time) | yes | When the reporter's lease lapses (or lapsed) for this session. |

### PresenceState

Open enum. Known values are `PresenceStateKnown`; accept and ignore unknown values.

Type: [PresenceStateKnown](#schema-PresenceStateKnown) or string.

### PresenceStateKnown

The merged answer to "is it live?", first match wins: `ended` (the session ended), `waiting` (an ask is open), `blocked` (a live lease reports attention `terminal`), `working`, `idle` and `online` (a live lease), `offline` (the lease lapsed or the harness is gone). `paused` is only an agent's: it outranks its sessions.

Type: string, one of `"ended"`, `"waiting"`, `"blocked"`, `"working"`, `"idle"`, `"online"`, `"offline"`, `"paused"`.

### PresenceRuntime

Open enum. Known values are `PresenceRuntimeKnown`; accept and ignore unknown values.

Type: [PresenceRuntimeKnown](#schema-PresenceRuntimeKnown) or string.

### PresenceRuntimeKnown

What a session's runtime is doing, as its machine reports it: `working` while a turn is open, `idle` once it ended (ready for a prompt), `online` from a liveness-only source, `offline` when the harness is gone. Presence reads `offline` once the lease lapses.

Type: string, one of `"working"`, `"idle"`, `"online"`, `"offline"`.

### PresenceAttention

Open enum. Known values are `PresenceAttentionKnown`; accept and ignore unknown values.

Type: [PresenceAttentionKnown](#schema-PresenceAttentionKnown) or string.

### PresenceAttentionKnown

Why a live runtime needs its person: `terminal` while a prompt waits at the computer (a permission request).

Type: string, one of `"terminal"`.

### PresenceSource

Open enum. Known values are `PresenceSourceKnown`; accept and ignore unknown values.

Type: [PresenceSourceKnown](#schema-PresenceSourceKnown) or string.

### PresenceSourceKnown

Which local signal a report came from: the harness's hooks, the Herdr bridge, `kabu mcp` or the kabu CLI.

Type: string, one of `"hook"`, `"herdr"`, `"mcp"`, `"cli"`.

### SessionList

| Property | Type | Required | Description |
|---|---|---|---|
| `object` | `"list"` | yes |  |
| `data` | array of [Session](#schema-Session) | yes |  |
| `has_more` | boolean | yes |  |
| `next_cursor` | string or null | yes |  |

### SessionUpdate

`live` with `enabled` off ends the session's Live Activity at once, with a generic last state. Turned on, it starts one, unless the session is automatic; after an end, at the session's first change eight hours after the ended activity started.

| Property | Type | Required | Description |
|---|---|---|---|
| `status` | string, one of `"running"`, `"idle"` | no |  |
| `headline` | string, at most 80 characters | no |  |
| `progress` | [Progress](#schema-Progress) | no | Measured progress, shown exactly as sent: value of total with label naming what is counted (54 of 172 projects), or known steps, the one in state doing named. Report progress only when it is measurable (items done of a known total) or for known steps; never an estimated percentage; otherwise send only a headline. |
| `live` | [Live](#schema-Live) | no | Live Activity / Dynamic Island / Android ongoing notification, shown to the session audience. |
| `metadata` | [Metadata](/docs/api/messages#schema-Metadata) | no | Your own key/value pairs (≤ 50 keys, values ≤ 500 chars), returned verbatim on every object and event. |

Unknown properties are refused.

### Progress

Measured progress, shown exactly as sent: value of total with label naming what is counted (54 of 172 projects), or known steps, the one in state doing named. Report progress only when it is measurable (items done of a known total) or for known steps; never an estimated percentage; otherwise send only a headline.

| Property | Type | Required | Description |
|---|---|---|---|
| `value` | number, ≥ 0 | no |  |
| `total` | number, > 0 | no |  |
| `label` | string, at most 60 characters | no |  |
| `steps` | array of [Step](#schema-Step), at most 8 items | no |  |

Unknown properties are refused.

### Step

| Property | Type | Required | Description |
|---|---|---|---|
| `label` | string, at most 40 characters | yes |  |
| `state` | string, one of `"todo"`, `"doing"`, `"done"`, `"failed"` | yes |  |

Unknown properties are refused.

### SessionEnd

| Property | Type | Required | Description |
|---|---|---|---|
| `outcome` | string, one of `"success"`, `"failure"`, `"cancelled"` | no |  |
| `summary` | string, at most 500 characters | no |  |
| `dismiss_after` | integer, from 0 to 14400 | no |  |

Unknown properties are refused.

### PresenceBeat

A reporter's whole state: every session it runs (at most 20), once a minute and at each transition; a session it leaves out goes offline. A batch is one transaction. Taking it renews the reporter's 150 s lease on the server's clock, which keeps its sessions live; a steady batch writes that lease and nothing else.

| Property | Type | Required | Description |
|---|---|---|---|
| `instance` | string, matching `^pin_[0-9A-Za-z]{8,40}$` | yes | The installation's id, made once and kept on the machine for its life, never per start. Kabu binds it to the key that sends it: the reporter is (key, instance), so the same id under another key is another reporter. |
| `generation` | integer, from 0 to 9007199254740991 | yes | The reporter's restart counter: kept on disk and raised by one at every start, never read from a clock. Generations of different reporters are never compared: another reporter takes a session only once its lease lapsed. A reporter that lost its counter resumes above the ack's `generation`. |
| `seq` | integer, from 1 to 9007199254740991 | yes | Raised on every request of a generation. The reporter's (generation, seq) is stored with every accepted batch, in the same transaction, so a retried or replayed request is refused (`stale`) and renews nothing: a retry takes a new seq. |
| `reports` | array of [PresenceReport](#schema-PresenceReport), 1 to 20 items | yes |  |

Unknown properties are refused.

### PresenceReport

One session's runtime, as enums only: no prompt, path, tool name or title is accepted.

| Property | Type | Required | Description |
|---|---|---|---|
| `session` | string, matching `^ses_[0-9A-Za-z]{1,64}$` | yes | A `ses_…` id the key can see: the session of the run, found or created with an `external_id` the machine computed as HMAC(installation secret, harness ‖ native session id), never the raw id. |
| `lane` | string, matching `^ln_[0-9a-f]{16,64}$` | yes | An opaque hash of the local lane (machine, harness and native session) the report folds; never a path. |
| `runtime` | [PresenceRuntimeKnown](#schema-PresenceRuntimeKnown) | yes | What a session's runtime is doing, as its machine reports it: `working` while a turn is open, `idle` once it ended (ready for a prompt), `online` from a liveness-only source, `offline` when the harness is gone. Presence reads `offline` once the lease lapses. |
| `attention` | [PresenceAttentionKnown](#schema-PresenceAttentionKnown) or null | no |  |
| `source` | [PresenceSourceKnown](#schema-PresenceSourceKnown) | yes | Which local signal a report came from: the harness's hooks, the Herdr bridge, `kabu mcp` or the kabu CLI. |

Unknown properties are refused.

### PresenceAck

| Property | Type | Required | Description |
|---|---|---|---|
| `object` | `"presence_ack"` | yes |  |
| `server_time` | [Timestamp](/docs/api/messages#schema-Timestamp) | yes |  |
| `next_beat_in` | integer | yes | Seconds until the next batch is due (60). A transition may go at once. |
| `generation` | integer | yes | The reporter's stored generation after this request. |
| `seq` | integer | yes | The reporter's stored seq after this request: send a higher one next. |
| `expires_at` | string (date-time) or null | yes |  |
| `results` | array of [PresenceResult](#schema-PresenceResult) | yes | One per report, in order. |

### PresenceResult

| Property | Type | Required | Description |
|---|---|---|---|
| `session` | string | yes |  |
| `accepted` | boolean | yes |  |
| `reason` | string or null | yes |  |
| `expires_at` | string (date-time) or null | yes |  |

### RuntimePresenceList

| Property | Type | Required | Description |
|---|---|---|---|
| `object` | `"list"` | yes |  |
| `data` | array of [RuntimePresence](#schema-RuntimePresence) | yes |  |
| `has_more` | boolean | yes |  |
| `next_cursor` | string or null | yes |  |
