Skip to content

ISS-10710: poll the code index every 5 minutes; reindex when HEAD moves - #29

Merged
wongk merged 3 commits into
mainfrom
campaign-iss-10710-20260919-c1
Sep 19, 2026
Merged

wongk merged 3 commits into
mainfrom
campaign-iss-10710-20260919-c1

Conversation

@wongk

@wongk wongk commented Sep 19, 2026

Copy link
Copy Markdown

Campaign iss-10710-20260919, chunk C1 of 3: poll every 5 minutes; reindex when HEAD moves

Lands PLN-2069 PR 1 for ISS-10710. Based on main, not stacked.

What changes

The code-index autosync loop used to stat-walk the whole repo every 60 s (1.25 s per walk on symphony-alpha) and start a whole-repo reindex whenever anything had changed. It now runs a fixed 60 s tick:

  • Every tick: a cheap git rev-parse HEAD. A HEAD move (pull, checkout, merge, rebase) triggers an incremental reindex on that tick.
  • Once per poll interval: the full-tree check. The interval now defaults to 5 minutes. code_context.autosync_poll_ms still overrides it, and the 60 s floor stays.
  • While a file watcher is alive: neither check runs, because the watcher already reindexes what a checkout changes.

Files: src/lemoncrow/pro/capabilities/code_context/engine.py, src/lemoncrow/core/settings_registry.py, tests/core/test_code_context.py.

Acceptance ledger: 4 criteria, 1 met, 3 met differently, 0 not met

Criterion Verdict Evidence
AC-1.1 full-tree check at most once per poll interval (default 5 min) met differently test_autosync_idle_ticks_walk_the_tree_once_per_poll_interval counts [1,1,1,1,1,2,2,2,2,2] walks over 10 ticks. It fails if the gate is removed. A HEAD-move reindex also reseeds the signature once (deviation 3).
AC-1.2 HEAD move → one incremental reindex within one tick met differently test_autosync_head_move_reindexes_on_the_next_tick_without_a_tree_walk: a commit in a tmp repo triggers exactly one reindex on the next tick, with no tree walk before it. Skipped while a watcher is alive (deviation 1).
AC-1.3 override honoured; values below 60 s raised to 60 s met test_autosync_poll_interval_default_override_and_floor (invalid value, 120000 and 1000 cases). Removing the override or the floor each fails it.
AC-1.4 at most two full checks and no reindex over 10 idle minutes on symphony-alpha met differently Simulated 10 minutes against symphony-alpha, using an injected clock and a copy-on-write clone of its index: 2 full checks (1.25 s, 1.22 s), 0 reindexes, 2.76 s of total tick time. The old 60 s cadence did 10 full checks in 12.41 s. The live-daemon check is left for rollout.

Every criterion's test fails against deliberately broken code: 14 counterfactual mutations were run. symphony-alpha's working tree and live index were not touched.

Deviations from PLN-2069 PR 1

  1. No HEAD check while a file watcher is alive. The plan didn't say; running both would reindex a checkout twice. This was a campaign decision.
  2. A full check that finds nothing now records a full_check event in the autosync history. Without it, AC-1.4 and the rollout check have nothing to count. After review, consecutive idle checks fold into one entry with a count (see below).
  3. A HEAD-move reindex reseeds the tree signature once and resets the poll timer. Without that, the next full check would see the change as new and reindex it a second time.
  4. AC-1.4 was measured with a simulated clock on an index clone, not on a live daemon. There is no live daemon in a build, and symphony-alpha must not be written to. The live check is left for rollout.
  5. known_change is str | None, carrying the reason (watcher_triggered or head_moved). The reindex helpers return whether a reindex succeeded, so a failed HEAD-move reindex keeps the old HEAD and retries on the next tick.
  6. Verification used scoped commands only. CI runs the full suite on this PR.

Review

