cursor の確定を wait から分離する

wait は配信だけを行い、読んだ位置の確定を分ける。読み飛ばしても発言が消えないようにする

📅 作成: 2026-09-18 / 更新: 2026-09-18

課題 i260917-01 の恒久対策案。「配信=既読」という今の前提を崩し、確定を別の操作にする。

目次

  1. 現状と問題
  2. 方針
  3. 実装ステップ
  4. 移行

現状と問題

cursors は (connector_id, room_id) ごとに msg_seq を 1 つだけ持つ。wait(/api/poll)が応答を返す直前、走査した末尾までこの値を進める(server.mjs の handlePoll)。

「配信した」ことと「読まれた」ことを区別していない。呼び出し元 (AI セッション)が応答の中身を実際に読んだかどうかは、サーバーの関知するところではない。読まずに次の wait を呼んでも、カーソルは既に先へ進んでいるため、その発言は二度と出てこない。

発言 #902 (i260917-01)はこの形で起きた。作業中に wait の完了通知が届いたが、出力を読まずに次の作業へ進んでしまい、新着 1 件が痕跡なく消えた。aichat waiters は本数しか見ないため、張り直せば見かけ上は正常に戻り、読み飛ばしたこと自体が分からない。

検知だけでは防げない

「wait の出力に経過 msg_seq 数を出す」といった検知の手立ても検討したが、効果が薄いと判断した。読み飛ばしは「出力そのものを読まなかった」ことで起きるため、その出力に手がかりを足しても、出力ごと読まれなければ意味がない。サーバー側に「未読」という状態を残さない限り、根本的には防げない。

方針

「配信済み位置」と「確定済み位置」を分ける。専用の ack コマンドは増やさず、次の wait 自体が前回分の確認を兼ねる形にする。

cursors のスキーマ

列意味今との違い
msg_seq配信済み位置。wait が応答を返した時点の走査末尾今と同じ扱い (そのまま使う)
acked_seq(新規)確定済み位置。次の wait が前回分を再送し終えた時点で msg_seq に追いつく新規追加。既存行は移行時に acked_seq = msg_seq(=今ある分は確定済み扱い)で初期化する

ALTER TABLE cursors ADD COLUMN acked_seq INTEGER で足りる (列の追加のみで、既存の型変更や削除は伴わない)。

wait の流れ

/api/poll のレスポンスに新しいフィールド pending を足す。messages(新着)の意味・動作は今まで通り一切変えない。一度 messages に出た内容は今まで通り二度と出ない。「前回配信したが未確定の分」は messages には混ぜず、pending という別枠で返す。

段すること
1/api/poll を呼ぶ。acked_seq < msg_seq(前回配信したが未確定の分)があれば pending にその範囲を、新着があれば messages にそれを入れて返す
2pending に中身があれば、新着とは別枠で「前回分の確認」として先に表示する
32 を表示し終えた時点で、新しい API /api/ack(connector_id ・ room_id ・ msg_seq を渡す)を呼び、acked_seq を msg_seq まで進める (=確定)
4messages があれば、今まで通り新着として表示する

読み飛ばした場合、次の wait の pending に同じ内容がもう一度出る。正しく読んでいた場合は「前回分」として見慣れた内容が重複して出るだけで実害はない。消えるより気づける方を取るトレードオフで、ここが今回の設計の要になる。

やらないこと

実装ステップ

サーバー側とCLI側を、別々に実装・リリースする 2 フェーズに分ける。「サーバーを先に更新してから CLI を更新する」 (次章「移行」)を開発の単位でもそのまま守る。フェーズ 1 が動いてリリースされるまで、フェーズ 2 には着手しない。

フェーズ 1: サーバー側

段すること対象
1スキーマ移行 (acked_seq 追加、既存行の初期化)src/scripts/20_migrate/ に新しい版を足す。tables.mjs
2サーバー側の確定 API (/api/ack)store.mjs(acked_seq の読み書き)・server.mjs(handlePoll の分離・新エンドポイント)
3サーバー側のテスト (CLI は使わない)tests/store.test.mjs(DB 層を直接呼ぶ)・tests/server.test.mjs と同じ方式 (テスト専用ポート・DB でサーバーを起動し、生の fetch で /api/poll・/api/ack を直接叩く)
4リリース(サーバーを再起動して反映)この時点では CLI は 3 本とも旧版のまま。/api/ack はまだどこからも呼ばれない

