dsh-memory-palace 1.3.0 → 1.4.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 +14 -44
- package/lib/api.mjs +2 -1
- package/lib/client.js +57 -28
- package/lib/common/prompts.mjs +44 -14
- package/lib/common/records.mjs +79 -0
- package/lib/common/retry.mjs +138 -0
- package/lib/distill.mjs +181 -90
- package/lib/index.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# dsh-memory-palace <img src="assets/memory-icon.svg" width="36" height="36" alt="dsh-memory-palace" />
|
|
2
|
-
---
|
|
3
2
|
|
|
4
3
|
把 WorkBuddy 的文件式记忆系统移植进 [DeepSeek Harness](https://www.deepseek.com/harness/) —— 为 Harness 提供**跨会话持久化、人类可直接编辑的 Markdown 记忆**。
|
|
5
4
|
|
|
@@ -36,10 +35,6 @@
|
|
|
36
35
|
└── 2026-08-16.md # 每日工作日志
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
> **布局迭代(v1.1.x)**:早期版本曾把项目级 MEMORY.md 放在 `.deepseek-harness/memory/MEMORY.md`(嵌套);现已改为与 `memory/` **同级**(`.deepseek-harness/MEMORY.md`)。旧嵌套位置仍会被读取(向后兼容、不丢旧数据),但**写入一律走同级**。
|
|
40
|
-
|
|
41
|
-
> **桥接规则**:`bridgeBuddyMemory` 开启时,只要项目里存在任一 buddy 记忆目录,就**只**读写这些目录,不再创建 `.deepseek-harness/memory/`;全部不存在时才回退到 dsh 目录。buddy 目录绝不被主动创建。
|
|
42
|
-
|
|
43
38
|
## 工作原理
|
|
44
39
|
|
|
45
40
|
**读取(每轮对话)**——`systemPrompt.section` 同步读盘,把以下内容拼进系统提示词:
|
|
@@ -79,7 +74,7 @@ turn/end ──► 轻量兜底闸门
|
|
|
79
74
|
|
|
80
75
|
```bash
|
|
81
76
|
dsh plugin --profile web add github:lovezi0/dsh-memory-palace
|
|
82
|
-
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.
|
|
77
|
+
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.4.1
|
|
83
78
|
```
|
|
84
79
|
|
|
85
80
|
方式二:clone 后本地安装(开发 / 修改源码场景)
|
|
@@ -98,7 +93,7 @@ dsh plugin --profile web add . # 装入 web profile(profile 名按你的实
|
|
|
98
93
|
# 直接由 dsh 从 npm 拉取并装入(本机若已配镜像会自动走镜像)
|
|
99
94
|
dsh plugin --profile web add dsh-memory-palace
|
|
100
95
|
# 锁定版本:
|
|
101
|
-
dsh plugin --profile web add dsh-memory-palace@1.
|
|
96
|
+
dsh plugin --profile web add dsh-memory-palace@1.4.1
|
|
102
97
|
|
|
103
98
|
# 或先手动用 npm 安装(显式指定镜像),再装入:
|
|
104
99
|
npm install dsh-memory-palace --registry=https://registry.npmmirror.com/
|
|
@@ -112,49 +107,24 @@ dsh plugin --profile web remove dsh-memory-palace
|
|
|
112
107
|
```
|
|
113
108
|
|
|
114
109
|
## 配置
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
| 配置项 | 默认值 | 说明 |
|
|
119
|
-
|---|---|---|
|
|
120
|
-
| 总开关 | `true` | 总开关,关闭后不注入、不写入 |
|
|
121
|
-
| 用户级记忆路径 | `~/.deepseek-harness/MEMORY.md` | 用户级记忆文件路径(支持 `~` 展开) |
|
|
122
|
-
| 工作区记忆目录 | `.deepseek-harness/memory` | 无 buddy 目录时回退的项目记忆目录 |
|
|
123
|
-
| 日志保留天数 | `30` | 每日日志保留天数,过期迁移进 `MEMORY.md` |
|
|
124
|
-
| 用户级记忆字数上限 | `4000` | 注入系统提示词的用户级记忆长度上限(字符) |
|
|
125
|
-
| 工作区级记忆字数上限 | `3000` | 注入系统提示词的工作区级记忆长度上限(字符) |
|
|
126
|
-
| 桥接 Buddy 记忆 | `true` | 检测并直接读写 WorkBuddy / CodeBuddy 项目记忆目录 |
|
|
127
|
-
| Buddy 记忆目录列表 | `[".workbuddy/memory", ".codebuddy/memory"]` | 要桥接的 buddy 目录列表(按优先级,已存在的全部同步写入) |
|
|
128
|
-
|
|
129
|
-
### 自动记录(设置页「记忆 → 自动记录」卡片)
|
|
130
|
-
|
|
131
|
-
| 配置项 | 默认值 | 说明 |
|
|
132
|
-
|---|---|---|
|
|
133
|
-
| 记忆模式 | `plugin` | 两种互斥模式:`plugin`=记忆公民指令+轮次轻量+错误捕获(默认);`smart`=LLM 智能会话摘要(summary→每日日志 + durable→MEMORY.md,带 `[smart]` 标记)。**切换需重启 dsh 生效** |
|
|
134
|
-
| 轮次结束自动记录 | `true` | 插件模式下:它是「agent 主动记忆」主路径失效时的安全网,保证实质工作不丢,代价是只留原始文本、不做总结。智能模式下该开关仍为总闸门 |
|
|
135
|
-
| 摘要模型 | `""`(空=复用当前会话模型) | 智能模式专用:留空自动复用当前会话 provider/model;填 `provider/model`(如 `deepseek/deepseek-chat`)固定廉价模型省 token |
|
|
136
|
-
| 对话出错自动记录 | `true` | 插件模式下:自动捕获 in-session 错误并写入「错误现象」到对应 MEMORY.md;「根因/方案」由 agent 主动记;默认开启,**关闭无需重启 dsh**。智能模式下错误由 LLM 摘要统一提炼 |
|
|
137
|
-
|
|
138
|
-
> **设置保存(v1.1.4 起)**:设置页保存已**真正落盘**
|
|
139
|
-
>
|
|
140
|
-
> 历史方案(仍可用作兜底):直接在 profile 的 `cordis.patch.yml` 注入配置(id-targeted config override,与插件 bundle insert 的 id 一致):
|
|
141
|
-
>
|
|
142
|
-
> ```yaml
|
|
143
|
-
> - id: memory-palace
|
|
144
|
-
> name: 'dsh-memory-palace'
|
|
145
|
-
> config:
|
|
146
|
-
> memoryMode: smart
|
|
147
|
-
> summaryModel: ''
|
|
148
|
-
> ```
|
|
149
|
-
>
|
|
150
|
-
> 修改后重启 dsh 生效。
|
|
110
|
+
*面板路径为 DSH 设置 →「记忆」*
|
|
111
|
+
所有配置项(基础 / 自动记录 / 开发 三张卡片的字段、默认值与说明)已整理至 [CONFIG.md](./CONFIG.md)
|
|
151
112
|
|
|
152
113
|
## 开发
|
|
153
114
|
|
|
154
|
-
|
|
115
|
+
架构 / 构建 / 技术要点 / 蒸馏失败重试 见 [DEVELOPMENT.md](./DEVELOPMENT.md)。
|
|
116
|
+
提示词与智能模式蒸馏(公民指令 / 摘要 / 蒸馏) 见 [PROMPTS.md](./PROMPTS.md)。
|
|
155
117
|
|
|
156
118
|
## 版本历史
|
|
157
119
|
|
|
120
|
+
- **1.4.1**
|
|
121
|
+
- 🐛修复输出预算使用错位的问题
|
|
122
|
+
- 🐛增加调试模式日志级别 *默认info 仅输出元数据日志,debug 输出完整LLM text*
|
|
123
|
+
- **1.4.0**
|
|
124
|
+
- 🔥智能模式增加最大输出Token限制
|
|
125
|
+
- 🔥智能模式增加失败重试机制
|
|
126
|
+
- 🔥智能模式会增加回馈存量记忆
|
|
127
|
+
- 🐛修复自动记录部分设置位置错误问题
|
|
158
128
|
- **1.3.0**
|
|
159
129
|
- 🔥新增计划模式禁止写入记忆
|
|
160
130
|
- 💪设置-记忆控件样式优化 *使用dsh原生样式*
|
package/lib/api.mjs
CHANGED
|
@@ -167,7 +167,8 @@ export function registerApi({ ctx, paths, distill, state, getSettingsFace }) {
|
|
|
167
167
|
throw Object.assign(new Error("no memory dirs for this workspace"), { code: "bad-request", status: 400 });
|
|
168
168
|
}
|
|
169
169
|
// 整段会话全量蒸馏(fromSeq=0);成功后推进增量断点,防智能模式对同段事件重复摘要。
|
|
170
|
-
|
|
170
|
+
// allowDelete:true —— 手动按钮开放 delete 操作(自动智能模式传 false,见 summarizeTurn)。
|
|
171
|
+
const r = await distill.distillSessionCore(session, dirs, 0, { allowDelete: true });
|
|
171
172
|
if (r.ok && session.id === state.summarySessionId) state.lastSummarizedSeq = session.seq;
|
|
172
173
|
apiWriteOk(res, r);
|
|
173
174
|
return;
|
package/lib/client.js
CHANGED
|
@@ -32,7 +32,7 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
wsBudgetLabel: "工作区预算(字符)",
|
|
33
33
|
wsBudgetHint: "工作区 MEMORY.md 的最大字符数",
|
|
34
34
|
summarizeLabel: "轮次结束自动记录",
|
|
35
|
-
summarizeHint: "
|
|
35
|
+
summarizeHint: "两种记忆模式的共用总闸门:关闭后无论插件模式还是智能模式都不写记忆。插件模式下它作为「agent 主动记忆」主路径失效时的安全网,只留原始文本、不做总结;智能模式下开启则走 LLM 智能摘要(总结 + 提炼)。",
|
|
36
36
|
autoCaptureErrorsLabel: "对话出错时自动记录到记忆",
|
|
37
37
|
autoCaptureErrorsHint: "对话中(含代码运行报错、工具执行失败)出错时,自动把「错误现象」写入对应 MEMORY.md(用户级/项目级);「根因/方案」由 agent 按记忆公民指令主动记。默认开,关闭无需重启。",
|
|
38
38
|
memoryModeLabel: "记忆模式",
|
|
@@ -64,10 +64,16 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
notifyProjectDone: "项目记忆蒸馏完成",
|
|
65
65
|
notifyFail: "记忆蒸馏失败",
|
|
66
66
|
distillDebugLogLabel: "蒸馏调试日志",
|
|
67
|
-
distillDebugLogHint: "开启后向 dsh 服务端控制台(stderr)输出蒸馏 LLM
|
|
67
|
+
distillDebugLogHint: "开启后向 dsh 服务端控制台(stderr)输出蒸馏 LLM 调用诊断。info 级别仅输出元数据(不含文本);配合 distillLogLevel=debug 会额外打印 LLM 原始响应(分隔符包裹),仅限受信本地排障。",
|
|
68
68
|
dev: "开发",
|
|
69
69
|
distillTimeoutLabel: "蒸馏超时(秒)",
|
|
70
|
-
distillTimeoutHint: "单次蒸馏 LLM 调用的超时上限(秒);超时视为失败并降级。覆盖智能模式摘要与手动蒸馏,默认 60 秒。"
|
|
70
|
+
distillTimeoutHint: "单次蒸馏 LLM 调用的超时上限(秒);超时视为失败并降级。覆盖智能模式摘要与手动蒸馏,默认 60 秒。",
|
|
71
|
+
summaryMaxTokensLabel: "会话摘要最大 Token(智能模式)",
|
|
72
|
+
summaryMaxTokensHint: "智能模式会话摘要 LLM 的【最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)】。默认 2000,可调大以容纳更多 durable 事实。",
|
|
73
|
+
projectMaxTokensLabel: "项目蒸馏输出软预算",
|
|
74
|
+
projectMaxTokensHint: "手动「蒸馏项目记忆」LLM 的【最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)】。默认 8000。",
|
|
75
|
+
feedbackEnabledLabel: "蒸馏回喂存量记忆",
|
|
76
|
+
feedbackEnabledHint: "开启后,自动智能模式(turn/end)与手动蒸馏会话按钮均会把项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)回喂 LLM,使其能基于既有记忆做 add/replace/delete 增量维护。delete 仅手动蒸馏按钮开放,自动模式跳过(防误删)。默认关。"
|
|
71
77
|
};
|
|
72
78
|
|
|
73
79
|
const en = {
|
|
@@ -92,7 +98,7 @@ window.__ModuleLoader__.load({
|
|
|
92
98
|
wsBudgetLabel: "Workspace budget (chars)",
|
|
93
99
|
wsBudgetHint: "Max chars for the workspace MEMORY.md",
|
|
94
100
|
summarizeLabel: "Auto-record at turn end",
|
|
95
|
-
summarizeHint: "
|
|
101
|
+
summarizeHint: "The shared master gate for both memory modes: when off, neither plugin nor smart mode writes any memory. In plugin mode it is the safety net when the 'agent proactive memory' main path fails (keeps raw text, no summarization); in smart mode it enables the LLM summarization path (summary + extraction).",
|
|
96
102
|
autoCaptureErrorsLabel: "Auto-record conversation errors",
|
|
97
103
|
autoCaptureErrorsHint: "On in-session errors, auto write the 'error' into the corresponding MEMORY.md (user/project); 'root cause / fix' is recorded proactively by the agent. On by default; off takes effect without restart.",
|
|
98
104
|
memoryModeLabel: "Memory mode",
|
|
@@ -124,15 +130,21 @@ window.__ModuleLoader__.load({
|
|
|
124
130
|
notifyProjectDone: "Project memory distilled",
|
|
125
131
|
notifyFail: "Memory distillation failed",
|
|
126
132
|
distillDebugLogLabel: "Distill debug log",
|
|
127
|
-
distillDebugLogHint: "When on, prints distill LLM call diagnostics to the dsh server console (stderr).
|
|
133
|
+
distillDebugLogHint: "When on, prints distill LLM call diagnostics to the dsh server console (stderr). info level prints metadata only (no text); with distillLogLevel=debug it additionally prints the raw LLM response (wrapped in delimiters), for trusted local troubleshooting only.",
|
|
128
134
|
dev: "Developer",
|
|
129
135
|
distillTimeoutLabel: "Distill timeout (s)",
|
|
130
|
-
distillTimeoutHint: "Timeout (in seconds) for a single distill LLM call; on timeout it fails and degrades. Covers both smart-mode summary and manual distillation. Default 60s."
|
|
136
|
+
distillTimeoutHint: "Timeout (in seconds) for a single distill LLM call; on timeout it fails and degrades. Covers both smart-mode summary and manual distillation. Default 60s.",
|
|
137
|
+
summaryMaxTokensLabel: "Session summary max tokens (smart mode)",
|
|
138
|
+
summaryMaxTokensHint: "Smart-mode session summary LLM final output soft budget (prompt constraint, thinking not limited; the actual hard cap is the model's own maxTokens). Default 2000; raise to fit more durable facts.",
|
|
139
|
+
projectMaxTokensLabel: "Project distill output soft budget",
|
|
140
|
+
projectMaxTokensHint: "Manual 'distill project memory' LLM final output soft budget (prompt constraint, thinking not limited; actual hard cap is the model's own maxTokens). Default 8000.",
|
|
141
|
+
feedbackEnabledLabel: "Feed existing memory into distillation",
|
|
142
|
+
feedbackEnabledHint: "When on, both the auto smart-mode (turn/end) and the manual session distillation feed the full project-level + user-level MEMORY.md (line-numbered, no truncation) back to the LLM so it can maintain memory incrementally via add/replace/delete. delete is only allowed via the manual distill button; auto mode skips it (to avoid accidental memory loss). Off by default."
|
|
131
143
|
};
|
|
132
144
|
|
|
133
145
|
|
|
134
|
-
const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars", "summaryTimeoutMs"]);
|
|
135
|
-
const FIELD_BOOL = new Set(["bridgeBuddyMemory", "summarize", "autoCaptureErrors", "distillDebugLog"]);
|
|
146
|
+
const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars", "summaryTimeoutMs", "summaryMaxTokens", "projectMaxTokens"]);
|
|
147
|
+
const FIELD_BOOL = new Set(["bridgeBuddyMemory", "summarize", "autoCaptureErrors", "distillDebugLog", "feedbackEnabled"]);
|
|
136
148
|
|
|
137
149
|
function projectDraft(snap) {
|
|
138
150
|
const v = snap && snap.value ? snap.value : {};
|
|
@@ -149,7 +161,10 @@ window.__ModuleLoader__.load({
|
|
|
149
161
|
distillDebugLog: v.distillDebugLog === false ? "false" : "true",
|
|
150
162
|
summaryTimeoutMs: v.summaryTimeoutMs != null ? String(Math.round((Number(v.summaryTimeoutMs) || 0) / 1000)) : "60",
|
|
151
163
|
memoryMode: v.memoryMode || "plugin",
|
|
152
|
-
summaryModel: v.summaryModel || ""
|
|
164
|
+
summaryModel: v.summaryModel || "",
|
|
165
|
+
summaryMaxTokens: v.summaryMaxTokens != null ? String(v.summaryMaxTokens) : "2000",
|
|
166
|
+
projectMaxTokens: v.projectMaxTokens != null ? String(v.projectMaxTokens) : "8000",
|
|
167
|
+
feedbackEnabled: v.feedbackEnabled === false ? "false" : "true"
|
|
153
168
|
};
|
|
154
169
|
}
|
|
155
170
|
|
|
@@ -311,9 +326,11 @@ window.__ModuleLoader__.load({
|
|
|
311
326
|
const userLayer = remote.user && typeof remote.user === "object" ? remote.user : {};
|
|
312
327
|
const isOverridden = (field) => field in userLayer;
|
|
313
328
|
|
|
329
|
+
// 左侧文字容器:min-width:0 + flexShrink:1 让长描述正常换行收缩,不再撑爆 flex 把 checkbox 挤变形。
|
|
330
|
+
const toggleLabelBoxStyle = { flexShrink: 1, minWidth: 0 };
|
|
314
331
|
const toggle = (label, hint, field, disabledOverride) =>
|
|
315
332
|
h("div", { style: toggleStyle }, [
|
|
316
|
-
h("div",
|
|
333
|
+
h("div", { style: toggleLabelBoxStyle }, [
|
|
317
334
|
h("p", { style: { ...labelStyle, margin: 0 } }, label),
|
|
318
335
|
h("p", { style: hintStyle }, hint)
|
|
319
336
|
]),
|
|
@@ -361,10 +378,14 @@ window.__ModuleLoader__.load({
|
|
|
361
378
|
paddingRight: "32px"
|
|
362
379
|
};
|
|
363
380
|
const checkboxStyle = {
|
|
364
|
-
width: "
|
|
365
|
-
height: "
|
|
381
|
+
width: "18px",
|
|
382
|
+
height: "18px",
|
|
366
383
|
margin: "0",
|
|
367
|
-
accentColor: "var(--dsw-alias-brand-primary, #4b5bff)"
|
|
384
|
+
accentColor: "var(--dsw-alias-brand-primary, #4b5bff)",
|
|
385
|
+
// 锁死:不被 flex 长描述挤压缩(flex-shrink 默认 1 会在布局阶段无视 width/!important)。
|
|
386
|
+
flexShrink: 0,
|
|
387
|
+
flexGrow: 0,
|
|
388
|
+
alignSelf: "center"
|
|
368
389
|
};
|
|
369
390
|
const toggleStyle = {
|
|
370
391
|
display: "flex",
|
|
@@ -464,7 +485,9 @@ window.__ModuleLoader__.load({
|
|
|
464
485
|
onChange: (e) => edit(field, e.target.value)
|
|
465
486
|
});
|
|
466
487
|
|
|
467
|
-
return h("div", { style: sectionStyle }, [
|
|
488
|
+
return h("div", { className: "memory-palace-settings", style: sectionStyle }, [
|
|
489
|
+
// 强制覆盖宿主 Webview 全局样式对原生 checkbox 尺寸的覆盖(内联 style 可能不敌全局 CSS)。
|
|
490
|
+
h("style", null, ".memory-palace-settings input[type=checkbox]{width:18px!important;height:18px!important;margin:0;flex-shrink:0!important;cursor:pointer;}"),
|
|
468
491
|
h("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" } }, [
|
|
469
492
|
h("div", null, [
|
|
470
493
|
h("h3", { style: headingStyle }, t("title")),
|
|
@@ -479,7 +502,7 @@ window.__ModuleLoader__.load({
|
|
|
479
502
|
h("div", { style: cardStyle }, [
|
|
480
503
|
h("p", { style: groupTitleStyle }, t("core")),
|
|
481
504
|
h("div", { style: toggleStyle }, [
|
|
482
|
-
h("div",
|
|
505
|
+
h("div", { style: { flexShrink: 1, minWidth: 0 } }, [
|
|
483
506
|
h("p", { style: { ...labelStyle, margin: 0 } }, t("bridgeLabel")),
|
|
484
507
|
h("p", { style: hintStyle }, t("bridgeHint"))
|
|
485
508
|
]),
|
|
@@ -500,6 +523,8 @@ window.__ModuleLoader__.load({
|
|
|
500
523
|
]),
|
|
501
524
|
h("div", { style: cardStyle }, [
|
|
502
525
|
h("p", { style: groupTitleStyle }, t("smartSummary")),
|
|
526
|
+
// 轮次结束自动记录(summarize):两种记忆模式的共用总闸门,始终显示,置于记忆模式之上、不随模式切换隐藏。
|
|
527
|
+
toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
|
|
503
528
|
row(t("memoryModeLabel"), draft.memoryMode === "smart" ? t("memoryModeHintSmart") : t("memoryModeHintPlugin"),
|
|
504
529
|
h("select", {
|
|
505
530
|
value: draft.memoryMode === "smart" ? "smart" : "plugin",
|
|
@@ -511,20 +536,24 @@ window.__ModuleLoader__.load({
|
|
|
511
536
|
h("option", { value: "smart" }, "智能模式")
|
|
512
537
|
]), "memoryMode"),
|
|
513
538
|
draft.memoryMode === "smart"
|
|
514
|
-
?
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
539
|
+
? h("div", null, [
|
|
540
|
+
row(t("summaryModelLabel"), t("summaryModelHint"),
|
|
541
|
+
h("select", {
|
|
542
|
+
// 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
|
|
543
|
+
// 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
|
|
544
|
+
value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
|
|
545
|
+
disabled,
|
|
546
|
+
style: selectStyle,
|
|
547
|
+
onChange: (e) => edit("summaryModel", e.target.value)
|
|
548
|
+
}, [
|
|
549
|
+
h("option", { value: "" }, "复用当前会话模型"),
|
|
550
|
+
...modelOptions.map((o) => h("option", { value: o.value }, o.label))
|
|
551
|
+
]), "summaryModel"),
|
|
552
|
+
toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
|
|
553
|
+
row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
|
|
554
|
+
row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
|
|
555
|
+
])
|
|
526
556
|
: h("div", null, [
|
|
527
|
-
toggle(t("summarizeLabel"), t("summarizeHint"), "summarize"),
|
|
528
557
|
toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
|
|
529
558
|
])
|
|
530
559
|
]),
|
package/lib/common/prompts.mjs
CHANGED
|
@@ -4,22 +4,49 @@
|
|
|
4
4
|
// 场景关键词(增强原方案 D):命中即视为"用户告知偏好 / 做出技术决策",纯文本轮次也写。
|
|
5
5
|
export const SCENE_KEYWORDS = ["记住", "记一下", "remind", "偏好", "决定", "以后都", "约定", "采用", "根因", "修复"];
|
|
6
6
|
|
|
7
|
-
// 智能模式(memoryMode==="smart")的摘要提炼 prompt
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
// 智能模式(memoryMode==="smart")的摘要提炼 prompt(v1.4.0 起为函数,支持回喂存量记忆)。
|
|
8
|
+
// 让 LLM 输出 {summary, durable:[{scope,fact}], memoryOps:[...]};memoryOps 用于基于编号的现有记忆做增量维护。
|
|
9
|
+
// allowDelete=true(手动蒸馏按钮):开放 add/replace/delete;false(自动智能模式):禁 delete,仅 add/replace。
|
|
10
|
+
// 注意:回喂是否实际发生由 distill.mjs 按 cfg().feedbackEnabled 决定(智能模式级,自动/手动均生效);
|
|
11
|
+
// 此处仅声明能力边界——allowDelete 决定 prompt 是否允许 delete 指令(自动模式禁 delete,手动按钮放开)。
|
|
12
|
+
export function SUMMARY_PROMPT({ allowDelete = false, outputBudget } = {}) {
|
|
13
|
+
const feedbackInstr = allowDelete
|
|
14
|
+
? "【维护现有记忆】若对话中提供了「项目级记忆 / 用户级记忆(逐行编号)」,你可在输出 memoryOps 中对现有记忆做增量维护:\n" +
|
|
15
|
+
'- add:新增一条事实(scope + fact),勿与现有重复;\n' +
|
|
16
|
+
'- replace:line=现有行号,oldText=该行精确原文,newText=替换后全文(仅内容行,禁止改动 # / <!-- 结构行);\n' +
|
|
17
|
+
'- delete:line=现有行号,oldText=该行精确原文(仅内容行,禁止删除 # / <!-- 结构行);\n' +
|
|
18
|
+
"所有 replace/delete 必须带 oldText 精确匹配(匹配失败将被拒绝、不修改任何内容)。"
|
|
19
|
+
: "【维护现有记忆】若对话中提供了「项目级记忆 / 用户级记忆(逐行编号)」,你可在输出 memoryOps 中对现有记忆做增量维护:\n" +
|
|
20
|
+
'- add:新增一条事实(scope + fact),勿与现有重复;\n' +
|
|
21
|
+
'- replace:line=现有行号,oldText=该行精确原文,newText=替换后全文(仅内容行);\n' +
|
|
22
|
+
"(当前为自动模式,禁止使用 delete,避免误删记忆;replace 必须带 oldText 精确匹配,匹配失败将被拒绝。)";
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
"你是会话记忆提炼助手。根据下面这段【新产生的】对话(含工具调用与结果),提炼两样东西:\n" +
|
|
26
|
+
"1) summary:一段简洁中文摘要——这段对话做了什么、关键结果(路径/命令/数字)。\n" +
|
|
27
|
+
"2) durable:值得跨 session 长期记住的事实列表(最多 3 条),每条带 scope:\n" +
|
|
28
|
+
' - "project" = 当前项目的约定/决策/技术事实\n' +
|
|
29
|
+
' - "user" = 跨项目的个人偏好/习惯\n' +
|
|
30
|
+
" 闲聊、一次性操作、显而易见的内容、纯错误现象(错误已有独立记录)不要提炼;没有就留空数组。\n" +
|
|
31
|
+
"【输出风格硬约束】\n" +
|
|
32
|
+
"1) summary 必须是**客观、第三人称、陈述性**的中文摘要,描述「对话完成了什么 / 得出了什么结论」;\n" +
|
|
33
|
+
" **严禁**包含:提问、提议、请示、寒暄、自我指涉(如「我可以帮你…」「需要的话…」「要不要…」)、\n" +
|
|
34
|
+
" 内心独白(如「But wait」「I should…」)或任何对话体 / 元叙述——你是在**写记忆**,不是在**对话**。\n" +
|
|
35
|
+
"2) durable 每条必须是**可独立成立的客观事实**;**严禁**把「助手说过的话 / 向用户提问 / 未确认的提议」本身当作事实记录。\n" +
|
|
36
|
+
"3) 若对话中仅含未决的提问或提议、无确定结论,summary 写「本次对话为未决讨论,暂无确定结论」,durable 留空,绝不可照抄对话体文本。\n" +
|
|
37
|
+
feedbackInstr +
|
|
38
|
+
(outputBudget != null ? "\n【输出长度约束】你的最终 JSON(summary + durable + memoryOps 合计)请控制在约 " + outputBudget + " token(≈" + Math.round(outputBudget * 1.8) + " 字)以内。思考/推理过程不受此限、可充分判断(含「该不该记」「是否一次性操作」「与旧记忆是否冲突需 update/delete」),但**最终成稿必须精炼、不超预算**;严禁为凑结构而填充无用内容。\n" : "") +
|
|
39
|
+
"\n只输出 JSON,不要多余文字,格式:\n" +
|
|
40
|
+
'{"summary":"...","durable":[{"scope":"project","fact":"一句话"}],"memoryOps":[{"op":"add","scope":"project","fact":"..."},{"op":"replace","line":12,"oldText":"...","newText":"..."},{"op":"delete","line":7,"oldText":"..."}]}\n' +
|
|
41
|
+
"(没有需要维护的现有记忆时,memoryOps 留空数组 []。)"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
18
44
|
|
|
19
45
|
// v1.2.0 项目记忆蒸馏 prompt(固化自 upgrade plan/v1.2.0/记忆蒸馏Prompt.md,供按钮「蒸馏项目记忆」稳定调用)。
|
|
20
46
|
// 用法:system = 本 prompt,user = 项目 MEMORY.md 全文,输出 = 精炼后的项目记忆(直接覆盖写回)。
|
|
21
|
-
export
|
|
22
|
-
|
|
47
|
+
export function DISTILL_PROMPT({ outputBudget } = {}) {
|
|
48
|
+
return (
|
|
49
|
+
"你是一名「项目记忆蒸馏器」。我会给你一份项目的原始材料(可能是记忆文件、会话记录、零散笔记,或多份材料的拼合)。你的任务:把它提炼成一段精炼的项目记忆。\n" +
|
|
23
50
|
"\n" +
|
|
24
51
|
"【提炼总原则】\n" +
|
|
25
52
|
"只保留「以后还会用到」的信息,删掉「一次性过程」。\n" +
|
|
@@ -68,4 +95,7 @@ export const DISTILL_PROMPT =
|
|
|
68
95
|
"5. 每个坑是否「现象 → 根因 → 解法」三要素齐全?\n" +
|
|
69
96
|
"6. 是否只输出了骨架要求的章节,没有额外发挥?\n" +
|
|
70
97
|
"\n" +
|
|
71
|
-
"
|
|
98
|
+
(outputBudget != null ? "【输出长度约束】你提炼后的项目记忆请控制在约 " + outputBudget + " token(≈" + Math.round(outputBudget * 1.8) + " 字)以内。思考不受限,但成稿须精炼、不超预算;只保留「以后还会用到」的信息,删掉「一次性过程」。\n" : "") +
|
|
99
|
+
"现在开始处理我提供的材料。"
|
|
100
|
+
);
|
|
101
|
+
}
|
package/lib/common/records.mjs
CHANGED
|
@@ -75,6 +75,85 @@ export async function removeLineByMatch(file, match) {
|
|
|
75
75
|
return { removed, ok: true, reason: "done", lines: removedLines };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// 全文逐行编号读取(v1.4.0 特性3:回喂存量记忆)。返回 { lines:[{n,raw,structural}], text }。
|
|
79
|
+
// 行号 n 与文件实际行一一对应(1 起),供 LLM 通过 line 引用;text 为每行 `n|raw` 拼接,直接喂模型。
|
|
80
|
+
// 不截断、无字符预算(用户明确要求全量回喂,避免截断导致记忆错误)。
|
|
81
|
+
export function readNumberedMemory(file) {
|
|
82
|
+
let cur = "";
|
|
83
|
+
try {
|
|
84
|
+
cur = readFileSync(file, "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
return { lines: [], text: "" };
|
|
87
|
+
}
|
|
88
|
+
if (!cur) return { lines: [], text: "" };
|
|
89
|
+
const lines = cur.split("\n").map((raw, i) => {
|
|
90
|
+
const t = raw.trim();
|
|
91
|
+
return { n: i + 1, raw, structural: isStructural(t) };
|
|
92
|
+
});
|
|
93
|
+
const text = lines.map((l) => `${l.n}|${l.raw}`).join("\n");
|
|
94
|
+
return { lines, text };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 基于编号现有记忆的增量维护操作(v1.4.0 特性3)。op 之一:
|
|
98
|
+
// - add:追加一行(text/newText,去重);scope 由 caller 决定目标文件。
|
|
99
|
+
// - replace:替换第 line 行(写前重读 + normLine(oldText) 精确匹配校验,防 stale 行号误改)。
|
|
100
|
+
// - delete:删除第 line 行(结构行 / 未匹配 oldText 均拒绝)。
|
|
101
|
+
// 写前重读(re-read before write)保证基于最新文件内容,避免并发/重试导致的 stale 行号错位。
|
|
102
|
+
// 返回 { ok, reason, changed, line?, removed?, actual? }
|
|
103
|
+
export async function applyMemoryOp(file, op) {
|
|
104
|
+
const o = op || {};
|
|
105
|
+
await mkdir(dirname(file), { recursive: true });
|
|
106
|
+
let cur = "";
|
|
107
|
+
try {
|
|
108
|
+
cur = readFileSync(file, "utf8");
|
|
109
|
+
} catch {
|
|
110
|
+
/* 文件尚不存在 */
|
|
111
|
+
}
|
|
112
|
+
const lines = cur.split("\n");
|
|
113
|
+
const normRaw = (s) => normLine((s || "").trim());
|
|
114
|
+
|
|
115
|
+
if (o.op === "add") {
|
|
116
|
+
const text = (o.text ?? o.newText ?? "").trim();
|
|
117
|
+
if (!text) return { ok: false, reason: "empty-add" };
|
|
118
|
+
if (cur && normRaw(cur).includes(normRaw(text))) return { ok: true, reason: "dup", changed: false };
|
|
119
|
+
const base = cur && !cur.endsWith("\n") ? cur + "\n" : cur;
|
|
120
|
+
await writeFile(file, `${base}${text}\n`, "utf8");
|
|
121
|
+
return { ok: true, reason: "added", changed: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (o.op !== "replace" && o.op !== "delete") {
|
|
125
|
+
return { ok: false, reason: "unknown-op", op: o.op };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const n = o.line;
|
|
129
|
+
if (!Number.isInteger(n) || n < 1 || n > lines.length) {
|
|
130
|
+
return { ok: false, reason: "bad-line", line: n, totalLines: lines.length };
|
|
131
|
+
}
|
|
132
|
+
const idx = n - 1;
|
|
133
|
+
const rawLine = lines[idx];
|
|
134
|
+
const trimmed = rawLine.trim();
|
|
135
|
+
if (isStructural(trimmed)) {
|
|
136
|
+
return { ok: false, reason: "structural-protected", line: n };
|
|
137
|
+
}
|
|
138
|
+
// 写前重读后精确匹配:确保操作基于最新内容(防止 stale 行号导致误改/误删)。
|
|
139
|
+
const oldNorm = normRaw(o.oldText);
|
|
140
|
+
if (oldNorm && normRaw(rawLine) !== oldNorm) {
|
|
141
|
+
return { ok: false, reason: "conflict", line: n, expected: o.oldText, actual: trimmed };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (o.op === "delete") {
|
|
145
|
+
const newLines = lines.slice(0, idx).concat(lines.slice(idx + 1));
|
|
146
|
+
await writeFile(file, newLines.join("\n"), "utf8");
|
|
147
|
+
return { ok: true, reason: "deleted", changed: true, line: n, removed: trimmed };
|
|
148
|
+
}
|
|
149
|
+
// replace
|
|
150
|
+
const newText = (o.newText ?? "").trim();
|
|
151
|
+
if (!newText) return { ok: false, reason: "empty-replace" };
|
|
152
|
+
lines[idx] = newText;
|
|
153
|
+
await writeFile(file, lines.join("\n"), "utf8");
|
|
154
|
+
return { ok: true, reason: "replaced", changed: true, line: n };
|
|
155
|
+
}
|
|
156
|
+
|
|
78
157
|
// 读取目录下最近的非今日每日日志日期(最多 limit 份,降序)。供删除范围圈定。
|
|
79
158
|
export async function recentLogDates(dir, limit = 3) {
|
|
80
159
|
let files = [];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// memory-palace 蒸馏 LLM 调用失败重试(v1.4.0 特性2)。
|
|
2
|
+
// 纯函数 + 通用重试驱动,与具体 distill 逻辑解耦,可被 test-load.mjs 直接单测。
|
|
3
|
+
//
|
|
4
|
+
// 设计要点(已与用户确认):
|
|
5
|
+
// - 错误分三类:retryable(5xx/网络/超时/中断,可重试)、limited(429/529 限流,可重试但分类不同)、
|
|
6
|
+
// fatal(401/403/404 鉴权/模型不存在/永久客户端错误,不重试)。
|
|
7
|
+
// - 退避:指数退避 delay = min(BASE_DELAY_MS * FACTOR^attempt, MAX_DELAY_MS) + 抖动(≤15%)。
|
|
8
|
+
// - 上限:通用 MAX_RETRIES=3;500 类服务端错误单独限到 HTTP500_MAX_RETRIES=1(500 多为主服务挂,
|
|
9
|
+
// 重试意义不大且拖慢降级)。
|
|
10
|
+
// - 每次尝试【独立超时】:makeAttempt 内部每次新开 AbortSignal.timeout(timeoutMs),避免单次超时耗尽累计预算。
|
|
11
|
+
export const RETRY_CONSTANTS = {
|
|
12
|
+
BASE_DELAY_MS: 2000,
|
|
13
|
+
FACTOR: 2,
|
|
14
|
+
MAX_RETRIES: 3,
|
|
15
|
+
MAX_DELAY_MS: 32000,
|
|
16
|
+
HTTP500_MAX_RETRIES: 1,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 把一个可能携带状态码的 error / finish.failure 归一为分类结果。
|
|
20
|
+
// 入参 err 可能是:stream 抛出的 Error、ctx.llm.stream 同步抛出的错误、或 finish.failure 对象。
|
|
21
|
+
export function classifyFailure(err) {
|
|
22
|
+
const e = err && typeof err === "object" ? err : {};
|
|
23
|
+
const status =
|
|
24
|
+
e.status ??
|
|
25
|
+
e.statusCode ??
|
|
26
|
+
e.code ??
|
|
27
|
+
e.response?.status ??
|
|
28
|
+
(typeof e.status === "number" ? e.status : undefined);
|
|
29
|
+
const code = e.code || e.name;
|
|
30
|
+
const message = String(e.message || e.statusText || "")
|
|
31
|
+
.concat(" ", String(e.response?.statusText || ""))
|
|
32
|
+
.trim();
|
|
33
|
+
|
|
34
|
+
// fatal:鉴权 / 模型不存在 / 永久客户端错误 —— 重试无意义。
|
|
35
|
+
if (status === 401 || status === 403) return { kind: "fatal", status, message };
|
|
36
|
+
if (status === 404) return { kind: "fatal", status, message };
|
|
37
|
+
if (status === 400) return { kind: "fatal", status, message };
|
|
38
|
+
|
|
39
|
+
// limited:限流 / 过载 —— 可重试,但 caller 可按 limited 走更激进退避(此处同用指数退避即可)。
|
|
40
|
+
if (status === 429 || status === 529) return { kind: "limited", status, message };
|
|
41
|
+
|
|
42
|
+
// retryable:5xx 服务端错误(含 500,单独限重试次数见 runWithRetry)。
|
|
43
|
+
if (typeof status === "number" && status >= 500 && status < 600) {
|
|
44
|
+
return { kind: "retryable", status, message };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 网络类错误(连接重置/超时/连接拒绝)——DNS 解析失败(ENOTFOUND) 不重试(见升级计划:通常需人工干预)。
|
|
48
|
+
if (
|
|
49
|
+
code === "ECONNRESET" ||
|
|
50
|
+
code === "ETIMEDOUT" ||
|
|
51
|
+
code === "ECONNREFUSED" ||
|
|
52
|
+
code === "ENETUNREACH"
|
|
53
|
+
) {
|
|
54
|
+
return { kind: "retryable", status, message };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 超时 / 中断:AbortSignal 触发、显式 aborted/timeout 字样。
|
|
58
|
+
if (
|
|
59
|
+
e.name === "AbortError" ||
|
|
60
|
+
code === "ABORT_ERR" ||
|
|
61
|
+
/aborted|timeout|timed out|deadline/i.test(message)
|
|
62
|
+
) {
|
|
63
|
+
return { kind: "retryable", status, message };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 兜底:未知错误不盲目重试(避免死循环),按 fatal 处理。
|
|
67
|
+
return { kind: "fatal", status, message };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 指数退避 + 抖动(≤15%)。attempt 从 0 起算。
|
|
71
|
+
export function backoffDelayMs(attempt) {
|
|
72
|
+
const { BASE_DELAY_MS, FACTOR, MAX_DELAY_MS } = RETRY_CONSTANTS;
|
|
73
|
+
const n = Math.max(0, attempt | 0);
|
|
74
|
+
const base = Math.min(BASE_DELAY_MS * Math.pow(FACTOR, n), MAX_DELAY_MS);
|
|
75
|
+
const jitter = Math.random() * (base * 0.15);
|
|
76
|
+
return Math.round(base + jitter);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function sleep(ms) {
|
|
80
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 重试失败后的结构化错误(caller 据此降级,不向上抛裸 Error)。
|
|
84
|
+
export class LlmRetryExhausted extends Error {
|
|
85
|
+
constructor(cls, attempts) {
|
|
86
|
+
super(`llm call exhausted after ${attempts} attempt(s): ${cls.kind} ${cls.status || ""} ${cls.message}`);
|
|
87
|
+
this.name = "LlmRetryExhausted";
|
|
88
|
+
this.cls = cls;
|
|
89
|
+
this.attempts = attempts;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 通用重试驱动。
|
|
95
|
+
* @param {() => Promise<{ finish?: { kind?: string, failure?: any }, text?: string }>} makeAttempt
|
|
96
|
+
* 单次尝试:内部自行创建独立 AbortSignal 并处理流;返回 { finish, text }。
|
|
97
|
+
* 成功 → finish.kind 为 undefined/'done'/'max-tokens';失败 → finish.kind 为 'error'/'aborted'(含 failure)
|
|
98
|
+
* 或抛异常(网络/超时)。
|
|
99
|
+
* @param {{ maxRetries?: number, classify?: (e:any)=>any, onAttempt?: (info:any)=>void }} opts
|
|
100
|
+
* @returns {Promise<{ finish?: any, text?: string, attempts: number }>}
|
|
101
|
+
*/
|
|
102
|
+
export async function runWithRetry(makeAttempt, opts = {}) {
|
|
103
|
+
const { maxRetries = RETRY_CONSTANTS.MAX_RETRIES, onAttempt } = opts;
|
|
104
|
+
const classify = opts.classify || classifyFailure;
|
|
105
|
+
let lastCls = null;
|
|
106
|
+
|
|
107
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
108
|
+
if (onAttempt) onAttempt({ attempt, maxRetries });
|
|
109
|
+
try {
|
|
110
|
+
const res = await makeAttempt(attempt);
|
|
111
|
+
const kind = res?.finish?.kind;
|
|
112
|
+
if (kind === "error" || kind === "aborted") {
|
|
113
|
+
const cls = classify(res.finish?.failure || {});
|
|
114
|
+
lastCls = cls;
|
|
115
|
+
// 500 类单独限重试次数;其余按 maxRetries。
|
|
116
|
+
const cap = cls.status === 500 ? RETRY_CONSTANTS.HTTP500_MAX_RETRIES : maxRetries;
|
|
117
|
+
if ((cls.kind === "retryable" || cls.kind === "limited") && attempt < cap) {
|
|
118
|
+
await sleep(backoffDelayMs(attempt));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
throw new LlmRetryExhausted(cls, attempt + 1);
|
|
122
|
+
}
|
|
123
|
+
// 成功 / max-tokens:直接返回,文本解析交给 caller。
|
|
124
|
+
return { ...res, attempts: attempt + 1 };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const cls = classify(err);
|
|
127
|
+
lastCls = cls;
|
|
128
|
+
const cap = cls.status === 500 ? RETRY_CONSTANTS.HTTP500_MAX_RETRIES : maxRetries;
|
|
129
|
+
if ((cls.kind === "retryable" || cls.kind === "limited") && attempt < cap) {
|
|
130
|
+
await sleep(backoffDelayMs(attempt));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw new LlmRetryExhausted(cls, attempt + 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 理论不可达(循环内必然返回或抛出),兜底。
|
|
137
|
+
throw new LlmRetryExhausted(lastCls || { kind: "fatal", message: "unknown" }, maxRetries + 1);
|
|
138
|
+
}
|
package/lib/distill.mjs
CHANGED
|
@@ -5,7 +5,8 @@ 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
7
|
import { expandHome, readMdSync, toHomeShort, nowStamp } from "./common/text.mjs";
|
|
8
|
-
import { appendLineDedup } from "./common/records.mjs";
|
|
8
|
+
import { appendLineDedup, readNumberedMemory, applyMemoryOp } from "./common/records.mjs";
|
|
9
|
+
import { runWithRetry, RETRY_CONSTANTS } from "./common/retry.mjs";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {{ ctx: object, getConfig: () => object, paths: object, records: object, state: object }} deps
|
|
@@ -59,10 +60,47 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
59
60
|
// 项目蒸馏乐观锁:按目标 MEMORY.md 绝对路径防并发重复蒸馏。
|
|
60
61
|
const distillLocks = new Set();
|
|
61
62
|
|
|
63
|
+
// 单次 LLM 流尝试(供 runWithRetry 调用):每次【新建独立 AbortSignal】,避免单次超时耗尽累计预算。
|
|
64
|
+
// 返回 { finish, text, chunks, deltaChars, gotFirstChunk, tFirstChunk, elapsed };流中抛错(网络/超时/中断)原样向上抛,
|
|
65
|
+
// 由 runWithRetry 分类决定是否重试。finish.kind 为 error/aborted 时不抛(让 runWithRetry 据此决定重试/降级)。
|
|
66
|
+
async function streamOnce(params, timeoutMs) {
|
|
67
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
68
|
+
const t0 = Date.now();
|
|
69
|
+
let gotFirstChunk = false, tFirstChunk = null, chunks = 0, deltaChars = 0;
|
|
70
|
+
const res = ctx.llm.stream({ ...params, signal });
|
|
71
|
+
const asm = new BlockAssembler();
|
|
72
|
+
try {
|
|
73
|
+
for await (const ch of res) {
|
|
74
|
+
signal.throwIfAborted();
|
|
75
|
+
chunks++;
|
|
76
|
+
deltaChars += (ch.text?.length || 0);
|
|
77
|
+
if (!gotFirstChunk) { gotFirstChunk = true; tFirstChunk = Date.now() - t0; }
|
|
78
|
+
asm.push(ch);
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// 流中抛错(网络/超时/中断):原样抛出供 runWithRetry 分类重试或失败。
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
const finish = asm.finish;
|
|
85
|
+
const text = asm
|
|
86
|
+
.blocks()
|
|
87
|
+
.filter((b) => b.type === "text")
|
|
88
|
+
.map((b) => b.text ?? "")
|
|
89
|
+
.join("")
|
|
90
|
+
.trim();
|
|
91
|
+
return { finish, text, chunks, deltaChars, gotFirstChunk, tFirstChunk, elapsed: Date.now() - t0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
// 蒸馏核心(v1.2.0 抽取):对指定 session 的 [fromSeq, ∞) surface 事件做 LLM 智能摘要并写盘。
|
|
63
95
|
// 自动智能模式(summarizeTurn,增量断点)与按钮「蒸馏会话」(全量 fromSeq=0)共用本核心。
|
|
64
96
|
// 返回 { ok, summary, durableCount };失败 { ok:false }(调用方各自决定降级策略)。
|
|
65
|
-
async function distillSessionCore(session, dirs, fromSeq) {
|
|
97
|
+
async function distillSessionCore(session, dirs, fromSeq, opts) {
|
|
98
|
+
const allowDelete = !!(opts && opts.allowDelete);
|
|
99
|
+
// 回喂存量记忆(v1.4.0 特性3):智能模式级能力——feedbackEnabled 开启即在【自动智能模式(turn/end)
|
|
100
|
+
// 与手动蒸馏按钮】两条链路都回喂项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)。
|
|
101
|
+
// delete 仍仅手动蒸馏(allowDelete=true)开放;自动模式 memoryOps 中的 delete 在 §5.5 被 !allowDelete 跳过。
|
|
102
|
+
// 回喂目标严格限定为 MEMORY.md;每日日志(.deepseek-harness/memory/YYYY-MM-DD.md)不读取、不改写、不删旧行。
|
|
103
|
+
const useFeedback = !!cfg().feedbackEnabled;
|
|
66
104
|
if (!session) {
|
|
67
105
|
dbgFail("no session");
|
|
68
106
|
return { ok: false };
|
|
@@ -90,71 +128,102 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
90
128
|
dbgFail("no events in range", { fromSeq, eventsLen: (session.events || []).length, newEvents: newEvents.length, hasDerive: !!session.deriveEventMessage });
|
|
91
129
|
return { ok: false };
|
|
92
130
|
}
|
|
131
|
+
// 2.5 回喂存量记忆(v1.4.0 特性3):仅当 useFeedback 时,读取项目级 + 用户级 MEMORY.md 全文
|
|
132
|
+
// (逐行编号、无截断)作为上下文前置消息,让 LLM 基于既有记忆做 add/replace/delete 增量维护。
|
|
133
|
+
let messages = hist;
|
|
134
|
+
const feedbackFiles = { project: null, user: null };
|
|
135
|
+
if (useFeedback) {
|
|
136
|
+
const sessCwd = session?.header?.cwd ?? undefined;
|
|
137
|
+
const projectMem = paths.memoryFileOf(dirs[0], sessCwd);
|
|
138
|
+
const userMem = expandHome(cfg().userMemoryPath);
|
|
139
|
+
// 始终记录目标文件路径(即使文件尚不存在),便于后续 add 操作创建文件。
|
|
140
|
+
feedbackFiles.project = projectMem;
|
|
141
|
+
feedbackFiles.user = userMem;
|
|
142
|
+
let fbText = "";
|
|
143
|
+
if (projectMem) {
|
|
144
|
+
const pm = readNumberedMemory(projectMem);
|
|
145
|
+
if (pm.text) fbText += "【项目级记忆(逐行编号,行号从 1 开始)】\n" + pm.text + "\n";
|
|
146
|
+
}
|
|
147
|
+
if (userMem) {
|
|
148
|
+
const um = readNumberedMemory(userMem);
|
|
149
|
+
if (um.text) fbText += "【用户级记忆(逐行编号,行号从 1 开始)】\n" + um.text + "\n";
|
|
150
|
+
}
|
|
151
|
+
if (fbText) {
|
|
152
|
+
messages = [
|
|
153
|
+
createUserMessage({
|
|
154
|
+
content: [{ type: "text", text: "以下是现有记忆(仅供你增量维护参考,不要当作本次对话内容):\n" + fbText }],
|
|
155
|
+
source: { kind: "plugin", plugin: "memory-palace" },
|
|
156
|
+
}),
|
|
157
|
+
...hist,
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
dbg("session feedback", { hasProject: !!feedbackFiles.project, hasUser: !!feedbackFiles.user });
|
|
161
|
+
}
|
|
93
162
|
// 3. LLM 调用:借鉴 dsh-sideband 的 summarizeJob(src/summarizer.ts)——
|
|
94
163
|
// a) SUMMARY_PROMPT 放 system 参数(GenerateOptions.system),指令不占 user 消息、模型更遵守;
|
|
95
164
|
// b) AbortSignal.timeout 超时保护(默认 60s,Config.summaryTimeoutMs 可调),流循环内 throwIfAborted,
|
|
96
165
|
// 杜绝「LLM 慢/挂起 → for await 无限等待」导致的摘要静默失败;
|
|
97
166
|
// c) finish.kind 细化:error/aborted → 失败降级(探针带 failure.message);max-tokens → 尝试用已有文本
|
|
98
|
-
//
|
|
167
|
+
// (解析失败时抽取 {...} JSON 块兜底,仍失败则 summary 置空、记极简占位,绝不把 LLM 自由文本当 summary 落盘)。
|
|
99
168
|
const timeoutMs = c.summaryTimeoutMs || 60000;
|
|
100
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
101
169
|
const t0 = Date.now();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const asm = new BlockAssembler();
|
|
113
|
-
try {
|
|
114
|
-
for await (const ch of res) {
|
|
115
|
-
signal.throwIfAborted();
|
|
116
|
-
chunks++;
|
|
117
|
-
deltaChars += (ch.text?.length || 0);
|
|
118
|
-
if (!gotFirstChunk) { gotFirstChunk = true; tFirstChunk = Date.now() - t0; dbg("session first chunk", { ms: tFirstChunk }); }
|
|
119
|
-
asm.push(ch);
|
|
120
|
-
}
|
|
170
|
+
dbg("session request", { provider, model, messages: messages.length, maxTokens: "(model native cap)", timeoutMs, maxRetries: RETRY_CONSTANTS.MAX_RETRIES });
|
|
171
|
+
let result;
|
|
172
|
+
try {
|
|
173
|
+
result = await runWithRetry(
|
|
174
|
+
() => streamOnce({ provider, model, system: SUMMARY_PROMPT({ allowDelete, outputBudget: c.summaryMaxTokens }), messages }, timeoutMs),
|
|
175
|
+
{
|
|
176
|
+
maxRetries: RETRY_CONSTANTS.MAX_RETRIES,
|
|
177
|
+
onAttempt: ({ attempt, maxRetries }) => dbg("session attempt", { attempt, maxRetries }),
|
|
178
|
+
},
|
|
179
|
+
);
|
|
121
180
|
} catch (e) {
|
|
122
|
-
|
|
181
|
+
const cls = e?.cls || { kind: "fatal", message: String(e?.message ?? e) };
|
|
182
|
+
dbgFail("session distill exhausted", { kind: cls.kind, status: cls.status ?? "", attempts: e?.attempts });
|
|
123
183
|
return { ok: false };
|
|
124
184
|
}
|
|
125
|
-
const finish =
|
|
126
|
-
dbg("session finish", { kind: finish?.kind });
|
|
185
|
+
const { finish, text, chunks, deltaChars, tFirstChunk } = result;
|
|
186
|
+
dbg("session finish", { kind: finish?.kind, attempts: result.attempts });
|
|
127
187
|
dbg("session stream done", { chunks, deltaChars, firstChunkMs: tFirstChunk });
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
188
|
+
// 本地调试(受 distillDebugLog + distillLogLevel==="debug" 双门控):把 LLM 原始响应 text 原样打到 stderr,
|
|
189
|
+
// 并用分隔符整段包裹,便于从 stderr 一眼框住起止、不与其它 dbg 行混淆。
|
|
190
|
+
if (cfg().distillDebugLog && cfg().distillLogLevel === "debug") {
|
|
191
|
+
console.error("[memory-palace][debug] session llm raw text begin >>>");
|
|
192
|
+
console.error("--------------------------->");
|
|
193
|
+
console.error(text);
|
|
194
|
+
console.error("<----------------------------");
|
|
195
|
+
console.error("[memory-palace][debug] session llm raw text end <<<");
|
|
131
196
|
}
|
|
132
197
|
if (finish && finish.kind === "max-tokens") {
|
|
133
198
|
console.error("[memory-palace] distill warn: llm finish max-tokens (attempting partial JSON)", JSON.stringify({}));
|
|
134
199
|
}
|
|
135
|
-
const text = asm
|
|
136
|
-
.blocks()
|
|
137
|
-
.filter((b) => b.type === "text")
|
|
138
|
-
.map((b) => b.text ?? "")
|
|
139
|
-
.join("")
|
|
140
|
-
.trim();
|
|
141
200
|
if (!text) {
|
|
142
201
|
dbgFail("empty llm text");
|
|
143
202
|
return { ok: false };
|
|
144
203
|
}
|
|
145
|
-
// 4. 解析 JSON:{summary, durable:[{scope,fact}]}
|
|
146
|
-
|
|
204
|
+
// 4. 解析 JSON:{summary, durable:[{scope,fact}], memoryOps:[...]}。
|
|
205
|
+
// 防污染(v1.4.0 修复):解析失败【绝不】退化成"全文当 summary"——否则对话原文 / 助手独白 / 向用户的请示
|
|
206
|
+
// 会被整段灌进每日日志。先尝试从自由文本抽取最后一个 {...} JSON 块再解析;仍失败则 summary 留空。
|
|
207
|
+
let summary = "";
|
|
147
208
|
let durable = [];
|
|
209
|
+
let memoryOps = [];
|
|
148
210
|
let jsonOk = false;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
211
|
+
const tryParseJson = (s) => {
|
|
212
|
+
try {
|
|
213
|
+
const o = JSON.parse(s);
|
|
214
|
+
return o && typeof o === "object" ? o : null;
|
|
215
|
+
} catch { return null; }
|
|
216
|
+
};
|
|
217
|
+
let parsed = tryParseJson(text);
|
|
218
|
+
if (!parsed) {
|
|
219
|
+
const block = text.match(/\{[\s\S]*\}/);
|
|
220
|
+
if (block) parsed = tryParseJson(block[0]);
|
|
221
|
+
}
|
|
222
|
+
if (parsed) {
|
|
223
|
+
jsonOk = true;
|
|
224
|
+
if (typeof parsed.summary === "string") summary = parsed.summary;
|
|
225
|
+
if (Array.isArray(parsed.durable)) durable = parsed.durable;
|
|
226
|
+
if (Array.isArray(parsed.memoryOps)) memoryOps = parsed.memoryOps;
|
|
158
227
|
}
|
|
159
228
|
// 5. 写盘:summary → 每日日志 [smart] 前缀;durable → MEMORY.md `- [smart] fact`(按 scope 分层,去重)。
|
|
160
229
|
// v1.2.0 修复:手动蒸馏(按钮)时 dirs 按 session.header.cwd 解析,但 memoryFileOf 缺省用闭包
|
|
@@ -162,7 +231,11 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
162
231
|
// 此处显式传会话 cwd,使 dsh 目录正确解析到同级 MEMORY.md(自动模式 activeCwd 即会话 cwd,行为不变)。
|
|
163
232
|
const sessCwd = session?.header?.cwd ?? undefined;
|
|
164
233
|
for (const dir of dirs) {
|
|
165
|
-
|
|
234
|
+
// summary 为空(解析失败/未产出)→ 记极简占位,绝不把 LLM 自由文本当 summary 落盘(v1.4.0 防污染)。
|
|
235
|
+
const dailyLine = summary
|
|
236
|
+
? `\n[smart] ${summary}\n\n`
|
|
237
|
+
: "\n[smart] 本段会话已蒸馏,但 LLM 未返回可解析的结构化摘要,已跳过原文落盘以避免污染记忆。\n\n";
|
|
238
|
+
await records.appendDaily(dir, dailyLine);
|
|
166
239
|
}
|
|
167
240
|
for (const d of durable) {
|
|
168
241
|
if (!d || typeof d !== "object") continue;
|
|
@@ -177,6 +250,23 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
177
250
|
}
|
|
178
251
|
}
|
|
179
252
|
}
|
|
253
|
+
// 5.5 应用 memoryOps(v1.4.0 特性3 增量维护):useFeedback 时由 LLM 产出(自动/手动皆可能)。
|
|
254
|
+
// add → 追加到对应 MEMORY.md(file 由 scope 决定,可能即时创建);replace/delete → applyMemoryOp
|
|
255
|
+
// (写前重读 + normLine 精确匹配 + 结构行保护,防止 stale 行号误改/误删)。
|
|
256
|
+
// 自动模式(allowDelete=false)prompt 同步禁用 delete,且下方 delete op 经 !allowDelete 跳过;
|
|
257
|
+
// 自动模式仅允许 add/replace 增量维护,delete 仅手动蒸馏按钮开放。
|
|
258
|
+
let opApplied = 0, opSkipped = 0;
|
|
259
|
+
for (const op of memoryOps) {
|
|
260
|
+
if (!op || typeof op !== "object") continue;
|
|
261
|
+
const scope = op.scope === "user" ? "user" : "project";
|
|
262
|
+
const file = scope === "user" ? feedbackFiles.user : feedbackFiles.project;
|
|
263
|
+
if (!file) { opSkipped++; continue; }
|
|
264
|
+
if (op.op === "delete" && !allowDelete) { opSkipped++; continue; }
|
|
265
|
+
const r = await applyMemoryOp(file, op).catch(() => ({ ok: false, reason: "exception" }));
|
|
266
|
+
if (r && r.ok && r.changed) opApplied++;
|
|
267
|
+
else if (r && !r.ok) dbgFail("memoryOp rejected", { op: op.op, reason: r.reason, line: r.line });
|
|
268
|
+
}
|
|
269
|
+
dbg("session memoryOps", { total: memoryOps.length, applied: opApplied, skipped: opSkipped });
|
|
180
270
|
dbg("session parsed", { jsonOk, summaryChars: summary.length, durableCount: durable.length });
|
|
181
271
|
dbg("session write done", { dirs: dirs.length, durableWritten: durable.length });
|
|
182
272
|
return { ok: true, summary, durableCount: durable.length };
|
|
@@ -216,7 +306,6 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
216
306
|
distillLocks.add(memFile);
|
|
217
307
|
try {
|
|
218
308
|
const t0 = Date.now();
|
|
219
|
-
let gotFirstChunk = false, tFirstChunk = null, chunks = 0, deltaChars = 0;
|
|
220
309
|
let provider = null, model = null;
|
|
221
310
|
// 模型解析:与 distillSessionCore 同序(summaryModel 注册表反查 > 会话 requestHeader)。
|
|
222
311
|
const resolved = await resolveModel(c.summaryModel, session);
|
|
@@ -224,45 +313,52 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
224
313
|
return { ok: false, message: "无法确定蒸馏模型(summaryModel 未配置且会话无模型信息)。" };
|
|
225
314
|
}
|
|
226
315
|
({ provider, model } = resolved);
|
|
227
|
-
dbg("project request", { provider, model, inputChars: raw.length, maxTokens:
|
|
316
|
+
dbg("project request", { provider, model, inputChars: raw.length, maxTokens: "(model native cap)", timeoutMs: c.summaryTimeoutMs || 60000 });
|
|
228
317
|
|
|
229
|
-
// LLM 蒸馏:system = 固化 DISTILL_PROMPT,user = MEMORY.md
|
|
318
|
+
// LLM 蒸馏:system = 固化 DISTILL_PROMPT,user = MEMORY.md 全文。带失败重试(runWithRetry)。
|
|
230
319
|
const timeoutMs = c.summaryTimeoutMs || 60000;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
320
|
+
let result;
|
|
321
|
+
try {
|
|
322
|
+
result = await runWithRetry(
|
|
323
|
+
() => streamOnce(
|
|
324
|
+
{
|
|
325
|
+
provider,
|
|
326
|
+
model,
|
|
327
|
+
system: DISTILL_PROMPT({ outputBudget: c.projectMaxTokens }),
|
|
328
|
+
messages: [
|
|
329
|
+
createUserMessage({
|
|
330
|
+
content: [{ type: "text", text: raw }],
|
|
331
|
+
source: { kind: "plugin", plugin: "memory-palace" },
|
|
332
|
+
}),
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
timeoutMs,
|
|
336
|
+
),
|
|
337
|
+
{
|
|
338
|
+
maxRetries: RETRY_CONSTANTS.MAX_RETRIES,
|
|
339
|
+
onAttempt: ({ attempt, maxRetries }) => dbg("project attempt", { attempt, maxRetries }),
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
} catch (e) {
|
|
343
|
+
const cls = e?.cls || { kind: "fatal", message: String(e?.message ?? e) };
|
|
344
|
+
dbgFail("project distill exhausted", { kind: cls.kind, status: cls.status ?? "", attempts: e?.attempts });
|
|
345
|
+
const kindZh = cls.kind === "fatal" ? "失败" : "限流/超时";
|
|
346
|
+
return { ok: false, message: `蒸馏${kindZh}(重试 ${e?.attempts || 0} 次后仍失败):${cls.message || ""}` };
|
|
252
347
|
}
|
|
253
|
-
const finish =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
348
|
+
const { finish, text, chunks, deltaChars, gotFirstChunk, tFirstChunk } = result;
|
|
349
|
+
// 本地调试(受 distillDebugLog + distillLogLevel==="debug" 双门控,与会话蒸馏路径一致):分隔符包裹整段原始响应。
|
|
350
|
+
if (cfg().distillDebugLog && cfg().distillLogLevel === "debug") {
|
|
351
|
+
console.error("[memory-palace][debug] project llm raw text begin >>>");
|
|
352
|
+
console.error("--------------------------->");
|
|
353
|
+
console.error(text);
|
|
354
|
+
console.error("<----------------------------");
|
|
355
|
+
console.error("[memory-palace][debug] project llm raw text end <<<");
|
|
356
|
+
}
|
|
357
|
+
if (finish && finish.kind === "max-tokens") {
|
|
358
|
+
dbg("project finish", { kind: "max-tokens", attempts: result.attempts });
|
|
359
|
+
} else {
|
|
360
|
+
dbg("project finish", { kind: finish?.kind, attempts: result.attempts });
|
|
258
361
|
}
|
|
259
|
-
dbg("project finish", { kind: finish?.kind });
|
|
260
|
-
const text = asm
|
|
261
|
-
.blocks()
|
|
262
|
-
.filter((b) => b.type === "text")
|
|
263
|
-
.map((b) => b.text ?? "")
|
|
264
|
-
.join("")
|
|
265
|
-
.trim();
|
|
266
362
|
if (!text) { dbgFail("project empty result", { chunks, deltaChars }); return { ok: false, message: "蒸馏结果为空,未覆盖任何文件。" }; }
|
|
267
363
|
|
|
268
364
|
// 写 cover(与 MEMORY.md 同目录,同文件系统保证 rename 原子)。
|
|
@@ -292,13 +388,8 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
292
388
|
};
|
|
293
389
|
} catch (e) {
|
|
294
390
|
const elapsedS = Math.round((Date.now() - t0) / 1000);
|
|
295
|
-
dbgFail("project distill timeout/error", { elapsed: Date.now() - t0,
|
|
296
|
-
|
|
297
|
-
if (gotFirstChunk) {
|
|
298
|
-
msg = `蒸馏中途超时:模型「${model}」已在 ${tFirstChunk}ms 返回首个字符、累计 ${deltaChars} 字符,但在 ${elapsedS}s 内未结束(疑似输出中断或已被服务端截断)。`;
|
|
299
|
-
} else {
|
|
300
|
-
msg = `蒸馏超时:模型「${model}」在 ${elapsedS}s 内未返回任何内容,疑似未响应或限流。`;
|
|
301
|
-
}
|
|
391
|
+
dbgFail("project distill timeout/error", { elapsed: Date.now() - t0, message: e?.message ?? String(e), stack: (e?.stack || "").split("\n")[0] });
|
|
392
|
+
const msg = `蒸馏失败:模型「${model || "?"}」在 ${elapsedS}s 内未成功完成(写入/覆盖阶段异常:${e?.message ?? String(e)})。`;
|
|
302
393
|
return { ok: false, message: msg };
|
|
303
394
|
} finally {
|
|
304
395
|
distillLocks.delete(memFile);
|
package/lib/index.js
CHANGED
|
@@ -53,7 +53,14 @@ export const Config = Schema.object({
|
|
|
53
53
|
memoryMode: Schema.union(["plugin", "smart"]).default("plugin").description("记忆模式:plugin=记忆公民指令+轮次轻量+错误捕获;smart=LLM 智能会话摘要(summary→每日日志 + durable→MEMORY.md,带 [smart] 标记)。切换需重启 dsh 生效。"),
|
|
54
54
|
summaryModel: Schema.string().default("").description("智能模式的摘要模型,留空=复用当前会话 provider/model;也可填 provider/model(如 deepseek/deepseek-chat)固定廉价模型省 token。"),
|
|
55
55
|
summaryTimeoutMs: Schema.number().default(60000).description("蒸馏 LLM 调用的超时(毫秒),超时视为失败并降级;覆盖智能模式摘要与手动蒸馏两条链路;默认 60000(60s)。"),
|
|
56
|
-
distillDebugLog: Schema.boolean().default(false).description("调试开关:向 dsh 服务端 stderr 输出蒸馏 LLM
|
|
56
|
+
distillDebugLog: Schema.boolean().default(false).description("调试开关:向 dsh 服务端 stderr 输出蒸馏 LLM 调用诊断。distillLogLevel=info 时仅输出元数据(模型解析/请求参数/流进度/错误详情,不含文本);distillLogLevel=debug 会额外打印 LLM 原始响应文本(分隔符包裹),仅限受信本地排障开启。"),
|
|
57
|
+
// ---- v1.4.1:蒸馏 stderr 日志级别(平铺键,不进 UI;默认 info;distillDebugLog=true 时生效) ----
|
|
58
|
+
distillLogLevel: Schema.string().default("info").description("蒸馏 stderr 日志级别:info=仅元数据诊断(默认,不打印 LLM 原始响应);debug=额外打印 LLM 原始响应文本(分隔符包裹),仅限受信本地排障开启。无需 UI 配置,经 profile/settings.yaml 设置 distillLogLevel 键。"),
|
|
59
|
+
// ---- v1.4.0:智能模式最终输出软预算(v1.4.1 起语义变更:prompt 软约束,思考不受限;实际硬上限由模型自身 maxTokens 决定) ----
|
|
60
|
+
summaryMaxTokens: Schema.number().default(2000).description("智能模式会话摘要 LLM 的【最终输出软预算(prompt 约束,思考不受限)】。默认 2000。实际硬上限由模型自身 maxTokens 决定;可调大以容纳更多 durable 事实。"),
|
|
61
|
+
projectMaxTokens: Schema.number().default(8000).description("手动「蒸馏项目记忆」LLM 的【最终输出软预算(prompt 约束,思考不受限)】。默认 8000。实际硬上限由模型自身 maxTokens 决定。"),
|
|
62
|
+
// ---- v1.4.0:蒸馏时回喂存量记忆(特性3,默认关;开启后自动智能模式 turn/end 与手动蒸馏按钮均回喂,delete 仅手动放开) ----
|
|
63
|
+
feedbackEnabled: Schema.boolean().default(false).description("蒸馏时把项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)回喂给 LLM,使其能基于既有记忆做 add/replace/delete 增量维护。默认关;开启后自动智能模式(turn/end)与手动蒸馏会话按钮均回喂存量记忆;delete 仍仅手动按钮开放,自动模式跳过(防误删)。"),
|
|
57
64
|
});
|
|
58
65
|
|
|
59
66
|
export const inject = ["systemPrompt", "tools", "llm", "webServer", "webRuntime"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-palace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "把 WorkBuddy 的文件式记忆系统移植进 DeepSeek Harness:用户级 + 工作区的人类可读 Markdown 记忆(~/.deepseek-harness/MEMORY.md 与 <cwd>/.deepseek-harness/memory/)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|