Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
473c59f
fix: use epsilon comparison for mtime dedup + add bulk pre-fetch
jphein Apr 10, 2026
8bcae9c
fix: cap search limit, paginate status tools, remove duplicate cache …
jphein Apr 10, 2026
70cf491
perf: batch ChromaDB writes per-file instead of per-chunk
jphein Apr 10, 2026
86eadc7
fix: add 73 technical terms to entity detector STOPWORDS
jphein Apr 10, 2026
f0856ca
feat: add similarity threshold filtering to search
jphein Apr 10, 2026
8aa33af
feat: hooks save to palace via MCP tools + auto-ingest transcripts
jphein Apr 10, 2026
fccb705
docs: add CLAUDE.md for project context
jphein Apr 10, 2026
5cd14bd
feat: concurrent mining with ThreadPoolExecutor + improved room routing
jphein Apr 10, 2026
77ebae7
feat: add mempalace export command for markdown palace backup
jphein Apr 10, 2026
4406c58
feat: add get/list/update drawer MCP tools + WAL chmod fix + metadata…
jphein Apr 10, 2026
d63ffd4
fix: configurable chunks, Layer1 scan cap, search filter dedup
jphein Apr 10, 2026
4a12748
fix: chunk_text tests, KG direction default, plugin version sync
jphein Apr 10, 2026
83c50ac
fix: PR review feedback — error handling, streaming export, L2 docs
jphein Apr 10, 2026
8f565c7
fix: address remaining PR review feedback (16 items)
jphein Apr 10, 2026
8bd289a
fix: use venv Python in hook scripts for cross-project compatibility
jphein Apr 10, 2026
d16ac4b
fix: portable Python resolution in hook scripts
jphein Apr 10, 2026
ad82c2d
test: add MCP tool tests for get/list/update_drawer
jphein Apr 10, 2026
d8c423b
docs: update expected test count to 573
jphein Apr 10, 2026
6dc8891
refactor: extract _MAX_RESULTS constant for search/list limit cap
jphein Apr 10, 2026
2da4a65
docs: update expected test count to 573
jphein Apr 10, 2026
0259154
feat: silent stop hook — save directly via Python API instead of bloc…
jphein Apr 10, 2026
1efa3ba
feat: configurable hook settings via MCP tool + config
jphein Apr 10, 2026
e9e06c8
fix: use short block reason for terminal visibility instead of stderr
jphein Apr 10, 2026
5ebfcd4
fix: make silent stop hook fully silent — no block, no error label
jphein Apr 10, 2026
930b6d8
feat: checkpoint ack MCP tool for minimal terminal confirmation
jphein Apr 10, 2026
995aed3
fix: fully silent stop hook — no block, no error label
jphein Apr 10, 2026
2209484
feat: systemMessage notification for stop hook checkpoints
jphein Apr 10, 2026
363ef37
fix: advance save marker only after successful checkpoint
jphein Apr 10, 2026
094677b
feat: palace-themed notification with conversation themes
jphein Apr 10, 2026
362a8e1
fix: address PR review feedback — rename, tests, docs
jphein Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .claude-plugin/hooks/mempal-precompact-hook.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
#!/bin/bash
# MemPalace PreCompact Hook — thin wrapper calling Python CLI
# All logic lives in mempalace.hooks_cli for cross-harness extensibility
#
# Python resolution order:
# 1. MEMPALACE_PYTHON env var (user override)
# 2. Plugin root's venv (development installs)
# 3. System python3 (pip install --user / pipx)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(dirname "$SCRIPT_DIR")"

if [ -n "$MEMPALACE_PYTHON" ] && [ -x "$MEMPALACE_PYTHON" ]; then
PYTHON="$MEMPALACE_PYTHON"
elif [ -x "$PLUGIN_ROOT/venv/bin/python3" ]; then
PYTHON="$PLUGIN_ROOT/venv/bin/python3"
else
PYTHON="python3"
fi
INPUT=$(cat)
echo "$INPUT" | python3 -m mempalace hook run --hook precompact --harness claude-code
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook precompact --harness claude-code
17 changes: 16 additions & 1 deletion .claude-plugin/hooks/mempal-stop-hook.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
#!/bin/bash
# MemPalace Stop Hook — thin wrapper calling Python CLI
# All logic lives in mempalace.hooks_cli for cross-harness extensibility
#
# Python resolution order:
# 1. MEMPALACE_PYTHON env var (user override)
# 2. Plugin root's venv (development installs)
# 3. System python3 (pip install --user / pipx)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_ROOT="$(dirname "$SCRIPT_DIR")"

