@modusensus/dsh-mneme 0.9.3 → 0.9.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 +56 -10
- package/bin/cli.mjs +215 -2
- package/docs/CONFIGURATION.md +10 -2
- package/docs/DAEMON.md +9 -2
- package/docs/DISTILL.md +66 -4
- package/docs/SHARE.md +552 -0
- package/docs/SLEEP.md +1 -1
- package/docs/STORAGE.md +1 -1
- package/lib/api-standalone.js +286 -8
- package/lib/api.js +254 -3
- package/lib/client.js +1000 -50
- package/lib/config.js +50 -1
- package/lib/document.js +15 -2
- package/lib/dream/decisions.js +88 -3
- package/lib/dream/sleep.js +63 -13
- package/lib/dream.js +66 -10
- package/lib/index.js +6 -1
- package/lib/inject.js +10 -25
- package/lib/lang.js +48 -6
- package/lib/memory-source.js +9 -0
- package/lib/organize.js +9 -3
- package/lib/pipeline.js +62 -1
- package/lib/profile-extract.js +161 -0
- package/lib/provenance-http.js +126 -0
- package/lib/serve.js +13 -0
- package/lib/service.js +70 -6
- package/lib/settings.js +17 -13
- package/lib/share-http.js +281 -0
- package/lib/share-import.js +234 -0
- package/lib/share.js +335 -0
- package/lib/store.js +272 -11
- package/lib/summarize.js +118 -8
- package/lib/tools.js +240 -1
- package/lib/user-settings.js +78 -0
- package/package.json +1 -1
- package/src/api-standalone.js +286 -8
- package/src/api.js +254 -3
- package/src/config.js +50 -1
- package/src/document.js +15 -2
- package/src/dream/decisions.js +88 -3
- package/src/dream/sleep.js +63 -13
- package/src/dream.js +66 -10
- package/src/index.js +6 -1
- package/src/inject.js +10 -25
- package/src/lang.js +48 -6
- package/src/memory-source.js +9 -0
- package/src/organize.js +9 -3
- package/src/pipeline.js +62 -1
- package/src/profile-extract.js +161 -0
- package/src/provenance-http.js +126 -0
- package/src/serve.js +13 -0
- package/src/service.js +70 -6
- package/src/settings.js +17 -13
- package/src/share-http.js +281 -0
- package/src/share-import.js +234 -0
- package/src/share.js +335 -0
- package/src/store.js +272 -11
- package/src/summarize.js +118 -8
- package/src/tools.js +240 -1
- package/src/user-settings.js +78 -0
- package/test/api.test.js +384 -3
- package/test/audit.test.js +2 -2
- package/test/cli-receive.test.js +390 -0
- package/test/cli-share.test.js +437 -0
- package/test/client-share.test.js +225 -0
- package/test/client.test.js +215 -6
- package/test/dedupe-candidate-window.test.js +180 -0
- package/test/dream-candidate-hybrid.test.js +2 -2
- package/test/dream-fence.test.js +164 -0
- package/test/dream.test.js +87 -5
- package/test/helpers/dream-mock.js +6 -2
- package/test/lang.test.js +11 -0
- package/test/llm-message-source.test.js +1 -1
- package/test/mint-source.test.js +1 -1
- package/test/organize-tool.test.js +232 -0
- package/test/persona-addressing.test.js +83 -0
- package/test/persona-settings.test.js +30 -8
- package/test/pipeline-r1r2.test.js +129 -0
- package/test/policy-epoch.test.js +2 -2
- package/test/profile-extract.test.js +269 -0
- package/test/provenance-http.test.js +184 -0
- package/test/readme-claims.test.js +45 -0
- package/test/reasoning-effort.test.js +10 -10
- package/test/scope-candidates.test.js +7 -4
- package/test/scope-state.test.js +130 -1
- package/test/serve.test.js +62 -0
- package/test/share-foreign-rows.test.js +144 -0
- package/test/share-http.test.js +144 -0
- package/test/share-import.test.js +577 -0
- package/test/share-imported-guard.test.js +311 -0
- package/test/share-imported-identity.test.js +111 -0
- package/test/share-pack.test.js +511 -0
- package/test/share-receive-http.test.js +756 -0
- package/test/share-routes.test.js +337 -0
- package/test/share-store.test.js +133 -0
- package/test/share-validate.test.js +101 -0
- package/test/standalone-api.test.js +524 -5
- package/test/summarize-parse-diagnostics.test.js +75 -0
- package/test/summarize.test.js +240 -2
- package/test/tools-dto-schema-matrix.test.js +48 -5
- package/test/tools.test.js +6 -1
- package/test/user-settings-preview.test.js +127 -0
package/src/organize.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// agent 主动整理接口(issue #231,口径基线 = #164 的「第三腿触发」):dryRun
|
|
3
3
|
// 出比对报告 → agent 判断 → apply 落库;筛除项进归档不删;调用全程留审计回执。
|
|
4
4
|
//
|
|
5
|
-
// 只做功能本体(维护者口径,issue #231):落在 service
|
|
6
|
-
//
|
|
5
|
+
// 只做功能本体(维护者口径,issue #231):落在 service 层并带测试。**调用入口在
|
|
6
|
+
// 第二批**(#231:`tools.js` 的 `memory_organize` + `organizeEnabled` 默认关)——
|
|
7
|
+
// 本模块至今不读任何配置键,闸门与 scope 盖章都在调用方那一侧,所以本体语义与
|
|
8
|
+
// 第一批逐字节相同。
|
|
7
9
|
//
|
|
8
10
|
// 独立成模块(AGENTS.md 尺寸约定,同 document.js / recall-stats.js 先例):
|
|
9
11
|
// service.js 只做依赖注入 + barrel 出口,调用方零改动。
|
|
@@ -28,6 +30,10 @@
|
|
|
28
30
|
|
|
29
31
|
import { createHash, randomUUID } from "node:crypto";
|
|
30
32
|
import { scopeKeyOf } from "./scope.js";
|
|
33
|
+
// 比对池要先剔掉外来行:报告里的 item.match.id 既给出「命中」结论,也是 apply 的
|
|
34
|
+
// archive 动作唯一许可的目标集(硬规则 3b),而 archive 直接 store.setArchived,
|
|
35
|
+
// 不经 saveWithDedupe 那道闸门——留着外来行,整理接口就能替别人决定归档面。
|
|
36
|
+
import { isImportedRow } from "./memory-source.js";
|
|
31
37
|
import { cosineSimilarity } from "./dream/clustering.js";
|
|
32
38
|
import { buildReceipt } from "./dream.js";
|
|
33
39
|
|
|
@@ -157,7 +163,7 @@ export function createOrganizer({ store, embedQuery, saveWithDedupe, transaction
|
|
|
157
163
|
if (!rowsByScope.has(key)) {
|
|
158
164
|
rowsByScope.set(
|
|
159
165
|
key,
|
|
160
|
-
store.list({ type: candidate.type, limit: null }).filter((m) => !m.archived && scopeMatches(m, candidate))
|
|
166
|
+
store.list({ type: candidate.type, limit: null }).filter((m) => !isImportedRow(m) && !m.archived && scopeMatches(m, candidate))
|
|
161
167
|
);
|
|
162
168
|
}
|
|
163
169
|
return rowsByScope.get(key);
|
package/src/pipeline.js
CHANGED
|
@@ -118,13 +118,33 @@ export function suggestRetirements(pipeline, { runs = [], now = Date.now(), obse
|
|
|
118
118
|
const violationSeen = new Set();
|
|
119
119
|
for (const run of runs || []) {
|
|
120
120
|
const doneIds = new Set((run.steps || []).filter((s) => s && !s.skipped && !s.detoured).map((s) => s.id));
|
|
121
|
+
// #461:一条 run 的判别键分两类,两类永不相撞。
|
|
122
|
+
//
|
|
123
|
+
// at 存在(合法账本的唯一形态——normalizeRun 在写侧强制必填)走 `at:<值>`。
|
|
124
|
+
// at 缺失(读侧被独立喂进非法形状,或未来写契约放宽)走 `anon:<内容指纹>`:
|
|
125
|
+
// by + triggeredBy + 按序的结构化 step 字段(字段取舍见下方 anonymousRunKey)。
|
|
126
|
+
// 用**内容**而不是数组索引,是因为
|
|
127
|
+
// violationSeen 跨整本账本共享,而索引只在「本次遍历的数组顺序」里唯一——
|
|
128
|
+
// 同一条 run 在账本里出现两次时,索引会让它的同一条违规被记成两条,
|
|
129
|
+
// 那正是本条要修的那类「键塌缩」的反面形态(#507 评审)。
|
|
130
|
+
//
|
|
131
|
+
// 评审里希望中间加一层 run.id,但 normalizeRun(src/pipeline-runs.js:187)
|
|
132
|
+
// 只产出 { at, by, triggeredBy, steps },没有 id 字段,而 suggestRetirements
|
|
133
|
+
// 收到的已经是归一化后的 run;补 id 要改写侧契约,不属这条防御深度问题。
|
|
134
|
+
// 内容指纹是同一意图下不需要动 schema 的等价物。
|
|
135
|
+
//
|
|
136
|
+
// 两类键必须带不同前缀:否则「第 0 条缺 at」与「另一条 at 字面量正好是
|
|
137
|
+
// #0」会生成同一个键,后一条 violation-precedence 被静默吞掉。
|
|
138
|
+
const runKey = isNonEmptyString(run.at)
|
|
139
|
+
? `at:${run.at}`
|
|
140
|
+
: anonymousRunKey(run);
|
|
121
141
|
for (const step of pipeline.steps) {
|
|
122
142
|
if (!doneIds.has(step.id) || !Array.isArray(step.needs)) continue;
|
|
123
143
|
for (const need of step.needs) {
|
|
124
144
|
if (doneIds.has(need)) continue;
|
|
125
145
|
const had = (run.steps || []).some((s) => s && s.id === need);
|
|
126
146
|
if (had) continue; // 前置做过(哪怕是 skipped)就不算违规
|
|
127
|
-
const vk = `${
|
|
147
|
+
const vk = `${runKey}|${step.id}|${need}`;
|
|
128
148
|
if (violationSeen.has(vk)) continue;
|
|
129
149
|
violationSeen.add(vk);
|
|
130
150
|
// 同一步对不同前置各报一条:修图的人需要知道**断在哪条边**上
|
|
@@ -241,6 +261,47 @@ function isNonEmptyString(value) {
|
|
|
241
261
|
return typeof value === "string" && value.trim() !== "";
|
|
242
262
|
}
|
|
243
263
|
|
|
264
|
+
/**
|
|
265
|
+
* at 缺失时一条 run 的判别键(#461 / CodeRabbit #507):给无时间戳的 run 一个
|
|
266
|
+
* **按内容**的稳定身份,好让 violationSeen 只并掉「同一条 run 重复出现」的同类
|
|
267
|
+
* 违规,而不把内容不同的两条 run 并成一条。
|
|
268
|
+
*
|
|
269
|
+
* 覆盖 normalizeSteps(src/pipeline-runs.js:242)产出的**全部结构化字段**——
|
|
270
|
+
* id / at / skipped / detoured / refuted。之所以必须收 step.at:漏了它,两条只在
|
|
271
|
+
* 某一步时间上不同的 at-less run 会共键,后一条的 violation-precedence 被去重吞掉
|
|
272
|
+
* (#507 指出的正是这条)。
|
|
273
|
+
*
|
|
274
|
+
* 用 JSON.stringify 而不是手写 `${id}@${at}` 拼接:run 侧 step id 只过
|
|
275
|
+
* isSafeSegment(src/pipeline-runs.js:83),`@` `,` `:` 都是合法 id,手写拼接会与
|
|
276
|
+
* 「id 里恰好含这些字符」的另一条 step 撞串——那会新造一类键塌缩(把另一类 at-less
|
|
277
|
+
* 输入从「不漏报」改成「漏报」)。JSON 自带无歧义分隔,顺带把 by/triggeredBy 里的
|
|
278
|
+
* `|`、以及 Date 与 ISO 串(Date.toJSON 归一成 ISO)的歧义一并收掉。
|
|
279
|
+
*
|
|
280
|
+
* note 不进来:它是自由文本,序列化进去会让「只差备注的同一条观测」被当成两次。
|
|
281
|
+
*
|
|
282
|
+
* 如实记下代价:skipped/detoured/refuted 对去重正确性是**惰性**的——violation 判定
|
|
283
|
+
* 只读 doneIds(由 id + skipped/detoured 决定)与 id 是否存在,refuted 根本不被读
|
|
284
|
+
* (见下方 suggestRetirements)。所以收这三个布尔防不住任何错误合并,却会让两条只差
|
|
285
|
+
* 这些旗标的 at-less run 各报一条**逐字相同**的提议。仍选择全收,是为了让规则统一
|
|
286
|
+
* (指纹 = 归一化 step 的每个结构化字段,唯一例外是自由文本 note),且将来 violation
|
|
287
|
+
* 逻辑若开始读某个旗标时不会突然开始漏合并;anon 路径本身没有身份锚点,接受这一条
|
|
288
|
+
* 重复的代价。注意这条路径在生产不可达:readStoredRun(src/pipeline-runs.js:262)
|
|
289
|
+
* 强制 run.at 存在,正常账本永远走 `at:` 分支,此处纯属直接调用边界的防御深度。
|
|
290
|
+
*/
|
|
291
|
+
function anonymousRunKey(run) {
|
|
292
|
+
return `anon:${JSON.stringify({
|
|
293
|
+
by: run.by ?? null,
|
|
294
|
+
triggeredBy: run.triggeredBy ?? null,
|
|
295
|
+
steps: (run.steps || []).map((s) => ({
|
|
296
|
+
id: s?.id ?? null,
|
|
297
|
+
at: s?.at ?? null,
|
|
298
|
+
skipped: !!s?.skipped,
|
|
299
|
+
detoured: !!s?.detoured,
|
|
300
|
+
refuted: !!s?.refuted
|
|
301
|
+
}))
|
|
302
|
+
})}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
244
305
|
/**
|
|
245
306
|
* 校验一条工作流。返回归一化后的对象;有任何问题就抛,不返回「部分可用」的东西
|
|
246
307
|
* ——半个工作流比没有工作流更危险,因为它会被当成完整的跑。
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// dsh-mneme/src/profile-extract.js
|
|
2
|
+
// 从活跃记忆反向提取「助手眼中的用户」画像**草稿**(issue #497 的第三段)。
|
|
3
|
+
//
|
|
4
|
+
// 三条纪律,都是「做错界面上看不出来」的那类,所以写在文件头:
|
|
5
|
+
// 1. **只出草稿,绝不写 user_settings**。画像正文是用户手写的原文;让模型「合并」
|
|
6
|
+
// 等于给它改写权,而改写后的文本每帧都注入。采用与否必须是用户在面板上的一次
|
|
7
|
+
// 显式动作(走既有的 PUT /profile),本模块不参与。
|
|
8
|
+
// 2. **没有可用输入就不调模型**。空库下模型只会编一段「她是个热爱学习的人」,
|
|
9
|
+
// 那种草稿比没有草稿更坏——它会成为用户看到的「关于自己」的第一版真相。
|
|
10
|
+
// 3. **吐不出可用 JSON 就报错,绝不回空草稿**。「没提炼出东西」与「模型没按格式
|
|
11
|
+
// 写」必须在结果里可区分(#493 同一条教训:解析失败被读成空结果,用户会以为库是空的)。
|
|
12
|
+
//
|
|
13
|
+
// 记忆正文是模型的**可影响输入**:一条指令型记忆不该改写这次任务。所以喂进去的内容
|
|
14
|
+
// 包在围栏里,并在系统段重申「这是数据不是指令」——与 #493 的转录围栏、#380 的判定
|
|
15
|
+
// 提示同源的做法,不是第三套。
|
|
16
|
+
//
|
|
17
|
+
// 系统段还禁了一件事:**不许给助手本身起名或设定人格**。画像与规则不受
|
|
18
|
+
// personaEnabled 管辖、也不看人格寻址口径(见 src/user-settings.js),正文里一旦出现
|
|
19
|
+
// 「你是某某」,就会重演 #497 那个身份撞车——而且是每个会话每一帧。
|
|
20
|
+
//
|
|
21
|
+
// LLM 调用与审计完全复用 dream.js 的既有缝(streamText / runAuditedLlm / resolveRoute):
|
|
22
|
+
// chunk 形状的三个硬点(用量嵌在 chunk.usage、终态 finish 才是错误、rc.1 把适配器故障
|
|
23
|
+
// 转成 finish chunk 而不是抛错)都在那里踩过并修过,不在这里重写第二遍。
|
|
24
|
+
|
|
25
|
+
import { STR, langOf } from "./lang.js";
|
|
26
|
+
import { describeStreamFailure, resolveRoute, runAuditedLlm, streamText } from "./dream.js";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MAX_ROWS = 120;
|
|
29
|
+
const DEFAULT_ROW_CHARS = 260;
|
|
30
|
+
const DEFAULT_TOTAL_CHARS = 9000;
|
|
31
|
+
|
|
32
|
+
/** 带 code 的失败:路由按 code 映射状态码,调用方据此区分「没东西可提」与「模型没配合」。 */
|
|
33
|
+
export class ProfileExtractError extends Error {
|
|
34
|
+
constructor(code, message) {
|
|
35
|
+
super(message ?? code);
|
|
36
|
+
this.name = "ProfileExtractError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 从模型输出里取那个 JSON 对象。先剥 markdown 围栏(模型常加),再退到「第一个 { 到
|
|
43
|
+
* 最后一个 }」的宽取——画像文本里带花括号是常态,严格 parse 失败时这一步能救回来。
|
|
44
|
+
* 取不到可用结构就返回 null,由调用方判失败;绝不返回空串冒充成功。
|
|
45
|
+
*/
|
|
46
|
+
export function parseProfileDraft(text) {
|
|
47
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
48
|
+
const cleaned = text.replace(/```(?:json)?\s*([\s\S]*?)```/gi, "$1").trim();
|
|
49
|
+
const candidates = cleaned.startsWith("{") ? [cleaned] : [];
|
|
50
|
+
const open = cleaned.indexOf("{");
|
|
51
|
+
const close = cleaned.lastIndexOf("}");
|
|
52
|
+
if (open >= 0 && close > open) candidates.push(cleaned.slice(open, close + 1));
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
let value;
|
|
55
|
+
try {
|
|
56
|
+
value = JSON.parse(candidate);
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (value && typeof value === "object" && typeof value.profile === "string" && value.profile.trim()) {
|
|
61
|
+
return value.profile.trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 活跃行读数:归档与被遗忘的行默认就不在这里(store.list 的默认口径)。 */
|
|
68
|
+
function collectRows(service, maxRows, totalChars, rowChars) {
|
|
69
|
+
// 多取一行只为如实回答「还有没有」:截断必须可被调用方察觉,静默少读会让用户
|
|
70
|
+
// 以为画像已经看全了整个库。
|
|
71
|
+
const fetched = service.list({ limit: maxRows + 1, order: "importance" });
|
|
72
|
+
const rows = [];
|
|
73
|
+
let chars = 0;
|
|
74
|
+
let truncated = fetched.length > maxRows;
|
|
75
|
+
for (const row of fetched.slice(0, maxRows)) {
|
|
76
|
+
const content = String(row.content ?? "").slice(0, rowChars);
|
|
77
|
+
if (content.length < String(row.content ?? "").length) truncated = true;
|
|
78
|
+
const size = `${row.type} ${row.title} ${content}`.length;
|
|
79
|
+
if (chars + size > totalChars) {
|
|
80
|
+
truncated = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
chars += size;
|
|
84
|
+
rows.push({ id: row.id, type: row.type, title: row.title, content });
|
|
85
|
+
}
|
|
86
|
+
return { rows, truncated, charsUsed: chars };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {{ ctx: object, service: object, config: object, logger?: object }} deps
|
|
91
|
+
* ctx 要带 llm(宿主 LLM 句柄);settings 不参与——本模块按设计不写画像。
|
|
92
|
+
* @returns {(opts?: {maxRows?: number, totalChars?: number, rowChars?: number}) => Promise<object>}
|
|
93
|
+
*/
|
|
94
|
+
export function createProfileExtractor({ ctx, service, config, logger }) {
|
|
95
|
+
const log = logger ?? ctx?.logger;
|
|
96
|
+
const language = langOf(config);
|
|
97
|
+
|
|
98
|
+
return async function extractProfile({
|
|
99
|
+
maxRows = DEFAULT_MAX_ROWS,
|
|
100
|
+
totalChars = DEFAULT_TOTAL_CHARS,
|
|
101
|
+
rowChars = DEFAULT_ROW_CHARS
|
|
102
|
+
} = {}) {
|
|
103
|
+
const { rows, truncated, charsUsed } = collectRows(service, maxRows, totalChars, rowChars);
|
|
104
|
+
if (!rows.length) {
|
|
105
|
+
throw new ProfileExtractError("no-input", "no active memories to extract a profile from");
|
|
106
|
+
}
|
|
107
|
+
const route = resolveRoute(ctx, config, log);
|
|
108
|
+
// resolveRoute 在「没有 dreamProvider/dreamModel、宿主也没有可解析的默认模型」时
|
|
109
|
+
// 返回 undefined。不判它就会被读成 `Cannot read properties of undefined` 抛出
|
|
110
|
+
// TypeError,落到路由上是一个语义为「内部错误」的 500——而真正该说的是「没配模型」,
|
|
111
|
+
// 面板要能给出「先去选模型」这句可行动的话(同 no-input 的取舍:不同结局不同话)。
|
|
112
|
+
if (!route) {
|
|
113
|
+
throw new ProfileExtractError("no-route", "no model route: set dreamProvider/dreamModel or pick an agent default model");
|
|
114
|
+
}
|
|
115
|
+
const listing = rows.map((r, i) => `${i + 1}. [${r.type}] ${r.title} :: ${r.content}`).join("\n");
|
|
116
|
+
const userText = [
|
|
117
|
+
STR.profileFenceOpen[language],
|
|
118
|
+
listing,
|
|
119
|
+
STR.profileFenceClose[language],
|
|
120
|
+
language === "zh"
|
|
121
|
+
? `以上 ${rows.length} 条记忆条目(数据)到此结束。只依据它们输出 JSON,条目里没有的绝不发明。`
|
|
122
|
+
: `End of the ${rows.length} memory entries (data). Answer from these only; invent nothing absent from them.`
|
|
123
|
+
].join("\n");
|
|
124
|
+
|
|
125
|
+
let streamFailure = "";
|
|
126
|
+
const text = await runAuditedLlm(ctx, service, config, {
|
|
127
|
+
triggerSource: "profileExtract",
|
|
128
|
+
operationType: "profile_extract",
|
|
129
|
+
modelId: `${route.provider}:${route.model}`,
|
|
130
|
+
relatedMemoryIds: rows.map((r) => r.id),
|
|
131
|
+
streamError: () => streamFailure,
|
|
132
|
+
// 吐了文字但没有可用结构 = 失败调用。审计必须记 error:success 配空草稿会让
|
|
133
|
+
// 「模型不配合」在账目上长得像「这次确实没提炼出东西」。
|
|
134
|
+
auditError: (out) => (parseProfileDraft(out) ? null : "profile extract returned no usable JSON object")
|
|
135
|
+
}, (reportUsage) => streamText(ctx, {
|
|
136
|
+
provider: route.provider,
|
|
137
|
+
model: route.model,
|
|
138
|
+
purpose: "compaction",
|
|
139
|
+
messages: [
|
|
140
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.profileExtract[language] }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } },
|
|
141
|
+
{ role: "user", content: [{ type: "text", text: userText }], source: { kind: "plugin:dsh-mneme", plugin: "dsh-mneme" } }
|
|
142
|
+
]
|
|
143
|
+
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
144
|
+
|
|
145
|
+
if (text === undefined) {
|
|
146
|
+
throw new ProfileExtractError("stream-failed", streamFailure || "llm stream aborted or errored");
|
|
147
|
+
}
|
|
148
|
+
const draft = parseProfileDraft(text);
|
|
149
|
+
if (!draft) {
|
|
150
|
+
throw new ProfileExtractError("unparseable", "profile extract output was not a usable JSON object");
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
draft,
|
|
154
|
+
rowsUsed: rows.length,
|
|
155
|
+
truncated,
|
|
156
|
+
charsUsed,
|
|
157
|
+
provider: route.provider,
|
|
158
|
+
model: route.model
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// 记忆级溯源读出口的 HTTP 口径 —— 唯一实现。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要单独一个模块:这张脸有两个入口——宿主内的面板路由(src/api.js 的
|
|
4
|
+
// /api/dsh-mneme/memory/provenance)与 Bearer 数据面(src/api-standalone.js 的
|
|
5
|
+
// /memory/provenance)。两者底下是同一份 service(provenanceFor + distillRecordsFor),
|
|
6
|
+
// 差别只在鉴权与路径。历史上(#509 第一版)它们是整段复制的,于是「改一边忘一边」
|
|
7
|
+
// 在结构上就可能发生——#411 已经为 reclaim / share 各踩过一次,本模块照那次的结论办。
|
|
8
|
+
//
|
|
9
|
+
// 口径(错误码、两种「空」的区分、窗口形状)现在只写在这里,两个入口都调它。
|
|
10
|
+
// 真要再写第二份,test/provenance-http.test.js 的静态锁会直接转红。
|
|
11
|
+
//
|
|
12
|
+
// 这里只做协议层的事:解析 id、判开关、组装窗口、区分「用户关了」「没接线」与
|
|
13
|
+
// 「没有来源」。一行数据都不改——读取在 src/store.js,透传在 src/service.js。
|
|
14
|
+
//
|
|
15
|
+
// 状态码阶梯(自前而后,先匹配的先回):
|
|
16
|
+
// 401 未鉴权 ——两个壳各自的门(api.js 的 register 包装 / standalone 的
|
|
17
|
+
// isAuthorized),在本模块之前,不在这里重复。
|
|
18
|
+
// 400 缺 id ——语法畸形,与服务端状态无关,所以排在功能开关之前;与
|
|
19
|
+
// reclaim 的 invalid-json 同档(reclaim-http.js:30-33 立的是
|
|
20
|
+
// 「503 要赢过**字段校验**的 400」,缺 id 不是字段校验而是请求
|
|
21
|
+
// 本身不成立,所以不适用那条)。判断在壳里
|
|
22
|
+
// (parseProvenanceQuery),走到本模块时 id 一定非空。
|
|
23
|
+
// ⚠ 将来要加**语义类** 400(如 limit 越界、id 格式不合法),
|
|
24
|
+
// 必须挪到接线检查之后——那条 reclaim 口径正是为它立的。
|
|
25
|
+
// 409 功能关了 ——distillRecords.enabled !== true。回的是「端点的属性」,
|
|
26
|
+
// 与 src/api.js:525 的 profileExtractEnabled 同构:用户关了
|
|
27
|
+
// 某功能 = 409,装配坏了 = 503,两者必须是可区分的答案
|
|
28
|
+
// (src/index.js:589-591 的设计原话)。
|
|
29
|
+
// 503 装配没接线 ——service 上没有 provenanceFor / distillRecordsFor。
|
|
30
|
+
// 读(200 / 500)
|
|
31
|
+
// 刻意不用 404:本端点永远不就「这条记忆是否存在」作断言(缺 id 是 400、未知 id
|
|
32
|
+
// 是 200 + 空 windows),404 在本仓另有稳定含义——未知路由与资源不存在
|
|
33
|
+
// (src/api-standalone.js:506/515、src/api.js:1367/1445)。把「功能关了」塞进
|
|
34
|
+
// 404,调用方会在状态码这个唯一被代理 / 监控 / 重试逻辑读取的通道上,把它读成
|
|
35
|
+
// 「这条记忆不存在」。
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 「溯源能力没接线」的错误码。headless / 旧装配下 service 上没有 provenanceFor 或
|
|
39
|
+
* distillRecordsFor,那是装配坏了,不是「这条记忆没有来源」——两种空必须分开读数,
|
|
40
|
+
* 否则调用方看到一个空 windows 会以为拿到了答案。
|
|
41
|
+
*/
|
|
42
|
+
export const PROVENANCE_UNAVAILABLE = "provenance-unavailable";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 「用户把蒸馏原文留存关掉了」的错误码。distillRecords.enabled 既是写侧的总闸
|
|
46
|
+
* (src/summarize.js:988)也是本读出口的总闸(#509 评审整改):关着时盘上可能
|
|
47
|
+
* 还有历史原文(保留期裁剪与写入同在 src/index.js:270 一道门里),但本端点不再
|
|
48
|
+
* 把它们读出去。与 api.js:525 的 "disabled" 分开:那个面只有一个功能开关,本仓
|
|
49
|
+
* 数据面上会有多个,特定码才让调用方分得清是哪一个被关。
|
|
50
|
+
*/
|
|
51
|
+
export const DISTILL_RECORDS_DISABLED = "distill-records-disabled";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 读侧唯一的开关判据,**与写侧 src/summarize.js:988 逐字同源**。
|
|
55
|
+
*
|
|
56
|
+
* 为什么不放在两个壳里各算一次:那句话已经是本仓第 3、第 4 份拷贝了
|
|
57
|
+
* (summarize.js:988、index.js:270),而 #411 的结论就是「两张皮整段复制必然
|
|
58
|
+
* 漂移」——读写两道门一旦漂移,就会出现「以为关了却还能读出去」或「开着却读不到」
|
|
59
|
+
* 这两种都更难排查的状态。壳只转发 config,判据只在这里出现一次。
|
|
60
|
+
*
|
|
61
|
+
* fail-closed:config 缺失(null / undefined)、distillRecords 组缺失、值不是
|
|
62
|
+
* 严格的 true("true" 字符串 / 1 / undefined)一律算关。这个端点返回的是蒸馏
|
|
63
|
+
* 窗口的逐字会话原文,判不出来时必须往「不读」那一侧倒。
|
|
64
|
+
*
|
|
65
|
+
* @param {object|null|undefined} config 启动期冻结的装配后 cfg(宿主 index.js:597
|
|
66
|
+
* 的 createApi 第 8 参 / index.js:621 的 createStandaloneApi config / daemon
|
|
67
|
+
* serve.js 的最小 cfg)。
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
export function isDistillRecordsEnabled(config) {
|
|
71
|
+
return config?.distillRecords?.enabled === true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 溯源读取失败的 warn 文案。两个入口共用,日志里不会出现两种拼法。 */
|
|
75
|
+
export function provenanceWarnMessage(error) {
|
|
76
|
+
return `[dsh-mneme] memory provenance read failed: ${String(error)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 解析 ?id。空 / 缺失 → 400;其余原样交给 service(id 不存在时返回空 windows,
|
|
81
|
+
* 那是有效答案,不是错误——读端点不替调用方判断记忆是否存在)。
|
|
82
|
+
*
|
|
83
|
+
* @returns {{ok: true, id: string} | {ok: false, status: 400, error: string}}
|
|
84
|
+
*/
|
|
85
|
+
export function parseProvenanceQuery(raw) {
|
|
86
|
+
const id = typeof raw === "string" ? raw.trim() : "";
|
|
87
|
+
if (!id) return { ok: false, status: 400, error: "missing-id" };
|
|
88
|
+
return { ok: true, id };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 组装答复:一个链接一个窗口(区间 + 区间内原文行,按 seq 升序)。
|
|
93
|
+
*
|
|
94
|
+
* 顺序敏感(阶梯全貌见文件头):409 功能关了 → 503 装配没接线 → 读。
|
|
95
|
+
* 503 要赢过空结果——装配没接线时报「这条记忆没有来源」是误导,与 reclaimReply 的
|
|
96
|
+
* maintenance-unavailable 同构;而 409 要赢过 503——「用户关了」比「装配坏了」更
|
|
97
|
+
* 具体,把前者报成后者会让人去查一个没坏的部署。
|
|
98
|
+
*
|
|
99
|
+
* @param {{service: object, id: string, config?: object|null, warn?: Function}} args
|
|
100
|
+
* config 缺省即关(fail-closed),见 isDistillRecordsEnabled。
|
|
101
|
+
* @returns {{status: number, body: object}}
|
|
102
|
+
*/
|
|
103
|
+
export function provenanceReply({ service, id, config = null, warn = null }) {
|
|
104
|
+
if (!isDistillRecordsEnabled(config)) {
|
|
105
|
+
return { status: 409, body: { error: DISTILL_RECORDS_DISABLED } };
|
|
106
|
+
}
|
|
107
|
+
if (typeof service?.provenanceFor !== "function" || typeof service?.distillRecordsFor !== "function") {
|
|
108
|
+
return { status: 503, body: { error: PROVENANCE_UNAVAILABLE } };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const links = service.provenanceFor(id);
|
|
112
|
+
const windows = links.map((link) => ({
|
|
113
|
+
session_id: link.session_id,
|
|
114
|
+
seq_start: link.seq_start,
|
|
115
|
+
seq_end: link.seq_end,
|
|
116
|
+
created_at: link.created_at,
|
|
117
|
+
// 到期裁剪后区间可能变空:链接刻意保留并指向空区间(clearDistillRecords 的
|
|
118
|
+
// 语义),面板据此显示「原文已过期」而非「没有来源」。
|
|
119
|
+
records: service.distillRecordsFor(link.session_id, link.seq_start, link.seq_end)
|
|
120
|
+
}));
|
|
121
|
+
return { status: 200, body: { memoryId: id, windows } };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
warn?.(provenanceWarnMessage(error));
|
|
124
|
+
return { status: 500, body: { error: "internal" } };
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/serve.js
CHANGED
|
@@ -61,6 +61,19 @@ export async function createServeRuntime({
|
|
|
61
61
|
// documentMemoryEnabled 关掉 document 子系统(daemon 不建 documentIndex,service
|
|
62
62
|
// 内部 if-guard 安全,index.js:261 的 documentIndex 仅宿主装配)。
|
|
63
63
|
const cfg = { language: "zh", documentMemoryEnabled: false };
|
|
64
|
+
// #509 评审整改补的一格:GET /memory/provenance 的读门读 cfg.distillRecords.enabled
|
|
65
|
+
// (判据在 src/provenance-http.js,与写侧 src/summarize.js:988 同源)。daemon 与宿主
|
|
66
|
+
// 共用同一个 memory.db、同一份 feature_flags kv,所以这里把用户显式开关展开进 cfg
|
|
67
|
+
// ——index.js:217-233 同款展开,只取蒸馏这一组(daemon 其余键仍刻意不吃宿主 config
|
|
68
|
+
// schema)。不展开的后果:daemon 对这一面恒定回「功能关了」,哪怕用户在面板里开着、
|
|
69
|
+
// 库里明明有原文——对这个端点那是一句谎话,而「两种空必须分开读数」正是本仓纪律。
|
|
70
|
+
// 残留不对称(写进 docs/DAEMON.md 与 PR):只靠 bundle 配置打开、从没在面板点过的
|
|
71
|
+
// 用户,宿主认、daemon 不认——daemon 结构上看不到宿主的 bundle config,这是它看不见
|
|
72
|
+
// 的那一半,不是本处能补的。
|
|
73
|
+
const persistedFlags = settings.getFeatureFlags?.() ?? {};
|
|
74
|
+
if (typeof persistedFlags["distillRecords.enabled"] === "boolean") {
|
|
75
|
+
cfg.distillRecords = { ...(cfg.distillRecords ?? {}), enabled: persistedFlags["distillRecords.enabled"] };
|
|
76
|
+
}
|
|
64
77
|
const mirror = createMirror(dir, langOf(cfg));
|
|
65
78
|
const service = createService({ store, mirror, config: cfg, logger });
|
|
66
79
|
|
package/src/service.js
CHANGED
|
@@ -7,9 +7,13 @@ import { STR, langOf } from "./lang.js";
|
|
|
7
7
|
import { computeHeat } from "./heat.js";
|
|
8
8
|
import { recallStats } from "./recall-stats.js";
|
|
9
9
|
import { evaluateMemoryQuality } from "./quality-filter.js";
|
|
10
|
+
// 外来行(source='imported',导入路径落的来源)的身份判定只许有一个定义处——本文件的
|
|
11
|
+
// 两处汇流口与巩固/降级那些读侧行池都调这同一个谓词,避免各处自己写字符串比较然后各自漂移。
|
|
12
|
+
import { isImportedRow } from "./memory-source.js";
|
|
10
13
|
import { applyDecisions } from "./dream/decisions.js";
|
|
11
14
|
import { createDocumentRegistrar } from "./document.js";
|
|
12
15
|
import { createOrganizer } from "./organize.js";
|
|
16
|
+
import { createShareImport } from "./share-import.js";
|
|
13
17
|
import { createBM25Index } from "./search/bm25.js";
|
|
14
18
|
import { adaptiveThreshold } from "./search/adaptive.js";
|
|
15
19
|
import { anchorSeeds, normalizePath, cascadeDepths, HOP_QUOTA } from "./graph/anchoring.js";
|
|
@@ -1368,7 +1372,31 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1368
1372
|
// dream 总览按 source 身份去重,而非本地化标题:memory.language 切换后
|
|
1369
1373
|
// 标题变化,若仍按 (type, title) 匹配会出现两个活跃的 summary 同时注入。
|
|
1370
1374
|
// 其余类型维持 (type, title) 匹配不变。
|
|
1371
|
-
|
|
1375
|
+
//
|
|
1376
|
+
// Issue #505:候选集此前是 store.list({ type, limit: 100 }) —— 那是「该类型
|
|
1377
|
+
// 最重要的 100 行」(order 默认 importance),与「找出同标题的那一行」无关。
|
|
1378
|
+
// 某类型里 importance 高于待写行的行数一旦超过 100,同标题行与待写行双双落在
|
|
1379
|
+
// 窗口外,下面的 find 恒不命中,于是静默走创建分支:不报错、不告警,合并语义
|
|
1380
|
+
// 随库增长而失效(实测 1047 行类型、importance=5 有 180 行时稳定复现)。
|
|
1381
|
+
// 改为按同标题定向取(store.activeByTypeAndTitle,无窗口)。
|
|
1382
|
+
//
|
|
1383
|
+
// spec §5 决策 2:本机巩固不许改写别人的原文。这里不是防呆——蒸馏路径的每一次
|
|
1384
|
+
// 同题命中都会 appendContent 覆盖外来行的正文与 updated_at,别人的记忆从此变成
|
|
1385
|
+
// 我们两家的混杂物。把外来行剔出候选集后只会落回既有的「新建一行」分支。
|
|
1386
|
+
// 谓词挂在**两个**候选集上:sourceScopedCandidates 在 dream/narrative 那两条分支
|
|
1387
|
+
// 里就是候选集本身,只挂 candidates 等于给外来行留一条后门。这一处过滤留在 JS 侧
|
|
1388
|
+
// 而不是下推 SQL(同 #515 的反面):那里下推是因为外来行会吃掉 LIMIT 窗口名额,
|
|
1389
|
+
// 这里 limit: null 本就无界,没有窗口可饿,而下推会多出第二份外来行判据。
|
|
1390
|
+
const candidates = store.activeByTypeAndTitle(memory.type, memory.title).filter((m) => !isImportedRow(m));
|
|
1391
|
+
// dream/narrative 两条分支按 source(及 tags[0])匹配,而不是按标题:它们的
|
|
1392
|
+
// 去重键本来就不含标题(语言切换后标题会变)。定向取同标题会把这两条路径
|
|
1393
|
+
// 打回「永远不命中」——所以这两类仍走「该类型活跃行」的候选集,只是窗口换成
|
|
1394
|
+
// 了无界且不带 importance 排序。类型内行数由 summary 的生产方(autoDream /
|
|
1395
|
+
// generateNarratives)决定,量级远小于普通记忆,且这两条分支本来就要求扫全
|
|
1396
|
+
// 类型才能找到同 source 的那一行——原来的 100 行窗口对它们同样是错的。
|
|
1397
|
+
const sourceScopedCandidates = (memory.type === "summary" && (memory.source === "dream" || memory.source === "narrative"))
|
|
1398
|
+
? store.list({ type: memory.type, limit: null }).filter((m) => !isImportedRow(m))
|
|
1399
|
+
: candidates;
|
|
1372
1400
|
// v0.8.0 A1(issue #17):去重键由 (type, title) 扩展为
|
|
1373
1401
|
// (type, title, agent_scope, workspace_scope, sensitivity)——不同作用域的同
|
|
1374
1402
|
// 标题记忆绝不互相物理合并(旧逻辑跨 agent/workspace 并行,事后无法拆分)。
|
|
@@ -1386,17 +1414,20 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
1386
1414
|
// 下面这段并入逻辑(appendContent + content_history + 质量处置),不另写第二份
|
|
1387
1415
|
// 实现。目标已被并发删除时回落到常规匹配。显式目标同样过 scope 门:跨作用域
|
|
1388
1416
|
// 的语义相似不并入(防泄漏),回落为独立新行。
|
|
1389
|
-
|
|
1417
|
+
// 显式目标还过外来门(spec §5 决策 2):调用方指到别人导入的原文时,这个目标
|
|
1418
|
+
// 按「未命中」处理,同样落回新建一行——两条路都只有 merged/created 两种结局。
|
|
1419
|
+
const explicitCandidate = memory._mergeInto ? store.getById(memory._mergeInto) : undefined;
|
|
1420
|
+
const explicitTarget = explicitCandidate && !isImportedRow(explicitCandidate) ? explicitCandidate : undefined;
|
|
1390
1421
|
const existing = explicitTarget
|
|
1391
1422
|
? (scopeMatches(explicitTarget) ? explicitTarget : undefined)
|
|
1392
1423
|
: (memory.type === "summary" && memory.source === "dream")
|
|
1393
|
-
? (
|
|
1424
|
+
? (sourceScopedCandidates.find((m) => m.source === "dream" && scopeMatches(m))
|
|
1394
1425
|
?? candidates.find((m) => scopeMatches(m) && m.title.trim() === String(memory.title).trim()))
|
|
1395
1426
|
: (memory.type === "summary" && memory.source === "narrative")
|
|
1396
1427
|
// 叙述条(#164)按主题键去重而非本地化标题:标题随语言变
|
|
1397
1428
|
// (叙述:X / Narrative: X),title 匹配在语言切换后会残留同 tag 双行。
|
|
1398
1429
|
// tag 存于 tags[0](generateNarratives 落库),跨语言稳定 → 原地刷新。
|
|
1399
|
-
? (
|
|
1430
|
+
? (sourceScopedCandidates.find((m) => m.source === "narrative" && m.tags?.[0] === memory.tags?.[0] && scopeMatches(m))
|
|
1400
1431
|
?? candidates.find((m) => scopeMatches(m) && m.title.trim() === String(memory.title).trim()))
|
|
1401
1432
|
: candidates.find((m) => scopeMatches(m) && m.title.trim() === String(memory.title).trim());
|
|
1402
1433
|
if (existing) {
|
|
@@ -2564,18 +2595,34 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
2564
2595
|
}
|
|
2565
2596
|
});
|
|
2566
2597
|
|
|
2598
|
+
// 车道 C 收侧的入库与整包退出(spec §5):内聚块在 src/share-import.js,这里只做依赖
|
|
2599
|
+
// 注入 + barrel 出口。注入的是本文件的 transaction 而不是 store.runAtomically——嵌入
|
|
2600
|
+
// 队列由 transaction 的最外层 COMMIT 排空,换那个助手等于静默不嵌入(#372 修过两次);
|
|
2601
|
+
// 退出那侧用它则是为了 finally 里的 syncMirror + notifyWrite(删完得让其余读侧知道)。
|
|
2602
|
+
const shareImport = createShareImport({ store, transaction, scheduleEmbed });
|
|
2603
|
+
|
|
2567
2604
|
return {
|
|
2568
2605
|
saveWithDedupe,
|
|
2569
2606
|
registerDocument,
|
|
2570
2607
|
// agent 主动整理接口(#231):内聚块在 src/organize.js,这里只做依赖注入 +
|
|
2571
|
-
// barrel
|
|
2572
|
-
//
|
|
2608
|
+
// barrel 出口。本体不带配置键(第一批口径),暴露闸住在工具层(#231 第二批的
|
|
2609
|
+
// `organizeEnabled` → `tools.js` 的 `memory_organize`):service 侧不认识开关,
|
|
2610
|
+
// 所以直连 service 的调用方(测试、内部管线)不受面板那格影响。
|
|
2573
2611
|
organize,
|
|
2574
2612
|
recoverMirror,
|
|
2575
2613
|
getMirrorHealth,
|
|
2576
2614
|
getMirrorState: () => store.getMirrorState(),
|
|
2577
2615
|
injectCandidates,
|
|
2578
2616
|
mergeHumanEdits,
|
|
2617
|
+
// 车道 C 收侧:两张皮(面板路由 / standalone 数据面)各自的壳只认这一份实现——
|
|
2618
|
+
// createApi 手里没有 store,「api 层不直接摸 store」这条纪律(见下面 embeddedCount
|
|
2619
|
+
// 那组 passthrough 的注释)要求导入从 service 进,否则两皮各摸一遍 store、幂等
|
|
2620
|
+
// 口径随之变成两份。
|
|
2621
|
+
importSharePack: shareImport.importSharePack,
|
|
2622
|
+
// 整包退出(C4)与导入同住 src/share-import.js:收侧的「进与出」共用同一份 store
|
|
2623
|
+
// 注入与同一个事务口径,键名照计划里的契约留在 service 的返回表上,两张皮(C5/C6)
|
|
2624
|
+
// 只认 service 这一个入口——api 层不直接摸 store 那条纪律对退出同样成立。
|
|
2625
|
+
deleteSharePack: shareImport.deleteSharePack,
|
|
2579
2626
|
toApiList,
|
|
2580
2627
|
isVisibleInScope,
|
|
2581
2628
|
// issue #471 政策轴:按下/松开与读取照 store 那三条件透传(纯 bookkeeping,
|
|
@@ -2598,6 +2645,9 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
2598
2645
|
saveDistillRecords: (records) => store.saveDistillRecords(records),
|
|
2599
2646
|
saveMemoryProvenance: (link) => store.saveMemoryProvenance(link),
|
|
2600
2647
|
provenanceFor: (memoryId) => store.provenanceFor(memoryId),
|
|
2648
|
+
// #421 读出口:按 (session, seq 区间) 取蒸馏原文。与 provenanceFor 成对——
|
|
2649
|
+
// 一个给区间、一个给区间里的原文,合起来才是「这条记忆从哪几句蒸出来的」。
|
|
2650
|
+
distillRecordsFor: (sessionId, seqStart, seqEnd) => store.distillRecordsFor(sessionId, seqStart, seqEnd),
|
|
2601
2651
|
enqueue,
|
|
2602
2652
|
setDreamHook(fn) { dreamHook = fn; },
|
|
2603
2653
|
setSleepHook(fn) { sleepHook = fn; },
|
|
@@ -2669,6 +2719,20 @@ export function createService({ store, mirror, config, onWrite, logger, document
|
|
|
2669
2719
|
// passthrough 形状与 embeddedCount 同款,api 层不直接摸 store。
|
|
2670
2720
|
embeddingModelBreakdown: () => store.embeddingModelBreakdown(),
|
|
2671
2721
|
embeddingDimensionBreakdown: () => store.embeddingDimensionBreakdown(),
|
|
2722
|
+
// #383:维度探针,供 /context 判「库里有没有一条活跃向量与查询同维」。注意
|
|
2723
|
+
// 它**不再是**「读一行的维度」(那个判据在混合维度库里两个方向都错,见 store.js
|
|
2724
|
+
// vectorDimensionState 的注释):换成存在性判据后成本是「一次全 id 扫描 + 按匹配
|
|
2725
|
+
// 短路的解析」,同维度健康库只解析 1 个向量,只有要报 degraded 时才走完全表。
|
|
2726
|
+
// 与上面那个全表扫的 breakdown 仍是两件事:一个是状态页的完整分布,一个是每请求
|
|
2727
|
+
// 用得起的判据。透传而非让 api 层摸 store,与同组其他条目一致;api 侧负责把探针
|
|
2728
|
+
// 异常隔离成 unknown,不在这里吞。
|
|
2729
|
+
vectorDimensionState: (dimension) => store.vectorDimensionState(dimension),
|
|
2730
|
+
// 整包透传:store.list 的每个口径(excludeTypes / excludeImported / includeArchived…)
|
|
2731
|
+
// 都从这里过去,service 侧不再枚举一遍——枚举就是第二份口径表,加一个键就要改两处,
|
|
2732
|
+
// 漏改的那处静默失效。政策轴一律走这里的查询参数,不许退回调用方的 .filter:
|
|
2733
|
+
// LIMIT 先生效的话,被丢掉的行照样吃掉窗口名额。前例是同文件 syncMirror 的那条注释
|
|
2734
|
+
// (src/service.js:2247,document 轴改由 excludeTypes 在 LIMIT 之前排除),
|
|
2735
|
+
// 同一取舍的另两次是 #230 与 #471。
|
|
2672
2736
|
list: (o) => store.list(o),
|
|
2673
2737
|
all: () => store.all(),
|
|
2674
2738
|
count: (type, opts) => store.count(type, opts),
|
package/src/settings.js
CHANGED
|
@@ -185,6 +185,10 @@ const FEATURE_FLAG_BOOLEANS = [
|
|
|
185
185
|
// 在慢/轻量模型上是多余往返,面板可关(默认关=行为不变)。
|
|
186
186
|
"disableMemorySearch",
|
|
187
187
|
"disableMemoryArchive",
|
|
188
|
+
// Issue #231 第二批:agent 主动整理(memory_organize 工具)的暴露闸。与上面两个
|
|
189
|
+
// 反向——那两个藏默认在场的工具,本键开默认不在场的能力(默认关=工具根本不存在,
|
|
190
|
+
// 行为逐字节不变)。面板可启停=线上回滚开关。
|
|
191
|
+
"organizeEnabled",
|
|
188
192
|
// 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
|
|
189
193
|
// 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
|
|
190
194
|
// 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
|
|
@@ -229,7 +233,13 @@ const FEATURE_FLAG_BOOLEANS = [
|
|
|
229
233
|
// LIGHT_MODE_OFF——人格段是用户自写的一小段轻量常驻文本,不引入任何重型后台
|
|
230
234
|
// 路径,轻量档用户显式勾选仍然赢(同 actionPipelineEnabled 的定性)。顶层扁平
|
|
231
235
|
// 键,不需要 NESTED_FLAG_PATHS 条目。
|
|
232
|
-
"personaEnabled"
|
|
236
|
+
"personaEnabled",
|
|
237
|
+
// 人格寻址口径(上一条打开时才有意义):开 = 身份没建卡/卡被关时不注入人格,
|
|
238
|
+
// default 只兜解析不到身份的会话。默认关(历史行为,升级零变化)。
|
|
239
|
+
"personaStrictAddressing",
|
|
240
|
+
// Issue #497:画像提取草稿(POST /profile/extract)。默认关——每次调用一次 LLM 且
|
|
241
|
+
// 读上百行记忆;产物只回草稿,绝不写 user_settings。进 LIGHT_MODE_OFF(预设关)。
|
|
242
|
+
"profileExtractEnabled"
|
|
233
243
|
];
|
|
234
244
|
// 整数开关的闭区间,与 config.js 里 z.natural().min().max() 对齐。
|
|
235
245
|
const FEATURE_FLAG_INT_RANGES = {
|
|
@@ -247,6 +257,9 @@ const FEATURE_FLAG_INT_RANGES = {
|
|
|
247
257
|
// Issue #239:蒸馏零 LLM 预判(窗口最小字符数)与每会话 run 预算(0 = 零行为变化)。
|
|
248
258
|
summarizeMinWindowChars: [0, 100000],
|
|
249
259
|
summarizeMaxRunsPerSession: [0, 1000],
|
|
260
|
+
// Issue #493:连续解析失败几次后放弃当前窗口(0 = 关)。与 config.js 的
|
|
261
|
+
// z.natural().min(0).max(10) 成对。
|
|
262
|
+
summarizeParseFailureMaxRetries: [0, 10],
|
|
250
263
|
// Issue #239 第 4 项:高峰顺延上限(分钟,0 = 不设上限)。
|
|
251
264
|
summarizePeakMaxDeferMinutes: [0, 1440],
|
|
252
265
|
// Issue #239 第 4 项镜像到巩固:同一口径(分钟,0 = 不设上限)。
|
|
@@ -489,8 +502,9 @@ export function createSettings(db) {
|
|
|
489
502
|
/**
|
|
490
503
|
* Agent persona (name / personality / replyStyle + enabled gate), stored as
|
|
491
504
|
* one JSON key. v1.2 多卡:存储形 { default, byAgent },旧 v1 单对象读作
|
|
492
|
-
* default 兼容。getPersona
|
|
493
|
-
*
|
|
505
|
+
* default 兼容。getPersona 返回全量多卡形(面板与注入寻址都用它)。
|
|
506
|
+
* 寻址与兜底口径不在这里——住 src/user-settings.js 的 resolvePersonaCard,
|
|
507
|
+
* 注入与面板预览共用那一份;这里若再留一个 getPersonaFor,就是两张皮。
|
|
494
508
|
*/
|
|
495
509
|
getPersona() {
|
|
496
510
|
return parsePersonaStore(getSetting("persona") ?? "{}");
|
|
@@ -507,16 +521,6 @@ export function createSettings(db) {
|
|
|
507
521
|
current.default = parsePersona(JSON.stringify(input));
|
|
508
522
|
setSetting("persona", serializePersonaStore(current));
|
|
509
523
|
},
|
|
510
|
-
/** Per-agent lookup for the injector: id card → default fallback. */
|
|
511
|
-
getPersonaFor(agentId) {
|
|
512
|
-
const store = parsePersonaStore(getSetting("persona") ?? "{}");
|
|
513
|
-
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
|
|
514
|
-
if (id) {
|
|
515
|
-
const card = store.byAgent[id];
|
|
516
|
-
if (card && card.enabled === true) return card;
|
|
517
|
-
}
|
|
518
|
-
return store.default;
|
|
519
|
-
},
|
|
520
524
|
/** Replace one byAgent card wholesale (panel save); default 不可经此改。 */
|
|
521
525
|
setPersonaFor(agentId, card) {
|
|
522
526
|
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
|