Skip to content

feat(room): gate every post on the floor (room, sidecar, screen, protocol 4) - #52

Merged
smileygames merged 7 commits into
mainfrom
issue-47-see-the-floor
Aug 23, 2026
Merged

feat(room): gate every post on the floor (room, sidecar, screen, protocol 4)#52
smileygames merged 7 commits into
mainfrom
issue-47-see-the-floor

Conversation

@smileygames

Copy link
Copy Markdown
Member

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.rsdeliver()last_seen を取り PostOutcome を返す。判定・刻印・hue の読みが Arc<Mutex<RoomInner>> の一回の取得に収まっている。socket 経路は投稿した接続にだけ post_result を返す。protocol 4。
  • sidecar/src/index.tssay_to_roomlast_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 件しか増えないことを検査。
  • 型 2(ゼロ tool の返信)で取りこぼしが起きない — say_to_roompost_result を待つため、呼び出し自体が境界になる。往復ハーネスは、無応答が「配った」とも「弾かれた」とも読めないことも検査する。
  • docs/0-requirements.md から「随時」が消え、tool 結果の境界で入ることが記述されている。
  • ツールは say_to_room 一本のまま — tool 一覧と input schema のキー集合で検査。
  • 往復ハーネスが上記を検査している、部分正規表現で文頭だけを押さえる形にしない — 作法は丸ごとの literal で押さえた。feat(sidecar): put turn-taking into the room manners #49 の 2 本は最初の読点までしか一致しておらず尾部を消しても緑のままだったが、現在は feat(sidecar): put turn-taking into the room manners #49 の尾部と本 issue の追加分のどちらを一行削ってもテストが落ちる(実際に削って確認済み)。

対象ファイル外に触れた 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-tauricargo test は CI に無く、cargo check#[cfg(test)] を通らないので、そこへ書いたテストは一度も走りません。docs/0-requirements.md が既に「純粋ロジックを src-tauri/src/ へ書き足すと書いた時点で検証不能になる」と定めており、それに従っています。

未確認

実機での再計測(床を入れた後の同時発話)。CI は静的検査とユニット・往復テストまでで、部屋に 2 セッションを立てた観測は含みません。

Claude Lin & Lay and others added 6 commits August 23, 2026 09:21
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>
@smileygames smileygames linked an issue Aug 23, 2026 that may be closed by this pull request
@smileygames smileygames self-assigned this Aug 23, 2026
型 2(ゼロ tool の返信)の完了条件は「呼び出し自体が境界を作ることによる」
であり、これまでは拒否の検査に暗黙で乗っているだけだった。フレームが wire に
出たあとも呼び出しが解決していないことを直接押さえる。送信時点で返す実装へ
戻すと落ちることを確認済み。

Refs #47

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@smileygames smileygames left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

セルフレビュー(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::admitcrates/room-floor/src/lib.rs)は watermark 解決・見落とし収集・追記を 1 メソッドで行う。deliver()src-tauri/src/room.rs)はそれを room.inner.lock() の単一取得の内側で呼び、seat の sincehue も同じ 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-tauricargo 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::admitdeliver() のロック範囲、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 モードのため人間チェックはなく、このままマージへ進む。

@smileygames
smileygames merged commit 6cb7377 into main Aug 23, 2026
2 checks passed
@smileygames
smileygames deleted the issue-47-see-the-floor branch August 23, 2026 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(room): let a participant see the floor before speaking

1 participant