Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8b7815a
fix(kg): reject inverted intervals in add_triple (valid_to < valid_from)
arnoldwender Apr 26, 2026
2aa0c68
fix(entity_registry): atomic write to prevent partial corruption on c…
arnoldwender Apr 26, 2026
40f295d
fix(entity_registry): fsync parent dir after rename for ext4 durability
arnoldwender Apr 28, 2026
38f7090
fix(compress): drop and recreate mempalace_compressed to avoid HNSW i…
guilhermefriol Apr 29, 2026
759b588
fix(kg): validate ISO-8601 date formats in temporal parameters at MCP…
Giggitycountless Apr 30, 2026
5101f54
fix: add from __future__ import annotations for Python 3.9 compat
Giggitycountless May 2, 2026
3afb800
fix: use Optional[str] instead of PEP 604 union for Python 3.9 compat
Giggitycountless May 3, 2026
5c2490a
fix: serialize ChromaCollection writes through palace lock
imtylervo Apr 27, 2026
5d4860d
fix(chroma): harden HNSW startup preflight
mjc Apr 30, 2026
199b451
fix(repair): rebuild collections through temp staging
mjc Apr 30, 2026
5529870
fix(cli): restore backup on repair failure
mjc Apr 30, 2026
0d0d3d3
fix(repair): close active backend before rollback restore
mjc Apr 30, 2026
1317e59
fix: avoid false hnsw divergence fallback
mjc May 1, 2026
84f1969
fix: use configured collection in recovery paths
mjc May 2, 2026
c187b69
fix(mcp_server): pass embedding_function= on collection reopen (#1299)
igorls May 1, 2026
971b421
fix(mcp): forward valid_to and source params in kg_add/kg_invalidate …
igorls May 3, 2026
19450b7
fix(mcp): case-insensitive agent name in diary_write/diary_read (#1243)
igorls May 3, 2026
157c890
fix(convo-miner): skip Claude Code subagent transcripts by default (#…
mvalentsev May 3, 2026
9af347e
fix(convo-miner): tighten subagent filter per pre-undraft review
mvalentsev May 3, 2026
a494e0d
fix(mcp): always co-locate KG with palace directory
zhapostolski May 3, 2026
2dbbbf8
feat(hooks): SessionStart loads palace context + protocol nudge
zhapostolski May 3, 2026
6c66c6b
feat(hooks): mirror local memory dirs into mempalace on Stop
zhapostolski May 3, 2026
97ab150
fix(hooks): pin wrapper scripts to mempalace venv (MEMPAL_PYTHON)
zhapostolski May 3, 2026
c859ecf
fix(hooks): use correct CLI entry point and harness detection
zhapostolski May 3, 2026
ac4724f
feat(layers): integrate knowledge graph summary into wake-up stack
zhapostolski May 3, 2026
35776ea
feat(hooks): unify KG integration across save and precompact events
zhapostolski May 3, 2026
82980f8
feat(hooks): add multi-agent and semantic workflow hook scripts
zhapostolski May 3, 2026
1dfa514
fix(normalize): restore accidentally deleted content from hooks fix c…
zhapostolski May 3, 2026
342a509
fix(normalize): strip Claude Code <local-command-*> tags and ANSI esc…
lucagentile May 3, 2026
1188a3f
fix(hnsw): force-quarantine segments idle >2h to prevent SIGSEGV on c…
zhapostolski May 4, 2026
ca15d79
fix(hooks): pre-warm ChromaDB at SessionStart + bump Claude Code hook…
zhapostolski May 4, 2026
7fe4eae
fix(hooks): prevent mine deadlock, block_once precompact, KG auto-write
zhapostolski May 4, 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

---

## [3.3.5] — unreleased

### Bug Fixes

- **`mempalace_diary_read` silently dropped entries on agent-name case mismatch.** `tool_diary_write` stored the `agent` metadata verbatim after `sanitize_name`, which preserves case, while `tool_diary_read` filtered by exact match. Writing as `"Claude"` and reading as `"claude"` (or vice-versa) returned zero rows. Both endpoints now lowercase `agent_name` immediately after sanitization, so reads are case-insensitive and the default per-agent wing slug is stable across casings. **Behavior change:** entries written prior to this fix under mixed-case agent names will not match the new lowercase filter; run `mempalace repair` if you need to migrate legacy diary metadata. (#1243)

---

## [3.3.4] — 2026-04-30

### Added
Expand All @@ -19,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Bug Fixes

- **MCP server `tool_diary_write` SIGSEGV when EF default differs.** `mcp_server._get_collection` bypassed `ChromaBackend.get_collection` and called `client.get_collection` / `client.create_collection` without `embedding_function=`. ChromaDB 1.x does not persist the EF identity, so the MCP server silently bound chromadb's `DefaultEmbeddingFunction` while the miner bound `mempalace.embedding.get_embedding_function()`. On bleeding-edge interpreters (python 3.14 + chromadb 1.5.x) this SIGSEGV'd the MCP server on first `col.add()`. `_get_collection` now resolves and passes the EF explicitly. (#1299, follow-up to #1262/#1289)
- **Cross-wing topic tunnels for hyphenated dir names.** `mempalace init` recorded the `topics_by_wing` registry key under the raw directory name (e.g. `mempalace-public`), while `mempalace.yaml`'s `wing` field used the lower-cased + separator-collapsed slug (`mempalace_public`). At mine time the miner read the slug from the yaml and missed the registry, so `_compute_topic_tunnels_for_wing` returned `0` silently. Real-world: any project whose folder contained a hyphen or space lost every topic tunnel. Producer side: `cmd_init`, `room_detector_local`, `miner.load_config` no-yaml fallback, and `convo_miner` now all route through a shared `normalize_wing_name()` in `config.py` so future writes use the same key. Lookup side: `palace_graph.create_tunnel`, `list_tunnels`, `follow_tunnels`, and `find_tunnels` normalize incoming wing names too, so existing palaces with raw-name keys on disk also recover. (#1194, #1195, #1197, follow-up to #1180)
- **HNSW index bloat from repeated resize+persist cycles.** ChromaDB's HNSW segment was growing into the tens of GB on palaces past ~15K drawers because `link_lists.bin` was being re-allocated on every flush. Setting `hnsw:batch_size` and `hnsw:sync_threshold` on collection metadata via the new `_HNSW_BLOAT_GUARD` constant pins the segment to one allocation per batch instead. Empirical: a fresh 39,792-drawer palace went from 30 GB on disk and segfaulting `mempalace status` to 376 MB and instant. Migration note — already-bloated palaces still need a `mempalace repair` or full re-mine; HNSW config is honoured at collection-create time only. (#1191, supersedes #346)
- **`max_seq_id` poisoning from old `_fix_blob_seq_ids` shim.** The 0.6.x → 1.5.x BLOB-to-INTEGER migration was running `int.from_bytes(blob, 'big')` over chromadb 1.5.x's native `b'\x11\x11' + ASCII-digit` `max_seq_id` format, yielding ~1.23e18 integers that silently suppressed every subsequent `embeddings_queue` write for the affected segment. The shim is now narrowed to the `embeddings` table only, with an additional defense-in-depth guard that skips sysdb-10-prefixed BLOBs even there. New `mempalace repair --mode max-seq-id` un-poisons existing palaces either from a pre-corruption sidecar DB (exact restore) or heuristically (`MAX(embeddings.seq_id)` over the owning collection). (#1135)
Expand Down
41 changes: 12 additions & 29 deletions hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,23 @@ The `stop_hook_active` flag prevents infinite loops: block once → AI saves →
```
Context window getting full → Claude Code fires PreCompact
↓
Find transcript (from input or session_id lookup)
↓
Auto-mine transcript → palace (tool output captured)
↓
{"decision": "block", "reason": "save tool output verbatim..."}
Hook may block (configurable)
↓
AI saves everything
↓
Compaction proceeds
```

#### PreCompact behavior modes

`mempal_precompact_hook.sh` supports `MEMPAL_PRECOMPACT_MODE`:

- `block_once` — block once per session (default for non-Claude harnesses)
- `block` — always block
- `proceed` — never block (default when the harness is detected as Claude/Claude Code)

The Claude default is `proceed` because some Claude setups don’t automatically retry compaction after a block, which can prevent compaction from happening at all.

No counting needed — compaction always warrants a save. The auto-mine captures raw tool output before the AI gets a chance to summarize it away.

## Debugging
Expand Down Expand Up @@ -155,27 +161,4 @@ export MEMPAL_PYTHON="/usr/bin/python3" # system Python is fin
export MEMPAL_PYTHON="$HOME/.venvs/mempalace/bin/python" # or your venv
```

Resolution priority: `$MEMPAL_PYTHON` (if set and executable) → `$(command -v python3)` → bare `python3`. The interpreter only needs `json` and `sys` from the standard library — `mempalace` itself does not need to be installed in it.

Note: the `mempalace mine` auto-ingest runs via the `mempalace` CLI, so that command also needs to be on the hook's `PATH`. Installing with `pipx install mempalace` or `uv tool install mempalace` puts it on a stable global location; otherwise extend the hook environment's `PATH` to include your venv's `bin/`.

## Backfill Past Conversations

The hooks only capture conversations going forward. To mine **past** Claude Code sessions into your palace, run a one-time backfill:

```bash
mempalace mine ~/.claude/projects/ --mode convos
```

This scans all JSONL transcripts from previous sessions and files them into the `conversations` wing. On a typical developer machine with months of history, this can yield 50K–200K drawers.

For Codex CLI sessions:
```bash
mempalace mine ~/.codex/sessions/ --mode convos
```

This only needs to be done once — after that, the hooks auto-mine each session as you go.

## Cost

**Zero extra tokens.** The hooks notify the AI that saves happened in the background — the AI doesn't need to write anything in the chat. All filing is handled automatically. Previous versions asked the AI to write diary entries and drawer content in the chat window, which cost ~$1/session in retransmitted tokens.
Resolution priority: `$MEMPAL_PYTHON` (if set and executable) → `python3` from the hook's `PATH`.
82 changes: 82 additions & 0 deletions hooks/mempal_enrich_knowledge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash
# MEMPALACE KNOWLEDGE ENRICHMENT HELPER
# Add structured facts to MemPalace knowledge graph for better querying
#
# Usage: ./mempal_enrich_knowledge.sh "subject" "predicate" "object" [--valid-from DATE] [--source-file FILE]
#
# Examples:
# ./mempal_enrich_knowledge.sh "ananas-ai" "is_project_of" "ananas-platform" --valid-from "2026-01-01"
# ./mempal_enrich_knowledge.sh "ai-marketing" "uses_mcp" "algolia" --source-file "~/projects/ai-marketing/algolia.ts"

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MEMPALACE_SRC="$(dirname "$SCRIPT_DIR")"

# Check minimum arguments
if [[ $# -lt 3 ]]; then
echo "Error: Subject, predicate, and object are required"
echo "Usage: $0 \"subject\" \"predicate\" \"object\" [--valid-from DATE] [--source-file FILE]"
exit 1
fi

SUBJECT="$1"
PREDICATE="$2"
OBJECT="$3"
shift 3

# Parse optional arguments
VALID_FROM=""
SOURCE_FILE=""

while [[ $# -gt 0 ]]; do
case "$1" in
--valid-from)
VALID_FROM="$2"
shift 2
;;
--source-file)
SOURCE_FILE="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done

# Create temporary Python script
TMP_PY=$(mktemp)
cat > "$TMP_PY" << EOF
import sys
sys.path.insert(0, '$MEMPALACE_SRC')
from mempalace.knowledge_graph import KnowledgeGraph
import json
import os

subject = '$SUBJECT'
predicate = '$PREDICATE'
object = '$OBJECT'
valid_from = '$VALID_FROM' if '$VALID_FROM' else None
source_file = '$SOURCE_FILE' if '$SOURCE_FILE' else None

# Expand ~ in source_file
if source_file and source_file.startswith('~/'):
source_file = os.path.expanduser(source_file)

try:
kg = KnowledgeGraph()
triple_id = kg.add_triple(subject, predicate, object,
valid_from=valid_from,
source_file=source_file)
print(json.dumps({'success': True, 'triple_id': triple_id}, indent=2))
except Exception as e:
print(json.dumps({'success': False, 'error': str(e)}, indent=2))
sys.exit(1)
EOF

python3 "$TMP_PY"
EXIT_CODE=$?
rm -f "$TMP_PY"
exit $EXIT_CODE
100 changes: 100 additions & 0 deletions hooks/mempal_opencode_hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/bin/bash
# MEMPALACE HOOK FOR OPENCODE
# Adapted from Claude Code hooks for use with OpenCode AI
#
# Install: Add to OpenCode's MCP config or create a custom command
# For OpenCode MCP integration, add to opencode.json:
# {
# "mcp": {
# "mempalace": {
# "type": "local",
# "command": "/home/zapostolski/.mempalace/src/hooks/mempal_opencode_hook.sh",
# "args": ["stop"]
# }
# }
# }
#
# Or run manually: echo '{}' | ./mempal_opencode_hook.sh stop

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
STATE_DIR="$HOME/.mempalace/hook_state"
SAVE_INTERVAL=10
SESSION_ID="opencode-$(date +%Y%m%d-%H%M%S)"

mkdir -p "$STATE_DIR"

log() {
echo "[$(date '+%H:%M:%S')] $1" >> "$STATE_DIR/hook.log"
}

run_save() {
log "OPENCODE SAVE TRIGGERED for session $SESSION_ID"

# Output block reason to trigger save in OpenCode
cat << 'HOOKJSON'
{
"decision": "block",
"reason": "AUTO-SAVE checkpoint. Save all new decisions, findings, milestones, blockers, architecture changes, code changes, and next steps from this session to your memory system. Be thorough - organize into wings/rooms. After saving, continue the conversation."
}
HOOKJSON
}

run_precompact() {
log "OPENCODE PRE-COMPACT triggered"

cat << 'HOOKJSON'
{
"decision": "block",
"reason": "COMPACTION IMMINENT. Save ALL topics, decisions, quotes, code, and context from this session to memory. After compaction detailed context will be lost. Save everything, then allow compaction."
}
HOOKJSON
}

handle_stop() {
local since_last="${1:-0}"

if [ "$since_last" -ge "$SAVE_INTERVAL" ]; then
run_save
else
log "OpenCode: $since_last exchanges since last save (threshold: $SAVE_INTERVAL)"
echo '{}'
fi
}

increment_counter() {
local counter_file="$STATE_DIR/opencode_counter"
local count=0
if [ -f "$counter_file" ]; then
count=$(cat "$counter_file" 2>/dev/null || echo 0)
fi
count=$((count + 1))
echo "$count" > "$counter_file"
echo "$count"
}

main() {
local hook_type="${1:-stop}"
INPUT=$(cat)

log "OpenCode hook triggered: $hook_type"

case "$hook_type" in
stop)
local count
count=$(increment_counter)
handle_stop "$count"
;;
precompact)
run_precompact
;;
*)
log "Unknown hook type: $hook_type"
echo '{}'
;;
esac
}

main "$@"
77 changes: 77 additions & 0 deletions hooks/mempal_opencode_simple.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/bash
# MEMPALACE OPENCODE HOOK (No Python dependencies required)
# This hook logs checkpoints and can be run alongside mempalace's Codex/Claude hooks
#
# Usage:
# 1. Run manually after important sessions: ./mempal_opencode_simple.sh save
# 2. Or integrate via OpenCode custom commands
#
# This creates checkpoint files that can be mined later by mempalace

set -euo pipefail

STATE_DIR="$HOME/.mempalace/hook_state"
CHECKPOINT_DIR="$STATE_DIR/opencode_checkpoints"
SESSION_ID="opencode-$(date +%Y%m%d-%H%M%S)"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

mkdir -p "$STATE_DIR" "$CHECKPOINT_DIR"

log() {
echo "[$(date '+%H:%M:%S')] OPENCODE: $1" >> "$STATE_DIR/hook.log"
}

create_checkpoint() {
local checkpoint_file="$CHECKPOINT_DIR/${SESSION_ID}.md"
local notes="${1:-}"

log "Creating checkpoint: $SESSION_ID"

cat > "$checkpoint_file" << EOF
# OpenCode Session Checkpoint

**Session:** $SESSION_ID
**Timestamp:** $TIMESTAMP
**User:** Zharko Apostolski

## Session Notes
$notes

## Key Topics Discussed

## Decisions Made

## Code Changes

## Next Steps

---
*Created by mempalace OpenCode hook*
EOF

echo "Checkpoint saved: $checkpoint_file"
log "Checkpoint created: $checkpoint_file"
}

main() {
local command="${1:-save}"

case "$command" in
save)
log "Save command received"
create_checkpoint "Manual checkpoint from OpenCode session"
;;
status)
log "Status check"
echo "OpenCode mempalace hook status:"
echo " Checkpoints: $(ls -1 "$CHECKPOINT_DIR" 2>/dev/null | wc -l)"
echo " Last: $(ls -t "$CHECKPOINT_DIR" 2>/dev/null | head -1)"
;;
*)
echo "Usage: $0 {save|status}"
exit 1
;;
esac
}

main "$@"
Loading