fix(hooks): ingest only the active transcript

This commit is contained in:
Sandro da Silva 2026-08-02 19:21:35 +00:00
parent a337cafeaf
commit d52d5120de
6 changed files with 76 additions and 11 deletions

View File

@ -1832,7 +1832,9 @@ def main():
# mine
p_mine = sub.add_parser("mine", help="Mine files into the palace")
p_mine.add_argument("dir", help="Directory to mine")
p_mine.add_argument(
"dir", help="Directory to mine, or one conversation file with --mode convos"
)
p_mine.add_argument(
"--backend",
default=None,

View File

@ -418,9 +418,16 @@ def scan_convos(convo_dir: str) -> list:
``sys.stderr`` with a `` SKIP: <relative-path> (symlink)`` line so the
caller can tell why an apparent conversation directory yielded no files.
"""
convo_path = Path(convo_dir).expanduser().resolve()
# A direct conversation file is a valid source. For a file, feed only
# its basename through the existing directory validation loop.
requested_path = Path(convo_dir).expanduser()
single_file = requested_path.is_file()
convo_path = (requested_path.parent if single_file else requested_path).resolve()
scan_entries = (
[(str(convo_path), [], [requested_path.name])] if single_file else os.walk(convo_path)
)
files = []
for root, dirs, filenames in os.walk(convo_path):
for root, dirs, filenames in scan_entries:
dirs[:] = [d for d in dirs if d not in CONVO_SKIP_DIRS]
for filename in filenames:
if filename.endswith(".meta.json"):

View File

@ -1090,12 +1090,12 @@ def _ingest_transcript(transcript_path: str):
_submit_daemon_job(
"mine",
{
"source": str(path.parent),
"source": str(path),
"mode": "convos",
"wing": "sessions",
"agent": "mempalace",
},
dedupe_key=_daemon_mine_dedupe_key(str(path.parent), "convos"),
dedupe_key=_daemon_mine_dedupe_key(str(path), "convos"),
wait=False,
)
_log(f"Transcript ingest submitted to daemon: {path.name}")
@ -1113,7 +1113,7 @@ def _ingest_transcript(transcript_path: str):
"-m",
"mempalace",
"mine",
str(path.parent),
str(path),
"--mode",
"convos",
"--wing",

View File

@ -834,3 +834,41 @@ def test_register_file_sentinel_includes_source_mtime():
assert abs(mined[str(tiny_file)] - os.path.getmtime(tiny_file)) < 0.001
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def test_mine_convos_dry_run_single_file_does_not_scan_siblings(
tmp_path,
capsys,
):
selected = tmp_path / "selected.txt"
sibling = tmp_path / "sibling.txt"
selected.write_text(
"> Which transcript should be mined?\n"
"SELECTED_ONLY_MARKER belongs to the active transcript.\n\n"
"> Should sibling files be included?\n"
"No. Only the selected transcript should be scanned.\n",
encoding="utf-8",
)
sibling.write_text(
"> Should this sibling be mined?\n"
"SIBLING_SHOULD_NOT_BE_MINED by the single-file invocation.\n\n"
"> Is that important?\n"
"Yes. It keeps hook-triggered mining narrowly scoped.\n",
encoding="utf-8",
)
palace_path = tmp_path / "palace"
mine_convos(
str(selected),
str(palace_path),
wing="sessions",
dry_run=True,
)
output = capsys.readouterr().out
assert "Files: 1" in output
assert "[DRY RUN] selected.txt" in output
assert "sibling.txt" not in output
assert not palace_path.exists()

View File

@ -713,3 +713,21 @@ class TestExtractAuthoredAt:
f = tmp_path / "session.jsonl"
f.write_text('{"timestamp": 1}\n{"timestamp": false}\n')
assert _extract_authored_at(f) is None
def test_scan_convos_accepts_one_file_without_scanning_siblings(
tmp_path,
):
selected = tmp_path / "selected.jsonl"
sibling = tmp_path / "sibling.jsonl"
selected.write_text(
'{"type": "user"}\n',
encoding="utf-8",
)
sibling.write_text(
'{"type": "user"}\n',
encoding="utf-8",
)
assert scan_convos(str(selected)) == [selected.resolve()]

View File

@ -1378,7 +1378,7 @@ def test_ingest_transcript_daemon_opt_in_submits_job(tmp_path):
mock_popen.assert_not_called()
mock_submit.assert_called_once()
payload = mock_submit.call_args.args[1]
assert payload["source"] == str(tmp_path)
assert payload["source"] == str(transcript.resolve())
assert payload["mode"] == "convos"
assert payload["wing"] == "sessions"
@ -1398,7 +1398,7 @@ def test_ingest_transcript_skips_when_target_running(tmp_path):
"-m",
"mempalace",
"mine",
str(transcript.parent),
str(transcript.resolve()),
"--mode",
"convos",
"--wing",
@ -1763,7 +1763,7 @@ def test_precompact_with_timeout(tmp_path):
assert result == {}
def test_precompact_mines_transcript_dir(tmp_path, monkeypatch):
def test_precompact_mines_only_active_transcript(tmp_path, monkeypatch):
"""Precompact ingests the active transcript via _ingest_transcript.
With no MEMPAL_DIR, _mine_sync is a no-op; the transcript ingest is
@ -1787,8 +1787,8 @@ def test_precompact_mines_transcript_dir(tmp_path, monkeypatch):
mock_run.assert_not_called()
mock_popen.assert_called_once()
cmd = mock_popen.call_args[0][0]
# Mines the transcript's parent dir as convos, into wing "sessions".
assert str(tmp_path) in cmd
# Mines only the active transcript as convos, into wing "sessions".
assert str(transcript.resolve()) in cmd
assert cmd[cmd.index("--mode") + 1] == "convos"
assert cmd[cmd.index("--wing") + 1] == "sessions"