dsh-memory-palace 1.4.1 → 1.6.1
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 +181 -166
- package/lib/api.mjs +17 -0
- package/lib/client.js +435 -414
- package/lib/common/prompts.mjs +7 -4
- package/lib/common/records.mjs +7 -2
- package/lib/common/sections.mjs +167 -0
- package/lib/common/text.mjs +51 -5
- package/lib/distill.mjs +6 -3
- package/lib/hybrid/index.mjs +18 -0
- package/lib/hybrid/prompts.mjs +42 -0
- package/lib/hybrid/subagent.mjs +349 -0
- package/lib/hybrid/tools.mjs +327 -0
- package/lib/index.js +356 -308
- package/package.json +56 -56
package/lib/common/prompts.mjs
CHANGED
|
@@ -52,11 +52,12 @@ export function DISTILL_PROMPT({ outputBudget } = {}) {
|
|
|
52
52
|
"只保留「以后还会用到」的信息,删掉「一次性过程」。\n" +
|
|
53
53
|
"唯一判断标准:删了这条,下次开工会不会出错 / 返工 / 重踩坑?会 → 留;不会 → 删。\n" +
|
|
54
54
|
"\n" +
|
|
55
|
-
"
|
|
55
|
+
"【只提炼「以后还会用到」的信息,无关的类别直接省略,不要硬凑】\n" +
|
|
56
|
+
"常见归档维度(参考,可按材料实际内容增删维度):\n" +
|
|
56
57
|
"1. 定位 —— 一句话说清任务本质 + 最终产出什么。\n" +
|
|
57
58
|
"2. 当前状态 —— 现在推进到哪一步、卡在哪、下一步是什么。\n" +
|
|
58
59
|
"3. 核心约束 —— 什么绝对不能变。注意:丢了会出事的才叫约束,丢了只是不方便的是细节,细节不写。\n" +
|
|
59
|
-
"4. AI 行为边界 —— AI
|
|
60
|
+
"4. AI 行为边界 —— AI 能做什么、绝不能做什么、什么留给用户。若设置该章节,则所有「谁来做 / 谁授权 / 谁拍板」的约定集中于此。\n" +
|
|
60
61
|
"5. 关键路径 —— 绝对路径、命令、数据源、模板、依赖。原样精确保留,禁止用占位符。\n" +
|
|
61
62
|
"6. 决策记录 —— 为什么这么做(而非只记做了什么),尽量带日期或版本锚点。废弃方案单独标注为「已废弃」,不要删除。\n" +
|
|
62
63
|
"7. 坑与教训 —— 现象 → 根因 → 解法,一条一个坑。一句话能讲清的坑不算坑、不记。\n" +
|
|
@@ -67,7 +68,9 @@ export function DISTILL_PROMPT({ outputBudget } = {}) {
|
|
|
67
68
|
"- 精确值:路径、命令、字段名、版本号原样保留,禁用 ~/、占位符或「大概」。\n" +
|
|
68
69
|
"- 时间锚点:决策和坑尽量带日期或版本,让人知道这是哪个阶段的结论。\n" +
|
|
69
70
|
"\n" +
|
|
70
|
-
|
|
71
|
+
// v1.4.1:骨架从强制改为参考——章节可按内容增删/合并/改名,以契合为先(修实测的
|
|
72
|
+
// 「章节与内容不匹配」)。下方自检第 6 条同步翻转为「查错位」,防收尾自检把自由度撤销。
|
|
73
|
+
"【输出格式】参考以下骨架组织输出,章节可按材料实际内容增删/合并/改名,以内容与章节契合为先,无关章节不硬凑:\n" +
|
|
71
74
|
"\n" +
|
|
72
75
|
"# <项目名> 项目笔记\n" +
|
|
73
76
|
"<一句话定位 + 当前状态>\n" +
|
|
@@ -93,7 +96,7 @@ export function DISTILL_PROMPT({ outputBudget } = {}) {
|
|
|
93
96
|
"3. 边界是否显式、无歧义、没有混在别的条目里?\n" +
|
|
94
97
|
"4. 路径 / 命令 / 版本号是否精确?\n" +
|
|
95
98
|
"5. 每个坑是否「现象 → 根因 → 解法」三要素齐全?\n" +
|
|
96
|
-
"6.
|
|
99
|
+
"6. 每个条目是否归在了最契合的章节?有没有为凑骨架而错位归类?\n" +
|
|
97
100
|
"\n" +
|
|
98
101
|
(outputBudget != null ? "【输出长度约束】你提炼后的项目记忆请控制在约 " + outputBudget + " token(≈" + Math.round(outputBudget * 1.8) + " 字)以内。思考不受限,但成稿须精炼、不超预算;只保留「以后还会用到」的信息,删掉「一次性过程」。\n" : "") +
|
|
99
102
|
"现在开始处理我提供的材料。"
|
package/lib/common/records.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { mkdir, readdir, unlink, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
6
|
-
import { todayISO, expandHome, readMdSync, toHomeShort, normLine, isStructural } from "./text.mjs";
|
|
6
|
+
import { todayISO, expandHome, readMdSync, toHomeShort, normLine, isStructural, stripSmartTag } from "./text.mjs";
|
|
7
7
|
|
|
8
8
|
// 追加一行记忆,若目标文件已包含相同内容则跳过(去重),返回是否实际写入。
|
|
9
9
|
export async function appendLineDedup(file, line) {
|
|
@@ -78,6 +78,8 @@ export async function removeLineByMatch(file, match) {
|
|
|
78
78
|
// 全文逐行编号读取(v1.4.0 特性3:回喂存量记忆)。返回 { lines:[{n,raw,structural}], text }。
|
|
79
79
|
// 行号 n 与文件实际行一一对应(1 起),供 LLM 通过 line 引用;text 为每行 `n|raw` 拼接,直接喂模型。
|
|
80
80
|
// 不截断、无字符预算(用户明确要求全量回喂,避免截断导致记忆错误)。
|
|
81
|
+
// v1.4.1:回喂行经 stripSmartTag 剥除行首 [smart] 标记——模型不可见该标签(写侧已停拼),
|
|
82
|
+
// 存量磁盘条目读时透明剥除。replace/delete 的 oldText 校验仍以磁盘原文(raw)为准,不受影响。
|
|
81
83
|
export function readNumberedMemory(file) {
|
|
82
84
|
let cur = "";
|
|
83
85
|
try {
|
|
@@ -90,7 +92,7 @@ export function readNumberedMemory(file) {
|
|
|
90
92
|
const t = raw.trim();
|
|
91
93
|
return { n: i + 1, raw, structural: isStructural(t) };
|
|
92
94
|
});
|
|
93
|
-
const text = lines.map((l) => `${l.n}|${l.raw}`).join("\n");
|
|
95
|
+
const text = lines.map((l) => `${l.n}|${stripSmartTag(l.raw)}`).join("\n");
|
|
94
96
|
return { lines, text };
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -203,6 +205,9 @@ export function createRecords({ getConfig, paths }) {
|
|
|
203
205
|
|
|
204
206
|
async function prune(dir) {
|
|
205
207
|
const c = cfg();
|
|
208
|
+
// v1.6.0:hybrid 模式下日志永不过期不删(dailyLogRetentionDays 不可用,定案)——
|
|
209
|
+
// 日志是子代理维护的证据层,过期并入 MEMORY.md 的旧逻辑在 hybrid 下也不应触达。
|
|
210
|
+
if (c.memoryMode === "hybrid") return;
|
|
206
211
|
const cutoff = Date.now() - c.dailyLogRetentionDays * 86400000;
|
|
207
212
|
let files = [];
|
|
208
213
|
try {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// memory-palace v1.6.0 hybrid 模式:Markdown 章节纯函数(无 IO、无副作用)。
|
|
2
|
+
// 服务三类调用方:记忆子 agent 的日志 ops(追加/新增/标删)、agent 的 memory_write /
|
|
3
|
+
// memory_update_section 工具、memory_reorganize 的门禁计算。
|
|
4
|
+
// 设计约束(v1.6.0 定案):
|
|
5
|
+
// - 全部返回 { ok, reason, ... },永不抛错——LLM 产出的 ops 不可信,机器侧兜底;
|
|
6
|
+
// - 只拆两级标题(`## `),`#` 文件标题与前言归 head,`###` 及更深归当前章节内容;
|
|
7
|
+
// - 不改动 text.mjs 既有函数(normLine/budgetClip 有 v1.4.1 测试锚定),本文件自持归一化。
|
|
8
|
+
|
|
9
|
+
// 章节标题归一化:剥 `#` 前缀、合空白、转小写。标题匹配全部走它,"带不带 ## 前缀"都鲁棒。
|
|
10
|
+
const normTitle = (s) =>
|
|
11
|
+
(s || "").trim().replace(/^#+\s*/, "").replace(/\s+/g, " ").toLowerCase();
|
|
12
|
+
|
|
13
|
+
// 章节块归一化:逐行 trim + 合空白、丢空行、转小写。整章节 stale 比对(防并发/重试错位)用。
|
|
14
|
+
const normBlock = (t) =>
|
|
15
|
+
(t || "")
|
|
16
|
+
.split("\n")
|
|
17
|
+
.map((l) => l.trim().replace(/\s+/g, " "))
|
|
18
|
+
.filter((l) => l)
|
|
19
|
+
.join("\n")
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
|
|
22
|
+
const SECTION_RE = /^##\s+(.+?)\s*$/;
|
|
23
|
+
|
|
24
|
+
// 解析 `## ` 章节。返回 { head: string[], sections: [{ title, lines }] };
|
|
25
|
+
// lines 含标题行本体;`#` 文件标题、前言与 `<!--` 注释归 head;`###` 及更深归当前章节。
|
|
26
|
+
export function parseSections(md) {
|
|
27
|
+
const lines = (md || "").split("\n");
|
|
28
|
+
const head = [];
|
|
29
|
+
const sections = [];
|
|
30
|
+
let cur = null;
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const m = SECTION_RE.exec(line);
|
|
33
|
+
if (m) {
|
|
34
|
+
cur = { title: m[1].trim(), lines: [line] };
|
|
35
|
+
sections.push(cur);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (cur) cur.lines.push(line);
|
|
39
|
+
else head.push(line);
|
|
40
|
+
}
|
|
41
|
+
return { head, sections };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 在 md 中定位章节,返回 { start, end, lines }(end 为下一章节起点或文件尾,不含)。
|
|
45
|
+
// 未命中返回 null。供内部函数与 hybrid/tools.mjs(memory_update_section 的 stale 校验)共用。
|
|
46
|
+
export function locateSection(md, section) {
|
|
47
|
+
const lines = (md || "").split("\n");
|
|
48
|
+
const want = normTitle(section);
|
|
49
|
+
if (!want) return null;
|
|
50
|
+
let start = -1;
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const m = SECTION_RE.exec(lines[i]);
|
|
53
|
+
if (m && normTitle(m[1]) === want) {
|
|
54
|
+
start = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (start < 0) return null;
|
|
59
|
+
let end = lines.length;
|
|
60
|
+
for (let j = start + 1; j < lines.length; j++) {
|
|
61
|
+
if (SECTION_RE.test(lines[j])) {
|
|
62
|
+
end = j;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { start, end, lines };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 章节内追加条目。章节存在 → 内容最后一个非空行后插入 `- entry`(全空行则插在标题行后);
|
|
70
|
+
// 章节不存在 → { ok:false, reason:"section-miss" }(调用方决定是否走新增)。
|
|
71
|
+
// entry 写入前剥除行首列表符(`- `/`* `/`-`/`*`,含缩进)——LLM 常按描述示例自带 `- ` 前缀,
|
|
72
|
+
// 不剥会导致 `- - xxx` 双列表符脏数据(v1.6.0 实装踩坑)。
|
|
73
|
+
export function appendToSectionText(md, section, entry) {
|
|
74
|
+
const clean = String(entry ?? "").trim().replace(/^[-*]\s+/, "").trim();
|
|
75
|
+
const entryLine = `- ${clean}`;
|
|
76
|
+
if (!entryLine.slice(2).trim()) return { ok: false, reason: "empty-entry" };
|
|
77
|
+
const loc = locateSection(md, section);
|
|
78
|
+
if (!loc) return { ok: false, reason: "section-miss" };
|
|
79
|
+
const { start, end, lines } = loc;
|
|
80
|
+
let insertAt = end;
|
|
81
|
+
while (insertAt > start + 1 && !lines[insertAt - 1].trim()) insertAt--;
|
|
82
|
+
lines.splice(insertAt, 0, entryLine);
|
|
83
|
+
return { ok: true, reason: "appended", text: lines.join("\n") };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 章节追加(upsert 语义):存在则追加,不存在则文件尾新增章节。日志 ops 的 "append" 与
|
|
87
|
+
// memory_write 工具共用——"不要强行匹配现有章节,不符合章节标题就走新增"。
|
|
88
|
+
export function upsertSectionText(md, section, entry) {
|
|
89
|
+
const r = appendToSectionText(md, section, entry);
|
|
90
|
+
if (r.ok) return r;
|
|
91
|
+
if (r.reason === "empty-entry") return r;
|
|
92
|
+
const title = `## ${String(section ?? "").trim()}`;
|
|
93
|
+
const base = (md || "").replace(/\s+$/, "");
|
|
94
|
+
const entryLine = `- ${String(entry ?? "").trim().replace(/^[-*]\s+/, "").trim()}`;
|
|
95
|
+
const text = base ? `${base}\n\n${title}\n${entryLine}\n` : `${title}\n${entryLine}\n`;
|
|
96
|
+
return { ok: true, reason: "section-created", text };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 显式新建章节(与 upsert 的区别:章节已存在时拒绝而非追加)——子 agent 日志 ops 的
|
|
100
|
+
// "new_section" 专用,防止模型把该走 append 的操作误标为 new_section 造成静默混写。
|
|
101
|
+
export function createSectionText(md, section, entry) {
|
|
102
|
+
if (locateSection(md, section)) return { ok: false, reason: "section-exists" };
|
|
103
|
+
return upsertSectionText(md, section, entry);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 整章节精确替换(stale 防护):oldSectionText 与当前章节全文(含标题行,归一化后)必须完全
|
|
107
|
+
// 一致才执行替换,否则拒绝并回显当前实际内容(供 agent 重读后再试)。newSectionText 原样落盘
|
|
108
|
+
// (模型负责含 `## 标题` 与删除线标记);匹配/拒绝的归一化对首尾空行不敏感。
|
|
109
|
+
export function replaceSectionText(md, section, oldSectionText, newSectionText) {
|
|
110
|
+
const newText = String(newSectionText ?? "").trim();
|
|
111
|
+
if (!newText) return { ok: false, reason: "empty-replacement" };
|
|
112
|
+
const loc = locateSection(md, section);
|
|
113
|
+
if (!loc) return { ok: false, reason: "section-miss" };
|
|
114
|
+
const { start, end, lines } = loc;
|
|
115
|
+
const current = lines.slice(start, end).join("\n").trim();
|
|
116
|
+
if (normBlock(current) !== normBlock(String(oldSectionText ?? ""))) {
|
|
117
|
+
return { ok: false, reason: "conflict", actual: current };
|
|
118
|
+
}
|
|
119
|
+
// 保留章节末尾的分隔空行(章节与下一章节之间通常有空行,整块替换时不能吞掉,
|
|
120
|
+
// 否则下一章节标题会紧贴上一章节最后一条目)。
|
|
121
|
+
const hasTrailingBlank = end > start && !lines[end - 1].trim();
|
|
122
|
+
const spliceEnd = hasTrailingBlank ? end - 1 : end;
|
|
123
|
+
lines.splice(start, spliceEnd - start, ...newText.split("\n"));
|
|
124
|
+
return { ok: true, reason: "replaced", text: lines.join("\n") };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 章节内标记删除(墓碑,不物理删除):命中行改写为删除线格式;已删除线 → already;
|
|
128
|
+
// 结构行(# / <!--)与已删除线条目永不被改写;章节未命中 / 行未命中分别上报。
|
|
129
|
+
// 删除线格式:保留原列表符(`- text` → `- ~~text~~`;无列表符 → `~~text~~`)。
|
|
130
|
+
export function markEntryDeletedText(md, section, entryOldText) {
|
|
131
|
+
const want = normLineLoose(entryOldText);
|
|
132
|
+
if (!want) return { ok: false, reason: "empty-entry" };
|
|
133
|
+
const loc = locateSection(md, section);
|
|
134
|
+
if (!loc) return { ok: false, reason: "section-miss" };
|
|
135
|
+
const { start, end, lines } = loc;
|
|
136
|
+
for (let i = start + 1; i < end; i++) {
|
|
137
|
+
const raw = lines[i];
|
|
138
|
+
const trimmed = raw.trim();
|
|
139
|
+
if (!trimmed || isStructuralLine(trimmed)) continue;
|
|
140
|
+
if (/^(-\s+)?~~[\s\S]+~~$/.test(trimmed)) {
|
|
141
|
+
if (normLineLoose(trimmed) === want) return { ok: true, reason: "already", text: md };
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (normLineLoose(trimmed) !== want) continue;
|
|
145
|
+
const struck = /^-\s+/.test(trimmed)
|
|
146
|
+
? trimmed.replace(/^(-\s+)([\s\S]+)$/, "$1~~$2~~")
|
|
147
|
+
: `~~${trimmed}~~`;
|
|
148
|
+
lines[i] = raw.replace(trimmed, struck);
|
|
149
|
+
return { ok: true, reason: "marked", text: lines.join("\n") };
|
|
150
|
+
}
|
|
151
|
+
return { ok: false, reason: "entry-miss" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 宽松行归一化:剥列表符与删除线包裹、合空白、转小写。标删比对专用(与 text.mjs normLine
|
|
155
|
+
// 语义相近但多剥一层 ~~,避免"已标删行"永远无法二次匹配)。
|
|
156
|
+
function normLineLoose(s) {
|
|
157
|
+
return (s || "")
|
|
158
|
+
.trim()
|
|
159
|
+
.replace(/^-\s+/, "")
|
|
160
|
+
.replace(/^~~([\s\S]+)~~$/, "$1")
|
|
161
|
+
.replace(/\s+/g, " ")
|
|
162
|
+
.toLowerCase();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isStructuralLine(t) {
|
|
166
|
+
return t.startsWith("#") || t.startsWith("<!--");
|
|
167
|
+
}
|
package/lib/common/text.mjs
CHANGED
|
@@ -33,7 +33,7 @@ export function expandHome(p) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// 反向简写:home 下的绝对路径 → ~ 简写(统一正斜杠)。AI 转述绝对路径极易拼错
|
|
36
|
-
//
|
|
36
|
+
// (实测出现过缺分隔符,如 ~/.deepseek-harness 被拼成 ~.deepseek-harness),一律喂给它 ~ 简写。
|
|
37
37
|
export function toHomeShort(p) {
|
|
38
38
|
if (!p) return p;
|
|
39
39
|
const h = homedir();
|
|
@@ -54,13 +54,59 @@ export function readMdSync(path) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// v1.
|
|
58
|
-
//
|
|
59
|
-
//
|
|
57
|
+
// v1.4.1 重写:修 LIFO/FIFO 错配(旧实现 slice(0, budget) 只留头部,而写入追加在尾部——
|
|
58
|
+
// 最新结论永远进不了注入视野,"写了等于没写")。新策略:保留全部结构行(# / <!--)+
|
|
59
|
+
// 从尾部向前累积内容行至预算用尽,中间以截断标记衔接。budget <= 0 / 未配置视为不限。
|
|
60
60
|
export function budgetClip(text, budget) {
|
|
61
61
|
if (!text || !budget || budget <= 0) return text;
|
|
62
62
|
if (text.length <= budget) return text;
|
|
63
|
-
|
|
63
|
+
const lines = text.split("\n");
|
|
64
|
+
const head = [];
|
|
65
|
+
let i = 0;
|
|
66
|
+
// 头部连续结构行(文件标题/注释区)与空行无条件保留
|
|
67
|
+
while (i < lines.length && (!lines[i].trim() || isStructural(lines[i].trim()))) {
|
|
68
|
+
head.push(lines[i]);
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
// 尾部向前累积内容行至预算用尽
|
|
72
|
+
const tail = [];
|
|
73
|
+
let used = 0;
|
|
74
|
+
let j = lines.length - 1;
|
|
75
|
+
while (j >= i && used < budget) {
|
|
76
|
+
const cost = lines[j].length + 1;
|
|
77
|
+
if (used + cost > budget && tail.length > 0) break;
|
|
78
|
+
tail.unshift(lines[j]);
|
|
79
|
+
used += cost;
|
|
80
|
+
j--;
|
|
81
|
+
}
|
|
82
|
+
if (j < i) return head.join("\n") + (head.length ? "\n" : "") + tail.join("\n");
|
|
83
|
+
const marker = `…(已截断,原 ${text.length} 字符,仅注入结构行与尾部最近条目)`;
|
|
84
|
+
return [...head, marker, ...tail].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// v1.4.1:剥除行首 `[smart]` 标记(含重复),保留列表符与其余内容。用于注入与回喂的读侧——
|
|
88
|
+
// 模型全程不可见该标签(写侧 v1.4.1 起不再拼接),存量磁盘条目自然消亡、无需迁移。
|
|
89
|
+
// 逐行处理:允许行首有 `N|` 计数前缀(readNumberedMemory 的回喂格式)与列表符,
|
|
90
|
+
// 只剥「列表符/行首之后紧跟的 [smart] 标记序列」;正文中间的 "smart" 字样不误伤。
|
|
91
|
+
export function stripSmartTag(text) {
|
|
92
|
+
if (!text) return text;
|
|
93
|
+
return text
|
|
94
|
+
.split("\n")
|
|
95
|
+
.map((line) =>
|
|
96
|
+
line.replace(/^(\s*(?:\d+\|)?\s*[-*]\s+|\s*(?:\d+\|)\s*)?(\[smart\]\s*)+/, "$1"),
|
|
97
|
+
)
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// v1.6.0:注入侧删除线过滤——整行都是删除线墓碑(`~~...~~`,可带 `- ` 列表符)的条目
|
|
102
|
+
// 视为已作废,注入 system prompt 时剔除(省 token、防模型把墓碑当现行有效);
|
|
103
|
+
// 行内局部删除线(`- 旧名 ~~原名~~` 之类)不是墓碑,不滤。文件本体保留墓碑供审计。
|
|
104
|
+
export function stripDeletedLines(text) {
|
|
105
|
+
if (!text) return text;
|
|
106
|
+
return text
|
|
107
|
+
.split("\n")
|
|
108
|
+
.filter((line) => !/^\s*(?:-\s+)?~~[\s\S]+~~\s*$/.test(line))
|
|
109
|
+
.join("\n");
|
|
64
110
|
}
|
|
65
111
|
|
|
66
112
|
// 行归一化:去掉 `- `/`* ` 前缀、合并空白、转小写,使"带不带前缀"的子串匹配都鲁棒。
|
package/lib/distill.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, unlink, writeFile, rename } from "node:fs/promises";
|
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { SUMMARY_PROMPT, DISTILL_PROMPT } from "./common/prompts.mjs";
|
|
7
|
-
import { expandHome, readMdSync, toHomeShort, nowStamp } from "./common/text.mjs";
|
|
7
|
+
import { expandHome, readMdSync, toHomeShort, nowStamp, stripSmartTag } from "./common/text.mjs";
|
|
8
8
|
import { appendLineDedup, readNumberedMemory, applyMemoryOp } from "./common/records.mjs";
|
|
9
9
|
import { runWithRetry, RETRY_CONSTANTS } from "./common/retry.mjs";
|
|
10
10
|
|
|
@@ -239,9 +239,12 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
239
239
|
}
|
|
240
240
|
for (const d of durable) {
|
|
241
241
|
if (!d || typeof d !== "object") continue;
|
|
242
|
-
|
|
242
|
+
let fact = String(d.fact ?? "").trim();
|
|
243
243
|
if (!fact) continue;
|
|
244
|
-
|
|
244
|
+
// v1.4.1:写侧不再拼接 [smart] 标签(防回喂二次拼接形成 [smart] [smart] 双前缀、
|
|
245
|
+
// 防标签注意力稀释);防御性剥除 LLM 自行带回的标签,保证落盘条目干净。
|
|
246
|
+
fact = stripSmartTag(fact).trim();
|
|
247
|
+
const line = `- ${fact}`;
|
|
245
248
|
if (d.scope === "user") {
|
|
246
249
|
await appendLineDedup(expandHome(c.userMemoryPath), line).catch(() => {});
|
|
247
250
|
} else {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// memory-palace v1.6.0 hybrid 模式:模块入口(装配)。
|
|
2
|
+
// 仅在 memoryMode === "hybrid" 时由 index.mjs 调用 registerHybrid(memoryMode 切换需重启
|
|
3
|
+
// dsh——注册时机安全)。不注册任何 DSH 全局资源以外的东西;runMemorySubagent 由 index.mjs
|
|
4
|
+
// 的 _settle hybrid 分支每轮调用。
|
|
5
|
+
import { runMemorySubagent } from "./subagent.mjs";
|
|
6
|
+
import { registerHybridTools, attachHybridGuards } from "./tools.mjs";
|
|
7
|
+
|
|
8
|
+
export { HYBRID_PROACTIVE } from "./prompts.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ ctx: object, getConfig: () => object, paths: object, records: object, state: object }} deps
|
|
12
|
+
* @returns {{ runMemorySubagent: Function }}
|
|
13
|
+
*/
|
|
14
|
+
export function registerHybrid({ ctx, getConfig, paths, records, state }) {
|
|
15
|
+
registerHybridTools({ ctx, getConfig, paths, records, state });
|
|
16
|
+
attachHybridGuards(ctx, getConfig, paths, state);
|
|
17
|
+
return { runMemorySubagent };
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// memory-palace v1.6.0 hybrid 模式提示词:注入段二(HYBRID_PROACTIVE)+ 记忆子 agent
|
|
2
|
+
// system prompt(SUBAGENT_SYSTEM)。纯常量,无状态。
|
|
3
|
+
//
|
|
4
|
+
// 定案锚点(爹 2026-08-30 拍板):
|
|
5
|
+
// - 日志由子 agent 每轮自动维护,agent 无需重复记录过程性信息;
|
|
6
|
+
// - MEMORY.md 写入权归还 agent(主动调工具、章节化格式);
|
|
7
|
+
// - 项目级全量重整双门禁(超预算 + 冷却 ≥7 天,机器校验),用户级禁止重整;
|
|
8
|
+
// - 操作规范写在工具 description(定案 #11),本文件只做方向性引导,不堆格式细节。
|
|
9
|
+
|
|
10
|
+
// hybrid 模式注入 system prompt 的段二(替换 plugin 的记忆公民指令 / smart 的"无需主动"说明)。
|
|
11
|
+
export const HYBRID_PROACTIVE =
|
|
12
|
+
"\n\n[记忆说明] 分工如下:\n" +
|
|
13
|
+
"① 每轮结束由记忆子代理自动提炼摘要写入今日工作日志(含去重:重复/过时条目会以删除线标记),你无需调用工具记录过程性信息,也无需复述日志内容。\n" +
|
|
14
|
+
"② 长期记忆 MEMORY.md(项目级与用户级)由你主动维护:完成任务、关键决策、踩坑修复、用户偏好等 durable 事实,用 memory_write 追加(章节不存在会自动新建);过时内容用 memory_update_section 整章节精确替换或标删,绝不手写覆盖记忆文件。\n" +
|
|
15
|
+
"③ 项目级 MEMORY.md 仅在「超出注入预算 且 距上次重整 ≥ 重整冷却期」两条件同时满足时,才可调用 memory_reorganize 全量重整;重整前先读日志与现有记忆对照,切勿丢失关键信息。不满足条件时只能用 memory_update_section 做章节级修正。\n" +
|
|
16
|
+
"④ 用户级 MEMORY.md 禁止全量重整,只允许增量写入与整章节精确替换。";
|
|
17
|
+
|
|
18
|
+
// 记忆子 agent system prompt(turn/end 触发,判定 + 产出一体)。
|
|
19
|
+
// {date} 注入今日日期;回喂正文见 subagent.mjs 的首轮 user 消息。
|
|
20
|
+
export function SUBAGENT_SYSTEM() {
|
|
21
|
+
return (
|
|
22
|
+
"你是记忆子代理,负责把本轮对话沉淀进【今日工作日志】(Markdown 文件,按 `## 章节` + `- 条目` 组织)。\n" +
|
|
23
|
+
"\n" +
|
|
24
|
+
"【判定(第一优先)】先判断本轮对话是否包含值得写入日志的实质内容:完成了任务、修了 bug、做了决策、得出结论、用户表达了偏好或约束。\n" +
|
|
25
|
+
"- 无重点(闲聊、寒暄、一句话问答、无任何可沉淀信息):直接输出一行纯文本说明,不调用任何工具,立即结束。\n" +
|
|
26
|
+
"- 有重点:通过工具把格式化摘要写入日志,然后输出一行简短确认,结束。\n" +
|
|
27
|
+
"\n" +
|
|
28
|
+
"【日志写入规则(严格)】\n" +
|
|
29
|
+
"1. 只能通过 log_write_ops 工具写入,每次可批量提交多个操作;禁止请求重写整个文件。\n" +
|
|
30
|
+
"2. 三种操作:\n" +
|
|
31
|
+
" - append:章节存在则在其末尾追加条目,不存在则自动新增章节(不要强行匹配现有章节,语义确实不符就走新章节名);\n" +
|
|
32
|
+
" - new_section:显式新建章节并写入首条条目(若章节已存在会被拒绝,此时改用 append);\n" +
|
|
33
|
+
" - mark_delete:把日志中重复、过时或被本轮推翻的旧条目标记删除(删除线墓碑,非物理删除)。oldText 必须是日志中该条目的原文(可含或不含 `- ` 前缀),逐行匹配,匹配失败会被拒绝。\n" +
|
|
34
|
+
"3. 去重是你的职责:回喂的今日日志中已存在的结论禁止再次追加;发现语义重复或相互矛盾的旧条目,用 mark_delete 标记其中过时的一条(保留更完整/更新的那条)。\n" +
|
|
35
|
+
"4. 条目格式:一行一条、以结论开头、带关键细节(命令/路径/数字),客观第三人称,禁止对话体、请示、内心独白;禁止输出 [smart] 等任何标签。【entry 一律不带列表符(`- `/`* `)——插件写入时会自动补齐,你带了会变成双列表符脏数据】\n" +
|
|
36
|
+
"5. 章节名用简短的中文主题词(如 `## 环境必知`、`## 渠道映射排查`),宁多章节少混杂。\n" +
|
|
37
|
+
"6. 今日日志为空(新的一天)时直接用 append/new_section 建章节写入即可。\n" +
|
|
38
|
+
"7. 需要读取日志章节(目录模式回喂时)用 log_read_section;需要多个章节时务必在单次调用中把 sections 数组全部传齐,避免多轮往返浪费轮次。\n" +
|
|
39
|
+
"\n" +
|
|
40
|
+
"【输出】全程只做上述两件事:要么纯文本收尾,要么调工具写日志后收尾。不要向用户提问,不要输出多余解释。"
|
|
41
|
+
);
|
|
42
|
+
}
|