# Referința protocolului (/ro/docs/sessions/protocol)

Shared Sessions v1 — obiecte, headere, fiecare rută HTTP, regulile lease-ului, idempotență, erori și limite.

Aceasta este suprafața normativă a **codai Shared Sessions v1.0.0** așa cum este servită de `https://ai.codai.ro`. Numele câmpurilor sunt `snake_case` pe fir; timestamp-urile numite `*_at` sunt string-uri ISO-8601; `ts` și `last_seen` sunt milisecunde întregi de la epoca Unix. JSON Schema-urile pentru fiecare obiect stau în [repository-ul protocolului](https://github.com/codai-ro/codai-protocol). Frame-urile realtime sunt pe [pagina de streaming](./streaming.mdx); share-urile și organizațiile au [pagina lor](./sharing-and-orgs.mdx).

## Transport și headere [#transport-și-headere]

Toate rutele sunt HTTPS sub URL-ul de bază al gateway-ului și sunt JSON dacă nu se spune altfel. Autentificarea este o cheie API Bearer; principalul este **utilizatorul** căruia îi aparține cheia.

```http
Authorization: Bearer codai_xxxxxxxx
```

### Headere de dispozitiv [#headere-de-dispozitiv]

| Header | Obligatoriu | Semnificație |
| --- | --- | --- |
| `x-codai-device` | La fiecare scriere, lease, stream și WebSocket; opțional la citiri. | Un **UUID ales de client**, stabil per instalare. Înregistrat lazy sub utilizatorul apelantului la prima apariție; `last_seen_at` actualizat la fiecare request. Orice nu este UUID primește `400`. |
| `x-codai-device-platform` | Recomandat | `android` · `ios` · `web` · `desktop` · `cli` · `agent`. Orice altceva (sau absent) se stochează ca `agent`. |
| `x-codai-device-name` | Recomandat | Etichetă lizibilă, trunchiată la 120 de caractere. Implicit `"<platform> device"`. Folosită doar la prima înregistrare. |
| `x-codai-push-token` | Opțional | Token de înregistrare FCM, 8 – 4096 caractere ASCII, salvat (upsert) pe dispozitiv la orice request HTTP care poartă `x-codai-device`. O valoare malformată este ignorată silențios. |
| `x-codai-share-token` | Când exerciți un share prin link | Token-ul de link share în clar. Alternativă: `?share=<token>` (pentru SSE/WS din browsere). |

Un id de dispozitiv deja înregistrat la **alt** utilizator este refuzat cu `403 forbidden` — *Device id is registered to another account.* — nu refolosit.

### Selectarea v2 pe căi suprapuse [#selectarea-v2-pe-căi-suprapuse]

`GET /v1/sessions` și `GET /v1/sessions/:id` împart căile cu un API legacy de memorie read-only. Handler-ul de sesiuni partajate este selectat când request-ul poartă `x-codai-device` **sau** `?v=2`. Trimite mereu `x-codai-device`; `?v=2` este pentru citiri rapide fără dispozitiv.

### Referințe la sesiune [#referințe-la-sesiune]

Oriunde o cale conține `:id` poți trimite fie `id`-ul UUID de server, fie propriul `session_key`. Când un `session_key` se ciocnește între utilizatori, sesiunea ta câștigă.

## Obiecte [#obiecte]

### Session [#session]

```json
{
  "id": "5b3e…-uuid",
  "session_key": "phone-7f1c…",
  "owner_user_id": "…-uuid",
  "title": "Book the dentist",
  "created_at": "2026-09-13T10:00:00.000Z",
  "last_event_at": "2026-09-13T10:04:12.512Z",
  "last_seq": 42,
  "executor_device_id": "…-uuid or null",
  "lease_expires_at": "2026-09-13T10:04:40.000Z or null",
  "e2e": false,
  "archived": false
}
```

`session_key` este ales de client (telefonul folosește același UUID pe care îl trimite ca `x-codai-session-id` la inferență); `id` este atribuit de server. Răspunsurile de listare adaugă `role` (al tău) și `presence_count`; răspunsul de detaliu adaugă `role`, `members[]`, `lease` și `presence[]`; `shared-with-me` adaugă `role` și `share_id`.

### Device [#device]

```json
{
  "id": "…-uuid",
  "name": "Pixel 9",
  "platform": "android",
  "capabilities": ["screen", "local_llm", "terminal", "contacts"],
  "has_push_token": true,
  "lastSeenAt": "2026-09-13T10:04:12.512Z",
  "createdAt": "2026-09-01T08:00:00.000Z",
  "online": true
}
```

`capabilities` este o listă liberă de ≤ 64 string-uri de ≤ 64 caractere. Token-ul push **nu este returnat niciodată** — clienții văd `has_push_token`. `online` (pe `GET /v1/devices`) este `true` când `lastSeenAt` e în ultimele 2 minute. Cele două timestamp-uri camelCase de pe acest obiect fac parte din v1 și nu vor fi redenumite.

### Event [#event]

```json
{
  "seq": 7,
  "kind": "tool_call",
  "ts": 1757757852512,
  "sender_device_id": "…-uuid",
  "turn_id": "t-…",
  "client_event_id": "c9f2…",
  "payload": { "name": "open_app", "args": { "package": "com.whatsapp" } }
}
```

- `seq` este **atribuit de server, dens per sesiune, începând de la 1**. Clienții nu îl trimit niciodată.
- `kind`: 1 – 64 caractere, opac pentru server; kind-urile necunoscute nu sunt respinse niciodată. Vocabularul de trace al executorului este `turn_start, step_start, req_start, local_gen, tool_start, tool_call, tool_result, ask, ask_resolved, wait_user, deadline, error, usage, turn_end`, plus `assistant`, `user`, precum și `screen_wait` și `todos`, emise de executor.
- `todos` conține lista completă de sarcini a agentului (ultima primită câștigă): `{"items":[{"id":"1","title":"…","status":"not-started|in-progress|completed"}],"source":"tool|user","tool_call_id":"call_…","done":1,"total":3}`. Cel mult 50 de elemente; `tool_call_id` apare doar când `source` este `tool`. Un dispozitiv care urmărește sesiunea își înlocuiește panoul de sarcini cu această listă; nu este un rând în transcriere.
- `ts`: ceasul clientului; implicit ora serverului când lipsește.
- `payload`: orice obiect JSON (implicit `{}`); `{ "hide": "<base64>" }` în modul E2E.

Kind-uri emise de server:

| kind | Emis când | Payload |
| --- | --- | --- |
| `control` | Un control este acceptat | `{ "control": Control, "applied": false }` |
| `lease_transferred` | Un lease este preluat forțat de la un deținător activ | `{ "from_device_id", "to_device_id", "forced": true }` |

### Control [#control]

```json
{
  "id": "ctl-8d1a…",
  "kind": "steer",
  "text": "Use the second search result instead.",
  "turn_id": "t-…",
  "ask_id": null,
  "from_device_id": "…-uuid",
  "target_device_id": null,
  "seq": 8,
  "applied": false,
  "created_at": "2026-09-13T10:04:13.000Z"
}
```

`kind` ∈ `send | answer | approve | deny | cancel | steer | inject | pause | resume`. `id` este **cheia de idempotență a clientului** (1 – 128 caractere); `text` ≤ 100 000 caractere. `target_device_id` este setat doar de [dispatch](#dispatch) și este `null` altfel; un control țintit este destinat acelui singur dispozitiv și orice alt executor ar trebui să-l sară. Un dispatch `mode: "open"` adaugă `"mode": "open"` pe controlul său `send` (fără text); câmpul lipsește în rest.

| kind | Executorul ar trebui să… |
| --- | --- |
| `send` | Pornească un turn nou cu `text` ca mesaj al utilizatorului. |
| `answer` | Rezolve `ask`-ul în așteptare identificat de `ask_id` cu `text`. |
| `approve` / `deny` | Lase confirmarea în așteptare (`ask_id`) să continue / o refuze. |
| `cancel` | Oprească turn-ul `turn_id` (sau pe cel curent). |
| `pause` | Pună pe pauză turn-ul în curs la următoarea graniță de pas (după ce se termină apelurile de unelte curente). Turn-ul rămâne deschis; executorul emite `run_state` `{"state":"paused"}`. Editorii îl pot trimite (aceeași încredere ca `cancel`). |
| `resume` | Continue turn-ul pus pe pauză — același turn, nu se retrimite nimic. Executorul emite `run_state` `{"state":"running"}`. |
| `steer` | Injecteze `text` ca ghidaj în turn-ul în curs fără să-l încheie. |
| `inject` | Adauge `text` la context pentru următorul apel de model. |

### Lease [#lease]

```json
{ "session_id": "…-uuid", "device_id": "…-uuid", "expires_at": "2026-09-13T10:04:40.000Z" }
```

TTL **30 s**; heartbeat la fiecare **10 s**. Frame-urile `lease` realtime folosesc `holder_device_id` (nullable) în loc de `device_id`.

### Presence [#presence]

```json
{ "device_id": "…-uuid", "user_id": "…-uuid", "role": "editor", "executor": false, "driving": false, "last_seen": 1757757852512, "online": true, "remote": true }
```

Efemer, niciodată persistat. `remote` este calculat pentru apelant și apare doar în `GET /v1/sessions/:id`; `online` apare doar în frame-urile `presence` (`false` pe frame-ul de plecare).

### Share și Org [#share-și-org]

Vezi [partajare și organizații](./sharing-and-orgs.mdx). Share: `{ id, session_id, principal_type: user|org|link, principal_id, role: viewer|editor|owner, has_token, expires_at, created_by_user_id, created_at }` (+ `token` o singură dată). Org: `{ id, name, ownerUserId, createdAt }` — camelCase pe fir — cu roluri de membru `owner | admin | member`.

## Roluri [#roluri]

1. `owner` când `owner_user_id` al sesiunii ești tu.
2. Altfel rolul **maxim** peste share-urile neexpirate care ți se potrivesc direct, oricărei organizații din care faci parte sau token-ului de link pe care îl prezinți.
3. Altfel niciun rol → `403 not_a_member`. Un id necunoscut este `404 not_found`.

| Acțiune | viewer | editor | owner |
| --- | :---: | :---: | :---: |
| GET session / events / controls / stream / WS | ✓ | ✓ | ✓ |
| POST control, POST dispatch | | ✓ | ✓ |
| PATCH / DELETE session | | | ✓ |
| POST / PUT / DELETE lease | | | ✓ |
| GET / POST / DELETE shares | | | ✓ |
| POST events, POST control/:cid/applied | **doar deținătorul lease-ului** (în practică un dispozitiv al owner-ului) | | |

## Rute HTTP [#rute-http]

Fiecare mutație urmează *validare → autentificare → autorizare → audit → execuție*. O validare eșuată este `400 bad_request` cu `details` care descrie câmpurile.

### Sesiuni [#sesiuni]

| Metodă | Cale | Rol minim | Request | Răspuns | Erori |
| --- | --- | --- | --- | --- | --- |
| `POST` | `/v1/sessions` | orice utilizator | `{ session_key?: string ≤256, title?: string ≤500, e2e?: bool }`. `session_key` implicit un UUID aleator. | `201` Session (nouă) · `200` Session (`session_key` existent) | 400 |
| `GET` | `/v1/sessions?v=2&limit=&archived=` | orice utilizator | `limit` implicit 50, max 200 (aplicat separat listei proprii și celei partajate); `archived=1` include arhivatele | `200 { sessions: [Session & { role, presence_count }] }` — ale tale întâi, apoi cele partajate | — |
| `GET` | `/v1/sessions/:id` | viewer | — | `200` Session & `{ role, members[], lease: Lease \| null, presence[] }` | 403, 404 |
| `PATCH` | `/v1/sessions/:id` | owner | `{ title?: string \| null, archived?: bool }` (≥ 1 câmp) | `200` Session | 400, 403, 404 |
| `DELETE` | `/v1/sessions/:id` | owner | — | `200 { deleted: true, id }` — șterge în cascadă events, controls, lease, shares | 403, 404 |

`members[]` = `{ user_id, role, remote, devices: [{ device_id, name, platform, last_seen_at, remote }] }`, o intrare per utilizator membru (owner, share-uri directe către utilizatori, membrii share-urilor către organizații și tu, dacă ai venit printr-un link).

### Evenimente [#evenimente]

| Metodă | Cale | Rol minim | Request | Răspuns | Erori |
| --- | --- | --- | --- | --- | --- |
| `GET` | `/v1/sessions/:id/events?after=&limit=` | viewer | `after` implicit 0; `limit` implicit 200, max 1000 | `200 { last_seq, events: Event[] }` crescător după `seq` | 403, 404 |
| `POST` | `/v1/sessions/:id/events` | **deținătorul lease-ului** | `{ events: IncomingEvent[1..200], expected_last_seq?: int }` | `200 { last_seq, accepted: int, events: [{ client_event_id, seq }] }` | 400, 403, 404, 409 `lease_held`, 409 `seq_conflict` |

`IncomingEvent` = `{ kind, ts?, turn_id?, client_event_id?, payload? }` — niciodată `seq`. `accepted` numără evenimentele **stocate nou**; duplicatele (după `client_event_id`) sunt raportate cu `seq`-ul lor original.

### Controls și dispatch [#controls-și-dispatch]

| Metodă | Cale | Rol minim | Request | Răspuns | Erori |
| --- | --- | --- | --- | --- | --- |
| `GET` | `/v1/sessions/:id/controls?applied=&target=` | viewer | `applied` implicit `false`; `target=me` (necesită `x-codai-device`) sau un UUID de dispozitiv | `200 { controls: Control[] }` (≤ 500, cele mai vechi întâi). **Nu depinde de lease.** | 400, 403, 404 |
| `POST` | `/v1/sessions/:id/control` | editor | `{ id, kind, text?, turn_id?, ask_id? }` | `202 { accepted: true, seq, duplicate: false }` · `200 { …, duplicate: true }` | 400, 403, 404 |
| `POST` | `/v1/sessions/:id/control/:cid/applied` | **deținătorul lease-ului** (sau ținta unui handoff `open`) | — | `200 { id, applied: true }` | 403, 404, 409 `lease_held` |
| `POST` | `/v1/sessions/:id/dispatch` | editor | `{ device_id: uuid, mode?: "send" \| "open", text?: string 1..100000, turn_id?, control_id? }` — `text` obligatoriu, cu excepția `mode: "open"` | `202 { control_id, seq, target_device_id, mode, queued: true, pushed, duplicate: false, push_reason? }` · `200` cu `duplicate: true` | 400, 403, 404 (sesiune sau dispozitiv țintă) |
| `GET` | `/v1/devices/me/dispatch?limit=` | sesiuni proprii | `limit` implicit 200, max 500; necesită `x-codai-device` | `200 { device_id, sessions: [{ id, session_key, title, controls: Control[] }] }` — fiecare control în așteptare țintit către acest dispozitiv din toate sesiunile tale nearhivate, într-un singur apel | 400 |

### Lease [#lease-1]

| Metodă | Cale | Rol minim | Request | Răspuns | Erori |
| --- | --- | --- | --- | --- | --- |
| `POST` | `/v1/sessions/:id/lease` | owner | `{ device_id?: uuid, force?: bool }` (body gol permis; `device_id` implicit `x-codai-device`) | `200 { session_id, device_id, expires_at }` | 400, 403, 404, 409 `lease_held` |
| `PUT` | `/v1/sessions/:id/lease` | owner | — (heartbeat) | `200 { session_id, device_id, expires_at }` | 403, 404, 409 `lease_held` |
| `DELETE` | `/v1/sessions/:id/lease` | owner | — | `200 { session_id, released: bool }` | 403, 404 |

### Dispozitive [#dispozitive]

| Metodă | Cale | Request | Răspuns | Erori |
| --- | --- | --- | --- | --- |
| `GET` | `/v1/devices` | — | `200 { devices: Device[] }` (ale tale, cele mai recente întâi) | — |
| `PATCH` | `/v1/devices/:id` | `{ push_token?: string \| null, capabilities?: string[] }` (≥ 1 câmp) | `200` Device | 400, 404 |
| `DELETE` | `/v1/devices/:id` | — | `200 { deleted: true, id }` | 400, 404 |

## Semantica lease-ului [#semantica-lease-ului]

- **TTL 30 s**, reînnoit prin `PUT …/lease` la fiecare **10 s**. `expires_at` este returnat la preluare și la fiecare heartbeat.
- **Preluarea este compare-and-set.** `POST …/lease` reușește când lease-ul este liber, expirat sau deja deținut de dispozitivul apelant. Dacă alt dispozitiv deține un lease *activ* → `409 lease_held`; deținătorul este numit în `message` ca `(holder_device_id=…)`.
- **Force.** `{ "force": true }` (owner) ia un lease activ de la alt dispozitiv. Serverul adaugă un eveniment `lease_transferred` și îl auditează. Fostul deținător află la următorul heartbeat (`409 lease_held`, *Lease lost to another device.*) și trebuie să oprească execuția, să devină viewer și să arate cine conduce.
- **Heartbeat pe un lease expirat** este `409 lease_held` cu *Lease expired.* și fără deținător.
- **Eliberarea** este idempotentă: `released: false` când nu îl dețineai.
- Fiecare preluare/heartbeat/eliberare publică un frame `lease` și oglindește `executor_device_id` / `lease_expires_at` pe sesiune.
- Adăugarea de evenimente sau marcarea unui control ca aplicat fără lease-ul activ → `409 lease_held`.

## Idempotență [#idempotență]

| Operație | Cheie | Comportament la repetare |
| --- | --- | --- |
| `POST /v1/sessions` | `session_key` (per utilizator) | `200` cu sesiunea existentă în loc de `201`. Un `session_key` egal cu `id`-ul UUID al unei sesiuni existente se rezolvă tot la acea sesiune. |
| `POST …/events` | `(session, sender_device, client_event_id)` | Duplicatele nu sunt stocate; răspunsul le mapează la `seq`-ul lor **original**; `accepted` le exclude. Elementele fără `client_event_id` nu sunt deduplicate niciodată — trimite mereu unul. |
| `POST …/control`, control WS | `(session, control.id)` | `200 { accepted: true, seq, duplicate: true }`; niciun eveniment nou, niciun rând nou de audit. |
| `POST …/dispatch` | `(session, control_id)`; `dispatch:<uuid>` generat de server când lipsește | `200` cu `duplicate: true`; **niciun push retrimis**. |
| `POST …/lease` | dispozitiv | Re-preluarea propriului lease reîmprospătează `expires_at`. |
| `POST …/control/:cid/applied` | — | Re-marcarea este un no-op `200`. |

**Append optimist.** `expected_last_seq` pe un body de evenimente face adăugarea condiționată: dacă `last_seq` al sesiunii diferă → `409 seq_conflict` și nu se scrie nimic. **Alocarea de seq** se face într-o singură tranzacție — `last_seq` este incrementat cu numărul de evenimente *noi* și acestea sunt inserate ca `base+1 … base+n`, așa că `seq` este dens și fără goluri.

## Dispatch [#dispatch]

Pornește o sarcină pe un **dispozitiv anume** din orice client editor+. Doar dispozitivele owner-ului pot deține lease-ul, așa că `device_id` trebuie să aparțină owner-ului — orice altceva este `404 not_found` (*Target device not found for this session.*). Alege id-ul din `GET /v1/sessions/:id → members[].devices`.

La `POST …/dispatch` serverul persistă un control `send` al cărui eveniment-ecou poartă `payload.control.target_device_id`, îl auditează și — dacă ținta are token push — trimite un mesaj FCM HTTP v1 **data-only, high-priority** `{ "type": "dispatch", "session_id", "control_id" }`. Push-ul este **fail-open**: un eșec nu face niciodată request-ul să eșueze; controlul este durabil oricum. `pushed: false` cu `push_reason` ∈ `no_push_token | no_token | auth_failed | network_error | unregistered | http_<status>` înseamnă *pus în coadă, nelivrat încă* — nu o eroare. La `unregistered` serverul șterge token-ul push al dispozitivului.

**Moduri.** `mode` este implicit `send`, care cere `text` și îl rulează ca tură nouă. `mode: "open"` este un **handoff**: ținta doar deschide sesiunea. `text` și `turn_id` sunt ignorate, nu se creează nicio tură de utilizator, iar controlul este un `send` fără text care poartă `mode: "open"` (în `GET …/controls`, `GET /v1/devices/me/dispatch`, evenimentul-ecou și datele FCM). Ținta poate marca un handoff `open` ca aplicat **fără** să dețină lease-ul. Răspunsul reflectă `mode`-ul efectiv.

Fluxul de trezire pe dispozitiv: push → pornește executorul headless → `GET /v1/devices/me/dispatch` (sau `GET …/controls?applied=false&target=me`, **înainte** de a deține lease-ul) → `POST …/lease` (`force: true` dacă politica permite) → execută `send`-ul consumat → `POST …/control/:cid/applied` → bucla normală. Dispatch-ul nu atinge niciodată lease-ul.

## Erori [#erori]

```json
{ "error": { "message": "Another device holds a live executor lease. (holder_device_id=1c2f…)", "type": "invalid_request_error", "code": "lease_held" } }
```

`details` este inclus pentru `bad_request` (rezultatul validării la nivel de câmp). Pentru `lease_held` și `seq_conflict` valoarea utilă este încorporată în `message`; re-`GET` sesiunea pentru `last_seq`-ul curent.

| HTTP | `code` | Când |
| --- | --- | --- |
| 400 | `bad_request` | JSON invalid, încălcare de schemă, `x-codai-device` lipsă/invalid, `?target` invalid, id greșit |
| 401 | `invalid_api_key` | Cheie Bearer lipsă sau invalidă |
| 403 | `not_a_member` | Sesiunea există, nu ai niciun rol (suficient) |
| 403 | `forbidden` | Id de dispozitiv deținut de alt cont; rol de organizație insuficient; nu ești membru al organizației |
| 404 | `not_found` | Sesiune / share / organizație / membru / dispozitiv / control necunoscut; ținta dispatch-ului nu e a owner-ului |
| 409 | `lease_held` | Preluare, heartbeat, adăugare de eveniment sau marcare applied fără lease-ul activ |
| 409 | `seq_conflict` | Nepotrivire `expected_last_seq` |
| 500 | `internal_error` | Eșec de stocare — această suprafață **nu** este fail-open |

Erorile WebSocket pe mesajele up folosesc `{ v: 1, t: "error", ref?, error: {…} }` și *includ* `details`.

## Limite [#limite]

| Limită | Valoare |
| --- | --- |
| Evenimente per batch `POST …/events` | 1 … **200** |
| Flush recomandat pe client | ≤ 200 ms sau **20** evenimente |
| `kind` de eveniment | 1 … 64 caractere |
| `turn_id`, `client_event_id`, `id` de control, `ask_id` | ≤ 128 caractere |
| `text` de control / dispatch | ≤ 100 000 caractere |
| `session_key` | ≤ 256 caractere |
| `title` | ≤ 500 caractere |
| `name` de organizație | 1 … 200 caractere |
| Replay per conectare / `GET …/events` | `limit` ≤ **1000** (implicit 200) |
| `limit` la `GET /v1/sessions` | ≤ 200 (implicit 50) |
| `GET …/controls` | ≤ 500 rânduri |
| `capabilities` de dispozitiv | ≤ 64 elemente × 64 caractere |
| Nume de dispozitiv | ≤ 120 caractere |
| Token push | 8 … 4096 caractere |
| TTL lease / heartbeat | 30 s / 10 s |
| Ping SSE / conexiune maximă | 15 s / 30 min |
| Destinatari E2E | ≤ 64 |

Nu există rate limit per rută specific acestei suprafețe; se aplică limitele generale per cheie API ale gateway-ului.

## Versionare [#versionare]

Frame-urile poartă `v: 1`. Câmpurile adăugate nu sunt breaking; un `kind` nou de eveniment nu este breaking (viewerii ignoră kind-urile necunoscute). Nimic din ce este listat aici nu este eliminat sau redenumit în cadrul v1; deprecierile sunt anunțate în `CHANGELOG.md` din repository-ul protocolului înainte ca comportamentul să se schimbe.

## Checklist de conformitate [#checklist-de-conformitate]

<Accordions>
<Accordion title="Un viewer TREBUIE">

- Să trimită `Authorization: Bearer` și un UUID stabil în `x-codai-device` (plus platformă și nume).
- Să ignore tipurile de frame, kind-urile de eveniment și câmpurile necunoscute.
- Să urmărească cel mai mare `seq` văzut și să se reconecteze cu `after=<seq>` după o deconectare sau după tăierea de 30 de minute.
- Să trateze `holder_device_id: null` ca „fără executor”.
- Să afișeze evenimentele `control` (`payload.control`, `payload.applied`) și frame-urile `presence`.
- Să nu pună niciodată cheia API într-un URL.

</Accordion>
<Accordion title="Un editor TREBUIE în plus">

- Să folosească un `id` de control unic per intenție și să-l refolosească la retry.
- Să trateze `200 duplicate: true` ca succes.

</Accordion>
<Accordion title="Un executor TREBUIE în plus">

- Să preia lease-ul înainte de a adăuga; să se oprească în clipa în care un heartbeat returnează `409`.
- Să facă heartbeat cel puțin la fiecare 10 s.
- Să trimită `client_event_id` pe fiecare eveniment; să tolereze `accepted < events.length`.
- Să consume `GET …/controls?applied=false` la pornire și să marcheze fiecare ca aplicat; să sară controls cu un `target_device_id` străin.
- La o trezire prin dispatch, să consume `target=me` **înainte** de a prelua lease-ul.
- Să elibereze lease-ul la o oprire curată.

</Accordion>
<Accordion title="Un agent care face dispatch TREBUIE în plus">

- Să aleagă `device_id` din `GET /v1/sessions/:id → members[].devices`.
- Să furnizeze propriul `control_id` la retry.
- Să trateze `pushed: false` ca pus în coadă, nu ca eșec.

</Accordion>
</Accordions>
