Skip to content

fix(mcp): unique cursor_hash for write/summarize toggles (#238) - #245

Merged
radimsem merged 1 commit into
devfrom
fix/memorywrite-unique-cursor-hash-toggle
Jun 4, 2026
Merged

fix(mcp): unique cursor_hash for write/summarize toggles (#238)#245
radimsem merged 1 commit into
devfrom
fix/memorywrite-unique-cursor-hash-toggle

Conversation

@radimsem

@radimsem radimsem commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Closes #238.

Problem

MemoryWrite/MemorySummarize (via emitNodeChange) derived a snapshot's cursor_hash from pure post-state content (diff.CursorHash(roots), which hashes only id:content_hash). Re-writing a node with a previously-used (anchor, payload) reproduced an earlier hash; snapshots.cursor_hash is CHAR(16) NOT NULL UNIQUE, so the INSERT failed and the whole Tx rolled back. A write A → write B → write A toggle broke with an opaque UNIQUE constraint failed: snapshots.cursor_hash, and the write was lost.

Fix

Add diff.CursorHashForChange(prevHeadID, deltas) which folds the monotonic parent snapshot id (snapshots.id is AUTOINCREMENT, never reused) into the digest, making the hash unique per snapshot even when the content/transition repeats. emitNodeChange now reads GetHeadSnapshotID (safe under the Store.OpMu already held by both callers) and uses the new hash. This matches how MemoryRollback (CursorHashForRollback) and MemoryForget (CursorHashForDeltas) already avoid the same collision.

Kept the UNIQUE constraint (no migration); cursor_hash becomes a per-snapshot token rather than a content digest.

Docs

docs/versioning.md and skills/remind/references/snapshots-diffs.md corrected: cursor_hash is an opaque, unique-per-snapshot fingerprint — not a content digest (the prior wording implied identical content reproduces the hash).

Tests

  • pkg/diff/hash_test.go: CursorHashForChange gives distinct hashes for a repeated transition under different parent ids, and is order-stable.
  • pkg/mcp/tools/tools_test.go: TestHandleWrite_TogglePingPong (A→B→A→B→A) and TestHandleSummarize_RevertSucceeds (s1→s2→s1) — both assert one snapshot per call and the correct final content. Both fail on the old code.

Verification

go build ./..., go test ./... (1181 passed), go vet, golangci-lint run ./..., make check-skills — all green.

Follow-up

#244 tracks the same latent collision class in MemoryCompile/MemoryForget (content/delta-only hashes that don't fold the parent id; rarer, compile mostly shielded by the zero-delta short-circuit).

🤖 Generated with Claude Code

@radimsem
radimsem merged commit b91dd48 into dev Jun 4, 2026
5 checks passed
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.

[bug] MemoryWrite hits UNIQUE cursor_hash on toggle: write A → write B → write A trips snapshots.cursor_hash

1 participant