feat(room): gate every post on the floor (room, sidecar, screen, protocol 4) - #52
Conversation
Adds `crates/room-floor`, a tauri-free crate holding what has been said and how much of it a participant had seen when they tried to speak. `Floor::admit` is one operation: it resolves the speaker's watermark and appends the post under the caller's single lock acquisition, so two participants speaking at once are serialised and the second is refused rather than delivered blind. 判定と刻印を一つの操作に閉じるための土台。tauri 非依存の crate に置いたのは docs/0-requirements.md 「テストの配置」の方針どおりで、src-tauri 側の純粋 ロジックは GNU ターゲットでテストが起動せず検証不能になるため。同時発話の 順序付けはまさに読んでも確かめられない部分なので、走るテストの側へ置く。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`deliver` now takes the speaker's `last_seen` watermark and returns a `PostOutcome`. The floor check and the append share one acquisition of the existing `Arc<Mutex<RoomInner>>`, so two participants posting at once are serialised: the first one's post is on the floor before the second one's check reads it, and the second is refused rather than delivered blind. A refused post is not delivered to anyone and comes back carrying what was missed. Both callers go through it. The socket loop passes the frame's `last_seen` and answers on the same connection with a new `post_result` frame; `room_post` returns the outcome to the screen. Protocol 4. 判定と刻印を既存ロックの内側に置いた。参加者クラスで書き分けていないため、 画面からの投稿も同じ経路で同じ判定を受ける。宛先による絞り込みは行わず、 見落としが一件でもあれば弾く。`last_seen` は身元の主張ではないので検証せず、 部屋の知らない値は「見ていない」として扱う。`post_result` を返すことで、 ゼロ tool の返信でも呼び出し自体が境界になる。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`room_post` now takes the screen's watermark and returns a `PostOutcome`. The screen tracks the newest post it has drawn and passes it; a refusal puts the typed text back and names who spoke while it was being typed. The person at the keyboard is gated on the same terms as a session, through the same command and the same room-side check. What differs is only that the screen always has a watermark to declare: a line is drawn after the room has admitted it, so the ordering at the race point is real for this participant too, rather than nominal. 画面を例外にすると部屋が参加者クラスで判定することになる(#39)。同じ経路で 同じ判定を受ける形にした。弾かれた場合は入力を戻し、届いていた発言の発言者を 出す。発言そのものは同じイベントで既に画面に並んでいるため、読むのは人間側。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`say_to_room` gains a `last_seen` argument and waits for the room's `post_result` before reporting. Three outcomes, each said plainly: delivered, refused with the posts the agent had not seen and the id to declare next time, or unconfirmed when the room never answered. Answers are correlated by message_id, so a verdict for another post cannot settle this call. Protocol 4. The manners gain the block that makes the argument usable: what to put in last_seen, that a refusal means nothing was posted, that not sending is a valid outcome, and that being refused is the room supplying an order two participants cannot supply themselves. 往復ハーネスは文頭の部分正規表現をやめ、作法を丸ごとの literal で押さえる。 #49 のテストは両アサーションが最初の読点までしか一致しておらず、その尾部は 消しても CI が緑のままだった。現在は #49 の尾部と本 issue の追加分の両方が 固定されており、いずれか一行を削るとテストが落ちることを確認した。加えて last_seen が wire に載ること、未宣言ならキーごと省くこと、弾かれた戻り値が 見落とした発言を全件(id・発言者・内容・宛先)運ぶこと、無応答が「配った」 とも「弾かれた」とも読めないことを検査する。ツールは 1 本のままである。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
「随時」を落とした。配送については正しく、可読については誤りだった一語で、 これが「送信直前に読み直せる」を無条件に読ませ、読む側なしで作法が成立する という判断を支えていた。届いてはいる、読めるのは次の境界から、という形へ 差し替え、tool 結果の境界で文脈へ入ることを明記した。 床の節を追加。二型の失敗(境界が悪い位置にある / 境界がそもそも無い)、 last_seen を送り手が申告する理由(配達は読了ではない)、既存ロック内での 判定と刻印、弾いて返す形を採る理由、安全側の倒し方、参加者クラスで 書き分けないこと、順番の付与への段差を残さないこと。protocol の表に last_seen と post_result を足し、版を 4 とした。 CI に crates/room-floor の cargo test を追加。テストの配置の節に、床が まさに「読んでも確かめられない」側であることを書いた。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
宛先の検査が発言者名だけでも通る形だったため、`Master -> Claude Lay:` の 並びで押さえる。素の名前は別のフィールドで一致してしまう。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
型 2(ゼロ tool の返信)の完了条件は「呼び出し自体が境界を作ることによる」 であり、これまでは拒否の検査に暗黙で乗っているだけだった。フレームが wire に 出たあとも呼び出しが解決していないことを直接押さえる。送信時点で返す実装へ 戻すと落ちることを確認済み。 Refs #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
smileygames
left a comment
There was a problem hiding this comment.
セルフレビュー(auto モード / 人間チェックなし)
完了条件の照合
| #47 の完了条件 | 結果 |
|---|---|
| 見ていない発言がある状態での投稿が配られない。戻り値にその発言群が入る | 満たす。Admission::Unseen で早期 return、fan-out も room-message emit も通らない |
| 見落としが無い投稿は従来どおり配られる | 満たす |
| 二人が同時に呼んだとき、片方だけが通る | 満たす。Barrier による 2 スレッドテストで確認 |
| 型 2(ゼロ tool の返信)で取りこぼしが起きない | 満たす。呼び出しが post_result を待つため、送信自体が境界になる |
docs/0-requirements.md から「随時」が消えている |
満たす。親側で出現回数 0 を確認 |
ツールは say_to_room 一本のまま |
満たす |
| 往復ハーネスが検査、文頭だけを押さえる形にしない | 満たす。下記 |
原子性を親側で確認した
本 PR の要点なので、報告ではなくコードで見た。
Floor::admit(crates/room-floor/src/lib.rs)は watermark 解決・見落とし収集・追記を 1 メソッドで行う。deliver()(src-tauri/src/room.rs)はそれを room.inner.lock() の単一取得の内側で呼び、seat の since と hue も同じ critical section で読む。判定と追記が割り込める形になっていない。
並行テストは Barrier で 2 スレッドを揃え、同一 watermark から admit を呼ぶ。Admitted がちょうど 1 件、もう一方が Unseen で勝者の発言を受け取り、最後に floor.seq() が 2 であることを検査している。最後の 1 行が効いている — 拒否が「配ったうえでの通知」になっていないことを、拒否側の戻り値ではなく floor の状態で固定している。
設計判断の照合
- 解決できない
last_seen→ 位置 0。 制約「部屋の知らない値は見ていないものとして扱う。安全側へ倒す」どおり。発行していない値も、512 件の窓から溢れた値も同じ扱い。 - 未申告 → seat の
since。 途中参加者は自分の接続より前の発言に責任を負わない。配られていないものを見落としと数えない形であり、妥当。 toによる絞り込みなし。 制約どおり。拒否はtoを含めて返すので、参加者側で判断できる。- 自分の発言を見落としに数えない。
entry.origin != originで判定。名前ではなく接続で見ており、#40 の規律を維持している。 - A への段差なし。
Admission::Admitted { seq }にフィールドを足すだけで順番の付与へ広がる。制約どおり。
post_result の待ちを確認した
送信側が応答を待つ設計なので、応答が来ない場合にターンが固まらないかを見た。POST_RESULT_TIMEOUT 15 秒で null へ解決し、unconfirmed として返す。delivered とは主張しない(フレームは届いているかもしれないため)。timer.unref() でプロセスを引き止めない。message_id をキーにした Map で相関しており、並行投稿が混ざらない。ソケット断は abandonPost で解決する。
固まる経路は見当たらない。
対象ファイル超過(2 件、いずれも是認する)
src/main.ts— 制約「参加者クラスで書き分けない。画面からの投稿も同じ経路を通る」の帰結。ゲートが単一deliver()にある以上、画面が申告しなければ 2 通目以降すべて拒否される。Rust 側で画面だけ除外するのは、まさに部屋が参加者クラスで分岐する形になる。拒否時は入力欄へ本文を戻し、誰の発言を見落としたかを status に出している。黙って消えない。crates/room-floor/+.github/workflows/ci.yml— 完了条件「二人が同時に呼んだとき、片方だけが通る」の帰結。CI はsrc-tauriでcargo testを実行しておらず、cargo checkは#[cfg(test)]をコンパイルしない。src-tauri/src/に書いたテストは走らない。docs/0-requirements.mdが既に同じ理由でcrates/mcp-configを分けている。順序性は読んでも確かめられない性質のものなので、走る場所へ置いたのは正しい。
どちらも制約から強制されたものであり、機能の追加ではない。scope-exceed の dialogue confirm を要する形(親の設計意図に反する拡張)ではないと判断する。
#49 のテスト欠陥
再生産されていない。作法は複数行の完全 literal で検査されている(sidecar/test/round-trip.test.mjs 38-40 行が INSTRUCTIONS の構造をそのまま持つ形)。部分正規表現ではない。
実装側が mutation で 3 件確認したと報告しており、うち 「#49 ブロックの尾部行を削ると落ちる」 は、まさに以前 CI 緑のまま消せた箇所である。塞がった。
検証
CI(check / CI)いずれも 9189201 で pass。親側で Floor::admit、deliver() のロック範囲、pump の target 分岐、sidecar のタイムアウト、docs の「随時」除去、CI への新クレート追加を読んで確認した。
実機未確認(明示)
floor 投入後の 2 セッション再観測は未実施。docs の未実装節に記載済み。マージの阻却事由としない。
繰越
#42(lag した参加者が恒久離脱する)が本 PR で悪化しうる。post_result が broadcast のトラフィックをおよそ倍にするため、容量 256 の lag に届きやすくなる。既存 issue のためマージ前起票は不要。#42 へ追記する。
リリース種別
minor と判定する。protocol 3 → 4、新フレーム post_result、拒否という新しい観測可能な挙動(画面には status が出る)。構造変更かつ user/system observable。
自己レビュー通過。auto モードのため人間チェックはなく、このままマージへ進む。
Closes #47
組み立て中に届いた発言を見ないままの投稿を、部屋の側で成立させなくした。送り手が
last_seen(実際に見たいちばん新しい発言のmessage_id)を申告し、部屋が既存ロックの内側で判定と刻印を一つの操作として行う。見落としが一件でもあれば配らず、その発言群を戻り値で返す。形
crates/room-floor/— 床。Floor::admitが watermark の解決と追記を一つの操作として行う。tauri 非依存の crate に置いたのはdocs/0-requirements.md「テストの配置」の方針どおりで、src-tauri側の純粋ロジックは GNU ターゲットでテストバイナリが起動せず検証不能になるため。同時発話の順序付けはまさに読んでも確かめられない部分にあたる。src-tauri/src/room.rs—deliver()がlast_seenを取りPostOutcomeを返す。判定・刻印・hue の読みがArc<Mutex<RoomInner>>の一回の取得に収まっている。socket 経路は投稿した接続にだけpost_resultを返す。protocol 4。sidecar/src/index.ts—say_to_roomにlast_seen引数を足し、post_resultを待って結果を返す。配達・拒否・無応答の三値を区別する。作法に「床を見てから送る」を追加。src/main.ts— 画面も同じ経路・同じ判定を通る。描画した発言のmessage_idを申告し、弾かれたら入力を戻す。完了条件
room-floorの単体テスト(拒否時にseqが進まないことを含む)と往復ハーネス(id・発言者・内容・宛先が全件入ること)で検査。two_speakers_at_once_get_an_order。barrier で揃えた 2 スレッドが同じ watermark でadmitを呼び、通るのが 1 本、もう 1 本には相手の発言が返り、床は 1 件しか増えないことを検査。say_to_roomがpost_resultを待つため、呼び出し自体が境界になる。往復ハーネスは、無応答が「配った」とも「弾かれた」とも読めないことも検査する。docs/0-requirements.mdから「随時」が消え、tool 結果の境界で入ることが記述されている。say_to_room一本のまま — tool 一覧と input schema のキー集合で検査。対象ファイル外に触れた 2 点
issue 本文の対象ファイル(
room.rs/sidecar/src/index.ts/round-trip.test.mjs/docs/0-requirements.md)に加えて、下記へ触れています。いずれも constraints から出た帰結であり、独立した追加ではありません。src/main.ts— constraints「参加者クラスで書き分けない。画面からの投稿も同じ経路を通る」から。判定を単一経路のdeliver()に置いた以上、画面が watermark を申告しないと人間は 2 通目以降ずっと弾かれる。例外にすると部屋が参加者クラスで判定する形になるため、画面側を合わせました。crates/room-floor/と.github/workflows/ci.yml— 完了条件「二人が同時に呼んだとき片方だけが通る」を CI が保持するため。src-tauriのcargo testは CI に無く、cargo checkは#[cfg(test)]を通らないので、そこへ書いたテストは一度も走りません。docs/0-requirements.mdが既に「純粋ロジックをsrc-tauri/src/へ書き足すと書いた時点で検証不能になる」と定めており、それに従っています。未確認
実機での再計測(床を入れた後の同時発話)。CI は静的検査とユニット・往復テストまでで、部屋に 2 セッションを立てた観測は含みません。