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.
- package/README.md +255 -291
- package/bin/dev-memory.js +125 -2
- package/hooks/README.md +181 -47
- package/hooks/codex-hooks.json +1 -1
- package/hooks/hooks.json +1 -1
- package/lib/dev_memory_capture.py +152 -11
- package/lib/dev_memory_common.py +68 -19
- package/lib/dev_memory_graduate.py +1 -1
- package/lib/dev_memory_session_scan.py +1565 -0
- package/lib/dev_memory_setup.py +8 -3
- package/lib/dev_memory_summary.py +25 -16
- package/lib/maintenance/archive.md +84 -0
- package/lib/maintenance/tidy.md +103 -0
- package/lib/ui-app.html +128 -1
- package/lib/ui-server.js +67 -1
- package/package.json +2 -1
- package/scripts/hooks/_common.py +48 -12
- package/scripts/hooks/stop.py +6 -0
- package/suite-manifest.json +4 -4
package/scripts/hooks/_common.py
CHANGED
|
@@ -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(
|
|
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
|
-
#
|
|
463
|
-
#
|
|
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
|
-
"
|
|
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(["", "
|
|
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",
|
|
902
|
-
max_message_chars=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGE_CHARS",
|
|
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"""
|
|
956
|
+
return f"""DEV_MEMORY_INTERNAL_SESSION_SUMMARY_V1
|
|
957
|
+
你是 dev-memory 的后台会话总结 worker。
|
|
922
958
|
|
|
923
959
|
输入:
|
|
924
960
|
- job JSON: {job_path}
|
package/scripts/hooks/stop.py
CHANGED
|
@@ -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
|
package/suite-manifest.json
CHANGED
|
@@ -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",
|