Skip to content

🤖 feat: reconcile downgrade-time edits of adopted sub-agent notebooks - #4224

Open
ThomasK33 wants to merge 7 commits into
memory-sharing-l3afrom
memory-sharing-l3a2
Open

🤖 feat: reconcile downgrade-time edits of adopted sub-agent notebooks#4224
ThomasK33 wants to merge 7 commits into
memory-sharing-l3afrom
memory-sharing-l3a2

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 11, 2026

Copy link
Copy Markdown
Member

Summary

Reconciliation for adopted legacy notebooks (#4220). When an older build edits, deletes, or renames a sub-agent's pre-sharing note after it was adopted into the shared store, the adoption pass now updates the adoption-created copy in place (never an owner-authored file), removes or moves adoption-created copies whose source is gone, recovers idempotently from an interrupted replacement or deletion, and gives every descendant its own adoption-created copy (a note identical to another descendant's copy lands under imported/<child>/, so replacement and deletion never touch a sibling's copy). It also closes the three hardening items carried from #4220's last review round: BOM-preserving byte-exact compare and copy, lstat classification of unknown dirent types in strict walks, and escaping of child IDs in imported/<child>/ paths.

Stack: layer 4 of the split of #4139 (base: memory-sharing-l3a, #4220). With this layer the minimum shippable bundle is complete: #4210 + #4217 + #4220 + #4224.

Background

#4220 kept downgrade behavior simple (an edited note re-adopts as an imported/ conflict copy; a deleted source keeps its copy). Codex showed a concrete data-loss path in that simplification: a second edit of a note already under imported/<child>/ could not be placed, so non-forced removal refused and a forced removal deleted the only copy. Every deferred thread on #4220 that named this layer is closed here.

Implementation

  1. Manifest records gain replaced, replacementContent, replacementStamp, pendingDeletion, and deleted (fail-closed flag parsing; 🤖 feat: adopt pre-sharing sub-agent notebooks into the shared store #4220-format records are read unchanged and upgraded when rewritten).
  2. In-place replacement: a target is replaced only while its live generation stamp still equals the stamp recorded at adoption or last replacement (proof that no owner edit happened); otherwise the edit lands under imported/<child>/ as before. Capacity is not charged for a replacement.
  3. Deletion/rename reconciliation: a source proven gone removes the adoption-created copy only when its bytes and live stamp are unchanged; pendingDeletion is written before the sidecar and file are removed so an interrupted deletion recovers; a rename transfers provenance to the successor with the on-disk generation.
  4. One owner per adoption-created copy: an identical owner file is reused only when it is not another descendant's adoption-created copy (sibling manifests are read strictly; an unreadable one makes the pass wait). Pins fold per copy; a reused owner-authored file keeps the owner's pin. This closes the pin-aggregation deferrals from 🤖 feat: adopt pre-sharing sub-agent notebooks into the shared store #4220 by removing shared targets rather than aggregating pins.
  5. Hardening: decoders use { fatal: true, ignoreBOM: true }; strict listFiles classifies unknown dirent types via lstat; legacyImportSegment escapes child IDs the memory path grammar rejects (=XX per UTF-8 byte).

Known limitations (later layers)

  1. Not merge-ready on its own: a stabilization layer (L3a-3) stacked on this PR owns the second review round's findings: one-time migration of parent-format shared receipts before destructive reconciliation, DT_UNKNOWN classification in the bounded fingerprint walk, downgrade safety of an interrupted in-place replacement, and treating an unavailable live stamp as transient.

  2. Refinement-row migration and rollback path remapping for adopted files (L3b); rollback of a child's own rows still fails closed.

  3. Throttled full-store fingerprint for over-cap legacy stores, cross-process sidecar locking, second-backend removal races (L4).

  4. Deletion tombstones are written with pending: true so the previous build treats them as unsettled; a descendant whose older record reused a copy that turns out to be sibling-owned migrates to its own copy on its next non-trivial pass.

Validation

26 new tests: in-place edit reflected without a duplicate, owner-edited target left alone, legacy deletion and rename follow adoption-created copies only, owner note recreated at a deleted target preserved, interrupted replacement and deletion recovery, pending-deletion target never read, reappeared identical source re-adopted, provenance transfer on rename, sibling-owned identical target forced to its own copy with owner-authored control, unreadable sibling manifest waits, rename at capacity lands in one pass, tombstones read as unsettled by the previous build, BOM survives byte-exact, DT_UNKNOWN dirents adopted, proj~1-childimported/proj=7E1-child/. Each part was red-checked against the previous code. Sibling suites: 1803 pass.

Risks

Medium, confined to the adoption pass and manifest. Replacement and deletion touch only files whose provenance stamp proves adoption created them; owner-authored files are never modified or removed.


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

…s, escape child ids in import paths

Three adoption-pass hardening items carried from #4220 review:

- BOM: both strict decoders (legacy source, owner destination) now pass
  ignoreBOM so a leading U+FEFF survives into the copy and its hash, and a
  BOM-less owner note no longer compares equal to a BOM-prefixed source.
- Strict walk: LocalMemoryStore.listFiles classifies a dirent whose type
  the filesystem did not report (DT_UNKNOWN, every predicate false) by
  lstat instead of dropping it, so removal's handover cannot see a
  complete listing that omits a note or a subtree.
- Import segment: imported/<childId>/ is built through
  legacyImportSegment — ids the memory path grammar admits are used
  verbatim, the rest (a legacy id keeping a project basename's `~`, `..`,
  `%2e`, control or XML characters) are escaped per UTF-8 byte as `=XX`;
  a verbatim segment never contains `=`, so the forms cannot collide.

---

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

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=86.05 -->
…ed legacy notes

The adoption pass now follows what a downgraded build does to a legacy
note after its first adoption, using the generation stamps the manifest
already records as provenance:

- In-place replacement: legacy bytes edited while the copy THIS adoption
  created is still its own generation (targetStamp, or replacementStamp
  on the far side of an interrupted replacement) replace that copy —
  staged, receipted, and installed only if the target is still the
  generation the decision was made against. A copy the owner edited or
  recreated keeps the owner's bytes and the edit lands under imported/.
  The pending prior record carries replacementContent/replacementStamp
  so a retry recognizes the copy on either side of an interrupted write;
  a copy the owner replaced is recorded `replaced` so the child's pin
  toggles no longer reach it.
- Deletion/rename reconciliation: a source proven gone (ENOENT/ENOTDIR)
  removes the adoption-created copy only while it is unchanged and still
  this adoption's generation; owner-authored or owner-edited files stay.
  A rename onto the path of its own conflict copy transfers provenance
  to the successor record (with the generation actually on disk) instead
  of deleting the file from under it. pendingDeletion is written before
  the removal so a crash between removal and tombstone is recovered as
  "removed by us", never as owner-changed; the record stays as a
  `deleted` tombstone (rollback mapping) and a reappearing source is
  adopted afresh. Target inspection goes through the R5 destination
  helper: non-regular entries, over-cap and non-UTF-8 files are owner
  state, transient failures keep the pass unmemoized.
- Manifest: replaced/replacementContent/replacementStamp/pendingDeletion/
  deleted fields parse fail-closed like the existing flags; records
  written by the previous layer read unchanged and are upgraded in place.

Tests are the FINAL reconciliation suite (17), adapted only where this
layer differs (lstat-based target inspection; a fresh sidecar instance
for a cross-instance pin read).

---

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

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=90.30 -->
…ed copy

A copy the adoption created has no owner choice behind its pin: the
descendants whose legacy notes it represents own it together, so their
pins now fold as an OR. Two pre-sharing descendants with the same note,
the first adopted unpinned: the second's pin is no longer dropped as
"the owner's choice". Both pinned, one unpinned on the old build: the
other's pin still protects the note, and a later unpin on that one is
the transition that clears it.

Ownership is decided by the LIVE generation, never by target path and
flags alone: this pass's record (a fresh write or a copy still stamped
as ours), or another descendant's settled record whose receipt
(targetStamp/replacementStamp) matches the stamp of the file on disk.
An owner-edited or recreated generation is the owner's — the owner's
pin stands and the existing transition rule applies. Only a true
aggregate is applied; with every owning pin off, an unpin remains this
child's own transition, so a mere view never clears a pin the owner set.

Sibling manifests are read tolerantly (nothing contributes on failure),
once per pass and only when a note has a sidecar entry to fold.
MemoryMetaService.mergeKeys gains `pinned: "on"` for the aggregate.

---

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

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=95.19 -->
@ThomasK33
ThomasK33 added this pull request to stack #4218 September 11, 2026 18:38
@ThomasK33 ThomasK33 changed the title memory sharing l3a2 🤖 feat: reconcile downgrade-time edits of adopted sub-agent notebooks Sep 11, 2026
@ThomasK33
ThomasK33 marked this pull request as ready for review September 11, 2026 18:39
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 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-11T19:41:27.709694Z 2286177 Manual request
🔒 Security Review Completed 2026-09-11T19:39:05.117195Z 2286177 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: 8a20160125

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: 8a20160125

ℹ️ 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
Comment thread src/node/services/memoryService.ts Outdated
Comment thread src/node/services/memoryService.ts Outdated
…hared copy

This reverts commit 8a20160 (mergeKeys `pinned: "on"`, the OR
aggregate over sibling manifests, its tests). Review of #4224 showed the
shared-copy model it aggregated over is itself the problem: one owner
file standing for several descendants' notes lets one descendant's
in-place replacement destroy a sibling's bytes, one descendant's source
deletion remove the copy the others still need, a reused generation
never be marked replaced, and an unreadable sibling manifest change pin
outcomes. The follow-up commit makes an adoption-created target belong
to exactly one descendant instead, so pins fold per copy under the
existing rule. The FINAL pin test that exercises that rule (owner-owned
generation untouched, a real child toggle folds) is kept.

---

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

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=104.71 -->
…an identical sibling's

An adoption-created target now belongs to exactly one descendant. A
descendant whose legacy note is byte-identical to an existing owner file
reuses it only when that file is NOT another descendant's adoption-created
copy — decided by the LIVE generation: a sibling's settled `created`
record naming the path whose receipt (targetStamp, or replacementStamp on
the far side of an interrupted replacement) equals the stamp on disk.
Otherwise the note gets its own copy under imported/<child>/ (created:
true), and its pin folds into that copy under the existing per-copy rule.
The owner's own identical notes keep the previous reuse (created: false,
pin "target"); an owner-edited generation is nobody's to reuse against.

