@modusensus/dsh-mneme 0.8.15 → 0.9.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.
Files changed (100) hide show
  1. package/README.md +21 -3
  2. package/assets/icon.svg +2 -2
  3. package/bin/cli.mjs +4 -2
  4. package/docs/CONFIGURATION.md +16 -3
  5. package/docs/CONTINUITY.md +18 -3
  6. package/docs/DISTILL.md +162 -0
  7. package/docs/STORAGE.md +11 -2
  8. package/docs/WORKFLOWS.md +299 -0
  9. package/lib/api-standalone.js +22 -39
  10. package/lib/api.js +294 -10
  11. package/lib/candidates.js +454 -0
  12. package/lib/capabilities.js +819 -0
  13. package/lib/client.js +2767 -337
  14. package/lib/config.js +99 -6
  15. package/lib/continuity.js +35 -6
  16. package/lib/dream/decisions.js +37 -1
  17. package/lib/dream/sleep.js +10 -2
  18. package/lib/dream.js +7 -1
  19. package/lib/entities/extractor.js +4 -0
  20. package/lib/hot-memory.js +62 -0
  21. package/lib/index.js +72 -18
  22. package/lib/inject.js +33 -64
  23. package/lib/maintenance.js +25 -5
  24. package/lib/pipeline.js +534 -0
  25. package/lib/recall-stats.js +22 -7
  26. package/lib/reclaim-http.js +150 -0
  27. package/lib/safe-slice.js +18 -0
  28. package/lib/scope.js +16 -4
  29. package/lib/serve.js +1 -1
  30. package/lib/service.js +83 -17
  31. package/lib/session-usage.js +350 -0
  32. package/lib/settings.js +19 -2
  33. package/lib/store.js +251 -10
  34. package/lib/summarize.js +239 -60
  35. package/lib/tools.js +11 -2
  36. package/package.json +1 -1
  37. package/scripts/publish-gate.mjs +197 -0
  38. package/src/api-standalone.js +22 -39
  39. package/src/api.js +294 -10
  40. package/src/candidates.js +454 -0
  41. package/src/capabilities.js +819 -0
  42. package/src/config.js +99 -6
  43. package/src/continuity.js +35 -6
  44. package/src/dream/decisions.js +37 -1
  45. package/src/dream/sleep.js +10 -2
  46. package/src/dream.js +7 -1
  47. package/src/entities/extractor.js +4 -0
  48. package/src/hot-memory.js +62 -0
  49. package/src/index.js +72 -18
  50. package/src/inject.js +33 -64
  51. package/src/maintenance.js +25 -5
  52. package/src/pipeline.js +534 -0
  53. package/src/recall-stats.js +22 -7
  54. package/src/reclaim-http.js +150 -0
  55. package/src/safe-slice.js +18 -0
  56. package/src/scope.js +16 -4
  57. package/src/serve.js +1 -1
  58. package/src/service.js +83 -17
  59. package/src/session-usage.js +350 -0
  60. package/src/settings.js +19 -2
  61. package/src/store.js +251 -10
  62. package/src/summarize.js +239 -60
  63. package/src/tools.js +11 -2
  64. package/test/api-routes-gaps.test.js +681 -0
  65. package/test/api.test.js +171 -8
  66. package/test/candidates.test.js +345 -0
  67. package/test/capabilities.test.js +577 -0
  68. package/test/client.test.js +715 -4
  69. package/test/continuity.test.js +84 -0
  70. package/test/contrast.test.js +314 -0
  71. package/test/dream-merge-guardrail.test.js +219 -0
  72. package/test/embed-ready-poll.test.js +94 -0
  73. package/test/entities.test.js +13 -2
  74. package/test/failure-query.test.js +147 -0
  75. package/test/failure-retention.test.js +105 -0
  76. package/test/graph-api.test.js +36 -2
  77. package/test/hot-memory.test.js +67 -1
  78. package/test/inject-recall.test.js +68 -1
  79. package/test/inject.test.js +23 -0
  80. package/test/llm-audit.test.js +190 -1
  81. package/test/maintenance.test.js +112 -2
  82. package/test/mint-source.test.js +202 -0
  83. package/test/pipeline.test.js +505 -0
  84. package/test/placement.test.js +1 -1
  85. package/test/pre-inject-gate.test.js +10 -0
  86. package/test/publish-gate.test.js +272 -0
  87. package/test/recall-retention.test.js +128 -0
  88. package/test/recall-stats.test.js +89 -4
  89. package/test/reclaim-http.test.js +209 -0
  90. package/test/release-workflow.test.js +77 -0
  91. package/test/scope-distill-stamp.test.js +135 -0
  92. package/test/serve.test.js +1 -1
  93. package/test/service.test.js +7 -2
  94. package/test/session-usage.test.js +308 -0
  95. package/test/settings.test.js +8 -0
  96. package/test/store-write-path-columns.test.js +191 -0
  97. package/test/store.test.js +108 -0
  98. package/test/summarize.test.js +350 -0
  99. package/test/supersede-state-tracking.test.js +4 -4
  100. package/test/tools.test.js +16 -1
