Semantic search over a Markdown knowledge base, served over MCP.
provider = "openai-compatible" (ADR-0022)
では、rebuild_index (grooveseek/src/indexer.rs) が embedding し直すファイルは
1 本ごとに 1 回以上の HTTP リクエストになる。この記録までは、リクエストが 1 つでも
失敗すると failed to embed chunks for <file> で run 全体が止まっていた。walk は毎回
同じ順でファイルを回るので、次の run も同じファイルで止まり、その後ろのファイルは
いつまでも索引されなかった。ループの後に続く段階も走らなかった: 消えたファイルの row を
消す sweep と、宣言フィールドの集合の記録である。
これを起こす失敗は 2 種類あり、性質が違う:
問いは、groove index・MCP rebuild_index・watcher がそれぞれをどう扱うか、それも
設定の誤りを成功に見える run の陰に隠さずに、である。
max_retries だけ。待ち時間は定数。 採用。model / document_model か endpoint を指している。
endpoint が前のバッチを受け付けた後で断ったファイルは、この拒否ではなく受け付けた
側に数える: 受け付けたバッチが設定の正しさを示している。断られた入力はそのファイルを skip し、一時的な失敗は再試行し、拒否しか起きなかった run は最後まで済ませてから失敗にする。
分類 (リクエストごと、grooveseek/src/embedder.rs):
| 起きたこと | 扱い |
|---|---|
| HTTP 400、413、422 | EmbedInputRejected: 再試行しない。indexer はそのファイルを skip する |
| HTTP 429、500-599 | 再試行する |
| status 行が届く前の timeout、接続の失敗 | 再試行する |
| その他の status (401、403、404、408 …) | その場で run を止める |
| 2xx で body が timeout した | 再試行する |
| 2xx で body が途中で切れた、正しい JSON でない、またはベクトルの件数・index・次元が合わない | その場で run を止める |
| status 行が届く前のその他の通信エラー (例えば、サーバが答える前に切った接続) | その場で run を止める |
status 行が届いたら、その行の扱いで決まり、その後に body が止まっても途中で切れても
変わらない: 429・5xx は Retry-After に従って再試行し、400・413・422 はそのファイルを
skip し、その他の 2xx 以外の status は run を止める。body はエラー文に使うだけで、
読めなかったときは空になる。
warning: <file>: embedding endpoint rejected the input (HTTP <status>); skipped, the index keeps what it had for this file
を出し (status だけで、応答 body は出さない)、そのファイルを skipped に数え、既存の
row を残して先へ進む。row の無いファイル (新しいファイル、または reset が索引を空にした
強制再構築でのすべてのファイル) は代わりに skipped, this file is not in the index と
名指しされる。1 ファイルが複数のバッチになり後のバッチが断られたときは、前の
バッチのベクトルは捨て、そのファイルについては何も書かない。[embedding] max_input_chars: openai-compatible 専用、既定 8000、0 は拒否。
送る前に各入力をこの文字数で切る。document と query の両方。database のチャンク本文と
全文索引は切らない。[embedding] max_retries: openai-compatible 専用、既定 3、0 から 10 まで受理
(MAX_EMBEDDING_RETRIES)。送り直す単位はバッチ 1 つで、後のバッチが失敗しても前の
バッチは送り直さない。0 なら 1 回だけ送り、最初のエラーをそのまま返す。最後の試行の
後のエラーは試行の回数を述べる。Retry-After (秒数か HTTP の日時) が 60 秒以下なら、その値
ちょうどを jitter なしで待つ。60 秒を超える Retry-After は待たない: そのバッチはその旨を
述べてすぐ失敗する。使える Retry-After が無ければ 1 秒・2 秒・4 秒…と倍にして 60 秒で
頭打ちにし、その待ちの 4 分の 1 未満の jitter を足す。endpoint・api_key・
request_dimensions・timeout_seconds と同じ扱い。FastEmbed はほかの endpoint 用
キーと同じく両方を拒否する。IndexResult::fails_all_inputs_rejected)。複数バッチ (64 チャンク超) のファイルを
endpoint が前のバッチを受け付けた後で断った場合も同じく skip するが、拒否ではなく
受け付けた側 (IndexResult::embedded) に数えるので、そうした run は失敗にならない。
run を途中で切らない: 削除の sweep と
後処理は完了し、rebuild_index は件数を返す。その上で groove index は exit 非 0 で
終わり、MCP rebuild_index は件数の隣に error を返す。どちらも文言は
IndexResult::all_inputs_rejected_message から取る。watcher は 1 ファイルずつ
索引し直すのでこの規則を当てず、断られたファイルを
watcher: skipped <file> (embedding endpoint rejected the input) と報告する。IndexResult::fails_forced_rebuild_rejections、PR #329 の codex P1 を受けて追加)。
groove index --force と MCP rebuild_index {force: true} は embedding し直す前に索引を
空にするので、断られたファイルには戻る先の row が無い: 索引から抜け落ち、exit 0 はそれを
隠してしまう。他のファイルはすべて索引し、run は上と同じく最後まで済ませる。その上で
同じ 2 つの面が IndexResult::forced_rebuild_rejections_message の文言で失敗する。両方の
規則が当たるときは、拒否しか起きなかった旨の文言を出す。skipped (Done in 行、MCP の stats) に数えられる。max_input_chars を変えても何も embedding し直さない。 この記録より前に作った
索引は、8000 文字より長いチャンクのベクトルを、そのチャンクが変わるか
groove index --force を打つまで持ち続ける。rebuild_index・watcher・
検索はどれもリクエストの間 embedder を握り、再試行の待ちはその中で起きるので、検索も
一緒に待たされる。既定値での 1 バッチの最悪は約 420 秒 (60 秒の timeout を 4 回と、最大
60 秒の待ちを 3 回)、max_retries = 10 で約 1260 秒。どちらも設定からの推定で、実測では
ない。すぐ答える必要のある daemon は max_retries = 0 にする。groove index --force
は、索引に黙って穴を残す代わりに失敗を報告する。ファイルは warning で名指しされ、直すか
max_input_chars を下げて groove index を打てば穴は埋まる。max_input_chars を下げれば
戻る。groove-schema.toml の宣言キーが変わった後、断られたファイルは「読めていない」ものとして
数えられるので、新しい宣言フィールドの集合が記録されず、そのファイルを直すか、
max_input_chars を下げるか、groove index --force を打つまで、fields /
fields_not の filter は断られる。grooveseek/tests/openai_compatible_failures.rs (400 / 413 / 422 での
skip、probe の拒否、再試行、Retry-After、document と query の切り詰め、拒否しか
起きなかった run を CLI と MCP で)、および grooveseek/src/embedder.rs・
grooveseek/src/config.rs・grooveseek/src/indexer.rs の unit test。grooveseek/src/embedder.rs (EmbedInputRejected、
OpenAiCompatibleConfig::with_limits)、grooveseek/src/indexer.rs
(IndexResult::fails_all_inputs_rejected)、
grooveseek/tests/openai_compatible_failures.rs。