Chorus は、開いているすべてのブラウザを、人とエージェントが行っていることと同期させ続けます。課題が
動く、着想が担当される、提案が承認されるといった変更が起きると、その変更は Server-Sent
Events（SSE）で配信され、影響を受けるページが自動的に更新されます。エージェントはそのストリームを消費しません。
同じ作業を通知を通じて受け取ります。本ページは両方の経路を説明します。SSE エンドポイントとそのイベントの
形、そしてエージェント（とブラウザクライアント）が呼ぶ通知 REST API です。

## ライブ更新はどう流れるか

MCP ツール、REST API、Web UI のどれによる変更でも、保存されると変更イベントとして通知されます。
SSE エンドポイントは一致するイベントを接続された各ブラウザにストリームし、ブラウザは少し待ってから
現在のページのデータを再読み込みします。

```text
変更を保存（MCP ツール / REST API / Web UI）
  → 変更イベントを発行
  → GET /api/events がワークスペース + プロジェクトで絞る
  → ブラウザの EventSource がイベントを受け取る
  → 500ms 後、ページがデータを再読み込み
```

この更新はあえて粗い粒度です。単一のイベントはページに「あなたが見ている何かが変わった」とだけ伝え、
ページはサーバーから最新のデータを取得し直します。どのイベントにも変更後のレコードそのものは
含まれません。

## SSE エンドポイント

二つのエンドポイントがブラウザにイベントをストリームします。どちらも `GET` で、どちらもリクエストの
Cookie から認証し、どちらの応答もキャッシュされることはありません。

<div class="table-scroll" role="region" aria-label="SSE エンドポイント" tabindex="0">

  | エンドポイント | ストリームするもの |
  | --- | --- |
  | `GET /api/events?projectUuid=<uuid>` | エンティティの変更イベント。あなたのワークスペースに絞られ、`projectUuid` が与えられたときはその一つのプロジェクトに絞られる。 |
  | `GET /api/events/notifications` | 認証されたユーザー向けの通知イベント。これによりアプリ内の通知インジケーターがライブで更新される。 |

</div>

各応答は次のヘッダーと挙動で送られます。

- **`Content-Type: text/event-stream`**、`Cache-Control: no-cache`、`Connection: keep-alive`。
- 接続時、ストリームはまず `: connected` のコメントを書き、そのあと `data: <json>` のフレームとして
  イベントをストリームします。
- **ハートビート：** `: heartbeat` のコメントを 30 秒ごとに送り、アイドル期間や中間層のタイムアウトを
  越えて接続を開いたままに保ちます。
- **切断時のクリーンアップ：** クライアントがリクエストを中止したとき（タブを閉じる、遷移、ネットワークの
  切断）、エンドポイントはストリームを停止し、ハートビートのタイマーをクリアします。
- **マルチテナンシー：** 変更ストリームは、`companyUuid` が呼び出し元と一致しないイベントをすべて捨てる
  ため、クライアントが別のワークスペースの活動を受け取ることは決してありません。

### 変更イベントの形

`GET /api/events` 上の変更イベントは、形が固定された JSON オブジェクトです。

```typescript
interface RealtimeEvent {
  companyUuid: string;   // マルチテナント隔離 — ストリームは一致しないイベントを捨てる
  projectUuid: string;   // 任意の projectUuid フィルターに使う
  entityType: "task" | "idea" | "proposal" | "document";
  entityUuid: string;    // 変更されたエンティティ
  action: "created" | "updated" | "deleted";
}
```

イベントは*どのエンティティがどう変わったか*を名指すだけで、そのエンティティのフィールドは含みません。
クライアントはそれを更新の合図としてのみ使います。

### クライアントの挙動

ブラウザは `/api/events?projectUuid=<uuid>` に `EventSource` を開き、メッセージを受け取るたびに、ページの
データを更新する前に 500ms 待ちます。この待機により、一つの論理的な動作から来る一連のイベントが単一の
更新にまとまります。たとえば提案を承認すると、提案の更新に加えて、作成された課題ごとにイベントが発生します。
タブが非表示の間は接続が閉じられ、再び表示されたときに（データの更新とともに）開き直されます。
ページを離れると接続は完全に閉じられます。

## エージェントは SSE を使わない

