memory-seed 2.2.3__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {memory_seed-2.2.3 → memory_seed-2.4.0}/PKG-INFO +17 -5
  2. {memory_seed-2.2.3 → memory_seed-2.4.0}/README.md +16 -4
  3. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/cli.py +2 -0
  4. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/core.py +249 -10
  5. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/agent-rules.md +46 -36
  6. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +7 -2
  7. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +6 -5
  8. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +1 -1
  9. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/code_search.md +1 -1
  10. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +1 -1
  11. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/index.md +1 -1
  12. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +1 -1
  13. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +1 -1
  14. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +1 -1
  15. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +1 -1
  16. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +1 -1
  17. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/AGENTS.md +3 -2
  18. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/CLAUDE.md +1 -1
  19. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/GEMINI.md +1 -1
  20. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed.egg-info/PKG-INFO +17 -5
  21. {memory_seed-2.2.3 → memory_seed-2.4.0}/pyproject.toml +1 -1
  22. {memory_seed-2.2.3 → memory_seed-2.4.0}/tests/test_memory_seed.py +294 -26
  23. {memory_seed-2.2.3 → memory_seed-2.4.0}/LICENSE +0 -0
  24. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/__init__.py +0 -0
  25. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/mcp_server.py +0 -0
  26. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/mcp_validate.py +0 -0
  27. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
  28. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
  29. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed/semantic_cache.py +0 -0
  30. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed.egg-info/SOURCES.txt +0 -0
  31. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed.egg-info/dependency_links.txt +0 -0
  32. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed.egg-info/entry_points.txt +0 -0
  33. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed.egg-info/requires.txt +0 -0
  34. {memory_seed-2.2.3 → memory_seed-2.4.0}/memory_seed.egg-info/top_level.txt +0 -0
  35. {memory_seed-2.2.3 → memory_seed-2.4.0}/setup.cfg +0 -0
  36. {memory_seed-2.2.3 → memory_seed-2.4.0}/tests/test_mcp_server.py +0 -0
  37. {memory_seed-2.2.3 → memory_seed-2.4.0}/tests/test_mcp_validation.py +0 -0
  38. {memory_seed-2.2.3 → memory_seed-2.4.0}/tests/test_semantic_cache.py +0 -0
  39. {memory_seed-2.2.3 → memory_seed-2.4.0}/tests/test_session_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memory-seed
3
- Version: 2.2.3
3
+ Version: 2.4.0
4
4
  Summary: Portable local memory seed for file-reading AI coding agents
5
5
  Author: Jean Nathan Tshibuyi
6
6
  License: MIT
@@ -135,7 +135,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
135
135
 
136
136
  | Agent or client | Support path |
137
137
  | --- | --- |
138
- | Codex | Starts from `AGENTS.md`; can use MCP when the client supports stdio MCP servers. |
138
+ | Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
139
139
  | Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
140
140
  | Gemini CLI | Starts from `GEMINI.md`. |
141
141
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
@@ -200,7 +200,7 @@ GEMINI.md
200
200
 
201
201
  ## Current Version
202
202
 
203
- The current reusable control-plane version is `2.2`.
203
+ The current reusable control-plane version is `2.3`.
204
204
 
205
205
  Legacy `.AGENTS/` projects remain supported as a fallback during migration.
206
206
 
@@ -304,7 +304,7 @@ When `--force` creates backups, Memory Seed adds `.memory-seed/backups/` to the
304
304
 
