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:
mempalace init <project> --backend chroma, then edit ~/.mempalace/config.json: point palace_path somewhere of your own and add a people_map entry.
- Leave one trailing comma before the closing brace, or save the file with a UTF-8 byte-order mark.
mempalace init <another-project> --yes --no-llm --backend chroma.
- 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."
What happened?
Editing
~/.mempalace/config.jsonby hand and getting one character wrong costs every setting in it, at the next command that changes a setting.MempalaceConfig.__init__reads the file behindPath.exists()and folds every failure into an empty dict (config.py:375-380ondevelop):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 --backendreaches one of them and--langa 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-entrypeople_mapandmax_backups. Onemempalace init <another-project> --yes --no-llm --backend chromaafterwards:{"backend": "chroma"}{"backend": "chroma"}Nothing in the output mentioned it, and no copy was left in the directory.
What that costs:
people_mapis only ever written by hand, since nothing in the package callssave_people_map, so those mappings are gone.palace_pathfalls back to the default, so the palace the user configured is not the one the tools read any more.embedding_modelfalls back tominilm, and the docstring atconfig.py:746says 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 withembeddinggemmastops answering untilmempalace repair rebuild-indexis run.Hand-editing is how the file is meant to be changed:
website/guide/configuration.mdprints its JSON and a table of its keys, andpeople_maphas 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_settingheld the file open for 357 ms, and a reader in another thread got a file that would not parse twice against three complete reads.SIGKILLinside 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 defaultpalace_pathand 0people_mapentries, and oneset_backend("chroma")in it leaves the same 25 bytes.A one-key config is not by itself a sign of this: a first
mempalace initon 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
000the read fails,set_backend("qdrant")returns without raising, and the file is unchanged: theexcept OSError: passin each setter swallows the write failure, so the command reports success for a setting it did not save.What did you expect?
FileNotFoundErroris 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:125states the reason in a comment.How to reproduce:
mempalace init <project> --backend chroma, then edit~/.mempalace/config.json: pointpalace_pathsomewhere of your own and add apeople_mapentry.mempalace init <another-project> --yes --no-llm --backend chroma.mempalace statusnow points at the default palace path.For the second shape,
chmod 000 ~/.mempalace/config.jsonbefore step 3 instead: the command exits 0 and the backend is not recorded.Environment:
develop@ 4bc0c43Notes
mempalace initreaches two of these setters,set_entity_languagesunder--lang(cli.py:316) andset_backendunder--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.jsonraises, 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) andwrite_embedder_sidecar(backends/_sidecar.py:53-68), whose docstring says it preserves the other collections' entries. The first two are reached by a plainmempalace mineand both write atomically, so what they drop is dropped durably. Each is its own report, none filed yet.CLAUDE.mdstates the rule this breaks: "Never destroy existing data to rebuild. A crash mid-operation must leave the existing palace untouched."