Skip to content

fix(graph): verify sealed retirement and preserve retry state - #4819

Open
ScriptedAlchemy wants to merge 90 commits into
masterfrom
fix/4616-verified-sealed-retirement-clean-history
Open

ScriptedAlchemy wants to merge 90 commits into
masterfrom
fix/4616-verified-sealed-retirement-clean-history

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

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:

  • Keeps a registered reader until both physical close and exact artifact removal succeed, including uncertain close and missing-directory cases.
  • Publishes complete row-spill ownership receipts before exposing directories. Recovery distinguishes producer incarnations and preserves live or unproven staging.
  • Bounds receipt and marker reads and filesystem census resource use; inaccessible owned generations return errors instead of an apparently complete census.
  • Carries namespace, projection, and generation identity through Doctor liveness and dependency accounting, so equal generation names in different projections are classified independently.

Native macOS Bazel verification:

  • At final head 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.
  • Strict Clippy and formatting passed for both affected test targets; commitlint and diff checks passed.
  • At preceding head 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.


Devin Review

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.
@changeset-bot

changeset-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7e05ab8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

Refs #4616

Reconcile all eleven formatting deltas from the exact-head Bazel
repository-gates failure without modifying retirement behavior or tests.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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.
devin-ai-integration[bot]

This comment was marked as resolved.

@ScriptedAlchemy ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

devin-ai-integration[bot]

This comment was marked as resolved.

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 ScriptedAlchemy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
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