🤖 fix: stabilize adoption reconciliation for shared receipts and downgrades - #4245
🤖 fix: stabilize adoption reconciliation for shared receipts and downgrades#4245ThomasK33 wants to merge 6 commits into
Conversation
…stination generation as transient - LocalMemoryStore.listFiles classifies DT_UNKNOWN dirents by lstat in the bounded walk as well (still capped), not only the strict one: the legacy store fingerprint otherwise dropped such entries, so a downgraded edit under an untyped directory never changed the check key and the memoized pass never picked it up. A bounded walk treats an unclassifiable entry as "other" as before; the strict walk still fails on it. - An identical adoption destination whose generation stamp cannot be read (the lstat fails after the content read) is an unanswered sibling-owner question, not "nobody's": legacyImportTarget and the prior-record reuse branch now throw, and the caller skips the note transiently (no reuse, no record, pass not memoized) instead of settling a reuse record against a file that may be another descendant's copy. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$112.74`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=112.74 -->
…efore the creator reconciles it The previous layers let a descendant's identical note settle on another descendant's adoption-created copy (a created: false record naming that target). Under the one-owner invariant such a record is not settled: the unchanged-record fast path now checks a created: false record's target against the strict sibling scan and, when a sibling's live generation owns it, falls through to placement — the note is copied from its legacy bytes into imported/<child>/ (created: true, fresh stamp), the sibling's file untouched. Removal's forced handover runs the same pass, so it performs the migration too. On the creator's side, the deletion loop keeps a copy that a sibling's settled reuse record still names (transient skip, pass unmemoized) until that sibling has migrated, so a source deletion never pulls a note out from under a sibling. Both checks read sibling manifests strictly; an unanswerable one makes the note wait. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$112.74`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=112.74 -->
…dopting an interrupted in-place edit An in-place replacement interrupted between its pending manifest write and the install, followed by a downgrade, left two visible copies: the prior build reads the pending record as an interrupted first adoption, finds the prior copy holding other bytes, and settles the edited note anew under imported/ — the original stays, named by no record, and this build could not tell it from the owner's own note. The replacement now also writes a superseded-original marker record beside its pending record, keyed under a control-character prefix no legacy note can be listed as, carrying the replaced generation (hash and stamp) in the fields every build preserves. The prior build keeps records it does not list, so once this build runs again it finds the marker, sees the note's record moved elsewhere (or tombstoned), and removes the original while it is still that generation; an owner-edited one is the owner's and only the marker is dropped. A marker whose record still names the same target is a replacement in flight (finished by the pass) or one resolved there (stale, dropped); settling drops it, and the install's record restore drops it too. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$112.74`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=112.74 -->
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff3f534836
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… unanswered generation probes transient Four in-layer fixes from review of the reconciliation stabilization: - The superseded-original cleanup applies the deletion loop's sibling reliance guard (shared siblingReliesOn): while any sibling's settled reuse receipt still names the original's generation, the file and the marker stay (transient skip) until that sibling has migrated. - In the same path, a generation probe that fails (unreadable lstat) is a transient skip that retains the marker, instead of dropping it unconditionally — the cleanup completes on the next pass. - The unchanged-record fast path's shared-receipt check treats an unreadable target generation as an unanswered question (transient skip, no memoization) rather than a settled receipt, mirroring the prior-record branch; an absent target is settled as before. - LocalMemoryStore.listFiles classifies DT_UNKNOWN entries by lstat BEFORE sorting, so an untyped directory sorts with its "name/" key and the bounded fingerprint's capped subset stays a global lexicographic prefix (a sibling file such as a.md is not pushed out by a large untyped a/). --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$127.20`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=127.20 -->
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 32509e4bfa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…crash, bound dirent typing, heal malformed sibling manifests Four follow-ups on the reconciliation stabilization: - A note migrating off a shared copy in a full owner store no longer deadlocks with the copy's creator: when the creator has lost its legacy source (the copy would follow it out, but cannot while this note relies on it) the file is taken over by rename — no slot, same generation as the receipt. While the creator keeps its source the note stays on the shared copy as a transient (unmemoized) skip, so freed space or the creator's deletion is picked up on the next access instead of after a restart. - While a migration's new copy is not yet installed, the receipt naming the old copy is retained as a created: false superseded-marker record, so the creator's reliance check still sees it: a crash in that window can no longer let the creator's source deletion remove the only copy. The marker is dropped when the migration settles or is restored; the marker step skips any marker whose record is pending. - LocalMemoryStore.listFiles types DT_UNKNOWN dirents with at most 16 lstats in flight (classification still precedes sorting). - The sibling scan quarantines a MALFORMED sibling manifest through the same self-healing step the sibling's own pass uses (it then claims nothing), instead of blocking this child's every pass and removal until a sibling that may never run a pass heals it; an UNREADABLE manifest still fails the question. --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$136.97`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=136.97 -->
…d spies Fills the classified-entry array by index instead of preallocating it (consistent-generic-constructors / no-unsafe-assignment), and types the readFile spy targets as strings (no-base-to-string). --- _Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$136.97`_ <!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=136.97 -->
|
@codex review |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b220dd4e69
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| transientSkips++; | ||
| continue; | ||
| } | ||
| target.takeOver = migratingFrom; |
There was a problem hiding this comment.
Preserve every relying sibling before taking over the copy
When two or more descendants have old created: false receipts for the same copy, the store is full, and the creator's legacy source is gone, processing the first descendant reaches this assignment and renames the shared path solely to that descendant's import target. No check accounts for the other reliance records, so those descendants point to an absent target; their fast path then treats that absence as settled and does not re-adopt them. Require sole reliance or migrate every outstanding receipt before renaming the shared copy.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
| if (migratingFrom === null) { | ||
| capacityExhausted = true; | ||
| skipped++; | ||
| continue; |
There was a problem hiding this comment.
Recover a pending takeover from its reliance marker
If the process exits after the pending manifest is written but before the takeover rename in a full owner store, the next pass inspects the primary record's new, absent target, so migratingFrom remains null even though the superseded marker still identifies the live old copy. This branch then records only a permanent capacity skip, memoizing ordinary access and making forced removal abort indefinitely. Fresh evidence is that the added marker is retained but never consulted to reconstruct migratingFrom on retry.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
| adopted.set(`${LEGACY_SUPERSEDED_MARKER_PREFIX}${relPath}`, { | ||
| content: previous.content, | ||
| sidecar: "", | ||
| target: previous.target, | ||
| created: false, |
There was a problem hiding this comment.
Keep reliance markers valid across a downgrade
When a user downgrades to the parent build after this marker is written but before installation, that build's unlisted-record reconciliation treats the control-key marker as a missing legacy source and tombstones it; at full capacity it also cannot finish the primary placement. Returning to this build then excludes the deleted marker from siblingReliesOn, allowing the creator to remove the only shared copy. Fresh evidence against the earlier receipt fix is the parent implementation's unconditional reconciliation of every unlisted manifest key, so the reliance must be encoded in a form that survives that downgrade behavior.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
| const siblingReliesOn = async (targetRelPath: string) => { | ||
| siblingRecords ??= await this.descendantAdoptionRecords(owner, childId); | ||
| return siblingRecords.some( |
There was a problem hiding this comment.
Fail closed when loading descendants for destructive checks
When config.json becomes transiently unreadable or malformed after the acting workspace's owner has already been resolved, descendantAdoptionRecords calls loadConfigOrDefault() and receives an empty project map. This newly added reliance check therefore reports no siblings, allowing the deletion and superseded-copy paths to remove a generation that another descendant's manifest still references. Use a strict config read or a previously verified topology snapshot so an unanswerable descendant scan causes a transient retry instead of a destructive decision.
Useful? React with 👍 / 👎.
| if (target.takeOver !== undefined) { | ||
| stagingPath = store.physicalPath(target.takeOver.target); | ||
| stagedStamp = target.takeOver.stamp; |
There was a problem hiding this comment.
Remove source metadata when taking over a shared copy
During a full-store takeover, this makes the existing owner file the rename source, but the install path only merges the migrating descendant's metadata into the new target and never removes the owner metadata keyed by the old target. The creator's later reconciliation sees that target as already absent and tombstones its record without calling removeKeys, leaving stale pins and usage indefinitely; recreating a note at the old path then inherits those values. Clean the source logical key as part of the takeover transaction.
Useful? React with 👍 / 👎.
Summary
Stabilization of the adoption reconciliation introduced in #4224. Four defects found in that layer's second review round are closed here with red-checked tests: settled shared receipts from the pre-simplification format migrate to the descendant's own copy before the creator can reconcile the shared target; untyped (
DT_UNKNOWN) dirents are classified in the bounded legacy fingerprint; an in-place edit interrupted before its rename stays safe when the app is downgraded to the previous reader and re-upgraded; and an unreadable live generation on an identical candidate is a transient skip instead of evidence that no sibling owns it.Stack: layer 5 of the split of #4139 (base:
memory-sharing-l3a2, #4224). With this layer every deferred thread on #4220 and #4224 that named a later adoption layer is closed, and the shippable bundle is#4210 + #4217 + #4220 + #4224 + this PR.Implementation
created: falserecord against the strict sibling scan; if a sibling's live generation owns the target, the record is not settled and falls through to placement underimported/<child>/(copied from the legacy bytes,created: true, fresh stamp). The creator's deletion loop keeps a copy that a sibling'screated: falserecord still names (transient skip) until that sibling has migrated. Removal's forced handover runs the same code.listFilesclassifies untyped dirents vialstatin both modes; the bounded walk stays capped and treats anlstatfailure asother, the strict walk still throws.imported/and the app is re-upgraded, the pass removes the original only if it still matches the marker's hash and stamp; an owner-edited original just drops the marker. The marker is dropped on completion or recovery.adoptionTargetStampreturningnullfor an identical occupied candidate throws to the caller's transient-skip path;siblingOwnsrequires a non-null stamp.Item 3 is the one shape not present in the reference implementation (#4139 had no handling for a downgrade during a pending replacement); it is one record shape inside the existing manifest, no new file.
Known limitations (later layers)
Validation
Five new tests, each red on the previous code: shared receipt migrated before the creator's deletion (and A's later strict removal completes), removal handover migrates the receipt too, nested downgrade edit under an untyped dirent re-runs the pass, original left behind by the previous build after an interrupted edit is removed leaving one live copy, unreadable candidate generation waits and settles on the next access. Sibling suites: 1808 pass.
Risks
Low to medium, confined to the adoption pass and manifest. Every destructive step still requires the provenance stamp to prove adoption created the file.
Generated with
xum• Model:anthropic:claude-fable-5-1• Thinking:high• Cost:$1188.63