Skip to content

Commit 89f36dc

Browse files
committed
fix(cli): prevent duplicate prompt recording by relying on Stop hook sync
1 parent c2ab57b commit 89f36dc

2 files changed

Lines changed: 15 additions & 20 deletions

File tree

src/ledgermind/server/cli.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,18 +352,17 @@ def bridge_context(path: str, prompt: str, cli: Optional[str] = None, threshold:
352352
default_cli = [cli] if cli else None
353353
bridge = IntegrationBridge(memory_path=path, default_cli=default_cli, relevance_threshold=threshold if threshold is not None else 0.7)
354354

355-
# 3. Robust History Sync (Hybrid Deduplication)
355+
# 4. Robust History Sync (Hybrid Deduplication)
356356
# We only sync if transcript_path is a FILE
357+
synced_content_this_turn = set()
357358
if transcript_path and os.path.isfile(transcript_path):
358-
sync_transcript_history(bridge, transcript_path)
359+
synced_content_this_turn = sync_transcript_history(bridge, transcript_path)
359360
elif transcript_path:
360361
sys.stderr.write(f"* Skipping sync: {transcript_path} is not a file\n")
361362

362-
# 5. Record the CURRENT prompt if not already handled
363-
if real_prompt and real_prompt != "-":
364-
rp_strip = real_prompt.strip()
365-
if rp_strip and rp_strip not in synced_content_this_turn:
366-
bridge.memory.process_event(source="user", kind="prompt", content=rp_strip)
363+
# 5. Record the CURRENT prompt (REMOVED - handled by Stop hook transcript sync)
364+
# Manual recording here causes duplicates with Stop hook sync due to TZ/precision diffs.
365+
# We rely on Stop hook to record everything robustly.
367366

368367
ctx = bridge.get_context_for_prompt(real_prompt)
369368
sys.stdout.write(ctx)

tests/server/test_hooks_integration.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_install_claude_hooks(mock_home, tmp_path):
5353
settings_file = mock_home / ".claude" / "settings.json"
5454

5555
assert (project_hooks_dir / "ledgermind_before_prompt.sh").exists()
56-
assert (project_hooks_dir / "ledgermind_after_interaction.sh").exists()
56+
assert (project_hooks_dir / "ledgermind_stop.sh").exists()
5757
assert settings_file.exists()
5858

5959
# Verify settings.json points to the project hooks and has the new matcher format
@@ -65,11 +65,10 @@ def test_install_claude_hooks(mock_home, tmp_path):
6565
assert ups[0]["matcher"] == "*"
6666
assert ups[0]["hooks"][0]["command"] == str(project_hooks_dir / "ledgermind_before_prompt.sh")
6767

68-
# Check PostToolUse
69-
ptu = settings["hooks"]["PostToolUse"]
70-
assert isinstance(ptu, list)
71-
assert ptu[0]["matcher"] == "*"
72-
assert ptu[0]["hooks"][0]["command"] == str(project_hooks_dir / "ledgermind_after_interaction.sh")
68+
# Check Stop
69+
stop = settings["hooks"]["Stop"]
70+
assert isinstance(stop, list)
71+
assert stop[0]["hooks"][0]["command"] == str(project_hooks_dir / "ledgermind_stop.sh")
7372

7473
# Verify content
7574
before_content = (project_hooks_dir / "ledgermind_before_prompt.sh").read_text()
@@ -104,10 +103,6 @@ def test_install_cursor_hooks(mock_home, tmp_path):
104103
assert '--cli "cursor"' in before_content
105104
assert ".ledgermind" in before_content
106105

107-
with open(hooks_file) as f:
108-
config = json.load(f)
109-
assert config["beforeSubmitPrompt"] == str(project_hooks_dir / "ledgermind_before.sh")
110-
111106
def test_install_gemini_hooks(mock_home, tmp_path):
112107
project_path = tmp_path / "project"
113108
project_path.mkdir()
@@ -133,11 +128,12 @@ def test_bridge_context_cli(tmp_path):
133128

134129
# Record a decision first so we have context
135130
from ledgermind.core.api.memory import Memory
136-
mem = Memory(vector_model="v5-small-text-matching-Q4_K_M.gguf", storage_path=str(memory_path))
131+
# Use fallback if no real model, but search_decisions should still work via FTS5
132+
mem = Memory(vector_model="non_existent.gguf", storage_path=str(memory_path))
137133
mem.record_decision(title="Test Rule", target="test", rationale="Important decision for testing hooks")
138134

139-
# Use a low threshold to ensure context injection regardless of exact similarity score
140-
result = run_cli(["bridge-context", "--path", str(memory_path), "--prompt", "Test Rule", "--threshold", "0.1"])
135+
# Use a 0.0 threshold to ensure context injection regardless of exact similarity score (FTS5 match)
136+
result = run_cli(["bridge-context", "--path", str(memory_path), "--prompt", "Test Rule", "--threshold", "0.0"])
141137
if result.returncode != 0:
142138
sys.__stderr__.write(f"STDERR: {result.stderr}\n")
143139
assert result.returncode == 0

0 commit comments

Comments
 (0)