dsh-memory-palace 1.7.2-alpha.2 → 1.7.2

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,7 +2,7 @@
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) [![npm](https://img.shields.io/npm/v/dsh-memory-palace.svg?label=npm&labelColor=000000&color=ff4b01)](https://www.npmjs.com/package/dsh-memory-palace) [![DeepSeek Harness:0.1.7-alpha.1](https://img.shields.io/badge/DeepSeek%20Harness-0.1.7--alpha.1-success.svg?labelColor=4D6BFE)](https://github.com/deepseek-ai/deepseek-harness)
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&labelColor=000000&color=ff4b01)](https://www.npmjs.com/package/dsh-memory-palace) [![DeepSeek Harness:0.1.7-rc.1](https://img.shields.io/badge/DeepSeek%20Harness-0.1.7--rc.1-success.svg?labelColor=4D6BFE)](https://github.com/deepseek-ai/deepseek-harness)
6
6
 
7
7
  ## 特性
8
8
 
@@ -13,10 +13,11 @@
13
13
  - **手动蒸馏**:会话标题栏一键把当前对话或项目记忆提炼成长期记忆。
14
14
  - **设置页集成**:全部配置均可在 DSH 设置面板中图形化调整(按板块折叠),无需改配置文件。
15
15
  - **自定义指令**:不适合写进记忆的特殊指令可在设置页配置,插件经系统提示词注入。
16
- - **主动记忆(插件模式)**:以指令引导 AI 在完成任务、修复问题、确定决策、获知偏好时主动落档。*即将移除*
17
- - **智能模式**:由模型自动提炼每轮新增内容,摘要进日志、长期事实进记忆。*即将移除*
18
16
  - **记忆注入**:长期记忆与今日日志以「会话起始快照」常驻上下文、按文件身份各只注入一次 —— 既保证每个步骤都可见,又不破坏前缀缓存、不堆积冗余快照。
19
- - **混合模式(🔥推荐)**:子代理每轮自动整理日志并去重,长期记忆由主 AI 主动维护,兼顾自动化与可控性。
17
+ - **记忆模式**
18
+ - **主动记忆(插件模式)**:以指令引导 AI 在完成任务、修复问题、确定决策、获知偏好时主动落档。*即将移除*
19
+ - **智能模式**:由模型自动提炼每轮新增内容,摘要进日志、长期事实进记忆。*即将移除*
20
+ - **混合模式(🔥推荐)**:子代理每轮自动整理日志并去重,长期记忆由主 AI 主动维护,兼顾自动化与可控性。
20
21
  - **标准插件包**:经官方插件机制一键安装,无需改动 harness。
21
22
 
22
23
  ## 记忆文件布局
@@ -77,7 +78,7 @@
77
78
 
78
79
  ```bash
79
80
  dsh plugin --profile web add github:lovezi0/dsh-memory-palace
80
- # 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.7.2-alpha.2
81
+ # 锁定版本:dsh plugin --profile web add github:lovezi0/dsh-memory-palace#v1.7.2
81
82
  ```
82
83
 
83
84
  方式二:clone 后本地安装(开发 / 修改源码场景)
@@ -119,6 +120,12 @@ dsh plugin --profile web remove dsh-memory-palace
119
120
  ## 版本历史
120
121
 
121
122
  - **1.7.2**
123
+ - 修正注释版本号笔误
124
+ - 徽标核验线更新至 0.1.7-rc.1
125
+ - npm publish
126
+ - **1.7.2-alpha.3**
127
+ - 🐛修复记忆投影通道并发导致记忆串台的问题 @sunligh91
128
+ - 🐛修复插件工具串台问题
122
129
  - **1.7.2-alpha.2**
123
130
  - 💪适配deepseek harness 0.1.7-alpha.1
124
131
  - 💪add memory plugin icon
package/lib/client.js CHANGED
@@ -784,7 +784,7 @@ window.__ModuleLoader__.load({
784
784
  document.head.appendChild(style);
785
785
  }
786
786
 
787
- // ---- v1.8.0 设置面板 nav 图标(「记忆」条目)----
787
+ // ---- v1.7.2-alpha.2 设置面板 nav 图标(「记忆」条目)----
788
788
  // 宿主 SettingsRoot.navIcon(id) 是**硬编码白名单**(account/models/agent-presets/plugins/archived-sessions),
789
789
  // 未列出的第三方 section 一律回退齿轮图标,且 slots 契约(id/order/label)没开 nav 图标位、
790
790
  // 官方文档亦无「从 manifest 取 nav 图标」的机制(manifest icon 只进插件管理面板的卡片/行)。
@@ -1046,7 +1046,7 @@ window.__ModuleLoader__.load({
1046
1046
  // v1.1.3:设置读写走自有 route(/memory-palace/api),不再 bind settingsScope——
1047
1047
  // 非 loopback 下 settingsScope persistence=memory(set() no-op),保存永不落盘。
1048
1048
  const controller = new MemoryPalaceController(ctx);
1049
- // v1.8.0:设置面板 nav 条目换成自家 sparkle-twinkle 星标——宿主无 nav 图标位,
1049
+ // v1.7.2-alpha.2:设置面板 nav 条目换成自家 sparkle-twinkle 星标——宿主无 nav 图标位,
1050
1050
  // 走「label 传节点 + CSS 隐藏回退齿轮」两步(原理/失效面见 40-sparkle.js 的 ensureNavStarStyles)。
1051
1051
  ensureNavStarStyles();
1052
1052
  const navLabel = () =>
@@ -249,9 +249,10 @@ export function createRecords({ getConfig, paths }) {
249
249
 
250
250
  // 错误捕获(目标2):按 scope 落对应 MEMORY.md,条目仅含「错误现象」。
251
251
  // (v0.7.1 起不再调 LLM 生成方案——「根因/绕过手法」由 agent 按记忆公民指令场景①主动调 memory_note 记。)
252
- async function captureError(turn, reason) {
252
+ // dirs 由调用方(_settle,已按会话 cwd 解析)显式传入——不再自取 paths.writeDirs(),
253
+ // 消除多会话并发时闭包 activeCwd 串台;空 dirs 仍按原语义回落用户级。
254
+ async function captureError(turn, reason, dirs, cwd) {
253
255
  const c = cfg();
254
- const dirs = paths.writeDirs();
255
256
  if (!dirs.length && !expandHome(c.userMemoryPath)) return;
256
257
  const scope = dirs.length ? "project" : "user";
257
258
  const errText = (reason?.message || reason?.error?.message || "(in-session error, no message)")
@@ -71,14 +71,19 @@ function sectionHasEntry(md, section, entry) {
71
71
  */
72
72
  export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
73
73
  const cfg = () => getConfig();
74
+ // 从宿主 execute 第二参(ToolRunContext,defineTool 原样转发)取发起会话 cwd。
75
+ // 用可选链自然得 undefined(勿 `?? null`——null 会绕过 paths 默认参数导致 dirs 恒空)。
76
+ const sessionCwd = (exec) => exec?.agent?.session?.header?.cwd;
74
77
 
75
78
  // 目标文件解析:project = 主目标(dirs[0],多 buddy 目录时与蒸馏按钮同语义只动主目标,
76
79
  // 章节级 stale 校验天然防止错位覆写);user = 用户级 MEMORY.md。
77
- function memFileOf(scope) {
80
+ // cwd 由调用方从 exec.agent.session.header.cwd 透传(多会话并发时 activeCwd 可能是别的
81
+ // 工作区,串台会写错项目);缺省(undefined)回落 paths 工厂默认 = activeCwd,保持旧行为。
82
+ function memFileOf(scope, cwd) {
78
83
  if (scope === "user") return { file: expandHome(cfg().userMemoryPath), dirs: [] };
79
- const dirs = paths.writeDirs();
84
+ const dirs = paths.writeDirs(cwd);
80
85
  if (!dirs.length) return null;
81
- return { file: paths.memoryFileOf(dirs[0]), dirs };
86
+ return { file: paths.memoryFileOf(dirs[0], cwd), dirs };
82
87
  }
83
88
 
84
89
  // ---------- memory_write:章节追加/新增(章节化格式规范承载于 description——定案 #11) ----------
@@ -112,19 +117,20 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
112
117
  },
113
118
  render: (_args, value) => [{ type: "text", text: value?.message ?? "Saved." }],
114
119
  },
115
- async execute(args) {
120
+ async execute(args, exec) {
116
121
  state.recentAgentWrote = true;
117
122
  const c = cfg();
118
123
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings." };
119
124
  const section = String(args.section ?? "").trim();
120
125
  const entry = String(args.entry ?? "").trim();
121
126
  if (!section || !entry) return { ok: false, message: "section 与 entry 均不能为空。" };
122
- const target = memFileOf(args.scope === "user" ? "user" : "project");
127
+ const cwd = sessionCwd(exec);
128
+ const target = memFileOf(args.scope === "user" ? "user" : "project", cwd);
123
129
  if (!target) return { ok: false, message: "No active workspace." };
124
130
  try {
125
131
  let wrote = 0;
126
132
  let skipped = 0;
127
- const files = target.dirs.length ? target.dirs.map((d) => paths.memoryFileOf(d)) : [target.file];
133
+ const files = target.dirs.length ? target.dirs.map((d) => paths.memoryFileOf(d, cwd)) : [target.file];
128
134
  for (const file of files) {
129
135
  const md = readMdSync(file);
130
136
  if (sectionHasEntry(md, section, entry)) {
@@ -195,14 +201,14 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
195
201
  render: (_args, value) =>
196
202
  [{ type: "text", text: value?.actual ? `${value.message}\n\n当前实际内容:\n${value.actual}` : value?.message ?? "Done." }],
197
203
  },
198
- async execute(args) {
204
+ async execute(args, exec) {
199
205
  state.recentAgentWrote = true;
200
206
  const c = cfg();
201
207
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings.", actual: "" };
202
208
  const section = String(args.section ?? "").trim();
203
209
  const oldText = String(args.oldText ?? "");
204
210
  if (!section || !oldText.trim()) return { ok: false, message: "section 与 oldText 均不能为空。", actual: "" };
205
- const target = memFileOf(args.scope === "user" ? "user" : "project");
211
+ const target = memFileOf(args.scope === "user" ? "user" : "project", sessionCwd(exec));
206
212
  if (!target) return { ok: false, message: "No active workspace.", actual: "" };
207
213
  try {
208
214
  const file = target.file;
@@ -251,13 +257,14 @@ export function registerHybridTools({ ctx, getConfig, paths, records, state }) {
251
257
  },
252
258
  render: (_args, value) => [{ type: "text", text: value?.message ?? "Done." }],
253
259
  },
254
- async execute(args) {
260
+ async execute(args, exec) {
255
261
  state.recentAgentWrote = true;
256
262
  const c = cfg();
257
263
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings." };
258
- const dirs = paths.writeDirs();
264
+ const cwd = sessionCwd(exec);
265
+ const dirs = paths.writeDirs(cwd);
259
266
  if (!dirs.length) return { ok: false, message: "No active workspace." };
260
- const file = paths.memoryFileOf(dirs[0]);
267
+ const file = paths.memoryFileOf(dirs[0], cwd);
261
268
  // 复核双门禁(pre-execute 已拦一道;这里防绕过)
262
269
  const gate = checkReorgGate(file, c);
263
270
  if (!gate.ok) return { ok: false, message: `重整被门禁拒绝:${gate.reason}` };
@@ -308,9 +315,10 @@ export function attachHybridGuards(ctx, getConfig, paths, state) {
308
315
  }
309
316
  if (name !== "memory_reorganize") return next();
310
317
  const c = getConfig();
311
- const dirs = paths.writeDirs();
318
+ const cwd = exec?.agent?.session?.header?.cwd;
319
+ const dirs = paths.writeDirs(cwd);
312
320
  if (!dirs.length) return { kind: "deny", reason: "无活动工作区,无法重整。" };
313
- const file = paths.memoryFileOf(dirs[0]);
321
+ const file = paths.memoryFileOf(dirs[0], cwd);
314
322
  const gate = checkReorgGate(file, c);
315
323
  if (!gate.ok) return { kind: "deny", reason: gate.reason };
316
324
  const args = (exec.arguments && typeof exec.arguments === "object") ? exec.arguments : {};
package/lib/index.js CHANGED
@@ -40,7 +40,7 @@ export const name = "memory-palace";
40
40
  // v1.6.0-rc1:settingsNamespace() 已删除,namespace 直接用字符串(宿主侧做类型级校验)。
41
41
  const MEMORY_PALACE_SETTINGS_NAMESPACE = "memory-palace";
42
42
 
43
- // v1.8.0:适配 dsh 0.1.7-alpha.1 契约破坏(DSH-CONTRACT-TRACKING.md §1.6)。
43
+ // v1.7.2-alpha.2:适配 dsh 0.1.7-alpha.1 契约破坏(DSH-CONTRACT-TRACKING.md §1.6)。
44
44
  // - 契约5:MessageSourceMap 删除通用 'plugin' kind → 投影/子代理消息 source 改用宿主
45
45
  // v3→v4 迁移约定 `plugin:dsh-memory-palace`(去重判据向后兼容旧形态,防升级前已 resume 的会话重复注入)。
46
46
  // - 契约10:SettingsProvider.installSection 整包删除 → ctx.settings 改挂 SettingsForms
@@ -90,7 +90,7 @@ export const Config = Schema.object({
90
90
  export const inject = ["systemPrompt", "tools", "llm", "webServer", "webRuntime"];
91
91
 
92
92
  /**
93
- * v1.8.0(dsh 0.1.7 适配):解包 cordis 解析后的 cosmokit Volatile 引用。
93
+ * v1.7.2-alpha.2(dsh 0.1.7 适配):解包 cordis 解析后的 cosmokit Volatile 引用。
94
94
  * 0.1.7 起 Config schema 字段标 `.volatile()`,loader 解析后该字段值不再是原始值,
95
95
  * 而是 `{ get(): snapshot, [writeSymbol]: fn }` 引用对象(热改时原地更新、不重载 fiber)。
96
96
  * 宿主自己的 consumer 一律 `.get()` 读;本插件全部配置经 source() 消费,故在此统一解包。
@@ -113,15 +113,15 @@ export function apply(ctx, config) {
113
113
  // 未挂载 settings 服务时回退到 cordis 组合里的 config。
114
114
  // v1.6.0-rc1:注册动作挪进下方 ctx.inject(["settings"]) 回调(installSection 是
115
115
  // SettingsProvider 实例方法,须等 settings 服务挂载;其内部同步 setSource,时序与旧版等价)。
116
- // v1.8.0:配置真源统一 = cordis 注入的 config(0.1.7 起即 profile 条目 config、热改原地生效)。
116
+ // v1.7.2-alpha.2:配置真源统一 = cordis 注入的 config(0.1.7 起即 profile 条目 config、热改原地生效)。
117
117
  let source = () => unwrapConfig(config);
118
118
  const baseEntry = { ...config };
119
119
 
120
- // ---- v1.1.3:设置读写 route(真保存)/ v1.8.0:适配 dsh 0.1.7 SettingsForms ----
120
+ // ---- v1.1.3:设置读写 route(真保存)/ v1.7.2-alpha.2:适配 dsh 0.1.7 SettingsForms ----
121
121
  // client fetch 同源 /memory-palace/api/settings.get|update,绕过 apiproxy allowlist;
122
122
  // handler 内直接走服务端 settings 面 → profile patch 持久化。
123
123
  // 写用 replace 语义(整节替换):表单里留空的字段自动回退 base/schema 默认,与「保存=提交整个表单」一致。
124
- // v1.8.0 适配(dsh 0.1.7-alpha.1):dsh-settings 整包重写为 SettingsForms——
124
+ // v1.7.2-alpha.2 适配(dsh 0.1.7-alpha.1):dsh-settings 整包重写为 SettingsForms——
125
125
  // ① 不再有 installSection / setSource / onChange:配置的**唯一真源 = profile 条目 config**,
126
126
  // 应用值就是 cordis 注入的 config,且热改经 loader 原地生效(volatile 字段经引用更新、
127
127
  // 其余字段整 fiber 重载),故 source 恒为 () => config,无需 setSource 切换。
@@ -143,7 +143,7 @@ export function apply(ctx, config) {
143
143
  ? { value: undefined, user: undefined, revision: undefined }
144
144
  : { value: descriptor.value, user: descriptor.user, revision: descriptor.revision };
145
145
  };
146
- // v1.8.0 双栈:旧宿主(0.1.6 线 SettingsProvider)有 installSection,须注册节并把 setSource
146
+ // v1.7.2-alpha.2 双栈:旧宿主(0.1.6 线 SettingsProvider)有 installSection,须注册节并把 setSource
147
147
  // 切到解析层;新宿主(0.1.7 SettingsForms)无此方法——配置真源即 profile 条目 config,
148
148
  // 热改经 loader 生效,source 保持 () => config 即可。describe/replace 两栈签名兼容。
149
149
  if (typeof sctx.settings.installSection === "function") {
@@ -454,7 +454,8 @@ export function apply(ctx, config) {
454
454
  // v1.7.2:静默会话(极简等预设)不写任何记忆——错误捕获 / 轻量兜底 / smart 摘要 / hybrid 子代理
455
455
  // 全部在此一处提前返回(hybrid 分支在其下方,天然被拦)。
456
456
  if (isSessionSilent(state.activeSession)) return;
457
- const dirs = paths.writeDirs();
457
+ const cwd = state.activeSession?.header?.cwd ?? null;
458
+ const dirs = paths.writeDirs(cwd);
458
459
  if (!dirs.length) return;
459
460
 
460
461
  // 基础闸门(A+D 合并):结构信号(工具/错误/主动记)或 场景关键词 → 开。
@@ -512,7 +513,7 @@ export function apply(ctx, config) {
512
513
  // 插件模式:错误捕获(受 autoCaptureErrors 门控)+ 轻量兜底记录。
513
514
  if (cfg.autoCaptureErrors && effectiveIsError) {
514
515
  const finalErrMsg = errMsg || toolErr || "(in-session error, no message)";
515
- void records.captureError(capturedTurn, { kind: "error", message: finalErrMsg }).catch(() => {});
516
+ void records.captureError(capturedTurn, { kind: "error", message: finalErrMsg }, dirs, cwd).catch(() => {});
516
517
  }
517
518
  void records.writeLightEntry(dirs, capturedTurn, effectiveIsError).catch(() => {});
518
519
  }
@@ -1,183 +1,185 @@
1
- // memory-palace E 投影 —— 记忆正文与今日日志的独立注入通道。
2
- //
3
- // 背景(v1.6.3 及以前):用户级 + 项目级 MEMORY.md 走 systemPrompt.section,受
4
- // 「仅首次注入」限制(`alreadyInjected` 打了标记就不撕,只有 compaction 清),
5
- // 导致记忆正文在 turn 1 的 step 2 之后就消失、全程不再进场。
6
- //
7
- // 现在:记忆正文改经 `agent/pre-step` 投影为**独立 user 消息**,常驻对话历史:
8
- // - 每个 step 都可见(消息在历史里,不依赖注入时机);
9
- // - v1.7.1 起去重判据由「全文深比较」改为「**文件身份**」(首行 heading,含路径 + 日期):
10
- // 同一文件只注一次,内容再变也不重注 —— 记忆正文由 agent 自己写入,其内容本就在
11
- // 上下文(tool/call 参数)里,重注纯属冗余;换文件(跨日期日志)才视为新身份注入;
12
- // - v1.7.1 起**今日工作日志由 systemPrompt.section 迁入本通道**。日志是 agent 唯一
13
- // 「不知道自己缺」的内容(hybrid 子代理以独立 LLM stream 写入、不回注主对话),
14
- // 必须注入;迁出后 section 只剩恒定指令 → system prompt 恒定 → 前缀缓存永久有效
15
- // (此前日志在 section 里每步求值,一变就让序列最前的整段前缀作废)。
16
- // - compaction 把消息移出 surface 后,身份判据即判为缺失 → 自动重注**磁盘最新版**,
17
- // 顺带完成一次「刷新到最新」。
18
- // - v1.7.2 起**静默会话整体不投影**(isSilent(session)):宿主「模式」是 agent 平面预设,
19
- // 本插件却在 host 平面,投影对每个 agent 都生效——官方 minimal 预设(persona complete:true
20
- // 独占 system prompt 的"裸测环境")下必须自行按会话静默,否则记忆正文仍会常驻历史吃 token。
21
- //
22
- // 实现照抄宿主 `packages/context/agent-instructions/src/index.ts` 三件套(API 全公开、无第一方特权):
23
- // 1. `inject: ['sessionProjections']`(服务随 dsh-base 默认装载)
24
- // 2. `ctx.on('agent/pre-step', async ({agent, messages, step, signal}, next) => {...})`
25
- // 3. `createUserMessage()`(来自 @deepseek-ai/dsh-llm)
26
- // 投递方式:同步版——折叠进 `decision.messages`(`toSpliced(lastClaimedIndex + 1, 0, ...pending)`),
27
- // 不做异步 inbox 刷新(agent-instructions 注释明写异步投影有两个 commit 边界的时序约束,
28
- // 本版刻意回避;记忆更新后下一步自然带上新内容)。
29
- //
30
- // 🔴 契约风险:pre-step 监听器抛错会让**整个 turn 失败**(宿主测试
31
- // packages/acp/acp/tests/turns.spec.ts:150-156 锚定),故钩子内整体 try-catch,
32
- // 任何异常降级为「本步不投影」并留一行 stderr 线索,绝不冒泡。详见 DEVELOPMENT.md §7.4。
33
- import { join } from "node:path";
34
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
35
- import { expandHome, toHomeShort, todayISO, readMdSync, budgetClip, stripSmartTag, stripDeletedLines } from "./common/text.mjs";
36
-
37
- // 投影消息的 source 标注。
38
- // v1.8.0(dsh 0.1.7 适配):宿主删除了通用 catch-all `plugin` kind(MessageSourceMap 不再含
39
- // 'plugin' 键),改为「每个生产者在自己的模块里声明自有 kind」;v3→v4 会话持久化迁移把第三方
40
- // 旧源 `{kind:'plugin',plugin:X}` 规范化为 `plugin:X`(见 session-format-v3-to-v4/sources.ts
41
- // producerKind())。故本插件合规 kind = 'plugin:dsh-memory-palace'。运行时不校验 kind 白名单
42
- // (createMessage 仅 deepFreeze(structuredClone)),但未知 kind 在 UI 走兜底呈现、且未来宿主
43
- // 若加校验即炸——按迁移约定写才安全。
44
- // - form: 'instructions' → 宿主 ContextForm 联合内的合法值(曾拟用 'memory',但其不在该联合内,
45
- // 属类型不合法;'instructions' 语义接近"指令性内容"且类型安全,UI 按指令样式呈现)。
46
- const PROJECTION_SOURCE = Object.freeze({
47
- kind: "plugin:dsh-memory-palace",
48
- form: "instructions",
49
- });
50
-
51
- // v1.7.1 判据:投影身份 = 消息首行 heading(已编码目标文件路径 + 日期)。
52
- // 只比身份、不比正文 —— 同一文件投影过一次后,内容再变也不重注。
53
- function projectionIdentity(msg) {
54
- const text = msg?.content?.[0]?.text ?? "";
55
- const i = text.indexOf("\n");
56
- return i === -1 ? text : text.slice(0, i);
57
- }
58
-
59
- // 只在双方都是本插件投影时才比身份 —— 规避真实用户消息与宿主注入消息
60
- // (runtime context / skill 目录等)因首行文本巧合而误撞。
61
- // v1.8.0:新消息 kind='plugin:dsh-memory-palace';同时**保留旧 kind='plugin'+plugin 字段的识别**
62
- // —— 历史会话里已持久化的旧投影消息经宿主 v3→v4 迁移会变成 plugin:dsh-memory-palace,但旧宿主
63
- // (<0.1.7)与未迁移数据仍是旧形状,双认可防「换 kind 当天在老会话里重复注入」。
64
- function isOwnProjection(msg) {
65
- const s = msg?.source;
66
- if (!s || s.form !== "instructions") return false;
67
- if (s.kind === "plugin:dsh-memory-palace") return true;
68
- return s.kind === "plugin" && s.plugin === "dsh-memory-palace";
69
- }
70
-
71
- function sameProjectionIdentity(left, right) {
72
- return isOwnProjection(left) && isOwnProjection(right)
73
- && projectionIdentity(left) === projectionIdentity(right);
74
- }
75
-
76
- // 扫描当前 surface,判断该身份是否已经作为本插件消息存在于会话历史中。
77
- // 存在 → 不重复注入("同一文件只注一次"靠它实现)。
78
- // compaction 把消息移出 surface 后即判为缺失 → 自动重注磁盘最新版
79
- // (身份不变、正文刷新,故无需 injectedSessionIds 之类的标记机制)。
80
- function alreadyOnSurface(session, desired) {
81
- const nodes = session?.surface?.nodes;
82
- if (!Array.isArray(nodes)) return false;
83
- for (const seq of nodes) {
84
- const event = typeof session.eventAt === "function" ? session.eventAt(seq) : undefined;
85
- if (event?.type === "user/message" && sameProjectionIdentity(event.data, desired)) return true;
86
- }
87
- return false;
88
- }
89
-
90
- // 构造一条投影消息;内容为空(文件不存在 / 无记忆)时返回 null。
91
- function buildProjection(text, heading) {
92
- if (!text) return null;
93
- return createUserMessage({
94
- content: [{ type: "text", text: `${heading}\n${text}` }],
95
- source: PROJECTION_SOURCE,
96
- });
97
- }
98
-
99
- /**
100
- * 收集本步要投影的消息(用户级 / 项目级 MEMORY.md / 今日工作日志,各自独立一条)。
101
- * 拆成多条独立消息(v1.7.0 定案,v1.7.1 扩展):否则"某一条内容一改,其余连带重发"。
102
- * v1.7.2:新增 `session` + `isSilent` —— 静默会话(官方 minimal 等预设)整体不投影。
103
- * @param {{ getConfig: () => object, paths: object, session?: object, isSilent?: (session?: object) => boolean }} deps
104
- * @returns {object[]} 按 [用户级, 项目级…, 今日日志…] 顺序的消息数组(可能为空)
105
- */
106
- export function buildProjections({ getConfig, paths, session, isSilent }) {
107
- const cfg = getConfig();
108
- if (!cfg.enabled) return [];
109
- // v1.7.2:静默会话(极简模式等)不投影——投影消息是**常驻历史**,比 section 更"重":
110
- // 它要一路吃 token 直到 compaction,正是"裸测环境"最不该有的东西。
111
- if (isSilent?.(session)) return [];
112
- const out = [];
113
- // 用户级 MEMORY.md
114
- const userFile = expandHome(cfg.userMemoryPath);
115
- const userText = readMdSync(userFile);
116
- if (userText) {
117
- const clipped = stripSmartTag(stripDeletedLines(budgetClip(userText, cfg.userBudgetChars)));
118
- const msg = buildProjection(clipped, `# 用户级记忆 (${toHomeShort(userFile)})`);
119
- if (msg) out.push(msg);
120
- }
121
- // 项目级 MEMORY.md:按 readDirs 顺序(dsh 优先叠加 buddy),同一文件路径只取一次。
122
- const seen = new Set();
123
- for (const dir of paths.readDirs()) {
124
- for (const file of paths.memoryReadCandidates(dir)) {
125
- if (seen.has(file)) continue;
126
- seen.add(file);
127
- const text = readMdSync(file);
128
- if (!text) continue;
129
- const clipped = stripSmartTag(stripDeletedLines(budgetClip(text, cfg.workspaceBudgetChars)));
130
- const msg = buildProjection(clipped, `# 项目级记忆 (${toHomeShort(file)})`);
131
- if (msg) out.push(msg);
132
- }
133
- }
134
- // 今日工作日志(v1.7.1 由 systemPrompt.section 迁入)。heading 沿用原 section 格式,
135
- // 天然含日期与目录 → 跨天 / 换目录自动成为新身份。预算沿用 workspaceBudgetChars,
136
- // 与项目级 MEMORY.md 各自独立 budgetClip,互不挤占。
137
- for (const dir of paths.readDirs()) {
138
- const text = readMdSync(join(dir, `${todayISO()}.md`));
139
- if (!text) continue;
140
- const clipped = stripSmartTag(stripDeletedLines(budgetClip(text, cfg.workspaceBudgetChars)));
141
- const msg = buildProjection(clipped, `# 今日工作日志 (${todayISO()} @ ${toHomeShort(dir)})`);
142
- if (msg) out.push(msg);
143
- }
144
- return out;
145
- }
146
-
147
- /**
148
- * 注册 E 投影钩子。
149
- * @param {{ ctx: object, getConfig: () => object, paths: object, isSilent?: (session?: object) => boolean }} deps
150
- */
151
- export function registerProjection({ ctx, getConfig, paths, isSilent }) {
152
- ctx.on("agent/pre-step", async ({ agent, messages, step, signal }, next) => {
153
- // 先让瀑布流继续,拿到宿主与其他插件合成后的决策——投影要折叠进它的 messages。
154
- const decision = await next();
155
- try {
156
- signal?.throwIfAborted?.();
157
- if (decision.kind !== "enter") return decision;
158
- const session = agent?.session;
159
- // v1.7.2:静默会话(极简模式等)不投影。放在 next() 之后、构造之前,成本最低。
160
- if (isSilent?.(session)) return decision;
161
- const desired = buildProjections({ getConfig, paths, session, isSilent });
162
- if (!desired.length) return decision;
163
- // 逐条去重(按**文件身份**、非内容):已被本步 messages 覆盖、或已在 surface 上的,跳过。
164
- const pending = desired.filter((msg) =>
165
- !messages.some((m) => sameProjectionIdentity(m, msg))
166
- && !decision.messages.some((m) => sameProjectionIdentity(m, msg))
167
- && !alreadyOnSurface(session, msg));
168
- if (!pending.length) return decision;
169
- // 折叠位置:紧跟在"本步被认领的消息"之后,使直接提示在前、驱动追加的 runtime context 在后。
170
- // 照抄 agent-instructions:lastClaimedIndex 可能为 -1(本步无认领消息),此时插到最前。
171
- const lastClaimedIndex = decision.messages.findLastIndex((m) => messages.includes(m));
172
- const entered = decision.messages.toSpliced(lastClaimedIndex + 1, 0, ...pending);
173
- return { ...decision, messages: entered };
174
- } catch (error) {
175
- // 🔴 绝不冒泡:pre-step 抛错会让整个 turn 失败(宿主契约,见文件头)。
176
- // 契约失效时表现为"记忆正文不注入",这行 warn 是唯一排查线索。
177
- console.error(
178
- `[memory-palace] projection skipped (step ${step}): ${error?.message || String(error)}`,
179
- );
180
- return decision;
181
- }
182
- });
183
- }
1
+ // memory-palace E 投影 —— 记忆正文与今日日志的独立注入通道。
2
+ //
3
+ // 背景(v1.6.3 及以前):用户级 + 项目级 MEMORY.md 走 systemPrompt.section,受
4
+ // 「仅首次注入」限制(`alreadyInjected` 打了标记就不撕,只有 compaction 清),
5
+ // 导致记忆正文在 turn 1 的 step 2 之后就消失、全程不再进场。
6
+ //
7
+ // 现在:记忆正文改经 `agent/pre-step` 投影为**独立 user 消息**,常驻对话历史:
8
+ // - 每个 step 都可见(消息在历史里,不依赖注入时机);
9
+ // - v1.7.1 起去重判据由「全文深比较」改为「**文件身份**」(首行 heading,含路径 + 日期):
10
+ // 同一文件只注一次,内容再变也不重注 —— 记忆正文由 agent 自己写入,其内容本就在
11
+ // 上下文(tool/call 参数)里,重注纯属冗余;换文件(跨日期日志)才视为新身份注入;
12
+ // - v1.7.1 起**今日工作日志由 systemPrompt.section 迁入本通道**。日志是 agent 唯一
13
+ // 「不知道自己缺」的内容(hybrid 子代理以独立 LLM stream 写入、不回注主对话),
14
+ // 必须注入;迁出后 section 只剩恒定指令 → system prompt 恒定 → 前缀缓存永久有效
15
+ // (此前日志在 section 里每步求值,一变就让序列最前的整段前缀作废)。
16
+ // - compaction 把消息移出 surface 后,身份判据即判为缺失 → 自动重注**磁盘最新版**,
17
+ // 顺带完成一次「刷新到最新」。
18
+ // - v1.7.2 起**静默会话整体不投影**(isSilent(session)):宿主「模式」是 agent 平面预设,
19
+ // 本插件却在 host 平面,投影对每个 agent 都生效——官方 minimal 预设(persona complete:true
20
+ // 独占 system prompt 的"裸测环境")下必须自行按会话静默,否则记忆正文仍会常驻历史吃 token。
21
+ //
22
+ // 实现照抄宿主 `packages/context/agent-instructions/src/index.ts` 三件套(API 全公开、无第一方特权):
23
+ // 1. `inject: ['sessionProjections']`(服务随 dsh-base 默认装载)
24
+ // 2. `ctx.on('agent/pre-step', async ({agent, messages, step, signal}, next) => {...})`
25
+ // 3. `createUserMessage()`(来自 @deepseek-ai/dsh-llm)
26
+ // 投递方式:同步版——折叠进 `decision.messages`(`toSpliced(lastClaimedIndex + 1, 0, ...pending)`),
27
+ // 不做异步 inbox 刷新(agent-instructions 注释明写异步投影有两个 commit 边界的时序约束,
28
+ // 本版刻意回避;记忆更新后下一步自然带上新内容)。
29
+ //
30
+ // 🔴 契约风险:pre-step 监听器抛错会让**整个 turn 失败**(宿主测试
31
+ // packages/acp/acp/tests/turns.spec.ts:150-156 锚定),故钩子内整体 try-catch,
32
+ // 任何异常降级为「本步不投影」并留一行 stderr 线索,绝不冒泡。详见 DEVELOPMENT.md §7.4。
33
+ import { join } from "node:path";
34
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
35
+ import { expandHome, toHomeShort, todayISO, readMdSync, budgetClip, stripSmartTag, stripDeletedLines } from "./common/text.mjs";
36
+
37
+ // 投影消息的 source 标注。
38
+ // v1.7.2-alpha.2(dsh 0.1.7 适配):宿主删除了通用 catch-all `plugin` kind(MessageSourceMap 不再含
39
+ // 'plugin' 键),改为「每个生产者在自己的模块里声明自有 kind」;v3→v4 会话持久化迁移把第三方
40
+ // 旧源 `{kind:'plugin',plugin:X}` 规范化为 `plugin:X`(见 session-format-v3-to-v4/sources.ts
41
+ // producerKind())。故本插件合规 kind = 'plugin:dsh-memory-palace'。运行时不校验 kind 白名单
42
+ // (createMessage 仅 deepFreeze(structuredClone)),但未知 kind 在 UI 走兜底呈现、且未来宿主
43
+ // 若加校验即炸——按迁移约定写才安全。
44
+ // - form: 'instructions' → 宿主 ContextForm 联合内的合法值(曾拟用 'memory',但其不在该联合内,
45
+ // 属类型不合法;'instructions' 语义接近"指令性内容"且类型安全,UI 按指令样式呈现)。
46
+ const PROJECTION_SOURCE = Object.freeze({
47
+ kind: "plugin:dsh-memory-palace",
48
+ form: "instructions",
49
+ });
50
+
51
+ // v1.7.1 判据:投影身份 = 消息首行 heading(已编码目标文件路径 + 日期)。
52
+ // 只比身份、不比正文 —— 同一文件投影过一次后,内容再变也不重注。
53
+ function projectionIdentity(msg) {
54
+ const text = msg?.content?.[0]?.text ?? "";
55
+ const i = text.indexOf("\n");
56
+ return i === -1 ? text : text.slice(0, i);
57
+ }
58
+
59
+ // 只在双方都是本插件投影时才比身份 —— 规避真实用户消息与宿主注入消息
60
+ // (runtime context / skill 目录等)因首行文本巧合而误撞。
61
+ // v1.7.2-alpha.2:新消息 kind='plugin:dsh-memory-palace';同时**保留旧 kind='plugin'+plugin 字段的识别**
62
+ // —— 历史会话里已持久化的旧投影消息经宿主 v3→v4 迁移会变成 plugin:dsh-memory-palace,但旧宿主
63
+ // (<0.1.7)与未迁移数据仍是旧形状,双认可防「换 kind 当天在老会话里重复注入」。
64
+ function isOwnProjection(msg) {
65
+ const s = msg?.source;
66
+ if (!s || s.form !== "instructions") return false;
67
+ if (s.kind === "plugin:dsh-memory-palace") return true;
68
+ return s.kind === "plugin" && s.plugin === "dsh-memory-palace";
69
+ }
70
+
71
+ function sameProjectionIdentity(left, right) {
72
+ return isOwnProjection(left) && isOwnProjection(right)
73
+ && projectionIdentity(left) === projectionIdentity(right);
74
+ }
75
+
76
+ // 扫描当前 surface,判断该身份是否已经作为本插件消息存在于会话历史中。
77
+ // 存在 → 不重复注入("同一文件只注一次"靠它实现)。
78
+ // compaction 把消息移出 surface 后即判为缺失 → 自动重注磁盘最新版
79
+ // (身份不变、正文刷新,故无需 injectedSessionIds 之类的标记机制)。
80
+ function alreadyOnSurface(session, desired) {
81
+ const nodes = session?.surface?.nodes;
82
+ if (!Array.isArray(nodes)) return false;
83
+ for (const seq of nodes) {
84
+ const event = typeof session.eventAt === "function" ? session.eventAt(seq) : undefined;
85
+ if (event?.type === "user/message" && sameProjectionIdentity(event.data, desired)) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ // 构造一条投影消息;内容为空(文件不存在 / 无记忆)时返回 null。
91
+ function buildProjection(text, heading) {
92
+ if (!text) return null;
93
+ return createUserMessage({
94
+ content: [{ type: "text", text: `${heading}\n${text}` }],
95
+ source: PROJECTION_SOURCE,
96
+ });
97
+ }
98
+
99
+ /**
100
+ * 收集本步要投影的消息(用户级 / 项目级 MEMORY.md / 今日工作日志,各自独立一条)。
101
+ * 拆成多条独立消息(v1.7.0 定案,v1.7.1 扩展):否则"某一条内容一改,其余连带重发"。
102
+ * v1.7.2:新增 `session` + `isSilent` —— 静默会话(官方 minimal 等预设)整体不投影。
103
+ * @param {{ getConfig: () => object, paths: object, session?: object, isSilent?: (session?: object) => boolean }} deps
104
+ * @returns {object[]} 按 [用户级, 项目级…, 今日日志…] 顺序的消息数组(可能为空)
105
+ */
106
+ export function buildProjections({ getConfig, paths, session, isSilent }) {
107
+ const cfg = getConfig();
108
+ if (!cfg.enabled) return [];
109
+ // v1.7.2:静默会话(极简模式等)不投影——投影消息是**常驻历史**,比 section 更"重":
110
+ // 它要一路吃 token 直到 compaction,正是"裸测环境"最不该有的东西。
111
+ if (isSilent?.(session)) return [];
112
+ const out = [];
113
+ // 用户级 MEMORY.md
114
+ const userFile = expandHome(cfg.userMemoryPath);
115
+ const userText = readMdSync(userFile);
116
+ if (userText) {
117
+ const clipped = stripSmartTag(stripDeletedLines(budgetClip(userText, cfg.userBudgetChars)));
118
+ const msg = buildProjection(clipped, `# 用户级记忆 (${toHomeShort(userFile)})`);
119
+ if (msg) out.push(msg);
120
+ }
121
+ // 项目级 MEMORY.md:按 readDirs 顺序(dsh 优先叠加 buddy),同一文件路径只取一次。
122
+ // 显式透传当前会话 cwd,避免依赖闭包 activeCwd 造成多会话并发/切换工作区时记忆串台
123
+ const seen = new Set();
124
+ const cwd = session?.header?.cwd;
125
+ for (const dir of paths.readDirs(cwd)) {
126
+ for (const file of paths.memoryReadCandidates(dir, cwd)) {
127
+ if (seen.has(file)) continue;
128
+ seen.add(file);
129
+ const text = readMdSync(file);
130
+ if (!text) continue;
131
+ const clipped = stripSmartTag(stripDeletedLines(budgetClip(text, cfg.workspaceBudgetChars)));
132
+ const msg = buildProjection(clipped, `# 项目级记忆 (${toHomeShort(file)})`);
133
+ if (msg) out.push(msg);
134
+ }
135
+ }
136
+ // 今日工作日志(v1.7.1 由 systemPrompt.section 迁入)。heading 沿用原 section 格式,
137
+ // 天然含日期与目录 → 跨天 / 换目录自动成为新身份。预算沿用 workspaceBudgetChars,
138
+ // 与项目级 MEMORY.md 各自独立 budgetClip,互不挤占。
139
+ for (const dir of paths.readDirs(cwd)) {
140
+ const text = readMdSync(join(dir, `${todayISO()}.md`));
141
+ if (!text) continue;
142
+ const clipped = stripSmartTag(stripDeletedLines(budgetClip(text, cfg.workspaceBudgetChars)));
143
+ const msg = buildProjection(clipped, `# 今日工作日志 (${todayISO()} @ ${toHomeShort(dir)})`);
144
+ if (msg) out.push(msg);
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * 注册 E 投影钩子。
151
+ * @param {{ ctx: object, getConfig: () => object, paths: object, isSilent?: (session?: object) => boolean }} deps
152
+ */
153
+ export function registerProjection({ ctx, getConfig, paths, isSilent }) {
154
+ ctx.on("agent/pre-step", async ({ agent, messages, step, signal }, next) => {
155
+ // 先让瀑布流继续,拿到宿主与其他插件合成后的决策——投影要折叠进它的 messages。
156
+ const decision = await next();
157
+ try {
158
+ signal?.throwIfAborted?.();
159
+ if (decision.kind !== "enter") return decision;
160
+ const session = agent?.session;
161
+ // v1.7.2:静默会话(极简模式等)不投影。放在 next() 之后、构造之前,成本最低。
162
+ if (isSilent?.(session)) return decision;
163
+ const desired = buildProjections({ getConfig, paths, session, isSilent });
164
+ if (!desired.length) return decision;
165
+ // 逐条去重(按**文件身份**、非内容):已被本步 messages 覆盖、或已在 surface 上的,跳过。
166
+ const pending = desired.filter((msg) =>
167
+ !messages.some((m) => sameProjectionIdentity(m, msg))
168
+ && !decision.messages.some((m) => sameProjectionIdentity(m, msg))
169
+ && !alreadyOnSurface(session, msg));
170
+ if (!pending.length) return decision;
171
+ // 折叠位置:紧跟在"本步被认领的消息"之后,使直接提示在前、驱动追加的 runtime context 在后。
172
+ // 照抄 agent-instructions:lastClaimedIndex 可能为 -1(本步无认领消息),此时插到最前。
173
+ const lastClaimedIndex = decision.messages.findLastIndex((m) => messages.includes(m));
174
+ const entered = decision.messages.toSpliced(lastClaimedIndex + 1, 0, ...pending);
175
+ return { ...decision, messages: entered };
176
+ } catch (error) {
177
+ // 🔴 绝不冒泡:pre-step 抛错会让整个 turn 失败(宿主契约,见文件头)。
178
+ // 契约失效时表现为"记忆正文不注入",这行 warn 是唯一排查线索。
179
+ console.error(
180
+ `[memory-palace] projection skipped (step ${step}): ${error?.message || String(error)}`,
181
+ );
182
+ return decision;
183
+ }
184
+ });
185
+ }
package/lib/tools.mjs CHANGED
@@ -11,6 +11,10 @@ import { appendLineDedup, findMatches, removeLineByMatch, recentLogDates } from
11
11
  */
12
12
  export function registerTools({ ctx, getConfig, paths, state }) {
13
13
  const cfg = () => getConfig();
14
+ // 从宿主 execute 第二参(ToolRunContext,defineTool 原样转发)/ pre-execute 事件载荷取
15
+ // 发起会话 cwd——多会话并发时 activeCwd 可能是别的工作区,串台会读写错项目。
16
+ // 缺省(undefined)回落 paths 工厂默认 = activeCwd(勿 `?? null`,null 会绕过默认参数)。
17
+ const sessionCwd = (exec) => exec?.agent?.session?.header?.cwd;
14
18
 
15
19
  // 删除作用域的中文标签。
16
20
  function levelLabel(level) {
@@ -24,7 +28,7 @@ export function registerTools({ ctx, getConfig, paths, state }) {
24
28
  // - user:用户级 MEMORY.md(~/.deepseek-harness/MEMORY.md)
25
29
  // - project:项目级 MEMORY.md(与 memory/ 同级)+ 今日每日日志
26
30
  // - daily:今日每日日志 + 最近 3 份历史每日日志(同工作区)
27
- async function deleteCandidates(level) {
31
+ async function deleteCandidates(level, cwd) {
28
32
  const c = cfg();
29
33
  const files = [];
30
34
  if (level === "user") {
@@ -32,11 +36,11 @@ export function registerTools({ ctx, getConfig, paths, state }) {
32
36
  files.push({ file, tier: "用户级", path: toHomeShort(file) });
33
37
  return files;
34
38
  }
35
- const dirs = paths.writeDirs();
39
+ const dirs = paths.writeDirs(cwd);
36
40
  for (const dir of dirs) {
37
41
  const dirShort = toHomeShort(dir);
38
42
  if (level === "project") {
39
- const mem = paths.memoryFileOf(dir);
43
+ const mem = paths.memoryFileOf(dir, cwd);
40
44
  files.push({ file: mem, tier: "项目级", path: toHomeShort(mem) });
41
45
  }
42
46
  // 每日级:今日日志必查;daily 作用域额外纳入最近 3 份历史日志
@@ -89,17 +93,18 @@ export function registerTools({ ctx, getConfig, paths, state }) {
89
93
  },
90
94
  render: (_args, value) => [{ type: "text", text: value?.message ?? "Saved." }],
91
95
  },
92
- async execute(args) {
96
+ async execute(args, exec) {
93
97
  state.recentAgentWrote = true;
94
98
  if (!cfg().enabled) return { ok: false, message: "memory-palace is currently disabled in settings." };
95
- const dirs = paths.writeDirs();
99
+ const cwd = sessionCwd(exec);
100
+ const dirs = paths.writeDirs(cwd);
96
101
  if (!dirs.length) return { ok: false, message: "No active workspace." };
97
102
  try {
98
103
  const line = `- ${args.content}`;
99
104
  let wrote = 0;
100
105
  let skipped = 0;
101
106
  for (const dir of dirs) {
102
- const mem = paths.memoryFileOf(dir);
107
+ const mem = paths.memoryFileOf(dir, cwd);
103
108
  if (await appendLineDedup(mem, line)) wrote++;
104
109
  else skipped++;
105
110
  }
@@ -202,9 +207,10 @@ export function registerTools({ ctx, getConfig, paths, state }) {
202
207
  },
203
208
  render: (_args, value) => [{ type: "text", text: value?.memory ?? value?.message ?? "No memory." }],
204
209
  },
205
- async execute(args = {}) {
210
+ async execute(args = {}, exec) {
206
211
  const c = cfg();
207
212
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings.", memory: "" };
213
+ const cwd = sessionCwd(exec);
208
214
  // v1.7.1:默认 scope='memory'(长期记忆)。刻意不设 'all' —— 默认全量会让 agent
209
215
  // 不加思考地把日志(实测单日可达 1.7 万字符)一起吞下,白白吃掉上下文。
210
216
  const scope = args?.scope ?? "memory";
@@ -223,13 +229,13 @@ export function registerTools({ ctx, getConfig, paths, state }) {
223
229
  const u = readMdSync(userFile);
224
230
  if (u) blocks.push(`# 用户级记忆 (${toHomeShort(userFile)})\n${u}`);
225
231
  }
226
- for (const dir of paths.readDirs()) {
232
+ for (const dir of paths.readDirs(cwd)) {
227
233
  const dirShort = toHomeShort(dir);
228
234
  if (wantProject) {
229
235
  // 标题必须用实际读到的 MEMORY.md 路径,而非日志目录 —— dsh 布局下 MEMORY.md 位于
230
236
  // 日志目录的上一级,旧实现拿 dir(.../memory)当标题会误导真实路径(buddy 布局
231
237
  // MEMORY.md 恰在 memory/ 内所以未暴露)。与 E 投影的 per-file 粒度对齐。
232
- for (const f of paths.memoryReadCandidates(dir)) {
238
+ for (const f of paths.memoryReadCandidates(dir, cwd)) {
233
239
  const t = readMdSync(f);
234
240
  if (t) blocks.push(`# 工作区记忆 (${toHomeShort(f)})\n${t}`);
235
241
  }
@@ -328,7 +334,7 @@ export function registerTools({ ctx, getConfig, paths, state }) {
328
334
  render: (_args, value) =>
329
335
  [{ type: "text", text: value?.confirmPrompt || value?.message || (value?.preview ? "Preview only — no deletion performed." : "Deleted.") }],
330
336
  },
331
- async execute(args) {
337
+ async execute(args, exec) {
332
338
  state.recentAgentWrote = true;
333
339
  const c = cfg();
334
340
  if (!c.enabled) {
@@ -343,7 +349,7 @@ export function registerTools({ ctx, getConfig, paths, state }) {
343
349
  return { ok: false, preview: false, level, removed: 0, matches: [], details: [], message: "match 不能为空:请提供要删除的记忆文本(子串)。", confirmPrompt: "" };
344
350
  }
345
351
 
346
- const candidates = await deleteCandidates(level);
352
+ const candidates = await deleteCandidates(level, sessionCwd(exec));
347
353
  if (!candidates.length && level !== "user") {
348
354
  return { ok: false, preview: false, level, removed: 0, matches: [], details: [], message: "当前没有活动工作区,无法定位项目/每日记忆文件。", confirmPrompt: "" };
349
355
  }
@@ -415,7 +421,7 @@ export function registerTools({ ctx, getConfig, paths, state }) {
415
421
  const match = (a.match || "").trim();
416
422
  if (!match) return next();
417
423
  try {
418
- const candidates = await deleteCandidates(level);
424
+ const candidates = await deleteCandidates(level, sessionCwd(exec));
419
425
  const matches = [];
420
426
  for (const cand of candidates) {
421
427
  const found = findMatches(cand.file, match, cand.tier, cand.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-palace",
3
- "version": "1.7.2-alpha.2",
3
+ "version": "1.7.2",
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",