dsh-memory-palace 1.4.1 → 1.6.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 +22 -4
- package/lib/api.mjs +17 -0
- package/lib/client.js +61 -32
- 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 +79 -24
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
[](./LICENSE) [](https://awesome-dsh-plugin.com)
|
|
6
6
|
|
|
7
|
+
> mian分支已适配**deepseek harness 0.1.2-alpha.2+**
|
|
8
|
+
> 旧版本已切换分支归档,见`dsh-v0.1.x`
|
|
9
|
+
|
|
7
10
|
## 特性
|
|
8
11
|
|
|
9
12
|
- **人类可读的真源**:记忆全部存储在 Markdown 文件中(`MEMORY.md` + 每日日志 `YYYY-MM-DD.md`),任何编辑器可直接修改,数据永远属于你。
|
|
@@ -15,7 +18,9 @@
|
|
|
15
18
|
- **会话标题栏「记忆」按钮**:支持手动蒸馏:①蒸馏会话 / ②蒸馏项目记忆。
|
|
16
19
|
- **设置页集成**:DSH 设置中内置「记忆」面板(中英双语),所有配置均可图形化调整,无需改配置文件。
|
|
17
20
|
- **主动记忆(主路径,插件模式)**:注入「记忆公民指令」引导 agent 在「修复 bug/根因+绕过」「验证 build/test 通过」「完成里程碑/关键决策」「用户表达偏好/约束」时主动调 `memory_note` / `memory_note_user` 落档 — 对标 WorkBuddy 的"智能记一笔"手感。
|
|
18
|
-
- **智能模式(LLM 智能会话摘要)**:智能模式下,每轮命中防闲聊闸门后由 harness 把本会话**新增对话增量**(按 session 事件 seq 断点)提炼成摘要——`summary`
|
|
21
|
+
- **智能模式(LLM 智能会话摘要)**:智能模式下,每轮命中防闲聊闸门后由 harness 把本会话**新增对话增量**(按 session 事件 seq 断点)提炼成摘要——`summary` 写每日日志 + durable 事实写 MEMORY.md(v1.4.2 起条目不再带 `[smart]` 标签)。
|
|
22
|
+
- **记忆注入(v1.4.2)**:记忆块仅在会话**首次**构建时注入(DSH 会话自身继承历史轮次,逐轮注入冗余且有过时断言干扰);发生上下文压缩(compaction)后自动重注。注入按预算截断时保留结构行与**尾部最新条目**(写入追加在尾部,确保最新结论始终可见)。
|
|
23
|
+
- **混合模式(hybrid,v1.6.0)**:记忆子代理在每轮 turn/end 自动把本轮摘要写入**今日工作日志**(章节化组织、回喂今日日志做增量去重、重复/过时条目标删除线墓碑);MEMORY.md 写入权归还 agent(`memory_write` 章节化追加 / `memory_update_section` 整章节精确替换 / 双门禁内 `memory_reorganize` 全量重整)。hybrid 下日志永不过期(作为证据层保留)。
|
|
19
24
|
- **标准 npm 插件包**:经 `dsh plugin` 一键装入 profile,`cordis.patch.yml` 声明 bundle patch,零手动改动 harness。
|
|
20
25
|
|
|
21
26
|
## 记忆文件布局
|
|
@@ -65,6 +70,9 @@ turn/end ──► 轻量兜底闸门
|
|
|
65
70
|
| `memory_note_user` | 用户级 | 把跨项目偏好写入 `~/.deepseek-harness/MEMORY.md`(去重) |
|
|
66
71
|
| `memory_read` | 聚合 | 一次性读取用户级 + 项目级记忆、今日日志与最近 3 份历史日志 |
|
|
67
72
|
| `memory_delete` | 用户级/项目级/每日级 | 按内容删除记忆条目(两阶段确认:先预览匹配位置与内容,用户确认后再删;删除动作经 harness 原生确认弹窗硬闸门,真人点允许才真正执行) |
|
|
73
|
+
| `memory_write`(hybrid) | 项目级/用户级 | 章节化写入:章节存在则末尾追加,不存在则新建章节(章节化格式规范) |
|
|
74
|
+
| `memory_update_section`(hybrid) | 项目级/用户级 | 整章节精确替换/单条标记删除:oldText 归一化精确匹配防 stale,失败拒绝并回显实际内容 |
|
|
75
|
+
| `memory_reorganize`(hybrid) | 仅项目级 | 全量重整 MEMORY.md:双门禁(超出注入预算 且 距上次重整 ≥ 冷却天数)机器校验,原子替换 + 时间戳注释,用户确认弹窗 |
|
|
68
76
|
|
|
69
77
|
## 安装
|
|
70
78
|
|
|
@@ -74,7 +82,7 @@ turn/end ──► 轻量兜底闸门
|
|
|
74
82
|
|
|
75
83
|
```bash
|
|
76
84
|
dsh plugin --profile web add github:lovezi0/dsh-memory-palace
|
|
77
|
-
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.
|
|
85
|
+
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.6.0
|
|
78
86
|
```
|
|
79
87
|
|
|
80
88
|
方式二:clone 后本地安装(开发 / 修改源码场景)
|
|
@@ -92,8 +100,6 @@ dsh plugin --profile web add . # 装入 web profile(profile 名按你的实
|
|
|
92
100
|
```bash
|
|
93
101
|
# 直接由 dsh 从 npm 拉取并装入(本机若已配镜像会自动走镜像)
|
|
94
102
|
dsh plugin --profile web add dsh-memory-palace
|
|
95
|
-
# 锁定版本:
|
|
96
|
-
dsh plugin --profile web add dsh-memory-palace@1.4.1
|
|
97
103
|
|
|
98
104
|
# 或先手动用 npm 安装(显式指定镜像),再装入:
|
|
99
105
|
npm install dsh-memory-palace --registry=https://registry.npmmirror.com/
|
|
@@ -117,6 +123,18 @@ dsh plugin --profile web remove dsh-memory-palace
|
|
|
117
123
|
|
|
118
124
|
## 版本历史
|
|
119
125
|
|
|
126
|
+
- **1.6.0**
|
|
127
|
+
- 🔥适配deepseek harness 0.1.2-alpha.2
|
|
128
|
+
- **1.6.0.alpha.1** *未发布npm*
|
|
129
|
+
- 🔥新增混合模式(hybrid) *仍默认插件模式,建议切换至混合模式*
|
|
130
|
+
- 🔥新增记忆子agent处理轮次会话日志
|
|
131
|
+
- 💪优化记忆文件结构做到真正人类可读
|
|
132
|
+
- 💪优化调试模式参数保存
|
|
133
|
+
- **1.4.2.alpha.1** *未发布npm*
|
|
134
|
+
- 💪记忆注入:仅首次注入 + compaction 重注
|
|
135
|
+
- 💪优化手动项目蒸馏prompt
|
|
136
|
+
- 💪[smart] 标签全移除
|
|
137
|
+
- 🐛修复记忆文件LIFO/FIFO 错配
|
|
120
138
|
- **1.4.1**
|
|
121
139
|
- 🐛修复输出预算使用错位的问题
|
|
122
140
|
- 🐛增加调试模式日志级别 *默认info 仅输出元数据日志,debug 输出完整LLM text*
|
package/lib/api.mjs
CHANGED
|
@@ -99,6 +99,19 @@ function apiWriteError(res, error) {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// v1.6.0 联动纯函数(可单测):distillDebugLog=true 时保证 distillLogLevel 存在(默认 info),
|
|
103
|
+
// false 时移除 distillLogLevel。distillLogLevel 是平铺键(不进 UI),由 settings.update 在此兜底。
|
|
104
|
+
export function applyDebugLogLinkage(section) {
|
|
105
|
+
if (section && typeof section === "object" && !Array.isArray(section)) {
|
|
106
|
+
if (section.distillDebugLog === true) {
|
|
107
|
+
if (!section.distillLogLevel) section.distillLogLevel = "info";
|
|
108
|
+
} else if (section.distillDebugLog === false) {
|
|
109
|
+
delete section.distillLogLevel;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return section;
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
/**
|
|
103
116
|
* @param {{ ctx: object, paths: object, distill: object, state: object, getSettingsFace: () => object | null }} deps
|
|
104
117
|
*/
|
|
@@ -136,6 +149,10 @@ export function registerApi({ ctx, paths, distill, state, getSettingsFace }) {
|
|
|
136
149
|
if (section === null || typeof section !== "object" || Array.isArray(section)) {
|
|
137
150
|
throw Object.assign(new Error("section must be a plain object"), { code: "bad-request", status: 400 });
|
|
138
151
|
}
|
|
152
|
+
// v1.6.0 联动:distillDebugLog=true 时保证 distillLogLevel 存在(默认 info,保证
|
|
153
|
+
// 服务端日志开启即输出元数据诊断);false 时移除 distillLogLevel(防止残留 debug
|
|
154
|
+
// 级别持续打印 LLM 原始响应文本)。distillLogLevel 是平铺键(不进 UI),在此统一兜底。
|
|
155
|
+
applyDebugLogLinkage(section);
|
|
139
156
|
if (!face) {
|
|
140
157
|
throw Object.assign(new Error("settings service is not mounted"), { code: "settings-rejected", status: 503 });
|
|
141
158
|
}
|
package/lib/client.js
CHANGED
|
@@ -5,7 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
-
|
|
8
|
+
// v1.6.0-rc1:移除 require("@deepseek-ai/dsh-client-runtime/client")——该包在
|
|
9
|
+
// DSH 0.1.2-alpha.2 已整体删除(原变量赋值后零使用),平台 seed 表的 react 足够。
|
|
9
10
|
|
|
10
11
|
const NS = "memory-palace";
|
|
11
12
|
|
|
@@ -38,6 +39,11 @@ window.__ModuleLoader__.load({
|
|
|
38
39
|
memoryModeLabel: "记忆模式",
|
|
39
40
|
memoryModeHintPlugin: "插件模式:记忆公民指令 + 轮次轻量 + 错误捕获。切换需重启 dsh 生效。",
|
|
40
41
|
memoryModeHintSmart: "智能模式:LLM 智能会话摘要(summary→每日日志 + durable→MEMORY.md)。切换需重启 dsh 生效。",
|
|
42
|
+
memoryModeHintHybrid: "混合模式:记忆子代理自动维护今日日志(章节化、标删去重),agent 主动维护 MEMORY.md(章节化写入、整章节替换、门禁内全量重整)。切换需重启 dsh 生效。",
|
|
43
|
+
reorgCooldownLabel: "重整冷却(天)",
|
|
44
|
+
reorgCooldownHint: "项目级 MEMORY.md 全量重整的冷却天数(距上次重整);与「超出注入预算」双条件同时满足才允许 memory_reorganize。默认 7 天。",
|
|
45
|
+
subagentLogBudgetLabel: "子代理日志回喂预算(字符)",
|
|
46
|
+
subagentLogBudgetHint: "记忆子代理回喂今日工作日志的字符上限;超出时仅回喂章节目录,子代理按需读取章节。默认 20000。",
|
|
41
47
|
summaryModelLabel: "摘要模型(智能模式)",
|
|
42
48
|
summaryModelHint: "留空=复用当前会话的 provider/model;也可固定廉价小模型省 token。",
|
|
43
49
|
save: "保存",
|
|
@@ -104,6 +110,11 @@ window.__ModuleLoader__.load({
|
|
|
104
110
|
memoryModeLabel: "Memory mode",
|
|
105
111
|
memoryModeHintPlugin: "Plugin mode: memory-citizen instructions + turn-end light entries + error capture. Switching requires a dsh restart.",
|
|
106
112
|
memoryModeHintSmart: "Smart mode: LLM conversation summarization (summary -> daily log + durable -> MEMORY.md). Switching requires a dsh restart.",
|
|
113
|
+
memoryModeHintHybrid: "Hybrid mode: a memory sub-agent auto-maintains today's daily log (chaptered, dedupe by strikethrough); the agent proactively maintains MEMORY.md (chaptered writes, section replace, gated full reorganize). Switching requires a dsh restart.",
|
|
114
|
+
reorgCooldownLabel: "Reorganize cooldown (days)",
|
|
115
|
+
reorgCooldownHint: "Cooldown days between full reorganizes of the project MEMORY.md; combined with 'over injection budget' both gates must pass for memory_reorganize. Default 7.",
|
|
116
|
+
subagentLogBudgetLabel: "Sub-agent log feed budget (chars)",
|
|
117
|
+
subagentLogBudgetHint: "Max chars of today's daily log fed back to the memory sub-agent; beyond this only the chapter TOC is fed and the sub-agent reads sections on demand. Default 20000.",
|
|
107
118
|
summaryModelLabel: "Summary model (smart mode)",
|
|
108
119
|
summaryModelHint: "Leave empty to reuse the current session's provider/model, or pin a cheap model (provider/model) to save tokens.",
|
|
109
120
|
save: "Save",
|
|
@@ -204,8 +215,30 @@ window.__ModuleLoader__.load({
|
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
|
|
218
|
+
// v1.6.0-rc1:模型枚举改走 ctx.remote.session.modelCatalog()——ApiProxy 包已在
|
|
219
|
+
// DSH 0.1.2-alpha.2 删除(旧 conn.api.llm.models 死了)。服务缺席 / 非 ok / 异常
|
|
220
|
+
// 一律降级为空列表,下拉只剩「复用当前会话模型」,UX 不变。
|
|
221
|
+
async function fetchModelOptions(ctx) {
|
|
222
|
+
try {
|
|
223
|
+
const remote = ctx && ctx.remote;
|
|
224
|
+
if (!remote || !remote.session || typeof remote.session.modelCatalog !== "function") return [];
|
|
225
|
+
const response = await remote.session.modelCatalog();
|
|
226
|
+
if (!response || !response.ok || !response.value) return [];
|
|
227
|
+
const opts = [];
|
|
228
|
+
for (const g of response.value.groups || []) {
|
|
229
|
+
for (const m of (g && g.models) || []) {
|
|
230
|
+
// m.id 已含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
|
|
231
|
+
opts.push({ value: m.id, label: (g.name || g.id) + " / " + (m.name || m.id) });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return opts;
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
207
240
|
function MemoryPalaceSection(props) {
|
|
208
|
-
const { t,
|
|
241
|
+
const { t, fetchModels } = props;
|
|
209
242
|
const [remote, setRemote] = react.useState({ value: null, user: {}, revision: undefined, loaded: false });
|
|
210
243
|
const [draft, setDraft] = react.useState(() => projectDraft({ value: null }));
|
|
211
244
|
const [dirty, setDirty] = react.useState(false);
|
|
@@ -214,7 +247,7 @@ window.__ModuleLoader__.load({
|
|
|
214
247
|
const [validation, setValidation] = react.useState(null);
|
|
215
248
|
const [modelOptions, setModelOptions] = react.useState([]);
|
|
216
249
|
|
|
217
|
-
// 初始加载(从自有 route
|
|
250
|
+
// 初始加载(从自有 route 读真实值)。
|
|
218
251
|
react.useEffect(() => {
|
|
219
252
|
let alive = true;
|
|
220
253
|
(async () => {
|
|
@@ -240,18 +273,8 @@ window.__ModuleLoader__.load({
|
|
|
240
273
|
let alive = true;
|
|
241
274
|
(async () => {
|
|
242
275
|
try {
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
const r = await conn.api.llm.models({});
|
|
246
|
-
const groups = (r && r.result && r.result.value && r.result.value.groups) || (r && r.groups) || [];
|
|
247
|
-
const opts = [];
|
|
248
|
-
for (const g of groups || []) {
|
|
249
|
-
for (const m of (g && g.models) || []) {
|
|
250
|
-
// m.id 已经包含 provider 前缀(如 nvidia/nemotron-3-ultra-550b-a55b),直接用,避免重复拼接
|
|
251
|
-
opts.push({ value: m.id, label: g.name + " / " + m.name });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (alive) setModelOptions(opts);
|
|
276
|
+
const opts = await (fetchModels && fetchModels());
|
|
277
|
+
if (alive) setModelOptions(Array.isArray(opts) ? opts : []);
|
|
255
278
|
} catch (e) {
|
|
256
279
|
/* 枚举失败:保持空列表,下拉只剩「复用当前会话模型」 */
|
|
257
280
|
}
|
|
@@ -523,17 +546,23 @@ window.__ModuleLoader__.load({
|
|
|
523
546
|
]),
|
|
524
547
|
h("div", { style: cardStyle }, [
|
|
525
548
|
h("p", { style: groupTitleStyle }, t("smartSummary")),
|
|
526
|
-
// 轮次结束自动记录(summarize)
|
|
549
|
+
// 轮次结束自动记录(summarize):三种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
|
|
527
550
|
toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
|
|
528
|
-
row(t("memoryModeLabel"),
|
|
551
|
+
row(t("memoryModeLabel"),
|
|
552
|
+
draft.memoryMode === "smart"
|
|
553
|
+
? t("memoryModeHintSmart")
|
|
554
|
+
: draft.memoryMode === "hybrid"
|
|
555
|
+
? t("memoryModeHintHybrid")
|
|
556
|
+
: t("memoryModeHintPlugin"),
|
|
529
557
|
h("select", {
|
|
530
|
-
value:
|
|
558
|
+
value: ["plugin", "smart", "hybrid"].includes(draft.memoryMode) ? draft.memoryMode : "plugin",
|
|
531
559
|
disabled,
|
|
532
560
|
onChange: (e) => edit("memoryMode", e.target.value),
|
|
533
561
|
style: selectStyle
|
|
534
562
|
}, [
|
|
535
563
|
h("option", { value: "plugin" }, "插件模式"),
|
|
536
|
-
h("option", { value: "smart" }, "智能模式")
|
|
564
|
+
h("option", { value: "smart" }, "智能模式"),
|
|
565
|
+
h("option", { value: "hybrid" }, "混合模式")
|
|
537
566
|
]), "memoryMode"),
|
|
538
567
|
draft.memoryMode === "smart"
|
|
539
568
|
? h("div", null, [
|
|
@@ -553,9 +582,14 @@ window.__ModuleLoader__.load({
|
|
|
553
582
|
row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
|
|
554
583
|
row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
|
|
555
584
|
])
|
|
556
|
-
:
|
|
557
|
-
|
|
558
|
-
|
|
585
|
+
: draft.memoryMode === "hybrid"
|
|
586
|
+
? h("div", null, [
|
|
587
|
+
row(t("reorgCooldownLabel"), t("reorgCooldownHint"), h("input", num("reorgCooldownDays")), "reorgCooldownDays"),
|
|
588
|
+
row(t("subagentLogBudgetLabel"), t("subagentLogBudgetHint"), h("input", num("subagentLogBudget")), "subagentLogBudget")
|
|
589
|
+
])
|
|
590
|
+
: h("div", null, [
|
|
591
|
+
toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
|
|
592
|
+
])
|
|
559
593
|
]),
|
|
560
594
|
h("div", { style: cardStyle }, [
|
|
561
595
|
h("p", { style: groupTitleStyle }, t("storage")),
|
|
@@ -839,14 +873,8 @@ window.__ModuleLoader__.load({
|
|
|
839
873
|
// v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
|
|
840
874
|
// 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
|
|
841
875
|
const controller = new MemoryPalaceController(ctx);
|
|
842
|
-
// v1.
|
|
843
|
-
|
|
844
|
-
try {
|
|
845
|
-
return ctx.get("connection");
|
|
846
|
-
} catch {
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
};
|
|
876
|
+
// v1.6.0-rc1:枚举已配置模型改走 ctx.remote.session.modelCatalog()(ApiProxy 已删),
|
|
877
|
+
// 枚举失败则降级为空列表。
|
|
850
878
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
851
879
|
name: "settings.section",
|
|
852
880
|
id: "memory-palace",
|
|
@@ -854,7 +882,7 @@ window.__ModuleLoader__.load({
|
|
|
854
882
|
label: () => t("nav"),
|
|
855
883
|
locale: NS,
|
|
856
884
|
inject: () => controller.inject()
|
|
857
|
-
}, (props) => MemoryPalaceSection({ ...props,
|
|
885
|
+
}, (props) => MemoryPalaceSection({ ...props, fetchModels: () => fetchModelOptions(ctx) })));
|
|
858
886
|
// v1.2.0:会话标题栏「记忆」按钮——headerUtilities 区(右对齐),order:-1 排在 Session log 左边。
|
|
859
887
|
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
860
888
|
name: "conversation.session.header.utilities",
|
|
@@ -865,7 +893,8 @@ window.__ModuleLoader__.load({
|
|
|
865
893
|
}
|
|
866
894
|
|
|
867
895
|
exports.apply = apply;
|
|
868
|
-
|
|
896
|
+
// v1.6.0-rc1:connection → remote + remote.session(模型枚举走 ctx.remote.session.modelCatalog)。
|
|
897
|
+
exports.inject = ["slots", "locale", "remote", "remote.session"];
|
|
869
898
|
return module.exports;
|
|
870
899
|
}
|
|
871
900
|
});
|
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
|
+
}
|