if [ -n "$MEMPALACE_PYTHON" ] && [ -x "$MEMPALACE_PYTHON" ]; then
PYTHON="$MEMPALACE_PYTHON"
elif [ -x "$PLUGIN_ROOT/venv/bin/python3" ]; then
PYTHON="$PLUGIN_ROOT/venv/bin/python3"
else
PYTHON="python3"
fi
INPUT=$(cat)
echo "$INPUT" | python3 -m mempalace hook run --hook stop --harness claude-code
echo "$INPUT" | "$PYTHON" -m mempalace hook run --hook stop --harness claude-code
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mempalace",
"version": "3.0.14",
"version": "3.1.0",
"description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.",
"author": {
"name": "milla-jovovich"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mempalace",
"version": "3.0.14",
"version": "3.1.0",
"description": "Give your AI a memory — mine projects and conversations into a searchable palace. 19 MCP tools, auto-save hooks, and guided setup.",
"author": {
"name": "milla-jovovich"
Expand Down
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# CLAUDE.md — memorypalace

## What This Is

JP's fork of [milla-jovovich/mempalace](https://github.com/milla-jovovich/mempalace) — a local AI memory system using ChromaDB for verbatim storage and semantic search.

- **Fork**: `jphein/mempalace` (origin) / `milla-jovovich/mempalace` (upstream)
- **Version**: 3.1.0 + local fixes
- **Python**: venv at `./venv/`, editable install with dev deps
- **Palace data**: `~/.mempalace/palace` (ChromaDB) + `~/.mempalace/config.json`

## Key Files

- `~/Projects/mempalace.yaml` — **do not delete**. Mining config with wing/room definitions. Regenerate with `mempalace init ~/Projects --yes` if lost.
- `~/.mempalace/config.json` — topic wings and hall keywords, customized for JP's domains (infrastructure, development, tools, creative, projects, system).
- `~/.mempalace/palace/` — ChromaDB vector store. The actual data.
- `~/.mempalace/hook_state/` — stop hook session tracking.

## Development

```bash
source venv/bin/activate
python -m pytest tests/ -x -q # run tests (576 expected)
mempalace status # check palace state
mempalace search "query" # test search
python -m mempalace.mcp_server # run MCP server standalone
```
Comment on lines +21 to +27

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

This testing note says “573 expected”, but the PR description/test plan indicates 576 tests passing and the suite has grown (new exporter/miner/MCP tests). Please update the expected test count here to match current reality so it remains useful during development.

Copilot uses AI. Check for mistakes.

Ruff for linting (`ruff check`), line length 100, target Python 3.9.

## Fork Changes (ahead of upstream)

1. **fix: epsilon mtime comparison** — `palace.py` uses `abs() < 0.01` instead of `==` for float mtime dedup
2. **feat: bulk_check_mined()** — paginated pre-fetch of all source_file/mtime pairs
3. **fix: MCP server** — search limit capped [1,100], status/taxonomy tools paginated past 10K, duplicate cache decls removed
4. **perf: batch ChromaDB writes** — one upsert per file instead of per chunk in both miners
5. **fix: entity detector STOPWORDS** — 73 technical terms added (Handler, Node, Service, etc.)
6. **feat: similarity threshold** — `min_similarity` parameter in search, default 1.5 L2 distance in MCP
7. **feat: hooks_cli** — stop hook saves directly via Python API with systemMessage notification, precompact blocks for AI-driven save, auto-ingest transcripts

## Upstream PRs

- milla-jovovich/mempalace#483 — mtime dedup fix
- milla-jovovich/mempalace#484 — search limit + pagination + cache fix

## Integration

- **Claude Code plugin**: installed at user scope via marketplace
- **MCP server**: global user scope — available in all projects
- **Stop hook**: fires every 15 messages, saves directly via Python API + systemMessage notification + auto-ingests transcript
- **PreCompact hook**: emergency save before context compaction

Comment on lines +48 to +52

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

This section says the stop hook “saves to palace via MCP tools”, but the stop hook now writes the diary checkpoint directly via Python (calling tool_diary_write without blocking) and only the precompact hook still instructs MCP tool usage. Please update this description so it matches the current hook behavior.

Copilot uses AI. Check for mistakes.
## Testing

Always run `python -m pytest tests/ -x -q` after changes. 576 tests expected to pass. Benchmark and stress tests are excluded by default (use `-m benchmark` or `-m stress` to include).
34 changes: 34 additions & 0 deletions mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def cmd_mine(args):
dry_run=args.dry_run,
respect_gitignore=not args.no_gitignore,
include_ignored=include_ignored,
workers=args.workers,
)


Expand Down Expand Up @@ -150,6 +151,23 @@ def cmd_split(args):
sys.argv = old_argv


def cmd_export(args):
from .exporter import export_palace

palace_path = os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
output_dir = os.path.expanduser(args.output)

print(f"\n{'=' * 55}")
print(" MemPalace Export")
print(f"{'=' * 55}\n")
print(f" Palace: {palace_path}")
print(f" Output: {output_dir}\n")

export_palace(palace_path=palace_path, output_dir=output_dir)

print(f"\n{'=' * 55}\n")

Comment on lines +154 to +169

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

The PR title/description focuses on the silent stop hook change, but this PR also introduces additional user-facing features/behavior (e.g., new export command, exporter module/tests, miner parallelism/workers, new MCP drawer tools). Please either update the PR description to cover these changes (and their motivation/risk) or split them into separate PRs to keep review scope manageable.

Copilot uses AI. Check for mistakes.

def cmd_status(args):
from .miner import status

Expand Down Expand Up @@ -442,6 +460,12 @@ def main():
default="exchange",
help="Extraction strategy for convos mode: 'exchange' (default) or 'general' (5 memory types)",
)
p_mine.add_argument(
"--workers",
type=int,
default=0,
help="Parallel workers for file processing (default: min(8, cpu_count); 1 = sequential)",
)

