Skip to content

feat: MCP tools, hooks, export, search threshold + fixes#493

Closed
jphein wants to merge 47 commits intoMemPalace:mainfrom
jphein:feat/mcp-hooks-export
Closed

feat: MCP tools, hooks, export, search threshold + fixes#493
jphein wants to merge 47 commits intoMemPalace:mainfrom
jphein:feat/mcp-hooks-export

Conversation

@jphein
Copy link
Copy Markdown
Collaborator

@jphein jphein commented Apr 10, 2026

Summary

Supersedes #483 and #484 (includes those fixes plus new features).

Test plan

  • 562 tests pass
  • Verify mempalace export -o /tmp/test produces correct markdown tree
  • Verify new MCP tools (get_drawer, list_drawers, update_drawer) via MCP protocol
  • Verify stop hook triggers mempalace save instructions

🤖 Generated with Claude Code

jphein and others added 19 commits April 9, 2026 19:15
Float equality on mtime fails due to JSON round-trip precision loss,
causing every file to be re-mined on each run. Use epsilon < 0.01.

Also adds bulk_check_mined() for fetching all source_file/mtime pairs
in paginated batches — turns 25K individual DB queries into ~5 fetches.

Fixes MemPalace#475

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…decls

- Clamp tool_search limit to [1, 100] to prevent memory exhaustion
- Replace hardcoded limit=10000 in status/taxonomy tools with paginated
  _fetch_all_metadata() helper (matches palace_graph.py pattern)
- Remove duplicate _client_cache/_collection_cache declarations

Fixes MemPalace#477, MemPalace#478, MemPalace#479

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Accumulate all chunks for a file into lists, then issue a single
collection.upsert() (miner) or collection.add() (convo_miner) call.
Reduces 125K-375K individual DB round-trips to ~25K batched calls.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Prevents false positives like Handler, Node, Service, Manager, Client
being flagged as project/person entities in code-heavy directories.

Fixes MemPalace#476

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds min_similarity parameter (L2 distance cutoff) to search_memories()
and MCP tool_search (default 1.5). Filters out clearly irrelevant
results instead of always returning top-N regardless of quality.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Updated STOP_BLOCK_REASON to instruct AI to use mempalace_diary_write
  and mempalace_add_drawer instead of generic "memory system"
- Updated PRECOMPACT_BLOCK_REASON with same MCP tool instructions
- Added _ingest_transcript() to mine Claude Code JSONL transcripts
  into the palace automatically on stop/precompact triggers
- Transcript goes into a "sessions" wing via convo_miner

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Documents fork relationship, key files, development workflow,
fork changes, upstream PRs, and integration details.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Mining:
- Added _prepare_file() for thread-safe file processing (read/chunk/route)
- mine() now supports --workers flag (default: min(8, cpu_count))
- Concurrent path: bulk mtime pre-fetch, parallel _prepare_file(), serialized
  ChromaDB writes in batches of 100. Sequential path unchanged (workers=1).

Room routing:
- Priority 1: exact folder match only (no substring)
- Priority 2: exact filename match only
- Content scan increased from 2KB to 5KB (full file if <10KB)
- Keyword scoring uses word-boundary regex instead of substring count
- Added 13 unit tests for detect_room covering all priority paths

Co-Authored-By: Claude Opus 4.6 <[email protected]>
New exporter.py: paginates all drawers, groups by wing/room, writes
browsable markdown tree with index.md table of contents. Each drawer
becomes a blockquoted section with metadata table.

Usage: mempalace export -o ./palace-export

Also fixes test_cli.py for new --workers arg on mine subparser.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
… cache

I7: Three new MCP tools — get_drawer, list_drawers (paginated),
update_drawer (with WAL audit logging and input sanitization).

I8: WAL file chmod(0o600) now only runs on file creation instead
of every write call.

I9: 5-second TTL metadata cache for status/wings/taxonomy tools.
Eliminates redundant full-palace pagination when tools are called
in quick succession.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
I6: Chunk size/overlap/min now configurable via ~/.mempalace/config.json
instead of hardcoded constants. Wired through mine() → process_file() →
chunk_text().

I11: Layer1.generate() capped at MAX_SCAN=2000 drawers (was unbounded).
Reduces wake-up from 250+ ChromaDB round-trips to 4 max.

