Skip to content

[WIP] CAS draft (adopting to CI/CD, not for review / merge)#2066

Closed
filimonov wants to merge 2238 commits into
Altinity:antalya-26.6from
filimonov:cas-gc-rebuild
Closed

[WIP] CAS draft (adopting to CI/CD, not for review / merge)#2066
filimonov wants to merge 2238 commits into
Altinity:antalya-26.6from
filimonov:cas-gc-rebuild

Conversation

@filimonov

Copy link
Copy Markdown
Member

Changelog category (leave one):

  • Not for changelog (changelog entry is not required)

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

content addressable storage - draft PR

Documentation entry for user-facing changes

TBD.

filimonov and others added 30 commits July 14, 2026 21:22
…-read caches rejected as alien to CA

User rejected the files-cache iteration too: comparing the current
backup against the previous one via filesystem metadata is new murky
side state, unnatural for content-addressed storage. Final form:
consolidation streams every hot file through the ordinary write path
(hash under the pool algo -> putIfAbsent/cold-reuse); incrementality and
manifest reuse fall out pool-side — the pool is the only index. The
trickle warmer needs no bookkeeping (age-threshold crossing is
one-shot; cursor loss = harmless re-hash). checksums.txt equality,
inode witnesses, and borg-style files caches are all recorded as
rejected skip-read shortcuts. Cost stated honestly: one streaming
read+hash of the hot tier per consolidation (~minutes/TB daily); the
lever if ever measured hot is pluggable hash speed, not bypassing
hashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t to §3)

User call: throw out the §7 dedup-skip-HEAD lever as unsafe. Unlike §3
(reader holds a live committed ref pinning the blob), the dedup path
producer holds no ref at HEAD time; after §5 an absent .meta reads as
Clean even for a fully-deleted blob, so the HEAD is the only thing that
distinguishes present from gone. Skipping it would admit a dangle, not
elide a bug-net. The dedup HEADs stay; recorded why so a future round
does not re-propose it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…ata loss

Optimization-round §5 ("absence-means-Clean blob meta") as specified
re-introduces the data-loss hole fixed on 2026-07-11: its Transition 5
(GcSpareHeal — a spare clears the condemned tombstone) keeps the
condemn-time token t1, so a stale deposed GC leader's pre-CAS
`deleteExact(t1)` deletes the live blob. This is the removed
`clearSparedMeta` "spare = clear" under a new representation; the
2026-07-11 Fix 4 made GC freshness metadata add-only for exactly this.

The §5 gate `CaMetaAbsenceClean.tla` was false-green: single-leader
model whose `GcSpareHeal` atomically empties `queuedDeletes` (line 188),
which the real code cannot do to a deposed leader's captured pre-CAS
delete. A probe with a faithful spare goes RED on
`INV_ABSENCE_NO_QUEUED_DELETE` in 37 states.

Confirmed five ways: the 2026-07-11 report; a gate-fidelity audit
(false-green); a premise-refutation audit (recovery is not only via a
token-displacing resurrect — `Build::adoptEvidence` re-references at
exact t1); the existing add-only guard tests still asserting
Condemned-after-spare; and the mechanical TLC probe. No unsafe code
landed (implementer WIP halted uncommitted, preserved in a stash).

§5 blocked for this round; revive add-only (drop Transition 5) behind a
rebuilt faithful two-leader + adoptEvidence-recovery gate plus the
absent-meta read-path design. A separate, pre-existing `adoptEvidence`
source-dropped-mid-relink exposure is filed for its own investigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…ality

Two per-event wastes on the hot emitter thread of the opt-in
content_addressed_log: (a) makeCasEventSink deep-copied every field incl.
the full std::map detail because the sink took `const CasEvent &`;
(b) the `reason` column was a full String though it is templated
rationale. Make CasEventSink take the event by value and emitEvent
rvalue-only (a missed call site becomes a compile error, not a silent
copy); makeCasEventSink std::moves each field into the log element;
`reason` becomes LowCardinality(String). Behavior-preserving.

