dsh-memory-palace 1.6.2-alpha.4 → 1.6.3

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 CHANGED
@@ -2,25 +2,21 @@
2
2
 
3
3
  把 WorkBuddy 的文件式记忆系统移植进 [DeepSeek Harness](https://www.deepseek.com/harness/) —— 为 Harness 提供**跨会话持久化、人类可直接编辑的 Markdown 记忆**。
4
4
 
5
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
6
-
7
- > mian分支已适配**deepseek harness 0.1.2-alpha.2+**
8
- > 旧版本已切换分支归档,见`dsh-v0.1.x`
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com) [![npm](https://img.shields.io/npm/v/dsh-memory-palace.svg?label=npm&color=CB3837&labelColor=4D6BFE)](https://www.npmjs.com/package/dsh-memory-palace) [![DeepSeek Harness:0.1.2-rc.1](https://img.shields.io/badge/DeepSeek%20Harness-0.1.2--rc.1-success.svg?labelColor=4D6BFE)](https://github.com/deepseek-ai/deepseek-harness)
9
6
 
10
7
  ## 特性
11
8
 
12
9
  - **人类可读的真源**:记忆全部存储在 Markdown 文件中(`MEMORY.md` + 每日日志 `YYYY-MM-DD.md`),任何编辑器可直接修改,数据永远属于你。
13
10
  - **双层记忆**:用户级(跨项目个人偏好,默认 `~/.deepseek-harness/MEMORY.md`)+ 工作区级(项目约定,默认 `<cwd>/.deepseek-harness/memory/`)。
14
- - **自动读写**:每轮对话将记忆注入系统提示词;每轮结束自动把轻量记录追加进当日日志。
15
11
  - **日志迁移**:超过保留天数(默认 30 天)的每日日志自动迁移进 `MEMORY.md` 后删除,长期记忆持续沉淀。
16
12
  - **WorkBuddy / CodeBuddy 桥接**:项目已存在 `.workbuddy/memory` 或 `.codebuddy/memory` 时直接读写这些目录,无需重复维护记忆。
17
13
  - **记忆工具**:`memory_note`(项目级写入)、`memory_note_user`(用户级写入)、`memory_read`(聚合读取)、`memory_delete`(按内容删除,两阶段确认),全部内置去重,防止重复追加。
18
14
  - **会话标题栏「记忆」按钮**:支持手动蒸馏:①蒸馏会话 / ②蒸馏项目记忆。
19
15
  - **设置页集成**:DSH 设置中内置「记忆」面板(中英双语),所有配置均可图形化调整,无需改配置文件。
20
16
  - **主动记忆(主路径,插件模式)**:注入「记忆公民指令」引导 agent 在「修复 bug/根因+绕过」「验证 build/test 通过」「完成里程碑/关键决策」「用户表达偏好/约束」时主动调 `memory_note` / `memory_note_user` 落档 — 对标 WorkBuddy 的"智能记一笔"手感。
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 下日志永不过期(作为证据层保留)。
17
+ - **智能模式(LLM 智能会话摘要)**:智能模式下,每轮命中防闲聊闸门后由 harness 把本会话**新增对话增量**(按 session 事件 seq 断点)提炼成摘要——`summary` 写每日日志 + durable 事实写 MEMORY.md。
18
+ - **记忆注入**:记忆块仅在会话**首次**构建时注入(DSH 会话自身继承历史轮次,逐轮注入冗余且有过时断言干扰);发生上下文压缩(compaction)后自动重注。
19
+ - **混合模式(子Agent+主Agent混合处理 🔥推荐)**:记忆子代理在每轮 turn/end 自动把本轮摘要写入**今日工作日志**(章节化组织、回喂今日日志做增量去重、重复/过时条目标删除线墓碑);MEMORY.md 写入权归还 agent(`memory_write` 章节化追加 / `memory_update_section` 整章节精确替换 / 双门禁内 `memory_reorganize` 全量重整)。
24
20
  - **标准 npm 插件包**:经 `dsh plugin` 一键装入 profile,`cordis.patch.yml` 声明 bundle patch,零手动改动 harness。
25
21
 
26
22
  ## 记忆文件布局
@@ -82,7 +78,7 @@ turn/end ──► 轻量兜底闸门
82
78
 
83
79
  ```bash
84
80
  dsh plugin --profile web add github:lovezi0/dsh-memory-palace
85
- # 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.6.2-alpha.4
81
+ # 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.6.3
86
82
  ```
87
83
 
88
84
  方式二:clone 后本地安装(开发 / 修改源码场景)
@@ -123,6 +119,11 @@ dsh plugin --profile web remove dsh-memory-palace
123
119
 
124
120
  ## 版本历史
125
121
 
122
+ - **1.6.3**
123
+ - npm publish
124
+ - **1.6.3.alpha.4**
125
+ - 🐛修复三种记忆模式配置互串的问题
126
+ - 🐛修复记忆子agent工具幻觉问题
126
127
  - **1.6.2**
127
128
  - **1.6.2.alpha.4**
128
129
  - 🔥适配deepseek harness 0.1.2-alpha.4
package/lib/client.js CHANGED
@@ -28,6 +28,7 @@ window.__ModuleLoader__.load({
28
28
  wsDirHint: "无 buddy 目录时回退到该目录,默认 .deepseek-harness/memory",
29
29
  retentionLabel: "日志保留天数",
30
30
  retentionHint: "超过该天数的每日日志会迁移进 MEMORY.md 后删除",
31
+ retentionHintHybrid: "hybrid 模式下此设置不生效:日志由记忆子代理维护、永不过期不删(证据层),保持完整。",
31
32
  userBudgetLabel: "用户级预算(字符)",
32
33
  userBudgetHint: "用户级 MEMORY.md 的最大字符数",
33
34
  wsBudgetLabel: "工作区预算(字符)",
@@ -44,8 +45,8 @@ window.__ModuleLoader__.load({
44
45
  reorgCooldownHint: "项目级 MEMORY.md 全量重整的冷却天数(距上次重整);与「超出注入预算」双条件同时满足才允许 memory_reorganize。默认 7 天。",
45
46
  subagentLogBudgetLabel: "子代理日志回喂预算(字符)",
46
47
  subagentLogBudgetHint: "记忆子代理回喂今日工作日志的字符上限;超出时仅回喂章节目录,子代理按需读取章节。默认 20000。",
47
- summaryModelLabel: "摘要模型(智能模式)",
48
- summaryModelHint: "留空=复用当前会话的 provider/model;也可固定廉价小模型省 token。",
48
+ summaryModelLabel: "模型",
49
+ summaryModelHint: "记忆插件当前使用的模型(智能模式会话摘要 / 手动蒸馏 / hybrid 记忆子代理共用)。留空=复用当前会话的 provider/model;也可固定廉价小模型省 token。",
49
50
  save: "保存",
50
51
  saving: "保存中…",
51
52
  discard: "放弃",
@@ -74,12 +75,12 @@ window.__ModuleLoader__.load({
74
75
  dev: "开发",
75
76
  distillTimeoutLabel: "蒸馏超时(秒)",
76
77
  distillTimeoutHint: "单次蒸馏 LLM 调用的超时上限(秒);超时视为失败并降级。覆盖智能模式摘要与手动蒸馏,默认 60 秒。",
77
- summaryMaxTokensLabel: "会话摘要最大 Token(智能模式)",
78
- summaryMaxTokensHint: "智能模式会话摘要 LLM 的【最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)】。默认 2000,可调大以容纳更多 durable 事实。",
79
- projectMaxTokensLabel: "项目蒸馏输出软预算",
80
- projectMaxTokensHint: "手动「蒸馏项目记忆」LLM 的【最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)】。默认 8000。",
81
- feedbackEnabledLabel: "蒸馏回喂存量记忆",
82
- feedbackEnabledHint: "开启后,自动智能模式(turn/end)与手动蒸馏会话按钮均会把项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)回喂 LLM,使其能基于既有记忆做 add/replace/delete 增量维护。delete 仅手动蒸馏按钮开放,自动模式跳过(防误删)。默认关。"
78
+ summaryMaxTokensLabel: "会话摘要最大 Token",
79
+ summaryMaxTokensHint: "最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)。默认 2000,可调大以容纳更多 durable 事实。",
80
+ projectMaxTokensLabel: "项目蒸馏最大 Token",
81
+ projectMaxTokensHint: "最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)。默认 8000。",
82
+ feedbackEnabledLabel: "回喂存量记忆",
83
+ feedbackEnabledHint: "每次蒸馏时附带项目级 + 用户级记忆作为参考,使其能基于既有记忆做增量维护(delete 仅手动蒸馏开放)。混合模式下仅手动触发蒸馏受影响(自动路径走记忆子代理,自带日志回喂)。默认关。"
83
84
  };
84
85
 
85
86
  const en = {
@@ -99,6 +100,7 @@ window.__ModuleLoader__.load({
99
100
  wsDirHint: "Used when no buddy dir exists, default .deepseek-harness/memory",
100
101
  retentionLabel: "Daily log retention (days)",
101
102
  retentionHint: "Older logs are migrated into MEMORY.md then removed",
103
+ retentionHintHybrid: "Not effective in hybrid mode: daily logs are maintained by the memory sub-agent and never expire (kept intact as an evidence layer).",
102
104
  userBudgetLabel: "User budget (chars)",
103
105
  userBudgetHint: "Max chars for the user-level MEMORY.md",
104
106
  wsBudgetLabel: "Workspace budget (chars)",
@@ -115,8 +117,8 @@ window.__ModuleLoader__.load({
115
117
  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
118
  subagentLogBudgetLabel: "Sub-agent log feed budget (chars)",
117
119
  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.",
118
- summaryModelLabel: "Summary model (smart mode)",
119
- summaryModelHint: "Leave empty to reuse the current session's provider/model, or pin a cheap model (provider/model) to save tokens.",
120
+ summaryModelLabel: "Model",
121
+ summaryModelHint: "The model currently used by the memory plugin (shared by smart-mode session summarization / manual distillation / the hybrid memory sub-agent). Leave empty to reuse the current session's provider/model, or pin a cheap model (provider/model) to save tokens.",
120
122
  save: "Save",
121
123
  saving: "Saving…",
122
124
  discard: "Discard",
@@ -145,16 +147,16 @@ window.__ModuleLoader__.load({
145
147
  dev: "Developer",
146
148
  distillTimeoutLabel: "Distill timeout (s)",
147
149
  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.",
148
- summaryMaxTokensLabel: "Session summary max tokens (smart mode)",
149
- 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.",
150
- projectMaxTokensLabel: "Project distill output soft budget",
151
- 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.",
150
+ summaryMaxTokensLabel: "Session summary max tokens",
151
+ summaryMaxTokensHint: "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.",
152
+ projectMaxTokensLabel: "Project distill max tokens",
153
+ projectMaxTokensHint: "Final output soft budget (prompt constraint, thinking not limited; actual hard cap is the model's own maxTokens). Default 8000.",
152
154
  feedbackEnabledLabel: "Feed existing memory into distillation",
153
- 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."
155
+ feedbackEnabledHint: "Feed the project-level + user-level memory in as reference on every distillation so the LLM can maintain existing memory incrementally (delete is only allowed via manual distillation). In hybrid mode only manual distillation is affected (the automatic path runs through the memory sub-agent, which feeds the log itself). Off by default."
154
156
  };
155
157
 
156
158
 
157
- const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars", "summaryTimeoutMs", "summaryMaxTokens", "projectMaxTokens"]);
159
+ const FIELD_NUMERIC = new Set(["dailyLogRetentionDays", "userBudgetChars", "workspaceBudgetChars", "summaryTimeoutMs", "summaryMaxTokens", "projectMaxTokens", "reorgCooldownDays", "subagentLogBudget"]);
158
160
  const FIELD_BOOL = new Set(["bridgeBuddyMemory", "summarize", "autoCaptureErrors", "distillDebugLog", "feedbackEnabled"]);
159
161
 
160
162
  function projectDraft(snap) {
@@ -175,6 +177,8 @@ window.__ModuleLoader__.load({
175
177
  summaryModel: v.summaryModel || "",
176
178
  summaryMaxTokens: v.summaryMaxTokens != null ? String(v.summaryMaxTokens) : "2000",
177
179
  projectMaxTokens: v.projectMaxTokens != null ? String(v.projectMaxTokens) : "8000",
180
+ reorgCooldownDays: v.reorgCooldownDays != null ? String(v.reorgCooldownDays) : "7",
181
+ subagentLogBudget: v.subagentLogBudget != null ? String(v.subagentLogBudget) : "20000",
178
182
  feedbackEnabled: v.feedbackEnabled === false ? "false" : "true"
179
183
  };
180
184
  }
@@ -564,38 +568,42 @@ window.__ModuleLoader__.load({
564
568
  h("option", { value: "smart" }, "智能模式"),
565
569
  h("option", { value: "hybrid" }, "混合模式")
566
570
  ]), "memoryMode"),
567
- draft.memoryMode === "smart"
571
+ draft.memoryMode === "hybrid"
568
572
  ? h("div", null, [
569
- row(t("summaryModelLabel"), t("summaryModelHint"),
570
- h("select", {
571
- // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
572
- // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
573
- value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
574
- disabled,
575
- style: selectStyle,
576
- onChange: (e) => edit("summaryModel", e.target.value)
577
- }, [
578
- h("option", { value: "" }, "复用当前会话模型"),
579
- ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
580
- ]), "summaryModel"),
581
- toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
582
- row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
583
- row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
573
+ row(t("reorgCooldownLabel"), t("reorgCooldownHint"), h("input", num("reorgCooldownDays")), "reorgCooldownDays"),
574
+ row(t("subagentLogBudgetLabel"), t("subagentLogBudgetHint"), h("input", num("subagentLogBudget")), "subagentLogBudget")
584
575
  ])
585
- : draft.memoryMode === "hybrid"
576
+ : draft.memoryMode === "plugin"
586
577
  ? 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
578
  toggle(t("autoCaptureErrorsLabel"), t("autoCaptureErrorsHint"), "autoCaptureErrors")
592
579
  ])
580
+ : null,
581
+ // ---- v1.6.3:蒸馏/模型参数提为常显(三模式共用,不受 memoryMode 限制)----
582
+ // summaryModel 同时被智能摘要与 hybrid 记忆子代理读取(subagent resolveModel);
583
+ // summaryMaxTokens/projectMaxTokens/feedbackEnabled 覆盖自动摘要 + 手动蒸馏两条链路。
584
+ h("div", null, [
585
+ row(t("summaryModelLabel"), t("summaryModelHint"),
586
+ h("select", {
587
+ // 已存储值不在下拉选项内(如旧版双前缀脏数据)→ 归零为「复用当前会话模型」,
588
+ // 避免 select 显示悬空值、保存时又把脏值写回 settings.yaml。
589
+ value: modelOptions.some((o) => o.value === draft.summaryModel) ? draft.summaryModel : "",
590
+ disabled,
591
+ style: selectStyle,
592
+ onChange: (e) => edit("summaryModel", e.target.value)
593
+ }, [
594
+ h("option", { value: "" }, "复用当前会话模型"),
595
+ ...modelOptions.map((o) => h("option", { value: o.value }, o.label))
596
+ ]), "summaryModel"),
597
+ toggle(t("feedbackEnabledLabel"), t("feedbackEnabledHint"), "feedbackEnabled"),
598
+ row(t("summaryMaxTokensLabel"), t("summaryMaxTokensHint"), h("input", num("summaryMaxTokens")), "summaryMaxTokens"),
599
+ row(t("projectMaxTokensLabel"), t("projectMaxTokensHint"), h("input", num("projectMaxTokens")), "projectMaxTokens")
600
+ ])
593
601
  ]),
594
602
  h("div", { style: cardStyle }, [
595
603
  h("p", { style: groupTitleStyle }, t("storage")),
596
604
  row(t("userPathLabel"), t("userPathHint"), h("input", { ...num("userMemoryPath"), type: "text" }), "userMemoryPath"),
597
605
  row(t("wsDirLabel"), t("wsDirHint"), h("input", { ...num("workspaceMemoryDir"), type: "text" }), "workspaceMemoryDir"),
598
- row(t("retentionLabel"), t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
606
+ row(t("retentionLabel"), draft.memoryMode === "hybrid" ? t("retentionHintHybrid") : t("retentionHint"), h("input", num("dailyLogRetentionDays")), "dailyLogRetentionDays"),
599
607
  row(t("userBudgetLabel"), t("userBudgetHint"), h("input", num("userBudgetChars")), "userBudgetChars"),
600
608
  row(t("wsBudgetLabel"), t("wsBudgetHint"), h("input", num("workspaceBudgetChars")), "workspaceBudgetChars")
601
609
  ]),
@@ -37,6 +37,8 @@ export function SUBAGENT_SYSTEM() {
37
37
  "6. 今日日志为空(新的一天)时直接用 append/new_section 建章节写入即可。\n" +
38
38
  "7. 需要读取日志章节(目录模式回喂时)用 log_read_section;需要多个章节时务必在单次调用中把 sections 数组全部传齐,避免多轮往返浪费轮次。\n" +
39
39
  "\n" +
40
- "【输出】全程只做上述两件事:要么纯文本收尾,要么调工具写日志后收尾。不要向用户提问,不要输出多余解释。"
40
+ "【输出】全程只做上述两件事:要么纯文本收尾,要么调工具写日志后收尾。不要向用户提问,不要输出多余解释。\n" +
41
+ "\n" +
42
+ "【环境边界】本环境不是代码执行环境,run_code 等工具均不存在;不要模仿上文出现过的任何工具调用,只能用 log_write_ops / log_read_section。"
41
43
  );
42
44
  }
@@ -17,6 +17,7 @@ import { HYBRID_PROACTIVE, SUBAGENT_SYSTEM } from "./prompts.mjs";
17
17
  export { HYBRID_PROACTIVE };
18
18
 
19
19
  const MAX_ROUNDS = 6; // 循环轮次上限(含首轮);超限降级
20
+ const MAX_RETRY = 3; // 未知工具触发的催促重试预算(v1.6.3,人定)
20
21
  const SURFACE = new Set(["user/message", "assistant/message", "tool/result"]);
21
22
 
22
23
  // 日志文件写锁(全局 promise 链串行化):子 agent 落盘与手动蒸馏按钮/其他写路径并发防护。
@@ -146,7 +147,16 @@ async function execLoopTool(block, logFiles, cfg) {
146
147
  if (!ops.length) return { ok: false, message: "ops 为空。" };
147
148
  return await applyLogOps(logFiles, todayISO(), ops);
148
149
  }
149
- return { ok: false, message: `未知工具 ${name}(仅 log_read_section / log_write_ops 可用)。` };
150
+ return {
151
+ ok: false,
152
+ unknown: true,
153
+ name,
154
+ message:
155
+ `工具 ${name} 不存在,调用已拒绝。本环境仅提供两个工具:\n` +
156
+ `- log_write_ops:写入今日日志(ops 数组,支持 append / new_section / mark_delete)\n` +
157
+ `- log_read_section:读取今日日志中指定章节(sections 数组)\n` +
158
+ `请立即改用 log_write_ops 提交 ops 完成本轮写入;不要调用其他工具,也不要直接结束。`,
159
+ };
150
160
  } catch (e) {
151
161
  return { ok: false, message: `执行失败:${e?.message || String(e)}` };
152
162
  }
@@ -214,7 +224,7 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
214
224
  // 调试日志(v1.6.0):与 distill.mjs 同款门控。dbgFail=失败/跳过留痕(无条件输出,排障可观测);
215
225
  // dbg=受 distillDebugLog 开关控制的详单(模型解析/请求参数/流进度/ops 结果计数)。
216
226
  // 隐私红线:只打计数/字符数/元数据/错误 message,绝不打印对话或日志正文文本。
217
- const dbgFail = (why, extra) => console.error(`[memory-palace] subagent skip: ${why}`, extra ? JSON.stringify(extra) : "");
227
+ const dbgFail = (why, extra) => console.error(`[memory-palace] subagent warn: ${why}`, extra ? JSON.stringify(extra) : "");
218
228
  const dbg = (why, extra) => { if (!cfg.distillDebugLog) return; console.error(`[memory-palace][debug] subagent ${why}`, extra !== undefined ? JSON.stringify(extra) : ""); };
219
229
  if (!cfg.enabled || !session || !dirs.length) {
220
230
  dbgFail("disabled or no session/dirs", { enabled: !!cfg.enabled, hasSession: !!session, dirs: dirs.length });
@@ -308,12 +318,47 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
308
318
  const timeoutMs = cfg.summaryTimeoutMs || 60000;
309
319
 
310
320
  try {
321
+ // v1.6.3:本轮工具执行统计——appliedWrites=成功落盘 ops 数、toolAttempts=调用过工具次数、
322
+ // unknownHits/unknownNames=误调未知工具次数与点名(供 stop 三态判定与催促重试文案)。
323
+ let appliedWrites = 0;
324
+ let toolAttempts = 0;
325
+ let unknownHits = 0;
326
+ let retries = 0;
327
+ const unknownNames = new Set();
311
328
  for (let round = 0; round < MAX_ROUNDS; round++) {
312
329
  const { finish, asm } = await streamTurn(ctx, { provider, model, system, messages, tools }, timeoutMs);
313
330
  if (finish.kind === "stop") {
314
- state.lastSummarizedSeq = session.seq;
315
- dbg("done", { round, mode: round === 0 ? "noop" : "written" });
316
- return { ok: true, mode: round === 0 ? "noop" : "written" };
331
+ // v1.6.3 stop 三态判定(修复:误调未知工具后模型直接收尾 → 日志丢失且断点被无脑推进)。
332
+ // ① 有落盘 → 正常完成,推进断点。
333
+ if (appliedWrites > 0) {
334
+ state.lastSummarizedSeq = session.seq;
335
+ dbg("done", { round, mode: "written", appliedWrites });
336
+ return { ok: true, mode: "written" };
337
+ }
338
+ // ② 从未调用任何工具 → 模型判定本轮无实质内容(noop),推进断点。
339
+ if (toolAttempts === 0) {
340
+ state.lastSummarizedSeq = session.seq;
341
+ dbg("done", { round, mode: "noop" });
342
+ return { ok: true, mode: "noop" };
343
+ }
344
+ // ③ 命中过未知工具且零写入 → 回告工具清单 + 催促重试(预算 MAX_RETRY 次)。
345
+ if (unknownHits > 0 && retries < MAX_RETRY) {
346
+ retries++;
347
+ const names = [...unknownNames].join("、");
348
+ messages.push(createUserMessage({
349
+ content: [{ type: "text", text:
350
+ `你刚才调用了不存在的工具(${names}),且尚未向日志写入任何内容,任务未完成。\n` +
351
+ `本轮唯一可用的写入工具是 log_write_ops(ops 支持 append / new_section / mark_delete);` +
352
+ `需要读取章节时用 log_read_section。请立即重新提交写入。\n` +
353
+ `若你重新判定本轮确实没有值得记录的内容,请输出一行纯文本说明后结束。` }],
354
+ source: { kind: "plugin", plugin: "memory-palace" },
355
+ }));
356
+ dbgFail("retry after unknown tool", { retries, names });
357
+ continue;
358
+ }
359
+ // ④ 重试预算耗尽 / 已知工具用错后自主放弃 → 零写入,不推进断点(下一轮 turn/end 补蒸)。
360
+ dbgFail("no write applied", { round, toolAttempts, unknownHits, retries });
361
+ return { ok: false, mode: "no-write" };
317
362
  }
318
363
  if (finish.kind !== "tool-calls") {
319
364
  // max-tokens 等异常收尾:返回失败,断点不推进——下一次 turn/end 子代理自动补蒸。
@@ -325,7 +370,13 @@ export async function runMemorySubagent({ ctx, getConfig, paths, state, session,
325
370
  const calls = asm.blocks().filter((b) => b.type === "tool-call");
326
371
  dbg("tool-calls round", { round, calls: calls.length, names: calls.map((c) => c.name) });
327
372
  for (const call of calls) {
373
+ toolAttempts++;
328
374
  const result = await execLoopTool(call, logFiles, cfg);
375
+ if (result.unknown) {
376
+ unknownHits++;
377
+ unknownNames.add(call.name);
378
+ }
379
+ if (result.ok && result.applied > 0) appliedWrites += result.applied;
329
380
  const body = result.ok && result.content !== undefined ? String(result.content) : JSON.stringify(result);
330
381
  messages.push(
331
382
  createToolResultMessage({
package/lib/index.js CHANGED
@@ -56,16 +56,16 @@ export const Config = Schema.object({
56
56
  // ---- v1.1.0:记忆模式(plugin=记忆公民指令+轮次轻量+错误捕获;smart=LLM 智能会话摘要)----
57
57
  // v1.6.0:+hybrid(子 agent 自动维护日志 + agent 主动维护 MEMORY.md),三档并存,切换需重启 dsh。
58
58
  memoryMode: Schema.union(["plugin", "smart", "hybrid"]).default("plugin").description("记忆模式:plugin=记忆公民指令+轮次轻量+错误捕获;smart=LLM 智能会话摘要(summary→每日日志 + durable→MEMORY.md);hybrid=记忆子代理自动维护今日日志(标删去重)+ agent 主动维护 MEMORY.md(章节化写入/整章节替换/门禁内全量重整)。切换需重启 dsh 生效。"),
59
- summaryModel: Schema.string().default("").description("智能模式的摘要模型,留空=复用当前会话 provider/model;也可填 provider/model(如 deepseek/deepseek-chat)固定廉价模型省 token。"),
59
+ summaryModel: Schema.string().default("").description("记忆插件当前使用的模型(智能模式会话摘要 / 手动蒸馏 / hybrid 记忆子代理共用)。留空=复用当前会话 provider/model;也可填 provider/model(如 deepseek/deepseek-chat)固定廉价模型省 token。"),
60
60
  summaryTimeoutMs: Schema.number().default(60000).description("蒸馏 LLM 调用的超时(毫秒),超时视为失败并降级;覆盖智能模式摘要与手动蒸馏两条链路;默认 60000(60s)。"),
61
61
  distillDebugLog: Schema.boolean().default(false).description("调试开关:向 dsh 服务端 stderr 输出蒸馏 LLM 调用诊断。distillLogLevel=info 时仅输出元数据(模型解析/请求参数/流进度/错误详情,不含文本);distillLogLevel=debug 会额外打印 LLM 原始响应文本(分隔符包裹),仅限受信本地排障开启。"),
62
62
  // ---- v1.4.1:蒸馏 stderr 日志级别(平铺键,不进 UI;默认 info;distillDebugLog=true 时生效) ----
63
63
  distillLogLevel: Schema.string().default("info").description("蒸馏 stderr 日志级别:info=仅元数据诊断(默认,不打印 LLM 原始响应);debug=额外打印 LLM 原始响应文本(分隔符包裹),仅限受信本地排障开启。无需 UI 配置,经 profile/settings.yaml 设置 distillLogLevel 键。"),
64
64
  // ---- v1.4.0:智能模式最终输出软预算(v1.4.1 起语义变更:prompt 软约束,思考不受限;实际硬上限由模型自身 maxTokens 决定) ----
65
- summaryMaxTokens: Schema.number().default(2000).description("智能模式会话摘要 LLM 的【最终输出软预算(prompt 约束,思考不受限)】。默认 2000。实际硬上限由模型自身 maxTokens 决定;可调大以容纳更多 durable 事实。"),
66
- projectMaxTokens: Schema.number().default(8000).description("手动「蒸馏项目记忆」LLM 的【最终输出软预算(prompt 约束,思考不受限)】。默认 8000。实际硬上限由模型自身 maxTokens 决定。"),
65
+ summaryMaxTokens: Schema.number().default(2000).description("会话摘要 LLM 的最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)。默认 2000;可调大以容纳更多 durable 事实。"),
66
+ projectMaxTokens: Schema.number().default(8000).description("手动「蒸馏项目记忆」LLM 的最终输出软预算(prompt 约束,思考不受限;实际硬上限由模型自身 maxTokens 决定)。默认 8000。"),
67
67
  // ---- v1.4.0:蒸馏时回喂存量记忆(特性3,默认关;开启后自动智能模式 turn/end 与手动蒸馏按钮均回喂,delete 仅手动放开) ----
68
- feedbackEnabled: Schema.boolean().default(false).description("蒸馏时把项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)回喂给 LLM,使其能基于既有记忆做 add/replace/delete 增量维护。默认关;开启后自动智能模式(turn/end)与手动蒸馏会话按钮均回喂存量记忆;delete 仍仅手动按钮开放,自动模式跳过(防误删)。hybrid 模式下此开关不参与(子代理自带日志回喂)。"),
68
+ feedbackEnabled: Schema.boolean().default(false).description("每次蒸馏时把项目级 + 用户级 MEMORY.md 全文(逐行编号、无截断)回喂给 LLM,使其能基于既有记忆做增量维护。delete 仍仅手动蒸馏按钮开放,自动模式跳过(防误删)。hybrid 模式下仅手动触发蒸馏受影响(自动路径走记忆子代理,自带日志回喂)。默认关。"),
69
69
  // ---- v1.6.0:hybrid 模式配置(仅 memoryMode=hybrid 时生效) ----
70
70
  reorgCooldownDays: Schema.number().default(7).description("hybrid 模式:项目级 MEMORY.md 全量重整的冷却天数(距上次重整)。与「超出注入预算」双条件同时满足才允许 memory_reorganize;时间戳以 HTML 注释落在 MEMORY.md 文件尾。"),
71
71
  subagentLogBudget: Schema.number().default(20000).description("hybrid 模式:记忆子代理回喂今日工作日志的字符上限。超出时仅回喂章节目录,子代理用 log_read_section 按需读取章节。"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-palace",
3
- "version": "1.6.2-alpha.4",
3
+ "version": "1.6.3",
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",