Skip to content

Commit 91c95f8

Browse files
dcramercodex
andcommitted
memory doctor: make provenance command non-mutating by default
- add provenance audit command for missing LEARNED_IN reporting - route 'memory doctor provenance' to audit instead of prune - keep prune-missing-provenance as explicit destructive action - add CLI regression test for provenance audit routing Co-Authored-By: GPT-5 Codex <codex@openai.com>
1 parent dc79065 commit 91c95f8

4 files changed

Lines changed: 79 additions & 1 deletion

File tree

src/ash/cli/commands/memory/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ async def _run_memory_action(
295295
memory_doctor_embed_missing,
296296
memory_doctor_fix_names,
297297
memory_doctor_normalize_semantics,
298+
memory_doctor_provenance_audit,
298299
memory_doctor_prune_missing_provenance,
299300
memory_doctor_quality,
300301
memory_doctor_reclassify,
@@ -335,7 +336,9 @@ async def _run_memory_action(
335336
await memory_doctor_embed_missing(store, force=force)
336337
elif subcommand == "normalize-semantics":
337338
await memory_doctor_normalize_semantics(store, force=force)
338-
elif subcommand in ("prune-missing-provenance", "provenance"):
339+
elif subcommand == "provenance":
340+
await memory_doctor_provenance_audit(store)
341+
elif subcommand == "prune-missing-provenance":
339342
await memory_doctor_prune_missing_provenance(store, force=force)
340343
elif subcommand == "self-facts":
341344
await memory_doctor_self_facts(store, force=force)

src/ash/cli/commands/memory/doctor/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
memory_doctor_normalize_semantics,
1313
)
1414
from ash.cli.commands.memory.doctor.prune_missing_provenance import (
15+
memory_doctor_provenance_audit,
1516
memory_doctor_prune_missing_provenance,
1617
)
1718
from ash.cli.commands.memory.doctor.quality import memory_doctor_quality
@@ -26,6 +27,7 @@
2627
"memory_doctor_embed_missing",
2728
"memory_doctor_fix_names",
2829
"memory_doctor_normalize_semantics",
30+
"memory_doctor_provenance_audit",
2931
"memory_doctor_prune_missing_provenance",
3032
"memory_doctor_quality",
3133
"memory_doctor_reclassify",

src/ash/cli/commands/memory/doctor/prune_missing_provenance.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,44 @@
1111
from ash.store.store import Store
1212

1313

14+
async def memory_doctor_provenance_audit(store: Store) -> None:
15+
"""Report active memories missing LEARNED_IN provenance (non-mutating)."""
16+
missing_ids = store.missing_learned_in_provenance_ids()
17+
if not missing_ids:
18+
success("No active memories are missing LEARNED_IN provenance")
19+
return
20+
21+
table = create_table(
22+
"Active Memories Missing Provenance",
23+
[
24+
("ID", {"style": "dim", "max_width": 8}),
25+
("Owner", {"style": "yellow", "max_width": 16}),
26+
("Chat", {"style": "cyan", "max_width": 8}),
27+
("Content", {"style": "white", "max_width": 42}),
28+
],
29+
)
30+
for memory_id in missing_ids[:10]:
31+
memory = store.graph.memories.get(memory_id)
32+
if memory is None:
33+
continue
34+
table.add_row(
35+
memory.id[:8],
36+
memory.owner_user_id or "-",
37+
(memory.chat_id[:8] if memory.chat_id else "-"),
38+
truncate(memory.content),
39+
)
40+
if len(missing_ids) > 10:
41+
table.add_row("...", "...", "...", f"... and {len(missing_ids) - 10} more")
42+
43+
console.print(table)
44+
console.print(
45+
f"\n[bold]{len(missing_ids)} active memories are missing LEARNED_IN provenance[/bold]"
46+
)
47+
console.print(
48+
"[dim]Run 'ash memory doctor prune-missing-provenance --force' to archive them.[/dim]"
49+
)
50+
51+
1452
async def memory_doctor_prune_missing_provenance(store: Store, force: bool) -> None:
1553
"""Archive active memories with no LEARNED_IN edge."""
1654
missing_ids = store.missing_learned_in_provenance_ids()

tests/test_cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,41 @@ async def _fake_embed_missing(*args, **kwargs):
195195
assert result.exit_code == 0
196196
assert calls == [False]
197197

198+
def test_memory_doctor_provenance_is_non_mutating_audit(
199+
self, cli_runner, config_file, monkeypatch
200+
):
201+
calls: list[str] = []
202+
203+
class DummyStore:
204+
pass
205+
206+
async def _fake_get_store(_config):
207+
return DummyStore()
208+
209+
async def _fake_audit(*_args, **_kwargs):
210+
calls.append("audit")
211+
212+
async def _fake_prune(*_args, **_kwargs):
213+
calls.append("prune")
214+
215+
monkeypatch.setattr(
216+
"ash.cli.commands.memory._helpers.get_store", _fake_get_store
217+
)
218+
monkeypatch.setattr(
219+
"ash.cli.commands.memory.doctor.memory_doctor_provenance_audit",
220+
_fake_audit,
221+
)
222+
monkeypatch.setattr(
223+
"ash.cli.commands.memory.doctor.memory_doctor_prune_missing_provenance",
224+
_fake_prune,
225+
)
226+
227+
result = cli_runner.invoke(
228+
app, ["memory", "doctor", "provenance", "--config", str(config_file)]
229+
)
230+
assert result.exit_code == 0
231+
assert calls == ["audit"]
232+
198233
def test_memory_doctor_all_runs_force_mode(
199234
self, cli_runner, config_file, monkeypatch
200235
):

0 commit comments

Comments
 (0)