Skip to content

🤖 fix: migrate off shared adoption copies by installing first, drop take-over renames and marker-key reliance - #4254

Closed
ThomasK33 wants to merge 2 commits into
mainfrom
memory-sharing-l3a4
Closed

🤖 fix: migrate off shared adoption copies by installing first, drop take-over renames and marker-key reliance#4254
ThomasK33 wants to merge 2 commits into
mainfrom
memory-sharing-l3a4

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

Replaces the two mechanisms that #4245's second review round introduced for migrating a descendant off a shared adoption copy — take-over-by-rename at the file cap, and a reliance record under the superseded-marker control key — with a simpler install-before-flip migration. The five findings from #4245's third review round (multi-sibling take-over, crash before the take-over rename, marker tombstoned on downgrade, tolerant config read in a destructive check, stale owner metadata after take-over) disappear by construction; memoryService.ts shrinks by 51 lines.

Stack: layer 6 of the split of #4139. Re-opened copy of #4248 (identical diff, rebased onto main after the merge queue landed #4210#4245 and GitHub auto-closed #4248 when its base branch was deleted). Closes the review threads that were open on #4245; the remaining bundle is this PR + #4250.

Implementation

  1. Install before flip. A note migrating off a sibling-owned copy stages and installs its own copy at imported/<child>/<rel> while the manifest still holds the plain created: false receipt naming the shared copy — the shape every build reads and exactly what siblingReliesOn checks. Only after the install does the record flip to created: true for the new target, in the pass's single manifest write. No pending record precedes the install, so no control key is ever written for a migration.
  2. Idempotent retry. Crash before install: nothing changed, the retry redoes the migration. Crash after install, before flip: the retry finds an identical file at this descendant's own import slot with no record and reuses it non-destructively (a plain created: false receipt naming it) — it is never claimed as adoption-created, because without a receipt recorded before the install it cannot be told from an owner-authored note. Destructive provenance comes only from a targetStamp recorded before an install.
  3. Downgrade. During the window an older build sees the receipt unchanged (its reliance/skip logic is unaffected) plus, at worst, one redundant identical file under imported/<child>/ that this build reuses (never deletes) on return; older builds do not reconcile unlisted files under imported/.
  4. Capacity. The migration install is exempt from the capacity check (at most one extra file per outstanding shared receipt, bounded by prior state; remainingCapacity is still decremented). The creator's file is never renamed, so its normal deletion loop (removeKeys + remove) returns the slot once no receipt relies on it. Migration skips are transient only.
  5. Strict sibling scan. descendantAdoptionRecords reads the config with throwOnError, so an unreadable or malformed config.json throws (callers already treat that as a transient skip) instead of yielding an empty project map that reports "no siblings" to the deletion paths.

Removed: siblingOwner/siblingSourceGone/migratingFrom/takeOver, the take-over staging/installable/restore branches, the DescendantAdoptionRecord scan shape, and the reliance-marker write. Kept from #4245: the deletion-loop reliance guard and superseded-copy cleanup, marker preserved when its generation is unreadable, bounded DT_UNKNOWN classification, malformed sibling manifest → quarantine, transient null stamps.

Validation

Four new tests, each red on memory-sharing-l3a3: three descendants at cap all migrate off one shared copy and the creator's deletion then clears the copy and its pin; a crash between stage and install leaves the manifest byte-identical (no pending record, no control keys) and the creator's deletion keeps the copy until the retry finishes; a copy installed before the flip is reused without being claimed, and an owner-authored identical file at the import slot survives the migration and the later source deletion; an unreadable config leaves everything in place and the pass unmemoized. The two #4245 tests that encoded take-over and marker reliance are replaced by these scenarios.

Risks

Low to medium, confined to the adoption pass and manifest. The one residual outcome is a single redundant identical file under a descendant's own import slot after a crash between install and flip; adoption reuses it as a non-owned receipt and never deletes it.


Generated with xum • Model: anthropic:claude-fable-5-1 • Thinking: high • Cost: $1197.33