package/lib/inject.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createScopeResolver } from "./scope.js";
2
- import { createHotMemory } from "./hot-memory.js";
2
+ import { createHotMemory, roundsFromEvents } from "./hot-memory.js";
3
3
  import { STR, langOf } from "./lang.js";
4
4
  import { MEMORY_GUIDE_SECTION } from "./guide.js";
5
5
  import { adaptiveInjectBudget } from "./search/adaptive.js";
6
6
  import { injectChildEnabled } from "./config.js";
7
+ import { safeSlice } from "./safe-slice.js";
7
8
 
8
9
  // Issue #179:注入预览的数据底座。systemPrompt 渲染是同步回调,面板只能事后
9
10
  // 拉取,所以在这里旁路缓存「最近一帧组装」——快照就是本次渲染用过的同一份
@@ -16,21 +17,19 @@ export function getInjectionSnapshot() {
16
17
  return injectionSnapshot;
17
18
  }
18
19
 
19
- // Issue #334:UTF-16 截断切开 emoji 代理对会留下孤立代理项(如 🔴 = D83D DD34
20
- // 在 800 处切,D83D 留下、DD34 被切走)。DSH 把它序列化成非法 UTF-8,DeepSeek
21
- // API 对每个请求回 400 INVALID_REQUEST——而且畸形文本随注入消息被永久写进会话
22
- // 历史,此后**每一轮**都带着它,表现为会话从某一轮起连续 400、换话题/重启都不
23
- // 恢复(会话级不可逆损坏)。防法是「少一个字符都行,绝不留半个」:尾字符是
24
- // 高位代理(0xD800–0xDBFF)说明它的低位搭档被切走了,把这个高位代理整个丢掉。
25
- // (反过来尾字符是低位代理不可能孤立——它的搭档必然紧邻在前、同在切点之内。)
26
- // ASCII-only 的文本零开销(length 相等直接短路)。
27
- function safeSlice(text, limit) {
28
- const s = String(text);
29
- if (s.length <= limit) return s;
30
- let cut = s.slice(0, limit);
31
- const tail = cut.charCodeAt(cut.length - 1);
32
- if (tail >= 0xd800 && tail <= 0xdbff) cut = cut.slice(0, -1);
33
- return cut;
20
+ // 「模型可见事件 seq」集合:宿主在 session.surface.nodes 维护可见面——rewind 的
21
+ // surfaceOp replace 与 compact 的隐藏替换都作用在这层(dsh-rewind 插件即据此计算
22
+ // 替换区间)。拿不到(老版本宿主)返回 null,调用方退回「全量事件」的旧行为。
23
+ // 读取必须在 try 内:`surface.nodes` 是宿主 getter,其 _assertProjections 在消息投影
24
+ // 所属插件被移除时会 throw(dsh-session lib/types/surface.js)。抛出等同于「没有
25
+ // 可见面」,绝不能让整块热记忆消失——那是比旧偏差更糟的退化。
26
+ function visibleSeqsOf(session) {
27
+ try {
28
+ const nodes = session?.surface?.nodes;
29
+ return Array.isArray(nodes) ? new Set(nodes) : null;
30
+ } catch {
31
+ return null;
32
+ }
34
33
  }
35
34
 
36
35
  // Best-effort extraction of the current user's latest message text from the
@@ -39,15 +38,23 @@ function safeSlice(text, limit) {
39
38
  // session event log (same event shape summarize.js consumes) and returns the
40
39
  // most recent human message. Any failure degrades to "" — the injector then
41
40
  // falls back to the legacy rule-based pick, never breaking the render.
42
- function lastUserQuery(ctx) {
41
+ //
42
+ // v0.9.0:导出给 tools.js 的 memory_update 用。失败记录表的 query 与注入
43
+ // 候选问的是同一件事——「当时到底想干什么」;这条取原话的路只该有一份实现。
44
+ // 复制一份的代价本仓库付过几次(src/lib 平价、snapshotEvents 兼容层),所以
45
+ // 这里只加 export,不改行为。
46
+ export function lastUserQuery(ctx) {
43
47
  try {
44
48
  const session = ctx?.agent?.session;
45
49
  // DSH ≥0.1.2-rc only exposes events via snapshotEvents(); older builds
46
50
  // still have the .events property, so fall back to it.
47
51
  const events = session?.snapshotEvents?.() ?? session?.events;
48
52
  if (!Array.isArray(events) || events.length === 0) return "";
53
+ const visibleSeqs = visibleSeqsOf(session);
49
54
  for (let i = events.length - 1; i >= 0; i--) {
50
55
  const event = events[i];
56
+ // 被撤回/被压缩遮蔽的事件不算「最近一条用户消息」(与 extractRounds 同口径)。
57
+ if (visibleSeqs && !visibleSeqs.has(event?.seq)) continue;
51
58
  if (event?.type !== "user/message") continue;
52
59
  const kind = event.data?.source?.kind;
53
60
  if (kind !== undefined && kind !== "user") continue;
@@ -66,58 +73,20 @@ function lastUserQuery(ctx) {
66
73
  }
67
74
 
68
75
  // Hot-memory round extraction (v0.5.0 1.3): pairs each user/message with the
69
- // next assistant reply from the materialized session log. Tolerates shapes
70
- // where assistant events carry a different type tag — anything whose payload
71
- // has content parts and is not a user message counts as a reply. Best-effort:
72
- // returns [] on any failure, and the hot block simply does not render.
76
+ // next assistant reply. The pairing lives in hot-memory.js (roundsFromEvents)
77
+ // so it can be unit-tested directly; this adapter only resolves the session's
78
+ // raw event log and the model-visible seq set (session.surface.nodes — see
79
+ // visibleSeqsOf). Rewind-shadowed / compacted events are thereby excluded:
80
+ // hot memory and the model see the same conversation. Best-effort: returns []
81
+ // on any failure, and the hot block simply does not render.
73
82
  function extractRounds(ctx, maxRounds) {
74
83
  try {
75
84
  const session = ctx?.agent?.session;
85
+ // DSH ≥0.1.2-rc only exposes events via snapshotEvents(); older builds
86
+ // still have the .events property, so fall back to it.
76
87
  const events = session?.snapshotEvents?.() ?? session?.events;
77
88
  if (!Array.isArray(events) || events.length === 0) return [];
78
- const rounds = [];
79
- let pendingQuery = null;
80
- const textOf = (event) => {
81
- // user/message 的正文就在 data 上,而 assistant/message(以及 tool/result)的消息体
82
- // 嵌在 data.message 下 —— DSH 侧由 @deepseek-ai/dsh-session 的
83
- // assertMessageEventShape 一行写死:const message = type === "user/message" ? record : record?.["message"]。
84
- // 只读 data.content 会让每一轮的 response 恒为空(Issue #129)。
85
- // 这里用 ?? 兜底而不是直接改成 data.message.content:user 消息与旧的扁平形状都照旧可用。
86
- const parts = event?.data?.content ?? event?.data?.message?.content;
87
- if (!Array.isArray(parts)) return "";
88
- // 只取正文 part(type === "text" 或字符串),跳过 reasoning(issue #162):
89
- // reasoning 里出现 Go template / Vue / Handlebars / 正则 / 日志原文的概率远高于
90
- // 正文,把它带进 hot memory 会让 `{{...}}` 模板片段注入 prompt。旧的扁平形状
91
- // (字符串 part)照旧取用,无 type 字段的兜底视为正文。
92
- return parts
93
- .map((p) => {
94
- if (typeof p === "string") return p;
95
- if (p?.type && p.type !== "text") return "";
96
- return p?.text ?? "";
97
- })
98
- .filter(Boolean)
99
- .join("\n")
100
- .trim();
101
- };
102
- for (const event of events) {
103
- const kind = event?.data?.source?.kind;
104
- const isUser = event?.type === "user/message" && (kind === undefined || kind === "user");
105
- if (isUser) {
106
- if (pendingQuery) rounds.push({ query: pendingQuery, response: "" });
107
- pendingQuery = safeSlice(textOf(event), 500);
108
- continue;
109
- }
110
- // Only assistant-originated events close a round; tool/system events
111
- // carrying text must not be mistaken for the model's reply.
112
- const isAssistant = typeof event?.type === "string" && event.type.includes("assistant")
113
- || kind === "assistant";
114
- const body = isAssistant ? textOf(event) : "";
115
- if (!body || !pendingQuery) continue;
116
- rounds.push({ query: pendingQuery, response: safeSlice(body, 800) });
117
- pendingQuery = null;
118
- }
119
- if (pendingQuery) rounds.push({ query: pendingQuery, response: "" });
120
- return rounds.slice(-maxRounds);
89
+ return roundsFromEvents(events, maxRounds, visibleSeqsOf(session));
121
90
  } catch {
122
91
  return [];
123
92
  }
@@ -6,7 +6,7 @@
6
6
  // 东西)。所以模块里没有任何定时器,挂点只有两个:standalone 数据面的
7
7
  // `POST /maintenance/reclaim` 与它上面的 `dsh-mneme reclaim` 子命令。
8
8
  //
9
- // 两项都零价值判断、零条数变化:
9
+ // 三项都零价值判断、零条数变化:
10
10
  // A `dream_runs.input` 置空。这一列的含义按 run_type 分叉:auto / sleep 的 run 存的
11
11
  // 是当时的记忆库快照,可由记忆库重建;**organize 的 run 存的是 apply 的重放载荷**,
12
12
  // 置空会让那次 apply 静默空转并把报告锁死,所以 organize 行不在可清范围内(谓词
@@ -14,6 +14,13 @@
14
14
  // 与 receipt 一律保留,行永不删(拍板 3:裁列同意、删行不同意)。
15
15
  // B 归档行向量置空:检索 SQL 恒带 `archived = 0`,这部分向量按定义不可达。它不是
16
16
  // 单程票——取消归档时 service 会重新排队嵌入(archiveMemory 的还原分支)。
17
+ // C #399 蒸馏原文记录按 distilled_at 到期删除:记录是蒸馏那次调用的凭证,越旧越
18
+ // 没有复核价值,而它按定义就是整段对话原文(本入口里唯一的「真删内容」项)。它的
19
+ // 常态保留期是独立配置键 distillRecords.retentionDays(启动期裁剪走那个键);本入口
20
+ // **不读那个键**,与 A/B 共用同一条 --older-than 截止(想对齐就显式传同样的天数,
21
+ // 见 STORAGE.md「两处不冲突」)。链接
22
+ // (memory_provenance)**不在可清范围内**:保留指向空区间即「来源已过期」,删链
23
+ // 接等于假装这条记忆从来没有过出处——那是篡改历史,不是回收。
17
24
  //
18
25
  // 报告口径(拍板 2):收益按 VACUUM 前后体积量,不按列字节估——实际释放来自溢出页与
19
26
  // 索引页回收,列文本大小只是上界,所以两个数分开报,不混成一个「省了多少」。
@@ -33,7 +40,8 @@ export const DEFAULT_INPUT_RETENTION_DAYS = 7;
33
40
  *
34
41
  * @param {object} deps
35
42
  * @param {object} deps.store 存储层(dreamRunInputStats / clearDreamRunInputs /
36
- * archivedEmbeddingStats / clearArchivedEmbeddings / storageStats / vacuum / saveLlmAudit)
43
+ * archivedEmbeddingStats / clearArchivedEmbeddings / distillRecordStats /
44
+ * clearDistillRecords / storageStats / vacuum / saveLlmAudit)
37
45
  * @param {object} [deps.config] 已解析配置(读 llmAudit.enabled:关掉 audit 时不落 receipt)
38
46
  * @param {object} [deps.logger]
39
47
  * @param {() => number} [deps.now] 时钟注入(测试用)
@@ -86,15 +94,26 @@ export function createMaintenance({ store, config, logger, now = Date.now } = {}
86
94
  const before = store.storageStats();
87
95
  const inputs = store.dreamRunInputStats(cutoff);
88
96
  const embeddings = store.archivedEmbeddingStats();
97
+ // #399:蒸馏原文记录与上面两项同款——按 distilled_at 到期裁剪。链接
98
+ // (memory_provenance)不在此列:它保留指向空区间,显示「来源已过期」。
99
+ const records = store.distillRecordStats(cutoff);
89
100
  const report = {
90
101
  dryRun,
91
102
  olderThanDays: days,
92
103
  cutoff,
93
104
  // planned = 这次会动几行;cleared = 实际动了几行(dry-run 恒 0)
94
- planned: { dream_run_inputs: inputs.runs, archived_embeddings: embeddings.rows },
105
+ planned: {
106
+ dream_run_inputs: inputs.runs,
107
+ archived_embeddings: embeddings.rows,
108
+ distill_records: records.rows
109
+ },
95
110
  // 列文本大小:上界,不是回收量(真实释放看下面的 size 差值)
96
- column_bytes: { dream_run_inputs: inputs.bytes, archived_embeddings: embeddings.bytes },
97
- cleared: { dream_run_inputs: 0, archived_embeddings: 0 },
111
+ column_bytes: {
112
+ dream_run_inputs: inputs.bytes,
113
+ archived_embeddings: embeddings.bytes,
114
+ distill_records: records.bytes
115
+ },
116
+ cleared: { dream_run_inputs: 0, archived_embeddings: 0, distill_records: 0 },
98
117
  size: {
99
118
  before_bytes: before.fileBytes,
100
119
  after_bytes: before.fileBytes,
@@ -114,6 +133,7 @@ export function createMaintenance({ store, config, logger, now = Date.now } = {}
114
133
  }
115
134
  report.cleared.dream_run_inputs = store.clearDreamRunInputs(cutoff);
116
135
  report.cleared.archived_embeddings = store.clearArchivedEmbeddings();
136
+ report.cleared.distill_records = store.clearDistillRecords(cutoff);
117
137
  if (report.vacuum.requested) {
118
138
  // VACUUM 失败不能把前面的清理一起吞掉:两个 UPDATE 已经各自提交了,此时抛出去
119
139
  // 就是「数据已经不可逆地清了,但没有任何回执」。所以失败记进报告、回执照写,