/code-review:deep returned APPROVED, with 2 verified findings (both MEDIUM) and no blocking or high ones. The review is proven to have read this branch at 54e0273c.

  • Fixed in fb7e5808: idle full_check entries pushed real events out of the 20-entry autosync history, losing reindex, error and bootstrap entries after about 100 idle minutes. Consecutive idle checks now fold into one entry with a count. A regression test fails on the old code.

  • Pushed back: reuse _read_git_head (zoekt/server.py) instead of spawning git rev-parse. Verbatim:

    Reusing _read_git_head would break HEAD-move detection. It reads <repo_root>/.git/HEAD. In a git worktree, .git is a file (gitdir: ...), not a directory, so the function returns None and the HEAD check would silently never fire. Probed on this chunk's own worktree: _read_git_head returns None, while git rev-parse HEAD returns 54e0273. It also has two other gaps. When a branch ref is packed it returns the literal ref: refs/heads/<branch> string, so the key flips on git pack-refs/gc and triggers a spurious head_moved reindex. And it cannot read reftable repos. Making the file reader correct would mean reimplementing git's gitdir/commondir/packed-refs/reftable resolution, which git rev-parse already does. The saving it offers is small. The spawn runs only while no file watcher is alive, once per 60 s tick. It measured about 30 ms here, and the median non-full-check tick on symphony-alpha was 29.3 ms. That replaces the roughly 1.2 s full-tree walk the old loop ran every 60 s, now needed only 1 minute in 5: 2.76 s versus 12.41 s of tick time per idle 10 minutes. The 3 s timeout keeps a hung git from stalling the autosync thread, and does not show the approach is fragile. I kept the subprocess.

    The orchestrator independently confirmed that _read_git_head returns None for a worktree and the correct commit for the main checkout.

  • Declined, as out of scope (found while triaging, not a review finding): Zoekt's HEAD refresh never fires in git worktrees, for the same reason. Verbatim:

    This is outside this chunk's files and plan (PLN-2069 PR 1 covers the code-index autosync, not Zoekt). The fix touches the Zoekt infra layer and its tests, and should be reviewed on its own. The simplest fix is to resolve HEAD with git rev-parse there as well, or to follow the gitdir pointer and commondir. It is worth filing as an issue.

The re-review gate computed SKIP: 1 prod file(s) changed, none overlapping blocking_high.

Verification

All pass:

  • uv run pytest tests/core/test_code_context.py -k autosync (11 tests)
  • tests/infra/code_intel/test_code_engine_autosync_retirement.py, test_reads_during_reindex.py, tests/core/test_settings_registry.py, tests/test_mypyc_compile_safety.py
  • mypy on the two source files
  • ruff on the changed files
  • git diff --check
  • the pre-commit hook on every commit

The full suite was not run locally; CI runs it here.

Still owed by later chunks (merge this first)

  • C2, PLN-2069 PR 2: an MCP edit reindexes its files once, off the response path. It also closes two gaps the campaign found: files written by a partially failed edit, and edits outside the daemon's workspace.
  • C3, PLN-2069 PR 3: delete full-text rows by rowid, so a single-file reindex takes under 200 ms, and make the full rebuild fast and atomic. It shares engine.py with this PR, so it starts after this one merges.

wongk and others added 3 commits September 19, 2026 09:58
PLN-2069 PR 1. The autosync loop now ticks every 60 s. Each tick checks git
HEAD and reindexes on a move; the full-tree check runs once per poll
interval (default 5 minutes, 60 s floor). Both are skipped while the file
watcher is alive.

Co-Authored-By: Claude <noreply@anthropic.com>
Consecutive unchanged full checks now bump a count on one entry instead of
appending, so an idle repo keeps its reindex, error and bootstrap entries.

Co-Authored-By: Claude <noreply@anthropic.com>
@wongk
wongk merged commit 06552f9 into main Sep 19, 2026
9 checks passed
@wongk
wongk deleted the campaign-iss-10710-20260919-c1 branch September 19, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant