@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.
- package/README.md +21 -3
- package/assets/icon.svg +2 -2
- package/bin/cli.mjs +4 -2
- package/docs/CONFIGURATION.md +16 -3
- package/docs/CONTINUITY.md +18 -3
- package/docs/DISTILL.md +162 -0
- package/docs/STORAGE.md +11 -2
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api-standalone.js +22 -39
- package/lib/api.js +294 -10
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +2767 -337
- package/lib/config.js +99 -6
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +37 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +7 -1
- package/lib/entities/extractor.js +4 -0
- package/lib/hot-memory.js +62 -0
- package/lib/index.js +72 -18
- package/lib/inject.js +33 -64
- package/lib/maintenance.js +25 -5
- package/lib/pipeline.js +534 -0
- package/lib/recall-stats.js +22 -7
- package/lib/reclaim-http.js +150 -0
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +16 -4
- package/lib/serve.js +1 -1
- package/lib/service.js +83 -17
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +19 -2
- package/lib/store.js +251 -10
- package/lib/summarize.js +239 -60
- package/lib/tools.js +11 -2
- package/package.json +1 -1
- package/scripts/publish-gate.mjs +197 -0
- package/src/api-standalone.js +22 -39
- package/src/api.js +294 -10
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +99 -6
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +37 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +7 -1
- package/src/entities/extractor.js +4 -0
- package/src/hot-memory.js +62 -0
- package/src/index.js +72 -18
- package/src/inject.js +33 -64
- package/src/maintenance.js +25 -5
- package/src/pipeline.js +534 -0
- package/src/recall-stats.js +22 -7
- package/src/reclaim-http.js +150 -0
- package/src/safe-slice.js +18 -0
- package/src/scope.js +16 -4
- package/src/serve.js +1 -1
- package/src/service.js +83 -17
- package/src/session-usage.js +350 -0
- package/src/settings.js +19 -2
- package/src/store.js +251 -10
- package/src/summarize.js +239 -60
- package/src/tools.js +11 -2
- package/test/api-routes-gaps.test.js +681 -0
- package/test/api.test.js +171 -8
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/client.test.js +715 -4
- package/test/continuity.test.js +84 -0
- package/test/contrast.test.js +314 -0
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/embed-ready-poll.test.js +94 -0
- package/test/entities.test.js +13 -2
- package/test/failure-query.test.js +147 -0
- package/test/failure-retention.test.js +105 -0
- package/test/graph-api.test.js +36 -2
- package/test/hot-memory.test.js +67 -1
- package/test/inject-recall.test.js +68 -1
- package/test/inject.test.js +23 -0
- package/test/llm-audit.test.js +190 -1
- package/test/maintenance.test.js +112 -2
- package/test/mint-source.test.js +202 -0
- package/test/pipeline.test.js +505 -0
- package/test/placement.test.js +1 -1
- package/test/pre-inject-gate.test.js +10 -0
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/recall-stats.test.js +89 -4
- package/test/reclaim-http.test.js +209 -0
- package/test/release-workflow.test.js +77 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/serve.test.js +1 -1
- package/test/service.test.js +7 -2
- package/test/session-usage.test.js +308 -0
- package/test/settings.test.js +8 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/store.test.js +108 -0
- package/test/summarize.test.js +350 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// 存储回收(maintenance/reclaim)的 HTTP 口径 —— 唯一实现。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要单独一个模块:这张脸有两个入口——宿主内的面板路由(src/api.js 的
|
|
4
|
+
// /api/dsh-mneme/maintenance/reclaim)与 Bearer 数据面(src/api-standalone.js 的
|
|
5
|
+
// /maintenance/reclaim)。两者底下是**同一份** src/maintenance.js,差别只在鉴权与
|
|
6
|
+
// 路径。历史上它们是整段复制的,于是 #410 修好面板侧的上界与兜错之后,数据面还停在
|
|
7
|
+
// 旧形态(#411)——复制粘贴漂移不是设计,是缺陷。
|
|
8
|
+
//
|
|
9
|
+
// 口径(错误码字符串、默认值、报告形状)现在只写在这里,两个入口都调它。这样
|
|
10
|
+
// 「改一边忘一边」在结构上就不可能发生;真要再写第二份,test/reclaim-http.test.js
|
|
11
|
+
// 的静态锁会直接转红。
|
|
12
|
+
//
|
|
13
|
+
// 这里只做协议层的事:解析、校验、把 maintenance.reclaim 的结果包成答复。一行数据
|
|
14
|
+
// 都不改——真正的丢弃逻辑在 src/maintenance.js。
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 回收窗口上界。整数校验挡不住量级:1e20 也是整数,而 maintenance 会拿它算
|
|
18
|
+
* `now() - days * 86400000` 再 toISOString(),抛 RangeError;GET 侧 400 位十进制
|
|
19
|
+
* 字面量还会化成 Infinity,被当成畸形输入静默落回默认窗口 7——调用方要一个巨大
|
|
20
|
+
* 窗口,实际只清了 7 天外的东西,而报告上写着 7 看不出异样。
|
|
21
|
+
*
|
|
22
|
+
* 取值与 config.js 的 llmAudit.retentionDays / recallRetentionDays 同一条线
|
|
23
|
+
* (3650 天 = 10 年):回收窗口比审计保留期还长没有意义,那之后的东西本来也不在。
|
|
24
|
+
*/
|
|
25
|
+
export const MAX_RECLAIM_WINDOW_DAYS = 3650;
|
|
26
|
+
|
|
27
|
+
/** 拒执行时的提示。两个入口必须逐字一致——它是给调用方看的下一步动作。 */
|
|
28
|
+
export const RECLAIM_CONFIRM_HINT = "先看 dry-run 报告,确认后带 {\"dryRun\":false,\"confirm\":true} 再发";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 「维护能力没接线」的错误码。两个入口各有一次**顺序敏感**的显式检查(503 要赢过
|
|
32
|
+
* 400:端点整个没接线时,报「窗口不合法」是误导),所以这条不能只藏在 reclaimReply
|
|
33
|
+
* 里;但也必须是同一份字符串,否则两张皮又能各写各的。
|
|
34
|
+
*/
|
|
35
|
+
export const RECLAIM_UNAVAILABLE = "maintenance-unavailable";
|
|
36
|
+
|
|
37
|
+
/** 回收失败的 warn 文案。两条路径共用,日志里不会出现两种拼法。 */
|
|
38
|
+
export function reclaimWarnMessage(error) {
|
|
39
|
+
return `[dsh-mneme] storage reclaim failed: ${String(error)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 解析 GET 的 ?olderThanDays(searchParams.get 的原始字符串)。
|
|
44
|
+
*
|
|
45
|
+
* query 是字符串域:只收十进制整数字面量,缺省才落默认窗口。**不能用 Number()**
|
|
46
|
+
* ——它把 ""/[]/" " 都化成 0,而 0 在这里是「清掉全部快照」,最坏解释绝不能从一个
|
|
47
|
+
* 畸形输入静默拿到(POST 侧收 JSON 数字,口径不同,见 readReclaimFields)。
|
|
48
|
+
*
|
|
49
|
+
* @returns {{ok: true, olderThanDays: number|undefined} | {ok: false, error: string}}
|
|
50
|
+
*/
|
|
51
|
+
export function parseReclaimQuery(raw) {
|
|
52
|
+
if (raw === null) return { ok: true, olderThanDays: undefined };
|
|
53
|
+
if (!/^\d+$/.test(raw)) return { ok: false, error: "invalid-older-than-days" };
|
|
54
|
+
const olderThanDays = Number(raw);
|
|
55
|
+
// 十进制字面量不保证是个能算日期的数:400 位数字会化成 Infinity。
|
|
56
|
+
if (!Number.isFinite(olderThanDays) || olderThanDays > MAX_RECLAIM_WINDOW_DAYS) {
|
|
57
|
+
return { ok: false, error: "invalid-older-than-days" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, olderThanDays };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 解析 POST body(已读成文本)的结构。只判 JSON 与顶层形状,字段在
|
|
64
|
+
* readReclaimFields 里读——两步分开是为了让调用方保住「invalid-json → invalid-body
|
|
65
|
+
* → maintenance 是否接线 → 字段」这个顺序,这个顺序是既有的对外行为。
|
|
66
|
+
*
|
|
67
|
+
* 不用 parseBody:它把坏 JSON 吞成 {},而 invalid-json 与 invalid-body 是两个不同
|
|
68
|
+
* 的错(前者是调用方发坏了,后者是发成了数组/null)。
|
|
69
|
+
*
|
|
70
|
+
* @returns {{ok: true, body: object} | {ok: false, error: string}}
|
|
71
|
+
*/
|
|
72
|
+
export function parseReclaimJson(text) {
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = JSON.parse(text || "{}");
|
|
76
|
+
} catch {
|
|
77
|
+
return { ok: false, error: "invalid-json" };
|
|
78
|
+
}
|
|
79
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
80
|
+
return { ok: false, error: "invalid-body" };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, body };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 读 POST 的字段并归一化。
|
|
87
|
+
*
|
|
88
|
+
* @returns {{ok: true, dryRun: boolean, olderThanDays: number|undefined, vacuum: boolean,
|
|
89
|
+
* confirm: boolean} | {ok: false, error: string}}
|
|
90
|
+
*/
|
|
91
|
+
export function readReclaimFields(body) {
|
|
92
|
+
const dryRun = body.dryRun !== false;
|
|
93
|
+
// 只收 JSON 数字:Number() 会把 ""/[]/false 都化成 0,而 0 在这里是「清掉全部
|
|
94
|
+
// 快照」——最坏解释绝不能让一个畸形输入静默拿到。
|
|
95
|
+
const olderThanDays = body.olderThanDays === undefined || body.olderThanDays === null
|
|
96
|
+
? undefined
|
|
97
|
+
: body.olderThanDays;
|
|
98
|
+
if (olderThanDays !== undefined
|
|
99
|
+
&& (!Number.isInteger(olderThanDays) || olderThanDays < 0 || olderThanDays > MAX_RECLAIM_WINDOW_DAYS)) {
|
|
100
|
+
return { ok: false, error: "invalid-older-than-days" };
|
|
101
|
+
}
|
|
102
|
+
if (body.vacuum !== undefined && typeof body.vacuum !== "boolean") {
|
|
103
|
+
return { ok: false, error: "invalid-vacuum" };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
dryRun,
|
|
108
|
+
olderThanDays,
|
|
109
|
+
vacuum: body.vacuum === true,
|
|
110
|
+
confirm: body.confirm === true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 把一次回收请求变成 { status, body }。调用方只管 sendJson,不自己拼答复。
|
|
116
|
+
*
|
|
117
|
+
* 报告一律不加包装层(就是 maintenance.reclaim 的原样返回):面板与数据面都直接读
|
|
118
|
+
* planned/cleared/vacuum 这几格。
|
|
119
|
+
*
|
|
120
|
+
* confirm-required 分支里那份 dry-run 报告是**在这里**算的。它曾经写在 readBody 的
|
|
121
|
+
* 回调里,而外层 try 那时已经退栈——抛出去就是一个没人接的 rejection,客户端拿到的
|
|
122
|
+
* 不是 500 而是「一个字节都没回」(#410 实测)。所以本地兜住,与执行路径给出同一个
|
|
123
|
+
* 500 internal。
|
|
124
|
+
*
|
|
125
|
+
* @param {{maintenance: object|null, dryRun: boolean, olderThanDays?: number,
|
|
126
|
+
* vacuum?: boolean, confirm?: boolean, warn?: ((msg: string) => void)|null}} input
|
|
127
|
+
* @returns {{status: number, body: object}}
|
|
128
|
+
*/
|
|
129
|
+
export function reclaimReply({ maintenance, dryRun, olderThanDays, vacuum = false, confirm = false, warn = null }) {
|
|
130
|
+
if (!maintenance) return { status: 503, body: { error: RECLAIM_UNAVAILABLE } };
|
|
131
|
+
if (!dryRun && confirm !== true) {
|
|
132
|
+
// 拒执行,但把 dry-run 报告一并回给调用方:确认的依据就是这份数字。报告必须描述
|
|
133
|
+
// **将要执行的那个动作**,所以 vacuum 原样带上——否则调用方看到的是「不 VACUUM」
|
|
134
|
+
// 的数字,却带着 vacuum 去执行。
|
|
135
|
+
let report;
|
|
136
|
+
try {
|
|
137
|
+
report = maintenance.reclaim({ olderThanDays, vacuum, dryRun: true });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
warn?.(reclaimWarnMessage(error));
|
|
140
|
+
return { status: 500, body: { error: "internal" } };
|
|
141
|
+
}
|
|
142
|
+
return { status: 400, body: { error: "confirm-required", hint: RECLAIM_CONFIRM_HINT, report } };
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return { status: 200, body: maintenance.reclaim({ olderThanDays, vacuum, dryRun }) };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
warn?.(reclaimWarnMessage(error));
|
|
148
|
+
return { status: 500, body: { error: "internal" } };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Issue #334:UTF-16 截断切开 emoji 代理对会留下孤立代理项(如 🔴 = D83D DD34
|
|
2
|
+
// 在 800 处切,D83D 留下、DD34 被切走)。DSH 把它序列化成非法 UTF-8,DeepSeek
|
|
3
|
+
// API 对每个请求回 400 INVALID_REQUEST——而且畸形文本随注入消息被永久写进会话
|
|
4
|
+
// 历史,此后**每一轮**都带着它,表现为会话从某一轮起连续 400、换话题/重启都不
|
|
5
|
+
// 恢复(会话级不可逆损坏)。防法是「少一个字符都行,绝不留半个」:尾字符是
|
|
6
|
+
// 高位代理(0xD800–0xDBFF)说明它的低位搭档被切走了,把这个高位代理整个丢掉。
|
|
7
|
+
// (反过来尾字符是低位代理不可能孤立——它的搭档必然紧邻在前、同在切点之内。)
|
|
8
|
+
// ASCII-only 的文本零开销(length 相等直接短路)。
|
|
9
|
+
// (原为 inject.js 模块私有;热记忆的轮次抽取同样要做正文截断,收拢成共享模块
|
|
10
|
+
// 以免两份实现漂移。)
|
|
11
|
+
export function safeSlice(text, limit) {
|
|
12
|
+
const s = String(text);
|
|
13
|
+
if (s.length <= limit) return s;
|
|
14
|
+
let cut = s.slice(0, limit);
|
|
15
|
+
const tail = cut.charCodeAt(cut.length - 1);
|
|
16
|
+
if (tail >= 0xd800 && tail <= 0xdbff) cut = cut.slice(0, -1);
|
|
17
|
+
return cut;
|
|
18
|
+
}
|
package/lib/scope.js
CHANGED
|
@@ -48,9 +48,17 @@ export function createScopeResolver({ ctx, config, logger } = {}) {
|
|
|
48
48
|
header = null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// header 字段的读取要各自护住:session.header 本身取得回来,不代表它的字段
|
|
52
|
+
// 取得回来——宿主可以给出会抛错的 accessor(sessionOf / sessionKeyOf 就是为
|
|
53
|
+
// 同一类宿主写的)。漏在这里的异常会穿出解析器,把调用方的整段写入拖挂:
|
|
54
|
+
// 蒸馏路径上是一个窗口的记忆与游标一起丢,工具路径上是一次 memory_save 直接失败。
|
|
51
55
|
let agentScope = null;
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
try {
|
|
57
|
+
const preset = header?.agentPreset;
|
|
58
|
+
if (typeof preset === "string" && preset.trim()) agentScope = preset.trim();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
warnOnce(`[dsh-mneme] session header agentPreset lookup failed: ${String(e)}`);
|
|
61
|
+
}
|
|
54
62
|
|
|
55
63
|
// workspace:registry 反查优先(成员资格 = id 命中 + header canonical cwd
|
|
56
64
|
// 校验过,path 已 realpath 归一),失败回退 header.cwd,再失败留 NULL。
|
|
@@ -70,8 +78,12 @@ export function createScopeResolver({ ctx, config, logger } = {}) {
|
|
|
70
78
|
);
|
|
71
79
|
}
|
|
72
80
|
if (!workspaceScope) {
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
try {
|
|
82
|
+
const cwd = header?.cwd;
|
|
83
|
+
if (typeof cwd === "string" && cwd.trim()) workspaceScope = cwd.trim();
|
|
84
|
+
} catch (e) {
|
|
85
|
+
warnOnce(`[dsh-mneme] session header cwd lookup failed: ${String(e)}`);
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
return { agent_scope: agentScope, workspace_scope: workspaceScope };
|
package/lib/serve.js
CHANGED
|
@@ -112,7 +112,7 @@ export async function createServeRuntime({
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// index.js:
|
|
115
|
+
// index.js 的 setRecallRecorder(2026-10-14 在 :339-350):检索回执落 recall_runs(searchMemories 的 recordRecall 默认开,
|
|
116
116
|
// 第三方检索统计因此不缺数)。best-effort:回执写失败绝不影响检索本身。
|
|
117
117
|
service.setRecallRecorder((recall) => {
|
|
118
118
|
try {
|
package/lib/service.js
CHANGED
|
@@ -13,7 +13,20 @@ import { createBM25Index } from "./search/bm25.js";
|
|
|
13
13
|
import { adaptiveThreshold } from "./search/adaptive.js";
|
|
14
14
|
import { anchorSeeds, normalizePath, cascadeDepths, HOP_QUOTA } from "./graph/anchoring.js";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Issue #369:蒸馏白名单(src/summarize.js 的 VALID)产得出 history,注入白名单
|
|
17
|
+
// 原先不收——本机活跃库实测 378/1820(20.8%)因此永不可自动注入,而它们在同一库
|
|
18
|
+
// 的 recall_runs `auto` 模式里被按需检索过 328 次:不是垃圾数据,只是进不了注入池。
|
|
19
|
+
// 收编后候选池 1362 → 1713(+25.8%),按 top_k=5 粗算约多占 1 席;重要度闸门对
|
|
20
|
+
// history 一视同仁(见 injectCandidates 的 importance >= threshold),低于闸门照旧出局。
|
|
21
|
+
// pattern 一并收编:它一度被当成「只由手工写入(实测 40 条全 source=manual)」——那个
|
|
22
|
+
// 判据是错的。机器一直在 mint:2026-10-14 按 dream_runs 的 patterns.applied 逐轮求和,
|
|
23
|
+
// 得 144,与表内 pattern 行数 144 相等(72 轮 sleep 里 48 轮 ok,auto 轮 0),来源是
|
|
24
|
+
// src/dream/sleep.js 的 phasePatterns;当时的 source=manual 只是 saveWithDedupe 没拿到
|
|
25
|
+
// source 时的兜底值,不是「人写的」的意思(现在 mint 会带 source=sleep/dream)。
|
|
26
|
+
// 但它确实已被 dreamMergeGuard 列为长保留受保护类型(docs/CONFIGURATION.md)——巩固层
|
|
27
|
+
// 当资产护住、注入面一次不给看,是同一条口径的两头不一致。这一点与作者是谁无关,
|
|
28
|
+
// 收编的结论不变,错的只是当初用来支持它的那句前提。
|
|
29
|
+
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint", "history", "pattern"]);
|
|
17
30
|
|
|
18
31
|
// document 摘要行(#230):仅 documentMemoryEnabled 开启时进注入候选。独立
|
|
19
32
|
// 集合而非常改共享 Set——INJECT_TYPES 本身是「类型可注入」的静态事实,flag
|
|
@@ -1285,7 +1298,10 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1285
1298
|
let quality = null;
|
|
1286
1299
|
if (qf?.enabled === true) {
|
|
1287
1300
|
try {
|
|
1288
|
-
|
|
1301
|
+
// #430:近重复判定只要最近 20 条正文,别再为了 slice(0,20) 先物化整库
|
|
1302
|
+
// (all() + 逐行 toRow,5k 行实测 100ms 级;本查询 0.06ms 级)。口径与
|
|
1303
|
+
// all().slice(0, 20) 一致:不加 WHERE、updated_at DESC、同刻顺序未定义。
|
|
1304
|
+
const recentContents = store.recentContents(20);
|
|
1289
1305
|
quality = evaluateMemoryQuality(memory, {
|
|
1290
1306
|
minContentLength: qf.minContentLength ?? 10,
|
|
1291
1307
|
recentContents
|
|
@@ -1634,9 +1650,14 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1634
1650
|
} catch { /* vector unavailable: fall through to the recall cache */ }
|
|
1635
1651
|
}
|
|
1636
1652
|
if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) {
|
|
1653
|
+
// CodeRabbit on #404:这条回填分支此前独漏重要度门槛——向量路(上方)与
|
|
1654
|
+
// BM25 路(下方 :1655 注释「门控与向量路径一致(含 importance 阈值)」)
|
|
1655
|
+
// 都带着它。#369 收编 history 后缺口会被放大:库里低于闸门的行(实测
|
|
1656
|
+
// history 378 条中有 27 条 i<=2)走一次检索进缓存就能绕开门槛占注入位。
|
|
1637
1657
|
for (const m of lastSemanticRecall.items) {
|
|
1638
1658
|
if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten &&
|
|
1639
|
-
(m.source !== "narrative" || allowNarrative) && codingGate(m)
|
|
1659
|
+
(m.source !== "narrative" || allowNarrative) && codingGate(m) &&
|
|
1660
|
+
(m.type === "summary" || m.type === "preference" || m.importance >= threshold)) semanticItems.push(m);
|
|
1640
1661
|
}
|
|
1641
1662
|
}
|
|
1642
1663
|
// Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。
|
|
@@ -1824,9 +1845,15 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1824
1845
|
if ((config?.recallRecordDefault ?? true) && recallRecorder && injected.length > 0) {
|
|
1825
1846
|
try {
|
|
1826
1847
|
recallRecorder({
|
|
1827
|
-
// recall_runs.query 是 NOT NULL(store.js
|
|
1828
|
-
//
|
|
1829
|
-
|
|
1848
|
+
// recall_runs.query 是 NOT NULL(store.js 的 recall_runs 建表语句,
|
|
1849
|
+
// 2026-10-14 在 :75)—— 注入是主动曝光,
|
|
1850
|
+
// 没有查询词时用空串而非 null——SQLite 改列约束需重建表,不值得。
|
|
1851
|
+
// v0.9.0:调用方(inject.js 的 lastUserQuery,2026-10-14 在 :343)本来就带着本轮的
|
|
1852
|
+
// 用户原话传进来,这里丢掉等于把「当时想干什么」从审计里抹掉
|
|
1853
|
+
// (2026-10-13 快照:18,231/18,231 注入行 query 全空,全空比例
|
|
1854
|
+
// 100%——这个比率才是要记的,绝对行数随表长,引用要带时刻)。
|
|
1855
|
+
// 有就记,真没有才空串。
|
|
1856
|
+
query: q,
|
|
1830
1857
|
mode: "inject",
|
|
1831
1858
|
topK: maxItems,
|
|
1832
1859
|
threshold: null,
|
|
@@ -1931,6 +1958,9 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1931
1958
|
setArchived: (mid, v) => archiveMemory(mid, v),
|
|
1932
1959
|
transaction: (fn) => transaction(fn)
|
|
1933
1960
|
};
|
|
1961
|
+
// 不传 mintSource——本路径由人在面板上点确认,即便日后有人在这里加
|
|
1962
|
+
// create,落 saveWithDedupe 的 manual 兜底恰好就是对的语义(人触发的写入)。
|
|
1963
|
+
// 这是刻意的,不是漏传。
|
|
1934
1964
|
const { committed, failures } = applyDecisions(
|
|
1935
1965
|
[{ action: "conflict", winner: winnerId, loser: loserId, reason: row.reason ?? "" }],
|
|
1936
1966
|
applyService,
|
|
@@ -1970,6 +2000,11 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1970
2000
|
type: m.type,
|
|
1971
2001
|
importance: m.importance,
|
|
1972
2002
|
content: m.content,
|
|
2003
|
+
// 裁决依据:两侧的新旧与重要度是「该留哪条」的客观信号。此前 side() 不带
|
|
2004
|
+
// 时间戳,面板只能显示 A 方/B 方两个标签,用户没有依据就整对删掉或乱选
|
|
2005
|
+
// (#400)。
|
|
2006
|
+
created_at: m.created_at ?? null,
|
|
2007
|
+
updated_at: m.updated_at ?? null,
|
|
1973
2008
|
archived: m.archived === true,
|
|
1974
2009
|
forgotten: m.forgotten === true
|
|
1975
2010
|
};
|
|
@@ -2132,18 +2167,22 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
2132
2167
|
try {
|
|
2133
2168
|
// 预先取本次要覆盖的全部活跃行(只读一次)。这里曾经是
|
|
2134
2169
|
// store.list({ limit: 500 })——活跃集超过 500 时镜像会静默少掉尾部记忆,
|
|
2135
|
-
// 而文件本身没有任何提示(#278
|
|
2136
|
-
//
|
|
2137
|
-
//
|
|
2138
|
-
// includeArchived:false
|
|
2139
|
-
//
|
|
2140
|
-
//
|
|
2141
|
-
//
|
|
2142
|
-
|
|
2170
|
+
// 而文件本身没有任何提示(#278 第一批)。在这里退回任何截断都不行——
|
|
2171
|
+
// 「宣称覆盖活跃集」与静默截断不能共存(这一条不变)。
|
|
2172
|
+
// #430:改成 list 的无界档(limit: null)+ SQL 侧过滤,替掉 all() + 逐行
|
|
2173
|
+
// filter——list 的默认 includeForgotten:false / includeArchived:false 与旧
|
|
2174
|
+
// filter 的 !forgotten && !archived 完全同口径(store.js 里就是这两条
|
|
2175
|
+
// clause),document 闸改由 excludeTypes 在 LIMIT 之前排除。旧写法先物化
|
|
2176
|
+
// 整库(embedding 列 + 逐行 27 字段 toRow)再逐行丢弃:被丢掉的行连带它们
|
|
2177
|
+
// 的物化一起省下了。要再压一层得换按 type 分页取,属另一批的事。
|
|
2178
|
+
const list = store.list({
|
|
2179
|
+
limit: null,
|
|
2180
|
+
order: "chrono",
|
|
2143
2181
|
// #296 第二批:镜像里的 documents.md 与 index.md 共用 documentMemoryEnabled
|
|
2144
2182
|
// 闸。关掉时把 document 行也从渲染集里去掉——sync 对「空 type」的既有处理
|
|
2145
2183
|
// 会把陈旧的 documents.md 删掉,不留一个「看着还在、其实已关」的视图。
|
|
2146
|
-
|
|
2184
|
+
excludeTypes: config?.documentMemoryEnabled === true ? null : ["document"]
|
|
2185
|
+
});
|
|
2147
2186
|
for (const memory of list) {
|
|
2148
2187
|
if (memory?.type && TYPE_FILE[memory.type]) {
|
|
2149
2188
|
coveredTypes.add(memory.type);
|
|
@@ -2480,6 +2519,13 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
2480
2519
|
transaction,
|
|
2481
2520
|
getDistillCursor: (sessionId) => store.getDistillCursor(sessionId),
|
|
2482
2521
|
setDistillCursor: (sessionId, lastSeq) => store.setDistillCursor(sessionId, lastSeq),
|
|
2522
|
+
// #399 蒸馏原始记录与溯源:与上面两条游标一样是纯 passthrough——它们不是
|
|
2523
|
+
// 记忆写入,不该触发 syncMirror / notifyWrite / 实体抽取(那些钩子挂在
|
|
2524
|
+
// saveWithDedupe 上)。distill_records 的落盘由 summarize.js 在蒸馏窗口内
|
|
2525
|
+
// 一次性批写,读回只给面板/CLI 的溯源视图用。
|
|
2526
|
+
saveDistillRecords: (records) => store.saveDistillRecords(records),
|
|
2527
|
+
saveMemoryProvenance: (link) => store.saveMemoryProvenance(link),
|
|
2528
|
+
provenanceFor: (memoryId) => store.provenanceFor(memoryId),
|
|
2483
2529
|
enqueue,
|
|
2484
2530
|
setDreamHook(fn) { dreamHook = fn; },
|
|
2485
2531
|
setSleepHook(fn) { sleepHook = fn; },
|
|
@@ -2499,11 +2545,31 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
2499
2545
|
// Async-initializing embedder: poll `ready` until it flips, then flush.
|
|
2500
2546
|
if ("ready" in emb && embedReadyTimer === null) {
|
|
2501
2547
|
let attempts = 0;
|
|
2548
|
+
let readyReadFailed = false;
|
|
2549
|
+
// 读 `ready` 必须先兜住异常再算天花板,否则天花板形同不存在:`ready` 是
|
|
2550
|
+
// getter(embedding.js 的 OpenAI embedder 直接读库),getter 一抛错,整条
|
|
2551
|
+
// `||` 的求值当场中断,右侧的 `attempts >= EMBED_READY_POLL_LIMIT` 永远轮
|
|
2552
|
+
// 不到 —— stopEmbedReadyPolling() 不执行,定时器就活过 3s 上限,并以未捕获
|
|
2553
|
+
// 异常的形式每 100ms 抛一次。实测(2026-10-14):卸载路径 store.close() 之后
|
|
2554
|
+
// 每次读 ready 都抛 "database is not open",5s 内读 46 次,天花板 31 被直接
|
|
2555
|
+
// 打穿,宿主事件循环因此永不排空。读不出来按「没就绪」处理,但上限照算。
|
|
2556
|
+
const readReady = () => {
|
|
2557
|
+
try {
|
|
2558
|
+
return emb.ready === true;
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
if (!readyReadFailed) {
|
|
2561
|
+
readyReadFailed = true; // 只报一次,别让 3s 上限变成 30 条刷屏
|
|
2562
|
+
logger?.warn?.("embedder ready probe failed, treating it as not-ready:", err);
|
|
2563
|
+
}
|
|
2564
|
+
return false;
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2502
2567
|
embedReadyTimer = setInterval(() => {
|
|
2503
2568
|
attempts++;
|
|
2504
|
-
|
|
2569
|
+
const ready = readReady();
|
|
2570
|
+
if (ready || attempts >= EMBED_READY_POLL_LIMIT) {
|
|
2505
2571
|
stopEmbedReadyPolling();
|
|
2506
|
-
if (
|
|
2572
|
+
if (ready) flushEmbedPending();
|
|
2507
2573
|
else embedPending = []; // init never landed: drop the queue
|
|
2508
2574
|
}
|
|
2509
2575
|
}, EMBED_READY_POLL_MS);
|