feat(graph): keep code graphs current automatically - #129
Conversation
Indexed repositories are re-indexed in the background on both transports, on by default. A code graph was previously only as fresh as the last manual marm_graph_index call, and a stale graph does not fail loudly: it answers confidently from deleted code. Change detection is a git signature computed outside the engine, so an idle cycle costs no engine lock. Deliberately not the engine's detect_changes, which reports dirty-tree-vs-HEAD and goes silent after a commit while the graph still lacks every symbol in it. A dirty repo is re-indexed every cycle instead, because git status reports which files changed and not what is in them, so repeated edits to one file produce byte-identical output. Every mutation of the engine's project store now passes one leased row in the memory database: the three manual index paths, the poller, and delete_project. HTTP and STDIO are separate processes with separate engine children over one shared store, so the previous in-process lock spanned nothing. The lease is released when the engine call returns rather than when its caller stops waiting, since asyncio.to_thread cancellation leaves the thread writing. - lease_lock.py extracts the concept lock's mechanics, parameterized by table. Its public API, table, and log event names are unchanged. - runtime_flags.py persists the on/off switches and per-project blocks. A saved override beats the environment variable, and both workers re-read per cycle, so projects auto off and knowledge auto off need no restart. - A Windows MAX_PATH overflow is now reported as such. The engine reports it as a contained per-file worker crash and advises re-running, which can never succeed, and the poller stops retrying until a manual index proves otherwise. - scripts/run-tests.py --fast keeps pytest temp outside the repository. Inside it, paths ran deep enough that the engine's derived database path crossed MAX_PATH and indexing failed during tests. Adds two tables to the memory database on first start. No user action required; marm-mcp-server projects auto off restores index-on-request behavior.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (6)
📜 Recent review details🧰 Additional context used📓 Path-based instructions (10)**/*📄 CodeRabbit inference engine (AGENTS.md)
Files:
marm-mcp-server/marm_mcp_server/**/*.py📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.py⚙️ CodeRabbit configuration file
Files:
marm-mcp-server/marm_mcp_server/core/{lease_lock.py,concept_worker.py,graph_index_worker.py}📄 CodeRabbit inference engine (AGENTS.md)
Files:
marm-mcp-server/marm_mcp_server/core/*worker.py📄 CodeRabbit inference engine (AGENTS.md)
Files:
marm-mcp-server/marm_mcp_server/endpoints/**/*.py📄 CodeRabbit inference engine (AGENTS.md)
Files:
marm-mcp-server/marm_mcp_server/core/{graph_supervisor.py,graph_client.py,graph_index_worker.py}📄 CodeRabbit inference engine (AGENTS.md)
Files:
marm-mcp-server/tests/**/*.py📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/tests/**⚙️ CodeRabbit configuration file
Files:
marm-mcp-server/marm_mcp_server/core/runtime_flags.py📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📓 Common learnings🔇 Additional comments (8)
📝 WalkthroughWalkthroughChangesAutomatic graph indexing
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| return { | ||
| "status": "error", | ||
| "error_code": "index_in_progress", | ||
| "message": str(busy), | ||
| } |
| return { | ||
| "status": "error", | ||
| "error_code": "index_in_progress", | ||
| "message": str(busy), | ||
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 506dbb642f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Read per cycle rather than cached in the state, so clearing the | ||
| # marker (any successful manual index does) resumes polling on the | ||
| # next cycle in every process, with no restart. | ||
| if await asyncio.to_thread(runtime_flags.is_unindexable, state.root): | ||
| continue | ||
| try: | ||
| await self._poll_one(state) |
There was a problem hiding this comment.
Recheck deletion suppressions before polling cached roots
When HTTP and STDIO are both running, deleting a project only calls drop_watch() on the current process, while the other process can retain the root until GRAPH_AUTO_INDEX_PROJECT_TTL expires. _cycle() rechecks only the unindexable marker, so a cached dirty repository is re-indexed on the next cycle despite its durable suppression, recreating the project the user deleted. Check is_watch_suppressed() here on every cycle, not only during _refresh_projects().
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
| try: | ||
| await asyncio.to_thread(release, holder) |
There was a problem hiding this comment.
Hold the lease when the owned task itself is cancelled
If the event loop shuts down while the executor thread is indexing, asyncio directly cancels _owned_call; the finally block then releases the lease even though asyncio.to_thread(fn, ...) did not stop the running thread. shield() only protects this task from cancellation propagated by its caller, not cancellation of the owned task during loop teardown, so another transport can acquire the gate and write concurrently. Tie release to actual executor completion even when this task is cancelled.
AGENTS.md reference: AGENTS.md:L17-L17
Useful? React with 👍 / 👎.
| client.call_tool( | ||
| "index_repository", {"repo_path": req.repo_path, "mode": req.mode} | ||
| ) |
There was a problem hiding this comment.
Route standalone graph mutations through the shared gate
When standalone marm_graph runs alongside marm-mcp-server, both use the same default engine store, but this raw index_repository call is still reached directly by marm_graph/endpoints/graph_ai.py:34 and marm_graph/server_stdio.py:78; standalone /ui/delete_project likewise bypasses the gate. The new poller can therefore index concurrently with those supported entrypoints, defeating the cross-process serialization and risking store corruption or deletion resurrection. Ensure the standalone mutation paths take the same lease or isolate their stores.
AGENTS.md reference: AGENTS.md:L17-L17
Useful? React with 👍 / 👎.
| if result.get("status") != "error": | ||
| if root_path: | ||
| try: | ||
| await asyncio.to_thread(runtime_flags.suppress_watch, root_path) |
There was a problem hiding this comment.
Persist the deletion suppression before releasing the gate
After a successful deletion, run_exclusive() releases the graph gate before this suppression write occurs. If another transport's poll cycle acquires the gate in that interval, it can start an opaque re-index of its cached root; writing the suppression afterward cannot stop that running call, so the deleted project reappears. Record the tombstone inside the gated delete operation after engine success but before its lease is released.
AGENTS.md reference: AGENTS.md:L17-L17
Useful? React with 👍 / 👎.
| head = _git(root, "rev-parse", "HEAD") | ||
| if head is None: | ||
| return None |
There was a problem hiding this comment.
Handle repositories with an unborn HEAD
For a valid Git repository with no commits, git rev-parse HEAD fails and this returns None. Because _poll_one() has already classified the directory as Git, it does not enter the non-Git slow lane and instead returns on every cycle, so edits in a manually indexed new repository are never refreshed. Represent an unborn HEAD with a stable sentinel or otherwise fall back to dirty/status-based polling.
AGENTS.md reference: AGENTS.md:L16-L16
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 12-23: Update the auto-index command documentation to use the
product CLI entry point: replace the affected marm-mcp-server projects auto
off/status references in CHANGELOG.md lines 12-23, including the fenced upgrade
command, and in marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md
lines 838-844 with marm-memory projects auto off/status as applicable. No other
documentation behavior needs to change.
In `@docs/FAQ.md`:
- Line 101: Update docs/FAQ.md lines 101-101 to state that the extraction
runtime loads on the first extraction, including background indexing, rather
than only during marm_concept_build. Update docs/PROTOCOL.md lines 36-37 to
state that graph startup occurs on the first graph call or when the auto-index
poller starts an already-downloaded engine.
In `@marm-mcp-server/marm_graph/core/tool_router.py`:
- Around line 227-255: The _windows_path_limit_error function currently returns
windows_path_too_long for any exit_nonzero outcome when the predicted path is
long, which misclassifies unrelated engine failures as path-length errors. Add a
check for specific Windows path-length error evidence in exc (the exception
message or hint) or payload before returning the windows_path_too_long error
dictionary. This guard ensures the error code is only returned when there is
actual evidence of a path-length failure, not just because the path happens to
be long and there was an exit_nonzero outcome.
In `@marm-mcp-server/marm_mcp_server/core/concept_worker.py`:
- Around line 54-63: Update ConceptWorker.status() to return self.enabled()
instead of the raw CONCEPT_AUTO_INDEX value, so status reflects saved runtime
overrides and the effective extraction state.
In `@marm-mcp-server/marm_mcp_server/core/graph_index_lock.py`:
- Around line 143-152: Update _owned_call so lease release waits for the
asyncio.to_thread engine operation to actually finish, even when _owned_call is
cancelled. Retain the existing heartbeat cancellation and cleanup, but keep or
track the worker future and await its completion before invoking
release(holder), ensuring run_exclusive cancellation cannot release the lease
while fn is still running.
In `@marm-mcp-server/marm_mcp_server/core/graph_index_worker.py`:
- Around line 276-283: Update the watch worker’s polling and failure-handling
flows around is_watch_suppressed(), R.do_index, mark_unindexable(), and the
manual recovery clear path so suppression checks, index-block writes/clears,
delete_project, and every code-index call occur within the same graph gate
transition. Prevent polling from recreating a project deleted by another
transport and prevent stale automatic failures from overwriting a later
successful manual recovery. Add cross-transport coverage for delete-then-poll
and automatic-failure-versus-manual-recovery races.
In `@marm-mcp-server/marm_mcp_server/core/runtime_flags.py`:
- Around line 41-55: Update runtime_flags.get so database read failures return a
distinct failure result instead of None, while preserving None for a confirmed
absent key. Adjust get_bool and suppression/tombstone callers to detect read
failures and skip the worker cycle or retain the last confirmed state, never
falling back to environment defaults or permitting indexing when runtime
controls cannot be read.
In `@marm-mcp-server/marm_mcp_server/endpoints/graph.py`:
- Around line 164-176: Update _resolve_and_delete to abort and return without
calling delete_project when _project_root_path(project) returns None. After a
successful deletion, call runtime_flags.suppress_watch(root_path) before
returning, while keeping graph_index_worker.drop_watch(root_path) outside the
graph gate as existing local worker cleanup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e4358b98-4a27-4349-a653-24d885b89ead
📒 Files selected for processing (41)
AGENTS.mdCHANGELOG.mdCONTRIBUTING.mdCONTRIBUTORS.mdREADME.mddocs/FAQ.mddocs/INSTALL-DOCKER.mddocs/INSTALL-LINUX.mddocs/INSTALL-PLATFORMS.mddocs/INSTALL-WINDOWS.mddocs/PROTOCOL.mdmarm-mcp-server/Dockerfilemarm-mcp-server/README.mdmarm-mcp-server/docker-compose.ymlmarm-mcp-server/marm_graph/core/models.pymarm-mcp-server/marm_graph/core/tool_router.pymarm-mcp-server/marm_mcp_server/__init__.pymarm-mcp-server/marm_mcp_server/cli.pymarm-mcp-server/marm_mcp_server/config/settings.pymarm-mcp-server/marm_mcp_server/core/concept_build_lock.pymarm-mcp-server/marm_mcp_server/core/concept_worker.pymarm-mcp-server/marm_mcp_server/core/graph_index_lock.pymarm-mcp-server/marm_mcp_server/core/graph_index_worker.pymarm-mcp-server/marm_mcp_server/core/lease_lock.pymarm-mcp-server/marm_mcp_server/core/memory_db.pymarm-mcp-server/marm_mcp_server/core/runtime_flags.pymarm-mcp-server/marm_mcp_server/endpoints/graph.pymarm-mcp-server/marm_mcp_server/resources/marm-docs/FAQ.mdmarm-mcp-server/marm_mcp_server/resources/marm-docs/PROTOCOL.mdmarm-mcp-server/marm_mcp_server/resources/marm-docs/README.mdmarm-mcp-server/marm_mcp_server/server.pymarm-mcp-server/marm_mcp_server/server_stdio.pymarm-mcp-server/marm_mcp_server/services/cli_parser.pymarm-mcp-server/marm_mcp_server/services/graph_auto_cli.pymarm-mcp-server/marm_mcp_server/services/runtime_status.pymarm-mcp-server/marm_mcp_server/services/stdio_graph_tools.pymarm-mcp-server/pyproject.tomlmarm-mcp-server/server.jsonmarm-mcp-server/tests/test_command_smoke.pymarm-mcp-server/tests/test_graph_auto_index.pyscripts/run-tests.py
💤 Files with no reviewable changes (1)
- docs/INSTALL-DOCKER.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
marm-mcp-server/Dockerfile
📄 CodeRabbit inference engine (AGENTS.md)
When bumping the version, update the Dockerfile version label.
Files:
marm-mcp-server/Dockerfile
marm-mcp-server/pyproject.toml
📄 CodeRabbit inference engine (AGENTS.md)
When bumping the version, update the package version in
pyproject.toml.
Files:
marm-mcp-server/pyproject.toml
**/*.{py,json}
📄 CodeRabbit inference engine (AGENTS.md)
Keep HTTP and STDIO transports in exact parity for all 14 public MCP tools; register HTTP tools through
server.pyand STDIO tools throughserver_stdio.pyand its graph registration path.
Files:
marm-mcp-server/server.jsonmarm-mcp-server/marm_mcp_server/services/graph_auto_cli.pymarm-mcp-server/marm_mcp_server/services/cli_parser.pymarm-mcp-server/marm_graph/core/models.pymarm-mcp-server/marm_mcp_server/__init__.pymarm-mcp-server/marm_mcp_server/server_stdio.pymarm-mcp-server/marm_mcp_server/services/runtime_status.pymarm-mcp-server/marm_graph/core/tool_router.pymarm-mcp-server/marm_mcp_server/core/graph_index_lock.pymarm-mcp-server/marm_mcp_server/core/memory_db.pymarm-mcp-server/marm_mcp_server/core/concept_build_lock.pymarm-mcp-server/marm_mcp_server/services/stdio_graph_tools.pymarm-mcp-server/marm_mcp_server/core/graph_index_worker.pymarm-mcp-server/marm_mcp_server/core/runtime_flags.pymarm-mcp-server/tests/test_graph_auto_index.pyscripts/run-tests.pymarm-mcp-server/tests/test_command_smoke.pymarm-mcp-server/marm_mcp_server/cli.pymarm-mcp-server/marm_mcp_server/config/settings.pymarm-mcp-server/marm_mcp_server/server.pymarm-mcp-server/marm_mcp_server/core/concept_worker.pymarm-mcp-server/marm_mcp_server/core/lease_lock.pymarm-mcp-server/marm_mcp_server/endpoints/graph.py
marm-mcp-server/server.json
📄 CodeRabbit inference engine (AGENTS.md)
marm-mcp-server/server.json: When adding or removing an MCP tool, update thetoolsarray and keep it consistent with the HTTP and STDIO tool sets.
When bumping the version, update all three version occurrences inserver.json, including the Docker identifier.
Files:
marm-mcp-server/server.json
**/*.md
⚙️ CodeRabbit configuration file
**/*.md: Only flag documentation issues that are materially wrong, misleading for installation/release behavior, or inconsistent with live MCP behavior. Skip style, phrasing, formatting, and wording preferences.
Files:
AGENTS.mddocs/INSTALL-PLATFORMS.mdCONTRIBUTORS.mdCONTRIBUTING.mddocs/PROTOCOL.mdmarm-mcp-server/marm_mcp_server/resources/marm-docs/PROTOCOL.mdCHANGELOG.mddocs/INSTALL-WINDOWS.mddocs/INSTALL-LINUX.mddocs/FAQ.mdmarm-mcp-server/marm_mcp_server/resources/marm-docs/FAQ.mdmarm-mcp-server/marm_mcp_server/resources/marm-docs/README.mdmarm-mcp-server/README.mdREADME.md
marm-mcp-server/marm_mcp_server/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
marm-mcp-server/marm_mcp_server/**/*.py: Implement endpoint logic undermarm_mcp_server/endpoints/by surface, and keep shared helpers incore/.
All memory writes must use the serialized async write queue; do not add bypass paths. A semantic-store failure inmarm_log_entrymust never fail the log write.
Keep the concept graph database and memory database isolated; never share connections or pools between them.
Graph and concept failures must never break the seven core memory tools; graph services should start lazily and operate degraded on failure.
Background concept and code indexing must never block writes, recall, or startup. Workers must re-read their runtime flag every cycle and continue running while disabled so another process can enable them.
Use the leased database-row locks incore/lease_lock.pyfor cross-process serialization, not asyncio locks. Keepconcept_build_lockandgraph_index_lockseparate; every code-index call anddelete_projectmust take the graph gate.
Do not release the graph gate based solely on cancellation of the awaiting task; release it based on engine-call completion because a cancelledasyncio.to_threadawait may leave the worker thread running.
marm_smart_recallmust keep primary memory ranking authoritative, add only bounded read-only graph context, fail open on graph errors, and trim graph details before primary results when enforcing limits.
Use one lazy-loaded, lock-serializedjinaai/jina-embeddings-v2-small-enencoder with 512 dimensions; writes must succeed when the encoder is unavailable.
Prefer the smallest change that solves the problem; avoid speculative abstractions and keep orchestration in the current owner file unless extracting at a real module boundary.
Keep comments minimal and explain only non-obvious reasons; never add comments that merely narrate the next line.
When bumping the version, update__version__and its docstring,SERVER_VERSION, and theserver.pydocstring; audit all occurren...
Files:
marm-mcp-server/marm_mcp_server/services/graph_auto_cli.pymarm-mcp-server/marm_mcp_server/services/cli_parser.pymarm-mcp-server/marm_mcp_server/__init__.pymarm-mcp-server/marm_mcp_server/server_stdio.pymarm-mcp-server/marm_mcp_server/services/runtime_status.pymarm-mcp-server/marm_mcp_server/core/graph_index_lock.pymarm-mcp-server/marm_mcp_server/core/memory_db.pymarm-mcp-server/marm_mcp_server/core/concept_build_lock.pymarm-mcp-server/marm_mcp_server/services/stdio_graph_tools.pymarm-mcp-server/marm_mcp_server/core/graph_index_worker.pymarm-mcp-server/marm_mcp_server/core/runtime_flags.pymarm-mcp-server/marm_mcp_server/cli.pymarm-mcp-server/marm_mcp_server/config/settings.pymarm-mcp-server/marm_mcp_server/server.pymarm-mcp-server/marm_mcp_server/core/concept_worker.pymarm-mcp-server/marm_mcp_server/core/lease_lock.pymarm-mcp-server/marm_mcp_server/endpoints/graph.py
**/*.py
⚙️ CodeRabbit configuration file
**/*.py: Prioritize runtime correctness, async/concurrency safety, SQLite transaction safety, auth/rate-limit behavior, release-breaking packaging issues, and MCP protocol compatibility.
Files:
marm-mcp-server/marm_mcp_server/services/graph_auto_cli.pymarm-mcp-server/marm_mcp_server/services/cli_parser.pymarm-mcp-server/marm_graph/core/models.pymarm-mcp-server/marm_mcp_server/__init__.pymarm-mcp-server/marm_mcp_server/server_stdio.pymarm-mcp-server/marm_mcp_server/services/runtime_status.pymarm-mcp-server/marm_graph/core/tool_router.pymarm-mcp-server/marm_mcp_server/core/graph_index_lock.pymarm-mcp-server/marm_mcp_server/core/memory_db.pymarm-mcp-server/marm_mcp_server/core/concept_build_lock.pymarm-mcp-server/marm_mcp_server/services/stdio_graph_tools.pymarm-mcp-server/marm_mcp_server/core/graph_index_worker.pymarm-mcp-server/marm_mcp_server/core/runtime_flags.pymarm-mcp-server/tests/test_graph_auto_index.pyscripts/run-tests.pymarm-mcp-server/tests/test_command_smoke.pymarm-mcp-server/marm_mcp_server/cli.pymarm-mcp-server/marm_mcp_server/config/settings.pymarm-mcp-server/marm_mcp_server/server.pymarm-mcp-server/marm_mcp_server/core/concept_worker.pymarm-mcp-server/marm_mcp_server/core/lease_lock.pymarm-mcp-server/marm_mcp_server/endpoints/graph.py
docs/INSTALL-*.md
📄 CodeRabbit inference engine (AGENTS.md)
When bumping the version, update version headers in all
docs/INSTALL-*.mdfiles.
Files:
docs/INSTALL-PLATFORMS.mddocs/INSTALL-WINDOWS.mddocs/INSTALL-LINUX.md
marm-mcp-server/tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
marm-mcp-server/tests/**/*.py: Test both transports for new or changed MCP tools, using real FastAPI endpoints and real SQLite; mock only when it meaningfully speeds testing and matches real behavior with at least 95% fidelity.
Every new MARM Console API route must have at least one happy-path FastAPI response-contract test with the MCP adapter stubbed.
Do not write existence-check or coded-to-pass tests; prefer deep tests that exercise real paths. Usepytest.mark.skiponly for genuinely unavailable dependencies.
Files:
marm-mcp-server/tests/test_graph_auto_index.pymarm-mcp-server/tests/test_command_smoke.py
**/tests/**
⚙️ CodeRabbit configuration file
**/tests/**: Focus on tests that are flaky, non-isolated, incorrectly asserting behavior, or missing coverage for a changed high-risk path. Skip minor naming, comments, and layout preferences.
Files:
marm-mcp-server/tests/test_graph_auto_index.pymarm-mcp-server/tests/test_command_smoke.py
{docs, marm-mcp-server/marm_mcp_server/resources/marm-docs}/@(FAQ|PROTOCOL|PROTOCOL-LITE).md
📄 CodeRabbit inference engine (AGENTS.md)
Treat
docs/FAQ.md,docs/PROTOCOL.md, anddocs/PROTOCOL-LITE.mdas source documents and keep their packaged copies identical; resync packaged copies after edits.
Files:
docs/PROTOCOL.mddocs/FAQ.md
**/README.md
📄 CodeRabbit inference engine (AGENTS.md)
**/README.md: Maintain the root README as the source of truth, the PyPI README as a separate variant, and the packagedresources/marm-docs/README.mdas a separate text-only subset.
When bumping the version, update the README h1 in all three maintained README files.
Files:
marm-mcp-server/marm_mcp_server/resources/marm-docs/README.mdmarm-mcp-server/README.mdREADME.md
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: Lyellr88/marm-memory
Timestamp: 2026-08-04T09:01:02.256Z
Learning: Never commit changes without an explicit user request.
Learnt from: CR
Repo: Lyellr88/marm-memory
Timestamp: 2026-08-04T09:01:02.256Z
Learning: Use semantic versioning: MAJOR for breaking changes, MINOR for new tools, parameters, or features, and PATCH for fixes and documentation updates.
📚 Learning: 2026-07-31T08:30:32.056Z
Learnt from: Lyellr88
Repo: Lyellr88/marm-memory PR: 125
File: docs/INSTALL-LINUX.md:323-323
Timestamp: 2026-07-31T08:30:32.056Z
Learning: In the Linux and Windows installation documentation, treat http://localhost:8001 as the primary MARM MCP Server endpoint. Do not validate its response contract against the standalone marm-mcp-server/marm_graph/server.py health route, because marm-graph is embedded in the primary marm_mcp_server service.
Applied to files:
docs/INSTALL-WINDOWS.mddocs/INSTALL-LINUX.md
🪛 ast-grep (0.45.0)
marm-mcp-server/marm_mcp_server/core/graph_index_worker.py
[error] 67-73: Command coming from incoming request
Context: subprocess.run(
["git", "-c", "core.fsmonitor=false", "-C", root, *args],
capture_output=True,
text=True,
timeout=_GIT_TIMEOUT_SECONDS,
env=_git_env(),
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
marm-mcp-server/tests/test_graph_auto_index.py
[error] 39-45: Command coming from incoming request
Context: subprocess.run(
["git", *args],
cwd=cwd,
check=True,
capture_output=True,
text=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[error] 94-100: Command coming from incoming request
Context: subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
timeout=120,
env=dict(os.environ),
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
[info] 1080-1080: use jsonify instead of json.dumps for JSON output
Context: json.dumps(found)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
[info] 55-55: Do not hardcode temporary file or directory names
Context: "/tmp/marm-pytest"
Note: [CWE-377] Insecure Temporary File.
(hardcoded-tmp-file)
🪛 GitHub Check: CodeQL
marm-mcp-server/marm_mcp_server/endpoints/graph.py
[warning] 215-219: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.
[warning] 415-419: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.
🪛 OpenGrep (1.26.0)
marm-mcp-server/marm_mcp_server/core/lease_lock.py
[ERROR] 74-76: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.
(coderabbit.sql-injection.python-fstring-execute)
[ERROR] 82-94: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.
(coderabbit.sql-injection.python-fstring-execute)
[ERROR] 126-128: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.
(coderabbit.sql-injection.python-fstring-execute)
[ERROR] 136-138: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.
(coderabbit.sql-injection.python-fstring-execute)
🔇 Additional comments (35)
marm-mcp-server/tests/test_command_smoke.py (1)
47-53: LGTM!AGENTS.md (1)
16-18: LGTM!Also applies to: 81-87
marm-mcp-server/marm_mcp_server/resources/marm-docs/PROTOCOL.md (1)
36-37: LGTM!marm-mcp-server/pyproject.toml (1)
7-7: LGTM!marm-mcp-server/server.json (1)
6-6: LGTM!Also applies to: 20-25
scripts/run-tests.py (1)
22-28: LGTM!marm-mcp-server/marm_mcp_server/resources/marm-docs/FAQ.md (1)
92-101: 📐 Maintainability & Code QualityPackaged FAQ matches the source FAQ.
No change needed.
marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md (1)
968-973: 📐 Maintainability & Code QualityNo change needed.
The documented
GRAPH_AUTO_INDEX_INTERVALminimum of 5,GRAPH_AUTO_INDEX_FULL_INTERVALminimum of 60, andGRAPH_AUTO_INDEX_LEASE_SECONDSdefault of 120 matchconfig/settings.py.marm-mcp-server/tests/test_graph_auto_index.py (1)
664-682: 📐 Maintainability & Code QualityNo change needed for this test
load_isolated_server(...)clearsmarm_mcp_server.*modules before import and restores them viamonkeypatch, so thegraph_supervisorsingleton is not carried forward across this isolated test.marm-mcp-server/marm_mcp_server/__init__.py (1)
17-20: 📐 Maintainability & Code QualityNo version metadata updates needed.
CONTRIBUTING.md (1)
56-56: LGTM!Also applies to: 67-85, 115-142, 165-166
CONTRIBUTORS.md (1)
15-16: LGTM!Also applies to: 17-20
README.md (2)
88-88: LGTM!Also applies to: 145-149, 691-691, 704-704, 937-939, 994-999, 1092-1103
866-870: 🎯 Functional CorrectnessNo change needed.
marm-mcp-serveris a supported CLI script installed from the package and points to the same main entry point asmarm-memory;marm-memory projects auto offis not required.> Likely an incorrect or invalid review comment.docs/FAQ.md (1)
92-92: LGTM!docs/INSTALL-LINUX.md (1)
2-3: LGTM!docs/INSTALL-PLATFORMS.md (1)
1-1: LGTM!docs/INSTALL-WINDOWS.md (1)
2-3: LGTM!marm-mcp-server/Dockerfile (1)
76-76: 📐 Maintainability & Code QualityAudit all 2.37.0 release metadata before publishing.
The changed Docker label, Compose image tag,
SERVER_VERSION, and health examples agree. The supplied files do not prove that__version__, its docstring, theserver.pydocstring,pyproject.toml,server.json, README headings, and installation headers use the same value. A mismatch would make the image, health output, and installed package report different releases.
marm-mcp-server/Dockerfile#L76-L76: verify the image label matches the package/runtime version.marm-mcp-server/docker-compose.yml#L8-L8: verify the image tag matches the release.marm-mcp-server/docker-compose.yml#L21-L21: verifySERVER_VERSIONmatches the release.docs/INSTALL-LINUX.md#L316-L316: verify the health example matches the runtime.docs/INSTALL-WINDOWS.md#L290-L290: verify the health example matches the runtime.As per coding guidelines, update all required version declarations and audit every occurrence with
python scripts/find-versions.py.Source: Coding guidelines
marm-mcp-server/README.md (1)
90-90: LGTM!Also applies to: 147-151, 693-693, 706-706, 939-941, 996-1001, 1094-1105
marm-mcp-server/marm_graph/core/models.py (1)
31-44: LGTM!marm-mcp-server/marm_mcp_server/config/settings.py (1)
209-209: LGTM!Also applies to: 372-427
marm-mcp-server/marm_mcp_server/core/memory_db.py (1)
416-439: LGTM!marm-mcp-server/marm_mcp_server/core/lease_lock.py (1)
1-197: LGTM!marm-mcp-server/marm_mcp_server/core/concept_build_lock.py (1)
14-29: LGTM!Also applies to: 33-50, 57-70, 96-106
marm-mcp-server/marm_mcp_server/core/concept_worker.py (1)
65-76: LGTM!Also applies to: 154-158
marm-mcp-server/marm_mcp_server/core/graph_index_lock.py (1)
1-119: LGTM!Also applies to: 153-167
marm-mcp-server/marm_graph/core/tool_router.py (1)
19-19: LGTM!Also applies to: 264-277
marm-mcp-server/marm_mcp_server/endpoints/graph.py (2)
29-31: LGTM!Also applies to: 116-136, 154-161, 196-213
214-219: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winInformation Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information
Reachability: External
Reachability path
● Entry marm-mcp-server/marm_mcp_server/cli.py │ ▼ ● Hop marm-mcp-server/marm_mcp_server/server_stdio.py:365 marm_graph_index │ ▼ ● Hop marm-mcp-server/marm_mcp_server/services/stdio_graph_tools.py:57 marm_graph_index │ ▼ ● Sink marm-mcp-server/marm_mcp_server/endpoints/graph.pyReturn a sanitized graph-busy message on both transports.
GraphIndexBusyincludes the internal holder purpose. An automatic holder purpose can contain a full repository root.
marm-mcp-server/marm_mcp_server/endpoints/graph.py#L214-L219: replacestr(busy)with a fixed HTTP-safe message.marm-mcp-server/marm_mcp_server/endpoints/graph.py#L414-L419: apply the same sanitization to project deletion.marm-mcp-server/marm_mcp_server/services/stdio_graph_tools.py#L109-L114: apply the same sanitization to STDIO responses.Log
busy.holder_purposeonly in trusted logs. Verify that no client-facing response needs this internal value.marm-mcp-server/marm_mcp_server/services/stdio_graph_tools.py (1)
21-25: LGTM!Also applies to: 61-108
marm-mcp-server/marm_mcp_server/services/cli_parser.py (1)
123-126: LGTM!Also applies to: 141-144
marm-mcp-server/marm_mcp_server/cli.py (1)
369-370: LGTM!Also applies to: 379-380, 442-448
marm-mcp-server/marm_mcp_server/services/graph_auto_cli.py (1)
1-78: LGTM!marm-mcp-server/marm_mcp_server/services/runtime_status.py (1)
201-219: LGTM!
| - Turn it off with `marm-mcp-server projects auto off`, or from an agent with `marm_graph_index(action="auto_off")`. `knowledge auto off` does the same for concept extraction. Both take effect on the next cycle with no restart, both survive one, and both work with the graph engine stopped. A saved switch beats the environment variable, so a `GRAPH_AUTO_INDEX=true` in a Dockerfile cannot silently re-enable something you turned off; `auto status` names which one won. | ||
| - Deleting a project records a durable suppression, so a poller holding a cached project list cannot recreate what you just deleted. An explicit manual index re-enrolls it. | ||
| - Pacing is `GRAPH_AUTO_INDEX_INTERVAL` (30s), `GRAPH_AUTO_INDEX_MODE` (`moderate`), `GRAPH_AUTO_INDEX_LEASE_SECONDS` (120), and `GRAPH_AUTO_INDEX_PROJECT_TTL` (300s). 30 seconds rather than the engine's own 5: a git signature measures ~103ms per project on Windows where process spawn dominates, and a code graph does not need sub-minute freshness. | ||
| - Nothing is downloaded on your behalf. Auto-indexing is on by default, but the poller stays dormant until the graph engine binary is already on disk, so a fresh install does not pull ~269MB at first boot for a user who never calls a graph tool. | ||
|
|
||
| ### Upgrade Note | ||
|
|
||
| No action required. Two tables are added to the memory database on first start. If you would rather index only on request, run: | ||
|
|
||
| ``` | ||
| marm-mcp-server projects auto off | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)cli_parser\.py$|CHANGELOG\.md$|marm-docs/README\.md$' || true
echo "== auto occurrences =="
rg -n 'auto (off|status|off)|projects auto|GRAPH_AUTO_INDEX|marm-memory|marm-mcp-server' CHANGELOG.md marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md || true
echo "== cli_parser outline/sections =="
fd -a 'cli_parser\.py' . | sed 's#^\./##'
for f in $(git ls-files | rg '(^|/)cli_parser\.py$'); do
echo "--- $f ---"
wc -l "$f"
rg -n 'marm-memory|marm-mcp-server|Projects|projects|auto|Auto|Sub|prog =|add_parser|subparsers' "$f" || true
done
echo "== surrounding README relevant lines =="
sed -n '1,160p' marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md | nl -ba
sed -n '820,860p' marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md | nl -ba
sed -n '110,135p' marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md | nl -baRepository: Lyellr88/marm-memory
Length of output: 37232
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== search command references in changed and likely docs/code =="
rg -n 'marm-(memory|mcp-server) .*auto|marm-(memory|mcp-server).*projects auto|projects auto off|projects auto status' . -g '!node_modules' -g '!dist' -g '!build' | head -200
echo "== inspect likely entry files =="
fd -a '.*(marm.*mcp.*server|cli_parser|__main__|marm.*mcp).*' . | sed 's#^\./##' | sort | head -200Repository: Lyellr88/marm-memory
Length of output: 6839
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== cli_parser entry selection and project-auto dispatch =="
sed -n '1,190p' marm-mcp-server/marm_mcp_server/services/cli_parser.py
echo "== dispatch handlers / project auto implementation =="
rg -n 'projects_command|auto|auto_off|auto_status|project.*auto|graph.*auto' marm-mcp-server/marm_mcp_server/services -g '*.py' || true
for f in $(git ls-files 'marm-mcp-server/marm_mcp_server/services/*.py'); do
if rg -q 'def .*auto|auto_off|auto_status|projects_command' "$f"; then
echo "--- $f ---"
rg -n -C 8 'def .*auto|auto_off|auto_status|projects_command' "$f" || true
fi
done
echo "== CLI compatibility references =="
rg -n 'marm-memory|MarmMemory|MARM_CLI|subparsers|prog=|add_parser' marm-mcp-server/marm_mcp_server -g '*.py' | head -200Repository: Lyellr88/marm-memory
Length of output: 32495
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== cli.py command dispatch =="
sed -n '460,540p' marm-mcp-server/marm_mcp_server/cli.py
echo "== projects_cli/auto dispatch =="
sed -n '1,90p' marm-mcp-server/marm_mcp_server/services/projects_cli.py
sed -n '45,90p' marm-mcp-server/marm_mcp_server/services/graph_auto_cli.py
echo "== read-only parser behavior probe =="
python3 - <<'PY'
import argparse
parser = argparse.ArgumentParser(prog="marm-memory", add_help=False)
parser.add_argument("-h", "--help", action="help")
parser.add_argument("-V", "--version", action="version", version="0.0.0")
subparsers = parser.add_subparsers(dest="command", required=True, parser_class=argparse.ArgumentParser)
knowledge = subparsers.add_parser("knowledge")
knowledge_sub = knowledge.add_subparsers(dest="knowledge_command", required=True)
knowledge_sub.add_parser("status")
projects = subparsers.add_parser("projects")
projects_sub = projects.add_subparsers(dest="projects_command", required=True)
projects_auto = projects_sub.add_parser("auto", help="Turn automatic code re-indexing on or off")
projects_auto.add_argument("state", choices=("on", "off", "status"))
samples = [
"marm-memory projects auto off",
"marm-memory projects auto status",
"marm-memory knowledge auto off",
"marm-memory knowledge auto status",
]
for sample in samples:
parsed = parser.parse_args(sample.split())
print({"input": sample, "parsed": parsed, "command": parsed.command})
PYRepository: Lyellr88/marm-memory
Length of output: 6999
Switch the auto-index command docs to the product CLI entry point.
projects auto off/status are registered on marm-memory projects auto ..., so the marm-mcp-server projects auto off commands are misleading release docs. Update CHANGELOG.md#L12-L23 and the fenced upgrade command, and update README.md#L838-L844 to marm-memory projects auto off.
📍 Affects 2 files
CHANGELOG.md#L12-L23(this comment)marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md#L838-L844
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CHANGELOG.md` around lines 12 - 23, Update the auto-index command
documentation to use the product CLI entry point: replace the affected
marm-mcp-server projects auto off/status references in CHANGELOG.md lines 12-23,
including the fenced upgrade command, and in
marm-mcp-server/marm_mcp_server/resources/marm-docs/README.md lines 838-844 with
marm-memory projects auto off/status as applicable. No other documentation
behavior needs to change.
| def _windows_path_limit_error(repo_path: str, exc: CbmToolError) -> Optional[dict]: | ||
| """Recognize a Win32 path-length failure behind the engine's generic message. | ||
|
|
||
| The engine reports this as a contained per-file worker crash and advises | ||
| re-running, which can never succeed: nothing about the path changes between | ||
| attempts. Users follow that hint into hunting for a corrupt source file that | ||
| does not exist. | ||
| """ | ||
| if os.name != "nt": | ||
| return None | ||
| payload = exc.payload if isinstance(exc.payload, dict) else {} | ||
| if payload.get("outcome") != "exit_nonzero": | ||
| return None | ||
| predicted = _predicted_store_path_length(repo_path) | ||
| if predicted < _WINDOWS_PATH_LIMIT - _WINDOWS_PATH_MARGIN: | ||
| return None | ||
| return { | ||
| "status": "error", | ||
| "error_code": "windows_path_too_long", | ||
| "message": str(exc), | ||
| "hint": ( | ||
| f"The repository path is {len(repo_path)} characters long, which makes " | ||
| f"the graph engine's database path about {predicted} characters against " | ||
| f"Windows' {_WINDOWS_PATH_LIMIT}-character limit, so its indexing worker " | ||
| "cannot open it. Re-running will not help. Index the repository from a " | ||
| "shallower path, or enable Win32 long paths." | ||
| ), | ||
| "payload": payload, | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Require evidence of a path-length failure before returning windows_path_too_long.
The current condition accepts every exit_nonzero failure when the predicted path is long. An unrelated engine failure on that repository is therefore classified as a path-length failure.
GraphIndexWorker._reindex treats this error code as terminal. It marks the root unindexable and stops automatic retries. Match a specific Windows path error from exc, exc.hint, or payload before returning this error code.
Proposed guard
payload = exc.payload if isinstance(exc.payload, dict) else {}
if payload.get("outcome") != "exit_nonzero":
return None
+ diagnostic = " ".join(
+ str(value)
+ for value in (exc, exc.hint, payload)
+ if value is not None
+ ).lower()
+ if not any(
+ marker in diagnostic
+ for marker in ("winerror 206", "filename or extension is too long")
+ ):
+ return None
predicted = _predicted_store_path_length(repo_path)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _windows_path_limit_error(repo_path: str, exc: CbmToolError) -> Optional[dict]: | |
| """Recognize a Win32 path-length failure behind the engine's generic message. | |
| The engine reports this as a contained per-file worker crash and advises | |
| re-running, which can never succeed: nothing about the path changes between | |
| attempts. Users follow that hint into hunting for a corrupt source file that | |
| does not exist. | |
| """ | |
| if os.name != "nt": | |
| return None | |
| payload = exc.payload if isinstance(exc.payload, dict) else {} | |
| if payload.get("outcome") != "exit_nonzero": | |
| return None | |
| predicted = _predicted_store_path_length(repo_path) | |
| if predicted < _WINDOWS_PATH_LIMIT - _WINDOWS_PATH_MARGIN: | |
| return None | |
| return { | |
| "status": "error", | |
| "error_code": "windows_path_too_long", | |
| "message": str(exc), | |
| "hint": ( | |
| f"The repository path is {len(repo_path)} characters long, which makes " | |
| f"the graph engine's database path about {predicted} characters against " | |
| f"Windows' {_WINDOWS_PATH_LIMIT}-character limit, so its indexing worker " | |
| "cannot open it. Re-running will not help. Index the repository from a " | |
| "shallower path, or enable Win32 long paths." | |
| ), | |
| "payload": payload, | |
| } | |
| def _windows_path_limit_error(repo_path: str, exc: CbmToolError) -> Optional[dict]: | |
| """Recognize a Win32 path-length failure behind the engine's generic message. | |
| The engine reports this as a contained per-file worker crash and advises | |
| re-running, which can never succeed: nothing about the path changes between | |
| attempts. Users follow that hint into hunting for a corrupt source file that | |
| does not exist. | |
| """ | |
| if os.name != "nt": | |
| return None | |
| payload = exc.payload if isinstance(exc.payload, dict) else {} | |
| if payload.get("outcome") != "exit_nonzero": | |
| return None | |
| diagnostic = " ".join( | |
| str(value) | |
| for value in (exc, exc.hint, payload) | |
| if value is not None | |
| ).lower() | |
| if not any( | |
| marker in diagnostic | |
| for marker in ("winerror 206", "filename or extension is too long") | |
| ): | |
| return None | |
| predicted = _predicted_store_path_length(repo_path) | |
| if predicted < _WINDOWS_PATH_LIMIT - _WINDOWS_PATH_MARGIN: | |
| return None | |
| return { | |
| "status": "error", | |
| "error_code": "windows_path_too_long", | |
| "message": str(exc), | |
| "hint": ( | |
| f"The repository path is {len(repo_path)} characters long, which makes " | |
| f"the graph engine's database path about {predicted} characters against " | |
| f"Windows' {_WINDOWS_PATH_LIMIT}-character limit, so its indexing worker " | |
| "cannot open it. Re-running will not help. Index the repository from a " | |
| "shallower path, or enable Win32 long paths." | |
| ), | |
| "payload": payload, | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@marm-mcp-server/marm_graph/core/tool_router.py` around lines 227 - 255, The
_windows_path_limit_error function currently returns windows_path_too_long for
any exit_nonzero outcome when the predicted path is long, which misclassifies
unrelated engine failures as path-length errors. Add a check for specific
Windows path-length error evidence in exc (the exception message or hint) or
payload before returning the windows_path_too_long error dictionary. This guard
ensures the error code is only returned when there is actual evidence of a
path-length failure, not just because the path happens to be long and there was
an exit_nonzero outcome.
| def _resolve_and_delete(project: str) -> tuple[str | None, dict]: | ||
| """Resolve the project's root, then delete it. Runs under the index gate. | ||
|
|
||
| The root has to be read before the delete, because afterwards the project is | ||
| gone and its root path with it, and without the path there is nothing to | ||
| suppress: the poller would re-index the root from its cached watch set and | ||
| recreate what the user just deleted. | ||
| """ | ||
| root_path = _project_root_path(project) | ||
| result = graph_supervisor.get_client().call_tool( | ||
| "delete_project", {"project": project} | ||
| ) | ||
| return root_path, result |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Persist deletion suppression before releasing the graph gate.
_resolve_and_delete continues with delete_project when _project_root_path returns None. The deletion then succeeds without durable suppression.
The successful path also releases the graph gate before Line 426 persists suppression. Another process can acquire the gate in this interval and start re-indexing the cached root.
If root resolution fails, abort the deletion. After a successful deletion, call runtime_flags.suppress_watch(root_path) inside _resolve_and_delete before it returns. Keep graph_index_worker.drop_watch(root_path) outside the gate because it only changes local worker state.
Also applies to: 411-429
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@marm-mcp-server/marm_mcp_server/endpoints/graph.py` around lines 164 - 176,
Update _resolve_and_delete to abort and return without calling delete_project
when _project_root_path(project) returns None. After a successful deletion, call
runtime_flags.suppress_watch(root_path) before returning, while keeping
graph_index_worker.drop_watch(root_path) outside the graph gate as existing
local worker cleanup.
Every durable decision the poller depends on was settled outside the gate that exists to order it, so HTTP and STDIO could each undo the other's. - Hold the lease until the engine call returns, not until its caller stops waiting. Acquire, call and release now happen in one worker thread, so a cancellation delivered to the owning task, which is what loop teardown does to every pending task, can no longer hand the gate to the other transport mid-write. - Settle the tombstone and the path-limit marker inside the gate, via one index_repository callable shared by all four index paths. An automatic failure and a manual success could previously release in either order and the loser's write won, leaving a recovered repo marked unindexable in both processes. The deletion tombstone moves inside the gate for the same reason. - Check the deletion tombstone every cycle rather than only when the watch cache reloads, which left a 5 minute window in which the other transport's poller re-indexed a deleted project and recreated it. - Fail closed when the flag database cannot be read. An unreadable row was indistinguishable from an unset one, so a locked database resolved every switch to its environment default: a saved "off" authorized indexing and a tombstone stopped protecting its project. - Poll a repository with no commits. rev-parse fails on an unborn HEAD, and treating that as a git error meant such a repo was never refreshed again. - Report the effective auto-index flag in concept status instead of the environment variable, and keep the repository path out of the gate-busy message that reaches API responses (CodeQL). Two existing tests stubbed run_exclusive, which now also stubs out the settling; they stub the engine boundary instead and exercise the real path. 7 tests added, each verified to fail against the previous behavior. 1078 passed, 2 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v2.37.0: Automatic Code Graph Indexing
Once a repository is indexed, MARM keeps it current on its own. A code graph was previously only as fresh as the last manual
marm_graph_indexcall, and a stale graph does not fail loudly: it answers confidently from deleted code. This repository's own index was four days and a full release behind when the work started, and nothing surfaced that.git statusreports which files changed and not what is in them: repeated edits to one already-modified file produce byte-identical output, so no cheaper fingerprint can see them.marm-mcp-server projects auto off, or from an agent withmarm_graph_index(action="auto_off").knowledge auto offdoes the same for concept extraction. Both take effect on the next cycle with no restart, survive one, and work with the graph engine stopped. A saved switch beats theGRAPH_AUTO_INDEXenvironment variable, so a value baked into a Dockerfile cannot silently re-enable what an operator turned off;auto statusreports which source won.delete_project. HTTP and STDIO are separate processes with separate engine children over one shared store, so the previous in-process lock spanned nothing. A delete arriving during an index is refused withindex_in_progressrather than being silently undone when that index completes and writes the project back. The lease is released when the engine call returns rather than when its caller stops waiting, because cancelling anasyncio.to_threadawait leaves the thread running and writing.MAX_PATHoverflow is now reported as one. The engine reports it as contained per-file worker crash and advises re-running, which can never succeed because nothing about the path changes between attempts, and the poller stops retrying that root until a manual index proves it indexable again.Measured
Against the pinned
codebase-memory-mcp0.9.0 binary on Windows, this repository as the corpus (3,836 nodes / 20,410 edges,mode=moderate), median of three runs through one persistent stdio session:rev-parse HEAD+ dirty check)index_repository, nothing changedlist_projectsindex_repository, four days of commits behindThe 30-second interval follows from the first row rather than from the engine's own 5-second base: process spawn dominates a git call on Windows, and a code graph does not need sub-minute freshness.
list_projectsis cached for the same reason.Validation
1,071 tests pass. Concurrency is covered by tests that spawn a real second interpreter against one memory database, since a single-process suite never exercises the boundary the lease exists for. The end-to-end tests run the real engine binary: index a repo, commit, poll once, and assert the new symbol is in the graph, which is the exact case the engine's own
detect_changesreports as clean.Notes for review
scripts/run-tests.pyis in this diff.--fastwas pointing pytest's temp directory inside the repository, which put test repos deep enough that the engine's derived database path crossedMAX_PATHand indexing failed during tests. It now uses a short path outside the repository.docs/current/graph-auto-index.md, which is gitignored, so it is not in this diff. It carries the measurements above and the reasoning for rejectingdetect_changesand a.marmignore.core/lease_lock.pyand parameterized by table so the code index could reuse them. Its public API, table, and log event names are unchanged, and its existing test suite passes unmodified.Summary by CodeRabbit
New Features
Bug Fixes
Documentation