Skip to content

Commit d45fae0

Browse files
committed
fix: improve recall archive discovery and memory validation
1 parent 88a6ded commit d45fae0

5 files changed

Lines changed: 315 additions & 4 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "csep"
7-
version = "1.3.5"
7+
version = "1.3.6"
88
description = "Self-evolution loop for OpenAI Codex: session reflection, session recall, and reflection-generated skills with zero Python runtime dependencies."
99
readme = "README.md"
1010
requires-python = ">=3.11"

‎src/codex_self_evolution/session_recall/archive.py‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,26 @@ def discover_transcript_for_hook_payload(
200200
"candidate_count": len(meta_matches),
201201
}
202202

203+
turn_discovery = _recent_content_discovery(
204+
recent_files,
205+
payload,
206+
key="turn_id",
207+
reason="recent_turn_id",
208+
min_chars=6,
209+
)
210+
if turn_discovery:
211+
return turn_discovery
212+
213+
assistant_discovery = _recent_content_discovery(
214+
recent_files,
215+
payload,
216+
key="last_assistant_message",
217+
reason="recent_last_assistant_message",
218+
min_chars=24,
219+
)
220+
if assistant_discovery:
221+
return assistant_discovery
222+
203223
cwd = str(payload.get("cwd") or "")
204224
if cwd:
205225
cwd_matches = [
@@ -427,6 +447,84 @@ def _session_meta(path: Path) -> dict[str, Any]:
427447
return {}
428448

429449

450+
def _recent_content_discovery(
451+
recent_files: list[Path],
452+
payload: dict[str, Any],
453+
*,
454+
key: str,
455+
reason: str,
456+
min_chars: int,
457+
) -> dict[str, Any] | None:
458+
"""Return a unique recent transcript whose JSON content contains a payload value."""
459+
needle = _normalized_search_text(str(payload.get(key) or ""))
460+
if len(needle) < min_chars:
461+
return None
462+
matches = [
463+
path for path in recent_files
464+
if _transcript_contains_normalized_text(path, needle)
465+
]
466+
matches = _prefer_payload_cwd(matches, payload)
467+
if len(matches) == 1:
468+
return _discovered(matches[0], reason)
469+
if len(matches) > 1:
470+
return {
471+
"status": "discovery_ambiguous",
472+
"reason": reason,
473+
"path": "",
474+
"candidate_count": len(matches),
475+
}
476+
return None
477+
478+
479+
def _prefer_payload_cwd(paths: list[Path], payload: dict[str, Any]) -> list[Path]:
480+
"""Prefer content matches whose session metadata cwd matches the hook payload."""
481+
if len(paths) <= 1:
482+
return paths
483+
cwd = str(payload.get("cwd") or "")
484+
if not cwd:
485+
return paths
486+
cwd_matches = [
487+
path for path in paths
488+
if _same_path(str(_session_meta(path).get("cwd") or ""), cwd)
489+
]
490+
return cwd_matches or paths
491+
492+
493+
def _transcript_contains_normalized_text(path: Path, needle: str) -> bool:
494+
"""Return whether any JSON leaf string or raw line contains normalized text."""
495+
try:
496+
with path.open("r", encoding="utf-8", errors="replace") as handle:
497+
for raw in handle:
498+
if needle in _normalized_search_text(raw):
499+
return True
500+
try:
501+
entry = json.loads(raw)
502+
except ValueError:
503+
continue
504+
if any(needle in _normalized_search_text(value) for value in _json_leaf_strings(entry)):
505+
return True
506+
except OSError:
507+
return False
508+
return False
509+
510+
511+
def _json_leaf_strings(value: Any) -> Iterable[str]:
512+
"""Yield all string leaf values from JSON-like transcript entries."""
513+
if isinstance(value, str):
514+
yield value
515+
elif isinstance(value, dict):
516+
for child in value.values():
517+
yield from _json_leaf_strings(child)
518+
elif isinstance(value, list):
519+
for child in value:
520+
yield from _json_leaf_strings(child)
521+
522+
523+
def _normalized_search_text(value: str) -> str:
524+
"""Normalize transcript text for robust exact-substring matching."""
525+
return " ".join(value.split()).lower()
526+
527+
430528
def _same_path(left: str, right: str) -> bool:
431529
if not left or not right:
432530
return False

‎src/codex_self_evolution/session_reflection/validation.py‎

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,35 +227,86 @@ def _hot_memory_has_noop_reflection_summary(text: str) -> bool:
227227
"""Return whether hot memory contains a per-run duplicate/no-new reflection line."""
228228
for line in text.splitlines():
229229
normalized = line.lower()
230-
if _has_noop_memory_signal(normalized) and _has_reflection_review_signal(normalized):
230+
if (
231+
_has_reflection_review_signal(normalized)
232+
and (_has_noop_memory_signal(normalized) or _has_duplicate_only_memory_signal(normalized))
233+
):
231234
return True
232235
return False
233236

234237

235238
def _memory_ref_is_noop_reflection_record(path: Path, text: str) -> bool:
236239
"""Return whether a ref file is only a duplicate/no-new reflection ledger."""
237240
normalized = text.lower()
238-
if not _has_noop_memory_signal(normalized):
241+
if not (_has_noop_memory_signal(normalized) or _has_duplicate_only_memory_signal(normalized)):
239242
return False
240-
if "csep-reflection-memory-skills" in path.name:
243+
if _has_reflection_ledger_filename(path):
241244
return True
242245
return _has_reflection_review_signal(normalized)
243246

244247

248+
def _has_reflection_ledger_filename(path: Path) -> bool:
249+
"""Return whether the ref filename is generated reflection-review bookkeeping."""
250+
name = path.name.lower()
251+
return any(
252+
marker in name
253+
for marker in (
254+
"csep-reflection-memory-skills",
255+
"csep-memory-skills-duplicate",
256+
"csep-reflection-review",
257+
)
258+
)
259+
260+
245261
def _has_noop_memory_signal(text: str) -> bool:
246262
"""Return whether text says the reflection found no reusable durable candidate."""
247263
return any(
248264
token in text
249265
for token in (
250266
"未识别新增",
251267
"未发现新增",
268+
"未出现可沉淀",
269+
"未发现可沉淀",
270+
"未发现可写入",
271+
"未新增可",
252272
"无新增",
253273
"nothing reusable",
254274
"no new reusable",
255275
)
256276
)
257277

258278

279+
def _has_duplicate_only_memory_signal(text: str) -> bool:
280+
"""Return whether text says all candidates were duplicate or already covered."""
281+
duplicate_signal = any(
282+
token in text
283+
for token in (
284+
"duplicate",
285+
"复用既有",
286+
"既有",
287+
"等价",
288+
"already covered",
289+
)
290+
)
291+
no_durable_signal = any(
292+
token in text
293+
for token in (
294+
"没有新的",
295+
"未产生",
296+
"仅记录",
297+
"仅需",
298+
"无需",
299+
"不写入",
300+
"不产生",
301+
"未创建",
302+
"未新增",
303+
"no durable",
304+
"no new",
305+
)
306+
)
307+
return duplicate_signal and no_durable_signal
308+
309+
259310
def _has_reflection_review_signal(text: str) -> bool:
260311
"""Return whether text is about the CSEP memory/skill reflection review."""
261312
return any(

‎tests/test_session_recall_archive.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,79 @@ def test_archive_from_hook_payload_does_not_discover_ambiguous_recent_cwd(tmp_pa
180180
store.close()
181181

182182

183+
def test_archive_from_hook_payload_disambiguates_recent_cwd_by_last_assistant_message(tmp_path):
184+
from codex_self_evolution.session_recall.archive import archive_from_hook_payload
185+
186+
sessions_root = tmp_path / "sessions"
187+
repo = tmp_path / "repo"
188+
repo.mkdir()
189+
_write_transcript(sessions_root / "one.jsonl", session_id="one", cwd=str(repo), message="other session")
190+
matched = sessions_root / "two.jsonl"
191+
_write_transcript(
192+
matched,
193+
session_id="two",
194+
cwd=str(repo),
195+
message="The exact final assistant sentence that appears in the Stop payload.",
196+
)
197+
payload = tmp_path / "payload.json"
198+
payload.write_text(
199+
json.dumps(
200+
{
201+
"session_id": "payload-session",
202+
"cwd": str(repo),
203+
"last_assistant_message": "The exact final assistant sentence that appears in the Stop payload.",
204+
}
205+
),
206+
encoding="utf-8",
207+
)
208+
209+
result = archive_from_hook_payload(
210+
payload,
211+
db_path=tmp_path / "state.db",
212+
sessions_root=sessions_root,
213+
)
214+
215+
assert result["status"] == "archived"
216+
assert result["session_id"] == "two"
217+
assert result["discovery_reason"] == "recent_last_assistant_message"
218+
assert result["discovered_transcript_path"] == str(matched.resolve())
219+
220+
221+
def test_archive_from_hook_payload_discovers_recent_turn_id(tmp_path):
222+
from codex_self_evolution.session_recall.archive import archive_from_hook_payload
223+
224+
sessions_root = tmp_path / "sessions"
225+
repo = tmp_path / "repo"
226+
repo.mkdir()
227+
transcript = sessions_root / "turn-match.jsonl"
228+
transcript.parent.mkdir(parents=True, exist_ok=True)
229+
transcript.write_text(
230+
"\n".join(
231+
[
232+
json.dumps({"type": "session_meta", "payload": {"id": "turn-session", "cwd": str(repo)}}),
233+
json.dumps({"id": "turn-abc", "role": "assistant", "content": "archive this turn"}),
234+
"",
235+
]
236+
),
237+
encoding="utf-8",
238+
)
239+
payload = tmp_path / "payload.json"
240+
payload.write_text(
241+
json.dumps({"session_id": "payload-session", "turn_id": "turn-abc", "cwd": str(repo)}),
242+
encoding="utf-8",
243+
)
244+
245+
result = archive_from_hook_payload(
246+
payload,
247+
db_path=tmp_path / "state.db",
248+
sessions_root=sessions_root,
249+
)
250+
251+
assert result["status"] == "archived"
252+
assert result["session_id"] == "turn-session"
253+
assert result["discovery_reason"] == "recent_turn_id"
254+
255+
183256
def test_archive_from_hook_payload_records_error_for_missing_transcript(tmp_path):
184257
from codex_self_evolution.session_recall.archive import archive_from_hook_payload
185258
from codex_self_evolution.session_recall.store import SessionRecallStore

‎tests/test_session_reflection_validation.py‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,95 @@ def test_validate_receipt_flags_duplicate_only_reflection_memory_writes(tmp_path
196196
]
197197

198198

199+
def test_validate_receipt_flags_duplicate_reflection_refs_without_fixed_noop_phrase(tmp_path: Path) -> None:
200+
"""Reflection refs that only say candidates are duplicate should not be durable memory."""
201+
memory_root = tmp_path / "project" / "memory"
202+
ref = memory_root / "refs" / "20260519T113111Z-csep-memory-skills-duplicate.md"
203+
ref.parent.mkdir(parents=True)
204+
ref.write_text(
205+
"## CSEP 反思作业\n\n"
206+
"本轮 `memory and skills` 复盘复用既有 `csep-reflect-session-receipt` 流程。\n\n"
207+
"- memory 候选:没有新的长期 fact、rule、preference。\n"
208+
"- skill 候选:workflow 路径与既有 skill 等价,判定为 duplicate。\n"
209+
"- durable 变更:仅记录本轮复盘摘要和 receipt.json。\n",
210+
encoding="utf-8",
211+
)
212+
receipt = tmp_path / "receipt.json"
213+
_write_receipt(
214+
receipt,
215+
memory_changes=[{"path": str(ref), "action": "add"}],
216+
skill_changes=[],
217+
)
218+
219+
result = validate_receipt(
220+
receipt,
221+
memory_roots=[memory_root],
222+
skills_root=tmp_path / "skills",
223+
skill_prefix="csep-reflect-",
224+
)
225+
226+
assert result["status"] == "partial"
227+
assert result["low_value_memory_writes"] == [{"reason": "memory_reflection_noop_ref", "path": str(ref)}]
228+
229+
230+
def test_validate_receipt_flags_no_writable_memory_reflection_refs(tmp_path: Path) -> None:
231+
"""No-writable-memory reflection refs are duplicate ledgers even without filename markers."""
232+
memory_root = tmp_path / "project" / "memory"
233+
ref = memory_root / "refs" / "20260519T090154Z-39cbc123.md"
234+
ref.parent.mkdir(parents=True)
235+
ref.write_text(
236+
"# CSEP 复盘记录\n\n"
237+
"- 任务范围:memory and skills。\n"
238+
"- 结果:未发现可写入长期 MEMORY 的新 fact/rule/preference。\n"
239+
"- 判定:继续复用现有 workflow,命中 `csep-reflect-session-receipt`。\n"
240+
"- 处理:本次无生产代码/配置改动、无新技能落地。\n",
241+
encoding="utf-8",
242+
)
243+
receipt = tmp_path / "receipt.json"
244+
_write_receipt(
245+
receipt,
246+
memory_changes=[{"path": str(ref), "action": "add"}],
247+
skill_changes=[],
248+
)
249+
250+
result = validate_receipt(
251+
receipt,
252+
memory_roots=[memory_root],
253+
skills_root=tmp_path / "skills",
254+
skill_prefix="csep-reflect-",
255+
)
256+
257+
assert result["status"] == "partial"
258+
assert result["low_value_memory_writes"] == [{"reason": "memory_reflection_noop_ref", "path": str(ref)}]
259+
260+
261+
def test_validate_receipt_allows_non_reflection_duplicate_memory(tmp_path: Path) -> None:
262+
"""Domain notes may mention duplicate behavior without being reflection noise."""
263+
memory_root = tmp_path / "project" / "memory"
264+
ref = memory_root / "refs" / "order-idempotency.md"
265+
ref.parent.mkdir(parents=True)
266+
ref.write_text(
267+
"订单接口会把 duplicate request id 作为幂等重试处理,调用方可以复用同一个 request id 查询结果。\n",
268+
encoding="utf-8",
269+
)
270+
receipt = tmp_path / "receipt.json"
271+
_write_receipt(
272+
receipt,
273+
memory_changes=[{"path": str(ref), "action": "add"}],
274+
skill_changes=[],
275+
)
276+
277+
result = validate_receipt(
278+
receipt,
279+
memory_roots=[memory_root],
280+
skills_root=tmp_path / "skills",
281+
skill_prefix="csep-reflect-",
282+
)
283+
284+
assert result["status"] == "succeeded"
285+
assert result["low_value_memory_writes"] == []
286+
287+
199288
def test_validate_receipt_rejects_legacy_user_memory(tmp_path: Path) -> None:
200289
"""Legacy USER.md is retained on disk but is no longer writable memory."""
201290
memory = tmp_path / "project" / "memory" / "USER.md"

0 commit comments

Comments
 (0)