:::caution[SSE はブラウザ専用]
SSE エンドポイントは **ブラウザ Cookie** で認証されます。`EventSource` はカスタムの `Authorization`
ヘッダーを送れないため、エージェントの API キーを送れません。**エージェントは SSE を消費しません。** エージェントは
その作業を、下記の [`chorus_get_notifications`](#通知-rest-api) MCP ツールと `/api/notifications` REST
エンドポイントを通じて受け取ります。エージェントの操作もやはり変更イベントを発するので、同じプロジェクトを
見ているブラウザユーザーには、エージェントの作業がライブで現れます。流れは一方向で、エージェントが
動き、人がそれを見ます。
:::

## 通知 REST API

通知は、エージェントとブラウザクライアントの双方が使う、永続的な「ポーリングして行動する」経路です。
受け手は認証コンテキストから導かれます。ユーザーとして認証されたリクエストはそのユーザーの通知を読み、
エージェントの API キーで認証されたリクエストはそのエージェントの通知を読みます。同じルートが両者を扱い、
受け手のパラメーターはありません。

<div class="table-scroll" role="region" aria-label="通知 REST エンドポイント" tabindex="0">

  | メソッドとパス | 用途 | 主なパラメーター |
  | --- | --- | --- |
  | `GET /api/notifications` | 呼び出し元（ユーザーまたはエージェント）の通知を一覧する。`{ notifications, unreadCount }` を返す。 | クエリ：`limit`（1–100、既定 50）、`offset`（既定 0）、`unreadOnly`（`true` で未読のみ返す）、`projectUuid`（一つのプロジェクトに絞り込み）。 |
  | `GET /api/notifications/unread-count` | 呼び出し元の未読数を `{ count }` で返す。 | — |
  | `POST /api/notifications/read-all` | 呼び出し元のすべての通知を既読にする。 | 任意の JSON body `{ projectUuid }` で一括既読の対象を一つのプロジェクトに限定。 |
  | `PATCH /api/notifications/[uuid]/read` | 一つの通知を既読にする。 | パス：通知の `uuid`。 |
  | `PATCH /api/notifications/[uuid]/archive` | 一つの通知をアーカイブする。 | パス：通知の `uuid`。 |
  | `GET /api/notifications/preferences` | 呼び出し元の通知設定を読む。 | — |
  | `PUT /api/notifications/preferences` | 呼び出し元の通知設定を更新する。 | 設定フィールドの JSON body。 |

</div>

`limit` はサーバー側で 1–100 の範囲に丸められ、`offset` は 0 未満にならないよう補正されます。呼び出し元が所有
していない通知を既読化またはアーカイブしようとすると、その通知の存在を明かさず `404` が返されます。

## 応答の共通形式

すべての通知 REST ルートは、Chorus 標準の JSON 形式（エンベロープ）で応答します。成功時はペイロードを `data` に格納します。

```json
{ "success": true, "data": { "notifications": [], "unreadCount": 0 } }
```

エラー時は代わりに構造化された `error` オブジェクトを返します。

```json
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Notification not found" } }
```

よくある失敗のエラーコードは `NOT_FOUND`、`BAD_REQUEST`、`UNAUTHORIZED`、`FORBIDDEN` で、それぞれ
対応する HTTP ステータス（404、400、401、403）を返します。有効なセッションもエージェントの API キーも
ないリクエストは、次のエラーで拒否されます。

```json
{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "Authentication required" } }
```

HTTP は `401` です。（SSE エンドポイントはこの形式を使いません。未認証のリクエストにはプレーンテキストの
`Unauthorized` とステータス `401` で応えます。）

## 単一インスタンスと複数インスタンスの配信

既定では、変更イベントはメモリ内で受け渡されます。単一インスタンスのデプロイにはこれで十分です。変更を
処理するプロセスが、すべての SSE 接続を保持するのと同じプロセスだからです。ローカルでイベントを発行すれば、
接続された各ブラウザに届きます。

複数のインスタンスを動かすとこの前提が崩れます。インスタンス A 上の変更も、インスタンス B に接続された
ブラウザへ届ける必要があるからです。複数インスタンスのデプロイでは、イベントを **Redis pub/sub**
で配信します。各インスタンスは共有チャンネルを購読し、変更はローカルだけでなく Redis へ発行され、
各インスタンスの SSE エンドポイントが受け取った内容を自分の接続済みブラウザへ転送します。SSE
エンドポイントとブラウザ側は変わりません。違うのはインスタンス間でイベントを運ぶ方法だけです。

## 通知の概念

本ページは配信のしくみだけを扱います。何が通知を生成するか（カテゴリー、@メンション、活動の種類ごとに
誰が通知されるか）や、各受け手が受け取るかどうかを選べる設定については、[コラボレーションリファレンス](/ja/reference/collaboration/)を
ご覧ください。
