Skip to content

fix(import): keep the replaced content when an import overwrites a memory - #556

Merged
kevintseng merged 6 commits into
mainfrom
fix/import-overwrite-history
Sep 30, 2026
Merged

kevintseng merged 6 commits into
mainfrom
fix/import-overwrite-history

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

Refs #530.

What was wrong

import with merge strategy overwrite cleared the observations, tags and title of every existing memory the bundle named, and kept nothing. There was no way to get the old content back.

What changed

  • An overwrite now keeps the replaced version (title, observations, tags) in the memory's metadata.replaced_history, the same way remember with replace: true does. MCP import, POST /v1/import and memesh import --merge overwrite all do this.
  • The same bounds apply: the newest 20 versions and at most 64 KB. A single version larger than that keeps as many observations, then tags, as fit and is marked truncated. remember replace now trims tags of an oversized version too.
  • Restoring the same backup again records nothing, so it cannot push genuine older versions out of the history. A change to only the title, only the tags or only the order of observations still records a version.
  • A bundle still cannot put its own history onto an existing memory.
  • The tool descriptions and docs/api/API_REFERENCE.md say what overwrite now does.

Tests

  • tests/core/import-overwrite-history.test.ts (new) and the updated cases in tests/core/export-import.test.ts fail without the fix and pass with it.
  • npm run verify is green.

…mory

An import with merge strategy overwrite cleared the observations and tags of
every existing memory the bundle named, and kept nothing. It now records the
replaced version (title, observations, tags) in the memory's
`metadata.replaced_history`, the same way `remember` with `replace` does, with
the same bounds: the newest 20 versions and at most 64 KB, a single oversized
version keeping the observations, then the tags, that fit. A bundle still
cannot put its own history onto an existing memory.

Refs #530
…ry out

An `import` overwrite whose content equals what the memory already holds
(title, observations in order, tags) now records no history entry.
Re-running a backup restore with overwrite used to add an identical version
each time, so after 20 restores the memory's genuine earlier versions were
gone from `replaced_history`. A change to only the title, only the tags or
only the order of observations still records a version.

The tool description exported for OpenAI-format clients no longer says
overwrite deletes the previous content.

Refs #530
…tory bounds

- An import file that names one memory more than once is refused before
  anything is written. Otherwise the second entry's overwrite filed the
  first (possibly huge) entry into the memory's history, and the size limit
  then pushed every earlier version out.
- `npm run audit:memory` now reports a memory whose replaced_history is not
  a list, holds more than 50 versions, or is larger than 256 KiB, the most
  any writer stores (a trusted restore of a fresh memory; `remember`
  replace and an import overwrite keep 20 and 64 KiB).
- The audit exits 2 when a file is not a database or cannot be queried,
  instead of printing a clean verdict; only a missing table or column is
  still skipped as an older schema.
- The audit prints the byte count the bound is judged by.
- The import documentation says overwrite adds the replaced version
  (nothing when the content is identical), and that `truncated` also
  covers tags.

Refs #530
…history

# Conflicts:
#	CHANGELOG.md
#	dist/mcp/THIRD_PARTY_NOTICES.txt
#	dist/mcp/server.js.map
#	dist/transports/mcp/handlers.js.map
…name

The duplicate-name refusal compared UTF-16 strings, but SQLite stores a
lone surrogate as U+FFFD, so two such names in one import file were the
same memory and slipped past it. The refusal now compares the spelling the
database keeps. The audit script's header states all of its exit codes.

Refs #530
The duplicate-name check now takes the name through a UTF-8 round trip,
which turns a lone surrogate into U+FFFD exactly as SQLite does, instead
of a hand-written pattern. The tests also cover lone low halves, runs of
high halves, and two emoji that share their second half.

Refs #530
@kevintseng
kevintseng merged commit 653d456 into main Sep 30, 2026
14 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.

1 participant