305
305
  The `update` command refreshes routing files, reusable runtime procedure files, and generic skill templates by version, sourcing them **from the installed package** rather than from PyPI — upgrade the package first to get newer templates (see [Updating](#updating)). Before replacing stale reusable control-plane files, it backs them up under `.memory-seed/backups/<timestamp>/` and archives their old version under `.memory-seed/archive/<old-version>/` or `.memory-seed/archive/unknown-<timestamp>/` when the old version is missing. Generated local memory files such as `index.md`, `policy.md`, and sessions are preserved.
306
306
 
307
- Use `update --dry-run` to list the reusable control-plane targets without writing files. Current behavior is conservative but broad: dry-run lists bundled seed paths rather than calculating which files are missing or version-mismatched. The real `update` command skips files that already have the current `memory-system-version` and preserves existing `.memory-seed/` runtime files.
307
+ Use `update --dry-run` to list the reusable control-plane targets without writing files. Current behavior is conservative but broad: dry-run lists bundled seed paths rather than calculating which files are missing or version-mismatched. The real `update` command skips files already at the current `memory-system-version` or newer — so a stale installed tool never downgrades a project — and preserves existing `.memory-seed/` runtime files.
308
308
 
309
309
  The `compact` command summarises recent session activity from the nearest runtime so an agent can identify durable facts to promote into `index.md`, `policy.md`, or skills:
310
310
 
@@ -365,7 +365,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
365
365
 
366
366
  Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
367
367
 
368
- **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
368
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
369
+
370
+ > **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
371
+
372
+ > **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
369
373
 
370
374
  If you are configuring the server manually, run it over stdio:
371
375
 
@@ -422,6 +426,14 @@ memory_get_chunk(chunk_id, cwd=".")
422
426
 
423
427
  The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
424
428
 
429
+ ### Performance characteristics
430
+
431
+ `memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
432
+
433
+ Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
434
+
435
+ When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
436
+
425
437
  Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
426
438
 
427
439
  For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
@@ -114,7 +114,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
114
114
 
115
115
  | Agent or client | Support path |
116
116
  | --- | --- |
117
- | Codex | Starts from `AGENTS.md`; can use MCP when the client supports stdio MCP servers. |
117
+ | Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
118
118
  | Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
119
119
  | Gemini CLI | Starts from `GEMINI.md`. |
120
120
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
@@ -179,7 +179,7 @@ GEMINI.md
179
179
 
180
180
  ## Current Version
181
181
 
182
- The current reusable control-plane version is `2.2`.
182
+ The current reusable control-plane version is `2.3`.
183
183
 
184
184
  Legacy `.AGENTS/` projects remain supported as a fallback during migration.
185
185
 
@@ -283,7 +283,7 @@ When `--force` creates backups, Memory Seed adds `.memory-seed/backups/` to the
283
283
 
284
284
  The `update` command refreshes routing files, reusable runtime procedure files, and generic skill templates by version, sourcing them **from the installed package** rather than from PyPI — upgrade the package first to get newer templates (see [Updating](#updating)). Before replacing stale reusable control-plane files, it backs them up under `.memory-seed/backups/<timestamp>/` and archives their old version under `.memory-seed/archive/<old-version>/` or `.memory-seed/archive/unknown-<timestamp>/` when the old version is missing. Generated local memory files such as `index.md`, `policy.md`, and sessions are preserved.
285
285
 
286
- Use `update --dry-run` to list the reusable control-plane targets without writing files. Current behavior is conservative but broad: dry-run lists bundled seed paths rather than calculating which files are missing or version-mismatched. The real `update` command skips files that already have the current `memory-system-version` and preserves existing `.memory-seed/` runtime files.
286
+ Use `update --dry-run` to list the reusable control-plane targets without writing files. Current behavior is conservative but broad: dry-run lists bundled seed paths rather than calculating which files are missing or version-mismatched. The real `update` command skips files already at the current `memory-system-version` or newer — so a stale installed tool never downgrades a project — and preserves existing `.memory-seed/` runtime files.
287
287
 
288
288
  The `compact` command summarises recent session activity from the nearest runtime so an agent can identify durable facts to promote into `index.md`, `policy.md`, or skills:
289
289
 
@@ -344,7 +344,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
344
344
 
345
345
  Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
346
346
 
347
- **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
347
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
348
+
349
+ > **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
350
+
351
+ > **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
348
352
 
349
353
  If you are configuring the server manually, run it over stdio:
350
354
 
@@ -401,6 +405,14 @@ memory_get_chunk(chunk_id, cwd=".")
401
405
 
402
406
  The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
403
407
 
408
+ ### Performance characteristics
409
+
410
+ `memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
411
+
412
+ Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
413
+
414
+ When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
415
+
404
416
  Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
405
417
 
406
418
  For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
@@ -118,6 +118,8 @@ def main(argv: list[str] | None = None) -> int:
118
118
  print("Memory Seed bootstrap is complete.")
119
119
  else:
120
120
  print("Memory Seed bootstrap is incomplete.")
121
+ for warning in result.warnings:
122
+ print(f"Warning: {warning}")
121
123
  if result.ok:
122
124
  return 0
123
125
  for missing in result.missing:
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import re
5
5
  import hashlib
6
+ import tomllib
6
7
  from dataclasses import dataclass, field
7
8
  from datetime import datetime, timedelta
8
9
  from pathlib import Path
@@ -10,7 +11,7 @@ from pathlib import Path
10
11
 
11
12
  PACKAGE_ROOT = Path(__file__).resolve().parent
12
13
  SEED_ROOT = PACKAGE_ROOT / "seed"
13
- VERSION = "2.2"
14
+ VERSION = "2.4"
14
15
  MEMORY_DIR_NAME = ".memory-seed"
15
16
  LEGACY_MEMORY_DIR_NAME = ".AGENTS"
16
17
  BACKUP_IGNORE_ENTRY = ".memory-seed/backups/"
@@ -49,6 +50,7 @@ class DoctorResult:
49
50
  missing: list[str] = field(default_factory=list)
50
51
  version_mismatches: list[dict[str, str]] = field(default_factory=list)
51
52
  bootstrap_missing: list[str] = field(default_factory=list)
53
+ warnings: list[str] = field(default_factory=list)
52
54
 
53
55
 
54
56
  @dataclass
@@ -350,14 +352,19 @@ def _merge_cursor_retrieval_hook(target_root: Path) -> bool:
350
352
 
351
353
 
352
354
  def _merge_claude_mcp(target_root: Path) -> bool:
353
- """Upsert the memory-seed-mcp stdio server entry in .claude/settings.json."""
354
- settings_path = target_root / ".claude" / "settings.json"
355
- expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS, "type": "stdio"}
355
+ """Upsert the memory-seed-mcp stdio server entry in the project-root .mcp.json.
356
+
357
+ Claude Code discovers project-scope MCP servers from .mcp.json, not from
358
+ .claude/settings.json (that key is silently ignored by Claude Code).
359
+ _strip_claude_settings_mcp removes the legacy settings.json entry.
360
+ """
361
+ mcp_path = target_root / ".mcp.json"
362
+ expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
356
363
 
357
364
  data: dict = {}
358
- if settings_path.exists():
365
+ if mcp_path.exists():
359
366
  try:
360
- with open(settings_path) as f:
367
+ with open(mcp_path) as f:
361
368
  data = json.load(f)
362
369
  except (json.JSONDecodeError, OSError):
363
370
  data = {}
@@ -371,7 +378,44 @@ def _merge_claude_mcp(target_root: Path) -> bool:
371
378
 
372
379
  data.setdefault("mcpServers", {})[_MCP_SERVER_KEY] = expected
373
380
 
374
- settings_path.parent.mkdir(parents=True, exist_ok=True)
381
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
382
+ with open(mcp_path, "w") as f:
383
+ json.dump(data, f, indent=2)
384
+ f.write("\n")
385
+
386
+ return True
387
+
388
+
389
+ def _strip_claude_settings_mcp(target_root: Path) -> bool:
390
+ """Remove the legacy memory-seed MCP entry from .claude/settings.json.
391
+
392
+ Versions 2.2.0-2.3.0 wrote the server into .claude/settings.json > mcpServers,
393
+ which Claude Code never reads. Now that the server lives in .mcp.json, drop the
394
+ dead entry so it does not mislead. Only our own entry is removed; a foreign
395
+ server under the same key is left untouched.
396
+ """
397
+ settings_path = target_root / ".claude" / "settings.json"
398
+ if not settings_path.exists():
399
+ return False
400
+ try:
401
+ with open(settings_path) as f:
402
+ data = json.load(f)
403
+ except (json.JSONDecodeError, OSError):
404
+ return False
405
+
406
+ servers = data.get("mcpServers")
407
+ if not isinstance(servers, dict) or _MCP_SERVER_KEY not in servers:
408
+ return False
409
+
410
+ existing = servers.get(_MCP_SERVER_KEY, {})
411
+ is_ours = existing.get("command") in _OWN_MCP_COMMANDS or "memory-seed-mcp" in existing.get("args", [])
412
+ if not is_ours:
413
+ return False # foreign server under the same key; leave it alone
414
+
415
+ del servers[_MCP_SERVER_KEY]
416
+ if not servers:
417
+ del data["mcpServers"]
418
+
375
419
  with open(settings_path, "w") as f:
376
420
  json.dump(data, f, indent=2)
377
421
  f.write("\n")
@@ -439,6 +483,120 @@ def _merge_gemini_mcp(target_root: Path) -> bool:
439
483
  return True
440
484
 
441
485
 
486
+ # Header line for our entry in .codex/config.toml. Codex accepts both the bare
487
+ # and quoted single-segment table-key forms; we write the bare form.
488
+ _CODEX_MCP_HEADER_RE = re.compile(
489
+ r'^\[mcp_servers\.(?:memory-seed|"memory-seed")\]\s*$'
490
+ )
491
+
492
+
493
+ def _codex_expected() -> dict:
494
+ """The MCP table we want present under [mcp_servers.memory-seed]."""
495
+ return {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
496
+
497
+
498
+ def _codex_standard_header_index(lines: list[str]) -> int | None:
499
+ """Index of the standard ``[mcp_servers.memory-seed]`` header line, or None.
500
+
501
+ The in-place stale-update path can only rewrite an entry written with this
502
+ header form. Shared by _merge_codex_mcp (to decide whether it can migrate)
503
+ and _codex_mcp_status (to decide stale-fixable vs stale-manual), so the two
504
+ always agree on what counts as auto-fixable.
505
+ """
506
+ return next(
507
+ (i for i, ln in enumerate(lines) if _CODEX_MCP_HEADER_RE.match(ln)),
508
+ None,
509
+ )
510
+
511
+
512
+ def _render_codex_mcp_block() -> str:
513
+ """Render our fixed [mcp_servers.memory-seed] TOML table.
514
+
515
+ args is a TOML array of strings, which is JSON-compatible, so json.dumps
516
+ produces valid TOML for it.
517
+ """
518
+ return (
519
+ f"[mcp_servers.{_MCP_SERVER_KEY}]\n"
520
+ f'command = "{_MCP_SERVER_COMMAND}"\n'
521
+ f"args = {json.dumps(_MCP_SERVER_ARGS)}\n"
522
+ )
523
+
524
+
525
+ def _merge_codex_mcp(target_root: Path) -> bool:
526
+ """Upsert the memory-seed-mcp stdio server in the project .codex/config.toml.
527
+
528
+ Codex reads project-scoped MCP servers from .codex/config.toml under
529
+ [mcp_servers.<name>] (trusted projects only). This is a zero-dependency text
530
+ upsert: tomllib (stdlib, Python >=3.11) is used only to *inspect* current
531
+ state; writes are line-based so existing content and comments are preserved.
532
+
533
+ Returns True if the file was written, False if already current.
534
+
535
+ Known limitation (in-place stale-entry update only): rewriting a present-but-
536
+ outdated entry while preserving comments relies on finding the standard
537
+ ``[mcp_servers.memory-seed]`` header line. Detection itself is robust (tomllib
538
+ parses semantically), but if a user *hand-wrote* the entry in a form that has
539
+ no such header line — dotted keys (``mcp_servers.memory-seed.command = ...``),
540
+ an inline subtable under ``[mcp_servers]``, a fully inline
541
+ ``mcp_servers = { ... }``, or a header with a trailing comment / leading
542
+ indentation — and the entry is stale, this no-ops (returns False) rather than
543
+ risk a duplicate-key / invalid-TOML write. The no-op is intentionally not
544
+ silent: ``doctor`` classifies this case via _codex_mcp_status as a
545
+ ``stale-manual`` warning telling the user to fix it by hand. Memory Seed only
546
+ ever writes the standard header form, so this path is only reachable through
547
+ manual edits.
548
+ """
549
+ config_path = target_root / ".codex" / "config.toml"
550
+ block = _render_codex_mcp_block()
551
+
552
+ text = ""
553
+ parsed: dict = {}
554
+ if config_path.exists():
555
+ try:
556
+ text = config_path.read_text(encoding="utf-8")
557
+ parsed = tomllib.loads(text)
558
+ except (tomllib.TOMLDecodeError, OSError):
559
+ text = ""
560
+ parsed = {}
561
+
562
+ existing = parsed.get("mcp_servers", {}).get(_MCP_SERVER_KEY, {})
563
+ if existing == _codex_expected():
564
+ return False
565
+ if existing:
566
+ is_ours = (
567
+ existing.get("command") in _OWN_MCP_COMMANDS
568
+ or "memory-seed-mcp" in existing.get("args", [])
569
+ )
570
+ if not is_ours:
571
+ return False # a different server holds this key; don't overwrite
572
+
573
+ if not existing:
574
+ # Append our block, preserving everything above it.
575
+ new_text = text
576
+ if new_text and not new_text.endswith("\n"):
577
+ new_text += "\n"
578
+ if new_text:
579
+ new_text += "\n"
580
+ new_text += block
581
+ else:
582
+ # Stale entry: replace just our table's lines (header to next table/EOF).
583
+ lines = text.splitlines(keepends=True)
584
+ start = _codex_standard_header_index(lines)
585
+ if start is None:
586
+ # No standard header to anchor the rewrite. Don't risk a duplicate
587
+ # key; leave it for the user. doctor() flags this as stale-manual.
588
+ return False
589
+ end = start + 1
590
+ while end < len(lines) and not lines[end].lstrip().startswith("["):
591
+ end += 1
592
+ replacement = block if block.endswith("\n") else block + "\n"
593
+ new_text = "".join(lines[:start]) + replacement + "".join(lines[end:])
594
+
595
+ config_path.parent.mkdir(parents=True, exist_ok=True)
596
+ config_path.write_text(new_text, encoding="utf-8")
597
+ return True
598
+
599
+
442
600
  def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = False) -> InitResult:
443
601
  target_root = Path(cwd).resolve()
444
602
  planned = [seed_file.destination for seed_file in SEED_FILES]
@@ -484,9 +642,11 @@ def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = Fal
484
642
  (_merge_codex_retrieval_hook, ".codex/hooks.json"),
485
643
  (_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
486
644
  (_merge_gemini_retrieval_hook, ".gemini/settings.json"),
487
- (_merge_claude_mcp, ".claude/settings.json"),
645
+ (_merge_claude_mcp, ".mcp.json"),
646
+ (_strip_claude_settings_mcp, ".claude/settings.json"),
488
647
  (_merge_cursor_mcp, ".cursor/mcp.json"),
489
648
  (_merge_gemini_mcp, ".gemini/settings.json"),
649
+ (_merge_codex_mcp, ".codex/config.toml"),
490
650
  )
491
651
  for merge, destination in hook_merges:
492
652
  if merge(target_root) and destination not in created:
@@ -517,7 +677,9 @@ def update_project(cwd: str | Path = ".", dry_run: bool = False) -> InitResult:
517
677
  if _is_runtime_local_file(seed_file.destination) and destination.exists():
518
678
  continue
519
679
 
520
- if destination.exists() and _read_memory_system_version(destination) == VERSION:
680
+ if destination.exists() and _version_at_least(
681
+ _read_memory_system_version(destination), VERSION
682
+ ):
521
683
  continue
522
684
 
523
685
  if destination.exists():
@@ -549,9 +711,11 @@ def update_project(cwd: str | Path = ".", dry_run: bool = False) -> InitResult:
549
711
  (_merge_codex_retrieval_hook, ".codex/hooks.json"),
550
712
  (_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
551
713
  (_merge_gemini_retrieval_hook, ".gemini/settings.json"),
552
- (_merge_claude_mcp, ".claude/settings.json"),
714
+ (_merge_claude_mcp, ".mcp.json"),
715
+ (_strip_claude_settings_mcp, ".claude/settings.json"),
553
716
  (_merge_cursor_mcp, ".cursor/mcp.json"),
554
717
  (_merge_gemini_mcp, ".gemini/settings.json"),
718
+ (_merge_codex_mcp, ".codex/config.toml"),
555
719
  )
556
720
  for merge, destination in hook_merges:
557
721
  if merge(target_root) and destination not in created:
@@ -598,6 +762,28 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
598
762
  control_plane_ok = not missing and not version_mismatches
599
763
  bootstrap_complete = not bootstrap_missing
600
764
 
765
+ warnings: list[str] = []
766
+ codex_status = _codex_mcp_status(target_root)
767
+ if (target_root / ".codex" / "hooks.json").exists() and codex_status == "absent":
768
+ warnings.append(
769
+ "Codex hooks are installed but .codex/config.toml has no memory-seed MCP "
770
+ "entry. Run `memory-seed update`, then trust this directory in Codex so it "
771
+ "loads the project MCP server (memory_search / memory_get_chunk)."
772
+ )
773
+ elif codex_status == "stale-fixable":
774
+ warnings.append(
775
+ "Codex .codex/config.toml has an outdated memory-seed MCP entry. Run "
776
+ "`memory-seed update` to migrate it to `uvx --from memory-seed "
777
+ "memory-seed-mcp --stdio`."
778
+ )
779
+ elif codex_status == "stale-manual":
780
+ warnings.append(
781
+ "Codex .codex/config.toml has an outdated memory-seed MCP entry written in "
782
+ "a non-standard TOML form that `memory-seed update` cannot safely auto-fix. "
783
+ 'Set it by hand to: command = "uvx", args = ["--from", "memory-seed", '
784
+ '"memory-seed-mcp", "--stdio"].'
785
+ )
786
+
601
787
  return DoctorResult(
602
788
  ok=control_plane_ok and bootstrap_complete,
603
789
  control_plane_ok=control_plane_ok,
@@ -605,9 +791,47 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
605
791
  missing=missing,
606
792
  version_mismatches=version_mismatches,
607
793
  bootstrap_missing=bootstrap_missing,
794
+ warnings=warnings,
608
795
  )
609
796
 
610
797
 
798
+ def _codex_mcp_status(target_root: Path) -> str:
799
+ """Classify our memory-seed entry in .codex/config.toml.
800
+
801
+ Returns one of:
802
+ "absent" - no entry (or no/unparseable file)
803
+ "current" - present and matches the expected uvx command + args
804
+ "foreign" - present but owned by a different server
805
+ "stale-fixable" - ours but outdated, written with a standard header that
806
+ `memory-seed update` can auto-migrate
807
+ "stale-manual" - ours but outdated, written in a form with no standard
808
+ header line, so update no-ops and the user must edit it
809
+ """
810
+ config_path = target_root / ".codex" / "config.toml"
811
+ if not config_path.exists():
812
+ return "absent"
813
+ try:
814
+ text = config_path.read_text(encoding="utf-8")
815
+ parsed = tomllib.loads(text)
816
+ except (tomllib.TOMLDecodeError, OSError):
817
+ return "absent"
818
+
819
+ existing = parsed.get("mcp_servers", {}).get(_MCP_SERVER_KEY, {})
820
+ if not existing:
821
+ return "absent"
822
+ if existing == _codex_expected():
823
+ return "current"
824
+ is_ours = (
825
+ existing.get("command") in _OWN_MCP_COMMANDS
826
+ or "memory-seed-mcp" in existing.get("args", [])
827
+ )
828
+ if not is_ours:
829
+ return "foreign"
830
+ if _codex_standard_header_index(text.splitlines()) is not None:
831
+ return "stale-fixable"
832
+ return "stale-manual"
833
+
834
+
611
835
  def compact_sessions(
612
836
  cwd: str | Path = ".",
613
837
  days: int = 7,
@@ -677,6 +901,21 @@ def _read_memory_system_version(path: Path) -> str | None:
677
901
  return match.group(1)
678
902
 
679
903
 
904
+ def _version_tuple(version: str | None) -> tuple[int, ...]:
905
+ """Dotted version -> int tuple. Missing/unparseable -> (-1,) so it always
906
+ compares older than any real version and is upgraded forward."""
907
+ if not version:
908
+ return (-1,)
909
+ try:
910
+ return tuple(int(part) for part in version.strip().split("."))
911
+ except ValueError:
912
+ return (-1,)
913
+
914
+
915
+ def _version_at_least(actual: str | None, minimum: str) -> bool:
916
+ return _version_tuple(actual) >= _version_tuple(minimum)
917
+
918
+
680
919
  def _is_runtime_local_file(destination: str) -> bool:
681
920
  reusable_runtime_files = {
682
921
  f"{MEMORY_DIR_NAME}/agent-rules.md",
@@ -1,5 +1,5 @@
1
1
  ---
2
- memory-system-version: 2.2
2
+ memory-system-version: 2.4
3
3
  tags:
4
4
  - memory-seed
5
5
  - agent-rules
@@ -292,12 +292,12 @@ Deferring or batching session log writes is a discipline failure, not an accepta
292
292
  The session file is strictly append-only and must stay in ascending time order. To guarantee this without ever reordering:
293
293
 
294
294
  - Append every new entry to the **end** of the day's file. Never insert an entry above an existing one.
295
- - **Use `>>` shell redirection or Python append mode (`open(f, 'a')`) to write entries do not use an editor replace/insert operation.** Replace/insert requires selecting an anchor line; if a prior edit already added content after that anchor, the new entry lands mid-file instead of at the end. Append mode writes to the physical end of the file unconditionally, with no anchor needed.
295
+ - **Append each entry at the physical end of the file; never insert above an existing entry.** That is the rule; the mechanism is up to your tools. The common failure is anchor-based edit tools — if you target a line you wrote earlier, a later edit may already sit below it, so the new entry lands mid-file. Avoid it by confirming the *actual* last line immediately before appending (for example, read the file's tail); never reuse an anchor from memory. With a POSIX shell or Python, append mode (`>> file`, `open(f, 'a')`) sidesteps anchors entirely — write UTF-8 (some shells, for example PowerShell `>>`, default to other encodings).
296
296
  - The entry heading timestamp is the **actual current clock time** at the moment you write it. Read it from the system clock; never reuse a time from your context, memory, or an earlier message, and never backdate it to when the work happened.
297
297
  - Because entries are always appended with the current time, file order, write order, and timestamp order are identical. No manual reordering is ever needed or allowed.
298
298
  - If you are recording work that completed earlier in the session (you forgot, or you are catching up), still stamp the heading with the current time. If the original work time matters, state it in the entry body — do not move the entry above newer ones and do not rewrite earlier headings.
299
299
 
300
- This is the invariant the "do not rewrite old session entries" rule protects: out-of-order or backdated entries force a human to manually re-sort the log, which is exactly what append-only is meant to prevent. The `>>` / append-mode discipline makes this invariant mechanical rather than reliant on careful anchor selection.
300
+ This is the invariant the "do not rewrite old session entries" rule protects: out-of-order or backdated entries force a human to manually re-sort the log, which is exactly what append-only is meant to prevent. Confirming the real last line before each append — or using append mode where your environment supports it — makes this invariant mechanical rather than reliant on a remembered anchor.
301
301
 
302
302
  Detailed work logs belong in the nearest active runtime. Add a parent/root summary only when sub-project work changes parent-visible topology, shared design, release behavior, policy inheritance, cross-project dependencies, risks, or active priorities. Do not mirror sub-project logs into root memory.
303
303
 
@@ -366,7 +366,7 @@ tags:
366
366
  session_date: 2026-05-02
367
367
  ---
368
368
 
369
- ## 2026-05-02 14:35 - Project setup and workflow update
369
+ ## 2026-05-02 14:35 - Switch cache key to content hash
370
370
 
371
371
  ```yaml
372
372
  entry_id: ms-8charhash
@@ -375,14 +375,17 @@ agent_type: codex
375
375
  project_path: .
376
376
  subproject_path: null
377
377
  ```
378
-
379
378
  ### Summary
380
379
 
381
- - Updated the project workflow or implementation area touched today.
380
+ - What changed or what was checked.
382
381
 
383
- ### Validation
382
+ ### Decision
384
383
 
385
- - Ran the smallest relevant verification.
384
+ - D: State the decision that was made or implemented. (mandatory)
385
+ - R: Explain the decisive reason in 1-3 bullets. (mandatory)
386
+ - A: Alternative considered or rejected, with reason, if it mattered. (optional)
387
+ - F: Files, artifacts, or behaviors changed. (optional)
388
+ - T: Tests or validation outcome. (optional)
386
389
 
387
390
  ### Follow-up
388
391
 
@@ -393,13 +396,13 @@ Keep session filenames date-only, such as `.memory-seed/sessions/2026-05-02.md`.
393
396
 
394
397
  ### Reason Rules
395
398
 
396
- **DRAFT** is the compact decision-record format used inside session entries. Use it whenever a meaningful decision was made or implemented.
399
+ **DRAFT** is the baseline decision-record format for session entries: default to it whenever a turn produced a decision or a durable change. Route to a simpler or richer shape only as the exception (see Entry Shapes), and never invent a decision just to fill the baseline.
397
400
 
398
401
  A DRAFT decision record uses compact labels:
399
402
 
400
- - D = Decision — what was chosen
401
- - R = Reason — the decisive reason, 1–3 bullets; **required**
402
- - A = Alternatives considered or rejected, with reason (optional unless it shaped the tradeoff)
403
+ - D = Decision (mandatory) — what was chosen
404
+ - R = Reason (mandatory) — the decisive reason, 1–3 bullets
405
+ - A = Alternatives considered or rejected (optional) — with reason, when it shaped the tradeoff
403
406
  - F = Files, artifacts, or behaviors changed (optional)
404
407
  - T = Tests or validation outcome (optional; may appear inline as `- T:` or as a separate `### Validation` section)
405
408
 
@@ -414,11 +417,32 @@ A DRAFT decision record uses compact labels:
414
417
 
415
418
  ### Entry Shapes
416
419
 
417
- Use the lightest entry shape that preserves future usefulness.
420
+ Default to the **Meaningful decision entry** a single DRAFT record. Route away from it only when the work does not fit:
421
+
422
+ - down to the **Small work entry** for routine edits, small fixes, or verification-only work with no real decision (do not invent reason — see Reason Rules);
423
+ - up to the **Multi-decision session entry** when one coherent task produced several decisions.
424
+
425
+ #### Meaningful decision entry
426
+
427
+ The baseline shape: use when a turn produced one durable decision.
428
+
429
+ ```markdown
430
+ ### Summary
431
+
432
+ - Summarize the coherent task.
433
+
434
+ ### Decision
435
+
436
+ - D: State the decision. (mandatory)
437
+ - R: Explain the decisive reason in 1-3 bullets. (mandatory)
438
+ - A: Alternative considered or rejected, with reason, if it mattered. (optional)
439
+ - F: Files, artifacts, or behaviors changed. (optional)
440
+ - T: Tests or validation outcome. (optional)
441
+ ```
418
442
 
419
443
  #### Small work entry
420
444
 
421
- Use for routine edits, small fixes, or verification-only work.
445
+ Simpler alternative — for routine edits, small fixes, or verification-only work with no real decision.
422
446
 
423
447
  ```markdown
424
448
  ### Summary
@@ -434,23 +458,9 @@ Use for routine edits, small fixes, or verification-only work.
434
458
  - Only include if there is residual risk or a next action.
435
459
  ```
436
460
 
437
- #### Meaningful decision entry
438
-
439
- Use when one durable decision was made or implemented.
440
-
441
- ```markdown
442
- ### Decision
443
-
444
- - D: State the decision.
445
- - R: Explain the decisive reason in 1-3 bullets.
446
- - A: Alternative considered or rejected, with reason, if it mattered.
447
- - F: Files, artifacts, or behaviors changed.
448
- - T: Tests or validation outcome.
449
- ```
450
-
451
461
  #### Multi-decision session entry
452
462
 
453
- Use one entry when several decisions belong to one coherent task, plan, or user goal. Split entries when decisions affect unrelated subsystems, sub-projects, or goals.
463
+ Richer alternative — use one entry when several decisions belong to one coherent task, plan, or user goal. Split entries when decisions affect unrelated subsystems, sub-projects, or goals.
454
464
 
455
465
  ```markdown
456
466
  ### Summary
@@ -461,16 +471,16 @@ Use one entry when several decisions belong to one coherent task, plan, or user
461
471
 
462
472
  #### D1 - Short decision name
463
473
 
464
- - D: State the choice.
465
- - R: Explain the decisive reason in 1-3 bullets.
466
- - A: Alternative considered or rejected, with reason, if it mattered.
467
- - F: Files, artifacts, or behaviors changed.
468
- - T: Tests or validation outcome.
474
+ - D: State the choice. (mandatory)
475
+ - R: Explain the decisive reason in 1-3 bullets. (mandatory)
476
+ - A: Alternative considered or rejected, with reason, if it mattered. (optional)
477
+ - F: Files, artifacts, or behaviors changed. (optional)
478
+ - T: Tests or validation outcome. (optional)
469
479
 
470
480
  #### D2 - Short decision name
471
481
 
472
- - D: State the choice.
473
- - R: Explain the decisive reason in 1-3 bullets.
482
+ - D: State the choice. (mandatory)
483
+ - R: Explain the decisive reason in 1-3 bullets. (mandatory)
474
484
 
475
485
  ### Implementation
476
486