A permanently malformed lockfile is indistinguishable from a torn mid-write read, so a corrupt payload reads as "no daemon" forever #157
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.
Dependencies
No dependencies set
Reference
h-dv/code-index#157
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 while diagnosing the Windows CI failure at
08a6eed, and it is the more general defect underneath it.The behaviour
Lockfile::read(crates/daemon/src/lifecycle.rs:190-228) maps a TOML parse failure toOk(None):That is correct for a torn read and should stay. The payload is written by rename, but a reader can still catch a partial file on some filesystems, and treating that as "no payload yet, poll again" is right.
The problem is that it is the only reading. A payload that is permanently malformed — truncated by a full disk, corrupted, hand-edited, or written by a buggy forge — returns exactly the same
Ok(None). Every caller therefore concludes there is no daemon, forever, and the polling that is correct for a torn read never terminates.How it presented, which is why it is worth filing
Windows CI failed on
readiness_deadline_tests::a_beating_daemon_extends_the_readiness_deadlinewith:An assertion about heartbeats, for a file that would never parse. The test's forge had interpolated a Windows root into a TOML basic string (
root = "C:\Users\..."—\Uis an invalid escape),readreturnedOk(None), anddaemon_is_still_startingfell to its_ => falsearm. A tolerance for a transient state swallowed a permanent one, and the diagnostic pointed at the wrong subsystem.That is this project's recurring shape — two states rendering identically — in the lockfile reader rather than in a payload. Compare #147 (an unreadable file's
index_updatingwith no reachable success condition) and #155/#156 (a transient absence indistinguishable from a permanent one).What a repair needs
Three states, not two:
(3) is the missing one. A cheap version: keep
Ok(None)for the first observation and have the polling caller escalate after a bounded number of identical failures — the file's own bytes are stable across the polls, so "the same malformed payload N times" is a measurable, not a guess.Note the bar this must clear: the operator-facing message should name the file and the fact that it is a cache.
#144added exactly that shape for a wedged migration ("the index is a cache and can always be rebuilt from the working tree — to recover, stop the daemon and delete it"), and a malformeddaemon.tomlis even cheaper to recover from — it is one file, not the index.Related
crates/daemon/tests/lockfile_forge_registry.rsnow gates the class🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Triage 2026-09-06: CLOSING. Three states became four, the malformed verdict is a measurement rather than a guess, and the operator message names the file and says waiting will not help.
Verified against master; landed in
d0279c0.Against the stated acceptance
PayloadState { Absent, Present, Malformed, ForeignRoot },crates/daemon/src/lifecycle.rs:227-240, rationale at:194-226.MalformedPayload { path, len, digest, reason }(lifecycle.rs:268-280) carries an FNV digest over the exact bytes, andSTABLE_MALFORMED_OBSERVATIONS = 5(:305) withMalformedWatch(:317) escalates only when the same digest returns five times, clearing on any non-malformed state. So a torn mid-write read can never escalate — it is structurally impossible, not merely unlikely.MalformedPayload::recovery_hint()(lifecycle.rs:286-297) gives the path, the parser's reason and the byte count, says re-reading will not change it, and says "delete it and the next tool call starts a fresh daemon" — #144's "it is a cache, delete it" shape, as asked.crates/mcp-server/src/main.rs:1016(MalformedWatch::new()) and:1021-1027, whichbail!s with the hint plus how many times and over how long it was observed. First-observation warning atmain.rs:832-841.Lockfile::readstill collapses toOptiondeliberately, so existing callers are untouched — the widening is additive.Runs (exit 0)
The load-bearing one is
only_stable_bytes_escalate(payload_state_e2e.rs:172) and it is strong:3Npolls with changing bytes asserting escalation never fires, then2Nwith stable bytes asserting it fires exactly once at observation N, then a successful read asserting the watch clears. That grades both directions of the distinction this issue is about.no_test_forges_a_daemon_payload_by_handis the anti-vacuity companion — it stops the suite drifting into grading hand-built structs.Residuals, named
mcp-serverbail itself is ungraded.MalformedWatchis graded at unit level; nothing drives the real MCP server against a permanently malformeddaemon.tomland asserts the operator sees the recovery hint rather than "the daemon did not answer". That is the exact user-visible behaviour this issue was filed about, so it is the natural next test — but the mechanism it would grade is present and unit-graded.rpc_index.rs:864'sMalformed → DaemonBuild::Unknownarm has no test. The renderer is graded (server.rs:18685); the producer mapping is not, so deleting that arm would be green.Both are test-coverage gaps on a shipped mechanism, not the silent-loss defect this issue names. Closing on that basis; if either is wanted as tracked work it should be a fresh issue rather than holding this one open.
🤖 Triage lane, 2026-09-06, master
45cf6e4code-index://docs/reason-codesis at 3,979 of its 4,000-token cap, so the next reason code this project mints cannot be documented #184