fix(poller): cap diff-path files per run against the subrequest budget [poller, pipeline, docs, tests] - #240
Conversation
…t [poller, pipeline, docs, tests] diff path に file 数軸の上限が無く、file 数の多い commit 単独で Worker の 1 invocation あたり subrequest 予算を使い切っていた。#237 の merge 後も watermark が前進せず、token 軸のエラーが `Too many subrequests by single Worker invocation` に置き換わっただけだった観測(neuron-graph-rag@92eb94d、 44 file、embed batch 3 本すべて拒否)に対応する。 変更点: - `diffFileBudgetPerPhase(repoCount)` を追加。diff surface の予算取り分 (`DIFF_SUBREQUEST_BUDGET_PER_RUN` = 1000 のうち 900) を `repoCount × 2` で割り、file 数に換算する。literal を固定しないのは POLL_REPOS が通常の config commit で増えるため(#233 が 6 番目を追加)。下限 5 file を置き、 予算 0 による恒久停止ではなく回復可能な超過側に倒す。 - forward / backward 両 phase が独立の file 予算を持つ。forward が先に走る ため予算を共有すると歴史遡行が永久に進まなくなる。 - `processAndUpsertCommitDiff` に `maxFiles` / `indexedFilePaths` を追加し、 戻り値に `deferred` / `alreadyIndexed` を追加。間引きではなく分割で、 未処理 file は必ず報告される。 - 分割中の commit は新 status `partial`。watermark 不変条件の判定は `status !== "ok"` なので、最後の file が index されるまで watermark は 前進しない。#178 の不変条件は変更していない。 - 分割の再開位置は structured store の `GET /diffs` から読む。これが無いと 上限付きの run が毎 cron 同じ先頭側を再 index して収束しない。予算を超える commit だけが 1 subrequest を払う。store 側エラーは「未 index」ではなく commit の失敗として扱う。 - 要件記述と installation doc を同一 PR で更新。 webhook 経路は per-push 1 invocation で圧力が異なり、watermark を持たない ため上限を入れると file を落とすことになる。本 PR の範囲外とし、webhook が 取りこぼした commit は forward phase が redundancy として拾い、そこでは 分割が効く。 test: embed-diff の分割 / 再開 / 収束、poller の予算導出(14 repo まで 1000 subrequest 以内、下限 5 を割らない)、44 file commit の複数 run 完走、 分割中の watermark 保持、phase ごとの独立予算、resume query 失敗時の扱い。 Closes #238
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
github-rag-mcp | ef46e86 | Aug 14 2026, 07:15 AM |
smileygames
left a comment
There was a problem hiding this comment.
AI self-review (auto mode, parent)
検証したもの
コードを読んで確認した点。
予算導出の算術 — 6 repo で phases = 12、floor(900/12) - 20 = 55、floor(55/3) = 18 files/phase。報告値と一致する。上限側も確認した。repo 13 以降は導出値が floor を下回り 5 に張り付く。14 repo で総コスト 28 phase × (5×3 + 20) = 980、Cloudflare の 1000 に収まる。15 repo で 1050 となり超える。「14 repo まで持つ」は正しい。
floor の向き — 0 に落として全 watermark を恒久停止させるより、予算超過(次 cron で再試行される一過性失敗)を選ぶ。この issue が消そうとしている失敗形そのものを floor 不在が再生産するので、向きは正しい。
watermark 不変条件 — 判定が status !== "ok" であり、partial は failed / deferred と同じ側に立つ。#178 の不変条件そのものは書き換わっていない。分割途中の commit が watermark を越えないことをテストが押さえている。
resume の scope(最も危険だった箇所) — GET /diffs?repo=...&commit_sha=... が commit 単位で絞られていることを store 側で確認した(src/store.ts L1332-1339、listDiffsByCommit(repo, commitSha))。両パラメータ必須で、欠けたら 400。もし commit_sha が無視される実装だったら、別 commit で同名 path を index 済みの file が「済み」と誤判定され、file が静かに落ちていた。issue の「file を落とさない」制約に直結する箇所だが、正しく絞られている。
収束 — 予算は未 index file に対して効き、resume が済みを除外するので進捗は単調。44 file / 18 = 3 run で完了する。
webhook 経路を無制限のまま残した判断 — 妥当と判断する。webhook は watermark を持たないため cap は deferral ではなく file 欠落になる。取りこぼした commit は forward phase が拾い、そこには分割が効く。データ欠落は生じない。
受け入れ条件
| criterion | 結果 |
|---|---|
| file 数の多い commit が単独で invocation 予算を使い切らない | pass(コード上)。実測は post-merge |
| 部分処理された commit の watermark が前進しない | pass — status !== "ok" 判定 + テスト |
| 既存テストが通り、回帰テストが追加されている | pass — 259 unit + 60 workers green、新規 26 assertion |
指摘なし
前回(#237)は定数の根拠に指摘を返したが、今回は返す点がない。予算を literal ではなく POLL_REPOS 数から導出した判断、phase ごとに予算を分けた判断(forward が先に走るため共有すると backward が恒久的に飢える)、floor の向き、いずれも根拠が明示されていて追える。
premise の誤り(起票側の責)
issue #238 本文に「cron 1 tick は 1 invocation で全 repo・全 surface を処理するため、subrequest 予算は docs / wiki / issue / release 経路と共有されている」と書いたが、これは誤り。
wrangler.toml の crons は 4 系統(0 / 15 comments / 30 diffs / 45 wiki)で、controller.cron で dispatch が分かれる(src/poller.ts L3202-3223)。diff は自前の invocation を持つ。
実際の共有は POLL_REPOS の全 repo × 両 phase の間で起きており、観測とも整合する(loop の先の repo は正常に index され、最後の neuron-graph-rag だけが飢えた)。結論は変わらないが、前提の事実誤認である。
起票時(06:17 UTC)は毎時単一 invocation と誤認していた。その後 06:53 UTC に cron が複数系統であることを発見して Master に報告したが、その事実を open issue の premise に反映しなかった。実装側が読み直して訂正し、PR body に記録している。
next step
auto mode のため human check なし。self-review pass をもって AI が直接 merge する。
merge 後、neuron-graph-rag@92eb94dcb88c5e7494bc3b69377249e08601d99f(44 file)を対象に観測する。予想は forward 3 run で完了し、最後の file が landed した run で watermark が前進。次の diff cron は毎時 30 分。これは #236 の最後の受け入れ条件でもある。
目的
diff path に file 数軸の上限が無く、file 数の多い commit が単独で Worker の 1 invocation あたり subrequest 予算(1000)を使い切っていた。#178 の watermark 不変条件により、取り込みに成功していない commit を watermark は追い越さないため、失敗は決定論的に毎 cron 再現し diff surface がそこで恒久停止する。#236 が閉じた token 軸と同じ停止の形。
Closes #238
観測との対応
#237 merge 後の最初の diff 巡回で
3030: Max context reachedは消滅したが、watermark は前進しなかった。neuron-graph-rag@92eb94dcb88c5e7494bc3b69377249e08601d99f(44 file)の embed batch 3 本すべてがToo many subrequests by single Worker invocationで拒否されている。premise の訂正
issue 本文は「subrequest 予算は docs / wiki / issue / release 経路と共有されている」としていたが、これは誤り。
handleScheduledは cron 式で dispatch を分けており、diffs は30 * * * *で専用 invocation を持つ(DIFFS_CRON)。ただし結論は変わらない。予算は
POLL_REPOSの全 repo × 両 phase で共有されており、実際に効いたのはそちらの共有である。上記の観測で、loop の手前にある repo は正常に index され、末尾のneuron-graph-ragだけが枯渇している事実がその形を示している。issue の constraint「diff path が予算をどれだけ使ってよいか を明示する」は、この repo 数軸に対して満たしている。変更点
予算の明示と導出
DIFF_SUBREQUEST_BUDGET_PER_RUN = 900— diff surface が 1 invocation で使ってよい取り分の宣言(Cloudflare の 1000 のうち)。diffFileBudgetPerPhase(repoCount)— 宣言した取り分をrepoCount × 2で割り、file 数に換算する。1 file の worst case コストは 3 subrequest(D1 FTS mirror、store row、embed batch の按分)。POLL_REPOSが通常の config commit で増えるため(chore(poller): add Liplus-Project/neuron-graph-rag to POLL_REPOS [config] #233 が 6 番目を追加)。その日の list に合わせた literal は次の追加で天井を超え、しかもその超過は原因となった変更ではなく loop が最後に到達した repo の失敗として現れる。今回の観測がまさにその形だった。forward / backward の予算分離
forward phase が先に走るため予算を共有すると、forward window が忙しい repo では歴史遡行が永久に進まなくなる。各 phase が独立の予算を持つ。
分割(間引きではない)
processAndUpsertCommitDiffにmaxFiles/indexedFilePathsを追加、戻り値にdeferred/alreadyIndexedを追加。予算を超える file は必ずdeferredとして報告され、落とさない。partial。watermark 不変条件の判定はstatus !== "ok"なので、partialはfailedと同じ扱いになり、最後の file が index されるまで watermark は前進しない。bug(diff-sync): failed commits advance watermark and leave permanent gaps #178 の不変条件そのものは変更していない。GET /diffsから読む。これが無いと上限付きの run が毎 cron 同じ先頭側を再 index して収束しない。file 数が予算を超える commit だけが 1 subrequest を払うので、通常の commit はこの判定に何も払わない。store 側エラーは「まだ何も index されていない」ではなく commit の失敗として扱う(前者は分割を黙って先頭からやり直させる)。partialを追加し、deferredを backward 側にも出すようにした。partialは分割途中、deferredはその run が到達しなかった commit で、いずれもエラーではない。範囲外としたもの
webhook 経路(
src/webhook.ts)は上限を入れていない。per-push 1 invocation で圧力が異なり、かつ watermark を持たないため、上限を入れると file を落とすことになる。webhook が取りこぼした commit は forward phase が redundancy として拾い、そこでは本 PR の分割が効く。test
src/pipeline/embed-diff.test.ts—maxFilesによる分割、再開(indexedFilePaths)、繰り返し呼び出しの収束(deferred=0に到達すること)、上限なし呼び出し(webhook 経路)の不変性、index 済み commit の再 embed 抑止。src/poller.test.ts— 予算導出(14 repo まで 1000 subrequest 以内 / repo 追加で縮む / 44 file commit に対して使える大きさ / 下限 5 を割らない)、44 file 相当 commit の複数 run 完走と単調前進、分割中の watermark 保持、予算消費後の後続 commit の deferred、予算内 commit が resume query を払わないこと、phase ごとの独立予算、resume query 失敗時の扱い。npm test(unit 259 / workers 60)、npx tsc --noEmit、node scripts/check-schema-drift.mjs、npx wrangler deploy --dry-runいずれも local で通過。要件記述
docs/0-requirements.md/docs/0-requirements.ja.mdの Diff Pipeline 節と、docs/installation.md/docs/installation.ja.mdの追いつき速度の記述を同一 PR で更新。