Tests: ReasonColumnIsLowCardinality (schema) and EmitEventMovesSourceIntoSink
(source event moved-from after emit). Sweep 985 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
Records the user decision to drop §5 entirely (not revive): spec §5 gets
an ABANDONED banner (design-history only), the finding report disposition
switches from blocked-pending-revival to abandoned (WIP stash dropped),
and the worklog captures the §6 completion and the §5 close-out. The
optimization round ships §1–§4 + §6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…e exposure (ClickHouse#42)

Read-only reachability investigation. Verdict PLAUSIBLE-NEEDS-REPRO
(scoped): all local-source relinks are REFUTED-UNREACHABLE (the source
is held as a live DataPartPtr; grabOldParts pins the blob at in-degree
>= 1 through the adopt->precommit window). Only fetch-by-relink (remote
source) is a residual low-probability exposure (short window; needs a
stalled stageManifest PUT across >= 2 GC folds + all-replica part drop +
a 2026-07-11 captured deleteExact(t1) firing post-promote).

§4 (8fe6331) widened it: it removed the promote-time
copyForwardFromCondemned that displaced a condemned adopted leaf's body
to a fresh token, the defence that made a captured deleteExact(t1) a
TokenMismatch no-op. Distinct from and outside the D4 content-trust
envelope (a temporal race that fails silent / fsck-only). Fix (task ClickHouse#43):
a liveness re-check at the GC pre-CAS deleteExact site closes the whole
deposed-leader same-token class; needs its own TLA gate + second consult.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…point

Design approved 2026-07-14: `uuid.txt`, `metadata_version.txt` AND `txn_version.txt`
become ordinary manifest tree entries; the per-ref `mutable_files` concept is deleted
end-to-end. Committed-part standalone writes (add+overwrite) and surgical removes go
through an audited manifest repoint over the existing `publishEntries` sequence.
MVCC tmp+rename is short-circuited on atomic-write storages (generic, upstream-candidate),
which deletes the B182 eager-dispatch hook — the fork diff outside ContentAddressed/
shrinks below today's. B123 unlink no-op evolves into staged removal marks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…a retention pin

Houses tasks ClickHouse#42/ClickHouse#43 in the r/o-replica snapshot-pin design as §8.1: the
fetch-by-relink commit-before-release gap (sender fire-and-forget releases
the source part before the receiver's adoptPartFromManifest commit → a
deep-tail same-token dangle when the receiver's edge-PUT stalls across >=2
GC folds) is closed the right way by reusing the GC retention floor — the
fetching replica takes a transient retention pin on the source snapshot for
the relink duration. No bespoke handshake (the Poco interserver transport is
half-duplex; no reverse ACK). Until the pin lands, ClickHouse#42/ClickHouse#43 is interim-accepted
(fsck backstops; deleteExact covers all token-change) — recorded as a comment
at adoptPartFromManifest. B66b (relink-into-detached) inherits the same handoff
and rides the same pin; B66a (local detached-ref atomicity) is housed alongside
as a detached-adjacent but pin-orthogonal item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…Repoint TLA gate)

Plan for spec 994507d: repoint primitive over publishEntries (+allow_repoint
promote mode matching the existing WRepoint TLA transition), publishStaging
carry-forward, MVCC atomic-write short-circuit, writeFile flip, relink
self-containment (cookie v2), B123 removal marks, mutable_files schema deletion
sweep, freeze special-case removal, docs+validation gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…decommission

Extracts the writable-mount tail of `Store::open` into `Store::mountWritable` with
a claim policy (`WaitForExpiry` for normal opens, `NoWait` for decommission), adds
`readOwnerUuid`, and the victim-impersonation factory per
docs/superpowers/specs/2026-07-13-cas-pool-member-decommission-design.md. `NoWait`
refuses immediately (no FORCE variant, no wait-and-observe) when the victim's mount
lease is not `Claimed`/`FencedSelf`, instead of the bounded reclaim wait a normal
open pays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…`member_decommission` events

Task 2 of the pool-member decommission plan: the ORCHESTRATOR that erases every
namespace of a DEAD pool member via the ordinary `Store::dropNamespace` writer
path (acting as a WRITER, never GC, never inventing a ref transition), over an
admin mount obtained through Task 1's `Store::openForDecommission`. Adds
`DropNamespaceStats` (`Store::dropNamespace` now reports the committed/precommit
counts its removal transaction named), `Core/CasDecommission.h/.cpp`
(`DecommissionReport` + `decommissionPoolMember`), and the `MemberDecommission`
audit event (`member_decommission`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…vent test

Task 2 review found two Important issues in `decommissionPoolMember`:

1. `DropNamespaceStats::precommits` (and `DecommissionReport.precommits_removed`)
   read 0 for realistic victims. `appendRefOps` hoists `maybeSweepStalePrecommits`
   at its top for every call, including `dropNamespace`'s own; since the admin
   decommission mount always mints a strictly-higher `writer_epoch` than the dead
   victim's, the hoisted sweep reclaims the victim's real precommit bindings in
   its own transaction before `dropNamespace`'s `build_ops` ever counts them.
   Fixed by threading a `skip_stale_precommit_sweep` flag through `appendRefOps`,
   set only by `dropNamespace`'s own call: its removal transaction already names
   every current precommit (stale or not) via `RemoveNamespace`, making the
   maintenance sweep redundant on this one path. No other caller is affected.

2. The `member_decommission` begin/namespace_removed/end events were emitted but
   asserted by no test.

Added `CasDecommission.CountsRealisticEpochPrecommit` (seeds a precommit at the
victim's own, lower-than-admin `writer_epoch` -- unlike the existing fixture's
artificially-high 999999 seed, which dodges this exact race) and
`CasDecommission.EmitsMemberDecommissionEvents` (capturing-sink event assertion).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…sk-agnostic

B1 proposed a CA-specific manifest_hash field on the ReplicatedMergeTree
/parts znode for cross-replica header-divergence detection. Rejected: it would
couple ReplicatedMergeTree to one disk implementation and grow the fork's
upstream-contact surface. Unnecessary too — fetch-by-relink learns the manifest
id in-band (interserver handshake), real divergence is already caught by the
stock checkPartChecksumsAndCommit, and manifest-level divergence is a benign,
self-cleaning dedup-MISS (01-architecture.md $benign-cross-replica-divergence).
Retires a former release GATE. Observability, if ever wanted, is pool-side
(fsck/ca-inspect), never a Keeper field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l to read-replica)

The fetch-by-relink commit-before-release fix (ClickHouse#42/ClickHouse#43) shares only the GC
retention-overlay primitive with the read-only replica feature; owner, payload,
lifetime, self-cleaning, and publisher all differ, and it needs none of the
reader-mount / snapshot-window / heartbeat / readonly-refresh machinery — so it
is independently shippable. New spec captures the converged mechanism: a
sender-created, receiver-build-owned, epoch-floor pin published before the part
is released (true zero-gap commit-before-release), released by the receiver
after its ref commit; bulk write-replica warm-up as a future generalization;
interim option-C status; the detached cluster (B66a/B66b). Read-replica §8.1
shrinks to a cross-reference; §5.2 is framed as the shared primitive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 3 of the pool-member decommission plan: after namespace erasure
(Task 2), reclaim the victim's physical debris. `sweepNamespace` now
returns the count of manifest bodies it actually deleted (classified via
`DeleteClass::Deleted`), which the manifest-debris phase sums across every
eligible old-epoch build prefix under the victim's `cas/manifests/` tree.
A new `deleteListedPrefix` helper drains the victim's `staging/<srid>/`
and `roots/<srid>/` prefixes by exact-token delete.

Fail-close nuance (spec §core "Fail-close"): a per-object failure in the
staging/roots drain -- a thrown exception or a `TokenMismatch`/`NotFound`
delete outcome -- is recorded as a warning and the sweep continues; this
is the only tolerated-and-continue class in the feature. A non-empty
`DecommissionReport::warnings` is the signal the (future) slot-deletion
phase gates on, so a re-run can finish an interrupted drain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
Add CasDecommission.PerObjectFailureWarnsAndContinuesDrain: a
FailingDeleteBackend fixture drives deleteListedPrefix's per-object
fail-close path directly -- one thrown exception and one TokenMismatch
outcome must each record a warning and let the rest of the drain
proceed, rather than aborting the phase or silently counting a miss as
deleted. Complements DrainsDebrisStagingAndRoots, which only exercised
the clean-drain path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…ent + README registry

Approved brainstorm outcome (supersedes codecs proposal v2):
- every persisted metadata format becomes human-readable text: JSON control
  plane (ReadHelpers/JSONEachRow primitives), sorted-NDJSON GC runs (no seek,
  no blocks; seal-held checksum verified on whole reads), part manifest as
  JSON descriptor + head -v-style banner payload zone, blob envelope as a
  256-byte JSON header line; only payload bytes stay binary
- full sweep: refsnaplog (cas_ref_log/cas_ref_snap) and the blob-meta sidecar
  convert too; inventory corrected against code (no CARS/CART)
- codecs move to Core/Formats/ with a physical include-layering rule;
  Formats/README.md becomes the living registry (bucket map + codec table +
  evolution rules); protobuf (full runtime + libprotoc) leaves the subsystem
- schema evolution: additive = tolerant keys, breaking = v bump +
  write-down-to-floor, deterministic formats strict + adoption pin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nue contract

Review finding (CRITICAL): the manifest-debris drain (spec §core step 4)
did not honor the same fail-close nuance as deleteListedPrefix (staging/
roots). Two defects: (a) sweepNamespace's per-key head/deleteExact was
unwrapped, so a transient backend exception propagated straight out of
decommissionPoolMember -- staging/roots never ran, no end event, no
report; (b) the protection-view-unavailable path (activeManifestKeys
throws) only LOG_WARNING'd, never reaching DecommissionReport::warnings
-- an unreadable protection view reported success with an undercounted
manifest_debris_removed, which would let the future slot-deletion phase
(gated on warnings.empty()) delete the mount body over an unconfirmed
drain.

sweepNamespace gains an optional out-`warnings` vector (nullptr default
preserves the exact original behaviour for every pre-existing caller):
when non-null, a per-key thrown exception and a protection-view failure
both push a warning and the sweep continues, instead of throwing or
silently under-reporting. The decommission call site now passes
&report.warnings. NotFound/TokenMismatch delete outcomes stay silently
spared either way -- that is the normal "a fresh owner reclaimed it"
race the periodic sweep expects, not a failure.

Added CasDecommission.ManifestDebrisPhaseFailuresWarnAndContinue: a
per-key delete-throws case and a protection-view-unavailable case (an
isolated namespace with manifest debris but no ref objects, so Task 2's
namespace erasure never touches it) both land in report.warnings, and
the run completes normally (namespace erasure and the staging drain both
still succeed) rather than aborting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
Full TDD plan for the foundation phase: CasFormat move + new FormatIds
(RefLog/RefSnapshot/BlobMeta/GcHeartbeat), per-format traits registry,
CasTextFormat (JSON micro-vocabulary, JsonObjectReader, header/trailer
lines, whole-object zstd arm with cap-before-alloc), shared format test
battery + proving instance, Formats/README.md living-registry skeleton.
Phases 2-8 get just-in-time plans against this foundation.

Spec: docs/superpowers/specs/2026-07-15-cas-codecs-v3-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n, fail-close resume

Task 4: the final phase of decommissionPoolMember, strictly last and only
over a clean drain (report.warnings.empty()). A clean drain gracefully
closes the admin Store (Store::~Store's mount-lease keeper stamps the
lease already-expired and folds in the watermark farewell, min_active =
UINT64_MAX -- the terminated state), then deletes the three control
objects (epoch, owner, mount, in that order -- mount last, since it is
the claim/resume anchor). A per-key delete failure here is itself a
drain-incomplete signal: it is appended to report.warnings and the
remaining keys (including the mount) are left alone, so a re-run can
retry. A non-empty warnings set from any earlier phase skips slot
deletion entirely -- the slot stays terminated-but-present, and the
report says so via slot_removed = false plus a LOG_WARNING. Once the
slot is gone, a re-run throws BAD_ARGUMENTS ("unknown pool member"), the
same refusal an srid that never existed gets.

Store gains poolBackendPtr() (an owning BackendPtr, not just a Backend&)
so the slot-retirement phase can keep the backend alive across
admin.reset(). The end MemberDecommission event moves from
EventEmitter{*admin} to calling `sink` directly, since admin is gone by
the time it fires -- this also makes its warnings/slot_removed detail
reflect the truly final state, including a slot-delete failure appended
during retirement itself.

Two pre-existing Task-2 tests assumed a slot survives a clean decommission
run, which is no longer true by design: ErasesAllVictimNamespaces's
tail (re-opening for decommission to verify Removed state) is replaced
with a direct slot_removed assertion, and RerunCountsAlreadyRemoved --
whose "clean run then plain re-run" premise is now structurally
impossible, since a clean run always removes the slot -- is removed;
its assertion is already covered by FailedDrainKeepsSlotThenResumes and
ManifestDebrisFailureKeepsSlotThenResumes (which exercise the same
already-removed-namespace counting via a keep-the-slot-alive resume
instead).

Added CasDecommission.RemovesSlotAndMakesRerunUnknown (the brief's own
test), CasDecommission.FailedDrainKeepsSlotThenResumes (a roots-phase
failure keeps the slot, a disarmed resume finishes and removes it), and
CasDecommission.ManifestDebrisFailureKeepsSlotThenResumes -- a review
follow-up covering the manifest-debris phase specifically, since the
plan's own example only exercises a roots-phase failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…-type, no threshold

Amendment (user, 2026-07-16): compression becomes deterministic per type
(Never / Always / PinnedRaw). Always = can-grow-large types (cas_ref_log,
cas_ref_snap, cas_part_manifest, cas_gc_outcomes) — stored under a .zst
key suffix, compressed regardless of instance size, so constructed-key
point-GETs stay deterministic and listings are self-describing
(zstdcat-able). Always-small types stay bare cat-able raw; deterministic
formats (cas_run, cas_fold_seal) stay raw for byte-adoption. The size
threshold dies; body magic is validated against the policy (compressed
body only legal on Always; raw readable anywhere as the repair path).
storedSuffix(FormatId) added to the phase-1 traits registry; compression
lands with each format's own cutover instead of a separate step 8 flip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, interpreter with one-row report

Task 5 of the pool-member decommission plan: the SQL surface over
Cas::decommissionPoolMember (Task 2) / Store::poolBackendPtr,
Store::poolConfig (Task 4).

- ASTSystemQuery: new Type::CONTENT_ADDRESSED_DROP_POOL_MEMBER, grouped
  with the two existing CONTENT_ADDRESSED_* enumerators (its magic_enum-
  derived keyword sequence is CONTENT ADDRESSED DROP POOL MEMBER);
  formatImpl round-trips `<srid> FROM DISK <disk>` as quoted string
  literals (an srid is an opaque server-root path, not identifier-shaped,
  unlike the sibling commands' bare disk target).
- ParserSystemQuery: parses the required srid and disk string literals
  plus FROM/DISK keywords, then ON CLUSTER via the same
  parseQueryWithOnCluster every other subcommand uses.
- AccessType: SYSTEM_CONTENT_ADDRESSED_DROP_POOL_MEMBER (GLOBAL, SYSTEM),
  grouped with its CONTENT ADDRESSED siblings; 01271_show_privileges
  reference updated with the corresponding row.
- InterpreterSystemQuery: reuses the disk-detection idiom already
  established by runContentAddressedGarbageCollection/
  runContentAddressedGcRebuild (getMetadataStorage() + isContentAddressed()
  + dynamic_cast, tolerating NOT_IMPLEMENTED from a non-object-storage
  disk) rather than a fresh dynamic_cast<DiskObjectStorage*> chain, and
  the MutableColumns/SourceFromSingleChunk one-row-result idiom
  SYNC_FILESYSTEM_CACHE already uses. Returns one row: srid,
  namespaces_removed, namespaces_already_removed, committed_refs_removed,
  precommits_removed, manifest_debris_removed, staging_objects_removed,
  mountpoint_objects_removed, slot_removed, and a semicolon-joined
  warnings string. Wired into getRequiredAccessForDDLOnCluster.
- Grammar/dispatch-only stateless test (0_stateless/05013_...): a
  nonexistent disk fails UNKNOWN_DISK before any CA-specific logic runs;
  real execution needs a CA disk, covered by the integration test.
- New src/Parsers/tests/gtest_system_content_addressed_drop_pool_member.cpp:
  parses-and-round-trips, ON CLUSTER acceptance, and the two required-
  argument failure shapes -- real grammar coverage runnable via
  unit_tests_dbms, without needing the full clickhouse server binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
Task-1 carry-forward, escalated by the Task-4 review: slot-retirement's
crash-safety (deleting epoch/owner/mount in that order, stopping on the
first failure) relies on Store::openForDecommission's owner-anchor-absent
+ mount-lease-present fallback ("partial hand-cleanup: adopt from the
lease", CasStore.cpp) to resolve identity on a re-run after a crash
between the second and third delete. No test exercised this path.

Add CasDecommission.MidRetirementCrashResumesViaMountLeaseFallback:
claims the slot once (as decommissionPoolMember's own first step would),
closes it gracefully, then manually strikes epochKey+ownerKey (leaving
mountKey) -- the byte-identical aftermath of that crash window -- and
asserts a re-run resolves identity via the fallback and finishes
retiring the slot.

Investigation finding worth flagging: claimOwnerOrThrow (CasServerRoot.cpp)
gates the owner-absent path a SECOND, stricter way -- it only re-claims
over a PROVABLY EMPTY data subtree (serverRootSubtreeEmpty:
cas/refs/<srid>/, cas/manifests/<srid>/, roots/<srid>/ all empty). A
victim with a real table trips this: decommission's drain phases never
physically delete ref-log/snapshot debris (that is GC's namespace-cleanup
item's job, on its own later schedule), so the subtree stays non-empty
right after even a fully successful drain. The fallback this test proves
therefore only unconditionally covers a victim with NO namespaces at the
moment of the crash; for a victim that had real tables, the SAME
mid-retirement crash would currently be refused ("identity lost over
existing data") until GC independently clears the residual debris. This
is a real narrowing of "mid-retirement crash-safety" worth a follow-up
decision, not something this task's scope covers fixing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…ber decommission

Wires the offline decommission core (`Cas::decommissionPoolMember`, landed in
Tasks 2-4) into a `clickhouse-disks` command: `ca-drop-member <srid>` opens the
CA disk read-only, resolves its `ContentAddressedMetadataStorage`, and prints
the `DecommissionReport` fields (namespaces/refs/precommits/manifest-debris/
staging/mountpoint counts, `slot_removed`, warnings). Pattern follows
`CommandCaGcDryRun.cpp`; positional-argument idiom follows `CommandCaInspect.cpp`.
Task 7 covers the shared core end-to-end; this is a facade-only, manually
smoke-tested addition (no functional pool run here).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…o gtest_Parser.cpp

Review intel (verified brief-drift pass): no ParserSystemQuery gtest
existed before Task 5, but src/Parsers/tests/gtest_Parser.cpp has the
established, reusable parameterized ParserTest fixture (round-trip via
formatted-AST equality; every other SYSTEM/DDL parser's coverage already
lives there) -- auto-globbed into unit_tests_dbms, no CMake edit needed.
Added an INSTANTIATE_TEST_SUITE_P(ParserSystemQuery, ParserTest, ...)
block there instead of a bespoke gtest file, matching the sole existing
convention for this kind of coverage.

This round-trip check caught a REAL bug the previous ad-hoc field-check
test missed: CONTENT_ADDRESSED_DROP_POOL_MEMBER was never added to
formatImpl's queries_with_on_cluster_at_end set, so `... FROM DISK
'disk1' ON CLUSTER my_cluster` formatted back as `... ON CLUSTER
my_cluster 'srv1' FROM DISK 'disk1'` -- ON CLUSTER printed BEFORE the
srid/disk arguments instead of after, even though the grammar parses it
last (ParserSystemQuery.cpp). The now-superseded gtest_system_content_
addressed_drop_pool_member.cpp only asserted query->cluster == "...",
which passes regardless of formatImpl's argument order, so it never
caught this. Fixed by adding the type to that set (mirroring
CLEAR_FILESYSTEM_CACHE/SYNC_FILESYSTEM_CACHE/CLEAR_QUERY_CACHE, the
other commands whose own arguments must print before ON CLUSTER).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
Task 8 of the decommission plan: `04-gc-protocol.md` gains the
fence-vs-decommission subsection (fence settles liveness only; the dead
member footprint is erased solely by the deliberate
`SYSTEM CONTENT ADDRESSED DROP POOL MEMBER` writer-role operation, and
`GC` still never invents a ref transition; known fail-closed narrowing
documented). `05-formats-and-backend.md` B200 entries flip to
implemented-without-roster with the Part IV roster forward-hook. The
backlog records B200 done with all landing commits and adds the
decommission-under-load scenario-card follow-up. The spec status is
stamped implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…P POOL MEMBER`, pool heals

Task 7 of the pool-member decommission plan: a two-node CA/RustFS
integration test modeled on test_content_addressed_shared_pool's
two-node topology (cluster fixture, per-node server_root_id override
files) combined with test_content_addressed_ref_snaplog's RustFS disk
config and read-only clickhouse-disks fsck idiom (MinIO cannot serve CA
pools).

Flow: node2 gets its own table and inserts (data that must NOT
survive), node1 gets its own table (data that MUST survive); hard-kill
node2 (SIGKILL, no graceful farewell); wait for node1 to observe
node2's mount as no longer live via system.content_addressed_mounts;
run SYSTEM CONTENT ADDRESSED DROP POOL MEMBER from node1 and assert the
one-row report (namespaces_removed >= 1, slot_removed = 1, warnings =
''); assert node1's data is untouched and node2's srid is gone from the
mounts table; drive GC to reclaim node2's now-unreferenced content and
poll the RustFS blob prefix back to baseline; assert a read-only fsck
reports dangling=0 and unaccounted=0; re-run the same command and
expect BAD_ARGUMENTS ("unknown pool member").

Execution status: BLOCKED on infrastructure, not a test-code defect.
Ran the new test twice (identical timeout: node1's clickhouse-server
never starts, container running but zero log output). Isolated the
cause with two controls: test_content_addressed_ref_snaplog (single-
node, RustFS) PASSES cleanly; test_content_addressed_shared_pool (the
pre-existing two-node MinIO model test, unmodified) FAILS identically.
The environment cannot currently start ANY two-node integration
cluster, independent of RustFS/MinIO or this test's own config. See
.superpowers/sdd/decom-task-7-report.md for the full diagnostic.

The deferred Task-5 stateless test (05013_system_content_addressed_
drop_pool_member) was run separately via the stateless job selector and
PASSES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…n integration-test configs

The three config files carried ` -- ` inside XML comments, which the XML
spec forbids; config merge failed with a SAXParseException before the
server opened its log files, so both nodes timed out at cluster setup
with an empty logs directory. This was the real cause of the Task-7
integration-run failure — not a broken docker environment: the model
test fails for a different, deterministic reason (it mounts the CA disk
over MinIO, and the capability probe correctly fail-closes because MinIO
does not enforce conditional deletes), which made the two failures look
identical at the symptom level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
node1 mounts the shared pool through two disks (the writable disk plus
the read-only fsck window), so `system.content_addressed_mounts` carries
one row per disk view for the same srid and the liveness wait got two
rows where `assert_eq_with_retry` expected one. Aggregate with `min` so
the assert sees a single row regardless of the disk count. First live
run after the XML fix reached this assertion (104s in, cluster up, both
nodes serving) — the remaining steps run next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
…ause; parse TSV

`ParserSystemQuery` is not part of `ParserQueryWithOutput`, so
`SYSTEM ... FORMAT JSONEachRow` is a syntax error (verified empirically:
every SYSTEM query rejects a FORMAT clause). Parse the default TSV row
instead, with the column order documented from the interpreter result
block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfRKU1Tt1CnaiFYh3kue27
filimonov and others added 26 commits July 17, 2026 12:07
…d-MOVE-kill atomicity never actually verified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…T queue B199 + S37-oracle before R5 resume

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…shing step 5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…lost, S39/S36/S37, 20m soak)

T4 gate results on the fixed binary (7748419):
- dl_probe (original acked-then-lost reproducer, now tracked): LOST(acked-but-absent)=0,
  submitted=2466 acked=2466 (pre-fix baseline was ~198/1314 lost).
- S39 (ClickHouse#37 fence tolerance) at scale=dev: 11/11 verdicts pass, matching the R3 landing run
  (8e39ea2). scale=ci hit a pre-existing scenario-config bug unrelated to this fix
  (short_fault_s=15 >= _MOUNT_RENEW_PERIOD_S=10, baked in since the S39 card's creation,
  ci never previously exercised) -- not a regression, left for a separate fix.
- S36 (MOVE PART/PARTITION): 26/26 pass.
- S37 (multi-disk policies): 22/23 pass; the one red is the known pre-existing
  MOVE-PARTITION-under-kill duplicate-row oracle artifact (rows=200 vs expected 100),
  tracked as GREEN-DEBT (docs/superpowers/cas/BACKLOG.md "S37 chaos-leg oracle bug"),
  not caused by this fix.
- 20-minute soak, seed=42 (the chaos recipe that exposed R4's loss), sync inserts:
  checkpoint model == observed at every checkpoint (count=813264), fsck clean
  (unreachable=0 dangling=0), no wedges.

This commit also carries the scenario-suite run history that had accumulated uncommitted
in this shared worktree from the ongoing prod-scale campaign (Step 9 / ClickHouse#38) since 2026-07-15,
appended here alongside the T4 rows -- nothing new was run under this commit besides the
dl_probe and soak-phase3 rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…reation, first ci run in DL-fix T4)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…sue draft (not filed), position: GREEN-DEBT then R5 restart from S01 on fixed binary

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…Directory comment updated to the new renameParts-publish reality, RUN_HISTORY chronology

Final whole-branch review: READY, zero Critical/Important.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…r overlay, no precommit API) — this working-tree revision was the authoritative text the DL-fix analysis referenced; found uncommitted during session-pause sweep

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…VE-leg PART_IS_TEMPORARILY_LOCKED retry hardening; S39 ci short_fault_s 15->9 + invariant comment

S37 oracle fix VALIDATED: dev rerun PASS 23/23 (run2, pre-hardening) — FIRST valid
mid-MOVE-kill atomicity verdict: GREEN, no product defect. Pending on resume:
one hardened-card S37 validation run + S39 --scale ci rerun (fix applied, unrun).
Run1 transient documented: PART_IS_TEMPORARILY_LOCKED race at the MOVE-back leg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
…night), R4 10ms query-profiler compose config, scenarios/BACKLOG night findings, stale tmp script deletions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
… GREEN-DEBT S37 23/23 validated, S39-ci/B199 pending; R5 restarts from S01 on fixed binary)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PeC1dg2dXBh55xkaZ63U1P
# Conflicts:
#	programs/disks/DisksApp.cpp
#	programs/disks/ICommand.h
#	src/Disks/DiskObjectStorage/ObjectStorages/Local/LocalObjectStorage.cpp
#	src/IO/S3/Client.h
#	src/Interpreters/InterpreterSystemQuery.cpp
#	src/Parsers/tests/gtest_Parser.cpp
#	src/Storages/MergeTree/DataPartStorageOnDiskBase.cpp
#	src/Storages/MergeTree/DataPartStorageOnDiskFull.cpp
#	tests/queries/0_stateless/02370_lost_part_intersecting_merges.sh
…c references

Behavior-preserving cleanup of src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/:
- Replace development-diary markers (milestone tokens, `spec §`/RFC pointers, task/phase
  codenames, dates) with self-contained explanations; the technical content (invariants,
  orderings, failure behavior) is kept or expanded in place.
- Documentation coverage for classes and nontrivial methods; contract comments in headers,
  mechanics near the implementation.
- Mechanical hygiene: duplicate/unused include prunes, a write-only local in `CasProbe`,
  a dead `if (detail || true)` wrapper in `CasFsck`, single-use `CAS_STRONG_STRING` macro
  expanded into a plain `RootNamespace` class, Allman reformat of a few one-liners.
- Review fixes on top of the base sweep: restored `kRefRecoveryMaxRestarts` (was deleted
  while still used), corrected an inverted trust-model description in
  `ContentAddressedExchange.h` (relink promotion trusts adopted refs via the durable
  manifest edge; no per-blob re-probe), corrected `BlobHashAlgo` persistence claim
  (byte lives in the source-edge run format, not pool metadata), removed an invented
  `header_hash` rationale, repaired garbled/mis-indented rewrites, and renamed `<table>`
  doc-comment placeholders to `<tbl>` to satisfy `-Wdocumentation-html`.

Validated: `dbms` and all CAS gtest objects compile cleanly (RelWithDebInfo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Top-level README: expand the bare layer map into a usable orientation —
what the storage is, the five persisted object kinds, write/read/fetch/GC
lifecycle walkthroughs, a config example with the observe-only mode, the
operator verbs (`fsck`, `ca-inspect`, `ca-gc-dryrun`, `ca-gc-rebuild`,
`ca-drop-member`), `system.content_addressed_log`, and how to run the
tests (including the comprehensive gtest filter — a bare `Cas*:CA*`
silently skips the `RefWriter*` suites).

Formats/README: drop internal design-document pointers and the completed
migration diary in favor of the timeless statement (the inventory is text
end to end), fix the stale part-manifest key shape
(`cas/manifests/<ns>/<epoch-hex>-<seq-hex>/<ordinal>.zst`), and split the
hex-width rule into fixed-width `UInt128` identities (32 chars) vs
algo-length blob digests rendered as `algo:hex`.

All command names, key shapes, config fields, job names, and gtest suite
names verified against the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Symbols with zero references anywhere (including tests), verified by grep
and by the compiler's -Wunused-private-field after removal:

- `CasCodecUtil.h`: `writeU128LE`, `readU128LE`, `writeLenPrefixed`,
  `readLenPrefixed`, `decodeGuarded` (only the big-endian UInt128 form,
  `readFixedBytes`, and the name/ref validators remain); pruned the
  now-unused includes and error-code externs.
- `CasTypes.h`: `DigestCodec::digestLen` accessor.
- `CasGc`: `readSealedCursors` (private, zero callers) and the
  `foldManifestEdgesForTest` seam no test uses.
- `CasServerRoot`: `SingleWriterSlot::lastRenewTime` and its write-only
  `last_renew_time` backing field.
- `ContentAddressedMetadataStorage`: `serverId` accessor.
- `PartFolderAccess`: `PartFolderView::manifestSize`/`publishedAtMs`
  accessors, the write-only `published_at_ms` field (constructor narrowed
  accordingly), the `clearForTest` hook no test calls, and the never-produced
  `LastDecision::Mismatch` enum value (diagnostic-only enum, not persisted).
- `CasBlobInDegree.h`: `kSourceEdgeKeySchema` constant (the self-describing
  key property it documented is stated on `SourceEdgeKeyCodec`).
- `CasGcShardPlan.h`: `CoordinatorPlan::shardCount` and the field behind it
  (the policy object is stateless; its booleans are constant for any
  `gc_shards >= 1`).

Validated: `dbms` and all CAS gtest objects compile cleanly, including the
adjusted `gtest_cas_part_folder_view.cpp` constructor call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The predicate family (`hasSingleGlobalFence`, `allowsPerShardFence`,
`requiresLeaseForReduce`, `requiresCoordinatorForSeal`,
`requiresCoordinatorForFence`) returned constants and was consulted by
nothing in production — only its own two gtests read it. Removed the class
and those tests.

The invariants it encoded are real and stay documented as a free-standing
comment in `CasGcShardPlan.h` (single global coordinator fence — a publish
into one root shard can protect a blob in any target shard, so a per-reducer
fence is unsafe; reducer work is lease-free, the lease being work-dedup
only). They are enforced by the actual round code (`Gc::fence`,
`CasGcScheduler`), not by this object.

The `CasGcShardCoordinator.ShardedFoldRoutesDeltasToOwningShards` test
remains: it exercises the live `blobShard` fold routing, not the removed
policy object.

Validated: `dbms` and the affected gtest objects compile cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three source-edge row tags (`kEdgeActive`, `kZeroMarker`, `kCondemned`)
are bytes of the `cas_run` row format, so they belong in the codec header
that owns that format. The separate Primitives header existed only because
the tags were originally extracted from the Gc-layer `CasBlobInDegree.h`
and needed a home the Formats codec could include; `Formats/` itself
satisfies that (`Gc → Formats` is the allowed include direction), and
`Gc/CasBlobInDegree.h` already includes `CasRecordStreamFormat.h`, so the
merge adds no new include edges anywhere.

The sentinel-key contract comment moves with the constants; the README's
Primitives inventory is updated.

Validated: `dbms` and all CAS gtest objects compile cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Earlier source-layout merges concatenated files, leaving mid-file
`#include` batches (with outright duplicates), second `ErrorCodes` blocks
buried hundreds of lines down, a `namespace fs` alias declared at line
1385, and gratuitous close/reopen churn of the same namespace. Cleaned,
behavior-preserving:

- `Parts/PartFolderAccess.h`: three include batches (with duplicated
  entries) consolidated at the top; three `DB::ContentAddressed` blocks
  merged to two (the surviving split carries the required `DB::Cas`
  forward declaration).
- `Parts/PartFolderAccess.cpp`: mid-file includes and the
  `ErrorCodes`/`ProfileEvents`/`CurrentMetrics` extern blocks hoisted to
  the top; single `DB::ContentAddressed` block.
- `Pool/CasServerRoot.cpp`: duplicate mid-file includes and a fully
  redundant second `ErrorCodes` block deleted; three `DB::Cas` blocks
  merged into one. `Pool/CasServerRoot.h`: three `DB::Cas` blocks merged
  into one.
- `ContentAddressedTransaction.cpp`: `namespace fs = std::filesystem` and
  the `ErrorCodes` block hoisted to the top.
  `ContentAddressedTransaction.h`: the old-style nested
  `namespace DB { namespace ContentAddressed` wrapper around a single
  declaration dissolved; `partFileMustStayBlob` now lives in the file's
  existing `DB::ContentAddressed` block.
- `Backend/CasBackend.h`, `Backend/CasRequestControl.cpp`,
  `Tools/CasFsck.cpp`, `Parts/PartFolderAccess.cpp`: compact
  `namespace DB::ErrorCodes` normalized to the nested form used by the
  other 35 files in this directory (and ~15:1 repo-wide).

No namespace of any symbol changed; validated by compiling `dbms` and all
CAS gtest objects cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`Cas` and `ContentAddressed` were synonyms naming two namespaces of one
subsystem, so the split signaled nothing: the directory is named
`ContentAddressed/` while most of its code was in `Cas`, the adapter
namespace covered a small subset (`Parts/`, the transaction buffers, and
— inconsistently — `Gc/CasGcScheduler`), and no name collisions existed
between the two (verified before merging). The real architectural rule
(part-semantics code may use the pool engine, never the reverse — zero
reverse references) is carried by the directory layering, not by the
namespace.

Everything from `DB::ContentAddressed` now lives in `DB::Cas`. The
facades stay in `DB` as before (`ContentAddressedMetadataStorage`,
`ContentAddressedTransaction`, `IContentAddressedExchange` — they
implement or serve `DB` interfaces and carry self-identifying names).

DB-pollution audit performed after the merge: the one generic name that
sat directly in `DB`, `enum class StagingBackend`, moved to
`DB::Cas::StagingBackend`; nothing else CAS-specific remains in the main
`DB` namespace except the facades and standard `ErrorCodes` externs.

Also merged two anonymous-namespace blocks that were closed and reopened
on the next line (`CasInMemoryBackend.cpp`, `CasPoolMeta.cpp`), and
replaced an agent-memory slug reference in a `CasPartWriteTxn.h` comment
with the invariant it stood for.

Validated: `dbms`, `DataPartsExchange.cpp.o`, and all CAS gtest objects
compile cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`ShardIncarnation` had zero users — not even the tests whose suite name
echoes it (`gtest_cas_gc_shard_incarnation.cpp` tests live discovery and
newborn-precommit behavior and never instantiates the type). It is a
leftover of the retired per-shard incarnation-stamp design; incarnation
ordering is carried by the strictly-increasing `RefTxnId` today.

Also removed a tombstone comment in `gtest_cas_pool.cpp` describing
already-deleted shard-birth-incarnation tests.

Validated: `dbms` and all CAS gtest objects compile cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old layout hid the concepts: `CasBlobHasher.{h,cpp}` contained no
`BlobHasher` class (it mixed the algorithm vocabulary with the streaming
buffers), the digest value family was buried in `CasTypes.h`, and
`CasXXH3.h`'s single class was named `Xxh3Streamer`.

New layout — file = concept:

- `Primitives/CasBlobDigest.{h,cpp}` — blob identity in one place:
  `BlobHashAlgo` (+ name/width/config parsing), `BlobDigest`,
  `DigestCodec`, `BlobRef`, `codecFor`, `blobHexOf`/`blobIdOf` (moved
  from `CasTypes.h` and the old `CasBlobHasher.h`). Dependency-light:
  `CasTypes.h` now includes this instead of pulling `IO/WriteBuffer.h`
  into every CAS translation unit.
- `Primitives/CasBlobHashingWriteBuffer.{h,cpp}` (ex `CasBlobHasher`) —
  only the streaming machinery: `IBlobHashingWriteBuffer` (renamed from
  the confusable `IHashingWriteBuffer`), `makeBlobHashingWriteBuffer`,
  `blobHashHexOneShot`.
- `Primitives/CasXxh3Streamer.h` (ex `CasXXH3.h`) — named after the
  class it defines; still the isolated vendored-xxHash system-header
  wrapper.

Includers re-pointed by what they actually use (six vocabulary-only,
three buffer users); no behavior change anywhere.

Validated: `dbms` and all CAS gtest objects compile cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sumer

The per-round tally of failed per-hash freshness-meta ops was folded into
`RoundReport` but never mapped into `GcRoundLogRecord`, so it reached no
system table and had no reader (tests included). The same failures already
increment `ProfileEvents::CasGcMetaWriteAnomaly` (which reaches the GC log
row via the per-round profile-events snapshot) and are logged via
`tryLogCurrentException`, so no observability is lost.

Removed the report field, the backing `meta_anomaly_count` atomic, its
per-round reset and fold, and the now-unneeded `this` lambda capture. The
operator-signal semantics the field's comment carried (a persistently
non-zero rate means the writer's meta point-read gate is drifting from the
ledger) moved into the `CasGcMetaWriteAnomaly` profile-event description.

Validated: `dbms` and all CAS gtest objects compile cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt hb owners

The steal gate's liveness check compared the observed `gc/hb` sequence
against the previous observation while matching the hb OWNER against the
CURRENT LEASE owner:

    hb_alive = has_observation && hb.owner == lease.owner && hb.hb_seq > last_seen_hb_seq

`hb_seq` values are only comparable under the same hb owner — which is
exactly why `last_seen_hb_owner` was stored — but the stored owner was
never read (the half-finished check this fixes). The hazard is reachable:
a deposed leader's heartbeat thread keeps pulsing (`i_am_leader` resets
only on its next round) with `owner = itself`, and `pulseHeartbeat`'s
losing CAS writes vanish silently, so a zombie old leader can keep
`gc/hb.owner` pointing at itself while the live new leader is pulsing
too. On both of a follower's ticks `hb.owner != lease.owner` then made
`hb_alive` false, and with the new leader's lease tuple frozen mid-round
the follower stole the lease from a live, pulsing leader.

The gate is now observation-symmetric with the frozen-lease check: ANY
movement of the observed `(owner, hb_seq)` pair between the contender's
two ticks is proof of life, and `hb_seq` is compared only under the same
remembered hb owner (an owner change re-arms the window). Liveness is
preserved — only leaders pulse and a deposed leader stops within one round
interval, so a genuinely dead leader still gets stolen from one window
later at most.

Failing-first regression test
`CasGcLease.StaleOwnerHeartbeatDoesNotEnableFalseSteal` reproduces the
zombie-masking interleaving deterministically (steal confirmed on the old
predicate) and pins both the no-false-steal and the still-steals-when-
frozen halves.

Validated: full corrected CAS gtest battery 906/907 — the one failure is
the pre-existing tracked `RefWriterRecoverySeal` F3-1a fragility
(`docs/superpowers/cas/BACKLOG.md`), unrelated to the lease protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@filimonov filimonov changed the title CAS draft (adopting to CI/CD, not for review / merge) [WIP] CAS draft (adopting to CI/CD, not for review / merge) Jul 17, 2026
@filimonov
filimonov marked this pull request as draft July 17, 2026 17:24
@filimonov

Copy link
Copy Markdown
Member Author

#2073

@filimonov filimonov closed this Jul 17, 2026
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.

1 participant