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.
- package/README.md +38 -103
- package/lib/api.mjs +3 -5
- package/lib/client.js +39 -84
- package/lib/common/planmode.mjs +32 -0
- package/lib/common/prompts.mjs +6 -8
- package/lib/common/records.mjs +7 -68
- package/lib/common/sections.mjs +35 -11
- package/lib/common/text.mjs +8 -3
- package/lib/distill.mjs +9 -20
- package/lib/hybrid/index.mjs +3 -4
- package/lib/hybrid/subagent.mjs +423 -419
- package/lib/hybrid/tools.mjs +35 -28
- package/lib/index.js +172 -169
- package/lib/projection.mjs +7 -5
- package/lib/tools.mjs +10 -7
- package/package.json +1 -1
package/lib/common/sections.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// 设计约束(v1.6.0 定案):
|
|
5
5
|
// - 全部返回 { ok, reason, ... },永不抛错——LLM 产出的 ops 不可信,机器侧兜底;
|
|
6
6
|
// - 只拆两级标题(`## `),`#` 文件标题与前言归 head,`###` 及更深归当前章节内容;
|
|
7
|
-
// -
|
|
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
|
|
86
|
-
// 不剥会导致 `- - xxx` 双列表符脏数据(v1.6.0 实装踩坑)。
|
|
99
|
+
// entry 先经 flattenEntry 扁平化为单行(防多行/标题注入破坏结构),行首列表符在此一并剥除。
|
|
87
100
|
export function appendToSectionText(md, section, entry) {
|
|
88
|
-
const
|
|
89
|
-
const entryLine = `- ${
|
|
90
|
-
if (!
|
|
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
|
|
104
|
-
|
|
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 = `- ${
|
|
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();
|
package/lib/common/text.mjs
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
14
|
+
* @param {{ ctx: object, getConfig: () => object, paths: object, records: object }} deps
|
|
14
15
|
*/
|
|
15
|
-
export function createDistill({ ctx, getConfig, paths, records
|
|
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
|
-
//
|
|
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
|
|
101
|
-
//
|
|
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,
|
|
391
|
+
return { distillSessionCore, distillProjectMemory, distillLocks };
|
|
403
392
|
}
|
package/lib/hybrid/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// memory-palace
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|