Skip to content

Do not overwrite an unrelated memory on a derived-slug collision - #132

Open
kratos0718 wants to merge 1 commit into
phasespace-labs:mainfrom
kratos0718:fix/derived-slug-collision
Open

Do not overwrite an unrelated memory on a derived-slug collision#132
kratos0718 wants to merge 1 commit into
phasespace-labs:mainfrom
kratos0718:fix/derived-slug-collision

Conversation

@kratos0718

Copy link
Copy Markdown

Fixes #129.

What was happening

When slug is omitted, it is derived from the opening words of the content. Two saves whose openings agree resolve to the same path, and the second replaces the first — no error, no warning, and a receipt identical to a fresh create. Reproducing your three-save case in a test:

expected 3 distinct paths, got
  ["insights/user-thanks-that-helps-a-lot.md",
   "insights/user-thanks-that-helps-a-lot.md",
   "insights/user-thanks-that-helps-a-lot.md"]
assert 1 == 3

The distinction the fix turns on

The existing-path branch is deliberate, and I did not want to disturb it. Re-saving an explicit slug is an update of the same logical memory — that is the case created_at carry-forward exists for, and it should keep overwriting.

A derived slug colliding is a different thing: two unrelated memories that happen to start alike. So the save path now records whether the slug was derived, and only that case is disambiguated. Explicit-slug behaviour is byte-for-byte unchanged.

Behaviour

  • Derived slug, path free → unchanged.
  • Derived slug, path taken by different content → next free slug-2, slug-3, … Nothing is overwritten and the receipt reports the path actually written.
  • Derived slug, path taken by identical content → same file, no duplicate. The incoming content hash is compared against the content_hash already in the target's frontmatter, so the same memory arriving twice does not litter the store.
  • Explicit slug → unchanged, still overwrites.

The suffix search is bounded, with a timestamp fallback so it always terminates.

Open question

I kept explicit-slug collisions silent, since that is the current contract. If you would rather they became loud too — a distinct receipt field, or a rejection when update_policy does not permit replacement — I am happy to follow up; it felt like your design call rather than mine, so I did not fold it into a bug fix.

Tests

tests/test_save_derived_slug_collision.py:

test before after
test_colliding_derived_slugs_do_not_overwrite FAIL — 1 distinct path, ALPHA and BRAVO gone pass
test_identical_content_is_not_duplicated FAIL pass
test_explicit_slug_still_overwrites pass pass

The third passes before and after on purpose — it pins the behaviour this change must not alter.

pytest -m "not slow"   3114 passed, 10 skipped, 5 xfailed
ruff check             All checks passed
bandit -r palinode -ll clean

When save derives a slug from the opening words of the content, two saves
whose openings agree resolve to the same path and the second replaces the
first -- no error, no warning, and a receipt identical to a fresh create.
The displaced memories remain in git history but leave the queryable
store entirely.

A derived slug that collides now takes the next free suffixed path, so
each memory keeps its own file. Re-saving identical content is still a
single file: the incoming content hash is compared against the
content_hash already in the target's frontmatter, so the same memory
arriving twice does not accumulate duplicates.

An explicitly passed slug is untouched. A collision there is an update of
the same logical memory, which is what the existing created_at
carry-forward assumes, and it keeps overwriting.
@Paul-Kyle

Copy link
Copy Markdown
Member

The main collision fix is right, but there is one idempotency case the current tests don't reach.

If A owns slug.md, the first save of B correctly lands at slug-2.md. Re-saving B then compares
only against A at the base path, skips slug-2.md because it exists without checking its recorded
content_hash, and writes the same B content again at slug-3.md.

Please make the suffix scan reuse an existing candidate whose content_hash matches the incoming
content, and add this regression:

  1. save A → slug.md
  2. save B → slug-2.md
  3. save B again → still slug-2.md, with exactly two files in the directory

Everything else in the approach looks good, including keeping explicit-slug overwrites and failing
away from an unreadable file rather than clobbering it. Once this case is green, I'll re-review the
exact head for merge.

@Paul-Kyle

Copy link
Copy Markdown
Member

One addition to the note above, and it's our sequencing rather than anything on your side: #131
merged a couple of hours ago, so this branch now also shows as behind main.

So when you push the idempotency fix, update onto main in the same go — GitHub's Update branch
button will do it. Flagging it now so you don't push, watch the checks go green, and then find the
merge still blocked.

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.

save silently overwrites an existing memory when the slug is derived from content

2 participants