Conversation
Adds first-class Cursor IDE integration alongside the existing Claude
Code and Codex hook flows, so Cursor users get the same automatic
diary saves, pre-compaction transcript capture, and session-start
memory recall — without changing any default behaviour for existing
users.
What's included
---------------
Cursor hook scripts (hooks/cursor/):
- mempal_save_hook_cursor.sh — Stop event, counter +
loop_count guard, pending-save marker consumption, background
mempalace mine, followup_message emission.
- mempal_precompact_hook_cursor.sh — synchronous mine before
compaction, drops a pending_save marker, returns user_message.
- mempal_wake_hook_cursor.sh — sessionStart event,
wing-scoped recall guidance via additional_context.
- lib/common.sh — shared parsing + state helpers
(bash 3.2 safe, no heredoc-in-subshell traps).
- install.sh — idempotent installer with
--scope, --variant, --dry-run, --uninstall. Recognises existing
entries by basename so re-installs across paths work.
- STDIN_SHAPE.md, README.md — payload schemas + quick
reference.
Cursor plugin (.cursor-plugin/ + repo-root components):
- plugin.json, marketplace.json, README.md.
- skills/mempalace/SKILL.md — model-invocable skill mirroring the
Claude plugin's skill surface.
- commands/mempalace-{help,init,mine,search,status}.md — slash
commands for marketplace-published installs (filename = slug).
- mcp.json — auto-registers the mempalace MCP
server, wrapped under the documented mcpServers key.
Examples + docs:
- examples/cursor/hooks.json, hooks.minimal.json + README.
- website/guide/cursor-hooks.md + sidebar entry.
- README.md and CHANGELOG.md updates.
Tests (129 new, all green):
- tests/test_cursor_hooks_shell.py — 75 behavioural tests for
the three hook scripts: kill switches, input parsing, counter
logic, loop prevention, pending markers, wing inference, logging.
- tests/test_cursor_hooks_install.py — 19 contract tests for the
installer: dry-run, idempotent merge, basename-matched uninstall,
refusal to overwrite malformed JSON.
- tests/test_cursor_plugin_manifest.py — 35 contract tests for the
plugin: manifest validity, version sync with mempalace.version,
mcp.json shape, skill/command frontmatter, default-discovery
layout invariants.
Design notes
------------
- Local-first and zero-API by default; hooks never call external
services. Same privacy model as the existing Claude Code hooks.
- Fail-open: hook scripts deliberately do not use set -e so a broken
hook can never block the user's conversation.
- Cursor preCompact cannot block + return a followup, so we
synchronously mine the transcript and drop a pending_save marker
that the next stop hook consumes — guarantees verbatim capture
before context window compression.
- Cursor's default plugin discovery requires real commands/, skills/,
and mcp.json at the plugin root (verified against the cached
cloudflare plugin); .cursor-plugin/{commands,skills} are convenience
symlinks back to those canonical locations.
- bash 3.2 compatibility throughout: avoids heredoc-in-command-
substitution parser bugs; uses python -c for JSON parsing;
basename-matched entry recognition in install.sh.
- All changes are additive. No existing files are removed, no
existing hooks change behaviour, and no new runtime dependencies
are introduced.
Co-authored-by: Cursor <cursoragent@cursor.com>
Five fixes from the Gemini Code Assist review on #1632 — three real bugs, two cleanups, all consistent with the bash-3.2-compatibility contract documented in the original commit. Bug fixes (high) ---------------- 1. hooks/cursor/lib/common.sh — config.json kill-switch check used a `python3 - <<'PYEOF' ... PYEOF` heredoc inside a `$(...)` command substitution. The heredoc body contains parens which trips the macOS bash 3.2.57 parser bug. Replaced with a `python -c '...'` call passing the config path as argv[1]. Matches the pattern already used in mempal_parse_stdin in the same file. 2. hooks/cursor/install.sh — a relative `--install-dir` was written verbatim into hooks.json. Cursor invokes hook commands from its own working directory (typically the project root), so a relative command path would silently fail to launch the hook. Now resolved to an absolute path against `$PWD` before being baked in. 3. hooks/cursor/mempal_save_hook_cursor.sh — `MEMPAL_SAVE_INTERVAL=0` would crash bash on `$((NEXT % 0))` (division by zero). Extended the existing sanitiser case to coerce 0 to the default interval alongside empty / non-numeric values. Cleanups (medium) ----------------- 4. hooks/cursor/install.sh — the EMPTY_CHECK_PY temp file is now inlined as `python -c '...'`. Removes a small leak window (tmpfile would linger if the script were interrupted between mktemp and rm -f) and shortens the script. 5. hooks/cursor/install.sh — `mktemp -t prefix` has subtly different semantics on BSD (macOS) vs GNU mktemp. Switched to the portable absolute-template form `mktemp "${TMPDIR:-/tmp}/...XXXXXX"` which behaves identically on both. Regression tests ---------------- - tests/test_cursor_hooks_shell.py test_save_interval_zero_is_coerced_to_default — guards fix #3. - tests/test_cursor_hooks_install.py — new TestInstallDirAbsolutePath class: test_relative_install_dir_is_absolutized_in_hooks_json — guards fix #2 against regression. test_absolute_install_dir_is_preserved_verbatim — guards that the relative-to-absolute resolution does not mangle paths that were already absolute. Verification ------------ - bash -n on all three edited scripts: clean. - uv run pytest tests/test_cursor_hooks_*.py tests/test_cursor_plugin_manifest.py: 132 passed (was 129; +3 regression tests). - uv run pytest tests/ --ignore=tests/benchmarks: 2399 passed, 3 skipped (pre-existing). - uv run ruff check . / ruff format --check .: clean. Co-authored-by: Cursor <cursoragent@cursor.com>
…ests) Adds first-class integration with Google's Antigravity IDE (https://antigravity.google/) as a third sibling to the existing Claude Code and Codex hook integrations. Strictly additive — no existing files in main are restructured. What ships ---------- * `.antigravity-plugin/` — verified-minimal plugin package: * `plugin.json` with `{"name": "mempalace"}` (no fabricated fields) * `mcp_config.json` registering the `mempalace-mcp` stdio server * `hooks.json.tmpl` templated with `__PLUGIN_DIR__` substitution * `skills/mempalace/SKILL.md` (real file — no symlinks) * `hooks/antigravity/`: * `lib/common.sh` — shared bash 3.2.57-compatible helpers with sentinel-guarded camelCase JSON parser, antigravity_*-namespaced state files, every existing kill switch, `MEMPAL_SAVE_INTERVAL >= 1` floor (no /0), and fail-open emitters * `mempal_save_hook_antigravity.sh` — Stop event handler: increments per-conversation counter, defers when fullyIdle=False or terminationReason=error, validates transcriptPath against `..` traversal, spawns `mempalace mine --mode convos` in a detached subprocess with a per-conversation pending marker, ALWAYS emits `{}` (never `{"decision":"continue"}` — that would force an infinite agent loop) * `mempal_wake_hook_antigravity.sh` — PreInvocation handler gated to invocationNum==1 with an atomic mkdir loop guard, runs `mempalace wake-up` with a 500ms hard timeout, emits verbatim output as `{"injectSteps":[{"ephemeralMessage":"..."}]}` or `{}` on any failure * `install.sh` — idempotent installer with cmp-gated copies, `__PLUGIN_DIR__` substitution, relative path absolutization, `--dry-run`, and basename-guarded `--uninstall` (refuses to wipe a directory whose basename isn't `mempalace`) * `INVESTIGATION.md` — verbatim quotes + URLs + dates from the five official Antigravity doc pages, recording every surface shipped and every surface deliberately omitted (PreCompact equivalent, slash-commands, rules/, plugin permissions field — the latter is third-party fabrication) * `STDIN_SHAPE.md` — exact stdin/stdout contract per event with worked examples * `README.md` — local hook docs + troubleshooting * `examples/antigravity/{hooks.json,mcp_config.json,README.md}` — standalone configs for users who don't want the full installer * `website/guide/antigravity.md` + sidebar entry — VitePress guide * Updates to `README.md`, `CHANGELOG.md` (Unreleased), `hooks/README.md` Tests (56 new, all passing) --------------------------- * `tests/test_antigravity_plugin_manifest.py` (11 tests) — schema contract on the in-repo `.antigravity-plugin/` directory, including guards against re-introducing the fabricated `permissions` field and against any symlink leak. * `tests/test_antigravity_hooks_shell.py` (31 tests) — invokes the bash hooks via subprocess with synthetic camelCase stdin, asserts `{}` on every failure path, kill-switch coverage (env vars + config.json + palace nuke), divide-by-zero floor, transcript traversal rejection, namespacing, wing inference, and the hard refusal to ever emit `decision=continue` from the Stop hook. * `tests/test_antigravity_hooks_install.py` (14 tests) — `--dry-run` side-effect-free, real install layout, executable bits preserved, byte-identical idempotent re-runs (md5 + filecmp), basename-match uninstall safety, refusal when plugin.json is missing or names a different plugin, relative path absolutization. Skipped on Windows. Verification ------------ * `uv run pytest tests/ --ignore=tests/benchmarks -v` → 2314 passed, 3 skipped (Windows), 1 unrelated warning * `uv run ruff check .` → all checks passed * `uv run ruff format --check .` → 139 files already formatted * `bash -n` clean on common.sh, both hook scripts, install.sh * Local install at `~/.gemini/config/plugins/mempalace/` verified end- to-end: layout correct, paths absolutized in hooks.json, both hooks fire with realistic camelCase JSON in <1s, wing inference picks `wing_mempalace` from workspacePaths[0], state files all `antigravity_*`-namespaced, second `install.sh` run produces byte-identical output (md5 snapshots match), uninstall removes only the mempalace plugin and leaves all 6 sibling Google plugins untouched. Constraints honoured -------------------- bash 3.2.57 (no mapfile / readarray / declare -A / `${var^^}`), verbatim guarantee on all wake injections, hooks <500ms / startup injection <100ms target (kill-switch path returns in <1.5s in CI), zero new runtime dependencies, no telemetry, no external API, strictly additive (existing Claude/Codex hooks unchanged). Refs: hooks/antigravity/INVESTIGATION.md for the full audit.
Five fixes for issues called out by the gemini-code-assist[bot] review.
Each gets a regression test that locks in the correction.
1. CRITICAL: marker-cleanup watcher used POSIX `wait` on a sibling pid
(save hook). bash `wait` only works on direct children of the
calling shell — the `( wait $MINE_PID ... ) &` subshell runs as a
sibling of MINE_PID, so wait fails immediately and the pending
marker is deleted within milliseconds, defeating the concurrency
guard. Replace with `while kill -0 $MINE_PID; do sleep 1; done`,
which queries pid existence regardless of parent-child relationship.
Test: test_save_hook_marker_watcher_uses_kill_polling.
2. Bare `mempalace` console-script invocation in the save hook fails
when the venv's bin/ is not on the hook's PATH (e.g. uv tool
install in some configurations, manually managed virtualenvs).
Switch to `"$MEMPAL_PYTHON_BIN" -m mempalace mine ...` so the
resolved interpreter runs the package directly via
mempalace/__main__.py. Tests:
test_save_hook_uses_python_module_invocation,
test_save_hook_missing_mempalace_python_module_does_not_crash.
3. Same issue in the wake hook's inner Python helper. Switch
`['mempalace', 'wake-up', ...]` to `[sys.executable, '-m',
'mempalace', 'wake-up', ...]` — sys.executable is the same
interpreter that resolved MEMPAL_PYTHON in lib/common.sh.
Test: test_wake_hook_uses_sys_executable_module_invocation.
4. The Python parser in lib/common.sh wrapped `json.load` in
`try/except` and silently fell back to `data = {}`. The script
then printed the `__MEMPAL_PARSE_OK__` sentinel even on parse
failure, so the bash sentinel-check on the caller side
(`[ "$_marker" != "__MEMPAL_PARSE_OK__" ]`) never triggered the
defense-in-depth `input parse failed` branch. Remove the
try/except so the exception propagates, Python exits non-zero,
and the sentinel is omitted on bad JSON. The traceback still
lands in antigravity_last_python_err.log for debugging.
Test: test_common_sh_parser_omits_sentinel_on_malformed_json.
5. `mempal_save_interval()` failed to strip leading zeros from
MEMPAL_SAVE_INTERVAL. Values like "08" or "09" then crashed the
modulo step `$((COUNT % INTERVAL))` because bash arithmetic
parses tokens starting with `0` as octal, and 8/9 are not valid
octal digits ("value too great for base"). Strip leading zeros
while preserving the literal "0" (which is then floored to 15).
Test: test_save_hook_handles_leading_zero_save_interval (4 cases).
Plus one cosmetic fix in install.sh: removed a no-op `(cd "$OLDPWD"
2>/dev/null || cd .) >/dev/null 2>&1` line in mempal_absolutize().
The subshell cd doesn't affect the parent shell, and the installer
never cd's in the main shell anyway, so $PWD is already correct.
Verification:
* 9 new regression tests, all 65 antigravity tests pass
* full repo: 2323 passed (was 2314), 3 skipped, 1 unrelated warning
* ruff check + ruff format --check both clean across 139 files
* bash -n clean on all four shell files
* clean reinstall to ~/.gemini/config/plugins/mempalace/ succeeds
* idempotent re-run produces zero file writes (cmp-gated)
* both hooks return {} exit 0 with synthetic camelCase stdin
Co-authored-by: Cursor <cursoragent@cursor.com>
…tate-file GC Addresses igorls' review on PR #1633 (antigravity branch only): - Atomic counter write: add mempal_write_counter_atomic (same-dir temp + mv -f rename) and use it in the save hook, replacing the truncate-then -write printf that the comment falsely called "atomic". Concurrent readers now always see a complete value. - Background the expensive probe: `mempalace --version` pays the full chromadb/onnx cold-start import (the mine subparser imports mempalace.miner before argparse handles --version), so running it in the foreground blew the <500ms save budget. Probe + mine + pending -marker cleanup now run in one detached subshell; the foreground returns immediately. This also retires the kill -0 watcher (the prior gemini fix) since cleanup is now sequential within the mine's own shell, removing the sibling-PID hazard entirely. - State-file GC: add mempal_state_ttl_days (default 30, env MEMPAL_STATE_TTL_DAYS) and mempal_gc_stale_state, a daily-throttled sweep (antigravity_last_sweep marker) that removes stale antigravity_save_count_*, antigravity_pending_*, and antigravity_woke_* artifacts. Called after the kill-switch check so a disabled hook touches nothing; specific name globs leave shared logs untouched. Tests: atomic-counter behavior + no-temp-leftover, single-subshell structure (no wait/kill -0/MINE_PID), backgrounded-probe timing (3s stub returns in <2s), async log polling for the missing-module path, GC sweep/throttle/TTL-validation, and GC gated by the kill switch. Also hardens the wake-missing test to pin MEMPAL_PYTHON so a shell -exported interpreter can't defeat the simulation. bash 3.2.57 safe, fail-open on every path, {}-only Stop output. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolves the maintainer review on the Cursor IDE support PR. Cursor-only
scope; cross-IDE items (wing-naming convention, shared-file merge order)
are coordinated on the separate Antigravity branch.
followup_message default (the one "decide before merge" item):
- Keep the stop-hook followup ON by default. Cursor's transcript format
is undocumented and mempalace/normalize.py has no Cursor parser, so the
background `mempalace mine --mode convos` is best-effort only and does
not yet yield clean verbatim drawers. The followup is therefore the
load-bearing verbatim-capture path; defaulting it off would leave a
default Cursor install capturing nothing.
- Add an opt-out (MEMPAL_CURSOR_SILENT=1, or MEMPAL_VERBOSE=false) for
users who want the Claude-style "zero tokens in chat" behaviour. The
hook still mines and keeps its counters/markers when silenced.
- Correct the misleading "background mine captures it" comments in the
save and precompact hooks; update hooks/cursor/README.md and the guide.
Hygiene fixes:
- Drop the hardcoded "version" field from .cursor-plugin/plugin.json and
marketplace.json (mempalace/version.py is the single source of truth);
tests now assert the field stays absent.
- Remove the committed .cursor-plugin/{commands,skills} symlinks (they
break on Windows clones with core.symlinks=false and were redundant
with the real repo-root components that `source: "."` already serves);
add a guard test that no symlinks exist under .cursor-plugin/.
- Document the preCompact synchronous-mine timeout tradeoff and that an
incremental/append-only mine is recoverable if killed (no corruption).
- Add a Cursor-namespaced, daily-throttled TTL sweep (MEMPAL_STATE_TTL_DAYS,
default 30) to lib/common.sh that GCs stale cursor_*.count/.pending only,
after the kill-switch check; shared logs and antigravity_* are untouched.
Verification: full suite green (2424 passed, 3 skipped), ruff check +
format clean, bash -n clean on all cursor scripts. +30 Cursor tests
(followup opt-out, state GC, TTL validation, no-symlink/version guards).
Co-authored-by: Cursor <cursoragent@cursor.com>
`uv tool install mempalace` / `pipx install` place the mempalace console scripts in an isolated environment whose interpreter is not the system python3. mempal_resolve_python previously resolved `command -v python3`, landing on a Python that cannot import mempalace: the `-m mempalace --version` probe failed and mining silently never fired (hit by a real user on PR #1633). Resolution now derives the interpreter from the mempalace-mcp / mempalace console-script shebang on PATH (the same script the MCP server launches) before falling back to python3. It is pure shebang parsing + stat — no Python subprocess at source time — so the hook performance budget is preserved. An env-style `#!/usr/bin/env python` shebang and a non-executable interpreter are both rejected and fall through. MEMPAL_PYTHON remains the explicit override. Adds 6 resolver regression tests, documents resolution + MEMPAL_PYTHON in the guide and hooks README (fixing the stale `command -v mempalace` note), and a CHANGELOG entry. Co-authored-by: Cursor <cursoragent@cursor.com>
mempalace migrate (.pre-migrate.* full-palace copies) and mempalace repair max-seq-id (chroma.sqlite3.max-seq-id-backup-* DB copies) each wrote a fresh, full-size, timestamped backup every run and never deleted the old ones. On a machine that mines or repairs on a schedule, those copies could silently accumulate until they filled the disk. Add a configurable max_backups setting (default 10; env MEMPALACE_MAX_BACKUPS or config.json) and a shared prune_backups helper that trims the oldest copies after each new backup is written. Pruning is keyed by filesystem mtime, scoped strictly to each backup's own naming pattern so live data is never touched, and best-effort so a deletion failure can never abort the migrate/repair that just succeeded. Set max_backups to 0 to keep every backup.
Expose mining as an MCP tool so clients that cannot shell out (Claude Desktop, LM Studio, Aionui, Desktop Commander) can index projects, conversations, or documents in-conversation, not only through the `mempalace mine` CLI. tool_mine is a synchronous wrapper over the existing miners (miner.mine, convo_miner.mine_convos, format_miner.mine_formats) that cmd_mine already calls, so it adds no new ingestion logic and no backend coupling. Miner stdout is captured at the Python and file-descriptor level so it cannot corrupt the JSON-RPC channel (#225); no Unix-only calls, so it works on Windows. The miners keep the palace write lock, so a concurrent mine returns a structured already-running error.
The mempalace_diary_write tool declared a top-level anyOf in its input schema to require either entry or content. Anthropic's Messages API rejects any tool schema with a top-level anyOf/oneOf/allOf and returns a 400 for the entire tools array, so every MCP session failed to start. The entry/content constraint is already enforced at dispatch: content is remapped to entry before the handler runs, and a missing value returns -32602. Removing the combinator restores compatibility without weakening validation. Closes #1711
…uarantining (#1710) _missing_dimensionality_appears_recoverable required total_elements_added == len(id_to_label) before keeping a dim-None segment, but total_elements_added is a monotonic add-counter while id_to_label holds only the live elements. After any delete the counts diverge, so every post-deletion segment was quarantined; once the WAL is pruned that destroys the only copy of those vectors. Relax the check to >= and keep the bijection and size checks as the integrity backstop.
…§10, #1726) The searcher hard-coded `max(0, 1 - distance)` for vector similarity, which is correct only for cosine. A backend reporting L2 or inner-product distances — or a legacy Chroma palace built without `hnsw:space=cosine` — was silently mis-ranked: L2 distances routinely exceed 1.0 and floored every result to 0. - backends/base.py: `BaseBackend.distance_metric` ClassVar (cosine default) and a `BaseCollection.distance_metric` property per RFC 001 §2.1. - backends/chroma.py: `ChromaCollection.distance_metric` reads the real `hnsw:space`, so a legacy-L2 palace reports "l2" and ranks correctly. - searcher.py: `_distance_to_similarity(distance, metric)` (cosine/l2/ip, monotonic + bounded) and `_metric_for_collection(col)`; `_hybrid_rank` and the CLI display are now metric-aware. Cosine output is unchanged. - Same conversion applied to the MCP `similarity` field, layers.py L3 search, and the dedup threshold so reported similarities are consistent. Cosine behavior is byte-for-byte preserved; only non-cosine metrics change. Part B of #1726 (routing the chroma.sqlite3 BM25 fallback through BaseCollection) is tracked separately. Refs #1726, #743.
feat(searcher): metric-aware distance→similarity conversion (RFC 001 §10)
…(RFC 001, #1724) The explicit-embedding backends (pgvector, qdrant, sqlite_exact) embed through EmbeddingCollection and persist nothing about which model produced their vectors. Swapping two same-dimension models (minilm <-> embeddinggemma, both 384-d) silently corrupts retrieval with no error — the worst failure class for a verbatim-recall system. This records the model name and refuses a swap on open. - base.py: EmbedderIdentity dataclass, Embedder protocol, EmbedderIdentityUnknownWarning, and a three-state check_embedder_identity() helper (known_match / known_mismatch / unknown) raising EmbedderIdentityMismatchError / DimensionMismatchError. BaseCollection gains get/set/effective_embedder_identity hooks. - embedding.py: current_model_name() (cheap) + get_embedder_identity() (probed dimension, cached per process). - Persistence per backend: sqlite_exact meta table, pgvector marker JSON (preserved across rewrites), chroma sidecar JSON. EmbeddingCollection forwards the hooks explicitly (BaseCollection methods shadow __getattr__). - palace.get_collection enforces at open: a model swap raises; a brand-new empty collection records the current model; a populated-but-unrecorded legacy palace warns and is resolved via the CLI. The check needs no model load. - CLI: `mempalace palace set-embedder [--model NAME] [--force]` records/overrides identity without mutating global config or loading a foreign model. Chroma additionally keeps its native embedding-function check. Qdrant identity persistence is a fast-follow (no local metadata slot — needs a companion store). Closes #1724. Refs #743.
feat(backends): embedder-identity contract + three-state enforcement (RFC 001)
… path (RFC 001, #1725) Adds the maintenance contract RFC 001 specifies but #1679 deferred, and gives pgvector an opt-in HNSW index path with concurrency-safe builds. - base.py: MaintenanceResult (status ran/already_running/noop + free-form stats), UnsupportedMaintenanceKindError, BaseBackend.maintenance_kinds ClassVar (reserved: analyze/compact/reindex; a backend with no analogue MUST omit, not no-op), and BaseCollection.maintenance_state()/run_maintenance() defaults. EmbeddingCollection forwards both (BaseCollection methods shadow __getattr__). - sqlite_exact: analyze (ANALYZE) + compact (VACUUM, autocommit + page stats); omits reindex (exact scan, no ANN index). maintenance_state reports row/page counts. - pgvector: reindex builds the optional HNSW index, serialized by a session-level pg_advisory_lock so concurrent daemon writers learn "already_running" instead of each stacking an ACCESS EXCLUSIVE build (the production wedge). It is opt-in: the default exact `<=>` scan is the 100%-recall path; an HNSW index trades exact recall for scale, so an operator invokes it deliberately. Also analyze; omits compact (autovacuum). Advertises supports_server_side_indexes. maintenance_state reports index presence. - qdrant/chroma: empty maintenance_kinds (qdrant self-optimizes; chroma maintenance is the separate repair CLI) — the faithful "omit" default. Tests: contract + sqlite (real, CI-runnable) + pgvector advisory-lock flow via a fake client (ran/noop/already_running, no live Postgres). Full suite green: 2488 passed, 82.47% coverage. Benchmark three-phase wiring is deferred — the existing benchmarks/ are task-benchmarks, not backend-comparison harnesses, so there is nothing to wire into yet. Closes #1725. Refs #743.
feat(backends): observable maintenance hooks + pgvector indexed-build path (RFC 001)
…FC 001, #1730) Completes the embedder-identity contract for the last backend (qdrant) and unifies identity persistence across all local-path backends. Identity is stored in a small per-palace sidecar (mempalace_embedder.json), NOT in a backend's mismatch marker. The marker's presence signals "palace initialized" (reads raise CollectionNotInitializedError when the marker exists but the store doesn't), so recording identity at first empty open must not create it — a sidecar is unguarded, so a brand-new palace records identity immediately. This fixes a latent gap (caught in review) where pgvector/qdrant palaces stayed permanently "unknown" because the marker isn't written until the first real write. - New mempalace/backends/_sidecar.py: shared read/write_embedder_sidecar with the isinstance robustness the review bots taught us; chroma, pgvector, and qdrant all use it (chroma's inline copy is removed, pgvector switches off the marker, qdrant adds it). - QdrantCollection.get/set_embedder_identity delegate to the sidecar, so palace.get_collection enforces a model swap on a qdrant palace exactly like the other backends — no live server needed for the check. Tests: sidecar roundtrip + brand-new-palace recording (creates sidecar without a marker) + enforcement model-swap raise, for qdrant and pgvector, all server-free; chroma identity tests unchanged. Full suite: 2495 passed, 82.52%. Closes #1730. Refs #743, #1724.
LLM clients (Claude, Codex) often autocorrect acronyms in tool args (ps5→PS5). When the only difference is casing, treat it as a no-op to prevent silent room fragmentation. Genuinely different room/wing values still apply normally. Fixes #1621
Addresses review: old_meta values could be non-string after corruption or external writes. str() prevents AttributeError.
Closes #1739. Hallways shipped in 3.3.6 (#1558) with Python API entry points `list_hallways(wing=None)` and `delete_hallway(...)` in `mempalace/hallways.py`, but neither was registered as an MCP tool in `mempalace/mcp_server.py`. Mining produces hallways visible in the mine log ("Hallways: +N within-wing entity link(s)") but they were not retrievable through MCP. This change wires both functions into the MCP tool registry mirroring the existing tunnel-tool pattern: - `mempalace_list_hallways(wing: str | None = None)` wraps `hallways.list_hallways`. Optional `wing` filter goes through `_sanitize_optional_name` just like `tool_list_tunnels`, so invalid names surface a structured error instead of crashing. - `mempalace_delete_hallway(hallway_id: str)` wraps `hallways.delete_hallway` and returns `{"deleted": bool}` so callers can distinguish a successful delete from a no-op. Tests in `tests/test_mcp_server.py` cover: - list returns all records without filter - list filters correctly by wing - list rejects invalid wing names with a structured error - delete removes the targeted record and returns `{"deleted": True}` - delete with an unknown id returns `{"deleted": False}` - delete with missing/non-string id returns a structured error - both tools are present in the public `TOOLS` registry Pure pass-throughs of the existing Python API. No behavior change in the underlying hallway storage, no schema change to the `hallways.json` file format.
CI's test_no_undocumented_tools enforces that every tool registered in the TOOLS dict has a corresponding section in mcp-tools.md. The two hallway tools from b866f41 were missing — adding them here. Sections mirror the format of the existing list_tunnels and delete_tunnel entries directly above.
feat(backends): shared embedder-identity sidecar + qdrant identity (RFC 001)
Ports the OpenClaw "search before answering" protocol to the Cursor and Claude plugin surfaces so the agent reads the palace before answering about past work, people, projects, or prior decisions instead of guessing from model memory. - integrations/shared/recall-protocol.md: single source of truth for the recall protocol, referenced by the skill and the rule so they cannot drift. - skills/mempalace-recall/SKILL.md: recall-only skill (the mempalace skill keeps setup/mine/status); cross-linked from the ops skill. - rules/mempalace-recall.mdc: plugin recall rule, alwaysApply: false so it only fires on recall-relevant turns and never adds MCP latency to greenfield work. - examples/cursor/rules/: opt-in copies for non-plugin users, including an aggressive alwaysApply: true variant documented with its latency tradeoff. - .claude-plugin/skills/mempalace-recall/SKILL.md: Claude plugin parity. - tests: assert the recall skill and rules/ discovery layout; the shipped rule must be alwaysApply: false. - docs: .cursor-plugin/README.md and the cursor-hooks guide now describe the three layers of recall (hook + skill + rule). The Antigravity plugin mirror lands as a follow-up on the antigravity branch, where .antigravity-plugin/ exists. Co-authored-by: Cursor <cursoragent@cursor.com>
…1747) A clean `mempalace repair --yes` (legacy path) finished without _vacuum_and_rebuild_fts5: the bulk delete_collection + re-upsert cycle leaves the FTS5 inverted index inconsistent, so the next repair aborts at the sqlite integrity preflight. rebuild_index() got this cleanup when #1517 was fixed; cmd_repair never did. Extract the shared epilogue _post_rebuild_cleanup() (close chroma handles, then VACUUM + rebuild FTS5) and call it from both full-rebuild paths so they cannot drift apart again. Cleanup runs on the legacy success path only; failure/restore paths are unchanged. Closes #1747 Co-Authored-By: nord- <3777600+nord-@users.noreply.github.com>
fix(backends): keep post-deletion dim-None HNSW segments instead of quarantining (#1710)
fix(ids): use length-prefixed recipe v3
fix(hallways): scope hallway-file path to MempalaceConfig.palace_path (#1778)
…nt-drawer-id fix(searcher): scope neighbor expansion by parent_drawer_id (#1580)
fix(mcp): drop top-level anyOf from diary_write schema
test: stabilize release validation on develop
…ncy-fail-closed fix(mcp): fail closed when add_drawer idempotency pre-check fails
fix: close blob seq sqlite migration connection
…tion fix: preserve collection_name on MCP search retry
fix(palace): clean source mine locks safely
…set-boost-fix test: stabilize closet boost fixture on Windows
Bump version across all sources (version.py, pyproject.toml, both Claude plugin manifests, Codex plugin manifest, README badge, uv.lock) and promote the Unreleased changelog to 3.4.1. Shipping: Cursor IDE plugin + hooks, first-class Antigravity IDE support (with zero-config interpreter resolution), embeddinggemma bulk re-embed OOM fix, and backup-retention pruning. Also rebuilds the CHANGELOG compare-link block, which had been left at v3.2.0: adds the full 3.3.0-3.4.1 chain plus the previously undocumented 3.4.0, and points Unreleased at v3.4.1...HEAD. Every version header now resolves to a compare link.
chore(release): 3.4.1
There was a problem hiding this comment.
Code Review
This pull request introduces first-class support for Google's Antigravity IDE and Cursor IDE through dedicated plugins, lifecycle hooks, and installers. It also implements embedder-identity persistence and verification (RFC 001) to prevent retrieval degradation across model swaps, adds backend maintenance hooks for database optimization, introduces backup retention pruning to limit disk usage, and updates the ID generation recipe to v3 for collision safety. The reviewer feedback highlights a critical issue where global stdout redirection (os.dup2) in the MCP server could disrupt JSON-RPC transport, alongside several macOS compatibility bugs in the shell scripts where date -r is used non-portably to check file modification times.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| saved_fd = os.dup(1) | ||
| except (OSError, AttributeError): | ||
| with contextlib.redirect_stdout(buf): | ||
| result = fn() | ||
| return result, buf.getvalue() | ||
|
|
||
| try: | ||
| with tempfile.TemporaryFile() as tmp: | ||
| os.dup2(tmp.fileno(), 1) | ||
| try: |
There was a problem hiding this comment.
Using os.dup2(tmp.fileno(), 1) redirects file descriptor 1 (stdout) globally for the entire process. In an asynchronous or multi-threaded MCP server (like one built on the mcp Python SDK), this will intercept and redirect the server's own stdio transport channel during the entire execution of fn().
Any concurrent JSON-RPC responses, pings, or notifications sent by the server's event loop to the client will be swallowed by the temporary file, leading to hangs, lost messages, or connection drops.
Suggested Alternatives:
- Subprocess Isolation (Recommended): Run the mining operation in a separate subprocess (e.g., using
multiprocessingorsubprocess.Popen). This completely isolates stdout/stderr and prevents the long-running, synchronous mining operation from blocking the main MCP event loop. - Python-level redirection only: Rely solely on
contextlib.redirect_stdoutand configure the underlying libraries (likeonnxruntimeorchromadb) to disable C-level logging/verbose output via their respective APIs.
There was a problem hiding this comment.
Pull request overview
Promotes develop to main for the v3.4.1 release, bringing in new IDE integrations (Cursor + Antigravity), multiple backend/search reliability fixes, and updated docs/tests to match the release surface.
Changes:
- Adds Cursor plugin assets plus Cursor hooks, and introduces Antigravity plugin + hook integration, along with shared recall protocol docs/skills/rules.
- Improves robustness across mining/search/backends (metric-aware similarity, embedder identity sidecar support, concurrency hardening,
--limitsemantics, backup retention pruning). - Bumps versions to 3.4.1 and updates website/docs/changelog and a large set of regression tests.
Reviewed changes
Copilot reviewed 118 out of 119 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| website/reference/mcp-tools.md | MCP tool reference updates (new tools + count text) |
| website/guide/configuration.md | Documents max_backups config + env override |
| website/.vitepress/config.mts | Adds new guide links to nav |
| uv.lock | Updates editable mempalace version to 3.4.1 |
| tests/test_sqlite_exact_backend.py | Adds concurrency regression test for sqlite_exact client caching |
| tests/test_searcher.py | Updates expected metric-labeled similarity output |
| tests/test_repair.py | Adds retention pruning tests for max-seq-id backups |
| tests/test_pgvector_backend.py | Adds concurrency + close semantics tests for pgvector client |
| tests/test_miner.py | Adds --limit behavior regression tests |
| tests/test_miner_fts5_validation.py | Makes FTS5 corruption fabrication skip safely on restricted SQLite builds |
| tests/test_mine_lock_lifecycle.py | Adds mine-lock lifecycle and stale-inode race tests |
| tests/test_migrate.py | Adds migrate backup retention pruning test |
| tests/test_legacy_shell_hooks.py | Verifies legacy hooks now use shared Python parser/helpers |
| tests/test_ids.py | Updates ID recipe assertions for v3 + length-prefix hashing |
| tests/test_hybrid_search.py | Tightens hybrid search expectations (closet boost metadata) |
| tests/test_hook_shell.py | Tests new mempalace.hook_shell parsing + CLI helpers |
| tests/test_hallways.py | Updates hallway test harness for new resolver/legacy detection |
| tests/test_hallways_palace_scoped.py | Adds hallway-file palace-scoping migration tests |
| tests/test_hallways_pagination.py | Updates pagination tests to new hallway file resolution |
| tests/test_format_miner.py | Adds format miner --limit skip-already-mined test |
| tests/test_embeddinggemma.py | Adds chunking + thread-safety tests for embeddinggemma batching/cache |
| tests/test_distance_metric.py | Adds distance-metric contract tests and ranking behavior checks |
| tests/test_convo_miner.py | Adds convo miner --limit skip-already-mined test |
| tests/test_config.py | Adds config parsing tests for max_backups |
| tests/test_cli.py | Adds CLI repair cleanup ordering + VACUUM/FTS5 rebuild tests |
| tests/test_backups.py | Adds unit tests for backup pruning helper |
| tests/test_backends.py | Adds regression tests for chroma sqlite connection closing + HNSW metadata quarantine |
| skills/mempalace/SKILL.md | Adds MemPalace operational skill doc (Cursor notes, CLI delegation) |
| skills/mempalace-recall/SKILL.md | Adds recall protocol skill doc (search-before-answer) |
| rules/mempalace-recall.mdc | Adds recall rule for Cursor-style rule engines |
| README.md | Updates links + hook documentation + version badge |
| pyproject.toml | Bumps version + adds pytest warning filters |
| mempalace/version.py | Bumps package version to 3.4.1 |
| mempalace/repair.py | Adds shared post-rebuild cleanup + backup pruning for max-seq-id |
| mempalace/miner.py | Changes --limit semantics to count only newly mined files |
| mempalace/migrate.py | Adds pre-migrate backup retention pruning |
| mempalace/layers.py | Uses metric-aware distance→similarity conversion |
| mempalace/ids.py | Switches ID recipe to v3 length-prefixed hashing |
| mempalace/hook_shell.py | New shared Python helpers for legacy shell hooks |
| mempalace/hallways.py | Moves hallway persistence to palace-scoped path + legacy orphan warning |
| mempalace/format_miner.py | Changes format miner limit semantics + summary accounting |
| mempalace/convo_miner.py | Changes convo miner limit semantics + summary accounting |
| mempalace/config.py | Adds max_backups + hallway_file properties |
| mempalace/cli.py | Adds palace set-embedder subcommand + repair cleanup integration |
| mempalace/backups.py | New best-effort retention pruning utility |
| mempalace/backends/sqlite_exact.py | Adds maintenance + embedder identity + connection caching race fixes |
| mempalace/backends/qdrant.py | Adds embedder identity sidecar persistence for qdrant |
| mempalace/backends/embedding_wrapper.py | Explicitly delegates distance metric + identity/maintenance methods |
| mempalace/backends/chroma.py | Fixes HNSW recoverable-metadata logic + adds metric/identity sidecar |
| mempalace/backends/base.py | Adds metric + maintenance + embedder identity contracts/types |
| mempalace/backends/_sidecar.py | New shared embedder-identity sidecar helper |
| mempalace/backends/init.py | Re-exports maintenance/identity-related symbols |
| mcp.json | Adds Cursor-style MCP server registration wrapper |
| integrations/shared/recall-protocol.md | New canonical recall protocol shared across integrations |
| hooks/README.md | Expands hook docs for Cursor + Antigravity |
| hooks/mempal_save_hook.sh | Switches parsing/counting logic to mempalace.hook_shell |
| hooks/mempal_precompact_hook.sh | Switches parsing logic to mempalace.hook_shell + improves messaging |
| hooks/cursor/STDIN_SHAPE.md | Documents Cursor hook stdin/stdout schema |
| hooks/cursor/README.md | Adds Cursor hook installation/config docs |
| hooks/cursor/mempal_wake_hook_cursor.sh | Adds Cursor sessionStart wake hook |
| hooks/cursor/mempal_precompact_hook_cursor.sh | Adds Cursor preCompact transcript snapshot hook |
| hooks/antigravity/STDIN_SHAPE.md | Documents Antigravity hook stdin/stdout contract |
| hooks/antigravity/README.md | Adds Antigravity hook installation/docs |
| hooks/antigravity/mempal_wake_hook_antigravity.sh | Adds Antigravity PreInvocation wake hook |
| examples/cursor/rules/README.md | Documents optional Cursor recall rules |
| examples/cursor/rules/mempalace-recall.mdc | Adds recall-scoped Cursor rule example |
| examples/cursor/rules/mempalace-recall-always.mdc | Adds always-on recall Cursor rule example |
| examples/cursor/README.md | Adds Cursor hooks.json examples documentation |
| examples/cursor/hooks.minimal.json | Minimal Cursor hooks wiring example |
| examples/cursor/hooks.json | Full Cursor hooks wiring example |
| examples/antigravity/README.md | Adds Antigravity standalone config examples documentation |
| examples/antigravity/mcp_config.json | Antigravity MCP config example |
| examples/antigravity/hooks.json | Antigravity hooks wiring example |
| commands/mempalace-status.md | Adds Cursor slash-command wrapper doc |
| commands/mempalace-search.md | Adds Cursor slash-command wrapper doc |
| commands/mempalace-mine.md | Adds Cursor slash-command wrapper doc |
| commands/mempalace-init.md | Adds Cursor slash-command wrapper doc |
| commands/mempalace-help.md | Adds Cursor slash-command wrapper doc |
| CHANGELOG.md | Adds 3.4.1 release notes + compare links |
| .cursor-plugin/README.md | Adds Cursor plugin documentation |
| .cursor-plugin/plugin.json | Adds Cursor plugin manifest |
| .cursor-plugin/mcp.json | Adds Cursor plugin MCP server registration |
| .cursor-plugin/marketplace.json | Adds Cursor marketplace metadata |
| .codex-plugin/plugin.json | Bumps plugin version to 3.4.1 |
| .claude-plugin/skills/mempalace-recall/SKILL.md | Adds Claude plugin recall skill |
| .claude-plugin/plugin.json | Bumps plugin version to 3.4.1 |
| .claude-plugin/marketplace.json | Bumps marketplace version to 3.4.1 |
| .antigravity-plugin/skills/mempalace/SKILL.md | Adds Antigravity ops skill |
| .antigravity-plugin/skills/mempalace-recall/SKILL.md | Adds Antigravity recall skill |
| .antigravity-plugin/rules/mempalace-recall.md | Adds Antigravity recall rule |
| .antigravity-plugin/README.md | Adds Antigravity plugin packaging documentation |
| .antigravity-plugin/plugin.json | Adds Antigravity plugin marker manifest |
| .antigravity-plugin/mcp_config.json | Adds Antigravity plugin MCP registration |
| .antigravity-plugin/hooks.json.tmpl | Adds Antigravity hooks template for installer rendering |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ```json | ||
| { | ||
| "mempalace": { | ||
| "command": "mempalace-mcp" | ||
| } | ||
| } | ||
| ``` |
Address review feedback surfaced on the 3.4.1 release promotion (#1810). Bug fix — `date -r FILE` is GNU-only. On BSD/macOS `date -r` expects epoch seconds, not a path, so the staleness/throttle checks in the new Cursor and Antigravity hooks silently failed on macOS: the state GC swept on every fire and the pending-save guard was skipped. Replace with a portable `os.path.getmtime` one-liner via the already-resolved $MEMPAL_PYTHON_BIN (cursor/lib, antigravity/lib, antigravity save hook). This restores the "bash 3.2.57 / macOS default" compatibility the Antigravity changelog claims. Docs: - Correct the MCP tool count to 33 (was 19/29/31 in 21 places across plugin manifests, READMEs, and website docs — all drifted from the TOOLS dict / mcp-tools.md reference, which both have 33). - Fix broken CHANGELOG link to the Cursor skill (skills/, not .cursor-plugin/skills/). - Fix one-too-many `../` in skills/mempalace/SKILL.md's cursor-hooks link (resolved above the repo root). - Add the required `mcpServers` wrapper to the mcp.json example in .cursor-plugin/README.md so copy-paste yields a valid Cursor config. Left intentionally unchanged: the os.dup2 fd-1 redirect in mcp_server.py is deliberate (#225 keeps JSON-RPC off fd 1).
The snippet has no shell interpolation — the path arrives via argv, not string interpolation — so single quotes are correct and make it unambiguous that nothing is shell-expanded. Behavior is identical: `sys.argv[1]` contains no `$`, so it was never expanded (verified empirically). Matches the single-quoted `python -c` blocks already in hooks/cursor/lib/common.sh. No functional change.
fix(hooks): portable mtime in macOS hook throttles; doc cleanup
Promote
develop→mainfor the 3.4.1 releaseReleases publish only from
main(per docs/RELEASING.md). This promotes the 3.4.1 bump (merged via #1809) plus alldevelopwork accumulated since v3.4.0.Version
All six sources at 3.4.1 (
version-guard.ymlgreen on the bump commit).Headline changes since v3.4.0
Features
.cursor-plugin/) + Cursor hooks (stop/preCompact/sessionStart)Bug fixes
embeddinggemmabulk re-embed OOM fixmax_backups)Full detail in CHANGELOG.md under
## [3.4.1].After merge
Draft a GitHub Release targeting
main, tagv3.4.1→ triggerspublish.yml(PyPI Trusted Publishing, manual approval on thepypienvironment).