fix(#262): reindex counter polls past the stale pre-reindex snapshot #264

Merged
vvzvlad merged 1 commits from fix/262-reindex-progress-realtime into develop 2026-06-30 11:21:02 +03:00
Collaborator

Summary

Чинит баг #262 (follow-up #242): счётчик «Indexed X of Y» при реиндексе эмбеддингов замирал на 0 до перезагрузки страницы. closes #262

Корень — чисто клиентский. Сразу после «Reindex now» клиент ещё держит ДО-реиндексный снапшот settings. Для уже полностью проиндексированного воркспейса он читается как reindexing=false, indexedPages>=totalPages. Эффект, гасящий дедлайн опроса, вычислял isReindexComplete(settings) против этого устаревшего снапшота → «готово» → снимал дедлайн ДО того, как придёт первый пост-реиндексный поллинг. Опрос не стартовал, счётчик замирал на 0 (перезагрузка просто тянула один свежий GET). Бэкенд при этом корректен: getMasked() отдаёт живой progress.done/reindexing=true, воркер инкрементит done по странице.

Фикс — гейт seenActive. Завершение засчитывается только после того, как поллинг реально увидел активный прогон:

  • reindexSeenActiveRef — сбрасывается в false в onSuccess мутации (ДО установки дедлайна), латчится в true в эффекте при settings?.reindexing.
  • isReindexComplete(status, seenActive) и nextReindexPollInterval({..., seenActive}) требуют seenActive — устаревший «полностью проиндексирован» снапшот больше не читается как «готово».
  • Серверный pre-seed (reindexing=true с момента enqueue на весь прогон) гарантирует, что seenActive латчится рано, поэтому реальное завершение по-прежнему останавливает опрос быстро; cap REINDEX_POLL_CAP_MS проверяется ПЕРВЫМ и всегда выигрывает → runaway-поллинга нет. Второй подряд реиндекс работает (ref сбрасывается в onSuccess).

How verified

На стенде (apps/client):

  • vitest run ai-provider-settings.spec.tsx30 passed (вкл. новый нон-вакуумный кейс «does NOT stop on the stale pre-reindex snapshot»: при seenActive:false, reindexing:false, indexed>=total → возвращает интервал, а не false; на старом коде вернул бы false).
  • tsc -p apps/client/tsconfig.json --noEmit → 0.
  • eslint обоих файлов → 0 ошибок (7 warnings — пред-существующие, идентичны на HEAD).

Внутренний ревью (отдельный субагент: прошёл всю последовательность шагов, серверный pre-seed, терминацию по cap, render-timing рефа, второй прогон) — APPROVE, гонок/runaway/поломки 2-го прогона не найдено.

Известные нюансы (некритичны, по итогам внутр. ревью)

  • Очень быстрый/пустой прогон (<5с), завершившийся ДО первого поллинга, не успеет увидеть reindexing=trueseenActive останется false → опрос идёт вхолостую до cap (120с). Счётчик при этом показывает корректное значение, спиннер не залипает (isReindexButtonLoading гейтится на status===true). Ограничено cap-ом, осознанный trade-off.
  • Тестами заперта чистая decision-логика (nextReindexPollInterval/isReindexComplete); проводка ref/effect/refetchInterval без юнит-теста (в файле нет компонентных рендер-тестов). Готов добавить рендер-тест, если хочешь зафиксировать и интеграцию.

Checklist

  • критерии #262: счётчик растёт 0→total без перезагрузки; завершение/cap корректны
  • вне scope ничего не менялось (один компонент + его спека)
## Summary Чинит баг #262 (follow-up #242): счётчик «Indexed X of Y» при реиндексе эмбеддингов замирал на 0 до перезагрузки страницы. closes #262 **Корень — чисто клиентский.** Сразу после «Reindex now» клиент ещё держит ДО-реиндексный снапшот `settings`. Для уже полностью проиндексированного воркспейса он читается как `reindexing=false, indexedPages>=totalPages`. Эффект, гасящий дедлайн опроса, вычислял `isReindexComplete(settings)` против этого устаревшего снапшота → «готово» → снимал дедлайн ДО того, как придёт первый пост-реиндексный поллинг. Опрос не стартовал, счётчик замирал на 0 (перезагрузка просто тянула один свежий GET). Бэкенд при этом корректен: `getMasked()` отдаёт живой `progress.done`/`reindexing=true`, воркер инкрементит `done` по странице. **Фикс — гейт `seenActive`.** Завершение засчитывается только после того, как поллинг реально увидел активный прогон: - `reindexSeenActiveRef` — сбрасывается в `false` в `onSuccess` мутации (ДО установки дедлайна), латчится в `true` в эффекте при `settings?.reindexing`. - `isReindexComplete(status, seenActive)` и `nextReindexPollInterval({..., seenActive})` требуют `seenActive` — устаревший «полностью проиндексирован» снапшот больше не читается как «готово». - Серверный pre-seed (`reindexing=true` с момента enqueue на весь прогон) гарантирует, что `seenActive` латчится рано, поэтому реальное завершение по-прежнему останавливает опрос быстро; cap `REINDEX_POLL_CAP_MS` проверяется ПЕРВЫМ и всегда выигрывает → runaway-поллинга нет. Второй подряд реиндекс работает (ref сбрасывается в `onSuccess`). ## How verified На стенде (`apps/client`): - `vitest run ai-provider-settings.spec.tsx` → **30 passed** (вкл. новый нон-вакуумный кейс «does NOT stop on the stale pre-reindex snapshot»: при `seenActive:false, reindexing:false, indexed>=total` → возвращает интервал, а не `false`; на старом коде вернул бы `false`). - `tsc -p apps/client/tsconfig.json --noEmit` → 0. - `eslint` обоих файлов → 0 ошибок (7 warnings — пред-существующие, идентичны на HEAD). Внутренний ревью (отдельный субагент: прошёл всю последовательность шагов, серверный pre-seed, терминацию по cap, render-timing рефа, второй прогон) — APPROVE, гонок/runaway/поломки 2-го прогона не найдено. ## Известные нюансы (некритичны, по итогам внутр. ревью) - Очень быстрый/пустой прогон (<5с), завершившийся ДО первого поллинга, не успеет увидеть `reindexing=true` → `seenActive` останется false → опрос идёт вхолостую до cap (120с). Счётчик при этом показывает корректное значение, спиннер не залипает (`isReindexButtonLoading` гейтится на `status===true`). Ограничено cap-ом, осознанный trade-off. - Тестами заперта чистая decision-логика (`nextReindexPollInterval`/`isReindexComplete`); проводка `ref`/effect/`refetchInterval` без юнит-теста (в файле нет компонентных рендер-тестов). Готов добавить рендер-тест, если хочешь зафиксировать и интеграцию. ## Checklist - [x] критерии #262: счётчик растёт 0→total без перезагрузки; завершение/cap корректны - [x] вне scope ничего не менялось (один компонент + его спека) <!-- state:review reviewed_head=67312a375 verdict=approved -->
agent_coder added 1 commit 2026-06-30 09:13:06 +03:00
After "Reindex now" the "Indexed X of Y" counter froze at 0 until a manual
reload. Root cause is purely client-side: right after the mutation the
client still holds the PRE-reindex settings snapshot, which for an already
fully-indexed workspace reads reindexing=false, indexed>=total. The
deadline-clearing effect evaluated isReindexComplete() against that stale
snapshot, read it as "done", and cleared the poll deadline before the first
post-reindex poll ever landed — so polling never ran and the counter stayed
at 0 (a reload just fetched one fresh snapshot).

Gate completion on having actually observed the active run: a
reindexSeenActiveRef, reset on each new reindex (mutation onSuccess, before
setting the deadline) and latched true once a poll reports reindexing=true.
isReindexComplete(status, seenActive) and nextReindexPollInterval now require
seenActive, so the stale fully-indexed snapshot no longer reads as finished.
The server pre-seeds reindexing=true from enqueue time, so seenActive latches
early and a genuine completion still stops polling promptly; the
REINDEX_POLL_CAP_MS cap is checked first and always wins, so polling can
never run away. closes #262

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-06-30 09:13:07 +03:00
Collaborator

Ревью 67312a375 — раунд 1 (6 аспектов вкл. COHERENCE; Lite+regressions — клиентский state-machine фикс ~75 строк прод-логики, без sensitive-путей).

Вердикт: PASS. Баг #262 закрыт, фикс корректен — проверено по коду многими аспектами.

Что чинит #264 (follow-up #242): счётчик «Indexed X of Y» замирал на 0 до перезагрузки. Корень чисто клиентский: сразу после «Reindex now» клиент держит ДО-реиндексный снапшот settings, который для уже-полностью-проиндексированного воркспейса читается как reindexing=false, indexed>=total; isReindexComplete(settings) против устаревшего снапшота → «готово» → снимал дедлайн опроса ДО первого пост-реиндексного поллинга → опрос не стартовал.

Фикс (seenActive-гейт) — корректен:

  • coherence/stability проследили сквозь: onSuccess сбрасывает reindexSeenActiveRef=false ДО установки дедлайна → refetchInterval (читает ref ЖИВО) стартует опрос (устаревший снапшот больше не «complete»); первый pre-seed-поллинг (reindexing=true) короткозамыкает refetchInterval ДО гейта и латчит seenActive=true в эффекте; реальное завершение (reindexing=false, indexed>=total при латченном seenActive) снимает дедлайн. Окно устаревшего снапшота закрыто, реальное завершение по-прежнему останавливает.
  • Ref/render-гонки нет: единственный поллинг, где seenActive важен — завершающий (reindexing=false), он на 5с позже активного, латч давно сфлашен; .current читается живо (нет деструктуризации-снапшота); эффект-deps [reindexDeadline, settings] включают settings. Кросс-run-лика нет (onSuccess ресетит; устаревший true инертен при deadline===null). Cap (120с) всегда выигрывает, таймер чистится на unmount.
  • Регрессий нет: все 3 прод-call-site + спека обновлены на новую сигнатуру (грэп: страгглеров/TS-брейка нет); нормальный поток прозрачен (reindexing-ветка короткозамыкает ДО гейта); пустой воркспейс (0/0) терминируется; cap не тронут.
  • Покрытие не-вакуозно: тест запирает баг В ОБЕ стороны — isReindexComplete(stale, false)→false + nextReindexPollInterval(stale, seenActive:false)→INTERVAL (на пре-фикс коде вернули бы противоположное), И реальное завершение (.., true)→true/false. Все seenActive-ветки обоих хелперов покрыты.

conventions (useRef-идиом, single-source-of-truth сохранён) / simplification (минимальный фикс, комменты документируют реальную гонку pre-seed↔stale-snapshot) — LGTM.

кодеру НЕ делать — calibration log (operator only):
- [below-threshold] suggestion [test coverage] латч-lifecycle компонентной обвязки (onSuccess-ресет + эффект-латч) тестом не покрыт — только чистые хелперы. Дроп: модуль по конвенции репо НЕ имеет render-тестов (логика вынесена в чистые fn и юнит-тестится); решающая логика фикса покрыта не-вакуозно; тащить @testing-library+QueryClient ради 3 строк ref-плумбинга = смена тест-парадигмы, author's discretion.

Объективные проверки: vitest сам прогнать не могу (нет node_modules); тесты независимо верифицированы аспектом test-coverage как не-вакуозные и запирающие баг обе стороны; кодер отчитался о прогоне. Поведенческая корректность state-machine проверена 4 аспектами трейсом. Готово к мержу.

Маркер reviewed_head обновлён на 67312a375.

Ревью **67312a375** — раунд 1 (6 аспектов вкл. COHERENCE; Lite+regressions — клиентский state-machine фикс ~75 строк прод-логики, без sensitive-путей). **Вердикт: PASS.** Баг #262 закрыт, фикс корректен — проверено по коду многими аспектами. **Что чинит #264 (follow-up #242):** счётчик «Indexed X of Y» замирал на 0 до перезагрузки. Корень чисто клиентский: сразу после «Reindex now» клиент держит ДО-реиндексный снапшот `settings`, который для уже-полностью-проиндексированного воркспейса читается как `reindexing=false, indexed>=total`; `isReindexComplete(settings)` против устаревшего снапшота → «готово» → снимал дедлайн опроса ДО первого пост-реиндексного поллинга → опрос не стартовал. **Фикс (`seenActive`-гейт) — корректен:** - coherence/stability проследили сквозь: `onSuccess` сбрасывает `reindexSeenActiveRef=false` ДО установки дедлайна → refetchInterval (читает ref ЖИВО) стартует опрос (устаревший снапшот больше не «complete»); первый pre-seed-поллинг (`reindexing=true`) короткозамыкает refetchInterval ДО гейта и латчит `seenActive=true` в эффекте; реальное завершение (`reindexing=false, indexed>=total` при латченном seenActive) снимает дедлайн. Окно устаревшего снапшота закрыто, реальное завершение по-прежнему останавливает. - **Ref/render-гонки нет**: единственный поллинг, где `seenActive` важен — завершающий (`reindexing=false`), он на 5с позже активного, латч давно сфлашен; `.current` читается живо (нет деструктуризации-снапшота); эффект-deps `[reindexDeadline, settings]` включают `settings`. Кросс-run-лика нет (`onSuccess` ресетит; устаревший `true` инертен при `deadline===null`). Cap (120с) всегда выигрывает, таймер чистится на unmount. - **Регрессий нет**: все 3 прод-call-site + спека обновлены на новую сигнатуру (грэп: страгглеров/TS-брейка нет); нормальный поток прозрачен (`reindexing`-ветка короткозамыкает ДО гейта); пустой воркспейс (0/0) терминируется; cap не тронут. - **Покрытие не-вакуозно**: тест запирает баг В ОБЕ стороны — `isReindexComplete(stale, false)→false` + `nextReindexPollInterval(stale, seenActive:false)→INTERVAL` (на пре-фикс коде вернули бы противоположное), И реальное завершение `(.., true)→true/false`. Все seenActive-ветки обоих хелперов покрыты. conventions (`useRef`-идиом, single-source-of-truth сохранён) / simplification (минимальный фикс, комменты документируют реальную гонку pre-seed↔stale-snapshot) — LGTM. ``` кодеру НЕ делать — calibration log (operator only): - [below-threshold] suggestion [test coverage] латч-lifecycle компонентной обвязки (onSuccess-ресет + эффект-латч) тестом не покрыт — только чистые хелперы. Дроп: модуль по конвенции репо НЕ имеет render-тестов (логика вынесена в чистые fn и юнит-тестится); решающая логика фикса покрыта не-вакуозно; тащить @testing-library+QueryClient ради 3 строк ref-плумбинга = смена тест-парадигмы, author's discretion. ``` Объективные проверки: vitest сам прогнать не могу (нет node_modules); тесты независимо верифицированы аспектом test-coverage как не-вакуозные и запирающие баг обе стороны; кодер отчитался о прогоне. Поведенческая корректность state-machine проверена 4 аспектами трейсом. Готово к мержу. Маркер `reviewed_head` обновлён на `67312a375`. <!-- state:review reviewed_head=67312a375 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-06-30 09:24:37 +03:00
vvzvlad merged commit e3ec9a2965 into develop 2026-06-30 11:21:02 +03:00
Sign in to join this conversation.