… plain receipt, drop take-over renames and marker-key reliance

Review of the previous layer found the two mechanisms it added for
shared-receipt migration to be the problem: taking the creator's copy
over by rename left every other descendant's receipt pointing at
nothing (their fast path read that as settled), left the owner metadata
keyed by the old path, and — after a crash between the pending manifest
write and the rename in a full store — a permanent capacity skip that
also blocked forced removal; and retaining the reliance receipt under
the superseded-marker control key was tombstoned by the previous build's
unlisted-key reconciliation on downgrade, after which the creator could
delete the only shared copy.

Both are removed. A migration now installs the descendant's own copy
FIRST while the manifest still holds the plain created: false receipt
naming the shared copy — the form every build reads, and the sibling's
reason to keep that copy (siblingReliesOn) — and only then flips the
record to created: true for the new target, in the pass's single
manifest write. No pending record precedes the install (it would replace
the receipt), so a retry recognizes a copy installed before the flip by
its identical bytes at this descendant's own import slot and claims it
instead of writing a second one; a crash before the install leaves
nothing to recover. A downgrade in the window sees the receipt exactly
as before plus, at worst, one redundant file this build claims on
return. The install is exempt from the capacity check — at most one
extra file per outstanding shared receipt, bounded by prior state — and
the creator's file is never renamed, so its normal deletion (removeKeys
included) returns the slot once nobody relies on it.

