Post a message to a conversation
POST /v1/api/conversations/{id}/messages — Phase A.4 (#726).
Decoupled from the SSE stream: returns 202 Accepted after
idempotency check, spawns the turn driver in the background,
caller observes events via GET /v1/api/conversations/{id}/stream
(already attached or about to attach).
Idempotency is decided IN THIS HANDLER, before spawning. The
spawn passes idempotency_key: None so the inner pipeline’s
prepare_turn skips its own re-check — avoids a double
SETNX that would log a spurious “duplicate” on the fresh
path.
Asynchronous turn — read the reply from the event stream
This endpoint does not return the agent’s reply. It accepts the message, starts the turn server-side, and returns immediately with202 Accepted. You observe progress and read the final reply on the conversation’s event stream.
Typical flow:
- Open
GET /v1/api/conversations/{id}/stream(SSE). POSTthe message here →202 Accepted.- Read this turn’s
text_delta…finalevents off the stream.
Authorizations
Personal Access Token. Send as Authorization: Bearer hq_pat_....
Path Parameters
Conversation id
Body
Body for the 202-style submit endpoint. A slim subset of
[PostMessageBody]: agent / tenant / channel are derived from
the conversation row, so the caller never has to know them.
File attachments (base64-encoded). Same shape as the inline endpoint; carried through to TurnPayload.attachments.
True iff this is a local-browser turn (the extension's "Use my browser"
mode). Fails fast if the user's browser isn't connected, then scopes the
turn to computer_* + injects the browser prompt via the per-turn
browser_session Forge flag - exactly like POST /v1/agent-browser/ask,
but on the streaming path so the computer_* tool calls render live.
Optional dedup token. Same semantics as the inline endpoint:
re-POST with the same key inside the 24h TTL is a no-op and
returns idempotent: true. Caller should generate a UUID
per logical submit (e.g. per Studio chat-send button click).
Free-form identity claims from the caller. PAT auth (#729) will replace this with auth-derived chain; for now the trust boundary is network-level reach to controlplane.
Reply-to-source channel for this turn. Defaults to "web"; the browser
extension (#729) sets "extension" so artifacts/replies aren't pushed
back into a Slack thread the conv may have been born in. Drives
conversations.last_turn_channel via turn.rs.
Response
Turn accepted (spawned in the background); attach to stream_url for events