Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ commit diff poller は 2-phase 構成:

1 repo 1 run あたりの上限は、各 phase が 2 軸で持つ: commit 数 5 件と、file 数 `diffFileBudgetPerPhase`。`processAndUpsertCommitDiff` の upsert は `(repo, commit_sha, file_path)` で idempotent なので、webhook / 両 phase 間で overlap しても副作用はない。

commit 数は phase の消費量を縛れない。1 file の index は worst case で 3 subrequest(D1 FTS mirror 書き込み、store row、および embed batch の按分——1 batch は 2 subrequest を使い、`MAX_EMBEDDING_INPUT_CHARS` が 1 input を 8000 文字に切る一方 batch 予算は 30000 token なので、1 batch は最低 3 file を載せる)であり、1 commit は最大 300 file を運ぶため、5 commits のコストは 2 桁の幅を持つ。実測された帰結: `POLL_REPOS` の末尾 repo にある 44 file の commit で、embed batch 3 本すべてが `Too many subrequests by single Worker invocation` で拒否された。同じ loop の手前にある repo は正常に index されていた。そして後述の不変条件がその commit で watermark を止める——token 軸が起こしたのとまったく同じ、毎 cron 決定論的に再現する停止が、file 軸で起きた(issue #238)。
commit 数は phase の消費量を縛れない。1 file の index は worst case で 3 subrequest(D1 FTS mirror 書き込み、store row、および embed batch の按分——1 batch は 2 subrequest を使い、`MAX_EMBEDDING_INPUT_CHARS` が 1 input を 8000 文字=最大 24000 UTF-8 バイトに切る一方 batch 予算は 60000 バイトなので、1 batch は最低 2 file を載せる)であり、1 commit は最大 300 file を運ぶため、5 commits のコストは 2 桁の幅を持つ。実測された帰結: `POLL_REPOS` の末尾 repo にある 44 file の commit で、embed batch 3 本すべてが `Too many subrequests by single Worker invocation` で拒否された。同じ loop の手前にある repo は正常に index されていた。そして後述の不変条件がその commit で watermark を止める——token 軸が起こしたのとまったく同じ、毎 cron 決定論的に再現する停止が、file 軸で起きた(issue #238)。

そこで diff surface は invocation 予算のうち自分の取り分を明示する: `DIFF_SUBREQUEST_BUDGET_PER_RUN` = Cloudflare が許す 1000 のうち 900。diffs は専用 cron を持つので、この天井を docs / wiki / issue / release と共有はしていない。しかし `POLL_REPOS` の全 repo と両 phase では共有しており、上記の失敗を生んだのはそちらの共有である。`diffFileBudgetPerPhase` は宣言した取り分を `repoCount × 2` で割り、残りを file 数に換算する。literal を固定せず repo list から導出するのは意図的である: `POLL_REPOS` は通常の config commit で増える(issue #233 が 6 番目の repo を追加した)ため、その日の list に合わせた literal は次の追加で天井を超え、しかもその超過は原因となった変更ではなく loop が最後に到達した repo の失敗として現れる。repo list が長く予算を下回るところまで割った場合は 5 file の下限が効く——超過は次の cron が再試行するが、予算 0 は全 diff watermark を恒久的に止める。それはいま取り除こうとしている失敗そのものである。

Expand Down Expand Up @@ -255,7 +255,7 @@ Responsibilities:
- metadata のみの経路(body は変わらず state / labels / milestone / assignees が変わった場合)では、mirror 書き込みの失敗を best-effort 扱いに**しない**。差分検出の基準を進めずに保持し、次の poll / webhook 配信で再試行させる。基準は IssueStore の record そのものなので、失敗した mirror を追い越して基準を進めると取り残しが恒久化する — state だけの変更は、embed 経路が待っている body 変更を二度と連れてこない(issue #209)
- この経路の dense / sparse mirror は互いに独立して書く。vector が欠けている行(issue #210)でも sparse 側の state は更新される
- commit diff は 1 commit 分の file リストを batch embed(Workers AI の `text: string[]` 対応を利用)し、batch ごとに 1 回の Vectorize upsert で N vector を書き込む
- batch の切れ目は file 件数でも token 数の推定でもなく、文字数予算(`MAX_EMBEDDING_BATCH_CHARS`)で決める。天井は bge-m3 の documented な per-input 上限 8192 token ではなく、1 call の input 全体を合算した endpoint 側の上限である。batch は合算され、拒否応答はその合計値を名指しする(`3030: Max context reached 85920 tokens but model supports only 60000`)。この値は非公開なので `WORKERS_AI_BATCH_CONTEXT_LIMIT` としてそのエラーから記録し、文字数予算はその天井そのものに置く。文字数が正しい単位である理由は、それが token 数の近似ではなく**上界**だからである。BPE / SentencePiece のいずれでも 1 token は入力の 1 文字以上に対応するので、各 input にその文字数と special token 2 個(`<s>` … `</s>`)を課金すれば、batch が天井の内側に収まることが無条件に成立する — 校正が要らず、payload の言語や記号密度にも依存しない。special token を 1 batch 固定の引当てではなく input ごとに課金するのは、batch の input 件数に上限が無く、極小 input が多数並ぶ場合に固定引当てでは足りなくなるためである。file 件数ではこれを表現できず、直前に置いていた推定でも表現できていなかった。推定は ASCII を 3 文字/token として読むが、bge-m3 が diff patch を割る密度は約 1.4 文字/token である — `+` / `-` 接頭、インデント、記号、短い識別子がいずれも細かく分割される — ため、肝心の surface で約 2.1x 楽観に振れ、実測 60678 / 64413 token の batch を「30000 token 予算に収まる」と判定して通していた(issue #241)。天井を超えた commit は chunk 全体を失敗させ、vector が載らなかった commit は diff watermark が留まる対象(issue #178)なので、その commit を飛ばすのではなく surface がそこで恒久的に停止する。しかも毎 cron 同じ commit で同じ結果になる決定論的な失敗で、同じログに同居する一過性の subrequest 超過とはそこで性質が分かれる(issue #236)。天井の下に margin は取らない — 構成上成立する上界に margin が買えるものは残っておらず、batch が 1 つ増えるごとに subrequest を 2 つ消費し、この worker は既に invocation 予算を超過しているためである。単独で予算を超える input はそのまま単独で送る — さらに削るのは truncate 軸(`MAX_EMBEDDING_INPUT_CHARS`)の仕事であり、落とせばその file が索引から消えるため。その truncate が 1 input を 8000 文字に抑えるので、payload の中身によらず 1 batch は最低 7 file を保持する。これが poller の per-file subrequest 見積りが立っている床である
- batch の切れ目は file 件数でも token 数の推定でも文字数でもなく、UTF-8 バイト数予算(`MAX_EMBEDDING_BATCH_BYTES`)で決める。天井は bge-m3 の documented な per-input 上限 8192 token ではなく、1 call の input 全体を合算した endpoint 側の上限である。batch は合算され、拒否応答はその合計値を名指しする(`3030: Max context reached 68736 tokens but model supports only 60000`)。この値は非公開なので `WORKERS_AI_BATCH_CONTEXT_LIMIT` としてそのエラーから記録し、バイト数予算はその天井そのものに置く。バイト数が正しい単位である理由は、それが token 数の近似ではなく**上界**だからである。tokenizer が作れる最も細かい分解が byte_fallback — 語彙に無い文字はその UTF-8 バイト列へ分解される — であり、それ以下には割れないので、1 token は入力の 1 バイト以上を必ず消費する。したがって各 input にその UTF-8 バイト数と `TOKEN_OVERHEAD_PER_INPUT` を課金すれば、batch は天井の内側に収まる。この overhead が 3 なのは、model が各 input を挟む sentinel 2 個(`<s>` … `</s>`)に加え、SentencePiece の語境界マーカーが先頭 piece に併合されない場合に単独の token として現れ、入力バイトを 1 つも消費しないためである。1 batch 固定の引当てではなく input ごとに課金するのは、batch の input 件数に上限が無く、極小 input が多数並ぶ場合(そのそれぞれが自分の 3 を持つ)に固定引当てでは足りなくなるためである。file 件数ではこれを表現できず、これ以前の 2 つの予算はいずれも実測に破られた前提の上に立っていた。1 つ目は token 数の推定で、ASCII を 3 文字/token として読むが bge-m3 が diff patch を割る密度は約 1.4 文字/token である — `+` / `-` 接頭、インデント、記号、短い識別子がいずれも細かく分割される — ため約 2.1x 楽観に振れ、実測 60678 / 64413 token の batch を「30000 token 予算に収まる」と判定して通していた(issue #241)。2 つ目は文字数で、「1 token は 1 文字以上に対応する」という前提に立っていたが、byte_fallback がまさにその前提を破る経路であり、3 バイトの日本語 1 文字が 3 token になりうる。実測では、文字数で高々 60000 と課金した batch に 68736 token が返った — 1 文字あたり 1.146 token である(issue #244)。バイト数はその両方の下にある。これより細かい分解が無いので、この軸が過小評価する payload は存在しない。残る前提は「tokenizer の NFKC 正規化が入力をバイト数で膨らませない」ことだけであり、これは互換分解を持つ文字(アラビア語の合字、CJK の組文字)を除いて成立する — 退けた 2 つの前提が通常の diff テキストで破れたのとは性質が異なり、こちらは入力の大半がその稀なブロックで占められている必要がある。天井を超えた commit は chunk 全体を失敗させ、vector が載らなかった commit は diff watermark が留まる対象(issue #178)なので、その commit を飛ばすのではなく surface がそこで恒久的に停止する。しかも毎 cron 同じ commit で同じ結果になる決定論的な失敗で、同じログに同居する一過性の subrequest 超過とはそこで性質が分かれる(issue #236)。天井の下に margin は取らない — 構成上成立する上界に margin が買えるものは残っておらず、batch が 1 つ増えるごとに subrequest を 2 つ消費し、この worker は既に invocation 予算を超過しているためである。単独で予算を超える input はそのまま単独で送る — さらに削るのは truncate 軸(`MAX_EMBEDDING_INPUT_CHARS`)の仕事であり、落とせばその file が索引から消えるため。その truncate が 1 input を 8000 文字=最大 24000 バイトに抑えるので、payload の中身によらず 1 batch は最低 2 file を保持する。これが poller の per-file subrequest 見積りが立っている床である。この床は文字数予算のときの 1/3 であり、日本語主体の commit はこれまで 1 call だったところが約 3 分割になる。増える subrequest は上界の代金であり、上の file 予算が 1 file あたりちょうど 3 subrequest としてすでに賄っている

**索引欠落の修復.** watermark の修正は漏れを止めるだけで、既に空いた穴は埋まらない — 取り残された項目が再 fetch されるのは `updated_at` が動いたときだけで、閉じた履歴はもう動かない。`POST /admin/backfill-issue-index?repo=owner/repo`(installation guide 参照)が欠落そのものを走査する。repository の issue 番号空間は密かつ有界なので、`search_docs` に issue / PR 行が無い番号がそのまま欠落集合であり、数値 cursor が「どこまで走査したか」を厳密に表せる。同じ集合を時刻 cursor で辿ると、欠陥が突いた順序をそのまま持ち込むことになる。GitHub 側に既に無い番号(削除・transfer 済み)は 404 を返すので、retry せず計上のみ。取り込みは body-hash 判定を強制的に飛ばす: 候補はいずれも retrieval surface が欠けていると分かっている項目であり、hash が一致していると(embed 成功後に FTS5 mirror が失敗した行がこの状態になる)そのまま恒久的に skip されてしまうため。state 修復と違いこちらは embed を伴い、1 候補ごとに取り込み fan-out の全額がかかるので、呼び出し側が batch を跨いで sweep を進める。`dry_run=true` は予算を使わずに欠落量だけを測る。sweep は poller の watermark と同じ不変条件に従う: **取り込みそこねた最初の候補を cursor が追い越さない。** これにより 1 call の上限を大きく取りすぎたときの代償は 1 回分の無駄な呼び出しであって取りこぼしではなく、結果が「上限値が subrequest 予算をどれだけ正確に写しているか」に依存しなくなる(issue #216)。GitHub 側に既に無い番号は例外 — 誰が何回試しても取り込めないので、そこで止めると retry の境界にならず sweep が停止する。トレードオフは poller と同じで、恒久的に失敗する候補があると sweep は止まる。ただしそれは応答に現れる(`nextCursor` が渡した `cursor` と同じ値で返る)し、この endpoint は cron ではなく人間 / AI が駆動するので、詰まった番号を手動の `cursor` 指定で越えられる。

Expand Down
Loading
Loading