# search
p_search = sub.add_parser("search", help="Find anything, exact words")
Expand All @@ -462,6 +486,15 @@ def main():
"--config", default=None, help="Entity config JSON (e.g. entities.json)"
)

# export
p_export = sub.add_parser("export", help="Export palace as browsable markdown files")
p_export.add_argument(
"--output",
"-o",
default="./palace-export",
help="Output directory (default: ./palace-export)",
)

# wake-up
p_wakeup = sub.add_parser("wake-up", help="Show L0 + L1 wake-up context (~600-900 tokens)")
p_wakeup.add_argument("--wing", default=None, help="Wake-up for a specific project/wing")
Expand Down Expand Up @@ -561,6 +594,7 @@ def main():
"mine": cmd_mine,
"split": cmd_split,
"search": cmd_search,
"export": cmd_export,
"mcp": cmd_mcp,
"compress": cmd_compress,
"wake-up": cmd_wakeup,
Expand Down
39 changes: 39 additions & 0 deletions mempalace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,42 @@ def hall_keywords(self):
"""Mapping of hall names to keyword lists."""
return self._file_config.get("hall_keywords", DEFAULT_HALL_KEYWORDS)

@property
def hook_silent_save(self):
"""Whether the stop hook saves directly (True) or blocks for MCP calls (False)."""
return self._file_config.get("hooks", {}).get("silent_save", True)

@property
def hook_desktop_toast(self):
"""Whether the stop hook shows a desktop notification via notify-send."""
return self._file_config.get("hooks", {}).get("desktop_toast", False)

def set_hook_setting(self, key: str, value: bool):
"""Update a hook setting and write config to disk."""
if "hooks" not in self._file_config:
self._file_config["hooks"] = {}
self._file_config["hooks"][key] = value
try:
with open(self._config_file, "w") as f:
json.dump(self._file_config, f, indent=2)
except OSError:
pass

@property
def chunk_size(self):
"""Characters per drawer chunk."""
return self._file_config.get("chunk_size", 800)

