@modusensus/dsh-mneme 0.7.32 → 0.8.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 +42 -20
- package/lib/api.js +60 -0
- package/lib/client.js +152 -2
- package/lib/commands.js +30 -3
- package/lib/config.js +21 -0
- package/lib/inject.js +39 -5
- package/lib/lang.js +12 -0
- package/lib/scope.js +83 -0
- package/lib/service.js +287 -62
- package/lib/settings.js +7 -0
- package/lib/store.js +100 -5
- package/lib/tools.js +86 -12
- package/package.json +1 -1
- package/src/api.js +60 -0
- package/src/commands.js +30 -3
- package/src/config.js +21 -0
- package/src/inject.js +39 -5
- package/src/lang.js +12 -0
- package/src/scope.js +83 -0
- package/src/service.js +287 -62
- package/src/settings.js +7 -0
- package/src/store.js +100 -5
- package/src/tools.js +86 -12
- package/test/api.test.js +90 -4
- package/test/client.test.js +58 -0
- package/test/commands.test.js +40 -3
- package/test/inject.test.js +93 -0
- package/test/scope-retrieval.test.js +197 -0
- package/test/scope-storage.test.js +247 -0
- package/test/scope-strict.test.js +165 -0
- package/test/service.test.js +76 -0
package/lib/settings.js
CHANGED
|
@@ -64,6 +64,13 @@ const FEATURE_FLAG_BOOLEANS = [
|
|
|
64
64
|
// Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
|
|
65
65
|
"dreamSkipInvalid",
|
|
66
66
|
"allowCrossTypeMerge",
|
|
67
|
+
// Issue #17(v0.8.0 A1):scope 隔离存储层总开关。开启后写入标注
|
|
68
|
+
// agent_scope/workspace_scope、去重键扩展(含 sensitivity);检索侧过滤在
|
|
69
|
+
// A2/A3。默认关=行为与 A1 前逐字节一致。
|
|
70
|
+
"scopeEnabled",
|
|
71
|
+
// Issue #17(v0.8.0 A3):strictScope 硬过滤——他 scope 完全不可见(关闭时
|
|
72
|
+
// 为 A2 软隔离:降权保留可见)。
|
|
73
|
+
"strictScope",
|
|
67
74
|
// 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
|
|
68
75
|
// 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
|
|
69
76
|
// 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
|
package/lib/store.js
CHANGED
|
@@ -310,7 +310,9 @@ function escapeLike(q) {
|
|
|
310
310
|
// 法值一律返回 undefined → 不进 WHERE(忽略而非报错:面板传坏参数时宁可放宽
|
|
311
311
|
// 过滤也不要白屏)。updated_at 列是 toISOString 产生的 UTC "Z" 字符串,字典
|
|
312
312
|
// 序与时间序一致,SQL 里可直接比较。
|
|
313
|
-
|
|
313
|
+
// v0.8.0 A2:导出复用——occurred_at 时间过滤(service 的搜索后置过滤)与
|
|
314
|
+
// updated_at 过滤共用同一套边界归一化,坏参数口径一致。
|
|
315
|
+
export function updatedAtBounds(updatedFrom, updatedTo) {
|
|
314
316
|
const norm = (raw, endOfDay) => {
|
|
315
317
|
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
316
318
|
const s = raw.trim();
|
|
@@ -332,6 +334,26 @@ function parseTags(raw) {
|
|
|
332
334
|
}
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
// scope 列(agent_scope/workspace_scope/sensitivity)写入归一化:非空 trim 字符串
|
|
338
|
+
// 原样收下,其余(undefined/null/空串/非字符串)一律 NULL。存储层不做枚举校验——
|
|
339
|
+
// scope 值是宿主身份标签(agentPreset id / 目录路径 / 自由 sensitivity 标签),
|
|
340
|
+
// 语义由调用方负责;这里只保证「脏输入落 NULL,绝不阻塞写入」。
|
|
341
|
+
function normalizeScopeText(raw) {
|
|
342
|
+
if (typeof raw !== "string") return null;
|
|
343
|
+
const s = raw.trim();
|
|
344
|
+
return s ? s : null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// occurred_at:事件发生时间(区别于 created_at 的入库时间)。可解析的时间戳
|
|
348
|
+
// 统一归一到 UTC ISO(A2 的时间过滤要按字典序直接比较);解析失败落 NULL,
|
|
349
|
+
// 同样不阻塞写入。
|
|
350
|
+
function normalizeOccurredAt(raw) {
|
|
351
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
352
|
+
const ms = Date.parse(raw.trim());
|
|
353
|
+
if (Number.isNaN(ms)) return null;
|
|
354
|
+
return new Date(ms).toISOString();
|
|
355
|
+
}
|
|
356
|
+
|
|
335
357
|
function toRow(row) {
|
|
336
358
|
if (!row) return undefined;
|
|
337
359
|
return {
|
|
@@ -347,6 +369,10 @@ function toRow(row) {
|
|
|
347
369
|
content_history: parseJsonArray(row.content_history),
|
|
348
370
|
quality_score: row.quality_score !== null && row.quality_score !== undefined ? Number(row.quality_score) : undefined,
|
|
349
371
|
epistemic_status: row.epistemic_status ?? "subjective",
|
|
372
|
+
agent_scope: row.agent_scope ?? undefined,
|
|
373
|
+
workspace_scope: row.workspace_scope ?? undefined,
|
|
374
|
+
sensitivity: row.sensitivity ?? undefined,
|
|
375
|
+
occurred_at: row.occurred_at ?? undefined,
|
|
350
376
|
created_at: row.created_at,
|
|
351
377
|
updated_at: row.updated_at,
|
|
352
378
|
last_accessed_at: row.last_accessed_at ?? undefined,
|
|
@@ -597,6 +623,14 @@ export function createStore(path) {
|
|
|
597
623
|
addColumn("memories", "content_history", "ALTER TABLE memories ADD COLUMN content_history TEXT");
|
|
598
624
|
addColumn("memories", "quality_score", "ALTER TABLE memories ADD COLUMN quality_score REAL");
|
|
599
625
|
|
|
626
|
+
// v0.8.0 A1(issue #17)scope 隔离存储层:四列全部可空、不带 DEFAULT——存量行
|
|
627
|
+
// 零重写,读侧把 NULL 视为未标注(= 全局可见)。是否落值由 scopeEnabled 门控
|
|
628
|
+
// (service/tools 层),存储层只负责归一化(normalizeScopeText/normalizeOccurredAt)。
|
|
629
|
+
addColumn("memories", "agent_scope", "ALTER TABLE memories ADD COLUMN agent_scope TEXT");
|
|
630
|
+
addColumn("memories", "workspace_scope", "ALTER TABLE memories ADD COLUMN workspace_scope TEXT");
|
|
631
|
+
addColumn("memories", "sensitivity", "ALTER TABLE memories ADD COLUMN sensitivity TEXT");
|
|
632
|
+
addColumn("memories", "occurred_at", "ALTER TABLE memories ADD COLUMN occurred_at TEXT");
|
|
633
|
+
|
|
600
634
|
// Legacy dream_runs without policy_epoch → backfill with the default epoch.
|
|
601
635
|
addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
|
|
602
636
|
addColumn("dream_runs", "run_type", "ALTER TABLE dream_runs ADD COLUMN run_type TEXT NOT NULL DEFAULT 'auto'");
|
|
@@ -643,7 +677,7 @@ export function createStore(path) {
|
|
|
643
677
|
return ts;
|
|
644
678
|
}
|
|
645
679
|
|
|
646
|
-
function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, updatedFrom = null, updatedTo = null } = {}) {
|
|
680
|
+
function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, updatedFrom = null, updatedTo = null, occurredFrom = null, occurredTo = null, visibility = null } = {}) {
|
|
647
681
|
const clauses = [];
|
|
648
682
|
const params = [];
|
|
649
683
|
if (type !== undefined) {
|
|
@@ -670,6 +704,33 @@ export function createStore(path) {
|
|
|
670
704
|
clauses.push("updated_at <= ?");
|
|
671
705
|
params.push(bounds.to);
|
|
672
706
|
}
|
|
707
|
+
// occurred_at 闭区间:与 list() 同口径(COALESCE 回退 created_at),
|
|
708
|
+
// memory_list 的 total 才能和过滤后的行保持一致。
|
|
709
|
+
const occurred = updatedAtBounds(occurredFrom, occurredTo);
|
|
710
|
+
if (occurred.from) {
|
|
711
|
+
clauses.push("COALESCE(occurred_at, created_at) >= ?");
|
|
712
|
+
params.push(occurred.from);
|
|
713
|
+
}
|
|
714
|
+
if (occurred.to) {
|
|
715
|
+
clauses.push("COALESCE(occurred_at, created_at) <= ?");
|
|
716
|
+
params.push(occurred.to);
|
|
717
|
+
}
|
|
718
|
+
// occurred_at 闭区间:与 list() 同过滤(visibility 同口径),memory_list
|
|
719
|
+
// 的 total 才能和过滤后的行保持一致。
|
|
720
|
+
if (visibility) {
|
|
721
|
+
if (visibility.agentScope != null) {
|
|
722
|
+
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
723
|
+
params.push(visibility.agentScope);
|
|
724
|
+
} else {
|
|
725
|
+
clauses.push("agent_scope IS NULL");
|
|
726
|
+
}
|
|
727
|
+
if (visibility.workspaceScope != null) {
|
|
728
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
729
|
+
params.push(visibility.workspaceScope);
|
|
730
|
+
} else {
|
|
731
|
+
clauses.push("workspace_scope IS NULL");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
673
734
|
if (!includeForgotten) {
|
|
674
735
|
clauses.push("forgotten = 0");
|
|
675
736
|
}
|
|
@@ -714,8 +775,8 @@ export function createStore(path) {
|
|
|
714
775
|
: inferEpistemicStatus(memory);
|
|
715
776
|
runAtomically(() => {
|
|
716
777
|
db.prepare(
|
|
717
|
-
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, created_at, updated_at)
|
|
718
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
778
|
+
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, agent_scope, workspace_scope, sensitivity, occurred_at, created_at, updated_at)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
719
780
|
).run(
|
|
720
781
|
id,
|
|
721
782
|
type,
|
|
@@ -729,6 +790,10 @@ export function createStore(path) {
|
|
|
729
790
|
Number.isFinite(memory.quality_score) ? memory.quality_score : null,
|
|
730
791
|
embedding,
|
|
731
792
|
epistemicStatus,
|
|
793
|
+
normalizeScopeText(memory.agent_scope),
|
|
794
|
+
normalizeScopeText(memory.workspace_scope),
|
|
795
|
+
normalizeScopeText(memory.sensitivity),
|
|
796
|
+
normalizeOccurredAt(memory.occurred_at),
|
|
732
797
|
now,
|
|
733
798
|
now
|
|
734
799
|
);
|
|
@@ -936,7 +1001,7 @@ export function createStore(path) {
|
|
|
936
1001
|
return rows.map(toRow);
|
|
937
1002
|
}
|
|
938
1003
|
|
|
939
|
-
function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null } = {}) {
|
|
1004
|
+
function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, depositedOnly = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null, occurredFrom = null, occurredTo = null, visibility = null } = {}) {
|
|
940
1005
|
const clauses = [];
|
|
941
1006
|
const params = [];
|
|
942
1007
|
if (type) {
|
|
@@ -964,6 +1029,36 @@ export function createStore(path) {
|
|
|
964
1029
|
clauses.push("updated_at <= ?");
|
|
965
1030
|
params.push(bounds.to);
|
|
966
1031
|
}
|
|
1032
|
+
// v0.8.0 A2(issue #17):occurred_at 闭区间过滤——按「事件发生时间」检索。
|
|
1033
|
+
// 未标注 occurred_at 的行(含全部存量)回退 created_at 比较(COALESCE),
|
|
1034
|
+
// 否则过滤器对旧库近乎不可用;边界归一化与 updated_at 共用 updatedAtBounds。
|
|
1035
|
+
const occurred = updatedAtBounds(occurredFrom, occurredTo);
|
|
1036
|
+
if (occurred.from) {
|
|
1037
|
+
clauses.push("COALESCE(occurred_at, created_at) >= ?");
|
|
1038
|
+
params.push(occurred.from);
|
|
1039
|
+
}
|
|
1040
|
+
if (occurred.to) {
|
|
1041
|
+
clauses.push("COALESCE(occurred_at, created_at) <= ?");
|
|
1042
|
+
params.push(occurred.to);
|
|
1043
|
+
}
|
|
1044
|
+
// v0.8.0 A3(issue #17):strictScope 硬过滤(memory_list 分页路径)。
|
|
1045
|
+
// SQL 与 service.isVisibleInScope 同口径:未标注(NULL)恒可见;当前维度
|
|
1046
|
+
// 解析不到时该维度只放行未标注行(fail-closed)。只有显式传 visibility
|
|
1047
|
+
// 的用户面调用才过滤——dream/质量过滤等内部 store.list 调用不受影响。
|
|
1048
|
+
if (visibility) {
|
|
1049
|
+
if (visibility.agentScope != null) {
|
|
1050
|
+
clauses.push("(agent_scope IS NULL OR agent_scope = ?)");
|
|
1051
|
+
params.push(visibility.agentScope);
|
|
1052
|
+
} else {
|
|
1053
|
+
clauses.push("agent_scope IS NULL");
|
|
1054
|
+
}
|
|
1055
|
+
if (visibility.workspaceScope != null) {
|
|
1056
|
+
clauses.push("(workspace_scope IS NULL OR workspace_scope = ?)");
|
|
1057
|
+
params.push(visibility.workspaceScope);
|
|
1058
|
+
} else {
|
|
1059
|
+
clauses.push("workspace_scope IS NULL");
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
967
1062
|
if (!includeForgotten) {
|
|
968
1063
|
clauses.push("forgotten = 0");
|
|
969
1064
|
}
|
package/lib/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { createScopeResolver } from "./scope.js";
|
|
2
3
|
import { describeLocalRuntime, resolveRuntimeEntry } from "./runtime/loader.js";
|
|
3
4
|
import { defaultRuntimeDir } from "./runtime/layout.js";
|
|
4
5
|
import { hostModulesDir, loadRuntimeManifest, provisionRuntime } from "./runtime/provision.js";
|
|
@@ -25,11 +26,44 @@ const MEMORY_ITEM_SCHEMA = {
|
|
|
25
26
|
tags: { type: "array", items: { type: "string" } },
|
|
26
27
|
importance: { type: "integer", required: true },
|
|
27
28
|
source: { type: "string" },
|
|
29
|
+
// v0.8.0 A2:scope 标注与事件发生时间随行透出(未标注时缺省,可选属性)。
|
|
30
|
+
agent_scope: { type: "string" },
|
|
31
|
+
workspace_scope: { type: "string" },
|
|
32
|
+
sensitivity: { type: "string" },
|
|
33
|
+
occurred_at: { type: "string" },
|
|
28
34
|
created_at: { type: "string", required: true },
|
|
29
35
|
updated_at: { type: "string", required: true }
|
|
30
36
|
}
|
|
31
37
|
};
|
|
32
38
|
|
|
39
|
+
// v0.8.0 A2:occurred 时间窗参数 → store 过滤选项(list/count 同口径);
|
|
40
|
+
// 未传参返回空对象,既有调用方不受影响。(A3 复核:此处入参仅含 occurred 边界。)
|
|
41
|
+
function occurredWindow(args) {
|
|
42
|
+
const from = args?.occurred_from ?? null;
|
|
43
|
+
const to = args?.occurred_to ?? null;
|
|
44
|
+
return from !== null || to !== null ? { occurredFrom: from, occurredTo: to } : {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// v0.8.0 A3:strictScope 硬过滤的 visibility 选项(store.list/count 同口径)。
|
|
48
|
+
// flag 关或会话 scope 未解析出(flag 关时解析器返回 null)→ 空对象不过滤;
|
|
49
|
+
// scope 为 {null,null}(匿名会话)时照传——store 侧 fail-closed 只放行未标注行。
|
|
50
|
+
function strictVisibility(config, scope) {
|
|
51
|
+
if (config?.strictScope !== true || !scope) return {};
|
|
52
|
+
return { visibility: { agentScope: scope.agent_scope, workspaceScope: scope.workspace_scope } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// v0.8.0 A2:检索结果的 provenance 附加段——只在有标注时出现,未标注行渲染
|
|
56
|
+
// 与 A2 前逐字节一致。
|
|
57
|
+
const MEMORY_PROVENANCE = (m) => {
|
|
58
|
+
const parts = [];
|
|
59
|
+
if (m.occurred_at) parts.push(`occurred: ${m.occurred_at}`);
|
|
60
|
+
if (m.agent_scope || m.workspace_scope) {
|
|
61
|
+
parts.push(`scope: ${m.agent_scope ?? "global"} / ${m.workspace_scope ?? "global"}`);
|
|
62
|
+
}
|
|
63
|
+
if (m.sensitivity) parts.push(`sensitivity: ${m.sensitivity}`);
|
|
64
|
+
return parts.length ? ` | ${parts.join(" | ")}` : "";
|
|
65
|
+
};
|
|
66
|
+
|
|
33
67
|
export function createTools(ctx, service, config, embedder) {
|
|
34
68
|
const toolsRegistry = ctx.tools;
|
|
35
69
|
let registeredTools = REGISTERED_TOOLS.get(toolsRegistry);
|
|
@@ -37,6 +71,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
37
71
|
registeredTools = new Set();
|
|
38
72
|
REGISTERED_TOOLS.set(toolsRegistry, registeredTools);
|
|
39
73
|
}
|
|
74
|
+
// v0.8.0 A1/A2(issue #17):会话 scope 解析(agentPreset + workspace 反查)。
|
|
75
|
+
// flag 关闭时恒返回 null——写入不标注,检索不加权,行为与 A1 前完全一致。
|
|
76
|
+
// logger 透传:registry 反查失败时 warnOnce 才有出口(否则静默降级无观测)。
|
|
77
|
+
const resolveSessionScope = createScopeResolver({ ctx, config, logger: ctx.logger });
|
|
40
78
|
const tools = [
|
|
41
79
|
defineTool({
|
|
42
80
|
name: "memory_save",
|
|
@@ -50,7 +88,9 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
50
88
|
content: { type: "string", required: true, description: "Memory body" },
|
|
51
89
|
tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
|
|
52
90
|
importance: { type: "integer", description: "1-5; >= threshold auto-injects into future sessions" },
|
|
53
|
-
source: { type: "string", description: "Optional provenance" }
|
|
91
|
+
source: { type: "string", description: "Optional provenance" },
|
|
92
|
+
sensitivity: { type: "string", description: "Optional sensitivity label (free-form, e.g. personal). Same-title entries with different sensitivity stay separate instead of merging." },
|
|
93
|
+
occurred_at: { type: "string", description: "Optional ISO-8601 instant the remembered event happened (differs from write time). Invalid values are ignored." }
|
|
54
94
|
},
|
|
55
95
|
output: {
|
|
56
96
|
schema: {
|
|
@@ -63,14 +103,21 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
63
103
|
},
|
|
64
104
|
render: (_args, value) => TEXT_OUTPUT(`memory ${value.action}: ${value.id}`)
|
|
65
105
|
},
|
|
66
|
-
async execute(args) {
|
|
106
|
+
async execute(args, exec) {
|
|
107
|
+
// scope 标注:agent_scope/workspace_scope 由会话身份解析(不作为模型
|
|
108
|
+
// 参数),sensitivity/occurred_at 来自显式参数。解析绝不抛错、取不到
|
|
109
|
+
// 落 NULL;flag 关闭时 scope 为 null,一个字段都不标注。
|
|
110
|
+
const scope = resolveSessionScope(exec);
|
|
67
111
|
const { action, memory } = service.saveWithDedupe({
|
|
68
112
|
type: args.type,
|
|
69
113
|
title: args.title,
|
|
70
114
|
content: args.content,
|
|
71
115
|
tags: args.tags ?? [],
|
|
72
116
|
importance: args.importance ?? 3,
|
|
73
|
-
source: args.source ?? "tool"
|
|
117
|
+
source: args.source ?? "tool",
|
|
118
|
+
...(scope ? { agent_scope: scope.agent_scope, workspace_scope: scope.workspace_scope } : {}),
|
|
119
|
+
...(args.sensitivity !== undefined ? { sensitivity: args.sensitivity } : {}),
|
|
120
|
+
...(args.occurred_at !== undefined ? { occurred_at: args.occurred_at } : {})
|
|
74
121
|
});
|
|
75
122
|
return { action, id: memory.id };
|
|
76
123
|
}
|
|
@@ -79,12 +126,15 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
79
126
|
defineTool({
|
|
80
127
|
name: "memory_search",
|
|
81
128
|
description: "Search the cross-session memory store. Use when you need past context: how a problem was solved, user preferences, project decisions. Substring-matches title/content/tags, and augments results with semantic (vector) recall + optional rerank when an embeddings provider is configured. Returns matching entries with source and timestamps.",
|
|
129
|
+
// A2 检索接线:occurred 时间窗 + 会话 scope 加成(见 execute)。
|
|
82
130
|
parameters: {
|
|
83
131
|
query: { type: "string", required: true, description: "Search text; substring match over title/content/tags" },
|
|
84
132
|
limit: { type: "integer", description: "Max results (default 20)" },
|
|
85
133
|
mode: { type: "string", enum: ["auto", "keyword", "vector", "hybrid"], description: "auto (default) = keyword hits first + vector fill when enabled; keyword = text only; vector = semantic recall first (falls back to keyword); hybrid = vector leads, keyword fills remaining slots" },
|
|
86
134
|
semantic: { type: "boolean", description: "Shorthand: enable semantic (vector) recall (same as mode=vector when true)" },
|
|
87
|
-
rerank: { type: "boolean", description: "Run cross-encoder rerank over candidates when a local reranker is configured (default true)" }
|
|
135
|
+
rerank: { type: "boolean", description: "Run cross-encoder rerank over candidates when a local reranker is configured (default true)" },
|
|
136
|
+
occurred_from: { type: "string", description: "Optional ISO date/timestamp lower bound on when the remembered event happened (occurred_at, falling back to created_at). Date-only values are inclusive from that day's 00:00Z." },
|
|
137
|
+
occurred_to: { type: "string", description: "Optional ISO date/timestamp upper bound on occurred_at. Date-only values are inclusive through that day's 23:59:59.999Z." }
|
|
88
138
|
},
|
|
89
139
|
output: {
|
|
90
140
|
schema: {
|
|
@@ -104,19 +154,26 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
104
154
|
.map((m, i) => {
|
|
105
155
|
const preview = (m.content ?? "").replace(/\s+/g, " ").trim();
|
|
106
156
|
const cut = preview.length > 200 ? `${preview.slice(0, 200)}…` : preview;
|
|
107
|
-
return `[${i + 1}] ${m.title}\n ID: ${m.id} | type: ${m.type} | importance: ${m.importance} | updated: ${m.updated_at}\n ${cut}`;
|
|
157
|
+
return `[${i + 1}] ${m.title}\n ID: ${m.id} | type: ${m.type} | importance: ${m.importance} | updated: ${m.updated_at}${MEMORY_PROVENANCE(m)}\n ${cut}`;
|
|
108
158
|
})
|
|
109
159
|
.join("\n\n");
|
|
110
160
|
return TEXT_OUTPUT(`Found ${items.length} memory entr${items.length === 1 ? "y" : "ies"}:\n\n${body}`);
|
|
111
161
|
}
|
|
112
162
|
},
|
|
113
|
-
async execute(args) {
|
|
163
|
+
async execute(args, exec) {
|
|
114
164
|
const limit = args.limit ?? 20;
|
|
115
165
|
const mode = args.semantic === true && !args.mode ? "vector" : args.mode ?? "auto";
|
|
166
|
+
// v0.8.0 A2(issue #17):scope 检索加权 + occurred 时间窗的检索接线。
|
|
167
|
+
// flag 关闭或解析不出 scope 时为 null,检索行为与 A2 前一致。
|
|
168
|
+
const scope = resolveSessionScope(exec);
|
|
169
|
+
const occurredFrom = args.occurred_from ?? null;
|
|
170
|
+
const occurredTo = args.occurred_to ?? null;
|
|
116
171
|
const rows = await service.searchMemories(args.query, {
|
|
117
172
|
mode,
|
|
118
173
|
topK: limit,
|
|
119
|
-
useRerank: args.rerank !== false
|
|
174
|
+
useRerank: args.rerank !== false,
|
|
175
|
+
...(scope ? { scope } : {}),
|
|
176
|
+
...(occurredFrom !== null || occurredTo !== null ? { occurredFrom, occurredTo } : {})
|
|
120
177
|
});
|
|
121
178
|
return { items: service.toApiList(rows) };
|
|
122
179
|
}
|
|
@@ -129,7 +186,9 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
129
186
|
type: { type: "string", enum: ["preference", "project", "decision", "history", "rejected_solution", "pitfall", "constraint"], description: "Filter by type; omit for all" },
|
|
130
187
|
limit: { type: "integer", description: "Page size (default 50)" },
|
|
131
188
|
offset: { type: "integer", description: "Page offset (default 0)" },
|
|
132
|
-
include_archived: { type: "boolean", description: "Include archived (hidden) entries so they can be found and restored (default false)" }
|
|
189
|
+
include_archived: { type: "boolean", description: "Include archived (hidden) entries so they can be found and restored (default false)" },
|
|
190
|
+
occurred_from: { type: "string", description: "Optional ISO date/timestamp lower bound on when the remembered event happened (occurred_at, falling back to created_at). Date-only values are inclusive from that day's 00:00Z." },
|
|
191
|
+
occurred_to: { type: "string", description: "Optional ISO date/timestamp upper bound on occurred_at. Date-only values are inclusive through that day's 23:59:59.999Z." }
|
|
133
192
|
},
|
|
134
193
|
output: {
|
|
135
194
|
schema: {
|
|
@@ -147,7 +206,7 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
147
206
|
const items = value.items ?? [];
|
|
148
207
|
if (items.length === 0) return TEXT_OUTPUT(`0 memory entries (of ${value.total}).`);
|
|
149
208
|
const body = items
|
|
150
|
-
.map((m, i) => `[${i + 1}] ${m.title} (type=${m.type}, importance=${m.importance})\n ID: ${m.id} | updated: ${m.updated_at}`)
|
|
209
|
+
.map((m, i) => `[${i + 1}] ${m.title} (type=${m.type}, importance=${m.importance})\n ID: ${m.id} | updated: ${m.updated_at}${MEMORY_PROVENANCE(m)}`)
|
|
151
210
|
.join("\n\n");
|
|
152
211
|
return TEXT_OUTPUT(`${items.length} memory entries (of ${value.total}):\n\n${body}`);
|
|
153
212
|
}
|
|
@@ -158,9 +217,13 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
158
217
|
type: args.type,
|
|
159
218
|
limit: args.limit ?? 50,
|
|
160
219
|
offset: args.offset ?? 0,
|
|
161
|
-
includeArchived
|
|
220
|
+
includeArchived,
|
|
221
|
+
...occurredWindow(args),
|
|
222
|
+
// A3:strict 开启时按会话 scope 硬过滤(宿主按 (args, runContext) 调用,
|
|
223
|
+
// 从 arguments 取第二参)。
|
|
224
|
+
...strictVisibility(config, resolveSessionScope(arguments[1]))
|
|
162
225
|
}));
|
|
163
|
-
return { items: rows, total: service.count(args.type, { includeArchived }) };
|
|
226
|
+
return { items: rows, total: service.count(args.type, { includeArchived, ...occurredWindow(args), ...strictVisibility(config, resolveSessionScope(arguments[1])) }) };
|
|
164
227
|
}
|
|
165
228
|
}),
|
|
166
229
|
|
|
@@ -186,6 +249,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
186
249
|
tags: { type: "array", items: { type: "string" } },
|
|
187
250
|
content: { type: "string", required: true },
|
|
188
251
|
source: { type: "string" },
|
|
252
|
+
agent_scope: { type: "string" },
|
|
253
|
+
workspace_scope: { type: "string" },
|
|
254
|
+
sensitivity: { type: "string" },
|
|
255
|
+
occurred_at: { type: "string" },
|
|
189
256
|
created_at: { type: "string" },
|
|
190
257
|
updated_at: { type: "string" }
|
|
191
258
|
}
|
|
@@ -194,12 +261,19 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
194
261
|
},
|
|
195
262
|
render: (_args, value) => {
|
|
196
263
|
const m = value.memory;
|
|
197
|
-
return TEXT_OUTPUT(`${m.title}\nID: ${m.id} | type: ${m.type} | importance: ${m.importance}\n\n${m.content}`);
|
|
264
|
+
return TEXT_OUTPUT(`${m.title}\nID: ${m.id} | type: ${m.type} | importance: ${m.importance}${MEMORY_PROVENANCE(m)}\n\n${m.content}`);
|
|
198
265
|
}
|
|
199
266
|
},
|
|
200
267
|
async execute(args) {
|
|
201
268
|
const memory = service.getById(args.id);
|
|
202
269
|
if (memory === undefined) throw new Error("memory not found");
|
|
270
|
+
// v0.8.0 A3:strictScope 下他 scope 的行按不存在处理(无存在性泄漏)。
|
|
271
|
+
// 宿主注册表按 (args, runContext) 两参调用处理器,从 arguments 取第二参。
|
|
272
|
+
const runContext = arguments[1];
|
|
273
|
+
const scope = resolveSessionScope(runContext);
|
|
274
|
+
if (config.strictScope === true && scope && !service.isVisibleInScope(memory, scope)) {
|
|
275
|
+
throw new Error("memory not found");
|
|
276
|
+
}
|
|
203
277
|
return { memory: service.toApiList([memory])[0] };
|
|
204
278
|
}
|
|
205
279
|
}),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/api.js
CHANGED
|
@@ -1014,6 +1014,66 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1014
1014
|
}
|
|
1015
1015
|
});
|
|
1016
1016
|
|
|
1017
|
+
// --- 冲突集中处理(v0.8.0):队列 + 人工确认 --------------------------------
|
|
1018
|
+
// GET /conflicts:未解决冲突 + 双方当前内容(此前面板只有计数与徽章,
|
|
1019
|
+
// resolveConflictPending 在存储层躺着没有任何调用方)。POST /conflicts/resolve:
|
|
1020
|
+
// {id, winner: "a"|"b"|null, apply}——盖章审计,apply 时按 dream 非冻结
|
|
1021
|
+
// conflict 路径同款处置(service.resolveConflictPending 内部复用
|
|
1022
|
+
// applyDecisions:CAS + 事务 + 注记 + 幂等)。认证与其他写路由一致。
|
|
1023
|
+
register({
|
|
1024
|
+
kind: "exact",
|
|
1025
|
+
path: "/api/dsh-mneme/conflicts",
|
|
1026
|
+
handler(req, res) {
|
|
1027
|
+
try {
|
|
1028
|
+
if (req.method !== "GET") {
|
|
1029
|
+
sendJson(res, 404, { error: "not-found" });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
1033
|
+
const url = new URL(req.url, "http://localhost");
|
|
1034
|
+
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") ?? 50) || 50));
|
|
1035
|
+
const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0) || 0);
|
|
1036
|
+
const items = service.listConflictQueue({ limit, offset });
|
|
1037
|
+
sendJson(res, 200, { items, total: service.countConflictPending?.() ?? items.length });
|
|
1038
|
+
} catch {
|
|
1039
|
+
sendJson(res, 500, { error: "internal" });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
register({
|
|
1045
|
+
kind: "exact",
|
|
1046
|
+
path: "/api/dsh-mneme/conflicts/resolve",
|
|
1047
|
+
handler(req, res) {
|
|
1048
|
+
try {
|
|
1049
|
+
if (req.method !== "POST") {
|
|
1050
|
+
sendJson(res, 404, { error: "not-found" });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
1054
|
+
return readBody(req).then((text) => {
|
|
1055
|
+
let body = {};
|
|
1056
|
+
try { body = JSON.parse(text || "{}"); } catch { /* 坏 JSON → 走缺参分支 */ }
|
|
1057
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
1058
|
+
if (!id) {
|
|
1059
|
+
sendJson(res, 400, { error: "missing-id" });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const winner = body.winner === "a" || body.winner === "b" ? body.winner : null;
|
|
1063
|
+
const apply = body.apply !== false;
|
|
1064
|
+
const result = service.resolveConflictPending(id, { winner, apply });
|
|
1065
|
+
if (!result) {
|
|
1066
|
+
sendJson(res, 404, { error: "not-found" });
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
sendJson(res, 200, { ok: true, conflict: result });
|
|
1070
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1071
|
+
} catch {
|
|
1072
|
+
sendJson(res, 500, { error: "internal" });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1017
1077
|
// --- health: mirror sync state (F-NEW-03 / v0.3.6) ---
|
|
1018
1078
|
// Auth-gated; only returns a sanitized error code (never raw last_error which
|
|
1019
1079
|
// may leak paths/token-like strings/internal hosts). On state read failure it
|
package/src/commands.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
1
2
|
import { STR } from "./lang.js";
|
|
2
3
|
|
|
3
4
|
// Custom slash-command manager: keeps the DSH command registry in sync with
|
|
4
5
|
// user-defined commands persisted in SQLite. Commands are registered on boot
|
|
5
6
|
// and (re)registered on add/remove through the API.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
8
|
+
// Per the DSH command contract, a handler's CommandResult text is only a UI
|
|
9
|
+
// settlement notice and never reaches the model. To actually drive the agent,
|
|
10
|
+
// the handler submits the user-authored instruction itself as a user followup
|
|
11
|
+
// message on the receiving agent (same pattern as the official /goal command).
|
|
12
|
+
// The notice just acknowledges the submission; an optional rawInput suffix
|
|
13
|
+
// typed after the command is appended for the model.
|
|
9
14
|
|
|
10
15
|
export function createCommandManager({ ctx, settings, logger, language = "zh" }) {
|
|
11
16
|
const registered = new Map(); // name -> disposer
|
|
@@ -18,7 +23,8 @@ export function createCommandManager({ ctx, settings, logger, language = "zh" })
|
|
|
18
23
|
name: command.name,
|
|
19
24
|
// 缺省描述随实例语言(命令描述进命令注册表,对 LLM 可见)。
|
|
20
25
|
description: command.description || STR.commandFallbackDesc[language](command.name),
|
|
21
|
-
|
|
26
|
+
input: { hint: STR.commandInputHint[language] },
|
|
27
|
+
handler: (invocation) => submit(command, invocation)
|
|
22
28
|
});
|
|
23
29
|
} catch (error) {
|
|
24
30
|
logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
|
|
@@ -27,6 +33,27 @@ export function createCommandManager({ ctx, settings, logger, language = "zh" })
|
|
|
27
33
|
registered.set(command.name, dispose);
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/** Submit the instruction (plus optional rawInput suffix) to the receiving agent. */
|
|
37
|
+
function submit(command, invocation) {
|
|
38
|
+
const suffix = typeof invocation?.rawInput === "string" ? invocation.rawInput.trim() : "";
|
|
39
|
+
const text = suffix ? `${command.instruction}\n\n${suffix}` : command.instruction;
|
|
40
|
+
const agent = invocation?.agent;
|
|
41
|
+
if (typeof agent?.followup !== "function") {
|
|
42
|
+
// 兜底:宿主未提供接收 agent 时退回旧行为,至少把指令展示在 notice 里。
|
|
43
|
+
return { kind: "success", text };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
agent.followup(createUserMessage({
|
|
47
|
+
content: [{ type: "text", text }],
|
|
48
|
+
source: { kind: "user" }
|
|
49
|
+
}));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger?.warn?.(`dsh-mneme: failed to submit /${command.name} to agent: ${String(error)}`);
|
|
52
|
+
return { kind: "error", text: STR.commandSubmitFailed[language](command.name) };
|
|
53
|
+
}
|
|
54
|
+
return { kind: "success", text: STR.commandSubmitted[language](command.name) };
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
function unregisterOne(name) {
|
|
31
58
|
const dispose = registered.get(name);
|
|
32
59
|
if (dispose) {
|
package/src/config.js
CHANGED
|
@@ -58,6 +58,11 @@ export const Config = z.object({
|
|
|
58
58
|
// 编码记忆注入加权系数:编码任务时对 rejected_solution/pitfall/constraint
|
|
59
59
|
// 记忆的 importance 乘以该系数排序,让编码记忆在编码场景更靠前。
|
|
60
60
|
codingBoostFactor: z.number().min(1).max(5).default(2),
|
|
61
|
+
// 注入边界花括号转义(issue #162,v0.7.4 曾修后被 v0.7.11 重构误删):默认开启,
|
|
62
|
+
// 在 memory / 短期上下文 / 用户设置三处注入出口对连续 2+ 个花括号插 `\`
|
|
63
|
+
// (`{{a}}`→`{\{a\}\}`),让 DSH 的 interpolate() 不再扫到非法变量名而 throw。
|
|
64
|
+
// 关闭时按原样透传。
|
|
65
|
+
escapePromptVariables: z.boolean().default(true),
|
|
61
66
|
autoDream: z.boolean().default(true),
|
|
62
67
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
63
68
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -438,6 +443,22 @@ export const Config = z.object({
|
|
|
438
443
|
recallRecordDefault: z.boolean().default(true),
|
|
439
444
|
// recall_runs 滚动清理保留天数。
|
|
440
445
|
recallRetentionDays: z.natural().min(1).max(3650).default(90),
|
|
446
|
+
|
|
447
|
+
// --- scope: v0.8.0 A1 存储层(issue #17)--------------------------------
|
|
448
|
+
// 总开关默认关:关闭时写入不标注 scope、去重维持 (type, title) 现状,行为
|
|
449
|
+
// 逐字节不变。开启后 memory_save 写入 agent_scope(session header 的
|
|
450
|
+
// agentPreset)与 workspace_scope(registry 反查 canonical path,header.cwd
|
|
451
|
+
// 兜底,取不到 NULL——解析绝不阻塞写入),去重键扩展为 (type, title,
|
|
452
|
+
// agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
|
|
453
|
+
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
454
|
+
scopeEnabled: z.boolean().default(false),
|
|
455
|
+
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见);
|
|
456
|
+
// 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
|
|
457
|
+
// 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
|
|
458
|
+
// 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
|
|
459
|
+
// 打开才有意义(没有写入标注就无可过滤维度),但独立成键:可先开标注积累
|
|
460
|
+
// 数据、观察 A2 加权质量后再切硬过滤。
|
|
461
|
+
strictScope: z.boolean().default(false),
|
|
441
462
|
});
|
|
442
463
|
|
|
443
464
|
// Fields forced to false by the light-mode preset. Everything not listed here
|