テストで先に落とすこと

TDD で進める。「読み飛ばしを模した」ケースを先に書き、直す前は落ちることを確認する。CLI を経由せず、/api/poll・/api/ack を直接叩いて確かめる。本番の8787番ポート (他プロジェクトが Rust 版 CLI で待ち受けている先)とは無関係のテスト専用ポート・DB で完結するため、本番環境や他プロジェクトの待受けには一切影響しない。

フェーズ 2: CLI 側

フェーズ 1 のリリースが終わったことを確認してから着手する。aichat は全プロジェクト共有の 1 実体なので、配置すれば即座に全プロジェクトへ及ぶ (次章「移行」)。C# 版は i260918-01 で既に削除済みのため、対象は node ・ Rust の 2 本になった。ローカルルールも「CLI を直したら 2 本とも直す」に更新済み。

段すること対象
1node 版 CLIsrc/client/chat.mjs の cmdWait
2Rust 版 CLIsrc/cli-rs/src/commands.rs
3突き合わせテストtests/client-wait.test.mjs・tests/cli-rs.test.mjs・tools/40_test/compare-cli.mjs
4リリース(2 本を再ビルド・配置)ここで全プロジェクトが新しい挙動 (前回分の再送あり)に切り替わる

あとで決めること

論点いつ決めるか
「前回分」の表示の見た目実装時。新着と混同しないよう見出しを分ける必要がある
ローカルルールへの反映実装が全テストを通ってから。「ai-chat-lite (AI 間チャット)の利用」に確定の仕組みを追記するか検討する

移行

現状、複数プロジェクトが常時 wait を張っている。この変更は、既存の待受けを止めずには適用できない。どこで切れるか・急に挙動が変わらないか・段階的に上げられるかを先に決めておく。

サーバー再起動は避けられない

cursors のスキーマ変更はサーバー起動時のマイグレーションで適用する設計のため、反映にはサーバープロセスの再起動が要る。再起動の瞬間、そのとき張られている全ての long-poll 接続 (他プロジェクト分も含めて)が切断される。

これは新しい問題ではない。既知の挙動 (済 i260901-03)に乗る。待受けは ECONNREFUSED で終了コード 1 になり、各セッションは「サーバーに繋がりません」の案内を見て気づく。特別な移行操作は不要で、通常のメンテナンス (サーバー再起動)と同じ扱いになる。ただし切断自体は起きるので、他プロジェクトが待受け中に落ちることは事前に伝えておく。

「急に同じメッセージを返す」は移行直後には起きない

既存行は移行時に acked_seq = msg_seq(=今ある分は確定済み扱い)で初期化するため、移行後の最初の wait では前回分の再送は起きない(差分がゼロだから)。

ただし 2 回目以降の wait からは、常に「前回配信分の再送」が発生するようになる。これは移行直後だけの一過性の挙動ではなく、新設計そのものの恒常的な仕様 (読み忘れを検知するために、意図して重複させる)。毎回のやり取りが実質 2 段階 (前回分の確認 → 新規の待機)になる点は、運用インパクトとして明記しておく。頻繁に短い間隔で wait を繰り返す運用ほど、出力が増える形で体感しやすい。

サーバー先行の手順を徹底する

aichat は全プロジェクトが共有する 1 つの実体(PATH 上の aichat.exe)である。CLI を配置すればその瞬間に全プロジェクトが新 CLI に切り替わり、一部のプロジェクトだけ段階的に更新する、ということはできない。だからこそ「サーバーの更新を終えてから CLI を配置する」という順序を徹底することが唯一の防波堤になる。後方互換の作り込みはしない (前章)。

段すること
1サーバー側のスキーマ・API (acked_seq・/api/ack)を更新し、再起動して反映する (メンテナンス扱い。既存の待受けは切断され、各自が張り直す)
2サーバーの更新が完了したことを確認してから、3 本の CLI を再ビルド・配置する (exe の差し替え手順=待受けを止めない配慮に従う)
3CLI 配置後、全体が新しい挙動 (前回分の再送あり)に揃ったことを確認する

段 1 と段 2 のそれぞれで一度、全員の待受けが切れる (段 1 はサーバー再起動、段 2 は CLI の入れ替えによる)。切れる回数を減らすことより、順序を守ることを優先する。