dev-memory-cli 0.22.1 → 0.22.2

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.
@@ -6,7 +6,7 @@ lazy-inits files on first write. Setup's job is now:
6
6
 
7
7
  1. Ensure skeleton exists (idempotent).
8
8
  2. Scan unsorted.md and present each entry to the user for classification.
9
- 3. Route user's choices into decisions/progress/risks/glossary/shared-*.
9
+ 3. Route user's choices into decisions/risks/glossary/shared-*.
10
10
  4. Mark manifest.setup_completed = true.
11
11
  """
12
12
 
@@ -92,7 +92,7 @@ def command_init(args):
92
92
  # plan.json format:
93
93
  # {
94
94
  # "classifications": [
95
- # {"entry": "original bullet text", "kind": "decision|progress|next|risk|glossary|source|shared-*|skip"},
95
+ # {"entry": "original bullet text", "kind": "decision|risk|glossary|source|shared-*|skip"},
96
96
  # ...
97
97
  # ],
98
98
  # "clear_unsorted_on_done": true
@@ -138,7 +138,12 @@ def _apply_classifications(paths, classifications):
138
138
  from dev_memory_common import append_to_section
139
139
  body = "\n".join(f"- {e}" for e in entries)
140
140
  path = paths[file_key]
141
- append_to_section(path, section, body)
141
+ append_to_section(
142
+ path,
143
+ section,
144
+ body,
145
+ max_entries=20 if file_key.startswith("repo_") else None,
146
+ )
142
147
  tally[f"{file_key}:{section}"] = len(entries)
143
148
  return {"applied": tally, "skipped": skipped}
144
149
 
@@ -68,16 +68,26 @@ def _extract_codex(obj):
68
68
  }
69
69
 
70
70
 
71
- def _iter_core_messages(transcript_path):
71
+ def _iter_core_messages(transcript_path, since_size=0):
72
72
  if not transcript_path:
73
73
  return []
74
74
  path = Path(transcript_path).expanduser()
75
75
  if not path.exists():
76
76
  return []
77
77
  out = []
78
- with path.open(encoding="utf-8") as f:
79
- for line in f:
80
- line = line.strip()
78
+ with path.open("rb") as f:
79
+ while True:
80
+ start = f.tell()
81
+ raw = f.readline()
82
+ if not raw:
83
+ break
84
+ end = f.tell()
85
+ if end <= since_size:
86
+ continue
87
+ try:
88
+ line = raw.decode("utf-8").strip()
89
+ except UnicodeDecodeError:
90
+ continue
81
91
  if not line:
82
92
  continue
83
93
  try:
@@ -86,12 +96,16 @@ def _iter_core_messages(transcript_path):
86
96
  continue
87
97
  msg = _extract_claude(obj) or _extract_codex(obj)
88
98
  if msg:
99
+ msg["start_offset"] = start
100
+ msg["end_offset"] = end
89
101
  out.append(msg)
90
102
  return out
91
103
 
92
104
 
93
105
  def _truncate(text, max_chars):
94
106
  text = (text or "").strip()
107
+ if not max_chars or max_chars < 0:
108
+ return text
95
109
  if len(text) <= max_chars:
96
110
  return text
97
111
  return text[: max_chars - 3].rstrip() + "..."
@@ -142,8 +156,8 @@ def _summary_job(job):
142
156
  def extract_core_payload(
143
157
  job,
144
158
  *,
145
- max_messages=40,
146
- max_message_chars=2000,
159
+ max_messages=0,
160
+ max_message_chars=0,
147
161
  max_memory_chars=6000,
148
162
  since_size=0,
149
163
  include_message_metadata=False,
@@ -159,14 +173,9 @@ def extract_core_payload(
159
173
  ("repo/decisions.md", repo_dir / "repo" / "decisions.md"),
160
174
  ("repo/glossary.md", repo_dir / "repo" / "glossary.md"),
161
175
  ]
162
- messages = _iter_core_messages(job.get("transcript_path"))
163
- if since_size:
164
- # JSONL is line-oriented but byte offsets can land mid-line. Keep this
165
- # option conservative for now: expose cursor metadata and let workers
166
- # use recent-tail until a precise offset reader is needed.
167
- pass
176
+ messages = _iter_core_messages(job.get("transcript_path"), since_size=since_size)
168
177
  messages = [m for m in messages if not _is_nonsemantic_user_text(m["text"])]
169
- recent = messages[-max_messages:]
178
+ recent = messages[-max_messages:] if max_messages and max_messages > 0 else messages
170
179
  core_messages = []
171
180
  for m in recent:
172
181
  item = {
@@ -213,10 +222,10 @@ def main():
213
222
  sub = parser.add_subparsers(dest="command", required=True)
214
223
  p = sub.add_parser("extract-core", help="Extract core transcript messages and current memory for a summary job")
215
224
  p.add_argument("job", help="Path to a session-summary pending job JSON")
216
- p.add_argument("--max-messages", type=int, default=40)
217
- p.add_argument("--max-message-chars", type=int, default=2000)
225
+ p.add_argument("--max-messages", type=int, default=0, help="0 keeps every semantic message")
226
+ p.add_argument("--max-message-chars", type=int, default=0, help="0 keeps complete message text")
218
227
  p.add_argument("--max-memory-chars", type=int, default=6000)
219
- p.add_argument("--since-size", type=int, default=0, help="Reserved cursor hint for future byte-offset extraction")
228
+ p.add_argument("--since-size", type=int, default=0, help="Read JSONL records ending after this byte offset")
220
229
  p.add_argument(
221
230
  "--include-message-metadata",
222
231
  action="store_true",
package/lib/ui-app.html CHANGED
@@ -75,6 +75,14 @@ button { font-family: inherit; }
75
75
  .topbar .brand { display: flex; align-items: baseline; gap: 12px; min-width: 0; }
76
76
  .topbar .title { font-weight: 600; font-size: 15px; }
77
77
  .topbar .meta { color: var(--fg-muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
+ .view-tabs { display: flex; gap: 4px; }
79
+ .view-tab { padding: 6px 10px; border: 1px solid var(--border); background: var(--bg-card); color: var(--fg); border-radius: var(--radius-sm); cursor: pointer; }
80
+ .view-tab.active { color: var(--accent-fg); background: var(--accent); border-color: var(--accent); }
81
+ .scan-table { width: 100%; border-collapse: collapse; background: var(--bg-card); border: 1px solid var(--border); font-size: 12px; }
82
+ .scan-table th, .scan-table td { padding: 9px 10px; text-align: left; border-bottom: 1px solid var(--border-subtle); vertical-align: top; }
83
+ .scan-table th { color: var(--fg-muted); background: var(--bg); font-weight: 600; }
84
+ .scan-table tr:last-child td { border-bottom: 0; }
85
+ .scan-table .num { text-align: right; font-variant-numeric: tabular-nums; }
78
86
 
79
87
  .main { display: grid; grid-template-columns: 320px 1fr; overflow: hidden; min-height: 0; }
80
88
 
@@ -453,6 +461,10 @@ button.icon-btn:hover {
453
461
  <span class="meta" id="meta">加载中…</span>
454
462
  </div>
455
463
  <div class="actions">
464
+ <span class="view-tabs">
465
+ <button class="view-tab active" id="memoryView">记忆</button>
466
+ <button class="view-tab" id="scanView">会话扫描</button>
467
+ </span>
456
468
  <button class="icon-btn" id="reload" title="重新加载">↻ 刷新</button>
457
469
  </div>
458
470
  </div>
@@ -490,6 +502,8 @@ button.icon-btn:hover {
490
502
 
491
503
  var state = {
492
504
  tree: null,
505
+ scan: null,
506
+ view: "memory",
493
507
  selectedRepoKey: null,
494
508
  selectedBranch: null,
495
509
  fileCache: new Map(),
@@ -672,6 +686,117 @@ function stat(label, value, small) {
672
686
  );
673
687
  }
674
688
 
689
+ function formatBytes(value) {
690
+ var n = Number(value || 0);
691
+ if (n < 1024) return n + " B";
692
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
693
+ if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + " MB";
694
+ return (n / 1024 / 1024 / 1024).toFixed(2) + " GB";
695
+ }
696
+
697
+ function renderScan() {
698
+ var data = state.scan;
699
+ var content = q("#content");
700
+ var sidebar = q(".sidebar");
701
+ sidebar.style.display = "none";
702
+ content.style.gridColumn = "1 / -1";
703
+ content.innerHTML = "";
704
+ if (!data || !data.exists) {
705
+ content.appendChild(el("div", { class: "placeholder" }, "暂无会话扫描记录"));
706
+ return;
707
+ }
708
+ content.appendChild(el("div", { class: "repo-header" }, el("h2", null, "会话扫描"), el("div", { class: "path mono" }, data.scanRoot)));
709
+ var totals = el("div", { class: "stat-row" });
710
+ totals.appendChild(stat("扫描运行", data.run_count));
711
+ totals.appendChild(stat("涉及仓库", data.repos.length));
712
+ totals.appendChild(stat("总结 Token", data.usage ? (data.usage.total_tokens || 0).toLocaleString() : "未上报"));
713
+ totals.appendChild(stat("Token 未上报调用", data.unavailable_invocations));
714
+ content.appendChild(totals);
715
+
716
+ content.appendChild(el("h3", { class: "section-title" }, "仓库统计"));
717
+ var repoTable = el("table", { class: "scan-table" });
718
+ repoTable.innerHTML = "<thead><tr><th>仓库 / 分支</th><th class='num'>扫描次数</th><th class='num'>会话</th><th class='num'>原始大小</th><th class='num'>新增数据</th><th class='num'>总结 Token</th><th>最近扫描</th></tr></thead>";
719
+ var repoBody = el("tbody");
720
+ data.repos.forEach(function (repo) {
721
+ repoBody.appendChild(el("tr", null,
722
+ el("td", null, el("div", { class: "mono" }, repo.repo_key), el("div", { class: "sub" }, repo.branches.join(", "))),
723
+ el("td", { class: "num" }, repo.scan_count),
724
+ el("td", { class: "num" }, repo.session_count),
725
+ el("td", { class: "num" }, formatBytes(repo.raw_bytes)),
726
+ el("td", { class: "num" }, formatBytes(repo.new_bytes)),
727
+ el("td", { class: "num" }, Number(repo.summary_tokens || 0).toLocaleString()),
728
+ el("td", null, formatDateTime(repo.last_scanned_at))
729
+ ));
730
+ });
731
+ repoTable.appendChild(repoBody);
732
+ content.appendChild(repoTable);
733
+
734
+ content.appendChild(el("h3", { class: "section-title" }, "最近运行"));
735
+ var runTable = el("table", { class: "scan-table" });
736
+ runTable.innerHTML = "<thead><tr><th>时间 / Run ID</th><th>执行器</th><th class='num'>会话</th><th class='num'>新增数据</th><th class='num'>Token</th><th>结果</th></tr></thead>";
737
+ var runBody = el("tbody");
738
+ data.runs.forEach(function (run) {
739
+ var usage = run.summary_usage && run.summary_usage.total_tokens;
740
+ runBody.appendChild(el("tr", null,
741
+ el("td", null, el("div", null, formatDateTime(run.started_at)), el("div", { class: "mono" }, run.run_id)),
742
+ el("td", null, [run.executor || "-", run.model || ""].filter(Boolean).join(" / ")),
743
+ el("td", { class: "num" }, run.session_count),
744
+ el("td", { class: "num" }, formatBytes(run.new_bytes)),
745
+ el("td", { class: "num" }, usage == null ? "未上报" : Number(usage).toLocaleString()),
746
+ el("td", null, run.done_count + " 完成 · " + run.failed_count + " 失败 · " + run.skipped_count + " 跳过")
747
+ ));
748
+ });
749
+ runTable.appendChild(runBody);
750
+ content.appendChild(runTable);
751
+
752
+ var recentSessions = [];
753
+ data.runs.forEach(function (run) {
754
+ (run.sessions || []).forEach(function (session) {
755
+ recentSessions.push({ run_id: run.run_id, started_at: run.started_at, session: session });
756
+ });
757
+ });
758
+ recentSessions.sort(function (a, b) { return String(b.started_at || "").localeCompare(String(a.started_at || "")); });
759
+ recentSessions = recentSessions.slice(0, 200);
760
+ content.appendChild(el("h3", { class: "section-title" }, "最近会话"));
761
+ var sessionTable = el("table", { class: "scan-table" });
762
+ sessionTable.innerHTML = "<thead><tr><th>会话 / 仓库</th><th class='num'>原始大小</th><th class='num'>本次新增</th><th class='num'>原会话 Token</th><th class='num'>总结 Token</th><th>状态</th></tr></thead>";
763
+ var sessionBody = el("tbody");
764
+ recentSessions.forEach(function (row) {
765
+ var session = row.session;
766
+ sessionBody.appendChild(el("tr", null,
767
+ el("td", null,
768
+ el("div", { class: "mono" }, session.session_id || "-"),
769
+ el("div", null, [session.repo_key, session.branch].filter(Boolean).join(" / ")),
770
+ el("div", { class: "sub mono" }, session.path || "")
771
+ ),
772
+ el("td", { class: "num" }, formatBytes(session.raw_size)),
773
+ el("td", { class: "num" }, formatBytes(session.new_bytes)),
774
+ el("td", { class: "num" }, session.session_usage && session.session_usage.total_tokens != null ? Number(session.session_usage.total_tokens).toLocaleString() : "未上报"),
775
+ el("td", { class: "num" }, session.summary_usage && session.summary_usage.total_tokens != null ? Number(session.summary_usage.total_tokens).toLocaleString() : "未上报"),
776
+ el("td", null, session.status + (session.reason ? " · " + session.reason : ""))
777
+ ));
778
+ });
779
+ sessionTable.appendChild(sessionBody);
780
+ content.appendChild(sessionTable);
781
+ }
782
+
783
+ function setView(view) {
784
+ state.view = view;
785
+ q("#memoryView").classList.toggle("active", view === "memory");
786
+ q("#scanView").classList.toggle("active", view === "scan");
787
+ if (view === "scan") {
788
+ fetch("/api/session-scan").then(function (r) { return r.json(); }).then(function (data) {
789
+ state.scan = data;
790
+ q("#meta").textContent = data.run_count + " 次扫描 · " + data.repos.length + " 个仓库 · " + data.scanRoot;
791
+ renderScan();
792
+ }).catch(function (err) { q("#meta").textContent = "错误:" + err.message; });
793
+ } else {
794
+ q(".sidebar").style.display = "block";
795
+ q("#content").style.gridColumn = "";
796
+ loadTree();
797
+ }
798
+ }
799
+
675
800
  function renderRepoList(repos, filterText) {
676
801
  var list = q("#repoList");
677
802
  list.innerHTML = "";
@@ -1068,8 +1193,10 @@ q("#search").addEventListener("input", function (e) {
1068
1193
  });
1069
1194
  q("#reload").addEventListener("click", function () {
1070
1195
  state.fileCache.clear();
1071
- loadTree();
1196
+ setView(state.view);
1072
1197
  });
1198
+ q("#memoryView").addEventListener("click", function () { setView("memory"); });
1199
+ q("#scanView").addEventListener("click", function () { setView("scan"); });
1073
1200
  q("#modalClose").addEventListener("click", closeModal);
1074
1201
  q("#modalEdit").addEventListener("click", enterEditMode);
1075
1202
  q("#modalSave").addEventListener("click", saveModalEdit);
package/lib/ui-server.js CHANGED
@@ -13,6 +13,11 @@ function getStorageRoot() {
13
13
  return process.env.DEV_ASSETS_ROOT || DEFAULT_STORAGE_ROOT;
14
14
  }
15
15
 
16
+ function getScanRoot() {
17
+ return process.env.DEV_MEMORY_SCAN_ROOT
18
+ || path.join(path.dirname(getStorageRoot()), "jobs", "session-scan");
19
+ }
20
+
16
21
  function safeReadDir(dir) {
17
22
  try {
18
23
  return fs.readdirSync(dir, { withFileTypes: true });
@@ -135,6 +140,62 @@ function buildTree() {
135
140
  return { storageRoot: root, exists: true, repos };
136
141
  }
137
142
 
143
+ function buildSessionScanData() {
144
+ const root = getScanRoot();
145
+ const runsDir = path.join(root, "runs");
146
+ const runs = [];
147
+ for (const ent of safeReadDir(runsDir)) {
148
+ if (!ent.isFile() || !ent.name.endsWith(".json")) continue;
149
+ const value = safeReadJson(path.join(runsDir, ent.name));
150
+ if (value && typeof value === "object") runs.push(value);
151
+ }
152
+ runs.sort((a, b) => String(b.started_at || "").localeCompare(String(a.started_at || "")));
153
+ const repos = new Map();
154
+ const usage = {};
155
+ let unavailableInvocations = 0;
156
+ for (const run of runs) {
157
+ unavailableInvocations += Number(run.usage_unavailable_invocations || 0);
158
+ for (const [key, value] of Object.entries(run.summary_usage || {})) {
159
+ if (Number.isFinite(value)) usage[key] = (usage[key] || 0) + value;
160
+ }
161
+ for (const session of run.sessions || []) {
162
+ if (!session.repo_key) continue;
163
+ const row = repos.get(session.repo_key) || {
164
+ repo_key: session.repo_key,
165
+ branches: new Set(), sessions: new Set(), scan_count: 0,
166
+ raw_bytes: 0, new_bytes: 0, summary_tokens: 0, done: 0, failed: 0, last_scanned_at: null,
167
+ };
168
+ row.scan_count += 1;
169
+ row.sessions.add(session.session_id);
170
+ if (session.branch) row.branches.add(session.branch);
171
+ row.raw_bytes += Number(session.raw_size || 0);
172
+ row.new_bytes += Number(session.new_bytes || 0);
173
+ row.summary_tokens += Number((session.summary_usage || {}).total_tokens || 0);
174
+ if (session.status === "done") row.done += 1;
175
+ if (session.status === "failed") row.failed += 1;
176
+ if (!row.last_scanned_at || session.last_scanned_at > row.last_scanned_at) {
177
+ row.last_scanned_at = session.last_scanned_at;
178
+ }
179
+ repos.set(session.repo_key, row);
180
+ }
181
+ }
182
+ const repoRows = Array.from(repos.values()).map((row) => ({
183
+ ...row,
184
+ branches: Array.from(row.branches).sort(),
185
+ session_count: row.sessions.size,
186
+ sessions: undefined,
187
+ })).sort((a, b) => String(b.last_scanned_at || "").localeCompare(String(a.last_scanned_at || "")));
188
+ return {
189
+ scanRoot: root,
190
+ exists: !!safeStat(root),
191
+ run_count: runs.length,
192
+ usage: Object.keys(usage).length ? usage : null,
193
+ unavailable_invocations: unavailableInvocations,
194
+ repos: repoRows,
195
+ runs: runs.slice(0, 100),
196
+ };
197
+ }
198
+
138
199
  function resolveSafePath(relPath) {
139
200
  const root = getStorageRoot();
140
201
  if (!relPath || typeof relPath !== "string") return null;
@@ -299,6 +360,11 @@ function start({ host = "127.0.0.1", port = 0, openBrowserFlag = true, readOnly
299
360
  res.end(JSON.stringify(tree));
300
361
  return;
301
362
  }
363
+ if (url.pathname === "/api/session-scan" && isRead) {
364
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
365
+ res.end(JSON.stringify(buildSessionScanData()));
366
+ return;
367
+ }
302
368
  if (url.pathname === "/api/injection-preview" && isRead) {
303
369
  const repoKey = url.searchParams.get("repo") || "";
304
370
  const branch = url.searchParams.get("branch") || "";
@@ -380,4 +446,4 @@ function start({ host = "127.0.0.1", port = 0, openBrowserFlag = true, readOnly
380
446
  return server;
381
447
  }
382
448
 
383
- module.exports = { start, buildTree, getStorageRoot };
449
+ module.exports = { start, buildTree, buildSessionScanData, getStorageRoot, getScanRoot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev-memory-cli",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "CLI for dev-memory hooks and repo-local setup (formerly @xluos/dev-assets-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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)
@@ -471,7 +502,7 @@ def _build_context_from_assets(assets, *, full=True, heading=None, brief_profile
471
502
 
472
503
  paths = assets["paths"]
473
504
  keys = _FULL_SECTION_KEYS if full else _brief_section_keys(brief_profile)
474
- sections = _extract_sections(paths, keys)
505
+ sections = _extract_sections(paths, keys, repo_newest_first=full)
475
506
  max_lines, max_chars = (8, 700) if full else (3, 200)
476
507
  brief_limits = _BRIEF_PROFILES.get(brief_profile, _BRIEF_PROFILES["standard"])
477
508
 
@@ -898,9 +929,14 @@ def build_summary_input(job_path):
898
929
  job = json.loads(Path(job_path).read_text(encoding="utf-8"))
899
930
  return extract_core_payload(
900
931
  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),
932
+ max_messages=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGES", 0),
933
+ max_message_chars=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MESSAGE_CHARS", 0),
903
934
  max_memory_chars=_int_env("DEV_MEMORY_SESSION_SUMMARY_MAX_MEMORY_CHARS", 4000),
935
+ since_size=(
936
+ ((job.get("previous_job") or {}).get("processed") or {}).get("transcript_size", 0)
937
+ if isinstance(job.get("previous_job"), dict)
938
+ else 0
939
+ ),
904
940
  )
905
941
 
906
942
 
@@ -918,7 +954,8 @@ def build_summary_prompt(job_path, summary_input=None, summary_input_path=None):
918
954
  summary_input = build_summary_input(job_path)
919
955
  summary_input_json = json.dumps(summary_input, ensure_ascii=False, indent=2)
920
956
  input_path_line = f"- summary input JSON: {summary_input_path}\n" if summary_input_path else ""
921
- return f"""你是 dev-memory 的后台会话总结 worker。
957
+ return f"""DEV_MEMORY_INTERNAL_SESSION_SUMMARY_V1
958
+ 你是 dev-memory 的后台会话总结 worker。
922
959
 
923
960
  输入:
924
961
  - 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