Skip to content

Commit 4ac8961

Browse files
jpheinclaude
andcommitted
fix: Codex message extraction, stopword leak, docstring, test hermeticity
- _extract_recent_messages() now handles Codex event_msg format (matching _count_human_messages behavior) so silent save works on Codex transcripts - Stopword list normalized: "don't" → "dont", "let's" → "lets" to match the apostrophe-stripping cleanup in _extract_themes() - _save_diary_direct() docstring clarified: "no MCP roundtrip" not "no MCP calls" - Tests mock MempalaceConfig so they don't depend on user's config.json Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 77631e3 commit 4ac8961

2 files changed

Lines changed: 31 additions & 19 deletions

File tree

mempalace/hooks_cli.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -169,21 +169,27 @@ def _extract_recent_messages(transcript_path: str, count: int = _RECENT_MSG_COUN
169169
for line in f:
170170
try:
171171
entry = json.loads(line)
172-
msg = entry.get("message", {})
173-
if not isinstance(msg, dict) or msg.get("role") != "user":
174-
continue
175-
content = msg.get("content", "")
176-
if isinstance(content, list):
177-
content = " ".join(
178-
b.get("text", "") for b in content if isinstance(b, dict)
179-
)
180-
if not isinstance(content, str) or not content.strip():
181-
continue
182-
if "<command-message>" in content or "<system-reminder>" in content:
183-
continue
184-
# Truncate long messages
185-
text = content.strip()[:200]
186-
messages.append(text)
172+
# Claude Code format
173+
msg = entry.get("message") or entry.get("event_message") or {}
174+
if isinstance(msg, dict) and msg.get("role") == "user":
175+
content = msg.get("content", "")
176+
if isinstance(content, list):
177+
content = " ".join(
178+
b.get("text", "") for b in content if isinstance(b, dict)
179+
)
180+
if not isinstance(content, str) or not content.strip():
181+
continue
182+
if "<command-message>" in content or "<system-reminder>" in content:
183+
continue
184+
messages.append(content.strip()[:200])
185+
# Codex CLI format
186+
elif entry.get("type") == "event_msg":
187+
payload = entry.get("payload", {})
188+
if isinstance(payload, dict) and payload.get("type") == "user_message":
189+
text = payload.get("message", "")
190+
if isinstance(text, str) and text.strip():
191+
if "<command-message>" not in text:
192+
messages.append(text.strip()[:200])
187193
except (json.JSONDecodeError, AttributeError):
188194
pass
189195
except OSError:
@@ -193,8 +199,8 @@ def _extract_recent_messages(transcript_path: str, count: int = _RECENT_MSG_COUN
193199

194200
_THEME_STOPWORDS = frozenset(
195201
"the a an and or but in on at to for of is it i me my you your we our "
196-
"this that with from by was were be been are not no yes can do did don't "
197-
"will would should could have has had let's let just also like so if then "
202+
"this that with from by was were be been are not no yes can do did dont "
203+
"will would should could have has had lets let just also like so if then "
198204
"ok okay sure yeah hey hi here there what when where how why which some "
199205
"all any each every about into out up down over after before between "
200206
"get got make made need want use used using check look see run try "
@@ -222,7 +228,7 @@ def _extract_themes(messages: list[str], max_themes: int = 3) -> list[str]:
222228
def _save_diary_direct(
223229
transcript_path: str, session_id: str, toast: bool = False,
224230
) -> dict:
225-
"""Write a diary checkpoint directly via Python API (no MCP calls).
231+
"""Write a diary checkpoint by calling the tool function directly (no MCP roundtrip).
226232
227233
Returns {"count": N, "themes": [...]} on success, {"count": 0} on failure.
228234
"""

tests/test_hooks_cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import os
55
from pathlib import Path
6-
from unittest.mock import patch
6+
from unittest.mock import MagicMock, patch
77

88
import pytest
99

@@ -162,11 +162,17 @@ def test_extract_recent_messages_missing_file():
162162
def _capture_hook_output(hook_fn, data, harness="claude-code", state_dir=None):
163163
"""Run a hook and capture its JSON stdout output."""
164164
import io
165+
from unittest.mock import PropertyMock
165166

166167
buf = io.StringIO()
167168
patches = [patch("mempalace.hooks_cli._output", side_effect=lambda d: buf.write(json.dumps(d)))]
168169
if state_dir:
169170
patches.append(patch("mempalace.hooks_cli.STATE_DIR", state_dir))
171+
# Mock MempalaceConfig so tests don't depend on user's ~/.mempalace/config.json
172+
mock_config = MagicMock()
173+
type(mock_config).hook_silent_save = PropertyMock(return_value=True)
174+
type(mock_config).hook_desktop_toast = PropertyMock(return_value=False)
175+
patches.append(patch("mempalace.config.MempalaceConfig", return_value=mock_config))
170176
with contextlib.ExitStack() as stack:
171177
for p in patches:
172178
stack.enter_context(p)

0 commit comments

Comments
 (0)