dev-memory-cli 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,6 +41,7 @@ CONTEXT_SCRIPT = PACKAGE_ROOT / "lib" / "dev_memory_context.py"
41
41
  CAPTURE_SCRIPT = PACKAGE_ROOT / "lib" / "dev_memory_capture.py"
42
42
  SUMMARY_WORKER_SCRIPT = PACKAGE_ROOT / "scripts" / "hooks" / "session_summary_worker.py"
43
43
  DEFAULT_CONFIG_PATH = Path(os.environ.get("DEV_MEMORY_CONFIG_PATH", "~/.dev-memory/config.json")).expanduser()
44
+ DEV_MEMORY_HOME = Path(os.environ.get("DEV_MEMORY_HOME", "~/.dev-memory")).expanduser()
44
45
  WORKSPACE_CONFIG_NAMES = (".dev-memory-workspace.json", ".dev-assets-workspace.json")
45
46
 
46
47
 
@@ -121,6 +122,28 @@ def read_hook_input():
121
122
  return {"raw": raw[:4000]}
122
123
 
123
124
 
125
+ def register_session_scan_candidate(hook_input):
126
+ """Persist a lightweight Codex Stop hint without starting summarization."""
127
+ if not isinstance(hook_input, dict):
128
+ return None
129
+ session_id = hook_session_id(hook_input)
130
+ transcript_path = _hook_transcript_path(hook_input)
131
+ if not session_id and not transcript_path:
132
+ return None
133
+ source = session_id or transcript_path
134
+ digest = hashlib.sha1(str(source).encode("utf-8")).hexdigest()[:20]
135
+ path = DEV_MEMORY_HOME / "jobs" / "session-scan" / "candidates" / f"{digest}.json"
136
+ _atomic_write_json(path, {
137
+ "schema_version": 1,
138
+ "event": "Stop",
139
+ "registered_at": now_iso(),
140
+ "session_id": session_id,
141
+ "transcript_path": transcript_path,
142
+ "cwd": _first_string(hook_input.get("cwd"), str(REPO_ROOT)),
143
+ })
144
+ return path
145
+
146
+
124
147
  def resolve_assets_for(repo_root):
125
148
  """Resolve asset paths for an explicit repo root (workspace-mode friendly)."""
126
149
  repo_root_str = str(repo_root)
@@ -219,7 +242,7 @@ def extract_section(path, title):
219
242
  return None if is_placeholder(body) else body
220
243
 
221
244
 