The sibling manifests are read strictly, once per pass and only when a
candidate is identical: an unreadable or malformed one leaves the
question unanswered, so the note is skipped transiently (no reuse, no pin
change, pass not memoized) rather than reused against a copy that may be
a sibling's. The check runs both at placement (legacyImportTarget) and
when a prior record's target turns out not to be ours.

By construction this removes the failure modes of a shared copy: one
child's in-place replacement rewriting a sibling's bytes, one child's
source deletion removing a copy others still need, a reused generation
that could never be marked replaced, and pin outcomes depending on a
sibling's manifest being readable.

---

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

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=104.71 -->
…ed notes

The deletion/rename reconciliation now runs BEFORE the listed notes are
adopted, and the slot a removed copy frees is credited to the same pass.
A downgraded rename in an owner store at capacity therefore lands at
once: previously the new name was skipped as "full" (and the pass
memoized) while the old copy still held the slot, so the rename never
completed until something else changed.

Reconciling first also makes the successor-provenance transfer
unnecessary: a note renamed onto the path of its own conflict copy finds
the path free (the old copy removed under its own record's provenance,
tombstoned created: true) and is adopted as a fresh copy with its own
generation, instead of one file being handed between two records. The
two FINAL successor tests are adapted to that outcome.

---

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

<!-- mux-attribution: model=anthropic:claude-fable-5-1 thinking=high costs=104.71 -->
…hem as unsettled

A `deleted` tombstone is now written with `pending: true`. The previous
build knows neither the flag nor the reconciliation: it ignored the
tombstone's unknown fields and, finding the settled hash and sidecar,
took a reappearing source as "folded in earlier" — its forced handover
then reported completion and deleted the child session while no copy
existed. Pending, that build re-adopts the source (its target is free).
Here `deleted` takes precedence, so a tombstone never reads as an
interrupted adoption.

---

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

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

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: 22861779b1

ℹ️ 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
Comment thread src/node/services/memoryService.ts
Comment thread src/node/services/memoryService.ts
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