課題 i260917-01 の恒久対策案。「配信=既読」という今の前提を崩し、確定を別の操作にする。
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 自体が前回分の確認を兼ねる形にする。
| 列 | 意味 | 今との違い |
|---|---|---|
msg_seq | 配信済み位置。wait が応答を返した時点の走査末尾 | 今と同じ扱い (そのまま使う) |
acked_seq(新規) | 確定済み位置。次の wait が前回分を再送し終えた時点で msg_seq に追いつく | 新規追加。既存行は移行時に acked_seq = msg_seq(=今ある分は確定済み扱い)で初期化する |
ALTER TABLE cursors ADD COLUMN acked_seq INTEGER で足りる (列の追加のみで、既存の型変更や削除は伴わない)。
/api/poll のレスポンスに新しいフィールド pending を足す。messages(新着)の意味・動作は今まで通り一切変えない。一度 messages に出た内容は今まで通り二度と出ない。「前回配信したが未確定の分」は messages には混ぜず、pending という別枠で返す。
| 段 | すること |
|---|---|
| 1 | /api/poll を呼ぶ。acked_seq < msg_seq(前回配信したが未確定の分)があれば pending にその範囲を、新着があれば messages にそれを入れて返す |
| 2 | pending に中身があれば、新着とは別枠で「前回分の確認」として先に表示する |
| 3 | 2 を表示し終えた時点で、新しい API /api/ack(connector_id ・ room_id ・ msg_seq を渡す)を呼び、acked_seq を msg_seq まで進める (=確定) |
| 4 | messages があれば、今まで通り新着として表示する |
読み飛ばした場合、次の wait の pending に同じ内容がもう一度出る。正しく読んでいた場合は「前回分」として見慣れた内容が重複して出るだけで実害はない。消えるより気づける方を取るトレードオフで、ここが今回の設計の要になる。
ack コマンドは追加しない。毎回の運用に確定という追加の一手間を挟むと、通常の「wait → 読む → 張り直す」という単純な流れが崩れる。次の wait 自体が確定を兼ねる方が、既存の運用を変えずに済む。/api/ack は CLI が内部で自動的に呼ぶだけで、利用者がコマンドとして打つものではない/api/ack 自体の後方互換のための作り込みはしない。新しいエンドポイントとして素直に増やす。サーバーを先に更新してから CLI を更新する順序を徹底し、旧サーバーに新 CLI が繋ぐ組み合わせ自体を起こさない (詳細は次章「移行」)。一方 pending は /api/poll の応答に足すだけの新フィールドなので、旧 CLI は素通りする (作り込み不要の自然な後方互換)サーバー側とCLI側を、別々に実装・リリースする 2 フェーズに分ける。「サーバーを先に更新してから CLI を更新する」 (次章「移行」)を開発の単位でもそのまま守る。フェーズ 1 が動いてリリースされるまで、フェーズ 2 には着手しない。
| 段 | すること | 対象 |
|---|---|---|
| 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 で完結するため、本番環境や他プロジェクトの待受けには一切影響しない。
/api/poll で新着 A が messages に入って返ったあと、確定させずに 2 回目の /api/poll を呼ぶと、A が今度は pending に入って返ること (messages には出ない)/api/ack で確定し、3 回目の /api/poll を呼んでも A は pending に出ない (確定済みのため)ことacked_seq が無い状態からの移行)は、移行直後は pending が常に空であることtests/server.test.mjs)は無修正のまま通ること。messages の契約を変えていないことの確認になるフェーズ 1 のリリースが終わったことを確認してから着手する。aichat は全プロジェクト共有の 1 実体なので、配置すれば即座に全プロジェクトへ及ぶ (次章「移行」)。C# 版は i260918-01 で既に削除済みのため、対象は node ・ Rust の 2 本になった。ローカルルールも「CLI を直したら 2 本とも直す」に更新済み。
| 段 | すること | 対象 |
|---|---|---|
| 1 | node 版 CLI | src/client/chat.mjs の cmdWait |
| 2 | Rust 版 CLI | src/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 の差し替え手順=待受けを止めない配慮に従う) |
| 3 | CLI 配置後、全体が新しい挙動 (前回分の再送あり)に揃ったことを確認する |
段 1 と段 2 のそれぞれで一度、全員の待受けが切れる (段 1 はサーバー再起動、段 2 は CLI の入れ替えによる)。切れる回数を減らすことより、順序を守ることを優先する。