Skip to content

fix(daemon): upgrade an outdated index before the daemon starts serving it - #330

Merged
sunerpy merged 4 commits into
mainfrom
fix/outdated-index-upgrade
Oct 22, 2026
Merged

sunerpy merged 4 commits into
mainfrom
fix/outdated-index-upgrade

Conversation

@sunerpy

@sunerpy sunerpy commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

In the default daemon mode nothing upgraded an index an older extraction version built. A cold serve --mcp session defers to its daemon, and the daemon's startup gate admits only a current index, so every tool call answered "outdated" until a manual codegraph sync. v0.55.0 moved the extraction version from 21 to 22, so every v0.54.0 index hits this. The v0.55.0 black-box acceptance found it; v0.54.0 behaves the same on a v0.53.0 index.

  • The daemon now runs codegraph_watch::upgrade_outdated_index_once before its startup gate. Under one exclusive lease it requires three things before it runs the migration codegraph sync runs:
    • an outdated namespace whose authoritative slot records the current phase;
    • no uninitialized tombstone;
    • the database file.
  • Anything else is left untouched, and the unchanged gate refuses it as before: an older binary's uninit --force, an interrupted older build, or a removed database. The state gate classifies every phase an older version wrote as outdated, so without these checks a daemon would rebuild the database an older uninit --force removed.
  • A failed upgrade is a failed migration, so the daemon refuses to start with that error before its gate and publishes no pid or socket. The rebuild's last step publishes Current, and the publisher's directory sync can still fail after that, so the gate alone could have served an index whose upgrade had just failed.
  • What a failed rebuild leaves depends on where it fails, exactly as for codegraph sync. A failure before it begins replacing the index leaves the older index untouched. A later failure can leave building, the store's interrupted-build marker. A later daemon then finds that state, and the gate decides as before.
  • plain_output projects no longer share a temporary directory when two tests start within one clock tick, which caused the failure in chore: release 0.55.0 #325's first CI attempt.
  • docs/cli.md: the extraction-version upgrade and detached-daemon lifecycle sections describe the new behavior, including that a tool call may wait up to 30 seconds or fail while the daemon upgrades, and what a rebuild that fails partway leaves behind.

Tests

New tests in crates/codegraph-cli/tests/upgrade_from_v054.rs:

Test Kind What it checks
a_cold_mcp_session_upgrades_a_v0_54_index_through_its_daemon end-to-end The daemon upgrades the index, the result equals a fresh init, and a later edit becomes searchable through the session. On b02b563 it times out after 60 s while the daemon refuses the outdated index.
a_cold_mcp_session_leaves_an_older_uninit_alone end-to-end The database stays absent, the tombstone and state slots are unchanged, and no daemon serves.
a_daemon_whose_upgrade_fails_still_refuses_the_outdated_index end-to-end With an unparsable config, the daemon refuses with the upgrade error before its gate runs. The database and state slots are unchanged, and no daemon serves.
the_daemon_upgrade_skips_an_interrupted_older_build direct call of the entry One guard per test.
the_daemon_upgrade_skips_an_older_index_with_a_tombstone direct call of the entry One guard per test.
the_daemon_upgrade_skips_an_older_index_without_its_database direct call of the entry One guard per test.

New test in crates/codegraph-cli/tests/plain_output.rs:

  • projects_created_within_one_clock_tick_get_distinct_directories.

Verification:

  • Mutation checks. Each mutant was applied, its pinned tests were run, and the file was restored and compared. Every mutant failed its tests:

    • the upgrade removed;
    • all three guards removed (the old-uninit test fails with "the daemon recreated the database an uninit removed");
    • each guard removed on its own;
    • the startup gate removed;
    • a failed upgrade continuing to the gate;
    • the plain_output sequence removed.
  • Focused suites. All passed:

    • affected codegraph-rs integration tests (upgrade, cold start, daemon_*, batch_m_*, proxy, home guard, lease), 127 passed;
    • codegraph-rs bin tests, 440 passed;
    • codegraph-watch, 342 passed;
    • codegraph-daemon, 181 passed;
    • cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --check, oxfmt --check, scripts/docs-check.py.
  • make pre-ci passed through the pre-push hook.

  • Manual acceptance. Released v0.54.0 and v0.53.0 binaries built the index, and this branch's binary served it, for both a cold session and a replaced v0.54.0 daemon:

    • the index was upgraded without a manual sync;
    • a write reached the index;
    • the export was byte-identical to a fresh init.

    The same scripts fail against the released v0.55.0.

