Conversation
Compress verbose bullet sections (concurrency/OCC, write_api, test commands, agent lifecycle, multi-instance coordination, state-consolidation Inc 1+2) into dense prose entries. No information removed - every contract fact, event type, API name and measured number is retained verbatim. 148 -> 88 lines (62 lines of headroom under the 150 cap) so multibox Inc 4b/5/7 can each append their section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the three things Inc 4a deliberately left open. All changes are in state_store/fs_claim_log.py; the pure fold from 4a is untouched and its 67 tests stay green. Durability. _append writes to a temp name in the SAME directory (os.replace is only atomic within one filesystem), flush + os.fsync to make the BYTES durable, os.replace to publish the name atomically, then _fsync_dir to make the directory ENTRY durable. A peer must never see a record name whose bytes are not yet durable, and no reader may see a partially written .json. The temp suffix is deliberately not .json so _read_records skips it: a crash mid-write leaves a file that was never published, so ignoring it reproduces the pre-write state exactly. A truncated .json WAS published and still blocks every path, so 4a's fail-closed rule is unchanged. _fsync_dir uses os.open(dir, O_RDONLY) + os.fsync on POSIX. Windows refuses to open a directory as a file, so it raises and falls through to a ctypes FlushFileBuffers fallback. Asserting that fallback SUCCEEDS against a real directory (rather than mocking it) found a live defect: FlushFileBuffers needs GENERIC_WRITE, and with GENERIC_READ the handle opens fine and the flush fails ERROR_ACCESS_DENIED, degrading to a silent no-op. Clock skew, writer side (4a shipped the fold side). _writer_epoch_ms clamps our stamp so it can never regress: a backwards NTP step would otherwise publish a record that looks older than one we already wrote, shortening our own lease. Clamping upward only lengthens, the safe direction. A new ClockSkewError fails claim() closed, before anything is written, when a peer record is stamped past now + max_skew_seconds: the fold's + max_skew only makes "skew lengthens, never shortens" true inside the bound. A peer behind by more than the bound would have its live lease expired early by our fold (a double grant); that direction is undetectable from a record alone, but the symmetric ahead direction is observable, so we fail closed on the observable half and leave measurement to Inc 0 preflight / Inc 7 gating. GC. compact(retain_seconds) deletes only what it can prove dead past ttl + max_skew + settle + retain; anything unprovable is kept forever. It is group-atomic (deleting a tombstone while its request survives would resurrect a released claim; deleting a request while its heartbeats survive would shorten a live lease), never touches a lease still winning a path, never collects a group containing a ttl-less legacy record, holds tombstoned leases to the same full bound, and collects a corrupt record only once mtime alone proves expiry past that bound. Idempotent and safe to run concurrently. Tests: 41 new in tests/test_fs_claim_log_durability.py. 12 durability cases assert the fsync/replace CALL ORDER over a recording os shim -- the proof is the ordering, since on tmpfs no assertion about a real fsync's effect is possible -- plus the refuse-dir-open platform branch and the real-host dir-sync success. 6 pure skew-matrix cells (ahead/behind/at-bound, and the past-bound cell asserted as the falsifiable case that makes max_skew load-bearing rather than decorative) + 7 writer-guard cases. 16 GC cases including never-delete-live under a skewed clock and under a heartbeat-extended lease, group atomicity, corrupt-by-mtime on both sides of the bound, idempotence, a concurrent-removal race, and the strongest property: compact never changes the answer of the fold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The table-driven GC cases each pin one hazard (skewed clock, heartbeat-extended lease, settle margin, ttl-less legacy, group atomicity, corrupt-by-mtime). This adds the property that subsumes them: over 200 seeded randomized claim/renew/release/advance histories, spanning the settle x max_skew x ttl x retain space, compaction never changes the answer the fold gives. Seeded, so any failure is exactly reproducible. Ran clean over 300 unseeded trials during development before being pinned to a fixed seed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Heads-up for whoever picks this up: CI will not run on this PR while it is stacked. Checks will fire once #697 merges and GitHub auto-retargets this base to |
|
Full Python suite re-run on the exact committed tree (all three commits, including the seeded GC property sweep): That supersedes the 4372 figure in the PR body, which was captured one commit earlier. |
…into feat/multibox-inc4b-durability # Conflicts: # tests/CLAUDE.md
…into feat/multibox-inc4b-durability # Conflicts: # tests/CLAUDE.md
Resolves PR #722 conflict by: - Accepting main's reformatted CLAUDE.md structure for write_api.py - Retaining snapshot optimization documentation (2026-08-03) - Composing with #722's FsClaimLog durability/skew/GC increments - Preserving OCC WriteConflict guard and snapshot==full-replay invariants Code merges cleanly: - coordination.py: snapshot optimization from main merged - write_api.py: snapshot optimization from main merged - fs_claim_log.py: durability/skew/GC from #722 preserved - Subprocess encoding from PR #764 applied throughout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stacked on #697 (Inc 4a). Base is
feat/multibox-inc4a-fsclaimlog, notmain.Closes the three things Inc 4a deliberately left open. All production changes are in
state_store/fs_claim_log.py; the pure fold from 4a is untouched and its 67 tests stay green.Durability
_appendno longer writes the final name directly. It writes a temp name in the same directory(
os.replaceis only atomic within one filesystem),flush()+os.fsync()to make the BYTESdurable,
os.replaceto publish the name atomically, then_fsync_dir()to make the directoryENTRY durable. The order is the contract: a peer must never see a record name whose bytes are not
yet durable, and no reader may see a partially written
.json.The temp suffix is deliberately not
.json, so_read_recordsskips it. A crash mid-writeleaves a file that was never published — no grant was ever made from it, so ignoring it
reproduces the exact pre-write state. This is not a weakening of 4a's fail-closed rule: a
truncated
.jsonwas published and still blocks every path (regression test included).A real defect the tests caught
_fsync_dirusesos.open(dir, O_RDONLY)+os.fsyncon POSIX. Windows refuses to open adirectory as a file, so that raises and falls through to a ctypes
FlushFileBuffersfallback.Asserting that the fallback succeeds against a real directory — rather than mocking it — found
a live bug:
FlushFileBuffersrequiresGENERIC_WRITE. WithGENERIC_READthe handle openshappily and the flush then fails
ERROR_ACCESS_DENIED(5), i.e. the helper degrades to a silentno-op — precisely the failure mode a "best-effort" path hides best. Also fixed: undeclared
argtypestruncate the returned HANDLE to a C int on 64-bit.Clock skew (writer side; 4a shipped the fold side)
Ordering stays lamport-keyed and skew-immune; only TTL uses the wall clock. Two guards:
_writer_epoch_ms()clamps our stamp so it can never regress. A backwards NTP step wouldotherwise publish a record that looks older than one we already wrote, shortening our own
lease. Clamping upward only LENGTHENS — the safe direction.
ClockSkewError(new) failsclaim()closed, before anything is written, when a peerrecord is stamped past
now + max_skew_seconds.Rationale for (2): the fold's
+ max_skewonly makes "skew lengthens, never shortens" trueinside the bound. A peer behind by more than the bound would have its live lease expired early
by our fold — a double-grant. That direction is undetectable from a record alone (an old-looking
stamp is indistinguishable from an old record), but the symmetric ahead direction is directly
observable, so we fail closed on the observable half and leave measurement of both halves to Inc 0
preflight / Inc 7 startup gating.
Skew matrix (re-derived from the shipped fold)
For every
|skew| <= max_skewthe computed deadline is>=the true deadline, so no early expiryever. The past-bound cell is asserted as the falsifiable case — that is what makes
max_skewaload-bearing measured precondition rather than decoration (same spirit as Inc 6 assertion 2).
GC
compact(retain_seconds=0.0) -> intdeletes only what it can prove dead pastttl + max_skew + settle + retain; anything unprovable is kept forever.all. Deleting a tombstone while its request survives would resurrect a released claim;
deleting a request while its heartbeats survive would silently shorten a live lease.
top of the deadline test); the
+ max_skewterm keeps that true for a skewed peer.not at GC time), so no deletion depends on how fresh a peer's listing is.
mtime + default_ttl + max_skew + settle + retain— verified to sit a full settle window afterthe fold stops treating them as a live unknown-holder claim.
error; an unreadable directory collects nothing).
Tests
42 new in
tests/test_fs_claim_log_durability.py; 4a's 67 unchanged and green.osshim —fsync -> replace -> open -> fsync -> close. The proof is the ordering, since on tmpfs noassertion about a real fsync's effect is possible. Plus the refuse-dir-open platform branch,
same-directory temp name, temp invisible to the fold, truncated
.jsonstill blocks, and thereal-host dir-sync success assertion (the one that caught the
GENERIC_WRITEbug).settle margin, ttl-less legacy, group atomicity, corrupt-by-mtime on both sides of the bound,
idempotence, concurrent-removal race,
compactnever changes the answer of the fold, and aseeded property sweep of 200 randomized claim/renew/release/advance histories across the
settle x max_skew x ttl x retainspace asserting that same invariant.Hermetic throughout: tempdirs only, injected clock, no real sleeps, no network, no cwd pollution.
Gates
tests.test_fs_claim_log67 OK (4a, unchanged) -tests.test_fs_claim_log_durability42 OKtests.test_claim_backend22 OK (Inc 2 contract suite still runs againstFsClaimLogunmodified)npm run test:node315/315 -npm run test:shexit 0claudemd_lint/claudemd_sync_gate/claudemd_contract/verify_test_suite_count/metrics_gate/ci_gate_runability/dispatch_lint/pre-push-policy.sh --testall cleansecret_scan.py --stagedexit 0Docs
First commit prunes
state_store/CLAUDE.md148 -> 88 lines by densifying verbose bulletsections into prose — no information removed, every contract fact, event type, API name and
measured number retained (word-level diff verified). Inc 4b's section brings it to 96/150,
leaving 54 lines of headroom for Inc 5 and Inc 7.
Do not merge before #697.
🤖 Generated with Claude Code