I12: Extracted _build_where_filter() helper in searcher.py, replaced
5 duplicate where-filter blocks across searcher.py and layers.py.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
I10: 10 unit tests for chunk_text() covering boundaries, overlap,
indices, empty/whitespace input, content preservation.

I13: KG query_entity default direction aligned from "outgoing" to
"both" to match the MCP schema default.

I14: Plugin versions synced to 3.1.0 in both .claude-plugin/ and
.codex-plugin/.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Float equality on mtime fails due to JSON round-trip precision loss,
causing every file to be re-mined on each run. Use epsilon < 0.01.

Also adds bulk_check_mined() for fetching all source_file/mtime pairs
in paginated batches — turns 25K individual DB queries into ~5 fetches.

Fixes MemPalace#475

Co-Authored-By: Claude Opus 4.6 <[email protected]>
…decls

- Clamp tool_search limit to [1, 100] to prevent memory exhaustion
- Replace hardcoded limit=10000 in status/taxonomy tools with paginated
  _fetch_all_metadata() helper (matches palace_graph.py pattern)
- Remove duplicate _client_cache/_collection_cache declarations

Fixes MemPalace#477, MemPalace#478, MemPalace#479

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds min_similarity parameter (L2 distance cutoff) to search_memories()
and MCP tool_search (default 1.5). Filters out clearly irrelevant
results instead of always returning top-N regardless of quality.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Updated STOP_BLOCK_REASON to instruct AI to use mempalace_diary_write
  and mempalace_add_drawer instead of generic "memory system"
- Updated PRECOMPACT_BLOCK_REASON with same MCP tool instructions
- Added _ingest_transcript() to mine Claude Code JSONL transcripts
  into the palace automatically on stop/precompact triggers
- Transcript goes into a "sessions" wing via convo_miner

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Documents fork relationship, key files, development workflow,
fork changes, upstream PRs, and integration details.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
New exporter.py: paginates all drawers, groups by wing/room, writes
browsable markdown tree with index.md table of contents. Each drawer
becomes a blockquoted section with metadata table.

Usage: mempalace export -o ./palace-export

Also fixes test_cli.py for new --workers arg on mine subparser.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
… cache

I7: Three new MCP tools — get_drawer, list_drawers (paginated),
update_drawer (with WAL audit logging and input sanitization).

I8: WAL file chmod(0o600) now only runs on file creation instead
of every write call.

I9: 5-second TTL metadata cache for status/wings/taxonomy tools.
Eliminates redundant full-palace pagination when tools are called
in quick succession.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copilot AI review requested due to automatic review settings April 10, 2026 03:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds MCP server enhancements, safer/cheaper metadata fetching, a new markdown exporter, and hook updates to better support MemPalace workflows (plus a few bug fixes around dedup/search limits).

Changes:

  • Add markdown export command (mempalace export) and exporter implementation + tests.
  • Extend MCP server tools (drawer CRUD helpers, pagination/caching, similarity threshold) and hook instructions/transcript ingest.
  • Improve dedup correctness (epsilon mtime compare) and add a bulk prefetch helper for mined-file checks.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
mempalace/cli.py Adds export command + forwards workers arg into mining.
mempalace/exporter.py New exporter that writes a wing/room markdown tree + index.
tests/test_exporter.py New tests validating exporter structure/content.
mempalace/mcp_server.py Pagination/cache helpers, search threshold, new MCP tools (get/list/update drawer), WAL chmod tweak.
mempalace/searcher.py Adds min_similarity/distance filtering and returns distance in results.
mempalace/hooks_cli.py Updates stop/precompact instructions + transcript auto-ingest logic.
mempalace/palace.py Epsilon mtime comparison + bulk_check_mined() helper.
tests/test_cli.py Updates mining CLI tests to include new workers arg.
CLAUDE.md Adds project context and workflow notes for Claude Code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/cli.py
Comment on lines 87 to 99
from .miner import mine

mine(
project_dir=args.dir,
palace_path=palace_path,
wing_override=args.wing,
agent=args.agent,
limit=args.limit,
dry_run=args.dry_run,
respect_gitignore=not args.no_gitignore,
include_ignored=include_ignored,
workers=args.workers,
)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already working — mine() has the workers parameter (line 661). The PR diff may not show it since it was added in the concurrent mining commit.

