Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions mempalace/convo_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB — skip files larger than this


def _register_file(collection, source_file: str, wing: str, agent: str):
"""Write a sentinel so file_already_mined() returns True for 0-chunk files.

Without this, files that normalize to nothing or produce zero chunks are
re-read and re-processed on every mine run because nothing was written to
ChromaDB on the first pass.
"""
sentinel_id = f"_reg_{hashlib.sha256(source_file.encode()).hexdigest()[:24]}"
collection.upsert(
documents=[f"[registry] {source_file}"],
ids=[sentinel_id],
metadatas=[
{
"wing": wing,
"room": "_registry",
"source_file": source_file,
"added_by": agent,
"filed_at": datetime.now().isoformat(),
"ingest_mode": "registry",
}
],
)


# =============================================================================
# CHUNKING — exchange pairs for conversations
# =============================================================================
Expand Down Expand Up @@ -282,9 +306,13 @@ def mine_convos(
try:
content = normalize(str(filepath))
except (OSError, ValueError):
if not dry_run:
_register_file(collection, source_file, wing, agent)
continue

if not content or len(content.strip()) < MIN_CHUNK_SIZE:
if not dry_run:
_register_file(collection, source_file, wing, agent)
continue

# Chunk — either exchange pairs or general extraction
Expand All @@ -297,6 +325,8 @@ def mine_convos(
chunks = chunk_exchanges(content)

if not chunks:
if not dry_run:
_register_file(collection, source_file, wing, agent)
continue

# Detect room from content (general mode uses memory_type instead)
Expand Down
51 changes: 51 additions & 0 deletions tests/test_convo_miner.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import tempfile
import shutil
from pathlib import Path

import chromadb

from mempalace.convo_miner import mine_convos
from mempalace.palace import file_already_mined


def test_convo_mining():
Expand All @@ -24,3 +28,50 @@ def test_convo_mining():
assert len(results["documents"][0]) > 0

shutil.rmtree(tmpdir, ignore_errors=True)


def test_mine_convos_does_not_reprocess_short_files(capsys):
"""Files below MIN_CHUNK_SIZE get a sentinel so they are skipped on re-run."""
tmpdir = tempfile.mkdtemp()
try:
# A file too short to produce any chunks
with open(os.path.join(tmpdir, "tiny.txt"), "w") as f:
f.write("hi")

palace_path = os.path.join(tmpdir, "palace")

# First run -- file is processed (sentinel written)
mine_convos(tmpdir, palace_path, wing="test")
capsys.readouterr() # drain output

# Verify sentinel was written (resolve path -- macOS /var -> /private/var)
resolved_file = str(Path(tmpdir).resolve() / "tiny.txt")
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
assert file_already_mined(col, resolved_file)

# Second run -- file should be skipped
mine_convos(tmpdir, palace_path, wing="test")
out2 = capsys.readouterr().out
assert "Files skipped (already filed): 1" in out2
finally:
shutil.rmtree(tmpdir, ignore_errors=True)


def test_mine_convos_does_not_reprocess_empty_chunk_files(capsys):
"""Files that normalize but produce 0 exchange chunks get a sentinel."""
tmpdir = tempfile.mkdtemp()
try:
# Content long enough to pass MIN_CHUNK_SIZE but with no exchange markers
# (no "> " lines), so chunk_exchanges returns []
with open(os.path.join(tmpdir, "no_exchanges.txt"), "w") as f:
f.write("This is a plain paragraph without any exchange markers. " * 5)

palace_path = os.path.join(tmpdir, "palace")

mine_convos(tmpdir, palace_path, wing="test")
mine_convos(tmpdir, palace_path, wing="test")
out2 = capsys.readouterr().out
assert "Files skipped (already filed): 1" in out2
finally:
shutil.rmtree(tmpdir, ignore_errors=True)