Not changed

  • The daemon startup gate itself, codegraph-daemon, codegraph-store, codegraph sync, and the direct-mode catch-up.
  • One existing behavior is not addressed here: codegraph sync also sees an older binary's uninit --force leftovers as outdated and would rebuild them. This comes from reading the code path; it was not reproduced.

…ng it

In the default daemon mode nothing upgraded an index an older extraction
version built. A cold `serve --mcp` session defers to its daemon, and the
daemon's startup gate admits only a current index, so every tool call
answered "outdated" until a manual `codegraph sync`. v0.55.0 moved the
extraction version from 21 to 22, so every v0.54.0 index hit this.

The daemon now upgrades the index before its startup gate, through a new
codegraph-watch entry, upgrade_outdated_index_once. Under one exclusive
lease it requires an outdated namespace whose authoritative slot records
the current phase, no uninitialized tombstone, and the database file, and
only then runs the migration `codegraph sync` runs. The state gate
classifies every phase an older version wrote as outdated, so without
these checks a daemon would rebuild the database an older
`uninit --force` removed. Anything else, and a failed upgrade, is left to
the unchanged gate, which refuses it as before.
Two tests that started within one clock tick shared a directory, and the
first to finish deleted the other's project mid-run, as in the first CI
attempt of #325. The name now carries a process-wide sequence, and the
directory is created with create_dir, so a collision fails instead of
sharing.
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.93151% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/codegraph-watch/src/sync.rs 79.59% 10 Missing ⚠️
crates/codegraph-cli/src/main.rs 95.83% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (84.93%) is below the target coverage (95.00%). You can increase the patch coverage or adjust the target coverage.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #330      +/-   ##
==========================================
- Coverage   95.60%   95.59%   -0.01%     
==========================================
  Files         280      280              
  Lines      166327   166400      +73     
==========================================
+ Hits       159016   159070      +54     
- Misses       7311     7330      +19     
Files with missing lines Coverage Δ
crates/codegraph-watch/src/lib.rs 97.72% <ø> (ø)
crates/codegraph-cli/src/main.rs 91.56% <95.83%> (+0.01%) ⬆️
crates/codegraph-watch/src/sync.rs 98.25% <79.59%> (-0.16%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The upgrade runs the migration `codegraph sync` runs, which publishes
`phase=building` before it removes the old database, so a rebuild that
fails partway leaves the index `building`, as an interrupted `sync` does,
and the daemon refuses it. The upgrade section said only that a later
tool call succeeds.
A failed upgrade is a failed migration, and a migration failure must stop.
The daemon logged the error and went on to its startup gate. The last step
of the rebuild publishes `Current`, and the publisher's directory sync can
still fail after that, so the gate could serve an index whose upgrade had
just reported an error. The daemon now refuses to start with that error,
before the gate, and publishes no pid or socket. A later daemon finds the
index as the failure left it, and the gate decides as before.

docs/cli.md now says what a failed rebuild can leave: the older index
untouched if it fails before it begins replacing it, `building` if it
fails later, as an interrupted `sync` does.
@sunerpy
sunerpy merged commit b6f0106 into main Oct 22, 2026
12 checks passed
@sunerpy
sunerpy deleted the fix/outdated-index-upgrade branch October 22, 2026 08:16
@github-actions github-actions Bot mentioned this pull request Oct 22, 2026
sunerpy added a commit that referenced this pull request Oct 22, 2026
Release-please PR #331 merged as 44fd8af and tag v0.55.1 was cut there. Release run 39754559422 passed all twelve jobs, and the downloaded release passed the black-box acceptance: an index built by v0.54.0 or v0.53.0 is upgraded by a cold session's daemon, and by a replacement daemon, with no manual sync. A second entry records #330's fix and the pre-existing sync behavior it leaves alone.
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