Comment thread mempalace/hooks_cli.py
Comment on lines +119 to +122
try:
palace_path = MempalaceConfig().palace_path
except Exception:
return
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — changed to MempalaceConfig() (validate config loads) instead of assigning to unused palace_path.

Comment thread tests/test_exporter.py
Comment on lines +5 to +7

import chromadb
import yaml
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — removed unused chromadb import from test_exporter.py.

Comment thread mempalace/exporter.py Outdated
Comment on lines +62 to +71
for wing in sorted(grouped):
wing_dir = os.path.join(output_dir, wing)
os.makedirs(wing_dir, exist_ok=True)
wing_drawer_count = 0

rooms = grouped[wing]
for room in sorted(rooms):
drawers = rooms[room]
room_path = os.path.join(wing_dir, f"{room}.md")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added _safe_path_component() that strips /\:*?"<>| and leading dots from wing/room metadata before using as directory/file names. Applied to both wing dirs and room filenames in the streaming exporter.

Comment thread mempalace/searcher.py Outdated
Comment on lines +149 to +159
"similarity": round(1 - dist, 3),
"distance": round(dist, 4),
}
)

# Filter out results exceeding the distance threshold.
# ChromaDB default L2: lower distance = more similar.
# min_similarity=0.0 (default) disables filtering for backwards compat.
if min_similarity > 0.0:
hits = [h for h in hits if h["distance"] <= min_similarity]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — distance filtering now happens on the raw float before rounding, then the display values are rounded for output. Avoids borderline precision loss.

Comment thread mempalace/mcp_server.py
Comment on lines 918 to 920
"query": {"type": "string", "description": "What to search for"},
"limit": {"type": "integer", "description": "Max results (default 5)"},
"wing": {"type": "string", "description": "Filter by wing (optional)"},
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added minimum: 1, maximum: 100 to the search limit schema and list_drawers limit/offset schemas.

Comment thread mempalace/mcp_server.py
Comment on lines +460 to +529
def tool_get_drawer(drawer_id: str):
"""Fetch a single drawer by ID. Returns full content and metadata."""
col = _get_collection()
if not col:
return _no_palace()
try:
result = col.get(ids=[drawer_id], include=["documents", "metadatas"])
if not result["ids"]:
return {"error": f"Drawer not found: {drawer_id}"}
meta = result["metadatas"][0]
doc = result["documents"][0]
return {
"drawer_id": drawer_id,
"content": doc,
"wing": meta.get("wing", ""),
"room": meta.get("room", ""),
"metadata": meta,
}
except Exception as e:
return {"error": str(e)}


def tool_list_drawers(wing: str = None, room: str = None, limit: int = 20, offset: int = 0):
"""List drawers with pagination. Optional wing/room filter."""
limit = max(1, min(limit, 100))
col = _get_collection()
if not col:
return _no_palace()
try:
where = None
conditions = []
if wing:
conditions.append({"wing": wing})
if room:
conditions.append({"room": room})
if len(conditions) == 1:
where = conditions[0]
elif len(conditions) > 1:
where = {"$and": conditions}

kwargs = {"include": ["documents", "metadatas"], "limit": limit, "offset": offset}
if where:
kwargs["where"] = where
result = col.get(**kwargs)

drawers = []
for i, did in enumerate(result["ids"]):
meta = result["metadatas"][i]
doc = result["documents"][i]
drawers.append(
{
"drawer_id": did,
"wing": meta.get("wing", ""),
"room": meta.get("room", ""),
"content_preview": doc[:200] + "..." if len(doc) > 200 else doc,
}
)
return {
"drawers": drawers,
"count": len(drawers),
"offset": offset,
"limit": limit,
}
except Exception as e:
return {"error": str(e)}


def tool_update_drawer(drawer_id: str, content: str = None, wing: str = None, room: str = None):
"""Update an existing drawer's content and/or metadata."""
col = _get_collection()
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted — will add MCP tool tests for get/list/update_drawer in a follow-up commit. The existing test_mcp_server.py covers the other tools; these three need coverage for basic CRUD and pagination.

