# Threads

> A conversation in Pidgeon: how messages are threaded, who owns one, the status it carries, and how to assign it to a person.

A thread is a row, not a grouping key. Messages are threaded when they arrive,
by `Message-ID`, `In-Reply-To` and `References`, and the thread carries the
two things software needs to coordinate with a person: a **status** and an
**owner**.

## One call across every address

`GET /threads` is workspace-wide — `identity_id` narrows it to one mailbox
and is optional — and every row names the address it is in. It is the
listing to build a shared inbox on, because [the folder
listing](https://pidgeon.ai/docs/mailbox.md) takes one address and one folder, and three
addresses over four folders is twelve requests for one screen.

A row carries enough of the newest message to draw it without a second
call: `folder`, `from`, `from_name`, `preview` and `has_attachments`,
beside `identity`, `subject`, `status`, `assignee_id` and `message_count`.
They are the newest message's facts, so `folder` is where that message is
filed and moves when you move it.

Fetch the conversation itself with `GET /messages?thread_id=…` when
somebody opens it. The row is for the list.

## Status

| Status         | Means                        |
| -------------- | ---------------------------- |
| `open`         | Nobody has finished with it. |
| `waiting`      | Waiting on somebody outside. |
| `resolved`     | Done.                        |
| `human_review` | A person needs to look.      |

## Owner

| Owner        | Means                       |
| ------------ | --------------------------- |
| `human`      | A person is reading it.     |
| `automation` | Deterministic code owns it. |
| `agent`      | A model owns it.            |

The pair is the coordination primitive. `agent` + `open` is software working
on something; `human` + `human_review` is the same conversation, in somebody's
inbox, waiting for them.

## Hand a conversation to a person

When your software is out of its depth, give the conversation back. It appears
in the inbox, and `thread.updated` — carrying the new owner — tells whatever
was driving that it no longer is.

```bash
curl https://pidgeon.ai/api/v1/threads/feaa4f72-af24-4fa5-ad05-368a0a9217c6 -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "owner_type": "human", "status": "human_review" }'
```

```ts
await threadsUpdate({
path: { id: 'feaa4f72-af24-4fa5-ad05-368a0a9217c6' },
body: { owner_type: 'human', status: 'human_review' },
});
```

```python
httpx.patch(
  "https://pidgeon.ai/api/v1/threads/feaa4f72-af24-4fa5-ad05-368a0a9217c6",
  headers={"Authorization": f"Bearer {os.environ['PIDGEON_API_KEY']}"},
  json={"owner_type": "human", "status": "human_review"},
)
```

The SDK's `handOff` is the same two fields, named. It is one call because it
is one intention, and splitting it into two `PATCH`es is how a conversation
ends up owned by a person but not marked for review.

## Who is answering it

`owner_type` says which *kind* of thing is answering. `assignee_id` says which
**person** — and the two compose rather than replacing one another. A thread
with `status: "human_review"` and no assignee is a handover nobody has taken,
which is the state a queue exists to make visible.

Somebody's id comes from `GET /workspace/members`. `null` puts the
conversation back in the unassigned queue, which is a decision like any other
and emits `thread.unassigned`.

```bash
curl https://pidgeon.ai/api/v1/threads/feaa4f72-af24-4fa5-ad05-368a0a9217c6 -X PATCH -H "Authorization: Bearer $PIDGEON_API_KEY" -H "Content-Type: application/json" -d '{ "assignee_id": "82438cb3-89a0-4c58-a79e-542e091f9ee8" }'
```

```ts
const { data: members } = await workspaceMembers();

await threadsUpdate({
path: { id: 'feaa4f72-af24-4fa5-ad05-368a0a9217c6' },
body: { assignee_id: members.data[0].id },
});
```

```python
httpx.patch(
  "https://pidgeon.ai/api/v1/threads/feaa4f72-af24-4fa5-ad05-368a0a9217c6",
  headers={"Authorization": f"Bearer {os.environ['PIDGEON_API_KEY']}"},
  json={"assignee_id": "82438cb3-89a0-4c58-a79e-542e091f9ee8"},
)
```

List one person's queue with `?assignee_id=`, and the work nobody has picked
up with `?assignee_id=unassigned`. `thread.assigned` and `thread.unassigned`
are both subscribable, so a queue running through Pidgeon can post to Slack or
open a ticket when a conversation changes hands.

Assignment is a property of a **shared** address — one created as a team
mailbox, or an agent's. A person's own inbox has no queue, however many
colleagues they have.

## Taking it back

There is no automatic hand-back. A person who has been given a conversation
keeps it until somebody sets the owner again, which is deliberate: software
deciding on its own that a human is finished is the failure mode this exists
to prevent.

## Pidgeon ships no agent runtime

The API and the webhooks are the substrate you build one on. There is no
prompt, no model and no loop here, and saying otherwise would be the wrong
kind of promise.
