dsh-memory-palace 1.2.2 → 1.3.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 +11 -10
- package/cordis.patch.yml +3 -11
- package/lib/client.js +46 -6
- package/lib/distill.mjs +85 -28
- package/lib/index.js +16 -2
- package/lib/tools.mjs +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
# dsh-memory-palace
|
|
1
|
+
# dsh-memory-palace <img src="assets/memory-icon.svg" width="36" height="36" alt="dsh-memory-palace" />
|
|
2
|
+
---
|
|
2
3
|
|
|
3
4
|
把 WorkBuddy 的文件式记忆系统移植进 [DeepSeek Harness](https://www.deepseek.com/harness/) —— 为 Harness 提供**跨会话持久化、人类可直接编辑的 Markdown 记忆**。
|
|
4
5
|
|
|
5
|
-
[](./LICENSE)
|
|
6
|
-
|
|
7
|
-
记忆就是普通文本文件,用记事本就能改。AI 每轮对话把它读进上下文,对话结束后把新内容追加进日志——不依赖任何私有格式、不落 JSON、不锁数据。
|
|
6
|
+
[](./LICENSE) [](https://awesome-dsh-plugin.com)
|
|
8
7
|
|
|
9
8
|
## 特性
|
|
10
9
|
|
|
@@ -80,7 +79,7 @@ turn/end ──► 轻量兜底闸门
|
|
|
80
79
|
|
|
81
80
|
```bash
|
|
82
81
|
dsh plugin --profile web add github:lovezi0/dsh-memory-palace
|
|
83
|
-
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.
|
|
82
|
+
# 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.3.0
|
|
84
83
|
```
|
|
85
84
|
|
|
86
85
|
方式二:clone 后本地安装(开发 / 修改源码场景)
|
|
@@ -99,7 +98,7 @@ dsh plugin --profile web add . # 装入 web profile(profile 名按你的实
|
|
|
99
98
|
# 直接由 dsh 从 npm 拉取并装入(本机若已配镜像会自动走镜像)
|
|
100
99
|
dsh plugin --profile web add dsh-memory-palace
|
|
101
100
|
# 锁定版本:
|
|
102
|
-
dsh plugin --profile web add dsh-memory-palace@1.
|
|
101
|
+
dsh plugin --profile web add dsh-memory-palace@1.3.0
|
|
103
102
|
|
|
104
103
|
# 或先手动用 npm 安装(显式指定镜像),再装入:
|
|
105
104
|
npm install dsh-memory-palace --registry=https://registry.npmmirror.com/
|
|
@@ -156,6 +155,12 @@ dsh plugin --profile web remove dsh-memory-palace
|
|
|
156
155
|
|
|
157
156
|
## 版本历史
|
|
158
157
|
|
|
158
|
+
- **1.3.0**
|
|
159
|
+
- 🔥新增计划模式禁止写入记忆
|
|
160
|
+
- 💪设置-记忆控件样式优化 *使用dsh原生样式*
|
|
161
|
+
- 💪清理重复patch信息
|
|
162
|
+
- 🔥增加调试模式 *默认关闭,仅用于智能模式蒸馏失败输出调试信息*
|
|
163
|
+
- **1.2.3**
|
|
159
164
|
- **1.2.2**
|
|
160
165
|
- 🐛修复智能模式自定义摘要模型不生效的问题
|
|
161
166
|
- **1.2.1**
|
|
@@ -179,10 +184,6 @@ dsh plugin --profile web remove dsh-memory-palace
|
|
|
179
184
|
- 🔥新增删除记忆工具
|
|
180
185
|
- **outdated(0.x)** — 双层 Markdown 记忆读写 / 设置页集成等 0.x 历史,见 [CHANGELOG.md](./CHANGELOG.md)
|
|
181
186
|
|
|
182
|
-
## 废弃方案
|
|
183
|
-
|
|
184
|
-
插件侧 LLM 自动摘要(`ctx.llm.stream()` 路线)曾在 v0.7.1 因 4 个契约/时机坑废弃;**v1.1.0 已重新论证并以「智能模式」可选形态复活**——经 `session.events` / `deriveEventMessage` / `requestHeader()?.config` 根除旧坑,默认仍为插件模式。见 [废弃方案:为什么不用 LLM 自动摘要](./ABANDONED-LLM-SUMMARY.md)。
|
|
185
|
-
|
|
186
187
|
## 参考与致谢
|
|
187
188
|
|
|
188
189
|
本插件开发深度参考了以下两个开源项目(本包实现为各自机制的简化落地,不含其完整功能):
|
package/cordis.patch.yml
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
# dsh-memory-palace bundle patch: 一个 insert 注册本插件。
|
|
2
2
|
# insert.name 必须是【包名】(pnpm add 后由 profile 的 node_modules 解析到 lib/index.js);
|
|
3
3
|
# insert.id 必须与入口导出的 cordis `name`('memory-palace')一致。
|
|
4
|
+
# 注:包不再自带 config 块——值与原 Config schema 默认一致,cordis 按 schema 填充默认,
|
|
5
|
+
# 不传 config 不会 undefined。如需在 profile 层注入覆盖配置,走自己 profile 的
|
|
6
|
+
# cordis.patch.yml(`- id: memory-palace` + `config:`,id-targeted,非 insert)。
|
|
4
7
|
- insert:
|
|
5
8
|
- id: memory-palace
|
|
6
9
|
name: 'dsh-memory-palace'
|
|
7
|
-
config:
|
|
8
|
-
enabled: true
|
|
9
|
-
userMemoryPath: '~/.deepseek-harness/MEMORY.md'
|
|
10
|
-
workspaceMemoryDir: '.deepseek-harness/memory'
|
|
11
|
-
dailyLogRetentionDays: 30
|
|
12
|
-
userBudgetChars: 4000
|
|
13
|
-
workspaceBudgetChars: 3000
|
|
14
|
-
bridgeBuddyMemory: true
|
|
15
|
-
buddyWorkspaceMemoryDirs:
|
|
16
|
-
- '.workbuddy/memory'
|
|
17
|
-
- '.codebuddy/memory'
|
package/lib/client.js
CHANGED
|
@@ -62,7 +62,12 @@ window.__ModuleLoader__.load({
|
|
|
62
62
|
distilling: "蒸馏中…",
|
|
63
63
|
notifySessionDone: "会话记忆蒸馏完成",
|
|
64
64
|
notifyProjectDone: "项目记忆蒸馏完成",
|
|
65
|
-
notifyFail: "记忆蒸馏失败"
|
|
65
|
+
notifyFail: "记忆蒸馏失败",
|
|
66
|
+
distillDebugLogLabel: "蒸馏调试日志",
|
|
67
|
+
distillDebugLogHint: "开启后向 dsh 服务端控制台(stderr)输出蒸馏 LLM 调用诊断,仅排障用,平时关闭。",
|
|
68
|
+
dev: "开发",
|
|
69
|
+
distillTimeoutLabel: "蒸馏超时(秒)",
|
|
70
|
+
distillTimeoutHint: "单次蒸馏 LLM 调用的超时上限(秒);超时视为失败并降级。覆盖智能模式摘要与手动蒸馏,默认 60 秒。"
|
|
66
71
|
};
|
|
67
72
|
|
|
68
73
|
const en = {
|
|
@@ -117,12 +122,17 @@ window.__ModuleLoader__.load({
|
|
|
117
122
|
distilling: "Distilling…",
|
|
118
123
|
notifySessionDone: "Session memory distilled",
|
|
119
124
|
notifyProjectDone: "Project memory distilled",
|
|
120
|
-
notifyFail: "Memory distillation failed"
|
|
125
|
+
notifyFail: "Memory distillation failed",
|
|
126
|
+
distillDebugLogLabel: "Distill debug log",
|
|
127
|
+
distillDebugLogHint: "When on, prints distill LLM call diagnostics to the dsh server console (stderr). For troubleshooting only; keep off normally.",
|
|
128
|
+
dev: "Developer",
|
|
129
|
+
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."
|
|
121
131
|
};
|
|
122
132
|
|
|
123
133
|
|
|
124
|
-
const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars"]);
|
|
125
|
-
const FIELD_BOOL = new Set(["bridgeBuddyMemory", "summarize", "autoCaptureErrors"]);
|
|
134
|
+
const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars", "summaryTimeoutMs"]);
|
|
135
|
+
const FIELD_BOOL = new Set(["bridgeBuddyMemory", "summarize", "autoCaptureErrors", "distillDebugLog"]);
|
|
126
136
|
|
|
127
137
|
function projectDraft(snap) {
|
|
128
138
|
const v = snap && snap.value ? snap.value : {};
|
|
@@ -136,6 +146,8 @@ window.__ModuleLoader__.load({
|
|
|
136
146
|
workspaceBudgetChars: v.workspaceBudgetChars != null ? String(v.workspaceBudgetChars) : "3000",
|
|
137
147
|
summarize: v.summarize === false ? "false" : "true",
|
|
138
148
|
autoCaptureErrors: v.autoCaptureErrors === false ? "false" : "true",
|
|
149
|
+
distillDebugLog: v.distillDebugLog === false ? "false" : "true",
|
|
150
|
+
summaryTimeoutMs: v.summaryTimeoutMs != null ? String(Math.round((Number(v.summaryTimeoutMs) || 0) / 1000)) : "60",
|
|
139
151
|
memoryMode: v.memoryMode || "plugin",
|
|
140
152
|
summaryModel: v.summaryModel || ""
|
|
141
153
|
};
|
|
@@ -266,7 +278,10 @@ window.__ModuleLoader__.load({
|
|
|
266
278
|
if (FIELD_BOOL.has(field)) {
|
|
267
279
|
section[field] = text === "true";
|
|
268
280
|
} else if (FIELD_NUMERIC.has(field)) {
|
|
269
|
-
if (text !== "")
|
|
281
|
+
if (text !== "") {
|
|
282
|
+
const n = Number(text);
|
|
283
|
+
section[field] = field === "summaryTimeoutMs" ? n * 1000 : n;
|
|
284
|
+
}
|
|
270
285
|
} else if (field === "buddyWorkspaceMemoryDirs") {
|
|
271
286
|
section[field] = text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
272
287
|
} else {
|
|
@@ -306,6 +321,7 @@ window.__ModuleLoader__.load({
|
|
|
306
321
|
type: "checkbox",
|
|
307
322
|
checked: draft[field] === "true",
|
|
308
323
|
disabled: disabled || disabledOverride,
|
|
324
|
+
style: checkboxStyle,
|
|
309
325
|
onChange: (e) => edit(field, e.target.checked ? "true" : "false")
|
|
310
326
|
})
|
|
311
327
|
]);
|
|
@@ -333,6 +349,23 @@ window.__ModuleLoader__.load({
|
|
|
333
349
|
fontSize: "13px",
|
|
334
350
|
lineHeight: "1.5"
|
|
335
351
|
};
|
|
352
|
+
const selectStyle = {
|
|
353
|
+
...inputStyle,
|
|
354
|
+
height: "34px",
|
|
355
|
+
appearance: "none",
|
|
356
|
+
WebkitAppearance: "none",
|
|
357
|
+
backgroundImage: "url(data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E)",
|
|
358
|
+
backgroundPosition: "right 12px center",
|
|
359
|
+
backgroundRepeat: "no-repeat",
|
|
360
|
+
backgroundSize: "12px 12px",
|
|
361
|
+
paddingRight: "32px"
|
|
362
|
+
};
|
|
363
|
+
const checkboxStyle = {
|
|
364
|
+
width: "15px",
|
|
365
|
+
height: "15px",
|
|
366
|
+
margin: "0",
|
|
367
|
+
accentColor: "var(--dsw-alias-brand-primary, #4b5bff)"
|
|
368
|
+
};
|
|
336
369
|
const toggleStyle = {
|
|
337
370
|
display: "flex",
|
|
338
371
|
alignItems: "center",
|
|
@@ -454,6 +487,7 @@ window.__ModuleLoader__.load({
|
|
|
454
487
|
type: "checkbox",
|
|
455
488
|
checked: draft.bridgeBuddyMemory === "true",
|
|
456
489
|
disabled,
|
|
490
|
+
style: checkboxStyle,
|
|
457
491
|
onChange: (e) => edit("bridgeBuddyMemory", e.target.checked ? "true" : "false")
|
|
458
492
|
})
|
|
459
493
|
]),
|
|
@@ -471,7 +505,7 @@ window.__ModuleLoader__.load({
|
|
|
471
505
|
value: draft.memoryMode === "smart" ? "smart" : "plugin",
|
|
472
506
|
disabled,
|
|
473
507
|
onChange: (e) => edit("memoryMode", e.target.value),
|
|
474
|
-
style:
|
|
508
|
+
style: selectStyle
|
|
475
509
|
}, [
|
|
476
510
|
h("option", { value: "plugin" }, "插件模式"),
|
|
477
511
|
h("option", { value: "smart" }, "智能模式")
|
|
@@ -483,6 +517,7 @@ window.__ModuleLoader__.load({
|
|
|
483
517
|
// 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
|
|
484
518
|
value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
|
|
485
519
|
disabled,
|
|
520
|
+
style: selectStyle,
|
|
486
521
|
onChange: (e) => edit("summaryModel", e.target.value)
|
|
487
522
|
}, [
|
|
488
523
|
h("option", { value: "" }, "复用当前会话模型"),
|
|
@@ -500,6 +535,11 @@ window.__ModuleLoader__.load({
|
|
|
500
535
|
row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
|
|
501
536
|
row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
|
|
502
537
|
row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
|
|
538
|
+
]),
|
|
539
|
+
h("div", { style: cardStyle }, [
|
|
540
|
+
h("p", { style: groupTitleStyle }, t("dev")),
|
|
541
|
+
row(t("distillTimeoutLabel"), t("distillTimeoutHint"), h("input", num("summaryTimeoutMs")), "summaryTimeoutMs"),
|
|
542
|
+
toggle(t("distillDebugLogLabel"), t("distillDebugLogHint"), "distillDebugLog")
|
|
503
543
|
])
|
|
504
544
|
]);
|
|
505
545
|
}
|
package/lib/distill.mjs
CHANGED
|
@@ -13,22 +13,46 @@ import { appendLineDedup } from "./common/records.mjs";
|
|
|
13
13
|
export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
14
14
|
const cfg = () => getConfig();
|
|
15
15
|
|
|
16
|
+
// 调试日志(v1.3.0):dbgFail=失败/跳过留痕(无条件输出,不受开关控制,保证排障可观测);
|
|
17
|
+
// dbg=受 distillDebugLog 开关控制的详单(模型解析/请求参数/流进度计数/错误详情)。
|
|
18
|
+
// 隐私红线:只打计数/字符数/元数据/错误 message+stack 首行,绝不打印对话或 MEMORY.md 文本。
|
|
19
|
+
const dbgFail = (why, extra) => console.error(`[memory-palace] distill skip: ${why}`, extra ? JSON.stringify(extra) : "");
|
|
20
|
+
const dbg = (why, extra) => { if (!cfg().distillDebugLog) return; console.error(`[memory-palace][debug] ${why}`, extra !== undefined ? JSON.stringify(extra) : ""); };
|
|
21
|
+
|
|
16
22
|
// 模型解析(v1.2.3 修复):summaryModel 存的是注册表 models[].id 原样(client 下拉 value = m.id),
|
|
17
23
|
// pi-ai getModel(provider, id) 按 model.id 全等匹配(pi-ai models.js: getModels(provider).find(m => m.id === id)),
|
|
18
24
|
// 所以 model 参数必须【原样直传】summaryModel——绝不能 split 拆段。v1.2.2 之前拆段把
|
|
19
25
|
// `nvidia/nemotron-3-ultra-550b-a55b` 变成裸 id `nemotron-3-ultra-550b-a55b`,全等匹配失败抛 UNKNOWN_MODEL。
|
|
20
|
-
// provider
|
|
26
|
+
// provider 解析优先级:
|
|
27
|
+
// 1) 注册表反查(最稳):遍历 ctx.llm.listProviders(),对每个 provider 调 listModels,
|
|
28
|
+
// 找 m.id === summaryModel 全等命中 → 返回 { provider: m.provider, model: m.id }。
|
|
29
|
+
// 带前缀 id(nvidia/nemotron-3-ultra-550b-a55b)与裸 id(mimo-v2.5 属 xiaomi)都能一次命中。
|
|
30
|
+
// 2) 首段兜底:listProviders 不可用(测试 mock 等)时,取 summaryModel 首段当 provider(openai/gpt-4o 格式)。
|
|
21
31
|
// summaryModel 为空 → 会话 requestHeader config 兜底(复用当前会话 provider/model,原样直传)。
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
async function findModelInRegistry(modelId) {
|
|
33
|
+
try {
|
|
34
|
+
const providers = typeof ctx.llm?.listProviders === "function" ? ctx.llm.listProviders() : [];
|
|
35
|
+
for (const p of providers || []) {
|
|
36
|
+
try {
|
|
37
|
+
const models = await ctx.llm.listModels(p.id);
|
|
38
|
+
const m = (models || []).find((x) => x.id === modelId);
|
|
39
|
+
if (m) return { provider: m.provider, model: m.id };
|
|
40
|
+
} catch { /* 该 provider 无模型列表或不可用,跳过 */ }
|
|
41
|
+
}
|
|
42
|
+
} catch { /* listProviders 不可用,跳过反查 */ }
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
async function resolveModel(summaryModel, session) {
|
|
25
46
|
const sm = (summaryModel || "").trim();
|
|
26
47
|
if (sm) {
|
|
48
|
+
const hit = await findModelInRegistry(sm);
|
|
49
|
+
if (hit) { dbg("resolveModel: registry hit", { provider: hit.provider, model: hit.model }); return hit; }
|
|
27
50
|
const provider = sm.split("/")[0].trim();
|
|
28
|
-
if (provider) return { provider, model: sm };
|
|
51
|
+
if (provider && sm.includes("/")) { dbg("resolveModel: prefix fallback", { provider, model: sm }); return { provider, model: sm }; }
|
|
29
52
|
}
|
|
30
53
|
const conf = session?.requestHeader?.()?.config;
|
|
31
|
-
if (conf && conf.provider && conf.model) return { provider: conf.provider, model: conf.model };
|
|
54
|
+
if (conf && conf.provider && conf.model) { dbg("resolveModel: session fallback", { provider: conf.provider, model: conf.model }); return { provider: conf.provider, model: conf.model }; }
|
|
55
|
+
dbgFail("resolveModel: no model resolved", { summaryModel: sm, hasHeader: !!(conf && conf.provider) });
|
|
32
56
|
return null;
|
|
33
57
|
}
|
|
34
58
|
|
|
@@ -39,16 +63,15 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
39
63
|
// 自动智能模式(summarizeTurn,增量断点)与按钮「蒸馏会话」(全量 fromSeq=0)共用本核心。
|
|
40
64
|
// 返回 { ok, summary, durableCount };失败 { ok:false }(调用方各自决定降级策略)。
|
|
41
65
|
async function distillSessionCore(session, dirs, fromSeq) {
|
|
42
|
-
const dbg = (why, extra) => console.error(`[memory-palace] distill skip: ${why}`, extra ?? "");
|
|
43
66
|
if (!session) {
|
|
44
|
-
|
|
67
|
+
dbgFail("no session");
|
|
45
68
|
return { ok: false };
|
|
46
69
|
}
|
|
47
70
|
const c = cfg();
|
|
48
|
-
// 1. 模型解析:summaryModel
|
|
49
|
-
const resolved = resolveModel(c.summaryModel, session);
|
|
71
|
+
// 1. 模型解析:summaryModel 注册表反查(带前缀/裸 id 均可)> 复用当前会话模型;皆缺 → 降级。
|
|
72
|
+
const resolved = await resolveModel(c.summaryModel, session);
|
|
50
73
|
if (!resolved) {
|
|
51
|
-
|
|
74
|
+
dbgFail("no model", { summaryModel: c.summaryModel, header: JSON.stringify(session.requestHeader?.()?.config) });
|
|
52
75
|
return { ok: false };
|
|
53
76
|
}
|
|
54
77
|
const { provider, model } = resolved;
|
|
@@ -57,13 +80,14 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
57
80
|
const newEvents = (session.events || []).filter(
|
|
58
81
|
(e) => e.seq >= fromSeq && SURFACE.has(e.type),
|
|
59
82
|
);
|
|
83
|
+
dbg("session core entry", { sessionId: session.id, fromSeq, eventsLen: newEvents.length });
|
|
60
84
|
const hist = [];
|
|
61
85
|
for (const e of newEvents) {
|
|
62
86
|
const m = session.deriveEventMessage ? session.deriveEventMessage(e) : null;
|
|
63
87
|
if (m) hist.push(m);
|
|
64
88
|
}
|
|
65
89
|
if (!hist.length) {
|
|
66
|
-
|
|
90
|
+
dbgFail("no events in range", { fromSeq, eventsLen: (session.events || []).length, newEvents: newEvents.length, hasDerive: !!session.deriveEventMessage });
|
|
67
91
|
return { ok: false };
|
|
68
92
|
}
|
|
69
93
|
// 3. LLM 调用:借鉴 dsh-sideband 的 summarizeJob(src/summarizer.ts)——
|
|
@@ -74,6 +98,9 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
74
98
|
// (JSON.parse 失败自然回落全文/降级,不会产生坏数据)。
|
|
75
99
|
const timeoutMs = c.summaryTimeoutMs || 60000;
|
|
76
100
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
101
|
+
const t0 = Date.now();
|
|
102
|
+
let gotFirstChunk = false, tFirstChunk = null, chunks = 0, deltaChars = 0;
|
|
103
|
+
dbg("session request", { provider, model, messages: hist.length, maxTokens: 800, timeoutMs });
|
|
77
104
|
const res = ctx.llm.stream({
|
|
78
105
|
provider,
|
|
79
106
|
model,
|
|
@@ -82,23 +109,28 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
82
109
|
maxTokens: 800,
|
|
83
110
|
signal,
|
|
84
111
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
121
|
} catch (e) {
|
|
92
|
-
|
|
122
|
+
dbgFail("stream aborted/timeout/error", { message: e?.message ?? "", aborted: signal.aborted, reason: signal.reason?.message ?? "" });
|
|
93
123
|
return { ok: false };
|
|
94
124
|
}
|
|
95
125
|
const finish = asm.finish;
|
|
126
|
+
dbg("session finish", { kind: finish?.kind });
|
|
127
|
+
dbg("session stream done", { chunks, deltaChars, firstChunkMs: tFirstChunk });
|
|
96
128
|
if (finish && (finish.kind === "error" || finish.kind === "aborted")) {
|
|
97
|
-
|
|
129
|
+
dbgFail("llm finish", { kind: finish.kind, message: finish.failure?.message ?? "" });
|
|
98
130
|
return { ok: false };
|
|
99
131
|
}
|
|
100
132
|
if (finish && finish.kind === "max-tokens") {
|
|
101
|
-
|
|
133
|
+
console.error("[memory-palace] distill warn: llm finish max-tokens (attempting partial JSON)", JSON.stringify({}));
|
|
102
134
|
}
|
|
103
135
|
const text = asm
|
|
104
136
|
.blocks()
|
|
@@ -107,15 +139,17 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
107
139
|
.join("")
|
|
108
140
|
.trim();
|
|
109
141
|
if (!text) {
|
|
110
|
-
|
|
142
|
+
dbgFail("empty llm text");
|
|
111
143
|
return { ok: false };
|
|
112
144
|
}
|
|
113
145
|
// 4. 解析 JSON:{summary, durable:[{scope,fact}]};解析失败回退全文当 summary。
|
|
114
146
|
let summary = text;
|
|
115
147
|
let durable = [];
|
|
148
|
+
let jsonOk = false;
|
|
116
149
|
try {
|
|
117
150
|
const parsed = JSON.parse(text);
|
|
118
151
|
if (parsed && typeof parsed === "object") {
|
|
152
|
+
jsonOk = true;
|
|
119
153
|
if (typeof parsed.summary === "string") summary = parsed.summary;
|
|
120
154
|
if (Array.isArray(parsed.durable)) durable = parsed.durable;
|
|
121
155
|
}
|
|
@@ -143,6 +177,8 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
143
177
|
}
|
|
144
178
|
}
|
|
145
179
|
}
|
|
180
|
+
dbg("session parsed", { jsonOk, summaryChars: summary.length, durableCount: durable.length });
|
|
181
|
+
dbg("session write done", { dirs: dirs.length, durableWritten: durable.length });
|
|
146
182
|
return { ok: true, summary, durableCount: durable.length };
|
|
147
183
|
}
|
|
148
184
|
|
|
@@ -170,19 +206,25 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
170
206
|
const memFile = paths.memoryFileOf(primary, cwd);
|
|
171
207
|
if (!memFile) return { ok: false, message: "无法解析项目 MEMORY.md 路径。" };
|
|
172
208
|
const memShort = toHomeShort(memFile);
|
|
209
|
+
dbg("project entry", { cwd, memFile: toHomeShort(memFile) });
|
|
173
210
|
|
|
174
211
|
const raw = readMdSync(memFile);
|
|
175
212
|
if (!raw) return { ok: false, message: `项目记忆为空(${memShort}),无需蒸馏。` };
|
|
213
|
+
dbg("project loaded", { memFile: toHomeShort(memFile), rawChars: raw.length });
|
|
176
214
|
|
|
177
215
|
if (distillLocks.has(memFile)) return { ok: false, message: "蒸馏正在进行中,请稍候再试。" };
|
|
178
216
|
distillLocks.add(memFile);
|
|
179
217
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
const t0 = Date.now();
|
|
219
|
+
let gotFirstChunk = false, tFirstChunk = null, chunks = 0, deltaChars = 0;
|
|
220
|
+
let provider = null, model = null;
|
|
221
|
+
// 模型解析:与 distillSessionCore 同序(summaryModel 注册表反查 > 会话 requestHeader)。
|
|
222
|
+
const resolved = await resolveModel(c.summaryModel, session);
|
|
182
223
|
if (!resolved) {
|
|
183
224
|
return { ok: false, message: "无法确定蒸馏模型(summaryModel 未配置且会话无模型信息)。" };
|
|
184
225
|
}
|
|
185
|
-
|
|
226
|
+
({ provider, model } = resolved);
|
|
227
|
+
dbg("project request", { provider, model, inputChars: raw.length, maxTokens: 4000, timeoutMs: c.summaryTimeoutMs || 60000 });
|
|
186
228
|
|
|
187
229
|
// LLM 蒸馏:system = 固化 DISTILL_PROMPT,user = MEMORY.md 全文。
|
|
188
230
|
const timeoutMs = c.summaryTimeoutMs || 60000;
|
|
@@ -203,19 +245,25 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
203
245
|
const asm = new BlockAssembler();
|
|
204
246
|
for await (const ch of res) {
|
|
205
247
|
signal.throwIfAborted();
|
|
248
|
+
chunks++;
|
|
249
|
+
deltaChars += (ch.text?.length || 0);
|
|
250
|
+
if (!gotFirstChunk) { gotFirstChunk = true; tFirstChunk = Date.now() - t0; dbg("project first chunk", { ms: tFirstChunk }); }
|
|
206
251
|
asm.push(ch);
|
|
207
252
|
}
|
|
208
253
|
const finish = asm.finish;
|
|
209
254
|
if (finish && (finish.kind === "error" || finish.kind === "aborted")) {
|
|
210
|
-
|
|
255
|
+
const kindZh = finish.kind === "aborted" ? "被中断" : "出错";
|
|
256
|
+
dbgFail("project finish failed", { kind: finish.kind, message: finish.failure?.message ?? "" });
|
|
257
|
+
return { ok: false, message: `蒸馏${kindZh}(LLM ${finish.kind}):${finish.failure?.message ?? ""}` };
|
|
211
258
|
}
|
|
259
|
+
dbg("project finish", { kind: finish?.kind });
|
|
212
260
|
const text = asm
|
|
213
261
|
.blocks()
|
|
214
262
|
.filter((b) => b.type === "text")
|
|
215
263
|
.map((b) => b.text ?? "")
|
|
216
264
|
.join("")
|
|
217
265
|
.trim();
|
|
218
|
-
if (!text) return { ok: false, message: "蒸馏结果为空,未覆盖任何文件。" };
|
|
266
|
+
if (!text) { dbgFail("project empty result", { chunks, deltaChars }); return { ok: false, message: "蒸馏结果为空,未覆盖任何文件。" }; }
|
|
219
267
|
|
|
220
268
|
// 写 cover(与 MEMORY.md 同目录,同文件系统保证 rename 原子)。
|
|
221
269
|
const coverFile = join(dirname(memFile), "memory-cover.md");
|
|
@@ -237,12 +285,21 @@ export function createDistill({ ctx, getConfig, paths, records, state }) {
|
|
|
237
285
|
await rename(backup, memFile).catch(() => {});
|
|
238
286
|
throw e;
|
|
239
287
|
}
|
|
288
|
+
dbg("project write done", { coverChars: text.length, backup: toHomeShort(backup) });
|
|
240
289
|
return {
|
|
241
290
|
ok: true,
|
|
242
291
|
message: `蒸馏成功:${memShort} 已更新(原 ${raw.length} 字符 → ${text.length} 字符),备份 ${toHomeShort(backup)}。`,
|
|
243
292
|
};
|
|
244
293
|
} catch (e) {
|
|
245
|
-
|
|
294
|
+
const elapsedS = Math.round((Date.now() - t0) / 1000);
|
|
295
|
+
dbgFail("project distill timeout/error", { elapsed: Date.now() - t0, chunks, deltaChars, gotFirstChunk, message: e?.message ?? String(e), stack: (e?.stack || "").split("\n")[0] });
|
|
296
|
+
let msg;
|
|
297
|
+
if (gotFirstChunk) {
|
|
298
|
+
msg = `蒸馏中途超时:模型「${model}」已在 ${tFirstChunk}ms 返回首个字符、累计 ${deltaChars} 字符,但在 ${elapsedS}s 内未结束(疑似输出中断或已被服务端截断)。`;
|
|
299
|
+
} else {
|
|
300
|
+
msg = `蒸馏超时:模型「${model}」在 ${elapsedS}s 内未返回任何内容,疑似未响应或限流。`;
|
|
301
|
+
}
|
|
302
|
+
return { ok: false, message: msg };
|
|
246
303
|
} finally {
|
|
247
304
|
distillLocks.delete(memFile);
|
|
248
305
|
}
|
package/lib/index.js
CHANGED
|
@@ -52,7 +52,8 @@ export const Config = Schema.object({
|
|
|
52
52
|
// ---- v1.1.0:双记忆模式(插件模式=现状;智能模式=LLM 智能会话摘要),设置页切换,互斥,切换需重启 dsh ----
|
|
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
|
-
summaryTimeoutMs: Schema.number().default(60000).description("
|
|
55
|
+
summaryTimeoutMs: Schema.number().default(60000).description("蒸馏 LLM 调用的超时(毫秒),超时视为失败并降级;覆盖智能模式摘要与手动蒸馏两条链路;默认 60000(60s)。"),
|
|
56
|
+
distillDebugLog: Schema.boolean().default(false).description("调试开关:向 dsh 服务端 stderr 输出蒸馏 LLM 调用诊断日志(模型解析、请求参数、流进度计数、错误详情)。仅排障用,平时关闭;开启后也不打印任何对话内容或记忆文本。"),
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
export const inject = ["systemPrompt", "tools", "llm", "webServer", "webRuntime"];
|
|
@@ -103,6 +104,7 @@ export function apply(ctx, config) {
|
|
|
103
104
|
settleTimer: null, // debounce 计时器
|
|
104
105
|
lastSummarizedSeq: -1, // 智能模式增量摘要断点(session 事件 seq)
|
|
105
106
|
summarySessionId: null,
|
|
107
|
+
planModeActive: false, // 计划模式:禁写记忆(硬编码默认,无开关;仅经 session/event 的 plan/mode 翻转;未装 dsh-plan-mode 永不触发=不拦截)
|
|
106
108
|
};
|
|
107
109
|
const TURN_BUFFER_MAX = 30;
|
|
108
110
|
// v1.1.3:字符上限提升到 30k(长工具型 request 的工具结果动辄数千字符,6k 上限会触发
|
|
@@ -131,6 +133,11 @@ export function apply(ctx, config) {
|
|
|
131
133
|
state.lastSummarizedSeq = session.firstLiveSeq;
|
|
132
134
|
}
|
|
133
135
|
const type = event?.type;
|
|
136
|
+
// 计划模式:禁写记忆(硬编码默认行为,无开关;不依赖 dsh-plan-mode 服务,未装则永不触发=安全降级=不拦截)
|
|
137
|
+
if (type === "plan/mode") {
|
|
138
|
+
state.planModeActive = !!(event?.data?.active);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
134
141
|
if (type === "user/message" || type === "assistant/message" || type === "tool/result") {
|
|
135
142
|
const text = extractText(event);
|
|
136
143
|
if (text) {
|
|
@@ -204,13 +211,20 @@ export function apply(ctx, config) {
|
|
|
204
211
|
: "你拥有持久化、人类可直接编辑的 Markdown 记忆文件(位于 ~/.deepseek-harness/MEMORY.md,以及各项目的 .deepseek-harness/MEMORY.md(长期记忆)与 .deepseek-harness/memory/(每日日志))。" +
|
|
205
212
|
"写入记忆:项目级约定用 memory_note 工具,跨项目个人偏好用 memory_note_user 工具;读取全部记忆用 memory_read 工具(不要手动 glob/read 记忆文件)。用它保持跨 session 一致性;看不到的内容不要编造。" +
|
|
206
213
|
antiMangle + proactive;
|
|
207
|
-
|
|
214
|
+
// 计划模式禁写提示(硬编码默认行为,无开关):无论记忆模式都追加,让 agent 自觉不写(网关物理兜底仍生效)
|
|
215
|
+
const planNote = state.planModeActive
|
|
216
|
+
? "\n\n[plan 模式] 当前处于 plan 模式,不要调用 memory_note / memory_note_user 写入记忆,也不要请求删除记忆(读取记忆用 memory_read,仍可用)。"
|
|
217
|
+
: "";
|
|
218
|
+
const introFull = intro + planNote;
|
|
219
|
+
return blocks.length ? [introFull, ...blocks].join("\n\n") : introFull;
|
|
208
220
|
},
|
|
209
221
|
});
|
|
210
222
|
|
|
211
223
|
// ---------- 结算一个完整 request:基于累积缓冲做「错误捕获 + 轻量兜底记录(闸门)」,不调 LLM ----------
|
|
212
224
|
// 主路径是 agent 按记忆公民指令主动调 memory_note;此处只兜底保证"实质轮次不丢"。
|
|
213
225
|
async function _settle(capturedTurn, isError, errMsg) {
|
|
226
|
+
// 计划模式禁写(硬编码默认,无开关):自动写路径全拦截(智能模式/轻量兜底/错误捕获同源跳过)
|
|
227
|
+
if (state.planModeActive) return;
|
|
214
228
|
const cfg = source();
|
|
215
229
|
if (!cfg.enabled) return;
|
|
216
230
|
const dirs = paths.writeDirs();
|
package/lib/tools.mjs
CHANGED
|
@@ -360,8 +360,15 @@ export function registerTools({ ctx, getConfig, paths, records, state }) {
|
|
|
360
360
|
// (即删除用户记忆时你看到的「沙箱授权弹窗」)——比两阶段的 AI 级 confirm:true 更可靠:
|
|
361
361
|
// 即便模型误传 confirm:true,没有真人点击弹窗允许也绝不会真正删除。
|
|
362
362
|
// 预览(confirm 省略或为 false)直接放行、不弹窗,由 execute 返回候选供 AI 转述、供用户选择删哪几条。
|
|
363
|
+
// 计划模式禁写(硬编码默认,无开关):三个写工具统一 deny(连预览/确认都拦),仅禁写不禁读。
|
|
364
|
+
const WRITE_TOOLS = new Set(["memory_note", "memory_note_user", "memory_delete"]);
|
|
363
365
|
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
364
|
-
|
|
366
|
+
const name = exec?.name;
|
|
367
|
+
// plan 模式禁写:三个写工具一律 deny,连预览/确认都拦
|
|
368
|
+
if (state.planModeActive && WRITE_TOOLS.has(name)) {
|
|
369
|
+
return { kind: "deny", reason: "plan 模式下禁止写入记忆" };
|
|
370
|
+
}
|
|
371
|
+
if (name !== "memory_delete") return next();
|
|
365
372
|
const a = (exec.arguments && typeof exec.arguments === "object") ? exec.arguments : {};
|
|
366
373
|
const confirm = a.confirm === true;
|
|
367
374
|
if (!confirm) return next(); // 预览阶段:只查不删,无需弹窗
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-palace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|