dsh-memory-palace 1.7.2 → 1.8.0-alpha.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.
@@ -4,7 +4,8 @@
4
4
  // 设计约束(v1.6.0 定案):
5
5
  // - 全部返回 { ok, reason, ... },永不抛错——LLM 产出的 ops 不可信,机器侧兜底;
6
6
  // - 只拆两级标题(`## `),`#` 文件标题与前言归 head,`###` 及更深归当前章节内容;
7
- // - 不改动 text.mjs 既有函数(normLine/budgetClip 有 v1.4.1 测试锚定),本文件自持归一化。
7
+ // - text.mjs 的 normLine 不动;budgetClip 已在 v1.8.0-alpha.1 按 D2 修复重写(兑现「保留全部
8
+ // 结构行」声明),v1.4.1 锚点同步更新——本文件自持归一化,不依赖其内部实现。
8
9
 
9
10
  // 章节标题归一化:剥 `#` 前缀、合空白、转小写。标题匹配全部走它,"带不带 ## 前缀"都鲁棒。
10
11
  const normTitle = (s) =>
@@ -80,34 +81,49 @@ export function locateSection(md, section) {
80
81
  return { start, end, lines };
81
82
  }
82
83
 
84
+ // 条目扁平化(v1.8.0-alpha.1 修复 D1):LLM 传来的 entry 可能内嵌换行——含 `\n## x` / `\n# x`
85
+ // 时会原样落盘生成真实章节标题、把原章节从中间劈开(实测结构损坏且无插件路径可复原)。
86
+ // 处理:按行拆开,逐行剥行首列表符(- / *)与井号(#),去空行后以「;」连接为单行——
87
+ // 内容零丢失、结构损坏向量消除。返回 { text, lines }(lines = 非空行数),调用方可在
88
+ // message 里明示「已合并单行」。memory_write 与子代理 log_write_ops 共用,一处修两条路径闭环。
89
+ export function flattenEntry(entry) {
90
+ const parts = String(entry ?? "")
91
+ .split(/\r?\n/)
92
+ .map((l) => l.trim().replace(/^[-*]\s+/, "").replace(/^#+\s*/, "").trim())
93
+ .filter(Boolean);
94
+ return { text: parts.join(";"), lines: parts.length };
95
+ }
96
+
83
97
  // 章节内追加条目。章节存在 → 内容最后一个非空行后插入 `- entry`(全空行则插在标题行后);
84
98
  // 章节不存在 → { ok:false, reason:"section-miss" }(调用方决定是否走新增)。
85
- // entry 写入前剥除行首列表符(`- `/`* `/`-`/`*`,含缩进)——LLM 常按描述示例自带 `- ` 前缀,
86
- // 不剥会导致 `- - xxx` 双列表符脏数据(v1.6.0 实装踩坑)。
99
+ // entry 先经 flattenEntry 扁平化为单行(防多行/标题注入破坏结构),行首列表符在此一并剥除。
87
100
  export function appendToSectionText(md, section, entry) {
88
- const clean = String(entry ?? "").trim().replace(/^[-*]\s+/, "").trim();
89
- const entryLine = `- ${clean}`;
90
- if (!entryLine.slice(2).trim()) return { ok: false, reason: "empty-entry" };
101
+ const f = flattenEntry(entry);
102
+ const entryLine = `- ${f.text}`;
103
+ if (!f.text) return { ok: false, reason: "empty-entry" };
91
104
  const loc = locateSection(md, section);
92
105
  if (!loc) return { ok: false, reason: "section-miss" };
93
106
  const { start, end, lines } = loc;
94
107
  let insertAt = end;
95
108
  while (insertAt > start + 1 && !lines[insertAt - 1].trim()) insertAt--;
96
109
  lines.splice(insertAt, 0, entryLine);
97
- return { ok: true, reason: "appended", text: lines.join("\n") };
110
+ return { ok: true, reason: "appended", text: lines.join("\n"), flattened: f.lines > 1 };
98
111
  }
99
112
 
100
113
  // 章节追加(upsert 语义):存在则追加,不存在则文件尾新增章节。日志 ops 的 "append" 与
101
114
  // memory_write 工具共用——"不要强行匹配现有章节,不符合章节标题就走新增"。
102
115
  export function upsertSectionText(md, section, entry) {
103
- const r = appendToSectionText(md, section, entry);
104
- if (r.ok) return r;
116
+ const f = flattenEntry(entry);
117
+ const r = appendToSectionText(md, section, f.text);
118
+ // appendToSectionText 会把已扁平化的文本再走一次 flattenEntry(幂等)——此时其 flattened
119
+ // 恒为 false,故这里必须用**本函数**的 f.lines 覆盖标记,否则多行 entry 的标记被吃掉。
120
+ if (r.ok) return { ...r, flattened: f.lines > 1 };
105
121
  if (r.reason === "empty-entry") return r;
106
122
  const title = `## ${String(section ?? "").trim()}`;
107
123
  const base = (md || "").replace(/\s+$/, "");
108
- const entryLine = `- ${String(entry ?? "").trim().replace(/^[-*]\s+/, "").trim()}`;
124
+ const entryLine = `- ${f.text}`;
109
125
  const text = base ? `${base}\n\n${title}\n${entryLine}\n` : `${title}\n${entryLine}\n`;
110
- return { ok: true, reason: "section-created", text };
126
+ return { ok: true, reason: "section-created", text, flattened: f.lines > 1 };
111
127
  }
112
128
 
113
129
  // 显式新建章节(与 upsert 的区别:章节已存在时拒绝而非追加)——子 agent 日志 ops 的
@@ -130,6 +146,14 @@ export function replaceSectionText(md, section, oldSectionText, newSectionText)
130
146
  if (normBlock(current) !== normBlock(String(oldSectionText ?? ""))) {
131
147
  return { ok: false, reason: "conflict", actual: current };
132
148
  }
149
+ // v1.8.0-alpha.1 修复(D3):newText 的标题行必须与 section 参数一致——否则会把章节静默
150
+ // 改名(实测标题改写成功而 message 仍回报旧名,掩盖误操作),且 newText 缺标题行时会把
151
+ // 章节标题整行吃掉(内容并入上一章节)。归一化比对(大小写/空白/`##` 前缀差异均容忍)。
152
+ const firstLine = newText.split("\n").find((l) => l.trim())?.trim() ?? "";
153
+ const heading = /^##\s+(.+)$/.exec(firstLine);
154
+ if (!heading || normTitle(heading[1]) !== normTitle(section)) {
155
+ return { ok: false, reason: "heading-mismatch", actual: current };
156
+ }
133
157
  // 保留章节末尾的分隔空行(章节与下一章节之间通常有空行,整块替换时不能吞掉,
134
158
  // 否则下一章节标题会紧贴上一章节最后一条目)。
135
159
  const hasTrailingBlank = end > start && !lines[end - 1].trim();
@@ -90,8 +90,13 @@ export function budgetClip(text, budget) {
90
90
  j--;
91
91
  }
92
92
  if (j < i) return head.join("\n") + (head.length ? "\n" : "") + tail.join("\n");
93
- const marker = `…(已截断,原 ${text.length} 字符,仅注入结构行与尾部最近条目)`;
94
- return [...head, marker, ...tail].join("\n");
93
+ // v1.8.0-alpha.1 修复(D2):中部结构行不再随内容一起丢弃——旧实现把中部的 `## 章节标题`
94
+ // 当普通内容行处理,落在尾部窗口外即消失,而截断标记却自称「仅注入结构行与尾部最近条目」
95
+ // (声明与行为不符,实测中部 3 个章节标题整体丢失)。现把窗口外幸存的结构行按原序
96
+ // 列在标记之后,保证章节大纲始终完整可见。结构行不计预算(大纲优先于条目)。
97
+ const middleStructural = lines.slice(i, j + 1).filter((l) => isStructural(l.trim()));
98
+ const marker = [`…(已截断,原 ${text.length} 字符,仅注入结构行与尾部最近条目)`, ...middleStructural];
99
+ return [...head, ...marker, ...tail].join("\n");
95
100
  }
96
101
 
97
102
  // v1.4.1:剥除行首 `[smart]` 标记(含重复),保留列表符与其余内容。用于注入与回喂的读侧——
@@ -164,7 +169,7 @@ export function extractText(event) {
164
169
 
165
170
  // 从工具执行结果中识别错误。in-session 错误未必以 turn/end 的 reason.kind==='error' 暴露——
166
171
  // 代码运行报错、工具执行失败常以 tool/result 形式呈现,agent 看到错误后正常收尾(reason.kind 为 completed),
167
- // 故需单独扫描工具结果文本。命中即视为错误信号(受 autoCaptureErrors 门控)。
172
+ // 故需单独扫描工具结果文本。命中即视为错误信号(作为 isError 传给记忆子代理)。
168
173
  export function extractToolErrorText(turn) {
169
174
  for (const b of turn) {
170
175
  if (b.role !== "tool") continue;
package/lib/distill.mjs CHANGED
@@ -1,5 +1,6 @@
1
- // memory-palace 蒸馏业务:会话蒸馏核心(自动智能模式 + 按钮「蒸馏会话」共用)+ 项目记忆蒸馏。
2
- // 经 createDistill 工厂注入 ctx/config/paths/records/运行时状态;返回句柄供 index.mjs 装配与 api.mjs 调用。
1
+ // memory-palace 蒸馏业务:会话蒸馏核心(按钮「蒸馏会话」)+ 项目记忆蒸馏。
2
+ // 经 createDistill 工厂注入 ctx/config/paths/records;返回句柄供 index.mjs 装配与 api.mjs 调用。
3
+ // v1.8.0:原 smart 模式自动摘要入口 summarizeTurn 已删除,distillSessionCore 唯一调用方 = 手动按钮。
3
4
  import { mkdir, unlink, writeFile, rename } from "node:fs/promises";
4
5
  import { join, dirname } from "node:path";
5
6
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -10,9 +11,9 @@ import { appendLineDedup, readNumberedMemory, applyMemoryOp } from "./common/rec
10
11
  import { runWithRetry, RETRY_CONSTANTS } from "./common/retry.mjs";
11
12
 
12
13
  /**
13
- * @param {{ ctx: object, getConfig: () => object, paths: object, records: object, state: object }} deps
14
+ * @param {{ ctx: object, getConfig: () => object, paths: object, records: object }} deps
14
15
  */
15
- export function createDistill({ ctx, getConfig, paths, records, state }) {
16
+ export function createDistill({ ctx, getConfig, paths, records }) {
16
17
  const cfg = () => getConfig();
17
18
 
18
19
  // 调试日志(v1.3.0):dbgFail=失败/跳过留痕(无条件输出,不受开关控制,保证排障可观测);
@@ -93,13 +94,12 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
93
94
  }
94
95
 
95
96
  // 蒸馏核心(v1.2.0 抽取):对指定 session 的 [fromSeq, ∞) surface 事件做 LLM 智能摘要并写盘。
96
- // 自动智能模式(summarizeTurn,增量断点)与按钮「蒸馏会话」(全量 fromSeq=0)共用本核心。
97
+ // v1.8.0:唯一调用方 = 按钮「蒸馏会话」(全量 fromSeq=0);原 smart 模式自动摘要链路已删除。
97
98
  // 返回 { ok, summary, durableCount };失败 { ok:false }(调用方各自决定降级策略)。
98
99
  async function distillSessionCore(session, dirs, fromSeq, opts) {
99
100
  const allowDelete = !!(opts && opts.allowDelete);
100
- // 回喂存量记忆(v1.4.0 特性3):智能模式级能力——feedbackEnabled 开启即在【自动智能模式(turn/end)
101
- // 与手动蒸馏按钮】两条链路都回喂项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)。
102
- // delete 仍仅手动蒸馏(allowDelete=true)开放;自动模式 memoryOps 中的 delete 在 §5.5 被 !allowDelete 跳过。
101
+ // 回喂存量记忆(v1.4.0 特性3):feedbackEnabled 开启即回喂项目级 + 用户级 MEMORY.md 全文
102
+ // (逐行编号、无截断)。delete 由 allowDelete 决定(手动按钮传 true)。
103
103
  // 回喂目标严格限定为 MEMORY.md;每日日志(.deepseek-harness/memory/YYYY-MM-DD.md)不读取、不改写、不删旧行。
104
104
  const useFeedback = !!cfg().feedbackEnabled;
105
105
  if (!session) {
@@ -275,17 +275,6 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
275
275
  return { ok: true, summary, durableCount: durable.length };
276
276
  }
277
277
 
278
- // 智能模式核心:LLM 智能会话摘要(增量范围,产物带 [smart] 标记)。
279
- // 输入:capturedTurn(本轮缓冲,仅用于失败降级)、dirs(写盘目标)、isError。
280
- // 核心逻辑在 distillSessionCore;此处只负责增量断点(state.lastSummarizedSeq)的推进。
281
- // 失败返回 false 由调用方降级轻量条目;handler 永不 reject(调用方包 catch)。
282
- async function summarizeTurn(capturedTurn, dirs, isError) {
283
- if (!state.activeSession) return false;
284
- const r = await distillSessionCore(state.activeSession, dirs, state.lastSummarizedSeq);
285
- if (r.ok) state.lastSummarizedSeq = state.activeSession.seq;
286
- return r.ok;
287
- }
288
-
289
278
  // ---------- v1.2.0 项目记忆蒸馏(按钮「蒸馏项目记忆」) ----------
290
279
  // 流程:读主目标 MEMORY.md → DISTILL_PROMPT 蒸馏 → 写 memory-cover.md(同目录,保证 rename 原子)
291
280
  // → 完整性检查 → 备份 MEMORY.md.{时间戳} → rename 覆盖 → cover 随 rename 消失。
@@ -399,5 +388,5 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
399
388
  }
400
389
  }
401
390
 
402
- return { distillSessionCore, summarizeTurn, distillProjectMemory, distillLocks };
391
+ return { distillSessionCore, distillProjectMemory, distillLocks };
403
392
  }
@@ -1,7 +1,6 @@
1
- // memory-palace v1.6.0 hybrid 模式:模块入口(装配)。
2
- // 仅在 memoryMode === "hybrid" 时由 index.mjs 调用 registerHybrid(memoryMode 切换需重启
3
- // dsh——注册时机安全)。不注册任何 DSH 全局资源以外的东西;runMemorySubagent 由 index.mjs
4
- // 的 _settle hybrid 分支每轮调用。
1
+ // memory-palace 记忆子代理模块:模块入口(装配)。
2
+ // v1.8.0:原 hybrid 模式已成唯一写入路径(plugin / smart 已删除),本模块由 index.mjs 无条件装配。
3
+ // 不注册任何 DSH 全局资源以外的东西;runMemorySubagent 由 index.mjs 的 _settle 每轮调用。
5
4
  import { runMemorySubagent } from "./subagent.mjs";
6
5
  import { registerHybridTools, attachHybridGuards } from "./tools.mjs";
7
6