Repository navigation
fix(graph): verify sealed retirement and preserve retry state - #4819
ScriptedAlchemy wants to merge 90 commits into
Conversation
Refs #4616 Combine the proven bounded receipt/ownership cleanup fixes with post-unlink collection-state ordering and compatible filesystem regressions. Replace the old separately reviewed PR history without rewriting either branch, and preserve all exclusion and fail-closed checks.
|
Refs #4616 Reconcile all eleven formatting deltas from the exact-head Bazel repository-gates failure without modifying retirement behavior or tests.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Reviewed commit 65bd98f against its parent and the real sealed-build call path. The pre-change missing/malformed receipt branch could delete foreign real-directory contents; the revised path refuses those entries and tests negative preservation plus subsequent legitimate recovery. The pre-existing sealed-root symlink test similarly proves no foreign staged writes and positive recovery. Scope is one graph-db source file, no CI weakening. This is partial #4616 safety work only: parent identity and post-check pathname races remain unresolved; Bazel/native verification is pending, so this is not an approval or merge recommendation.
Repair three hosted automation_runner_test failures by supplying the configured executable identity required by the production prefilter. Use the same identity for the prefilter and retained runner, preserving manual-schedule, exact-ledger, live-lock and lock-race assertions. Add a production-prefilter regression for unconfigured refusal, deduplication and same-occurrence recovery after binding, with zero backend calls and no effectful terminal. Do not relax backend admission, change timeouts, or alter the graph publication changes. Refs #4532
Refs #4616 Use the strict generation/digest-bound retirement authority on failed recovery adoption and post-install reopen failures. Preserve unverified artifacts instead of path-deleting them. Cover missing, corrupt, wrong- digest and owned receipts plus unrelated sibling preservation.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Reviewed the exact 882d642 diff against its predecessor and the production sealed build/install path. Previously, std::fs::rename(staging, generation) could silently replace an empty existing real directory on Linux/macOS. The new production call uses the existing capability-relative rename_noreplace authority via a no-follow sealed-root/staging helper. The native regression constructs exactly that empty occupied destination, proves refusal and staging preservation, then proves successful installation after the destination is removed. A separate filesystem regression proves that a symlinked sealed root or staging entry cannot redirect publication into a foreign tree. Two source files changed; no test or CI gate was weakened.
Remaining safety boundary: ambient store ancestors are not yet pinned to the original store-owner identity, and staging writes remain pathname-based. A real directory substituted between staging creation and subsequent writes or between the staging handle check and rename requires a separate owner-bound I/O fix. This is a partial Refs #4616 fix only, not an approval to merge/close. Hosted exact-head CI is pending; no Bazel/native macOS pass claimed.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — reviewed exact commit e2f3371c against its parent and the real recovery path (open_sealed_generation_store_if_present -> discard_failed_sealed_adoption -> ownership-checked retirement). The previous restart_recovery_adopts_or_discards_the_on_disk_artifact assertion demanded removal of a tampered/unverified receipt, contradicting the new fail-closed ownership contract and causing a native regression failure once this PR's safety code executes. The repaired test checks all three outcomes of the real recovery journey: no sealed adoption, byte-for-byte preservation of the unverified on-disk receipt/directory, and successful reads from staging rows. It still proves positive sealed adoption before tampering. One test file, no behavior or CI weakening; exact-head nonforce FF verified. This is partial, not acceptance of all #4616 or approval to merge. Bazel/native macOS results remain outstanding; CI run 42540628881 is queued.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — reviewed exact commit b568770d against its parent and build_or_open_sealed_store's production stage/create/install path. The previous unconditional remove_child_directory_nofollow was capability-relative but authenticated no ownership: a pre-existing real .staging-<generation> directory was recursively erased before its contents were examined. The new exclusive create_dir refuses occupied staging (including symlinks), with a native regression that checks byte-exact foreign-sentinel preservation, no artifact installation, and successful verified sealing after fixture cleanup. Only sealed_store.rs changed; no gate, timeout, release, or other PR was modified.
This is a narrowly reviewed safety repair, not approval for full #4616 acceptance. Outstanding separately: the store-open abandoned-staging sweeper still deletes by filename pattern without an ownership receipt, and store-ancestor/staging-path substitution remains unsafe without a stable owner capability. This head has no Bazel/native macOS pass. Confirm exact-head CI and nonvacuous Bazel graph-db tests before any merge.
Refs #4616 Do not treat a staging-shaped name as deletion authority. Retire only staged children with bounded matching sealed-generation receipts through held no-follow capabilities, preserving unknown or substituted contents. Exercise real store reopen for owned cleanup and unverified preservation.
Refs #4616 A copied receipt with rewritten physical namespace must not authorize recursive cleanup of an unrelated staging directory. Exercise the forgery and malformed-receipt refusals through real store reopen.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — independently reviewed the exact two-commit staging-sweep repair 5de9d92c..0bf8a868 against the prior b568770d and the actual store-open/build/receipt paths.
Previously sweep_abandoned_sealed_staging handed every filename-shaped .staging-<hex> child to a no-follow receipt-free recursive remover. A foreign real directory with an arbitrary payload was therefore deleted on store open. The change reuses the existing held-directory, no-follow, bounded-receipt collector; it rejects unreceipted/malformed/oversized/linked entries and binds the receipt's logical namespace + projection + generation to its canonical physical namespace before authorizing deletion. An interrupted seal with a matching real receipt still reclaims. No changes to store admission, CI, model work or other branches.
The real graph-store reopen regression exercises positive reclamation plus preservation of exact foreign payloads, missing receipt, wrong physical name, forged/copied receipt, malformed/oversized receipt, symlinked directory and installed serving continuity. This is behavioral evidence in the source tree, not an executed test result. CI is queued at https://github.com/ScriptedAlchemy/tracedecay/actions/runs/42552653839; Bazel/native results remain unproven. Remaining #4616 root risks include trusted ancestor identity before capability acquisition, path substitution during staging writes, unrecoverable pre-receipt interrupted staging, N>3 end-to-end retirement and master acceptance. This review is a scope/safety review, not approval to merge.
Keep accepted master tests and isolate sealed-generation retirement changes. Refs #4616
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — independently reviewed the published bd4f39eb regression correction and the accepted master synchronization 95e8a8b6.
Root cause: the previous direct_build_recovers_from_interruption_and_matches_the_staged_build test created an unreceipted occupied .staging-<generation> containing a torn container and required the builder to erase it and rebuild. The production builder now deliberately refuses that name collision (ownership cannot be established), so that pre-existing test was both failing and demanding unsafe behavior.
The revised native test asserts the precise GraphDbError::Unavailable refusal, byte-exact preservation of the torn file, absence of an installed generation, then removes the test fixture and proves direct/staged/parallel payload equivalence and successful install. No production guard is relaxed.
The merge commit has two exact parents: existing PR head bd4f39eb and accepted master 83694e00. The resulting tree is byte-identical to its PR parent; master comparison now contains only six graph-db/private-fs files, with the previously overlapping automation-runner test file absent. Both updates were nonforce expected-old-head fast-forwards. This review is source-level evidence, not a Bazel/native-test pass. Exact-head CI run 42554042152 is queued; do not merge until CI and independent acceptance are proven.
Refs #4616 Preserve foreign row-spill directories on store-open sweeping and drop. Persist bounded owner receipts before writing rows; reclaim only from receipt-verified held directories, with native crash/replacement tests.
Refs #4616 Keep deterministic concurrent hard-link publication coverage in the filesystem census's private unit tests instead of exporting a feature-gated production API. Retain real layered-generation coverage through the public census, and move Unix-only metadata imports to module scope.
Refs #4616 Keep the private concurrent-publication regression compilable by placing the filesystem metadata trait in the module that owns that test, rather than the unrelated marker-replacement tests.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — independent source/diff review of nonforce expected-head commits cc772b3 and 80977b3 on PR #4819 (exact head 80977b3).
Confirmed repair: The former capability_dir::test_support::census_in_entry_order was a feature-gated public library API with no production caller, contrary to the repository rule against test-only production ports. It is now deleted. The scheduled hard-link publication regression executes the actual private census_receipted_children algorithm in its owning #[cfg(test)] module, never through a public test API. It constructs a real on-disk inode, visits one or both original hard links, publishes a new hard link while the census iterator advances, and requires single physical charging transferred to the later live head (8 live bytes; 3 or 6 bytes of old receipts). Both new link-count variants are asserted; the earlier nlink-based eviction implementation double-counted them.
Preserved production integration: Graph-db still builds authentic compact and layered sealed stores, publishes one or two layers, and compares census_sealed_store head/retired bytes against a filesystem inode inventory. Unix MetadataExt imports moved to module scope; follow-up 80977b3 places the trait in the private-fs tests module rather than the separate replacement-test module.
Scope and limitations: Exactly two issue-owned Rust files; no production census behavior, CI, dependencies, protected drafts, other owner files, or release changes. GitHub compare: 2 commits ahead / 0 behind. Static source review only: repository Bazel/native macOS tests unavailable locally; the hosted exact-head run is pending. This is not an approval to merge or close #4616. Scheduled N>3 automatic retirement under sustained writers, stable ancestor ownership, Doctor on master, and incident-scale OOM-safe reporting remain unproven.
Refs #4616 The real layered-generation regression must require a single live head, all retired siblings and no unrecognized artifacts, not only byte totals.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — independent exact-commit review of 6ac1861 against 80977b3. The three added assertions restore the layered regression's generation-count contract: exactly one verified head, exactly 1 + initial_layers superseded generations and no unrecognized entries, while preserving the independently computed inode-byte comparisons for both classes. This directly addresses the new review finding without reinstating the removed test-only public API. One Rust test file, +3 lines, no CI or production logic change. Hosted exact-head CI #42777493212 is queued; no native/Bazel pass claimed, and this is not a merge or issue-closure approval.
Refs #4616 Apply the exact hosted rustfmt 2024 hunk for the head/retired inode census regression, preserving behavior and assertions.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — exact-head source/CI review of 9e581d74 against 6ac18619.
The Bazel repository-gates log for run #42777493212 identifies precisely one rustfmt_check_2024 difference at capability_dir.rs:1228: the nested Some(&(if ... , if ...)) tuple must be multiline. The published commit applies that exact formatting hunk to the existing native late-hardlink census regression. Verified GitHub comparison: one commit ahead, zero behind; one owned Rust test source file, +4/-1. The assertions, production algorithm and CI policy remain unchanged. This resolves the observed source-formatting failure only, not an independently executed Bazel pass.
The same prior Linux fleet run also recorded //crates/tracedecay-global-db:unit_test TIMEOUT (300 s) and a tracedecay-sessions clock-sensitive test failure (unsettled_bucket_native_change_invalidates_even_with_same_entry_names, failed the unsettled-clock precondition); neither is modified by this commit. New exact-head CI run #42907590052 was queued at verification. No local checkout/Bazel, no Cargo. This is a focused CI repair, not approval to merge or close #4616; headless retired-tombstone reconciliation, sustained-writer scheduled retirement, master Doctor reclaim and incident-scale report acceptance remain open.
Refs #4616 An inaccessible real generation must not be silently reclassified as an unknown zero-byte directory during Doctor's sealed-store census. Propagate the held-child open refusal and exercise restoration plus complete recovery against the same durable payload.
Refs #4616 Only absent, malformed and nonregular receipts become unrecognized entries. A permission or resource failure reading a real generation's receipt instead refuses complete coverage. Extend the native regression to restore permissions and prove the original bytes remain measurable.
Refs #4616 Keep the native permission-refusal assertion and fixture behavior unchanged while matching the repository's short-call layout.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — source-level review of three exact-old-head, nonforce commits 527b1df → 942a95b → eea5e3f on the existing issue-specific PR.
Confirmed the defect and narrow scope: the sealed-directory census previously used root.open_dir_nofollow(&name).ok() and treated a real but inaccessible generation as an unrecognized zero-byte entry; it separately swallowed all receipt I/O errors with .ok(). This can make a bounded Doctor/storage census appear complete despite unreadable owned bytes. The updated path propagates held child-open errors, classifies only absent/malformed/nonregular receipts as unknown, and propagates denied/resource/other unexpected receipt-read failures. No-follow ownership, unknown-receipt preservation, and existing inode-accounting policy remain intact.
The native Unix regression changes actual filesystem permissions on a real generation and its receipt, requires typed PermissionDenied for both, restores permissions, and demands exact recovery of the same 12-byte receipt+payload with unchanged durable payload. Independent POSIX mechanism probes observed EACCES (errno 13) for both modes; those probes are NOT a Rust/Bazel regression pass.
Exact-head hosted CI run: https://github.com/ScriptedAlchemy/tracedecay/actions/runs/42917282249 (queued at this review). No local Bazel or native macOS runner; do not merge from this review. Still unresolved: the N>3 scheduled sealed retirement under live writers, stable root/ancestor ownership, prior headless-projection tombstones owned by #4800, Doctor reclaim missing from master, and combined large-store acceptance. #4616 stays OPEN; protected #4776/model/release PRs untouched.
Apply the pinned formatter's wrapping to the retained payload check in the receipt census regression.
Refs #4616 Preserve typed I/O refusals for regular receipts, but classify a Unix socket's ENXIO as nonregular only after a held-directory no-follow stat. Exercise a real socket beside an intact sealed head without hiding its bytes or deleting foreign data.
Refs #4616 Keep native regression imports at module scope per repository conventions.
ScriptedAlchemy
left a comment
There was a problem hiding this comment.
Refs #4616 — new cross-projection sealed-census identity blocker (source review, not a code fix).
On current master b9d9df58, doctor_kernel::live_sealed_generations returns only replay.generation strings (SQL UNION), discarding shard_id/namespace/projection. This PR's census_sealed_store then classifies a receipt as Head whenever head_generations.contains(&receipt.generation), even though the physical sealed directory derives from (namespace, projection, generation) through GenerationLocator::physical_namespace.
I reproduced the exact SQL in SQLite using two projections: the active head of projection A is generation same_generation; projection B's superseded replay is also same_generation while B's active head is new_generation. The SQL returns {same_generation,new_generation}; the current production classification counts B's old sealed artifact as Head, not Superseded. This is a false-negative for Doctor's dead sealed-generation reporting; it does not prove deletion of live bytes.
Required fix: retain the canonical projection-scoped generation identity throughout the Doctor query, dependency protection and census API, and compare each receipt's complete bound identity (not just the generation field). Add a real two-projection regression requiring one head per projection and the shared-name old generation to be counted superseded, with unchanged filesystem bytes. The related Doctor caller is concurrently edited in #4794; coordinate with that owner, do not overwrite its branch or bypass this using held #4776. No merge clearance or #4616 closure on this finding; exact-head Rust/Bazel and master acceptance remain outstanding.
Use the existing stable ELOOP-to-InvalidInput normalization for held-directory receipt reads instead of naming the unstable FilesystemLoop error kind. Keep permission and resource errors visible as incomplete census coverage. Add a real symlink receipt regression proving a link to otherwise valid owner bytes stays unrecognized while the verified head and all stored bytes remain. Refs #4616.
Track sealed generations by namespace, projection, and generation within the registered shard. Keep native retirement dependencies tied to their exact replay sequence. Add real journal and sealed-container regressions covering colliding generation names, namespace and shard isolation, retained dependencies, and unchanged bytes.
Reuse the schema fixture runtime for schema-shape assertions and drop the redundant pre-cancellation reopen. Preserve the cancellation recovery and actual reopen checks.
Integrate master while keeping receipt-authorized cleanup and no-replace directory installation. Report a new durable segment only when this writer installed it, and retain the concurrent-writer regression with a real producer receipt.
The test module retained build_sealed_container after its callers moved to build_sealed_container_with_work. Both Clippy and the Linux test prebuild failed to resolve the old name. Remove that unused import from the existing test module.
A staging directory replaced by a symlink after the owner marker could redirect the live producer's container, receipt, and auxiliary writes into a foreign tree. The builder now creates the staging entry exclusively through a held directory capability, writes the marker, receipt, spool, and base links relative to that handle, and resolves the one path-based container writer from the handle rather than the substitutable name. Installation compares the held directory identity before and after the no-replace rename, so a substituted entry is refused and its bytes are preserved.
Exercise exclusive staging creation, anonymous spool cleanup, relative hard links, pathname substitution, and identity-checked installation through the held directory capability.
Retiring a graph generation could mark it collected before its sealed artifact was removed, and cleanup could follow a reused pathname into unrelated data. This change verifies bounded generation receipts through held directory capabilities, preserves unknown artifacts, and retains failed retirement state for retry.
The final implementation also:
Native macOS Bazel verification:
40ab61b083, all 27 capability-directory tests and the isolated Doctor projection/shard-identity regression passed. Both previously failed: socket receipts returned macOS EOPNOTSUPP without classification, and the Doctor fixture depended on another test registering its schema installer. The repairs retain the existing assertions and regular-file I/O refusal behavior.4108e7d75b, all four sealed-census and 15 retirement regressions passed, including shared generation names, shared-inode accounting, uncertain close, relocated artifacts, and retry ownership.Hosted CI for the final head remains pending. Earlier verification is retained in the review history.
Refs #4616. This PR delivers the ownership, retirement, and census repairs above. Sustained-writer retention, representative large-store measurements, and broader platform qualification remain tracked in #4616.