@max-null/dsh-memory 0.7.1 → 0.7.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/dist/engine.d.ts +22 -1
- package/dist/engine.js +47 -1
- package/dist/index.js +28 -3
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -218,6 +218,8 @@ export declare class MemoryEngine extends Service {
|
|
|
218
218
|
private globalTable?;
|
|
219
219
|
/** project 域按工作区 cwd 懒打开 + 缓存(多会话并发各工作区独立)。 */
|
|
220
220
|
private projectTables;
|
|
221
|
+
/** 进行中的工作区打开(key → promise):并发调用复用同一次,见 {@link projectTableFor}。 */
|
|
222
|
+
private readonly projectOpenings;
|
|
221
223
|
private projectFacilities;
|
|
222
224
|
/** backend 只注册一次(registry 重名抛 duplicate;reload 清缓存后不得重复注册)。 */
|
|
223
225
|
private registeredProjectBackends;
|
|
@@ -236,15 +238,34 @@ export declare class MemoryEngine extends Service {
|
|
|
236
238
|
private projectRootFor;
|
|
237
239
|
/** 稳定 backend 名(registry 重名抛错,按 cwd hash 唯一化;只允许 [a-z0-9_],domain 名校验)。 */
|
|
238
240
|
private projectBackendName;
|
|
241
|
+
/**
|
|
242
|
+
* 工作区表是否已经打开(同步判断)。组装路径用它决定「要不要等一次预热」——
|
|
243
|
+
* 表已打开是常态,那时这条路径不该付出任何异步代价。
|
|
244
|
+
*/
|
|
245
|
+
projectOpen(projectCwd?: string): boolean;
|
|
239
246
|
/**
|
|
240
247
|
* 按工作区 cwd 取 project 表(懒打开 + 缓存)。无 cwd(未选工作区)
|
|
241
248
|
* 返回 undefined——调用方(工具/面板)按此跳过 project 部分。
|
|
249
|
+
*
|
|
250
|
+
* **打开是异步的,调用方却会并发**:预热是 fire-and-forget,注入路径、工具路径、
|
|
251
|
+
* 面板路径各来一次。底层 unit 句柄只允许一个,两次并发打开会让后到者以
|
|
252
|
+
* `unit '…' is already open; a unit has exactly one live handle` 失败——而调用方
|
|
253
|
+
* 普遍把预热失败当无碍吞掉,于是表现为「这一轮工作区记忆缺席」。所以这里复用
|
|
254
|
+
* 进行中的那次打开。
|
|
242
255
|
*/
|
|
243
256
|
private projectTableFor;
|
|
257
|
+
/** {@link projectTableFor} 的实际打开步骤;并发去重由调用方负责。 */
|
|
258
|
+
private openProjectTable;
|
|
244
259
|
/**
|
|
245
260
|
* 强制重载存储:关闭全局域与全部已开工作区域后重开(unit 释放后
|
|
246
261
|
* backend 重读文件),放弃内存缓存。供外部编辑记忆文件后刷新
|
|
247
|
-
* (JsonStorageBackend 打开时加载一次,无 watch——2026-08-19
|
|
262
|
+
* (JsonStorageBackend 打开时加载一次,无 watch——2026-08-19 实测);
|
|
263
|
+
* 多实例写入协调(0.7.1)让它在每次「磁盘被别人改过」时自动触发。
|
|
264
|
+
*
|
|
265
|
+
* **已打开的工作区要重开,而不是一关了之**:注入 provider 是同步回调、只读
|
|
266
|
+
* 已打开的表,关掉就等同于「这个工作区没有常驻记忆」——工作区记忆会从上下文里
|
|
267
|
+
* 静默消失,直到某次工具调用把它重新打开。重开的表同样从磁盘重读,
|
|
268
|
+
* 「放弃内存缓存」这条语义不变,改变的只是「重读完别忘了装回去」。
|
|
248
269
|
*/
|
|
249
270
|
reload(): Promise<void>;
|
|
250
271
|
/**
|
package/dist/engine.js
CHANGED
|
@@ -166,6 +166,8 @@ export class MemoryEngine extends Service {
|
|
|
166
166
|
globalTable;
|
|
167
167
|
/** project 域按工作区 cwd 懒打开 + 缓存(多会话并发各工作区独立)。 */
|
|
168
168
|
projectTables = new Map();
|
|
169
|
+
/** 进行中的工作区打开(key → promise):并发调用复用同一次,见 {@link projectTableFor}。 */
|
|
170
|
+
projectOpenings = new Map();
|
|
169
171
|
projectFacilities = new Map();
|
|
170
172
|
/** backend 只注册一次(registry 重名抛 duplicate;reload 清缓存后不得重复注册)。 */
|
|
171
173
|
registeredProjectBackends = new Set();
|
|
@@ -211,9 +213,24 @@ export class MemoryEngine extends Service {
|
|
|
211
213
|
h = ((h << 5) + h + text.charCodeAt(i)) | 0;
|
|
212
214
|
return `memory_project_${Math.abs(h).toString(36)}`;
|
|
213
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* 工作区表是否已经打开(同步判断)。组装路径用它决定「要不要等一次预热」——
|
|
218
|
+
* 表已打开是常态,那时这条路径不该付出任何异步代价。
|
|
219
|
+
*/
|
|
220
|
+
projectOpen(projectCwd) {
|
|
221
|
+
if (projectCwd === undefined || projectCwd === '')
|
|
222
|
+
return false;
|
|
223
|
+
return this.projectTables.has(join(projectCwd));
|
|
224
|
+
}
|
|
214
225
|
/**
|
|
215
226
|
* 按工作区 cwd 取 project 表(懒打开 + 缓存)。无 cwd(未选工作区)
|
|
216
227
|
* 返回 undefined——调用方(工具/面板)按此跳过 project 部分。
|
|
228
|
+
*
|
|
229
|
+
* **打开是异步的,调用方却会并发**:预热是 fire-and-forget,注入路径、工具路径、
|
|
230
|
+
* 面板路径各来一次。底层 unit 句柄只允许一个,两次并发打开会让后到者以
|
|
231
|
+
* `unit '…' is already open; a unit has exactly one live handle` 失败——而调用方
|
|
232
|
+
* 普遍把预热失败当无碍吞掉,于是表现为「这一轮工作区记忆缺席」。所以这里复用
|
|
233
|
+
* 进行中的那次打开。
|
|
217
234
|
*/
|
|
218
235
|
async projectTableFor(projectCwd) {
|
|
219
236
|
if (projectCwd === undefined || projectCwd === '')
|
|
@@ -222,6 +239,20 @@ export class MemoryEngine extends Service {
|
|
|
222
239
|
const cached = this.projectTables.get(key);
|
|
223
240
|
if (cached !== undefined)
|
|
224
241
|
return cached;
|
|
242
|
+
const inFlight = this.projectOpenings.get(key);
|
|
243
|
+
if (inFlight !== undefined)
|
|
244
|
+
return await inFlight;
|
|
245
|
+
const opening = this.openProjectTable(key);
|
|
246
|
+
this.projectOpenings.set(key, opening);
|
|
247
|
+
try {
|
|
248
|
+
return await opening;
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
this.projectOpenings.delete(key);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** {@link projectTableFor} 的实际打开步骤;并发去重由调用方负责。 */
|
|
255
|
+
async openProjectTable(key) {
|
|
225
256
|
const backendName = this.projectBackendName(key);
|
|
226
257
|
// 0.5.2:旧实现 domain 名多一层 memory_project_ 前缀(文件名双重前缀,
|
|
227
258
|
// 且 unit 文件头 name 同为旧名)。打开前统一迁移到规范名:
|
|
@@ -262,15 +293,30 @@ export class MemoryEngine extends Service {
|
|
|
262
293
|
/**
|
|
263
294
|
* 强制重载存储:关闭全局域与全部已开工作区域后重开(unit 释放后
|
|
264
295
|
* backend 重读文件),放弃内存缓存。供外部编辑记忆文件后刷新
|
|
265
|
-
* (JsonStorageBackend 打开时加载一次,无 watch——2026-08-19
|
|
296
|
+
* (JsonStorageBackend 打开时加载一次,无 watch——2026-08-19 实测);
|
|
297
|
+
* 多实例写入协调(0.7.1)让它在每次「磁盘被别人改过」时自动触发。
|
|
298
|
+
*
|
|
299
|
+
* **已打开的工作区要重开,而不是一关了之**:注入 provider 是同步回调、只读
|
|
300
|
+
* 已打开的表,关掉就等同于「这个工作区没有常驻记忆」——工作区记忆会从上下文里
|
|
301
|
+
* 静默消失,直到某次工具调用把它重新打开。重开的表同样从磁盘重读,
|
|
302
|
+
* 「放弃内存缓存」这条语义不变,改变的只是「重读完别忘了装回去」。
|
|
266
303
|
*/
|
|
267
304
|
async reload() {
|
|
305
|
+
// 进行中的打开先落地:否则它们会在下面的 clear 之后把表塞回已清空的映射,
|
|
306
|
+
// 留下「表在映射里但 facility 已关」的半死状态。
|
|
307
|
+
await Promise.allSettled([...this.projectOpenings.values()]);
|
|
308
|
+
const opened = [...this.projectTables.keys()];
|
|
268
309
|
await this.facility?.closeAll();
|
|
269
310
|
for (const facility of this.projectFacilities.values())
|
|
270
311
|
await facility.closeAll();
|
|
271
312
|
this.projectTables.clear();
|
|
272
313
|
this.projectFacilities.clear();
|
|
273
314
|
await this.openGlobalFacility();
|
|
315
|
+
for (const key of opened) {
|
|
316
|
+
// 重开失败 = 该工作区退回「未打开」(本轮注入缺这部分,下次访问重试),
|
|
317
|
+
// 不该让触发重载的那一整次写入跟着失败。
|
|
318
|
+
await this.projectTableFor(key).catch(() => undefined);
|
|
319
|
+
}
|
|
274
320
|
}
|
|
275
321
|
/**
|
|
276
322
|
* Create one record. 低危内容直接 `approved`(静默生效,2026-09-15 静默记忆机制);
|
package/dist/index.js
CHANGED
|
@@ -159,6 +159,8 @@ export async function apply(ctx, config) {
|
|
|
159
159
|
order: 49,
|
|
160
160
|
text: SELF_DESCRIPTION,
|
|
161
161
|
});
|
|
162
|
+
const injectionBudget = config?.injectionBudget ?? DEFAULT_INJECTION_BUDGET;
|
|
163
|
+
const summaryChars = config?.summaryChars ?? DEFAULT_SUMMARY_CHARS;
|
|
162
164
|
ctx.systemPrompt.context({
|
|
163
165
|
name: 'memory:recall',
|
|
164
166
|
order: 50,
|
|
@@ -166,11 +168,34 @@ export async function apply(ctx, config) {
|
|
|
166
168
|
const cwd = assemblyProjectCwd(assembly);
|
|
167
169
|
if (cwd !== undefined)
|
|
168
170
|
void memory.ensureProjectOpen(cwd).catch(() => { });
|
|
169
|
-
|
|
170
|
-
const summaryChars = config?.summaryChars ?? DEFAULT_SUMMARY_CHARS;
|
|
171
|
-
return recallText(memory, cwd, budget, summaryChars);
|
|
171
|
+
return recallText(memory, cwd, injectionBudget, summaryChars);
|
|
172
172
|
},
|
|
173
173
|
});
|
|
174
|
+
// 上面那条 provider 是**同步**的(`SystemPrompt.context` 的 `text` 类型就是
|
|
175
|
+
// `string | ((context) => string)`),而工作区表是异步打开的:重启后首次组装时
|
|
176
|
+
// 它还没打开,工作区记忆会整整缺席一轮(等预热完成,第二轮才回来)。
|
|
177
|
+
// `system-prompt/assemble` 是异步 waterfall、文档写明「返回值权威」,在这里补一次。
|
|
178
|
+
//
|
|
179
|
+
// **不能拿「此刻表是否打开」当作「provider 渲染时表是否打开」的判据**:provider 里那次
|
|
180
|
+
// 预热的 Promise 可能在 `next()` 的 await 期间就完成了,于是这里看到「已打开」而跳过,
|
|
181
|
+
// 而 provider 渲染时它还是空的。所以这里总是重渲染一遍——`renderInjection` 是纯函数、
|
|
182
|
+
// 读的是内存表,代价可忽略;文本没变就原样返回,不动 downstream 看到的对象。
|
|
183
|
+
ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
184
|
+
const assembled = await next();
|
|
185
|
+
const cwd = assemblyProjectCwd(context);
|
|
186
|
+
if (cwd === undefined || cwd === '')
|
|
187
|
+
return assembled;
|
|
188
|
+
if (!memory.projectOpen(cwd))
|
|
189
|
+
await memory.ensureProjectOpen(cwd).catch(() => undefined);
|
|
190
|
+
const text = recallText(memory, cwd, injectionBudget, summaryChars);
|
|
191
|
+
const current = assembled.contexts.find(entry => entry.name === 'memory:recall');
|
|
192
|
+
if (current === undefined || current.text === text)
|
|
193
|
+
return assembled;
|
|
194
|
+
return {
|
|
195
|
+
...assembled,
|
|
196
|
+
contexts: assembled.contexts.map(entry => entry.name === 'memory:recall' ? { ...entry, text } : entry),
|
|
197
|
+
};
|
|
198
|
+
});
|
|
174
199
|
// 会话创建即预热对应工作区的 project 表(0.5.1):让首轮模型请求就
|
|
175
200
|
// 能注入工作区常驻记忆(provider 同步只读已打开的表)。
|
|
176
201
|
// session/created 事件由 dsh-session 提供(可选依赖:无 session 服务
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Cross-session plaintext memory plugin for the DeepSeek Harness — deterministic BM25 recall, writes take effect immediately, signal-driven injection, no embeddings",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|