222
- def extract_repo_file_body(path):
245
+ def extract_repo_file_body(path, *, newest_first=False):
223
246
  """Extract full body of a repo-level file, skipping the H1 title and
224
247
  ``## 仓库`` metadata section. Placeholder-only sections within the file
225
248
  are dropped individually so that real content in sibling sections is
@@ -235,6 +258,11 @@ def extract_repo_file_body(path):
235
258
  for sec in sections:
236
259
  sec_body = strip_managed_markers(sec).strip()
237
260
  if sec_body and not is_placeholder(sec_body):
261
+ if newest_first and sec_body.startswith("## "):
262
+ heading, separator, body = sec_body.partition("\n")
263
+ if separator and body.strip():
264
+ blocks = _split_recent_blocks(body)
265
+ sec_body = heading + "\n\n" + "\n\n".join(blocks)
238
266
  kept.append(sec_body)
239
267
  body = "\n\n".join(kept).strip()
240
268
  return body or None
@@ -443,11 +471,14 @@ def _brief_section_keys(profile):
443
471
  return tuple(_BRIEF_PROFILES.get(profile, _BRIEF_PROFILES["standard"]).keys())
444
472
 
445
473
 
446
- def _extract_sections(paths, keys):
474
+ def _extract_sections(paths, keys, *, repo_newest_first=False):
447
475
  out = []
448
476
  for file_key, title in keys:
449
477
  if title is None:
450
- body = extract_repo_file_body(paths[file_key])
478
+ body = extract_repo_file_body(
479
+ paths[file_key],
480
+ newest_first=repo_newest_first and file_key in {"repo_decisions", "repo_glossary"},
481
+ )
451
482
  display_title = _REPO_DISPLAY_TITLES.get(file_key, paths[file_key].stem)
452
483
  else:
453
484
  body = extract_section(paths[file_key], title)
@@ -459,19 +490,18 @@ def _extract_sections(paths, keys):
459
490
 
460
491
  def _build_context_from_assets(assets, *, full=True, heading=None, brief_profile="standard"):
461
492
  if not assets["branch_dir"].exists():
462
- # v2: capture lazy-inits on first write, so branch_dir typically
463
- # exists after any real interaction. Missing here just means no
464
- # write has happened yet — no need to push setup.
493
+ # Memory is opt-in. A missing branch directory stays silent in
494
+ # workspace briefs; the primary repo gets a concise init hint.
465
495
  if heading is None:
466
496
  return (
467
497
  "当前仓库+分支还没有 dev-memory 记忆。"
468
- "下一次 `dev-memory-capture` 写入时会自动 lazy init;现有结论若值得记一笔,直接走 capture。"
498
+ "需要启用时运行 `dev-memory-cli init`;之后由定期 session-scan 沉淀会话语义。"
469
499
  )
470
500
  return None
471
501
 
472
502
  paths = assets["paths"]
473
503
  keys = _FULL_SECTION_KEYS if full else _brief_section_keys(brief_profile)
474
- sections = _extract_sections(paths, keys)
504
+ sections = _extract_sections(paths, keys, repo_newest_first=full)
475
505
  max_lines, max_chars = (8, 700) if full else (3, 200)
476
506
  brief_limits = _BRIEF_PROFILES.get(brief_profile, _BRIEF_PROFILES["standard"])
477
507
 
@@ -586,7 +616,7 @@ def _build_context_from_assets(assets, *, full=True, heading=None, brief_profile
586
616
  "",
587
617
  f"归档分支查询:`grep -r 'KEYWORD' {archive_dir}/` (体量大时派 Task 子 agent)",
588
618
  ])
589
- footer_lines.extend(["", "新决策 / 进展 / 阻塞 → `dev-memory-capture` 写入。"])
619
+ footer_lines.extend(["", "新增会话语义由定期 session-scan 沉淀;主动查旧记忆使用 `dev-memory-read`。"])
590
620
  parts.append("\n".join(footer_lines))
591
621
 
592
622
  return "\n\n".join(parts)
@@ -898,9 +928,14 @@ def build_summary_input(job_path):
898
928
  job = json.loads(Path(job_path).read_text(encoding="utf-8"))
899
929
  return extract_core_payload(
900
930
  job,
901
- max_messages=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGES", 30),
902
- max_message_chars=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGE_CHARS", 1600),
931
+ max_messages=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGES", 0),
932
+ max_message_chars=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGE_CHARS", 0),
903
933
  max_memory_chars=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MEMORY_CHARS", 4000),
934
+ since_size=(
935
+ ((job.get("previous_job") or {}).get("processed") or {}).get("transcript_size", 0)
936
+ if isinstance(job.get("previous_job"), dict)
937
+ else 0
938
+ ),
904
939
  )
905
940
 
906
941
 
@@ -918,7 +953,8 @@ def build_summary_prompt(job_path, summary_input=None, summary_input_path=None):
918
953
  summary_input = build_summary_input(job_path)
919
954
  summary_input_json = json.dumps(summary_input, ensure_ascii=False, indent=2)
920
955
  input_path_line = f"- summary input JSON: {summary_input_path}\n" if summary_input_path else ""
921
- return f"""你是 dev-memory 的后台会话总结 worker。
956
+ return f"""DEV_MEMORY_INTERNAL_SESSION_SUMMARY_V1
957
+ 你是 dev-memory 的后台会话总结 worker。
922
958
 
923
959
  输入:
924
960
  - job JSON: {job_path}
@@ -5,13 +5,19 @@ from _common import (
5
5
  is_workspace_mode,
6
6
  log,
7
7
  maybe_record_head,
8
+ read_hook_input,
8
9
  record_head_all_repos,
10
+ register_session_scan_candidate,
9
11
  resolve_assets,
10
12
  )
11
13
 
12
14
 
13
15
  def main():
16
+ hook_input = read_hook_input()
14
17
  try:
18
+ candidate = register_session_scan_candidate(hook_input)
19
+ if candidate:
20
+ log(f"[dev-memory][Stop] registered session scan candidate {candidate}")
15
21
  if is_no_git_mode():
16
22
  log("[dev-memory][Stop] no-git mode: nothing to record (no HEAD)")
17
23
  return 0
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "suite_name": "dev-memory-skill-suite",
3
3
  "skills": [
4
- "dev-memory-read",
4
+ "dev-memory-read"
5
+ ],
6
+ "legacy_skills": [
5
7
  "dev-memory-setup",
6
8
  "dev-memory-capture",
7
9
  "dev-memory-graduate",
8
- "dev-memory-tidy"
9
- ],
10
- "legacy_skills": [
10
+ "dev-memory-tidy",
11
11
  "branch-context",
12
12
  "branch-context-setup",
13
13
  "dev-asset-setup",