dsh-prime-memory 0.18.2 → 0.18.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/CHANGELOG.en.md +14 -0
- package/CHANGELOG.ja.md +14 -0
- package/CHANGELOG.ko.md +14 -0
- package/CHANGELOG.md +42 -0
- package/README.en.md +4 -0
- package/README.ja.md +4 -0
- package/README.ko.md +4 -0
- package/README.md +4 -0
- package/cordis.patch.yml +6 -0
- package/dist/client.js +1333 -168
- package/dist/config.d.ts +37 -0
- package/dist/config.js +7 -0
- package/dist/contract.d.ts +155 -1
- package/dist/hooks/capture-recovery.d.ts +59 -0
- package/dist/hooks/capture-recovery.js +88 -0
- package/dist/hooks/capture.js +114 -1
- package/dist/hooks/recall-ack.d.ts +48 -0
- package/dist/hooks/recall-ack.js +79 -0
- package/dist/hooks/recall.d.ts +23 -1
- package/dist/hooks/recall.js +124 -8
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -4
- package/dist/metadata-validators.d.ts +5 -0
- package/dist/metadata-validators.js +11 -1
- package/dist/pipeline/l1.d.ts +4 -0
- package/dist/pipeline/l1.js +9 -2
- package/dist/pipeline/reconcile.js +4 -0
- package/dist/pipeline/ruminate.d.ts +11 -1
- package/dist/pipeline/ruminate.js +18 -1
- package/dist/pipeline/runner.js +62 -0
- package/dist/prompts/boundary.d.ts +17 -0
- package/dist/prompts/boundary.js +20 -0
- package/dist/prompts/graph-projection.js +4 -1
- package/dist/prompts/l1-dedup.d.ts +3 -3
- package/dist/prompts/l1-dedup.js +9 -1
- package/dist/prompts/l1-extraction.d.ts +3 -3
- package/dist/prompts/l1-extraction.js +12 -0
- package/dist/prompts/persona.js +6 -0
- package/dist/prompts/scene.js +7 -1
- package/dist/room-review.d.ts +196 -0
- package/dist/room-review.js +281 -0
- package/dist/stats.d.ts +26 -2
- package/dist/stats.js +270 -13
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +8 -0
- package/dist/store/l1.d.ts +34 -0
- package/dist/store/l1.js +68 -1
- package/dist/store/recall-usage.d.ts +43 -0
- package/dist/store/recall-usage.js +149 -0
- package/dist/store/rooms-registry.d.ts +66 -0
- package/dist/store/rooms-registry.js +250 -0
- package/dist/store/sqlite.d.ts +16 -0
- package/dist/store/sqlite.js +57 -0
- package/dist/store/trace.d.ts +39 -0
- package/dist/store/trace.js +149 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +249 -2
- package/dist/types.d.ts +4 -0
- package/dist/util/redact.d.ts +27 -0
- package/dist/util/redact.js +228 -0
- package/package.json +13 -12
- package/dist/hall-backfill.d.ts +0 -33
- package/dist/hall-backfill.js +0 -95
- package/dist/hall-migrate.d.ts +0 -32
- package/dist/hall-migrate.js +0 -88
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §F 结构化追踪(memorax-absorb Wave 4 / task_28)——recall/distill 事件流。
|
|
3
|
+
*
|
|
4
|
+
* 模块级单例(token-cost.ts 同款):trace() 埋点拿不到 store 实例(store 在
|
|
5
|
+
* index.ts 运行时才有 dataDir),initTraceStore 启动时注入;未初始化或
|
|
6
|
+
* trace.enabled=false 时 trace() 为零副作用 no-op。
|
|
7
|
+
*
|
|
8
|
+
* 存储:按天 JSONL(`dataDir/trace/trace-YYYY-MM-DD.jsonl`,appendJsonl 原语),
|
|
9
|
+
* 文件名即日期 → 保留期清理在切日时触发(删除超 retentionDays 的旧文件);
|
|
10
|
+
* 单文件 >5MB 停写 + 写入一行 trace_truncated marker(防无限增长);
|
|
11
|
+
* 写失败静默(filelog 同款纪律——追踪绝不影响主链路)。
|
|
12
|
+
*
|
|
13
|
+
* 隐私默认 metadata-only:recall 事件只带 queryChars+querySha,
|
|
14
|
+
* captureContent=true 才存 queryText(截断 200)——由埋点方决定字段,本模块不感知。
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { appendJsonl, dayKey, readJsonl } from '../util/io.js';
|
|
19
|
+
const TRACE_DIR = 'trace';
|
|
20
|
+
const DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
21
|
+
export class TraceStore {
|
|
22
|
+
dataDir;
|
|
23
|
+
opts;
|
|
24
|
+
/** 已停写的日期(5MB 上限触发;当日内后续事件丢弃,次日自动恢复)。 */
|
|
25
|
+
truncatedDays = new Set();
|
|
26
|
+
lastPruneDay = '';
|
|
27
|
+
chain = Promise.resolve();
|
|
28
|
+
constructor(dataDir, opts) {
|
|
29
|
+
this.dataDir = dataDir;
|
|
30
|
+
this.opts = opts;
|
|
31
|
+
}
|
|
32
|
+
/** 追加事件(异步串行链,失败静默)。 */
|
|
33
|
+
append(event) {
|
|
34
|
+
const day = dayKey(event.ts);
|
|
35
|
+
if (this.truncatedDays.has(day))
|
|
36
|
+
return;
|
|
37
|
+
this.chain = this.chain
|
|
38
|
+
.then(() => this.writeOne(day, event))
|
|
39
|
+
.catch(() => {
|
|
40
|
+
/* 静默:追踪失败不影响主链路 */
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** 等待挂起的写入排空(dispose 序用)。 */
|
|
44
|
+
flush() {
|
|
45
|
+
return this.chain;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 尾部读取:从最近的日期文件倒序收集,按 kind 过滤,返回至多 `lines` 条
|
|
49
|
+
* (按写入顺序 = 时间升序)。坏行跳过(readJsonl 同款)。
|
|
50
|
+
*/
|
|
51
|
+
async tail(lines, kind) {
|
|
52
|
+
const limit = Math.max(1, Math.min(500, Math.round(lines) || 100));
|
|
53
|
+
const files = this.recentFiles();
|
|
54
|
+
const collected = [];
|
|
55
|
+
for (let i = files.length - 1; i >= 0 && collected.length < limit; i--) {
|
|
56
|
+
const rows = await readJsonl(files[i]);
|
|
57
|
+
for (let j = rows.length - 1; j >= 0 && collected.length < limit; j--) {
|
|
58
|
+
const row = rows[j];
|
|
59
|
+
if (!row || typeof row !== 'object' || !row.kind)
|
|
60
|
+
continue;
|
|
61
|
+
if (kind && row.kind !== kind)
|
|
62
|
+
continue;
|
|
63
|
+
collected.push(row);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return collected.reverse();
|
|
67
|
+
}
|
|
68
|
+
dir() {
|
|
69
|
+
return path.join(this.dataDir, TRACE_DIR);
|
|
70
|
+
}
|
|
71
|
+
fileFor(day) {
|
|
72
|
+
return path.join(this.dir(), `trace-${day}.jsonl`);
|
|
73
|
+
}
|
|
74
|
+
recentFiles() {
|
|
75
|
+
try {
|
|
76
|
+
return fs
|
|
77
|
+
.readdirSync(this.dir())
|
|
78
|
+
.filter((f) => f.startsWith('trace-') && f.endsWith('.jsonl'))
|
|
79
|
+
.sort()
|
|
80
|
+
.map((f) => path.join(this.dir(), f));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async writeOne(day, event) {
|
|
87
|
+
if (day !== this.lastPruneDay) {
|
|
88
|
+
this.lastPruneDay = day;
|
|
89
|
+
this.pruneOldDays(day);
|
|
90
|
+
}
|
|
91
|
+
const file = this.fileFor(day);
|
|
92
|
+
try {
|
|
93
|
+
const stat = await fs.promises.stat(file);
|
|
94
|
+
if (stat.size > (this.opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES)) {
|
|
95
|
+
this.truncatedDays.add(day);
|
|
96
|
+
await appendJsonl(file, [
|
|
97
|
+
{ kind: 'trace_truncated', ts: event.ts, note: '单日文件超上限,当日停写(次日自动恢复)' },
|
|
98
|
+
]);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* 文件不存在 = 首写,继续 */
|
|
104
|
+
}
|
|
105
|
+
await appendJsonl(file, [event]);
|
|
106
|
+
}
|
|
107
|
+
/** 删除超过保留期的旧日文件(retentionDays 0 = 永久)。 */
|
|
108
|
+
pruneOldDays(today) {
|
|
109
|
+
if (this.opts.retentionDays <= 0)
|
|
110
|
+
return;
|
|
111
|
+
let files;
|
|
112
|
+
try {
|
|
113
|
+
files = fs.readdirSync(this.dir());
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const todayMs = Date.parse(`${today}T00:00:00`);
|
|
119
|
+
for (const f of files) {
|
|
120
|
+
const m = /^trace-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(f);
|
|
121
|
+
if (!m)
|
|
122
|
+
continue;
|
|
123
|
+
const dayMs = Date.parse(`${m[1]}T00:00:00`);
|
|
124
|
+
if (!Number.isFinite(dayMs) || !Number.isFinite(todayMs))
|
|
125
|
+
continue;
|
|
126
|
+
if (todayMs - dayMs > this.opts.retentionDays * 86_400_000) {
|
|
127
|
+
fs.promises.rm(path.join(this.dir(), f), { force: true }).catch(() => { });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── 模块级单例(token-cost.ts 同款) ──
|
|
133
|
+
let store = null;
|
|
134
|
+
/** 插件启动时注入(index.ts 调用);enabled=false 或 0 天保留不影响初始化本身。 */
|
|
135
|
+
export function initTraceStore(dataDir, opts) {
|
|
136
|
+
store = new TraceStore(dataDir, opts);
|
|
137
|
+
}
|
|
138
|
+
/** 测试/卸载:清空单例。 */
|
|
139
|
+
export function resetTraceStore() {
|
|
140
|
+
store = null;
|
|
141
|
+
}
|
|
142
|
+
/** 埋点入口:未初始化 = no-op。fire-and-forget,永不抛。 */
|
|
143
|
+
export function trace(event) {
|
|
144
|
+
store?.append(event);
|
|
145
|
+
}
|
|
146
|
+
/** RPC tail 数据源;未初始化返回 undefined(端点回空集)。 */
|
|
147
|
+
export function getTraceStore() {
|
|
148
|
+
return store;
|
|
149
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { PersonaStore } from '../store/persona.js';
|
|
|
22
22
|
import type { SceneStore } from '../store/scenes.js';
|
|
23
23
|
import type { SessionModeStore } from '../store/session-modes.js';
|
|
24
24
|
import type { MemoryFamily, MemoryLogger } from '../types.js';
|
|
25
|
+
import type { RoomRegistryStore } from '../store/rooms-registry.js';
|
|
25
26
|
export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, stores: {
|
|
26
27
|
l0: L0Store;
|
|
27
28
|
l1: L1Store;
|
|
@@ -31,4 +32,6 @@ export declare function registerMemoryTools(ctx: Context, cfg: MemoryConfig, sto
|
|
|
31
32
|
graph?: GraphStore;
|
|
32
33
|
}, logger: MemoryLogger, modes: SessionModeStore, live: LiveSettingsHandle,
|
|
33
34
|
/** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
|
|
34
|
-
ruminate?: RuminateController
|
|
35
|
+
ruminate?: RuminateController,
|
|
36
|
+
/** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
|
|
37
|
+
roomRegistry?: RoomRegistryStore): void;
|
package/dist/tools/index.js
CHANGED
|
@@ -3,13 +3,37 @@ import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from '../store/r
|
|
|
3
3
|
import { listConflictPairs, renderConflictResolution, renderConflicts, resolveConflictPair } from '../conflict-service.js';
|
|
4
4
|
import { WING_CATALOG, WING_FALLBACK, normPersistence, normScope, resolveRecordScope } from '../types.js';
|
|
5
5
|
import { scopeFilterOf, workspaceIdOf } from '../workspace.js';
|
|
6
|
+
import { annotateOrphanCandidates, confirmReview, mergeRoom, nextReview, pendingReviewCount, renameRoom, skipReview, } from '../room-review.js';
|
|
6
7
|
import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
|
|
8
|
+
import { redactSecrets } from '../util/redact.js';
|
|
7
9
|
const OFF_NOTICE = '本会话的记忆档位为"关闭":该会话对记忆系统完全隐身,不读取也不写入记忆。';
|
|
8
10
|
const WRITE_ONLY_NOTICE = '本会话为只写模式:记忆照常沉淀,但不读取。';
|
|
9
11
|
const GLOBAL_OFF_NOTICE = '记忆注入已全局停用:本会话不读取记忆(沉淀照常)。';
|
|
12
|
+
/** §C 一次性告警标记:手动写入路径的脱敏失败只提示一次(不阻断写入)。 */
|
|
13
|
+
let manualRedactWarnedOnce = false;
|
|
14
|
+
/**
|
|
15
|
+
* §C 手动写入路径脱敏(memorax-absorb / task_18):memory_add / memory_import
|
|
16
|
+
* 与捕获路径走同一 8 类词表,两条 L1 写入边界语义一致。
|
|
17
|
+
* 红线:脱敏失败放行原文 + 一次性 warn(不阻断写入)。
|
|
18
|
+
*/
|
|
19
|
+
function applyManualRedaction(record, cfg, logger) {
|
|
20
|
+
if (cfg.capture?.redactSecrets === false)
|
|
21
|
+
return;
|
|
22
|
+
try {
|
|
23
|
+
record.content = redactSecrets(record.content).text;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (!manualRedactWarnedOnce) {
|
|
27
|
+
manualRedactWarnedOnce = true;
|
|
28
|
+
logger.warn(`[memory] 手动写入脱敏失败,放行原文(一次性提示): ${err instanceof Error ? err.message : String(err)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
10
32
|
export function registerMemoryTools(ctx, cfg, stores, logger, modes, live,
|
|
11
33
|
/** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
|
|
12
|
-
ruminate
|
|
34
|
+
ruminate,
|
|
35
|
+
/** Room 注册表(分类管理 beta.4;缺省 = memory_room_admin 返回未装配提示)。 */
|
|
36
|
+
roomRegistry) {
|
|
13
37
|
if (!cfg.tools)
|
|
14
38
|
return;
|
|
15
39
|
/**
|
|
@@ -147,6 +171,145 @@ ruminate) {
|
|
|
147
171
|
};
|
|
148
172
|
},
|
|
149
173
|
}));
|
|
174
|
+
// ── memory_room_admin: Room 粒度目录治理(分类管理 beta.4;beta.5 起 merge/rename/retire 已注册) ──
|
|
175
|
+
// list 免预算零风险;merge/rename/retire 走 dryRun 预览→实跑,执行前自动备份,完成入队场景重算。
|
|
176
|
+
ctx.tools.register(defineTool({
|
|
177
|
+
name: 'memory_room_admin',
|
|
178
|
+
description: 'Room 分类管理(目录粒度)。action="list" 列出注册表条目与自生长 Room 计数;' +
|
|
179
|
+
'action="register" 预注册一个 Room(高权限;两级制——大类上限 200,小类用「大类/小类」细分不占额度,如 dsh-plugin/merge;' +
|
|
180
|
+
'action="merge" 合并两个 Room(高权限;dryRun 默认 true 只返预览,实跑改写全部匹配记录的 tags 并入队场景重算,执行前自动备份);' +
|
|
181
|
+
'action="rename" 改名(= merge 1:1 + 注册表改名);action="retire" 退役一个注册 Room(高权限;存量记录不动)。' +
|
|
182
|
+
'merge/rename 与反刍并发会相互覆盖,请在反刍空闲时执行。',
|
|
183
|
+
parameters: {
|
|
184
|
+
action: { type: 'string', required: true, description: 'list | register(高权限) | merge(高权限) | rename(高权限) | retire(高权限)' },
|
|
185
|
+
slug: { type: 'string', description: 'register 的 Room slug(小写字母数字连字符)' },
|
|
186
|
+
label: { type: 'string', description: 'register 的人类可读名(可选,可中文)' },
|
|
187
|
+
description: { type: 'string', description: 'register 的归类说明(可选;喂给标注器帮助归类)' },
|
|
188
|
+
from: { type: 'string', description: 'merge/rename 的源 Room slug' },
|
|
189
|
+
to: { type: 'string', description: 'merge/rename 的目标 Room slug' },
|
|
190
|
+
dryRun: { type: 'string', description: 'merge/rename:默认 true 只返预览;"false" 才实际执行' },
|
|
191
|
+
},
|
|
192
|
+
output: {
|
|
193
|
+
schema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
notice: { type: 'string' },
|
|
197
|
+
rooms: { type: 'string' },
|
|
198
|
+
slug: { type: 'string' },
|
|
199
|
+
created: { type: 'boolean' },
|
|
200
|
+
},
|
|
201
|
+
additionalProperties: false,
|
|
202
|
+
},
|
|
203
|
+
render: (_args, value) => [{ type: 'text', text: value.notice ?? JSON.stringify(value) }],
|
|
204
|
+
},
|
|
205
|
+
execute: async (args) => {
|
|
206
|
+
const action = String(args.action ?? '');
|
|
207
|
+
if (action === 'list') {
|
|
208
|
+
if (!roomRegistry)
|
|
209
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
210
|
+
const registry = roomRegistry.list();
|
|
211
|
+
const counts = stores.l1.listRooms();
|
|
212
|
+
const countOf = (slug) => counts.find((r) => r.room === slug)?.count ?? 0;
|
|
213
|
+
const regLines = registry.map((e) => `- ${e.slug}${e.label ? `(${e.label})` : ''} [${e.source}/${e.status}] 现有 ${countOf(e.slug)} 条${e.description ? ` — ${e.description}` : ''}`);
|
|
214
|
+
const grownOnly = counts.filter((r) => !registry.some((e) => e.slug === r.room));
|
|
215
|
+
return {
|
|
216
|
+
notice: `注册表 ${registry.length} 条(上限 200),自生长未注册 ${grownOnly.length} 个。\n` +
|
|
217
|
+
(regLines.join('\n') || '(注册表为空——用 action=register 预注册)') +
|
|
218
|
+
`\n自生长未注册:${grownOnly.map((r) => `${r.room}(${r.count})`).join(", ") || "无"}`,
|
|
219
|
+
rooms: `${registry.length}+${grownOnly.length}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (action === 'register') {
|
|
223
|
+
if (!roomRegistry)
|
|
224
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
225
|
+
if (!live.get().memoryMutate)
|
|
226
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
227
|
+
const slug = String(args.slug ?? '').trim();
|
|
228
|
+
try {
|
|
229
|
+
const r = await roomRegistry.register({
|
|
230
|
+
slug,
|
|
231
|
+
label: typeof args.label === 'string' ? args.label : undefined,
|
|
232
|
+
description: typeof args.description === 'string' ? args.description : undefined,
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
notice: r.created
|
|
236
|
+
? `已预注册 Room:${r.entry.slug}${r.entry.label ? `(${r.entry.label})` : ''}。候选标注器将优先把相关记忆挂到它上面。`
|
|
237
|
+
: `Room 已存在:${r.entry.slug}(本次只补全了缺失的 label/description)。`,
|
|
238
|
+
slug: r.entry.slug,
|
|
239
|
+
created: r.created,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return { notice: `注册失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (action === 'update') {
|
|
247
|
+
if (!roomRegistry)
|
|
248
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
249
|
+
if (!live.get().memoryMutate)
|
|
250
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
251
|
+
const slug = String(args.slug ?? '').trim();
|
|
252
|
+
if (!slug)
|
|
253
|
+
return { notice: 'update 需要 slug。' };
|
|
254
|
+
try {
|
|
255
|
+
const ok = await roomRegistry.update(slug, {
|
|
256
|
+
label: typeof args.label === 'string' ? args.label : undefined,
|
|
257
|
+
description: typeof args.description === 'string' ? args.description : undefined,
|
|
258
|
+
});
|
|
259
|
+
return { notice: ok ? `已更新:${slug}(显示名/归类说明)。` : `无变化:${slug} 未找到或未提供非空字段。`, slug };
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
return { notice: `更新失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (action === 'merge' || action === 'rename') {
|
|
266
|
+
if (!roomRegistry)
|
|
267
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
268
|
+
if (!live.get().memoryMutate)
|
|
269
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
270
|
+
const from = String(args.from ?? '').trim();
|
|
271
|
+
const to = String(args.to ?? '').trim();
|
|
272
|
+
if (!from || !to)
|
|
273
|
+
return { notice: 'merge/rename 需要 from 与 to 两个 slug。' };
|
|
274
|
+
const dryRun = String(args.dryRun ?? 'true').toLowerCase() !== 'false';
|
|
275
|
+
try {
|
|
276
|
+
const fn = action === 'merge' ? mergeRoom : renameRoom;
|
|
277
|
+
const r = await fn({ l1: stores.l1, registry: roomRegistry, logger }, from, to, { dryRun });
|
|
278
|
+
if (r.dryRun) {
|
|
279
|
+
return {
|
|
280
|
+
notice: `[dryRun 预览] ${from} → ${to}:影响 ${r.affected} 条${r.hasMore ? '(超过单次上限,需续跑)' : ''}。` +
|
|
281
|
+
`样例:${r.preview.join(', ') || '无'}。确认无误后以 dryRun="false" 执行(执行前自动备份,重写含已退场行,完成后自动入队场景重算)。`,
|
|
282
|
+
affected: r.affected,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
notice: `${action === 'merge' ? '合并' : '改名'}完成:${from} → ${to},重写 ${r.applied} 条` +
|
|
287
|
+
`(备份:${r.backupFile ?? '无'})。`,
|
|
288
|
+
applied: r.applied,
|
|
289
|
+
id: to,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
return { notice: `${action} 失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (action === 'retire') {
|
|
297
|
+
if (!roomRegistry)
|
|
298
|
+
return { notice: 'Room 注册表未装配(需要插件重载以初始化)。' };
|
|
299
|
+
if (!live.get().memoryMutate)
|
|
300
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
301
|
+
const slug = String(args.slug ?? '').trim();
|
|
302
|
+
try {
|
|
303
|
+
const ok = await roomRegistry.setStatus(slug, 'retired');
|
|
304
|
+
return { notice: ok ? `已退役 Room:${slug}(存量记录的 tags 不动,仅词表不再推荐)。` : `未找到注册条目:${slug}(自生长 Room 无需退役,会随 tags 自然消失)。`, slug };
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
return { notice: `退役失败: ${err instanceof Error ? err.message : String(err)}` };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return { notice: `非法 action:${action}(允许 list/register/merge/rename/retire)。` };
|
|
311
|
+
},
|
|
312
|
+
}));
|
|
150
313
|
// ── conversation_search: L0 原始对话 ──
|
|
151
314
|
ctx.tools.register(defineTool({
|
|
152
315
|
name: 'conversation_search',
|
|
@@ -362,6 +525,7 @@ ruminate) {
|
|
|
362
525
|
return { notice: 'content 为空,未写入' };
|
|
363
526
|
const scene = typeof args.scene === 'string' && args.scene.trim() ? args.scene.trim().slice(0, 120) : '__manual__';
|
|
364
527
|
const record = buildRecord(args, scene, Date.now(), workspaceIdOf(exec));
|
|
528
|
+
applyManualRedaction(record, cfg, logger);
|
|
365
529
|
await stores.l1.appendNew([record]);
|
|
366
530
|
logger.info(`[memory] 高权限写入记忆(${record.type}${record.metadata?.hall ? '/' + String(record.metadata.hall) : ''},时间轴 ${record.persistence ?? '?'}):${record.content.slice(0, 120)}`);
|
|
367
531
|
return { id: record.id };
|
|
@@ -455,7 +619,9 @@ ruminate) {
|
|
|
455
619
|
return;
|
|
456
620
|
}
|
|
457
621
|
seen.add(key);
|
|
458
|
-
|
|
622
|
+
const record = buildRecord(item, scene, now, workspaceIdOf(exec));
|
|
623
|
+
applyManualRedaction(record, cfg, logger);
|
|
624
|
+
records.push(record);
|
|
459
625
|
});
|
|
460
626
|
if (records.length > 0)
|
|
461
627
|
await stores.l1.appendNew(records);
|
|
@@ -740,6 +906,87 @@ ruminate) {
|
|
|
740
906
|
},
|
|
741
907
|
}));
|
|
742
908
|
// ── 反刍状态查询(查看当前反刍进度) ──
|
|
909
|
+
// ── memory_room_review:孤儿记忆的 Room 候选逐个复查(agent 长尾治理)──────
|
|
910
|
+
// next/annotate 只读或预算内标注(无需门);confirm/skip 改写 metadata,
|
|
911
|
+
// confirm 吃 live.memoryMutate 高权限门(与 memory_delete 同款)。
|
|
912
|
+
ctx.tools.register(defineTool({
|
|
913
|
+
name: 'memory_room_review',
|
|
914
|
+
description: '孤儿记忆(无 Room 绑定)的逐个复查:action="next" 取下一条待复查记录(带系统预标记的候选 Room 与现有词表,agent 依据记录内容判定);' +
|
|
915
|
+
'action="confirm" 按 id 把 1-3 个 Room slug 写入该记忆(高权限);action="skip" 跳过(不再进队列);' +
|
|
916
|
+
'action="annotate" 立即对一批孤儿做候选预标记(平时由反刍自动推进)。建议长会话空闲时逐个过,保持分类收敛——优先挂靠现有 Room。',
|
|
917
|
+
parameters: {
|
|
918
|
+
action: { type: 'string', required: true, description: 'next(取下一条)| confirm(确认写入)| skip(跳过)| annotate(批量预标记)' },
|
|
919
|
+
id: { type: 'string', description: 'confirm/skip 的目标记录 id(action=next 返回的 id)' },
|
|
920
|
+
rooms: { type: 'string', description: 'confirm 的 Room slug 列表(逗号分隔,1-3 个,小写字母数字连字符;可含词表外新 slug)' },
|
|
921
|
+
},
|
|
922
|
+
output: {
|
|
923
|
+
schema: {
|
|
924
|
+
type: 'object',
|
|
925
|
+
properties: {
|
|
926
|
+
notice: { type: 'string' },
|
|
927
|
+
id: { type: 'string' },
|
|
928
|
+
content: { type: 'string' },
|
|
929
|
+
candidates: { type: 'string' },
|
|
930
|
+
roomCatalog: { type: 'string' },
|
|
931
|
+
remaining: { type: 'number' },
|
|
932
|
+
confirmedTags: { type: 'string' },
|
|
933
|
+
annotated: { type: 'number' },
|
|
934
|
+
deferred: { type: 'number' },
|
|
935
|
+
},
|
|
936
|
+
additionalProperties: false,
|
|
937
|
+
},
|
|
938
|
+
render: (_args, value) => [{ type: 'text', text: value.notice ?? JSON.stringify(value) }],
|
|
939
|
+
},
|
|
940
|
+
execute: async (args) => {
|
|
941
|
+
const action = String(args.action ?? '');
|
|
942
|
+
const io = { l1: stores.l1, logger };
|
|
943
|
+
if (action === 'next') {
|
|
944
|
+
const item = nextReview(io.l1);
|
|
945
|
+
if (!item)
|
|
946
|
+
return { notice: '没有待复查的孤儿记忆(roomCandidates 队列为空)。可用 action=annotate 先做一轮候选预标记。' };
|
|
947
|
+
return {
|
|
948
|
+
id: item.id,
|
|
949
|
+
content: item.content,
|
|
950
|
+
candidates: item.candidates.join(', '),
|
|
951
|
+
roomCatalog: item.roomCatalog.map((r) => `${r.room}(${r.count})`).join(', '),
|
|
952
|
+
remaining: item.remaining,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (action === 'annotate') {
|
|
956
|
+
const r = await annotateOrphanCandidates(ctx, cfg, { ...io, registry: roomRegistry }, 40);
|
|
957
|
+
return {
|
|
958
|
+
notice: `候选预标记完成:选中 ${r.selected} 条,产候选 ${r.candidates} 条,写 pending ${r.written} 条。用 action=next 逐个复查。`,
|
|
959
|
+
annotated: r.candidates,
|
|
960
|
+
deferred: r.selected - r.written,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
if (action === 'confirm') {
|
|
964
|
+
if (!live.get().memoryMutate)
|
|
965
|
+
return { notice: MUTATE_OFF_NOTICE };
|
|
966
|
+
const id = String(args.id ?? '').trim();
|
|
967
|
+
if (!id)
|
|
968
|
+
return { notice: 'confirm 需要 id(action=next 返回的 id)。' };
|
|
969
|
+
const rooms = String(args.rooms ?? '')
|
|
970
|
+
.split(/[,,、]/)
|
|
971
|
+
.map((x) => x.trim())
|
|
972
|
+
.filter(Boolean);
|
|
973
|
+
if (rooms.length === 0)
|
|
974
|
+
return { notice: 'confirm 需要 rooms(逗号分隔的 1-3 个 Room slug)。' };
|
|
975
|
+
const r = confirmReview(io.l1, id, rooms);
|
|
976
|
+
if (!r)
|
|
977
|
+
return { notice: `确认失败:id=${id} 不存在、已被确认过、或 rooms 全部非法(需 1-3 个合法 slug)。` };
|
|
978
|
+
return { notice: `已挂到 Room:${r.tags.join(', ')}(面板下一次刷新即可见)。`, confirmedTags: r.tags.join(', '), id };
|
|
979
|
+
}
|
|
980
|
+
if (action === 'skip') {
|
|
981
|
+
const id = String(args.id ?? '').trim();
|
|
982
|
+
if (!id)
|
|
983
|
+
return { notice: 'skip 需要 id。' };
|
|
984
|
+
const ok = skipReview(io.l1, id);
|
|
985
|
+
return { notice: ok ? `已跳过 ${id}(不再进复查队列)。` : `跳过失败:id=${id} 不存在。`, id };
|
|
986
|
+
}
|
|
987
|
+
return { notice: `非法 action:${action}(允许 next/confirm/skip/annotate)。剩余待复查 ${pendingReviewCount(stores.l1)} 条。` };
|
|
988
|
+
},
|
|
989
|
+
}));
|
|
743
990
|
ctx.tools.register(defineTool({
|
|
744
991
|
name: 'memory_ruminate_status',
|
|
745
992
|
description: '查询当前记忆反刍的状态与进度(是否运行中、当前阶段、已完成/总会话数等)。\n\n注意:在 DSH Web GUI 的对话中无法直接调用此工具,因为模型可调用工具列表不包含 ruminate_status。如需查询进度,请在记忆库面板的蒸馏面板查看。',
|
package/dist/types.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export interface RoomCount {
|
|
|
41
41
|
room: string;
|
|
42
42
|
/** 该 Room 下的记录数(与 `wingL1Counts()` 同口径:含 retired 行)。 */
|
|
43
43
|
count: number;
|
|
44
|
+
/** 来源标示(rooms-get 端点层合并注册表后附带;'grown' = 自生长,可缺省)。 */
|
|
45
|
+
source?: 'pre-registered' | 'grown';
|
|
46
|
+
/** 人类可读名(注册表条目才有;面板 chip 可用 label 展示)。 */
|
|
47
|
+
label?: string;
|
|
44
48
|
}
|
|
45
49
|
/** 记录族标签推断:work_* 前缀 → work,其余(含 auto 档兜底)→ chat。 */
|
|
46
50
|
export declare function familyForType(type: string): MemoryFamily;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §C 载荷脱敏(memorax-absorb Wave 2 / task_14)——捕获边界的密钥擦除。
|
|
3
|
+
*
|
|
4
|
+
* 用户在对话里贴入的密钥(PEM/Bearer/JWT/Cookie/厂商 API key/邮箱/长号/高熵 ID)
|
|
5
|
+
* 若原样进 L0/L1,会被逐轮召回注入上下文并在 UI 展示。本模块在捕获单点把它们
|
|
6
|
+
* 替换为类型化占位符 `[REDACTED:<KIND>]`——占位符保留类别语义,关键词检索仍可命中。
|
|
7
|
+
*
|
|
8
|
+
* 词表/正则/allowlist/span 合并算法以 memorax `payload-redaction.ts` 为基准逐条移植
|
|
9
|
+
* (全文快照见计划 evidence/payload-redaction-snapshot.ts;findings.md 有等价内联)。
|
|
10
|
+
*
|
|
11
|
+
* 契约(sanitize 同款,tests/redact.test.ts 钉住):
|
|
12
|
+
* - 纯函数,零 I/O 零时钟零随机;输入原样返回(未命中)= 引用不变无关紧要,字符串不可变;
|
|
13
|
+
* - **幂等**:已是 `[REDACTED:KIND]` 的值不会被二次匹配;
|
|
14
|
+
* - 安全值 allowlist 命中不脱(`example`/`change-me`/`$VAR`/`${{…}}` 等);
|
|
15
|
+
* - 本函数**永不抛错**的保证由调用方 try/catch 兜底(capture 接线层,§C 红线:失败放行原文)。
|
|
16
|
+
*/
|
|
17
|
+
export type RedactionKind = 'PRIVATE_KEY' | 'AUTH_TOKEN' | 'COOKIE' | 'API_KEY' | 'CREDENTIAL' | 'EMAIL' | 'LONG_NUMBER' | 'OPAQUE_ID';
|
|
18
|
+
export interface RedactionResult {
|
|
19
|
+
text: string;
|
|
20
|
+
counts: Partial<Record<RedactionKind, number>>;
|
|
21
|
+
redacted: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 对文本执行 8 类脱敏。规则各自扫描 → 收集值级 span → 合并重叠 → 重拼插入占位符。
|
|
25
|
+
* 同一位置被多条规则命中时取更长/更高优先级的 span。
|
|
26
|
+
*/
|
|
27
|
+
export declare function redactSecrets(text: string): RedactionResult;
|