The sibling scan reads the config strictly: an unreadable or malformed
config.json no longer answers "no other descendants" (which let the
deletion and superseded-copy paths remove a generation a sibling still
names); callers treat the throw as transient.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$153.06`_

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=153.06 -->
…any identical occupied candidate

A migration retry that found an identical file at the descendant's own
import slot marked it adoption-created. No receipt was recorded before
that install, so the file was indistinguishable from one the owner wrote
there — the slot is inside the writable notebook — and after the legacy
source was deleted, reconciliation trusted that provenance and removed
the owner's file.

The claim is dropped. The retry now follows the same rule as first-time
placement for a byte-identical occupied candidate: reused
non-destructively with a plain created: false receipt naming it, never
claimed. Destructive provenance is only ever taken from a receipt
recorded before an install (targetStamp), never from a byte match. The
scenario still converges in one pass with no second copy; the bounded
outcome of a crash between install and flip is one redundant copy that
adoption never deletes.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking: `high` • Cost: `$171.20`_

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=171.20 -->
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 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-14T12:25:19.222871Z 64bb582 Manual request
🔒 Security Review Completed 2026-09-14T12:22:20.895220Z 64bb582 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.

@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: 64bb582c9c

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: 64bb582c9c

ℹ️ 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
@ThomasK33
ThomasK33 removed this pull request from stack #4255 September 14, 2026 13:49
@ThomasK33

Copy link
Copy Markdown
Member Author

Superseded: after the merge queue landed #4210#4245, the remaining two layers were collapsed into a single PR against main, #4250, which contains this diff plus the fixes for the two review threads here. Closing.

@ThomasK33 ThomasK33 closed this Sep 14, 2026
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Sep 14, 2026
…rden guarded reads, removal pinning and legacy adoption (coder#4250)

## Summary

Final layer of the split of coder#4139. coder#4210coder#4245 landed on `main` through
the merge queue; this PR carries the two remaining layers as one diff
(the queue's head-branch auto-delete closed coder#4248 and dropped this PR
mid-stack, so they were rebased onto `main` and collapsed here): the
install-before-flip migration that replaced take-over-by-rename (former
coder#4248 / coder#4254), the post-rebase hardening of guarded reads, removal
pinning and legacy adoption, and the recovery round from this PR's own
review against `main`.

## Implementation

**Migration off a shared adoption copy** (former coder#4248/coder#4254)

1. *Install before flip.* A note migrating off a sibling-owned copy
stages and installs its own copy at `imported/<child>/<rel>` while the
manifest still holds the plain `created: false` receipt naming the
shared copy — the shape every build reads and what `siblingReliesOn`
checks. Only after the install does the record flip, in the pass's
single manifest write. No control key is ever written for a migration,
so a downgrade in the window sees exactly the receipt it already
understands.
2. *Retry.* Crash before install: nothing changed. Crash after install,
before flip: an identical file at the child's own import slot is reused
non-destructively (plain `created: false` receipt) — never claimed as
adoption-created, because without a receipt recorded before the install
it cannot be told from an owner-authored note. If a downgraded build
then edits the legacy note in that window, the pass fails closed
(documented limitation at `legacyImportTarget`): the edit stays in the
legacy session store, the receipt keeps protecting the shared copy, and
the non-forced handover rejects until the redundant slot is cleared by
hand.
3. *Capacity.* A migration at the per-scope cap is a transient,
unmemoized skip: the note keeps standing on the shared copy, the
creator's deletion waits, and the migration completes once a slot frees.
The store never exceeds the cap; take-over by rename and the earlier cap
exemption are both gone, so no receipt is ever left naming an absent
target and no owner metadata is stranded.
4. *Strict sibling scan.* `descendantAdoptionRecords` reads the config
with `throwOnError`; an unreadable or malformed `config.json` is a
transient skip, never "no siblings".

**Hardening** (former layer 7 + this PR's review rounds)

5. Guarded reads: the acting/`guardedWorkspaceId` tombstone check runs
before and after reads in every scope, with a final all-scope gate over
the assembled hot set, index and root listing; a late tombstone first
observed in the second gate also clears global/project items.
6. Descendant pinning during removal is a config-only edit that runs
before the metadata lookup (phantom-cleanup path included); a non-forced
removal aborts when the pin cannot be verified.
7. Legacy adoption: names the parser only admits after normalization are
a reported permanent skip; a non-regular entry at the old source counts
as deletion; manifest keys and receipt targets are validated for
containment before any probe; excluded dot entries skip `DT_UNKNOWN`
classification.
8. Sibling checks use file identity (`ino:size:mtimeNs`) rather than
spelling, with the receipt identities probed once per pass (cache
evicted for paths the pass mutates). A same-inode match counts as
relying when it is a single directory entry (`nlink 1`) or the spellings
fold to the same name; an independent hard link does not block the
creator's deletion.
9. Read-side re-ranking emits the memory change event with `reason:
"access"` (optional field on the in-memory/IPC event) so `MemoryBrowser`
no longer labels unchanged notes as agent-edited.

No new persisted fields, files or modules.

## Validation

Every fix has a test that was red on the code it replaced, including:
three descendants at cap migrate one by one as slots free and the store
never exceeds the cap; a crash between stage and install leaves the
manifest byte-identical; a copy installed before the flip is reused
without being claimed and an owner-authored identical file survives the
later source deletion; an edit made after a crash between install and
flip is left unfolded with nothing overwritten; hot set, index and root
listing all withhold already-accumulated global items on a late
tombstone; receipt-identity probes are bounded by the receipt count;
hard link vs case alias.

## Known limitations (later work)

- Crash between a migration's install and its receipt flip followed by a
downgraded edit of the same note: fail-closed, manual slot clean-up (see
`legacyImportTarget`).
- Cross-process ownership revalidation of an `AgentSession`'s cached
memory context after another backend removes the owner (multi-backend
layer).
- Rollback owner-root resolution for adopted rows; throttled full-store
fingerprint; sidecar locking; concurrent removal across backends (as
replied on the earlier layers' threads).

## Risks

Low to medium, confined to the adoption pass, manifest, and read gating
(fail-closed in both directions). No destructive decision is ever taken
from byte identity alone; every destructive step requires a receipt
recorded before the install.

---

_Generated with `xum` • Model: `anthropic:claude-fable-5-1` • Thinking:
`high` • Cost: `$1260.46`_

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high
costs=1260.46 -->
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