feat(daemon): graceful stale-format migration core — version-gate serve cache + loop-guarded auto-reindex (#5907)#5910
Merged
Conversation
…he (#5907 FIX1) openReader on the zero-copy MCP serve path called fbreader.Open with no version check (unlike internal/graph/load.go's loadFBDoc). fbreader.Open recovers-then-RE-PANICS on a malformed/incompatible flatbuffer vtable, so a breaking on-disk cut (the #5890 segmented format read by a binary that predates it) or a genuinely corrupt buffer opened here would re-panic and crash the daemon. Add openAndGate: a single recover covers both fbreader.Open AND the r.Version() scalar read, converting any panic into a plain error (cache miss). It then enforces the same format-version gate loadFBDoc does — an on-disk FormatVersion below fbversion.Version returns the SAME typed *graph.FormatVersionError, Closed before return so a stale-format reader is never cached (Get discards the handle on any openReader error) nor leaked. Callers can errors.As it and degrade to serve-empty/reindex-required, mirroring internal/mcp/state.go's detection. No fbversion bump; no segmented format. Crash-prevention core only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017quGgaqK7NRoxGT6BTqV2o
…ection (#5907 FIX2) ReindexRequired had zero action-consumers: a repo whose graph.fb was written by an older grafel build sat idle forever until a manual `grafel index`. The engine's status writer now auto-migrates it by enqueuing a KindReindex request through the SAME requests->drain->scheduler plumbing Service.Index uses (the daemon-side analogue of the CLI's FormatVersionError->full-reindex fallback). The load-bearing piece is the loop-guard (staleReindexGuard, stale_reindex.go). The status writer recomputes ReindexRequired on EVERY heartbeat, so a naive "if required { enqueue }" would storm one request per heartbeat (the #5891-class hazard). The guard fires AT MOST ONCE per (repo, stale generation), keyed by a fingerprint of graph.fb mtime + reason: - the mtime advances only when graph.fb is rewritten, so heartbeats observing the same stale file dedup, and an in-flight reindex (stale file untouched until the gen pointer flips) never triggers a second request; - when the graph goes current ReindexRequired flips false and the guard forgets the repo, self-clearing so a genuinely new stale generation later fires exactly one fresh request while a current repo never enqueues. stale_reindex_test.go proves exactly-once across 12 heartbeats, none for a current repo, and the full self-clear lifecycle. No fbversion bump; no segmented format; no dashboard/doctor/install surfacing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017quGgaqK7NRoxGT6BTqV2o
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Functional core of #5907 — makes an on-disk graph-format upgrade migrate gracefully (no crash, no silent stall), the foundation required before the #5890 breaking segmented format ships. Fully testable now via a simulated old `fbversion`; no format bump, no segmented format here.
FIX1 — crash prevention (`internal/daemon/mcp/graph_cache.go`): the zero-copy MCP query cache's `openReader` opened the mmap ungated (unlike `loadFBDoc`), so an incompatible format would re-panic (reader.go recovers-then-re-panics) → SIGSEGV in serve. Now version-gates (`r.Version() < fbversion.Version` → typed `*graph.FormatVersionError`, reader Closed, not cached) and panic-recovers `fbreader.Open`, so every serve path refuses an incompatible file instead of crashing/serving-stale.
FIX2 — kill the silent stall (`internal/daemon/stale_reindex.go` + statuswriter hook): `ReindexRequired` had zero action-consumers — a stale-format repo sat idle forever. The engine now auto-enqueues a `KindReindex` through the same requests→drain→scheduler plumbing `Service.Index` uses. Loop-guard (the #5891-class hazard): fingerprint = (graph.fb mtime | reason); mtime advances only on a real rewrite, so every heartbeat on the same stale file dedupes → exactly one request per stale generation; `required==false` self-clears; write-failure isn't recorded (retries); mutex-guarded; bounded (one entry per repo, deleted on heal); a genuinely-new stale generation re-fires once.
Reviewed (mutation-proven): FIX1 gate removed → both stale-format tests fail; FIX2 dedup removed → storm test fails ("in-flight heartbeat must not re-enqueue"); `-race -count=10` clean; current-format repos enqueue nothing. gofmt/vet clean, 24 new tests, broader daemon+mcp+graph green.
Deferred to next slices: dashboard/doctor/install surfacing (FIX3/4); segmented-reader format-sniff (needs #5890's format).
🤖 Generated with Claude Code
https://claude.ai/code/session_017quGgaqK7NRoxGT6BTqV2o