Commit Graph

742 Commits

Author SHA1 Message Date
Jeffrey Hein df3ee289fc fix: add wing param to diary_write/diary_read, derive from transcript path (#659)
* fix: add wing param to diary_write/diary_read, derive from transcript path

Without a wing override, all diary entries from the stop hook land in
wing_session-hook regardless of which project the session is in, making
per-project diary search impossible.

- tool_diary_write(): add optional `wing` param; sanitize and use it when
  provided, fall back to wing_{agent_name} when omitted
- tool_diary_read(): add optional `wing` param for filtering by target wing
- TOOLS dict: expose `wing` in input_schema for both diary tools
- hooks_cli: add _wing_from_transcript_path() helper that extracts the
  project name from Claude Code paths like
  ~/.claude/projects/-home-jp-Projects-kiyo-xhci-fix/... → kiyo-xhci-fix
- hook_stop: derive project wing and append wing= hint to block reason so
  Claude writes diary entries to the correct per-project wing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: sanitize wing param, cross-platform paths, tighten test assertions

Addresses Copilot review feedback on #659.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: wing_ prefix + agent filter on diary_read

Addresses bensig's 2-issue review on this PR.

1. _wing_from_transcript_path() was returning bare project names
   (e.g. "myproject") while all existing wings follow the wing_*
   convention from AAAK_SPEC. Entries landed in wing="myproject"
   while diary_read defaulted to wing="wing_<agent_name>" —
   orphaning every diary entry written by the stop hook. Now
   returns "wing_<project>" and falls back to "wing_sessions".

2. tool_diary_read() did not include agent_name in the ChromaDB
   where filter when a custom wing was provided — any caller with
   a shared wing could read entries written by other agents.
   Add {"agent": agent_name} to the $and clause. Also flagged by
   Qudo and left unresolved until now.

Tests updated to expect the wing_ prefix (6 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 15:07:25 -07:00
Ben Sigman 818b7f4d48 Merge pull request #1118 from MemPalace/ben/crystal-lattice-brand
feat(website): Crystal Lattice brand — fonts, palette, hero copy
2026-04-22 22:15:40 -07:00
bensig 4f00390335 feat(website): apply Crystal Lattice brand — fonts, palette, hero copy
- Swap Inter/Cormorant Garamond/Geist → Neue Machina/Satoshi/Onest (all free/web)
- Align color palette to Crystal Lattice decision (0002): void #080C18, cyan-vivid #38BDF8, ice #DBE7F5
- Update hero: "Memory *is* identity." with italic blue "is", white "identity"
- New hero subtext: "Every conversation, every idea, every small decision… held somewhere safe."
- JetBrains Mono unchanged (already OFL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 20:24:39 -07:00
wahajahmed010 b595cb3978 docs: fix HOOKS_TUTORIAL.md paths, matcher, and missing timeout
Fixes four issues causing silent hook failures:

1. **Relative paths** → Absolute paths (/absolute/path/to/hooks/...)
   Claude Code resolves hooks from working directory, not repo root.

2. **Wrong matcher** → Stop uses *, PreCompact has no matcher
   PreCompact doesn't use matcher (only Stop hooks do).

3. **Missing timeout** → Added timeout: 30 to both hooks
   Matches hooks/README.md specification.

4. **Ambiguous target** → Specified ~/.claude/settings.local.json
   Clarified global vs project-scoped config.

Also added executable chmod instructions and path replacement note.

Fixes #1037
2026-04-22 11:31:40 +02:00
Ben Sigman 9b35d9f760 Merge pull request #661 from jphein/perf/graph-cache
Re-reviewed: both requested changes addressed (threading.Lock on cache globals, col-ignoring documented). Merging.
2026-04-21 17:38:31 -07:00
Ben Sigman 23ee2a02fd Merge pull request #673 from jphein/feat/deterministic-hook-save
Clean squash by jphein on 2026-04-21. Backwards-compatible via hook_silent_save config flag. Save marker now only advances after confirmed write — strictly safer than status quo.
2026-04-21 17:38:21 -07:00
Ben Sigman 02aafc0715 Merge pull request #1021 from jphein/upstream-fix/silent-save-visibility
silent_guard default fixed to True per 2026-04-19 review. ImportError/AttributeError handling narrowed. CI all green.
2026-04-21 17:38:13 -07:00
Ben Sigman 810f9a56c4 Merge pull request #851 from vnguyen-lexipol/fix/status-paginate-large-palaces
Merging to fix active pagination crash — 3 users hit this on v3.3.2 in the last 20 hours (54K, 114K, 465K drawer palaces). None-guard regression fixed. 9-day production soak on 165K-drawer palace confirms stability.
2026-04-21 17:38:04 -07:00
jp 74e9cbcfd3 feat: deterministic hook saves — zero data loss via silent Python API
Adds a `hook_silent_save` mode (default `true` in new installs) where
the stop and precompact hooks write diary entries directly via the
Python API — no AI block, no MCP tool roundtrip, no possibility of the
AI forgetting or ignoring the save instruction.

**Two modes, controlled by `hook_silent_save` in `~/.mempalace/config.json`:**

1. **Silent mode** (default): Direct call to `tool_diary_write()`. Plain
   text, no AI involved, deterministic. Save marker advances only after
   the write is confirmed, so mid-save failures do not lose exchanges.
   Shows `"✦ N memories woven into the palace"` as a systemMessage
   notification so the user knows the save fired.

2. **Block mode** (legacy): Returns `{"decision": "block"}` asking the
   AI to call the MCP tool chain. Non-deterministic — the AI may ignore,
   summarize lossy, or fail. Kept for backward compatibility.

**Extras rolled in:**
- Block reasons name "MemPalace" explicitly and instruct the AI not to
  write to Claude Code's native auto-memory (.md files) — prevents the
  two memory systems from stepping on each other.
- Codex transcript handling (`event_msg` payloads) in
  `_count_human_messages` + `_extract_recent_messages`.
- Tightened stopword leak in diary summaries; docstring polish; test
  hermeticity fixes (per-test `STATE_DIR` patching).

**Tests:** hooks_cli tests cover silent-save path, save-marker
advancement after confirmed write only, and systemMessage formatting.

Rebased fresh on upstream/develop. Only touches files germane to the
feature (hooks_cli.py, tests, hooks/README.md, HOOKS_TUTORIAL.md) —
stale fork-local `.sh` wrapper and plugin manifest changes dropped.
2026-04-21 13:20:52 -07:00
Igor Lins e Silva 1b00f93b37 Merge pull request #833 from MemPalace/fix/hooks-python-resolution
fix(hooks): real python-resolution for .sh hooks, with MEMPAL_PYTHON override
2026-04-21 01:47:50 -03:00
Igor Lins e Silva 48eb6271a7 fix(hooks): MEMPAL_PYTHON override for .sh hooks' internal python3 calls
The legacy hook scripts `hooks/mempal_save_hook.sh` and
`hooks/mempal_precompact_hook.sh` shell out to `python3` for JSON
parsing and transcript-message counting. On macOS GUI launches of
Claude Code — `open -a`, Spotlight, the dock — the harness inherits
`PATH` from launchd (`/usr/bin:/bin:/usr/sbin:/sbin`), which may not
contain a `python3` at all, or may contain only a system Python that
lacks what the hook needs. The hook then fails silently in the
background log where users never look.

`mempalace` auto-ingest itself is unaffected — #340 switched that
path to the `mempalace` CLI entry point, which pipx/uv install on a
stable global PATH.

This PR adds a `MEMPAL_PYTHON` environment variable that users can
set to point the hook at any Python 3 interpreter. Resolution order
applied at each `python3` invocation site inside the two hooks:

  1. $MEMPAL_PYTHON (if set and executable)
  2. $(command -v python3) on PATH
  3. bare `python3` as a last resort

The interpreter does not need `mempalace` installed in it — only the
standard-library `json` and `sys` modules. The hook's `mempalace mine`
call runs via the CLI, independent of this override.

hooks/README.md documents the macOS GUI PATH issue and the
MEMPAL_PYTHON override. tests/test_hooks_shell.py adds 3 regression
tests (Linux/macOS only, POSIX bash):

  - MEMPAL_PYTHON override wins over PATH (proved via a
    marker-emitting shim that proxies to the real interpreter).
  - Non-executable MEMPAL_PYTHON falls back to PATH rather than
    crashing on permission denied.
  - Unset MEMPAL_PYTHON resolves via PATH.

`hooks_cli.py` (the Python implementation invoked via
`mempalace hook run ...`) already uses `sys.executable` and is
therefore trivially correct — no changes needed there.

Supersedes abandoned branch `fix/hook-bugs`.

Co-Authored-By: MSL <232237854+milla-jovovich@users.noreply.github.com>
2026-04-21 01:43:08 -03:00
Igor Lins e Silva 5522d348a5 Merge pull request #340 from messelink/fix/mcp-pipx-compat
fix: add mempalace-mcp entry point for pipx/uv compatibility
2026-04-21 01:36:51 -03:00
Pim Messelink 9e53228ea3 test: update test_cli assertions for mempalace-mcp entry point
Three assertions in test_mcp_command_* were still checking for the old
`python -m mempalace.mcp_server` output string. Update to match the new
`mempalace-mcp` command printed by cmd_mcp().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 01:26:47 -03:00
Pim Messelink 982d421510 fix: update mempalace mcp command to use mempalace-mcp entry point
cmd_mcp() in cli.py was still printing `python -m mempalace.mcp_server`
as the setup command. Update to use the mempalace-mcp console entry
point added in the previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 01:26:47 -03:00
Pim Messelink 67a067701e fix: use mempalace CLI in top-level hook scripts
hooks/mempal_precompact_hook.sh and hooks/mempal_save_hook.sh used
python3 -m mempalace mine which fails when mempalace is installed via
pipx or uv. Switch to the mempalace CLI entry point which pipx/uv put
on PATH. Also removes the now-unused PYTHON variable from mempal_save_hook.sh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 01:26:47 -03:00
Pim Messelink be89e49add fix: use mempalace CLI in hook scripts instead of python3 -m
Hook scripts used `python3 -m mempalace` which fails when mempalace is
installed via pipx or uv. Using the `mempalace` CLI command directly
works for all installation methods. Dev users running from source should
use `pip install -e .` as documented in CONTRIBUTING.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 01:26:35 -03:00
Pim Messelink 9f5b8f5fd6 fix: add mempalace-mcp console entry point for pipx/uv compatibility
The MCP server config used `python -m mempalace.mcp_server` which fails
when mempalace is installed via pipx or uv, since the system python
cannot find the module in the isolated venv. Adding a `mempalace-mcp`
console_scripts entry point ensures the MCP server works regardless of
installation method (pip, pipx, uv, conda).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 01:26:00 -03:00
Igor Lins e Silva 4fb0ee57e7 Merge pull request #942 from fatkobra/fix-hooks-resolve-claude-plugin
fix(hooks): resolve Claude plugin hook runner cross-platform
2026-04-21 01:21:36 -03:00
Igor Lins e Silva 1a180cd7cb Merge pull request #1051 from itfarrier/feat/i18n-belarusian
feat(i18n): add Belarusian
2026-04-21 01:09:54 -03:00
Igor Lins e Silva 6d42f61e64 Merge pull request #1001 from mvalentsev/feat/i18n-de-es-fr-entity
feat(i18n): add entity detection to German, Spanish, and French locales
2026-04-21 00:55:33 -03:00
Igor Lins e Silva 2a5914b630 Merge pull request #945 from lmanchu/feat/zh-entity-detection
feat(i18n): add Traditional + Simplified Chinese entity detection
2026-04-21 00:55:04 -03:00
Igor Lins e Silva cf0477bfa7 Merge pull request #1052 from MemPalace/merge/main-into-release-3.3.2
release: merge main into release/3.3.2
2026-04-20 15:34:24 -03:00
Igor Lins e Silva 936f1866af release: merge main into release/3.3.2 — keep 3.3.2 version bumps
Conflicts resolved by taking the 3.3.2 side for all version files:
- pyproject.toml, mempalace/version.py (3.3.2)
- .claude-plugin/marketplace.json, .claude-plugin/plugin.json (3.3.2)
- .codex-plugin/plugin.json (3.3.2)
- README.md version badge (3.3.2)
- uv.lock (3.3.2)
- CHANGELOG.md keeps [3.3.2] section on top of main's [3.3.1]

No source-code conflicts; main's 3.3.1 commit footprint is already in
develop's history via the earlier sync boundaries.

1033 tests pass on the merged tree.
2026-04-20 15:21:08 -03:00
Igor Lins e Silva 04e11ae545 Merge pull request #1045 from MemPalace/fix/copilot-review-release-3.3.2
fix: address Copilot review on release/3.3.2
2026-04-20 15:16:20 -03:00
Dzmitry Padabed 54c314d8d9 feat(i18n): add Belarusian 2026-04-20 21:00:39 +03:00
Igor Lins e Silva 65b17a6e0f fix: address Copilot review on release/3.3.2
Non-ASCII glyphs (regression of the #681 class of Windows UnicodeEncodeError):
- mempalace/cli.py: "✗" → "ERROR:", "⚠" → "WARNING:", em dash → "-"
- mempalace/sweeper.py: "⚠" → "WARNING:"

Backend arg validation:
- mempalace/backends/chroma.py: `_normalize_get_collection_args` now
  raises TypeError on unexpected trailing positional args instead of
  silently dropping them — surfaces call-site bugs early.

Docs site:
- website/.vitepress/config.mts: gate Google Analytics scripts behind
  MEMPALACE_DOCS_GA_ID env var (default off). Self-hosters no longer
  get GA injected unconditionally.

Landing page SPA hygiene:
- website/.vitepress/theme/landing/useLandingEffects.js: collect all
  IntersectionObserver disconnects and removeEventListener thunks in a
  shared `cleanups` registry; drain it in `onBeforeUnmount` so observers
  and form/replay listeners don't leak across SPA navigations.
2026-04-19 18:19:28 -03:00
Igor Lins e Silva 5e9451407f release: v3.3.2
Version bumps across pyproject.toml, mempalace/version.py, README badge,
uv.lock, and plugin manifests (.claude-plugin/*, .codex-plugin/*).

CHANGELOG aligned with main (post-3.3.1) and a new [3.3.2] section added
covering the 11 PRs merged on develop since v3.3.1 — silent-transcript-drop
fix + tandem sweeper (#998), None-metadata guards (#999, #1013),
chromadb ≥1.5.4 for Py 3.13/3.14 (#1010), Windows Unicode (#681),
HNSW quarantine recovery (#1000), PID stacking guard (#1023), doc-path
cleanup (#996, #1012), and RFC 001/002 internal scaffolding (#995, #1014, #990).
2026-04-19 16:55:25 -03:00
jp d657626736 style: ruff format — collapse AttributeError log call to single line 2026-04-19 08:34:43 -07:00
jp 2629ae5b71 fix(hooks): default silent_guard=True — config-read failure must not suppress saves
Addresses bensig's review on PR #1021.

silent_guard was initialized to False, so when both MempalaceConfig
import and .hook_silent_save attribute access failed, silent_guard
stayed False. Then `if not silent_guard:` fired and returned empty —
silently dropping the save. In silent mode (the default since v3.3.0),
saves should ALWAYS proceed on config-read failure. Changing the
initial value to True makes that the safe default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:23:02 -07:00
fatkobra 0b316d4053 test: normalize wrapper script path for bash on Windows 2026-04-19 10:34:11 +02:00
Vu Nguyen 5d2da04bcd Merge remote-tracking branch 'upstream/develop' into fix/status-paginate-large-palaces
# Conflicts:
#	mempalace/miner.py
2026-04-19 02:02:28 -05:00
Vu Nguyen 3004ac4ee4 fix(miner): port None-metadata guard into paginated status loop
Upstream develop commit feba7e8 (2026-04-18) added `m = m or {}` to the
single-shot `for m in metas:` loop after this branch already rewrote
status() to paginate. Without porting the guard forward, merging this PR
would silently drop jp's fix and crash again on palaces with null-metadata
drawers.

Addresses bensig's review on #851.
2026-04-19 01:35:24 -05:00
Ben Sigman 32ec74d8eb Merge pull request #1023 from jphein/pr/pid-file-guard
fix(hooks): PID file guard prevents stacking mine processes
2026-04-18 23:33:47 -07:00
Ben Sigman caf503f442 Merge pull request #1000 from jphein/fix/quarantine-stale-hnsw
feat(backends): quarantine_stale_hnsw — recover from HNSW/sqlite drift (closes #823)
2026-04-18 23:28:00 -07:00
Ben Sigman 62439e1368 Merge pull request #681 from jphein/fix/unicode-checkmark
fix: replace Unicode checkmark with ASCII for Windows encoding (#535)
2026-04-18 23:27:57 -07:00
jp dfba247454 fix: cross-platform PID check — os.kill(pid, 0) TERMINATES on Windows
Real bug surfaced on CI for this PR. On POSIX, os.kill(pid, 0) is
the canonical no-op existence probe. On Windows, Python's os.kill
maps to TerminateProcess(handle, sig), which *terminates* the target
with exit code sig. os.kill(pid, 0) therefore kills the target with
exit code 0 — silently destroying our mine child (or, as happened
in test_mine_already_running_live_pid, the pytest process itself).

Fix: split into _pid_alive(pid) helper with a Windows branch using
ctypes.windll.kernel32.OpenProcess + GetExitCodeProcess.
PROCESS_QUERY_LIMITED_INFORMATION opens a handle only if the PID
exists; STILL_ACTIVE (259) distinguishes running from exited processes.

No new dependencies — stdlib ctypes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:30:17 -07:00
jp fe6b8899bc fix: broaden _mine_already_running catch — Windows os.kill raises plain OSError
On Windows, os.kill(bogus_pid, 0) raises OSError[WinError 87]
"The parameter is incorrect" — NOT ProcessLookupError. The old
except tuple missed it, so test_mine_already_running_dead_pid
failed on Windows CI.

Catching OSError covers ProcessLookupError + PermissionError +
FileNotFoundError on POSIX and WinError 87 on Windows. ValueError
still guards the int() parse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:13:37 -07:00
jp a6b6e55247 fix: PID file guard prevents stacking mine processes
Every stop hook fire spawned a new background `mempalace mine` via
subprocess.Popen with no dedup — 4 concurrent mines at ~770% CPU
observed in production. Add `_mine_already_running()` (reads
`hook_state/mine.pid`, uses `os.kill(pid, 0)` as an existence check)
and `_spawn_mine()` (writes the child PID to the lock file after
Popen returns). `_maybe_auto_ingest` bails early when the guard
reports True.

Tests: 4 new unit tests for `_mine_already_running` (no file, dead
PID, live PID using `os.getpid()`, corrupt file), 1 new test
covering the skip-when-running branch of `_maybe_auto_ingest`, and
existing spawn tests patched to redirect `_MINE_PID_FILE` into
tmp_path so they don't touch the real state dir.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 20:27:56 -07:00
jp 2183d866f3 style(hooks): ruff format hooks_cli.py and test_hooks_cli.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 18:09:29 -07:00
jp 1531a253be test: add missing import os in test_hooks_cli
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 18:05:18 -07:00
jp 5deb815f0b fix(hooks): address Copilot review feedback on #1021
- _output(): use sys.modules.get() instead of unconditional import to
  avoid triggering mcp_server's stdout redirect as a side effect
- _output(): write-all loop for os.write() to handle partial writes and
  EINTR; fall back to sys.stdout.buffer on OSError
- _output() docstring: remove inaccurate _save_diary_direct reference
- stop_hook_active guard: narrow except to ImportError/AttributeError,
  default silent_guard=False (safe: preserves block-mode loop prevention
  when config load fails) and log a warning instead of silently changing
  behavior
- tests: two new regression tests covering the real-stdout-fd path and
  the fd-1 fallback path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 18:05:18 -07:00
jp 6a3a5c7a3d fix(hooks): write hook JSON to real stdout, bypassing mcp_server redirect
mempalace.mcp_server redirects stdout → stderr at module-level import
(both Python-level and fd-level via os.dup2) to protect the MCP stdio
protocol from ChromaDB's C-level noise. Silent-save imports mcp_server
transitively via _save_diary_direct, so by the time _output() calls
print(), sys.stdout is actually stderr.

Claude Code reads hook output from fd 1. With the redirect in effect,
fd 1 points to fd 2, so our {"systemMessage": "✦ N memories woven..."}
JSON lands on stderr and Claude Code never renders it. The save still
happens, the marker still advances — the user just never sees the
beautiful checkpoint notification in their terminal.

Fix: _output() now writes to _REAL_STDOUT_FD (saved by mcp_server before
the redirect) via os.write(), falling back to sys.stdout only when the
saved fd is unavailable (e.g., hooks_cli imported without mcp_server).

Test: bash hook script 2>/dev/null now shows only the JSON;
2>&1 >/dev/null shows only the Diary entry log line — clean separation
restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:05:18 -07:00
jp 914945637c fix(hooks): honor silent_save when stop_hook_active is set
Claude Code 2.1.114 passes stop_hook_active:true on every Stop fire
after the first in a session (plugin-dispatched hooks in particular).
The legacy guard at line 426 was written for block-mode, where a
re-fire with the flag set meant "you already blocked, don't block
again" — correct loop prevention when the hook returns
{"decision":"block"}.

Silent-save mode (default since #673) never blocks — it saves
directly and returns. The flag is meaningless there, so the old
guard was suppressing every auto-save after the first one in a
Claude Code session. Symptom: terminal never shows the "✦ N
memories woven" notification again, hook.log stays silent, save
marker stuck.

Fix: only skip on stop_hook_active when block mode is configured.
Silent mode runs through as normal — the save is deterministic and
idempotent, no loop risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:05:18 -07:00
jp 0c38deaab5 feat(backends): quarantine_stale_hnsw — recover from HNSW/sqlite drift
Add a helper that renames HNSW segment directories whose
`data_level0.bin` is significantly older than `chroma.sqlite3`. Drift
between the on-disk HNSW graph and the live embeddings table is the
root cause of a segfault class where the Rust graph-walk dereferences
dangling neighbor pointers for entries in the metadata segment that no
longer exist in the HNSW index, crashing in a background thread on
`count()` or `query()`.

Issue #823 describes the same drift as a silent-staleness symptom
(semantic search returns stale results after `add_drawer` because
`data_level0.bin` lags the sqlite metadata under the default
`sync_threshold=1000`). Under heavier load or after an interrupted
write, the same drift can escalate from "silent stale results" to
"SIGSEGV on next open," which is the failure mode observed at
neo-cortex-mcp#2 (chromadb 1.5.5, Python 3.12) and acknowledged at
chroma-core/chroma#2594.

On one 135K-drawer palace where `index_metadata.pickle` claimed 137,813
elements against 135,464 rows in sqlite (2,349-entry drift), fresh
Python processes crashed in `col.count()` 17/20 times; after renaming
the segment dir out of the way and letting ChromaDB rebuild lazily, the
same 20-run check went to 0 crashes.

The recovery path #823 suggests (export / recreate / reimport) is heavy
— it re-embeds every drawer. This helper is lighter: rename the segment
dir so ChromaDB reopens without it, and the indexer rebuilds lazily on
the next write. The original directory is renamed (not deleted) so the
operator can recover if the heuristic misfires.

If `chroma.sqlite3` is more than `stale_seconds` (default 3600) newer
than the segment's `data_level0.bin`, the segment is considered
suspect. One hour is deliberately conservative — normal HNSW flush
cadence is seconds to minutes, so an hour of drift implies a crashed
mid-write, not routine lag.

- Additive: exposes `quarantine_stale_hnsw(palace_path, stale_seconds)`
  as a helper. Not wired into `_client()` / startup on this PR — the
  goal is to land the primitive first so operators and higher layers
  can opt in. A follow-up could call it automatically on palace open
  behind an env var or config flag.
- Closes #823 by giving operators a first-class recovery path without
  having to install `chromadb-ops` or re-mine.

Four new tests in `tests/test_backends.py`:
- renames drifted segment, preserves original files under `.drift-TS` suffix
- leaves fresh segments alone
- no-op on missing palace path / missing `chroma.sqlite3`
- skips already-quarantined (`.drift-` suffixed) directories

`pytest tests/test_backends.py` → 11 passed. `ruff check` / `ruff format
--check` — clean.
2026-04-18 18:04:05 -07:00
Igor Lins e Silva 109d7f267c Merge pull request #990 from MemPalace/docs/rfc-source-adapter-plugin-spec
docs: RFC 002 — Source adapter plugin specification
2026-04-18 19:12:20 -03:00
Igor Lins e Silva 66090b2bcb Merge pull request #1014 from MemPalace/refactor/rfc-002-sources-scaffolding
refactor(sources): RFC 002 §9 scaffolding — BaseSourceAdapter, registry, PalaceContext
2026-04-18 18:44:52 -03:00
Igor Lins e Silva 8a130fc509 Merge pull request #1012 from MemPalace/docs/use-real-claude-projects-path-996
docs: use real ~/.claude/projects/ path in first-run help and README (#996)
2026-04-18 18:43:53 -03:00
Igor Lins e Silva 7af3bfae8f Merge pull request #1010 from MemPalace/fix/chromadb-1-5-4-py-3-13-compat-via-581
fix: upgrade chromadb to >=1.5.4 for Python 3.13/3.14 compatibility + fix 1.5.x queue-stall (closes #1006)
2026-04-18 18:43:34 -03:00
Ben Sigman 64266695b5 Merge pull request #1013 from MemPalace/fix/layer3-search-raw-none-guard-1011
fix: guard Layer3.search_raw against None doc/meta from ChromaDB (#1011)
2026-04-18 13:41:55 -07:00
Ben Sigman 1b89b49b78 Merge pull request #999 from jphein/fix/searcher-none-metadata
fix(searcher): guard against None metadata in CLI print path
2026-04-18 13:41:52 -07:00