feat(room): one post frame, so a participant is a participant - #41
Conversation
部屋のフレームを `post` 一種類に統一し、人間の発言と AI の発言を同じ 行為として扱う。旧 `say` / `reply` の非対称は配送行の書き忘れではなく 語そのものの形だったため、語を落とした。 - `deliver()` を唯一の投稿経路とし、fan-out と `room-message` emit を 1 箇所へ集約。人間 / AI の分岐が経路から消える。 - 自分の発言の抑止は接続の同一性で判定する。接続ごとに room 側で origin を採番し、fan-out のタグとして frame の「外」に載せる。 wire に出ないため送信側は詐称も設定もできない。名前では判定しない (名前衝突中に他人の発言まで落ちるため / #40)。 - `speaker` は frame から読まず、hello で登録した接続の名前を room が 刻む。名簿と表示が食い違わない。 - 名簿を `participants` へ改称し、画面の人間を `room_join` / `room_post` で着席させる。人間が宛先として名指せるようになる。 - `RoomMessage.kind`(human / agent)を廃止し、表示軸を `own` (自分か他人か)へ置き換える。 - protocol を 2 へ。同梱パス起動により版の食い違いが構造上無いため 旧フレームの互換は残さない。 Refs #39
サイドカーを protocol 2 の `post` フレームへ合わせる。 - 送信フレームから `agent` を落とす。発言者は接続から room が刻むため、 自分を名乗る欄そのものを持たない。宛先は名乗れて自分は名乗れない、 という非対称が詐称の余地を消す。 - `hello` の `agent` を `name` へ。受信は `say` から `post` へ。 - `instructions` に「人間と AI を区別しない」前提を明記し、宛先の説明を 参加者一般に対して書き直す。人間も同じやり方で名指せることを示す。 - 自分の発言の抑止はサイドカー側に置かない。room が接続で落とすため こちらは自分を認識する必要がなく、名前で判定しないことが #40 の 名前衝突時に他人の発言を落とさない条件になる。 - channel meta の `user` キーは host 側の契約なので維持し、wire の `speaker` からこの境界で読み替える。 テストは `post` 形へ移行し、次を追加した。 - 送信フレームが `speaker` を持たないこと - 自分と同名の参加者の発言が抑止されずに channel へ届くこと(#40) - `instructions` が人間 / AI を区別しない前提を述べていること Refs #39
画面側を参加者モデルへ合わせる。 - `RoomMessage.kind`(human / agent)を廃止し `own` を使う。表示の軸は 「自分か他人か」であり、人間か AI かではない。`styles.css` の `[data-kind="human"]` を `[data-own="true"]` へ。 - `room_join` で起動時と改名時に人間を名簿へ着席させる。発言していなくても 名簿に並び、他の参加者から名指せる。 - 宛先の選択肢を「自分以外の参加者」とする。人間もセッションも同じ列に 並び、指定の仕方は変わらない。 - `room_say` → `room_post`、`room_agents` → `room_participants`、 `room-agents` → `room-participants`。 Refs #39
`docs/0-requirements.md` を参加者モデルで書き直す。 - `## purpose` に「投稿モデル」節を追加。掲示板に近い場であること、 参加者はひと種類であること、発言はひとつの行為であること、自分の発言は 戻らないこと、宛先は誰でも名指せることを前提として書く。旧 `say` / `reply` の非対称が語の形の問題だった経緯も残す。 - 会話ループ図を参加者起点へ。人間用と AI 用の道が無いことを明記。 - フレーム表を `hello` / `post` の 2 種類へ。`speaker` を部屋が接続から 刻む理由、origin が wire に出ないため詐称できない理由、名前で判定 しない理由(#40)を記述。 - 部屋の作法に、`instructions` が人間 / AI を区別しない前提を述べる 必要があること、宛先の相手による違いが無いことを追加。 - 実装状況を更新。実機での複数セッション往復は未確認であり、2026-08-22 の実測は修正前のものだと明示する。 - 切り分け手順 3 を補正。人間が発言前から名簿に載るため、参加者表示が 空でないことは接続の根拠にならない。 Refs #39
smileygames
left a comment
There was a problem hiding this comment.
セルフレビュー(auto モード / 人間チェックなし)
完了条件の照合
| #39 の完了条件 | 結果 |
|---|---|
| 参加者 A の発言が参加者 B へ届く。人間と AI のどちらでも同じように届く | 満たす(コードレベル)。deliver() が唯一の投稿経路であり、room_post と socket の "post" 腕の両方がそこを通る |
| 参加者 A に自分の発言が戻らない | 満たす。fan-out 時に発信元の接続を飛ばす |
宛先が誰の発言でも meta を通じて届く |
満たす。往復ハーネスで固定 |
| 名簿に人間が並び、宛先として名指せる | 満たす。room_join で起動時・改名時に着席、宛先候補は自分以外 |
| 発言の経路に人間 / AI の分岐が残っていない | 満たす。RoomMessage.kind を廃止し own(自分か他人か)へ |
docs/0-requirements.md が掲示板の前提と投稿モデルを記述 |
満たす |
制約の照合
- 人間 / AI の区別を持たせない — 満たす。参加者クラスを値域に取るフィールドは残っていない。
ownは viewer 相対の軸であり speaker の属性ではない。 - 配送を宛先で絞らない — 満たす。テストで固定。
- 自分の発言の抑止を接続の同一性で判定 — 満たす。詳細は下記。
- 人間を名簿に載せ、宛先は自分以外 — 満たす。
- 画面への配送経路は一本 — 満たす。
deliver()に集約。 - 表示上の区別は「自分か他人か」 — 満たす。
[data-kind="human"]→[data-own="true"]。
指示より強い形になっている点(是認する)
自己エコー抑止について、親は「origin を部屋側で接続から導出し、送信側の値を上書きせよ」と指示した。実装は origin をフレームの外側(Fanout タグ)に置き、シリアライズしていない。送信側は自分の origin を設定することも他人のそれを騙ることもできず、上書きという操作自体が不要になっている。指示の軸(接続同一性・部屋側導出・詐称不可)を満たしたうえで詐称面をゼロにしており、指示より強い。是認する。
同じ理屈が speaker にも適用されている。サイドカーは speaker を送らず、部屋が hello の名前から刻む。テストが "speaker" in post === false を固定しており、この不変条件は回帰で壊れない。
スコープ
crates/mcp-config/ の LIPLUS_AGENT_NAME と既定表示名 "human" を残した判断は妥当。どちらも参加者名という #40 の軸であり、#39 の対象ファイル列挙にも完了条件にも無い。src-tauri/src/lib.rs はコマンド改名に機械的に強制される変更であり、scope-exceed には当たらない。
検証
CI(CI / check)いずれも ebedb59 で pass。往復ハーネスに 3 件追加されており、特に「自分と同名の参加者の発言が抑止されずに届くこと」が #40 の名前衝突ハザードに対する回帰ガードになっている。
実機未確認(明示)
2026-08-22 の実測(2 セッションが互いの発言を受け取らない)は修正前のもの。修正後の実機往復は未計測であり、docs の実装状況と PR 本文に明記されている。完了条件はコードレベルで満たすが、実機観測は Master の側にある。この未確認はマージの阻却事由としない。auto モードの設計上、実機検証は継続観測であって単発のゲートではない。
繰越(マージ前に起票済み)
#42 — 配送ポンプが broadcast の lag で恒久離脱する。while let Ok(..) = recv().await が RecvError::Lagged で抜けるため、遅延した参加者がソケットを開いたまま無言になる。#39 以前からある形だが、全参加者への fan-out が動き出すこの PR で露出確率が上がるため、同時に起票した。本 PR の範囲外。
リリース種別
minor と判定する。プロトコルの版が 1 → 2 へ上がり、フレームの形と画面の挙動(名簿に人間が並ぶ、色分けの軸が変わる)がいずれも観測可能に変わる。構造変更かつ user/system observable であり、rules/operations/release-version-rule.md の minor に当たる。
自己レビュー通過。auto モードのため人間チェックはなく、このままマージへ進む。
Closes #39
何をしたか
部屋のプロトコルを、人間と AI を同じ参加者として扱う投稿モデルへ移した。発言は誰が出しても同一の行為であり、部屋は発言者以外の全員へ配る。
旧プロトコルは人間の発言を
say、AI の発言をreplyとして別の行為に分けており、replyには他の参加者へ配る線がそもそも引かれていなかった。並列に立てた 2 セッションが互いの発言を受け取らないという実測はその帰結である。配送行を 1 行足すのではなく、非対称を生んでいた語そのものを落とした。主な変更
フレームの統一(
src-tauri/src/room.rs,sidecar/src/index.ts)say/replyをpost一種類へ統合し、protocol を 2 へ上げた。同梱パス起動により部屋とサイドカーの版が食い違う形が構造上無いため(#36)、旧フレームの互換は残していない。deliver()を唯一の投稿経路とし、fan-out とroom-messageemit をそこへ集約した。人間用の道と AI 用の道という分岐が経路から消えている。発言者の詐称不可
speakerはサイドカーが送らない。部屋がhelloのnameから、フレームの届いた接続に紐づけて刻む。宛先は名指せるが自分は名乗れない、という非対称が、名簿と表示の食い違いと成りすましの両方を構造的に潰している。自分の発言の抑止
部屋が接続ごとに origin を採番し、fan-out のときにその発言を出した接続だけを飛ばす。origin はフレームの外側をタグとして流れ、wire には出ない。したがって送信側は自分の origin を設定することも他人の origin を騙ることもできない。
名前では判定していない。名前が衝突している間に他の参加者の発言まで落ちるためである(#40)。サイドカー側に自己判定は置いていない。
名簿と宛先(
src-tauri/src/room.rs,src/main.ts,index.html)agentsをparticipantsへ改め、room_joinで人間を起動時と改名時に着席させた。発言していなくても名簿に並び、他の参加者から名指せる。宛先の選択肢は「自分以外の参加者」であり、人間もセッションも同じ列に並ぶ。表示の軸(
src/main.ts,src/styles.css)RoomMessage.kind(human / agent)を廃止しownに置き換えた。[data-kind="human"]は[data-own="true"]へ。表示上の区別は残るが、軸は「自分か他人か」である。作法(
sidecar/src/index.ts)instructionsに「人間と AI を区別しない」前提を明記した。フレームから区別を消しても、作法が「人間に答えよ」と言っていればエージェントはもう無い区別を読む。判定材料と判定規律は同時に動かす。完了条件との対応
deliver()が両経路から fan-outmetaを通じて届くpost.to→meta.to(両方向)room_join+ 自分以外を宛先候補にdeliver()## purposeに「投稿モデル」節、フレーム表と作法を書き直しテスト
sidecar/test/round-trip.test.mjsをpost形へ移行し、次を追加した。speakerを持たないこと(自分を名乗れない)instructionsが人間 / AI を区別しない前提を述べていることローカルで CI の 5 コマンドすべて green。
cargo checkは警告 0。未確認
実機での複数セッション往復は未確認である。2026-08-22 の実測(2 セッションが互いの発言を受け取らない)は修正前のものであり、修正後の再計測は済んでいない。docs の実装状況にもそう明記した。
範囲について
crates/mcp-config/のLIPLUS_AGENT_NAMEは改名していない。フレーム欄でも配送経路でもなく起動パラメータであり、issue の対象ファイルにも完了条件にも含まれないため。画面の既定表示名"human"も同様に据え置いた。参加者名は #40 が持つ軸である。