Skip to content

mempalace init --backend overwrites a config.json it could not read, losing every setting in it #2356

Description

@mvalentsev

What happened?

Editing ~/.mempalace/config.json by hand and getting one character wrong costs every setting in it, at the next command that changes a setting.

MempalaceConfig.__init__ reads the file behind Path.exists() and folds every failure into an empty dict (config.py:375-380 on develop):

if self._config_file.exists():
    try:
        with open(self._config_file, "r", encoding="utf-8") as f:
            self._file_config = json.load(f)
    except (json.JSONDecodeError, UnicodeDecodeError, OSError):
        self._file_config = {}

All four setters then serialize that dict back over the whole file, opening it with mode "w": set_entity_languages (:719), set_embedding_model (:812), set_backend (:830), set_hook_setting (:1103). mempalace init --backend reaches one of them and --lang a second, on the global config, whatever directory the command is run in.

Measured through the CLI on a 219-byte config holding palace_path, collection_name, backend, embedding_model, a three-entry people_map and max_backups. One mempalace init <another-project> --yes --no-llm --backend chroma afterwards:

what was wrong with the config what was left
one trailing comma before the closing brace 25 bytes, {"backend": "chroma"}
a UTF-8 byte-order mark, which an editor can add on its own 25 bytes, {"backend": "chroma"}

Nothing in the output mentioned it, and no copy was left in the directory.

What that costs: people_map is only ever written by hand, since nothing in the package calls save_people_map, so those mappings are gone. palace_path falls back to the default, so the palace the user configured is not the one the tools read any more. embedding_model falls back to minilm, and the docstring at config.py:746 says switching models on an existing palace needs re-embedding because ChromaDB rejects reads when the persisted EF name does not match, so a palace mined with embeddinggemma stops answering until mempalace repair rebuild-index is run.

Hand-editing is how the file is meant to be changed: website/guide/configuration.md prints its JSON and a table of its keys, and people_map has no other way in.

The setters' own writes are the second source of files that do not parse. On a 3,617,966-byte config one set_hook_setting held the file open for 357 ms, and a reader in another thread got a file that would not parse twice against three complete reads. SIGKILL inside that window left a truncated config in 10 runs out of 10, from 508,435 to 1,770,851 bytes. A process started on one of those answers the default palace_path and 0 people_map entries, and one set_backend("chroma") in it leaves the same 25 bytes.

A one-key config is not by itself a sign of this: a first mempalace init on a machine with no config leaves exactly that file, for its own reason.

A second shape loses nothing and says nothing either. With the config present and at mode 000 the read fails, set_backend("qdrant") returns without raising, and the file is unchanged: the except OSError: pass in each setter swallows the write failure, so the command reports success for a setting it did not save.

What did you expect?

FileNotFoundError is the one outcome that establishes there is no config here, because nothing resolved under that name. Everything else says this process did not read it, which is a reason to keep it: rename it aside and say where it went, or leave it alone and say so, but do not let defaults become the file's new contents.

And a write that replaces a file wholesale belongs behind a temporary file and a rename. The package does that in eight other places, and hallways.py:125 states the reason in a comment.

How to reproduce:

  1. mempalace init <project> --backend chroma, then edit ~/.mempalace/config.json: point palace_path somewhere of your own and add a people_map entry.
  2. Leave one trailing comma before the closing brace, or save the file with a UTF-8 byte-order mark.
  3. mempalace init <another-project> --yes --no-llm --backend chroma.
  4. Read the file. It is 25 bytes holding the one key that run set, and mempalace status now points at the default palace path.

For the second shape, chmod 000 ~/.mempalace/config.json before step 3 instead: the command exits 0 and the backend is not recorded.

Environment:

  • OS: Linux (WSL2)
  • Python version: 3.12.3
  • MemPal version: develop @ 4bc0c43

Notes

mempalace init reaches two of these setters, set_entity_languages under --lang (cli.py:316) and set_backend under --backend (:477); onboarding reaches a third (onboarding.py:407, :518), and the MCP hook-settings tool the fourth (mcp_server.py:4239, :4242). cfg.init() itself writes only when the file does not exist. save_people_map (:1146) writes its own file the same truncating way.

#2234 reports the crash a non-object config.json raises, addressed by #2235; this report is about a file that does not parse at all and about what the setters then write over it.

This is the overwrite half of the shape #2320 reports for deletions, and that report names this file as one of the two places outside its scope. The other, miner.add_to_known_entities, is filed separately.

Four more places fold a read that did not conclude into an empty default and write that default back over the same file: palace_graph._load_tunnels (:490-499), hallways._load_hallways (:98-104), EntityRegistry.load (entity_registry.py:308-316) and write_embedder_sidecar (backends/_sidecar.py:53-68), whose docstring says it preserves the other collections' entries. The first two are reached by a plain mempalace mine and both write atomically, so what they drop is dropped durably. Each is its own report, none filed yet.

CLAUDE.md states the rule this breaks: "Never destroy existing data to rebuild. A crash mid-operation must leave the existing palace untouched."

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions