Skip to content

feat(daemon): graceful stale-format migration core — version-gate serve cache + loop-guarded auto-reindex (#5907)#5910

Merged
cajasmota merged 2 commits into
mainfrom
worktree-agent-a1b7b3c0a267dfc54
Jul 22, 2026
Merged

feat(daemon): graceful stale-format migration core — version-gate serve cache + loop-guarded auto-reindex (#5907)#5910
cajasmota merged 2 commits into
mainfrom
worktree-agent-a1b7b3c0a267dfc54

Conversation

@cajasmota

Copy link
Copy Markdown
Owner

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

jcajasmc and others added 2 commits July 22, 2026 14:56
…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
@cajasmota
cajasmota merged commit 568d5ba into main Jul 22, 2026
0 of 2 checks passed
@cajasmota
cajasmota deleted the worktree-agent-a1b7b3c0a267dfc54 branch July 22, 2026 07:32
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.

2 participants