Commit Graph

20 Commits

Author SHA1 Message Date
raman325 2f000ad4b4
Merge branch 'develop' into feat/hermes-provider-core
* develop: (45 commits)
  ci: retry transient Chroma reader initialization failure
  fix(encoding): recover undefined CP1252 continuation bytes
  fix(encoding): make repair conservative and reversible
  fix(windows): add legacy encoding repair tool
  feat(searcher): wire i18n stop words into BM25 tokenizer (#973)
  test: expect dry_run=False on rebuild-index alias call
  fix(repair): honor --dry-run for repair --mode from-sqlite
  fix(hooks): ingest only the active transcript
  fix(embedding): remap unsupported EmbeddingGemma token IDs
  fix(convos): honor mined state during dry runs
  fix: harden release polish for bot findings and search errors
  chore(release): 3.7.0
  fix: reopen immutable readers and clear identity on promote
  fix: address backend ownership edge cases
  fix: serialize SQLite writes before palace lease
  fix: take mine-lock before archive in repair --mode from-sqlite
  fix(chroma): adopt chromadb's own HNSW write defaults
  fix: retry transient MCP ownership failures
  fix: address MCP ownership review feedback
  fix: address single-writer review feedback
  ...

# Conflicts:
#	mempalace/convo_miner.py
#	tests/test_convo_miner.py
2026-08-05 11:37:54 -04:00
Igor Lins e Silva 5e1437e96d merge: resolve #2137 onto develop (keep dry-run + single-file tests) 2026-08-03 00:54:03 -03:00
Sandro da Silva d52d5120de fix(hooks): ingest only the active transcript 2026-08-02 19:21:35 +00:00
Sandro da Silva 5cebe6608a fix(convos): honor mined state during dry runs 2026-08-02 17:12:25 +00:00
rohan richard 54397a5cc0 fix: dedup content hashes per conversation and per wing
normalize() joined every conversation in a Claude.ai privacy export
bundle into one string before it was hashed, so re-exporting the bundle
with one new conversation added changed the whole-file hash and the
existing conversations were re-mined as duplicates. Split normalization
into normalize_conversations() so each conversation can be hashed and
deduped on its own.

Also scope the content-hash map by wing — mining the same transcript
into a second wing is a deliberate re-file, not a duplicate, and was
leaving that wing with only the registry sentinel.
2026-07-20 22:35:21 +05:30
rohan richard 283dae45e3 fix: prevent duplicate drawers when re-mining LLM conversations
Repeated exports from Claude/ChatGPT land the same conversation under a
new filename each run (timestamped bundle, regenerated slug, etc.), so
the existing source_file/mtime skip never recognized it as already
mined and re-filed a duplicate set of drawers. Now content is hashed
after normalization and checked against previously filed hashes, so
the same conversation under a new path is skipped instead of
duplicated.
2026-07-20 17:30:21 +05:30
raman325 37bc50d98a
fix(hermes): address PR #1915 review findings
- _scan_metadatas: fetch cap+1 and compare len > cap, so a collection
  holding exactly STATUS_SCAN_LIMIT rows is no longer reported as
  truncated (the view is complete). Callers still get at most cap rows.
- status/list_wings/list_rooms: tolerate None metadata entries from
  legacy palaces / raw writers instead of failing the tool call.
- _match_wing_by_keywords: skip non-string keywords so a hand-edited
  wing_config.json can't break live turn filing.
- file_conversation_exchange: extra_metadata can no longer overwrite
  canonical keys (matches the documented append-only contract), and
  wing/room are validated with sanitize_name — invalid names fall back
  to wing_general / conversations rather than dropping the turn, per
  the verbatim-first mandate.
- Fix two stale docstrings left from the pre-split layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:08:58 -04:00
J.R. Murray 9b432699ce fix(convo_miner): treat transcripts as mutable, not immutable
Conversation transcripts were assumed immutable once mined: the bulk
skip-check (prefetch_mined_set) only tracked "have we seen this
source_file before at the current normalize_version", with no mtime
comparison at all. That's wrong for how Claude Code sessions actually
work -- a session keeps appending to its own JSONL file while active,
and /compact or /clear can rewrite one in place. Once a session file
was mined, any content appended after that point would silently never
get mined, with no error or warning -- the file just looked
"already filed" forever.

palace.py:
- prefetch_mined_set() now returns dict[source_file, stored_mtime]
  instead of a bare set[source_file]. `if src in mined_set` still works
  identically (dict `in` checks keys), so this is a source-compatible
  change for that access pattern; a caller that wants staleness
  detection reads mined_set[src] and compares against the file's
  current mtime itself. None means no mtime was ever stored (or
  getmtime failed when the drawer was written) and must be treated as
  stale, not "unknown, assume unchanged".
- Removed bulk_check_mined(): it already existed for exactly this
  purpose (bulk mtime prefetch) but had zero callers anywhere in the
  codebase and was missing the normalize_version/extract_mode filtering
  prefetch_mined_set has -- folded its intent into prefetch_mined_set
  instead of maintaining two subtly-different, overlapping bulk scans
  over the same underlying data.
- file_already_mined()'s docstring corrected: it previously claimed
  "transcripts are assumed immutable" for convo mining. That's no
  longer true; corrected to describe the actual current split (convo
  miner's bulk skip-check uses prefetch_mined_set's stored mtimes; this
  function's check_mtime=True path is now only its per-file,
  lock-held race-condition recheck).

convo_miner.py:
- New _is_unchanged_since_last_mine() helper (extracted to keep
  _mine_convos_impl under the repo's cyclomatic-complexity gate):
  false whenever the file isn't in the prefetched map, its stored mtime
  is None, getmtime fails, or the mtimes don't match -- true only when
  genuinely unchanged.
- _file_chunks_locked's metadata now stamps source_mtime on every real
  drawer (mirroring miner.py's existing pattern), and its in-lock
  recheck now passes check_mtime=True.
- _register_file's 0-chunk sentinel also stamps source_mtime, so a
  short file that later grows past MIN_CHUNK_SIZE is detected as
  changed instead of being skipped forever by the sentinel.

One-time cost worth flagging: no existing convo drawer has source_mtime
stored (this field never existed for convo mining before now), so the
first `mempalace mine --mode convos` after this ships will see every
already-mined file as stale and fully re-mine it. Not a bug --
_file_chunks_locked's existing purge-before-insert means no
duplication results -- just a real, one-time cost across a large
corpus.

tests/test_convo_miner.py: 7 new tests -- grown-file re-mine picks up
new content, unchanged file still skipped (the mtime check must not
regress the existing optimization), grown-file re-mine purges stale
drawers rather than accumulating duplicates (checked via unique content
markers, not raw counts -- ChromaDB collections can carry unrelated
bookkeeping rows), prefetch_mined_set's returned mtime matches the real
file, None handling for a drawer with no stored mtime, a legacy
drawer (no source_mtime field, simulating pre-this-change data) is
correctly re-mined rather than skipped forever, and the sentinel path
stamps source_mtime too.

Full suite: 3327 passed, 20 skipped, 0 failed. ruff check / ruff
format -- clean.
2026-07-07 16:41:22 -04:00
mvalentsev c9dc4c466d fix(miner): count only new work toward --limit, not already-mined skips (#1535) 2026-06-06 17:56:08 +05:00
Igor Lins e Silva e97b3621ca Merge branch 'develop' into fix/convos-mine-concurrency
Resolves conflicts from 128-commit divergence:

- mempalace/convo_miner.py imports: kept both `mine_palace_lock` (this PR)
  and `prefetch_mined_set` (develop).
- mempalace/convo_miner.py docstring: kept this PR's lock-wrapping
  description, added a one-line pointer to the chunking-config section
  whose body now lives in `_mine_convos_impl`.
- mempalace/convo_miner.py body: develop placed `cfg_chunk_size` /
  `cfg_min_chunk_size` setup inline in `mine_convos`. This PR factored
  the body into `_mine_convos_impl`, so the inline setup would have
  left `cfg_chunk_size` referenced-but-undefined inside the impl.
  Moved the `MempalaceConfig()` setup into `_mine_convos_impl` so the
  variables are in scope where they're used.
- tests/test_convo_miner.py: kept both additive test sets (lock
  concurrency from this PR + wing_api auto-routing from develop).

Local: ruff check / format pass; full pytest suite passes
(2103 passed, 3 skipped).

Co-Authored-By: Igor Lins e Silva <4753812+igorls@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:07:46 -03:00
Milla J 60d460b395
Merge pull request #1236 from MemPalace/feat/convo-miner-wing-api-auto-route
feat(convo_miner): auto-route AI tool sessions to wing_api
2026-05-21 09:45:37 -07:00
Igor Lins e Silva 2cab6f5a5b style: reformat tree with ruff 0.15.9
One-time mechanical reformat so `ruff format --check .` passes under the
newly pinned ruff. Layout only (assert-message parenthesization etc.),
no behavior change. 29 files: 28 under tests/ + 1 tools helper, no core
mempalace/ modules. Produced by `ruff format .`.
2026-05-17 23:20:01 -03:00
Igor Lins e Silva 83e4711593 style(tests): apply ruff 0.4.x format to test_convo_miner (#1528)
CI pins ruff>=0.4.0,<0.5 (resolves 0.4.10); one new assert block was
laid out in ruff-0.5+ style, failing `ruff format --check .`.
Reformat with the exact CI ruff version (assertion layout only, no
semantic change). `ruff check .` + `ruff format --check .` both pass
under 0.4.10.
2026-05-17 22:52:01 -03:00
Rahul 92de001c20 fix(convo_miner): scope skip-check and drawer ids by extract_mode (#1505)
Mining a transcript with --extract general was silently skipped when the
same file had already been mined with --extract exchange (or vice versa)
because file_already_mined() and prefetch_mined_set() only looked at
source_file. The two extraction modes produce different drawer content
and rooms, so they should coexist for the same source.

Changes:
- file_already_mined() and prefetch_mined_set() take an optional
  extract_mode arg and only return True when stored drawer metadata
  matches. Legacy drawers without extract_mode are treated as
  exchange-mode for back-compat.
- _file_chunks_locked() purges only same-mode drawers when rebuilding
  on a normalize-version bump, so a schema bump on one mode does not
  drop drawers filed under the other.
- Drawer ids and sentinel ids include extract_mode so the two modes
  cannot collide on hash.
- Pagination on the direct skip-check path so large transcripts (>1k
  drawers) are classified correctly when the bulk prefetch is skipped.

Adds regression coverage for the extract-mode-aware helper, the
pagination path, and an end-to-end mine_convos run that files
exchange then general for the same transcript without skipping.
2026-05-16 17:55:10 +05:30
Fergus Ching 96e3be9fe2 fix(convo_miner): wrap mine_convos in mine_palace_lock to stop pile-up
The project-files mine path (miner.mine) has wrapped _mine_impl in
mine_palace_lock since #1264 — a non-blocking flock that raises
MineAlreadyRunning so the second runner exits cleanly instead of
queueing as a waiter that drives parallel HNSW inserts. The convos
mine path (convo_miner.mine_convos) was missing the same guard.

In practice this meant any caller that spawned `mempalace mine
--mode convos` repeatedly against the same palace — most notably
the Stop-hook transcript ingest before the per-target PID slot
landed — could stack up arbitrarily many concurrent mines, each
holding a ChromaDB client open, each writing to the same HNSW
index. Recently observed: 28 stuck convos mines on one machine
consuming ~18 GB of RAM and contributing to a load spike.

Fix: refactor mine_convos into a thin wrapper that holds the
per-palace flock around _mine_convos_impl, mirroring miner.mine
exactly. Dry-run skips the lock since it never writes.

Tests: two cross-process tests in tests/test_convo_miner.py —
one asserts MineAlreadyRunning when a child process holds the
lock, one asserts dry-run is unaffected. Same spawn-context
pattern as test_palace_locks.py (fork-with-chromadb deadlocks
on Python 3.13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:47:36 +00:00
MSL 4098c54551 feat(convo_miner): auto-route AI tool sessions to wing_api
When mempalace mine --mode convos is invoked against a directory inside
a known AI-tool storage path (Claude Code, Codex CLI, Gemini CLI), the
destination wing now auto-defaults to wing_api rather than the directory
basename. Conversations from external API-keyed tools land grouped under
a single dedicated wing for visibility.

Detected paths (exact-segment match — substrings like .gemini-backup or
.codex-archive do NOT match):

  - any segment .codex (Codex CLI sessions / archives)
  - any segment .gemini (Gemini CLI sessions under ~/.gemini/tmp/...)
  - the consecutive segment pair .claude/projects (Claude Code).
    .claude alone is NOT matched - that is the settings/config dir,
    not a conversation source.

Wing-resolution precedence (first match wins):

  1. Explicit --wing argument from the user - always wins
  2. AI-tool path detection -> wing_api
  3. Basename fallback (existing behavior, unchanged)

Two new helpers split out of mine_convos for unit-test coverage:

  - _is_ai_tool_path(path: Path) -> bool
  - _resolve_wing(convo_path: Path, wing: Optional[str]) -> str

mine_convos now calls _resolve_wing in place of its inline basename
logic. No other call sites or downstream consumers change.

Test coverage:

  - 15 unit tests covering positive matches (Claude Code subdir + root,
    Codex root + sessions, Gemini root + chats), negative cases
    (.claude alone is settings dir, unrelated paths, substring no-match
    on .gemini-backup / .codex-archive), explicit --wing override,
    auto-route trio, basename fallback, empty-string-as-no-wing.
  - End-to-end smoke test (manual): real-shape Claude Code JSONL fixture
    mined via the actual CLI; sqlite read-back of /tmp palace confirms
    drawers landed with wing='wing_api' and verbatim content preserved;
    mempalace search --wing wing_api returns expected content ranked.
  - Full pytest sweep: 1388 baseline + 15 new = 1403 passed, zero
    regressions.

Design context:

This change reflects Aya's product call that conversations from
API-keyed AI tools should land in a structural wing_api rather than be
scattered across topical wings derived from directory basenames. Igor's
ADR-0017 in mempalace-ts proposes the alternative of source-prefix
metadata (source LIKE 'api/%') with topical wing assignment instead;
that approach has architectural merit (wings stay topical) but does not
deliver the single-wing visibility users get here. Open for review
discussion - explicit --wing flag and basename fallback both unchanged,
so this is additive and reversible.

Closes part of #59 for the auto-routing UX.
2026-04-27 01:57:06 -07:00
Igor Lins e Silva 7e5eeda9a5 feat(normalize): auto-rebuild stale drawers via NORMALIZE_VERSION schema gate
Without this, the strip_noise improvement only helps new mines. Every
user who had already mined Claude Code JSONL sessions would keep their
noise-polluted drawers forever, because convo_miner's file_already_mined
skip short-circuits before re-processing.

Adds a versioned schema gate so upgrades propagate silently:

- palace.NORMALIZE_VERSION=2 — bumped when the normalization pipeline
  changes shape (this PR's strip_noise is the v1→v2 bump).
- file_already_mined now returns False if the stored normalize_version
  is missing or less than current, triggering a rebuild on next mine.
- Both miners stamp drawers with the current normalize_version.
- convo_miner now purges stale drawers before inserting fresh chunks
  (mirrors miner.py's existing delete+insert), extracted into
  _file_convo_chunks helper to keep mine_convos under ruff's C901 limit.

User experience: upgrade mempalace, run `mempalace mine` as usual, old
noisy drawers get silently replaced with clean ones. No erase needed,
no "you need to rebuild" changelog footgun.

Tests:
- test_file_already_mined_returns_false_for_stale_normalize_version —
  pins the version gate contract for missing/v1/current.
- test_add_drawer_stamps_normalize_version — fresh project-miner drawers
  carry the field.
- test_mine_convos_rebuilds_stale_drawers_after_schema_bump — end-to-end
  proof that a pre-v2 palace gets silently cleaned on next mine, with
  orphan drawers purged and NOT skipped.

Existing test_file_already_mined_check_mtime updated to include the
new field; all other tests unaffected.
2026-04-13 16:20:55 -03:00
Mikhail Valentsev 87e8bafad8
fix: prevent convo_miner from re-processing 0-chunk files on every run (#654) (#732)
* fix: register 0-chunk files to prevent re-processing on every mine (#654)

mine_convos() has three early-exit paths (OSError, content too short,
zero chunks) that skip writing anything to ChromaDB. Since
file_already_mined() checks for the presence of a document with a
matching source_file, these files are re-read and re-processed on
every subsequent run.

Add _register_file() that upserts a lightweight sentinel document
(room="_registry", ingest_mode="registry") so file_already_mined()
returns True on future runs.

Note: Bug 2 from the issue (drawers_added counter always 0) was
already resolved upstream via the switch from collection.add() to
collection.upsert().

* fix: resolve macOS path symlink in test + remove unused variable
2026-04-12 14:25:34 -07:00
Tal Muskal abd52534bb test: bring coverage to 85%, set threshold to 85, reset version to 3.0.11
- Add tests for config, convo_miner, spellcheck, knowledge_graph
- Fix Windows PermissionError in test cleanup (chromadb file locks)
- Add UTF-8 encoding to split_mega_files, entity_registry, hooks_cli
- Fix mcp_server parse_known_args logging for unknown args
- Set coverage threshold to 85 in pyproject.toml and CI
- Reset all version files to 3.0.11

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 21:38:12 +03:00
bensig 0f8fa8c7d5 bench: add benchmark runners, results docs, and test suite
Benchmarks: LongMemEval, LoCoMo, ConvoMem, MemBench runners with
methodology docs and hybrid retrieval analysis.

Tests: config, miner, convo_miner, normalize — 9 tests, all passing.
2026-04-04 18:33:42 -07:00