@property
def chunk_overlap(self):
"""Overlap between adjacent chunks."""
return self._file_config.get("chunk_overlap", 100)

@property
def min_chunk_size(self):
"""Minimum chunk size — skip smaller chunks."""
return self._file_config.get("min_chunk_size", 50)

def init(self):
"""Create config directory and write default config.json if it doesn't exist."""
self._config_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -187,6 +223,9 @@ def init(self):
"collection_name": DEFAULT_COLLECTION_NAME,
"topic_wings": DEFAULT_TOPIC_WINGS,
"hall_keywords": DEFAULT_HALL_KEYWORDS,
"chunk_size": 800,
"chunk_overlap": 100,
"min_chunk_size": 50,
}
with open(self._config_file, "w") as f:
json.dump(default_config, f, indent=2)
Expand Down
56 changes: 33 additions & 23 deletions mempalace/convo_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,34 +326,44 @@ def mine_convos(
if extract_mode != "general":
room_counts[room] += 1

# File each chunk
drawers_added = 0
# Batch all chunks into a single add call per file
batch_docs = []
batch_ids = []
batch_metas = []
for chunk in chunks:
chunk_room = chunk.get("memory_type", room) if extract_mode == "general" else room
if extract_mode == "general":
room_counts[chunk_room] += 1
drawer_id = f"drawer_{wing}_{chunk_room}_{hashlib.sha256((source_file + str(chunk['chunk_index'])).encode()).hexdigest()[:24]}"
try:
collection.add(
documents=[chunk["content"]],
ids=[drawer_id],
metadatas=[
{
"wing": wing,
"room": chunk_room,
"source_file": source_file,
"chunk_index": chunk["chunk_index"],
"added_by": agent,
"filed_at": datetime.now().isoformat(),
"ingest_mode": "convos",
"extract_mode": extract_mode,
}
],
)
drawers_added += 1
except Exception as e:
if "already exists" not in str(e).lower():
raise
batch_docs.append(chunk["content"])
batch_ids.append(drawer_id)
batch_metas.append(
{
"wing": wing,
"room": chunk_room,
"source_file": source_file,
"chunk_index": chunk["chunk_index"],
"added_by": agent,
"filed_at": datetime.now().isoformat(),
"ingest_mode": "convos",
"extract_mode": extract_mode,
}
)
drawers_added = 0
_ADD_BATCH_SIZE = 100
if batch_docs:
for batch_start in range(0, len(batch_docs), _ADD_BATCH_SIZE):
batch_end = batch_start + _ADD_BATCH_SIZE
try:
collection.add(
documents=batch_docs[batch_start:batch_end],
ids=batch_ids[batch_start:batch_end],
metadatas=batch_metas[batch_start:batch_end],
)
drawers_added += len(batch_docs[batch_start:batch_end])
except Exception as e:
if "already exists" not in str(e).lower():
raise

total_drawers += drawers_added
print(f" ✓ [{i:4}/{len(files)}] {filepath.name[:50]:50} +{drawers_added}")
Expand Down
72 changes: 72 additions & 0 deletions mempalace/entity_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,78 @@
"networks",
"training",
"inference",
# Common technical/documentation terms that appear capitalized but aren't entities
"handler",
"node",
"service",
"manager",
"client",
"server",
"worker",
"plugin",
"module",
"interface",
"event",
"request",
"response",
"update",
"config",
"builder",
"factory",
"component",
"controller",
"provider",
"wrapper",
"helper",
"util",
"parser",
"loader",
"renderer",
"adapter",
"proxy",
"listener",
"observer",
"validator",
"formatter",
"converter",
"resolver",
"selector",
"reducer",
"dispatcher",
"compiler",
"optimizer",
"analyzer",
"generator",
"template",
"registry",
"repository",
"gateway",
"middleware",
"pipeline",
"container",
"context",
"session",
"token",
"stream",
"buffer",
"cache",
"queue",
"schema",
"entity",
"instance",
"object",
"method",
"property",
"attribute",
"parameter",
"argument",
"variable",
"constant",
"function",
"package",
"framework",
"runtime",
"platform",
}

# For entity detection — prose only, no code files
Expand Down
Loading