Thanks to visit codestin.com
Credit goes to github.com

Skip to content

🤖 fix: stabilize adoption reconciliation for shared receipts and downgrades - #4245

Open
ThomasK33 wants to merge 6 commits into
memory-sharing-l3a2from
memory-sharing-l3a3
Open

🤖 fix: stabilize adoption reconciliation for shared receipts and downgrades#4245
ThomasK33 wants to merge 6 commits into
memory-sharing-l3a2from
memory-sharing-l3a3

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 13, 2026

Copy link
Copy Markdown
Member

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

  1. Shared receipts: the unchanged-record fast path checks a created: false record against the strict sibling scan; if a sibling's live generation owns the target, the record is not settled and falls through to placement under imported/<child>/ (copied from the legacy bytes, created: true, fresh stamp). The creator's deletion loop keeps a copy that a sibling's created: false record still names (transient skip) until that sibling has migrated. Removal's forced handover runs the same code.
  2. Fingerprint: listFiles classifies untyped dirents via lstat in both modes; the bounded walk stays capped and treats an lstat failure as other, the strict walk still throws.
  3. Interrupted replacement: the replacement writes a superseded-original marker record beside its pending record (a key the memory path grammar can never list, so the previous build preserves it). After the previous build has re-placed the edit under 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.
  4. Unreadable generation: adoptionTargetStamp returning null for an identical occupied candidate throws to the caller's transient-skip path; siblingOwns requires 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)

  1. Refinement-row migration and rollback path remapping for adopted files (L3b); rollback of a child's own rows still fails closed.
  2. Throttled full-store fingerprint for over-cap legacy stores, cross-process sidecar locking, second-backend removal races (L4).
  3. A creator's strict (non-forced) removal waits while a sibling's shared receipt still names its copy; it clears on that sibling's next pass, which the owner's access triggers.

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

…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 -->
@ThomasK33
ThomasK33 added this pull request to stack #4218 September 13, 2026 19:46
@ThomasK33 ThomasK33 changed the title memory sharing l3a3 🤖 fix: stabilize adoption reconciliation for shared receipts and downgrades Sep 13, 2026
@ThomasK33
ThomasK33 marked this pull request as ready for review September 13, 2026 19:47
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T21:12:17.255618Z b220dd4 Manual request
🔒 Security Review Completed 2026-09-13T21:07:06.230164Z b220dd4 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: ff3f534836

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/memoryService.ts Outdated
Comment thread src/node/services/memoryService.ts Outdated
… 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 -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 32509e4bfa

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/memoryService.ts Outdated
Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/memoryService.ts
…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 -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: b220dd4e69

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +2094 to +2097
if (migratingFrom === null) {
capacityExhausted = true;
skipped++;
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +2205 to +2209
adopted.set(`${LEGACY_SUPERSEDED_MARKER_PREFIX}${relPath}`, {
content: previous.content,
sidecar: "",
target: previous.target,
created: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +1568 to 1570
const siblingReliesOn = async (targetRelPath: string) => {
siblingRecords ??= await this.descendantAdoptionRecords(owner, childId);
return siblingRecords.some(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +2150 to +2152
if (target.takeOver !== undefined) {
stagingPath = store.physicalPath(target.takeOver.target);
stagedStamp = target.takeOver.stamp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

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