@modusensus/dsh-mneme 0.8.4 → 0.8.5
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 +16 -7
- package/bin/dsh-mneme-mcp.mjs +11 -3
- package/lib/api-standalone.js +11 -0
- package/lib/api.js +21 -0
- package/lib/client.js +213 -31
- package/lib/config.js +82 -0
- package/lib/document.js +221 -0
- package/lib/dream/sleep.js +6 -4
- package/lib/dream.js +1347 -1319
- package/lib/guide.js +44 -0
- package/lib/heat.js +2 -0
- package/lib/inject.js +91 -15
- package/lib/lang.js +6 -0
- package/lib/organize.js +450 -0
- package/lib/recall-stats.js +23 -1
- package/lib/runtime/layout.js +30 -0
- package/lib/runtime/loader.js +4 -1
- package/lib/runtime/verify.js +19 -6
- package/lib/search/adaptive.js +38 -0
- package/lib/service.js +165 -12
- package/lib/settings.js +31 -2
- package/lib/store.js +109 -8
- package/lib/summarize.js +253 -8
- package/lib/tools.js +136 -8
- package/package.json +6 -6
- package/runtime-manifest.json +104 -174
- package/scripts/build-runtime-manifest.mjs +8 -1
- package/scripts/sync-test-badge.mjs +18 -12
- package/src/api-standalone.js +11 -0
- package/src/api.js +21 -0
- package/src/config.js +82 -0
- package/src/document.js +221 -0
- package/src/dream/sleep.js +6 -4
- package/src/dream.js +1347 -1319
- package/src/guide.js +44 -0
- package/src/heat.js +2 -0
- package/src/inject.js +91 -15
- package/src/lang.js +6 -0
- package/src/organize.js +450 -0
- package/src/recall-stats.js +23 -1
- package/src/runtime/layout.js +30 -0
- package/src/runtime/loader.js +4 -1
- package/src/runtime/verify.js +19 -6
- package/src/search/adaptive.js +38 -0
- package/src/service.js +165 -12
- package/src/settings.js +31 -2
- package/src/store.js +109 -8
- package/src/summarize.js +253 -8
- package/src/tools.js +136 -8
- package/test/adaptive-inject-budget.test.js +85 -0
- package/test/api.test.js +65 -3
- package/test/client.test.js +63 -2
- package/test/document.test.js +523 -0
- package/test/dream.test.js +51 -0
- package/test/inject-content-cap.test.js +5 -3
- package/test/inject-pools.test.js +170 -0
- package/test/inject-recall.test.js +105 -0
- package/test/inject.test.js +35 -1
- package/test/llm-audit.test.js +153 -0
- package/test/organize.test.js +418 -0
- package/test/recall-stats.test.js +7 -1
- package/test/runtime-loader.test.js +18 -0
- package/test/runtime-manifest.test.js +2 -1
- package/test/runtime-verify.test.js +65 -0
- package/test/sleep.test.js +37 -0
- package/test/store.test.js +13 -0
- package/test/summarize.test.js +407 -3
- package/test/tools-dto-schema-matrix.test.js +325 -0
- package/test/tools.test.js +36 -3
package/src/guide.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// #249(第一批):能力说明——写给模型的「怎么用这套记忆」,不是给用户的文案。
|
|
2
|
+
//
|
|
3
|
+
// 为什么英文、且只留一份正本:注入指引的参照实现全部用英文(ACP 的
|
|
4
|
+
// ACP_SYSTEM_PROMPT + HOW_TO_COMPRESS_RULES、mnemon 的 ROUTING_GUIDANCE、
|
|
5
|
+
// 宿主压缩摘要规则),仓库既有先例也是 src/tools.js 的工具描述硬编码英文。
|
|
6
|
+
// src/lang.js 的 memory.language 管的是「生成出来的记忆内容与块内标题」,与本
|
|
7
|
+
// 模块是两件事,故不并入 STR、不做 zh/en 双写(双写只会让两份文本日后漂移)。
|
|
8
|
+
//
|
|
9
|
+
// 两个承载位(#249 §5):①工具描述——常驻、零注入成本,放「何时用这个工具」
|
|
10
|
+
// 这类单工具指引(TOOL_GUIDE);②系统提示段 order 150——放工具描述装不下的
|
|
11
|
+
// 总则(优先序、何时查 / 何时写 / 何时 no-op)。该段必须同会话内稳定,否则
|
|
12
|
+
// 每轮变化会作废其后的前缀缓存,因此这里全是常量,不含任何运行时插值。
|
|
13
|
+
//
|
|
14
|
+
// 前缀 `[dsh-mneme memory]` 是机械校验用的:测试按它断言「只在开时出现、且不
|
|
15
|
+
// 逐轮复读」,也让维护者能一眼认出这段文本的归属。
|
|
16
|
+
|
|
17
|
+
const SECTION_LINES = [
|
|
18
|
+
"[dsh-mneme memory] Read once; it applies to every turn of this session.",
|
|
19
|
+
"1. Precedence: the current instruction and the repository's actual state outrank any stored memory. " +
|
|
20
|
+
"When a memory contradicts either, check the instruction or the repository itself before relying on it — memory_search " +
|
|
21
|
+
"searches stored memories only, so it finds earlier context, never the present state. Never treat an old memory as current fact.",
|
|
22
|
+
"2. Recall on demand: call memory_search when the task depends on earlier decisions, user preferences, or project history " +
|
|
23
|
+
"that is not already in context. Do not search for facts you can read directly from the repository.",
|
|
24
|
+
"3. Write back sparingly: use memory_save for durable, cross-session value — a preference, a decision with its rationale, " +
|
|
25
|
+
"an engineering constraint, a pitfall with its root cause. Trivial single-turn work is not worth a memory.",
|
|
26
|
+
"4. When unsure, do nothing. Not acting is a valid outcome: a useless memory is paid for by every future session.",
|
|
27
|
+
"5. Prefer the reversible tools. memory_archive hides an entry from lists, search, injection and consolidation, and " +
|
|
28
|
+
"memory_forget suppresses it from injection, search results and lists — both are recoverable. memory_delete is permanent, " +
|
|
29
|
+
"so reach for it only when an entry is wrong or unwanted, not merely stale."
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** 系统提示段(order 150)的总则文本。常量:同会话内稳定是硬约束。 */
|
|
33
|
+
export const MEMORY_GUIDE_SECTION = SECTION_LINES.join("\n");
|
|
34
|
+
|
|
35
|
+
/** 单工具判断指引,追加到对应工具描述尾部(`injectGuidanceEnabled` 开启时)。 */
|
|
36
|
+
export const TOOL_GUIDE = {
|
|
37
|
+
memory_search:
|
|
38
|
+
" Use this when the task depends on earlier decisions, preferences, or project history that is not already in context, " +
|
|
39
|
+
"or to look for a newer memory behind one that seems stale. " +
|
|
40
|
+
"Skip it for facts you can read directly from the repository.",
|
|
41
|
+
memory_save:
|
|
42
|
+
" Save only durable, cross-session value (a preference, a decision with its rationale, an engineering constraint, " +
|
|
43
|
+
"a pitfall with its root cause). Trivial single-turn work does not belong here, and when unsure, do not save."
|
|
44
|
+
};
|
package/src/heat.js
CHANGED
|
@@ -16,6 +16,8 @@ export const TYPE_DECAY_DEFAULTS = Object.freeze({
|
|
|
16
16
|
preference: 0, // 免疫:用户画像需长期保持
|
|
17
17
|
pattern: 0, // 免疫:发现型稳定规律
|
|
18
18
|
summary: 0, // 免疫:已是压缩产物
|
|
19
|
+
document: 0, // 免疫:指针行(#230)——全文在文件系统,行失效靠 supersede
|
|
20
|
+
// 记账而非遗忘;遗忘曲线灭掉指针 = 按需读全文的入口消失
|
|
19
21
|
project: 0.0008, // 慢衰减
|
|
20
22
|
decision: 0.002, // 中速衰减
|
|
21
23
|
history: 0.006, // 较快(会话摘要不断被合并)
|
package/src/inject.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { createScopeResolver } from "./scope.js";
|
|
2
2
|
import { createHotMemory } from "./hot-memory.js";
|
|
3
3
|
import { STR, langOf } from "./lang.js";
|
|
4
|
+
import { MEMORY_GUIDE_SECTION } from "./guide.js";
|
|
5
|
+
import { adaptiveInjectBudget } from "./search/adaptive.js";
|
|
6
|
+
|
|
7
|
+
// Issue #179:注入预览的数据底座。systemPrompt 渲染是同步回调,面板只能事后
|
|
8
|
+
// 拉取,所以在这里旁路缓存「最近一帧组装」——快照就是本次渲染用过的同一份
|
|
9
|
+
// candidates/hotText/body,不重新检索、不二次组装,零额外开销。模块级单例:
|
|
10
|
+
// 注入器全局唯一(index.js 仅在 autoInject 开启时注册一个),面板经 api.js 的
|
|
11
|
+
// 只读 getter 读取;autoInject 关闭时注入器不存在,快照保持 null——「预览
|
|
12
|
+
// 不可用」本身就是「注入关闭」的诚实呈现。
|
|
13
|
+
let injectionSnapshot = null;
|
|
14
|
+
export function getInjectionSnapshot() {
|
|
15
|
+
return injectionSnapshot;
|
|
16
|
+
}
|
|
4
17
|
|
|
5
18
|
// Best-effort extraction of the current user's latest message text from the
|
|
6
19
|
// live session, for semantic-first injection (Bug4). The system-prompt
|
|
@@ -92,7 +105,7 @@ function extractRounds(ctx, maxRounds) {
|
|
|
92
105
|
|
|
93
106
|
export function createInjector(ctx, service, settings, config) {
|
|
94
107
|
const language = langOf(config);
|
|
95
|
-
const
|
|
108
|
+
const baseMaxItems = config.maxInjectedItems ?? 5;
|
|
96
109
|
const threshold = config.importanceThreshold ?? 3;
|
|
97
110
|
// Issue #205:注入位跨轮轮换。rotationTurns = 最近 N 个「不同用户查询」轮次
|
|
98
111
|
// 注入过的记忆本轮不再优先(0 = 关闭,保持既有行为)。历史按会话维护——
|
|
@@ -143,13 +156,18 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
143
156
|
// stale 1500) and an entry that would exceed it collapses to its title only.
|
|
144
157
|
const maxContent = config.injectContentMaxChars ?? 300;
|
|
145
158
|
const MAX_BLOCK = Math.max(1500, maxContent + 600);
|
|
159
|
+
// #249 第一批:pin 条目的正文硬顶。逐字保真不等于无界——单条上限取
|
|
160
|
+
// injectContentMaxChars 与 2000 的较大者,默认档下约束/偏好不会因常规截断
|
|
161
|
+
// 失真,同时给「一条超长约束每轮吃满常驻段」留一道闸(超顶仍带截断提示)。
|
|
162
|
+
const PINNED_CONTENT_MAX = Math.max(maxContent, 2000);
|
|
146
163
|
|
|
147
|
-
// Compressed injection (v0.5.0 2.1): a sleep-demoted row
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
164
|
+
// Compressed injection (v0.5.0 2.1): a sleep-demoted row carries its summary in
|
|
165
|
+
// `content` with the original parked in `_full_content` — inject the summary
|
|
166
|
+
// verbatim instead of re-truncating the (already short) text. 但「降级过」不等于
|
|
167
|
+
// 「天然够短」:原先那条 `_full_content` 早返回的唯一效果就是**跳过 maxLength**
|
|
168
|
+
// (两条分支取的都是 m.content),于是 pin 的硬顶在这一路上形同不存在(#266 评审)。
|
|
169
|
+
// 现在只有一条截断路径:超限照旧带提示,原文永远可通过 memory_get 取回。
|
|
151
170
|
function injectMemory(m, maxLength = maxContent) {
|
|
152
|
-
if (m?._full_content) return String(m.content ?? "");
|
|
153
171
|
const text = String(m?.content ?? "");
|
|
154
172
|
if (text.length <= maxLength) return text;
|
|
155
173
|
return `${text.slice(0, maxLength)}…${STR.truncatedHint[language](maxLength, text.length, m.id)}`;
|
|
@@ -192,26 +210,41 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
192
210
|
return escapePromptVars(STR.hotHeader[language](rounds.length, body));
|
|
193
211
|
}
|
|
194
212
|
|
|
195
|
-
function render(candidates) {
|
|
213
|
+
function render(candidates, pinnedStats = null) {
|
|
196
214
|
if (!candidates.length) return "";
|
|
197
215
|
const header = STR.memoryHeader[language];
|
|
198
216
|
const lines = [header];
|
|
199
217
|
let budget = MAX_BLOCK - header.length;
|
|
200
|
-
|
|
218
|
+
// #249 第一批:pin 池(约束/偏好)排在块头且不参与块的塌缩预算——「与情景
|
|
219
|
+
// 日志抢同一份预算后被降级」正是本议题要修的结构缺陷。单条仍有硬顶
|
|
220
|
+
// (PINNED_CONTENT_MAX),超出照旧带截断提示,不静默。
|
|
221
|
+
const pinnedCount = Math.max(0, Math.min(candidates.length, pinnedStats?.shown ?? 0));
|
|
222
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
223
|
+
const m = candidates[i];
|
|
224
|
+
const pinned = i < pinnedCount;
|
|
201
225
|
// Epistemic trust (v0.4.5): when enabled, measured observations are
|
|
202
226
|
// flagged so the agent can weigh them above guesses/opinions.
|
|
203
227
|
const verified = config.trustEpistemicWeighting === true && m.epistemic_status === "observation"
|
|
204
228
|
? STR.verified[language]
|
|
205
229
|
: "";
|
|
206
230
|
const title = STR.entryTitle[language](m.title, m.importance);
|
|
207
|
-
const content = injectMemory(m);
|
|
231
|
+
const content = injectMemory(m, pinned ? PINNED_CONTENT_MAX : maxContent);
|
|
208
232
|
const full = STR.entryLine[language](m.type, verified, title, content);
|
|
209
|
-
if (
|
|
233
|
+
if (pinned) {
|
|
234
|
+
// pin 不扣块预算(上面那条设计注释的落地):pin 一条就够击穿 MAX_BLOCK
|
|
235
|
+
// (PINNED_CONTENT_MAX 2000 > MAX_BLOCK 1500),照扣会把 budget 压成负数,
|
|
236
|
+
// 同一轮随后的普通候选全部退化成标题行——正是本议题要修的结构缺陷。
|
|
237
|
+
lines.push(full);
|
|
238
|
+
} else if (budget - full.length >= 0) {
|
|
210
239
|
lines.push(full);
|
|
211
240
|
budget -= full.length;
|
|
212
241
|
} else {
|
|
213
242
|
lines.push(`- [${m.type}] ${verified}${title}`);
|
|
214
243
|
}
|
|
244
|
+
if (pinned && i === pinnedCount - 1) {
|
|
245
|
+
const hidden = Math.max(0, pinnedStats?.suppressed ?? 0);
|
|
246
|
+
if (hidden > 0) lines.push(STR.pinnedOverflow[language](hidden));
|
|
247
|
+
}
|
|
215
248
|
}
|
|
216
249
|
return escapePromptVars(lines.join("\n"));
|
|
217
250
|
}
|
|
@@ -250,7 +283,17 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
250
283
|
return escapePromptVars(lines.join("\n"));
|
|
251
284
|
}
|
|
252
285
|
|
|
286
|
+
// #249 第一批:能力说明的系统提示段。order 150 = 插件指引段的既有惯例
|
|
287
|
+
// (ACP 的 ACP_SYSTEM_PROMPT 与 mnemon 的 routing 段都在 150)。文本是常量、
|
|
288
|
+
// 同会话内不随轮次变化,故不影响其后的前缀缓存,也不逐轮复读。宿主若不提供
|
|
289
|
+
// section seam 就静默跳过——#249 §3 的口径是「只用现成 seam,拿不到位的语义
|
|
290
|
+
// 降级处理」:能力说明仍落在工具描述上,不算失败。
|
|
291
|
+
const guideSection = config.injectGuidanceEnabled === true && typeof ctx.systemPrompt?.section === "function"
|
|
292
|
+
? ctx.systemPrompt.section({ name: "memory-guide", order: 150, text: MEMORY_GUIDE_SECTION })
|
|
293
|
+
: null;
|
|
294
|
+
|
|
253
295
|
const disposers = [
|
|
296
|
+
...(guideSection ? [guideSection] : []),
|
|
254
297
|
ctx.systemPrompt.context({
|
|
255
298
|
name: "memory",
|
|
256
299
|
order: 90,
|
|
@@ -270,16 +313,48 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
270
313
|
const rotate = recentInjectedIds(sessionId, query);
|
|
271
314
|
// rotateWindow 随集合一起传入:候选池按 maxItems×(N+1) 扩容——窗口要的
|
|
272
315
|
// 牌比既有 maxItems×2 池多时,轮换才有新鲜牌可换(#205 补测)。
|
|
273
|
-
|
|
274
|
-
|
|
316
|
+
// Issue #239(第 5 项):注入条数的查询自适应(默认关)。确定性强的话题收缩
|
|
317
|
+
// 条数、模糊话题维持上限——单向收缩,绝不越过 maxInjectedItems;判据只看查询
|
|
318
|
+
// 本身,不做额外检索(先探针检索等于白付一次 fuseRecall)。
|
|
319
|
+
const maxItems = config.injectUncertaintyAdaptive === true
|
|
320
|
+
? adaptiveInjectBudget(query, baseMaxItems)
|
|
321
|
+
: baseMaxItems;
|
|
322
|
+
// #249 第一批:pin 池统计走可选出参,不动 injectCandidates 的数组契约。
|
|
323
|
+
const pinnedStats = {};
|
|
324
|
+
const candidates = service.injectCandidates({ query, queryVector, maxItems, threshold, scope, rotate, rotateWindow: rotationTurns, pinnedStats });
|
|
325
|
+
// #249:pin 条目不进轮换历史——它们每轮固定前置,记进去只会占满轮换
|
|
326
|
+
// 窗口、挤掉情景候选的新鲜度(验收:pin 不参与跨轮轮换)。
|
|
327
|
+
recordInjection(sessionId, query, pinnedStats.shown > 0 ? candidates.slice(pinnedStats.shown) : candidates);
|
|
275
328
|
// Hot memory (v0.5.0 1.3) leads the single memory block: the agent
|
|
276
329
|
// sees the short-term rounds first, then the cross-session recall —
|
|
277
330
|
// the documented injection order 1→2. Folding it here (instead of a
|
|
278
331
|
// separate context) keeps the prompt assembly stable at two blocks.
|
|
279
332
|
const hotText = renderHotContext(ctx);
|
|
280
|
-
const body = render(candidates);
|
|
281
|
-
|
|
282
|
-
|
|
333
|
+
const body = render(candidates, pinnedStats);
|
|
334
|
+
const finalBody = !hotText ? body : body ? `${hotText}\n\n${body}` : hotText;
|
|
335
|
+
// Issue #179:旁路缓存一帧——面板「注入预览」卡据此展示构成与体积。
|
|
336
|
+
// chars 为条目内容的截断后近似值(与 render 同一函数计长),totalChars
|
|
337
|
+
// 是本次实际返回块的精确长度。maxItems 反映自适应收缩后的生效值。
|
|
338
|
+
injectionSnapshot = {
|
|
339
|
+
at: Date.now(),
|
|
340
|
+
sessionId,
|
|
341
|
+
query: query || "",
|
|
342
|
+
maxItems,
|
|
343
|
+
threshold,
|
|
344
|
+
adaptive: config.injectUncertaintyAdaptive === true,
|
|
345
|
+
scoped: scope && (scope.agent_scope || scope.workspace_scope) ? scope : null,
|
|
346
|
+
rotated: rotate ? rotate.size : 0,
|
|
347
|
+
hotChars: hotText.length,
|
|
348
|
+
entries: candidates.map((m) => ({
|
|
349
|
+
id: m.id,
|
|
350
|
+
type: m.type,
|
|
351
|
+
title: m.title,
|
|
352
|
+
importance: m.importance,
|
|
353
|
+
chars: injectMemory(m).length
|
|
354
|
+
})),
|
|
355
|
+
totalChars: finalBody.length
|
|
356
|
+
};
|
|
357
|
+
return finalBody;
|
|
283
358
|
}
|
|
284
359
|
}),
|
|
285
360
|
ctx.systemPrompt.context({
|
|
@@ -292,6 +367,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
292
367
|
return () => {
|
|
293
368
|
queryVectorCache.clear();
|
|
294
369
|
rotationHistory.clear();
|
|
370
|
+
injectionSnapshot = null; // 注入器卸载即失效:快照不得跨生命周期存留
|
|
295
371
|
for (const dispose of disposers) {
|
|
296
372
|
if (typeof dispose === "function") dispose();
|
|
297
373
|
}
|
package/src/lang.js
CHANGED
|
@@ -75,6 +75,12 @@ export const STR = {
|
|
|
75
75
|
zh: (max, len, id) => `〔已截断:上限 ${max},原 ${len} 字符;全文用 memory_get "${id}" 查看〕`,
|
|
76
76
|
en: (max, len, id) => `[truncated: limit ${max}, original ${len} chars; memory_get "${id}" for full text]`
|
|
77
77
|
},
|
|
78
|
+
// Issue #249(第一批):pin 池超预算时的如实标注——分层不等于丢弃,也不允许
|
|
79
|
+
// 静默省略未展示条数(agent 才知道库里的约束不止这几条,需按需 memory_search)。
|
|
80
|
+
pinnedOverflow: {
|
|
81
|
+
zh: (n) => `〔约束/偏好类另有 ${n} 条未展示〕`,
|
|
82
|
+
en: (n) => `[${n} more constraint/preference entries not shown]`
|
|
83
|
+
},
|
|
78
84
|
userSettingsHeader: {
|
|
79
85
|
zh: "[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
80
86
|
en: "[User settings] Profile and rules from dsh-mneme:"
|