Comment thread CLAUDE.md
Comment on lines +21 to +24
```bash
source venv/bin/activate
python -m pytest tests/ -x -q # run tests (534 expected)
mempalace status # check palace state
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated CLAUDE.md from 534 to 562 expected tests.

Comment thread mempalace/searcher.py
Comment on lines 93 to 100
def search_memories(
query: str, palace_path: str, wing: str = None, room: str = None, n_results: int = 5
query: str,
palace_path: str,
wing: str = None,
room: str = None,
n_results: int = 5,
min_similarity: float = 0.0,
) -> dict:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the naming is confusing. For now I've added comprehensive L2 distance documentation in the function docstring and MCP schema description. A full rename to max_distance would be a breaking change — happy to do it in a separate PR with a deprecation alias if you'd prefer.

Comment thread mempalace/mcp_server.py
Comment on lines +996 to +1003
"limit": {
"type": "integer",
"description": "Max results per page (default 20, max 100)",
},
"offset": {
"type": "integer",
"description": "Offset for pagination (default 0)",
},
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added minimum/maximum bounds to list_drawers schema: limit min 1 max 100, offset min 0.

Copy link
Copy Markdown

@web3guru888 web3guru888 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid PR, jphein — this addresses several gaps I've hit in production use.

Similarity threshold — This is the feature I've wanted most. We implemented min_similarity filtering internally (threshold 0.72 for our use case) and it dramatically cuts noise on broad queries. One critical thing to document: is min_similarity filtering against L2 distance (higher = less similar) or cosine similarity (higher = more similar)? ChromaDB's default embedding function returns L2 by default, so a threshold of 1.5 is excluding scores above 1.5, not including. That's counterintuitive for users coming from cosine-similarity workflows and should be called out clearly in CLAUDE.md and/or the docstring. Easy fix but prevents a lot of confusion.

Hooks rewrite — Going through MCP tools (mempalace_diary_write, mempalace_add_drawer) instead of direct Python calls is the right architectural choice. Keeps hooks decoupled from internals and benefits automatically from validation, pagination, etc. The auto-ingest of Claude Code JSONL transcripts on compaction is clever — turns a normally lossy event into a structured memory commit.

New MCP toolslist_drawers with wing/room pagination fills a real gap. Right now you're flying blind unless you already know what to search for. get_drawer by ID is handy for follow-up fetches after a search result. Both straightforward, good additions.

export_palace at scale — Does this stream or paginate? If a palace has 10K–50K drawers (which is realistic for long-running integrations), a single export call risks timeout at the MCP layer. I'd suggest either: (a) returning a paginated export with page/page_size params, or (b) writing to a temp file on disk and returning the path. The exporter module design looks right for (b) if export_palace just wraps a file write.

Conflict with #484 — Both this PR and #484 touch mcp_server.py. Worth checking if there's overlap before merge — specifically around the search tool signature changes.

Mtime epsilon fix is a clean correctness improvement. No concerns there.

Overall: 👍 on direction. Threshold docs + export scaling are the two things worth addressing before merge.

jphein and others added 5 commits April 9, 2026 21:10
Address web3guru888's review feedback across PRs MemPalace#492 and MemPalace#493:

- palace.py: remove unused filepaths param from bulk_check_mined(),
  replace bare except with logger.warning for partial fetch visibility
- miner.py: wrap future.result() in try/except so one file failure
  doesn't abort the entire concurrent mining run
- exporter.py: stream drawers in batches instead of loading entire
  palace into memory — keeps memory bounded for large palaces
- searcher.py: document min_similarity as L2 distance (not cosine)
  with typical range guidance in docstring

Co-Authored-By: Claude Opus 4.6 <[email protected]>
# Conflicts:
#	mempalace/exporter.py
#	mempalace/palace.py
- Rename _build_where_filter → build_where_filter (public cross-module API)
- Add float() cast + TypeError/ValueError handling in _is_already_mined
- Add chunk_overlap validation (must be >= 0 and < chunk_size)
- Batch convo_miner adds to 100 docs per call (avoid SQLite limits)
- Stream miner writes as futures complete (bounded memory)
- Remove unused palace_path in hooks_cli
- Remove unused chromadb import in test_exporter
- Sanitize wing/room as path components in exporter (prevent traversal)
- Filter on raw distance before rounding in searcher
- Clamp negative offset in tool_list_drawers
- No-op early return + cache invalidation in tool_update_drawer
- Add min/max schema bounds for search limit and list_drawers limit/offset
- Update CLAUDE.md test count (534 → 562)
- Improve chunk coverage test with position-unique tokens

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Stop and precompact hooks used bare `python3` which resolves to system
Python in sessions outside the memorypalace project directory, causing
`No module named mempalace` errors. Now uses the venv's Python with
fallback to system python3.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@psaghelyi
Copy link
Copy Markdown

Tested this branch (feat/mcp-hooks-export @ 548abd6) against our 14,902-drawer palace — posting here since @web3guru888 asked me on #478 to verify. Partial fix: the MCP server side lands cleanly, but the CLI still has the old bug.

✓ MCP server — correctly fixed

mcp_server.py adds _fetch_all_metadata(col, where=None) paginating in 1000-item batches, with a 5s TTL cache for repeated calls. All four tools — tool_status, tool_list_wings, tool_list_rooms, tool_get_taxonomy — now call it. Pattern matches palace_graph.py:49-51. Verified by reading the branch source rather than reconfiguring our running MCP server (didn't want to touch the live plugin install).

✗ CLI mempalace status — still broken

miner.py:850-870 (def status(palace_path)) wasn't touched by this PR. Line 861 still reads:

r = col.get(limit=10000, include=["metadatas"])
...
print(f"  MemPalace Status — {len(metas)} drawers")

Running the PR branch directly on our palace:

$ uvx --from 'git+https://github.com/jphein/mempalace@feat/mcp-hooks-export' mempalace status | head -3
=======================================================
  MemPalace Status — 10000 drawers
=======================================================

Same 11-of-17 wing truncation we saw on 3.1.0 — askalot-analysis, docs, e2e-tests, infrastructure, scripts, shared-frontend are all silently dropped. col.count() still reports 14,902.

Suggested trivial follow-up

The CLI status() function is a tight ~20 lines and can call straight into the new helper (or duplicate the pattern if you'd rather not import from mcp_server):

def status(palace_path: str):
    try:
        client = chromadb.PersistentClient(path=palace_path)
        col = client.get_collection("mempalace_drawers")
    except Exception:
        print(f"\n  No palace found at {palace_path}")
        return

    total = col.count()
    metas = []
    offset = 0
    while offset < total:
        batch = col.get(limit=1000, offset=offset, include=["metadatas"])
        metas.extend(batch["metadatas"])
        if not batch["metadatas"]:
            break
        offset += len(batch["metadatas"])

    wing_rooms = defaultdict(lambda: defaultdict(int))
    for m in metas:
        wing_rooms[m.get("wing", "?")][m.get("room", "?")] += 1

    print(f"\n{'=' * 55}")
    print(f"  MemPalace Status — {total} drawers")
    # ... rest unchanged

Happy to send a PR against your branch if that's easier than bundling it into this one. Otherwise #478 stays half-open since the user-visible CLI still reports the wrong count.

Everything else in the MCP server diff (_fetch_all_metadata, the cache, the search limit cap, the new read tools) looks good from a code-review standpoint — I didn't test those end-to-end.

@web3guru888
Copy link
Copy Markdown

Thanks @psaghelyi — really valuable field test on a real 14.9K palace.

Good catch on status() in miner.py. I reviewed the MCP server path (_fetch_all_metadata(), the tool registry, WAL guard) but didn't walk through the CLI surface in miner.py:850–870. That's on me — same underlying defect, different call site, should have been part of the same sweep.

Your patch is correct and clean: replace col.get(limit=10000) with the paginated loop, accumulate, then report len(all_ids). It mirrors exactly how _fetch_all_metadata() was fixed on the server side, which is the right approach.

I'd suggest @jphein roll this in before merge. Shipping the PR with MCP server fixed but status() still capped at 10K would leave half of #478 open — and status() is the first thing ops people reach for when something looks wrong. If you're up for it @psaghelyi, a PR against feat/mcp-hooks-export is probably the fastest path.

The CLI `mempalace status` command still capped at 10,000 drawers after
this PR's MCP server fix, because miner.status() retained the original
`col.get(limit=10000, include=["metadatas"])` call. On palaces larger
than 10K, it printed a wrong total and silently dropped every wing past
the cutoff (tested on a 14,902-drawer / 17-wing palace: CLI reported
"10000 drawers" and listed only 11 wings).

Replace the single bounded call with the same paginated offset loop the
new `_fetch_all_metadata()` helper uses in mcp_server.py (1,000-item
batches), and report the true total via `col.count()`.

Closes the CLI half of MemPalace#478.
@psaghelyi
Copy link
Copy Markdown

@jphein — rolled the miner.status() fix into a small PR against your feature branch: jphein#1

One hunk, mirrors the pagination pattern from your new _fetch_all_metadata() helper on the server side. Verified against our 14,902-drawer / 17-wing palace — CLI now reports the correct total and all wings. If you merge it into feat/mcp-hooks-export before this PR lands upstream, #478 closes cleanly on both the MCP and CLI surfaces in one go.

jphein added 2 commits April 10, 2026 08:08
Clean fix — mirrors the paginated offset loop from _fetch_all_metadata() in mcp_server.py. Verified by psaghelyi against 14,902-drawer palace. Closes the CLI half of MemPalace#478.
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 10, 2026

@psaghelyi — great catch and thanks for the thorough field test against your 14.9K palace. Merged your PR (#1) into feat/mcp-hooks-export — the CLI status() now paginates identically to the MCP server's _fetch_all_metadata(). Both surfaces of #478 are now fixed in this branch.

Also merged latest main into both PR branches (fix/perf-quality and feat/mcp-hooks-export) to pick up the _MAX_RESULTS constant extraction and test count update.

@web3guru888
Copy link
Copy Markdown

@jphein — the cross-branch coordination with psaghelyi is exactly the right approach. The status() CLI and MCP _fetch_all_metadata() being pagination-identical is important — having two surfaces of #478 that behave differently would be a persistent source of confusion.

Merging main into both branches to pick up the _MAX_RESULTS constant extraction is also good hygiene — keeps the constant single-sourced before the final merge. Looking forward to the combined PR landing.

jphein and others added 10 commits April 10, 2026 10:03
…king MCP

Stop hook no longer blocks Claude with MCP tool call instructions every 15
messages. Instead it saves a diary checkpoint directly via the Python API
and shows a single-line terminal notification + desktop toast.

Fixes MemPalace#554

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add hooks.silent_save and hooks.desktop_toast to config.json, readable
via new mempalace_hook_settings MCP tool (get/set). Stop hook checks
config to decide between silent direct save vs legacy blocking MCP.
Restore STOP_BLOCK_REASON for legacy mode. Toast is opt-in via config.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
stderr from hook subprocesses doesn't reach the Claude Code terminal.
Block with a one-liner notification after the direct save completes —
save already happened, Claude just continues.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Claude Code shows all hook blocks as "Stop hook error:" with no info
level available. Return {} for truly invisible saves.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Hook saves directly, then blocks asking Claude to call
mempalace_checkpoint_ack — a zero-param tool returning one line
like "✦ Journal entry filed — 30 messages tucked into drawers".
Replaces both the verbose MCP diary/drawer calls and the invisible
silent mode with a single clean terminal line.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Claude Code labels all hook blocks as "Stop hook error:" with no way
to customize. Go fully silent instead — save happens invisibly.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Stop hook now outputs {"systemMessage": "✦ N messages filed away"} which
Claude Code renders as a visible one-line terminal notification — no MCP
tool call needed. Also renames checkpoint_ack → memories_filed_away and
fixes MCP server to silently ignore all notifications/ methods per spec.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copilot review caught that hook_stop() updated the last-save marker
before _save_diary_direct() ran. If save failed, the marker would
still advance and skip the next checkpoint. Move marker write after
save confirms success. Also updates CLAUDE.md test count and hook docs.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
jphein and others added 4 commits April 10, 2026 11:20
Stop hook now extracts topic keywords from recent messages and displays
them in the notification: "✦ 10 memories woven into the palace — hooks,
notifications, MCP". Stopword filtering keeps only distinctive terms.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Rename min_similarity → max_distance (searcher + MCP schema), keep
  backwards compat alias in MCP tool handler
- Fix ingest comment accuracy (async/best-effort, not guaranteed)
- Add notification protocol tests (all notifications/* return None,
  unknown methods without id return None)
- 578 tests passing

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@jphein
Copy link
Copy Markdown
Collaborator Author

jphein commented Apr 10, 2026

Superseded by consolidated PR — all changes plus review feedback incorporated into a single clean PR from main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants