bug: a metadata-only touch bricks changed_symbols + review_diff for up to 30 minutes, with a retry hint that cannot come true #82
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
Reference
h-dv/code-index#82
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found dogfooding v0.22.2 (
4463d60). Live, reproduced end-to-end, pre-existing — not introduced by the reclassification fix in that commit (see "Not the new guard" below).Reproduction
md5 verified identical across step 2, and the file was indexed after its content change — so the index provably matches disk. Both tools refuse to answer about a correct index.
The error:
Retrying does not help. It heals only when a content event arrives, or at the 30-minute periodic reconcile (
watcher.rs:71). Confirmed both ways: repeated retries stayed bricked; restoring the file (a content event) healed it instantly.Mechanism
Deliberate, and each step is individually reasonable:
watcher.rs:952—EventKind::Modify(ModifyKind::Metadata(_)) => {}. Pure metadata edits (chmod / chown / touch) are dropped by design, labelled P1-2: "Without this filter,chmod -Ron a 100k tree thrashes the writer with hash reads."files.mtime_nsis never refreshed for a metadata-only change.file_freshness(crates/mcp-server/src/server.rs) computes staleness asdisk_mtime != overlap.mtime_ns→index_stale: true.index_updating.Step 1 is right in isolation. Step 3 uses mtime as a proxy for "content may have changed", which the metadata filter has just made permanently false for this file. Nothing reconciles the two.
Why this isn't a manual-
touchcuriosityThe realistic triggers are ordinary:
chmod -R/chown -Ranywhere in the treegit checkout/git stash poprestoring byte-identical contentrsync -t,tar -p,cp -p— the exact overwritesParseTask.ctime_nswas added to detectAny of those landing on a file that is also modified against the diff base bricks both tools for up to 30 minutes.
A fifth I034 door
I034's fix was walker-proved
not_indexedfor files that can never be indexed. This file can be indexed, is indexed, and is correct — the failure is in the mtime bookkeeping used as a freshness proxy. Different door, same outcome: the barrier waits for something that will not arrive.Worse than I034 in one respect: the hint names a remedy that cannot work. I063 was about a disclosure asserting a cause nothing measured; this asserts a remedy nothing can deliver.
Not the new guard
4463d60changed the hash-tier shortcut inrun_task. It is not implicated: the watcher drops the metadata event atwatcher.rs:952, before any of that code runs, and the classification here never changes (code stays code), so the guard's condition is satisfied and behaviour is byte-identical to before. Reproduced against the installed4463d60build; the mechanism is unchanged from every prior release.Proposed fix
Two parts, both generic — no file type or trigger named.
1. Make the barrier's staleness check truthful, for the bounded set it gates on. The barrier only cares about files in the diff — a small, already-enumerated set. For those, when mtime differs, verify by content hash before blocking. That is the same evidence
classify_hashalready uses, applied where the answer changes behaviour. It costs one read per changed file and makes the barrier's claim true rather than probable. It does not touch the watcher's P1-2 filter, which remains correct for the 100k-filechmod -Rcase it was written for.2. A hint that cannot be acted on is worse than none. If the barrier still gives up, it must not promise a retry. The honest text names the actual state and the actual remedies (touch the content, or wait for the periodic reconcile) — the
index_coverage/not_indexed_reasondiscipline from I063, applied to a remedy instead of a cause.Test shape
Registry-style, both directions:
Runtime-plugin architecture requirement
#75 makes freshness two-dimensional: source bytes and active extraction generation. The immediate metadata-touch fix remains required, but it must not hard-code “fresh means file hash matches.”
For a diff file, the barrier’s usable-state predicate becomes:
The bounded content-hash verification proposed above remains the correct repair for metadata-only touches. #78 must reuse it when validating source snapshots during generation builds, and #80 must pin distinct reason codes for source_pending, generation_pending, plugin_rejected and permanently_unclaimed.
Additional acceptance:
Fixed and graded — closing the bookkeeping
The fix is
c807368"fix: a touch is not a change (#82)", verified an ancestor of master and shipping since. Re-checked today rather than taken from a note.The defect, as the test's own doc records it:
mtimeis a proxy for changed content, and the watcher deliberately drops metadata-only events — so atouchleaves the proxy permanently wrong. mtime moved, bytes did not, and nothing refreshes the row before the 30-minute reconcile. Measured live, that brickedchanged_symbolsandreview_diffwithindex_updating("retry in a moment") against an index that was provably correct.Graded by
a_touch_is_not_a_change_but_an_edit_still_is(crates/mcp-server/src/server.rs), and the name is the point: it carries both directions. Its doc says so explicitly —which is the failure this class invites: a freshness barrier that never reports staleness passes every "it did not brick" test and is useless. The control is what makes the fix a fix rather than a mute.
The barrier's neighbouring states are graded in the same module — a file that cannot be classified must be reported neither PENDING (the barrier polls that flag and then denies the whole tool when it never heals) nor permanently uncoverable ("a verdict about the user's file invented out of a daemon that could not answer").
Closing. This was linked from #80's adjacent-blocker list, so that list is one shorter.
index_coveragereports "Indexed and current" for a file whose facts were refused by the validator #136